• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 19488739555

19 Nov 2025 03:27AM UTC coverage: 74.33% (+0.009%) from 74.321%
19488739555

push

github

web-flow
fix: align lookup grouping sort and toolbar hydration (#2147)

11449 of 12403 branches covered (92.31%)

55 of 66 new or added lines in 2 files covered. (83.33%)

1508 existing lines in 34 files now uncovered.

55091 of 74117 relevant lines covered (74.33%)

4337.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

38.73
/apps/nestjs-backend/src/db-provider/generated-column-query/sqlite/generated-column-query.sqlite.ts
1
/* eslint-disable sonarjs/no-identical-functions */
2✔
2
import { GeneratedColumnQueryAbstract } from '../generated-column-query.abstract';
3

4
/**
2✔
5
 * SQLite-specific implementation of generated column query functions
6
 * Converts Teable formula functions to SQLite SQL expressions suitable
7
 * for use in generated columns. All generated SQL must be immutable.
8
 */
2✔
9
export class GeneratedColumnQuerySqlite extends GeneratedColumnQueryAbstract {
2✔
10
  private isEmptyStringLiteral(value: string): boolean {
2✔
11
    return value.trim() === "''";
×
12
  }
×
13

14
  private normalizeBlankComparable(value: string): string {
2✔
15
    return `COALESCE(NULLIF(CAST((${value}) AS TEXT), ''), '')`;
×
16
  }
×
17

18
  private buildBlankAwareComparison(operator: '=' | '<>', left: string, right: string): string {
2✔
19
    const shouldNormalize = this.isEmptyStringLiteral(left) || this.isEmptyStringLiteral(right);
×
20
    if (!shouldNormalize) {
×
21
      return `(${left} ${operator} ${right})`;
×
22
    }
×
23

24
    const normalizedLeft = this.isEmptyStringLiteral(left)
×
25
      ? "''"
×
26
      : this.normalizeBlankComparable(left);
×
27
    const normalizedRight = this.isEmptyStringLiteral(right)
×
28
      ? "''"
×
29
      : this.normalizeBlankComparable(right);
×
30

31
    return `(${normalizedLeft} ${operator} ${normalizedRight})`;
×
32
  }
×
33

34
  // Numeric Functions
2✔
35
  sum(params: string[]): string {
2✔
36
    if (params.length === 0) {
×
37
      return 'NULL';
×
38
    }
×
39
    if (params.length === 1) {
×
40
      return `${params[0]}`;
×
41
    }
×
42
    // SQLite doesn't have SUM() for multiple values, use addition
×
43
    return `(${this.joinParams(params, ' + ')})`;
×
44
  }
×
45

46
  average(params: string[]): string {
2✔
47
    if (params.length === 0) {
×
48
      return 'NULL';
×
49
    }
×
50
    if (params.length === 1) {
×
51
      return `${params[0]}`;
×
52
    }
×
53
    // Calculate average as sum divided by count
×
54
    return `((${this.joinParams(params, ' + ')}) / ${params.length})`;
×
55
  }
×
56

57
  max(params: string[]): string {
2✔
58
    if (params.length === 0) {
×
59
      return 'NULL';
×
60
    }
×
61
    if (params.length === 1) {
×
62
      return `${params[0]}`;
×
63
    }
×
64
    // Use nested MAX functions for multiple values
×
65
    return params.reduce((acc, param) => `MAX(${acc}, ${param})`);
×
66
  }
×
67

68
  min(params: string[]): string {
2✔
69
    if (params.length === 0) {
×
70
      return 'NULL';
×
71
    }
×
72
    if (params.length === 1) {
×
73
      return `${params[0]}`;
×
74
    }
×
75
    // Use nested MIN functions for multiple values
×
76
    return params.reduce((acc, param) => `MIN(${acc}, ${param})`);
×
77
  }
×
78

79
  round(value: string, precision?: string): string {
2✔
80
    if (precision) {
×
81
      return `ROUND(${value}, ${precision})`;
×
82
    }
×
83
    return `ROUND(${value})`;
×
84
  }
×
85

86
  roundUp(value: string, precision?: string): string {
2✔
87
    if (precision) {
×
88
      // Use manual power calculation for 10^precision (common cases)
×
89
      const factor = `(
×
90
        CASE
91
          WHEN ${precision} = 0 THEN 1
×
92
          WHEN ${precision} = 1 THEN 10
×
93
          WHEN ${precision} = 2 THEN 100
×
94
          WHEN ${precision} = 3 THEN 1000
×
95
          WHEN ${precision} = 4 THEN 10000
×
96
          ELSE 1
97
        END
98
      )`;
×
99
      return `CAST(CEIL(${value} * ${factor}) / ${factor} AS REAL)`;
×
100
    }
×
101
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
102
  }
×
103

104
  roundDown(value: string, precision?: string): string {
2✔
105
    if (precision) {
×
106
      // Use manual power calculation for 10^precision (common cases)
×
107
      const factor = `(
×
108
        CASE
109
          WHEN ${precision} = 0 THEN 1
×
110
          WHEN ${precision} = 1 THEN 10
×
111
          WHEN ${precision} = 2 THEN 100
×
112
          WHEN ${precision} = 3 THEN 1000
×
113
          WHEN ${precision} = 4 THEN 10000
×
114
          ELSE 1
115
        END
116
      )`;
×
117
      return `CAST(FLOOR(${value} * ${factor}) / ${factor} AS REAL)`;
×
118
    }
×
119
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
120
  }
×
121

122
  ceiling(value: string): string {
2✔
123
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
124
  }
×
125

126
  floor(value: string): string {
2✔
127
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
128
  }
×
129

130
  even(value: string): string {
2✔
131
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 0 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
132
  }
×
133

134
  odd(value: string): string {
2✔
135
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 1 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
136
  }
×
137

138
  int(value: string): string {
2✔
139
    return `CAST(${value} AS INTEGER)`;
×
140
  }
×
141

142
  abs(value: string): string {
2✔
143
    return `ABS(${value})`;
×
144
  }
×
145

146
  sqrt(value: string): string {
2✔
147
    // SQLite doesn't have SQRT function, use Newton's method approximation
×
148
    // One iteration of Newton's method: (x/2 + x/(x/2)) / 2
×
149
    return `(
×
150
      CASE
151
        WHEN ${value} <= 0 THEN 0
×
152
        ELSE (${value} / 2.0 + ${value} / (${value} / 2.0)) / 2.0
×
153
      END
154
    )`;
×
155
  }
×
156

157
  power(base: string, exponent: string): string {
2✔
158
    // SQLite doesn't have POWER function, implement for common cases
×
159
    return `(
×
160
      CASE
161
        WHEN ${exponent} = 0 THEN 1
×
162
        WHEN ${exponent} = 1 THEN ${base}
×
163
        WHEN ${exponent} = 2 THEN ${base} * ${base}
×
164
        WHEN ${exponent} = 3 THEN ${base} * ${base} * ${base}
×
165
        WHEN ${exponent} = 4 THEN ${base} * ${base} * ${base} * ${base}
×
166
        WHEN ${exponent} = 0.5 THEN
×
167
          -- Square root case using Newton's method
168
          CASE
169
            WHEN ${base} <= 0 THEN 0
×
170
            ELSE (${base} / 2.0 + ${base} / (${base} / 2.0)) / 2.0
×
171
          END
172
        ELSE 1
173
      END
174
    )`;
×
175
  }
×
176

177
  exp(value: string): string {
2✔
178
    return `EXP(${value})`;
×
179
  }
×
180

181
  log(value: string, base?: string): string {
2✔
182
    if (base) {
×
183
      return `(LOG(${value}) / LOG(${base}))`;
×
184
    }
×
185
    // SQLite LOG is base 10, but formula LOG should be natural log (base e)
×
186
    return `LN(${value})`;
×
187
  }
×
188

189
  mod(dividend: string, divisor: string): string {
2✔
190
    return `(${dividend} % ${divisor})`;
×
191
  }
×
192

193
  value(text: string): string {
2✔
194
    return `CAST(${text} AS REAL)`;
×
195
  }
×
196

197
  // Text Functions
2✔
198
  concatenate(params: string[]): string {
2✔
199
    // Handle NULL values by converting them to empty strings for CONCATENATE function
×
200
    // This mirrors the behavior of the formula evaluation engine
×
201
    const nullSafeParams = params.map((param) => `COALESCE(${param}, '')`);
×
202
    return `(${this.joinParams(nullSafeParams, ' || ')})`;
×
203
  }
×
204

205
  // String concatenation for + operator (treats NULL as empty string)
2✔
206
  stringConcat(left: string, right: string): string {
2✔
207
    return `(COALESCE(${left}, '') || COALESCE(${right}, ''))`;
×
208
  }
×
209

210
  equal(left: string, right: string): string {
2✔
211
    return this.buildBlankAwareComparison('=', left, right);
×
212
  }
×
213

214
  notEqual(left: string, right: string): string {
2✔
215
    return this.buildBlankAwareComparison('<>', left, right);
×
216
  }
×
217

218
  find(searchText: string, withinText: string, startNum?: string): string {
2✔
219
    if (startNum) {
×
220
      return `CASE WHEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) > 0 THEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) + ${startNum} - 1 ELSE 0 END`;
×
221
    }
×
222
    return `INSTR(${withinText}, ${searchText})`;
×
223
  }
×
224

225
  search(searchText: string, withinText: string, startNum?: string): string {
2✔
226
    // SQLite INSTR is case-sensitive, so we use UPPER for case-insensitive search
×
227
    if (startNum) {
×
228
      return `CASE WHEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) > 0 THEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) + ${startNum} - 1 ELSE 0 END`;
×
229
    }
×
230
    return `INSTR(UPPER(${withinText}), UPPER(${searchText}))`;
×
231
  }
×
232

233
  mid(text: string, startNum: string, numChars: string): string {
2✔
234
    return `SUBSTR(${text}, ${startNum}, ${numChars})`;
×
235
  }
×
236

237
  left(text: string, numChars: string): string {
2✔
238
    return `SUBSTR(${text}, 1, ${numChars})`;
×
239
  }
×
240

241
  right(text: string, numChars: string): string {
2✔
242
    return `SUBSTR(${text}, -${numChars})`;
×
243
  }
×
244

245
  replace(oldText: string, startNum: string, numChars: string, newText: string): string {
2✔
246
    return `SUBSTR(${oldText}, 1, ${startNum} - 1) || ${newText} || SUBSTR(${oldText}, ${startNum} + ${numChars})`;
×
247
  }
×
248

249
  regexpReplace(text: string, pattern: string, replacement: string): string {
2✔
250
    // SQLite doesn't have built-in regex replace, would need extension
×
251
    return `REPLACE(${text}, ${pattern}, ${replacement})`;
×
252
  }
×
253

254
  substitute(text: string, oldText: string, newText: string, instanceNum?: string): string {
2✔
255
    // SQLite REPLACE replaces all instances, no direct support for specific instance
×
256
    return `REPLACE(${text}, ${oldText}, ${newText})`;
×
257
  }
×
258

259
  lower(text: string): string {
2✔
260
    return `LOWER(${text})`;
×
261
  }
×
262

263
  upper(text: string): string {
2✔
264
    return `UPPER(${text})`;
×
265
  }
×
266

267
  rept(text: string, numTimes: string): string {
2✔
268
    // SQLite doesn't have REPEAT function, need to use recursive CTE or custom function
×
269
    return `REPLACE(HEX(ZEROBLOB(${numTimes})), '00', ${text})`;
×
270
  }
×
271

272
  trim(text: string): string {
2✔
273
    return `TRIM(${text})`;
×
274
  }
×
275

276
  len(text: string): string {
2✔
277
    return `LENGTH(${text})`;
×
278
  }
×
279

280
  t(value: string): string {
2✔
281
    return `CASE
×
282
      WHEN ${value} IS NULL THEN ''
×
283
      WHEN ${value} = CAST(${value} AS INTEGER) THEN CAST(${value} AS INTEGER)
×
284
      ELSE CAST(${value} AS TEXT)
×
285
    END`;
×
286
  }
×
287

288
  encodeUrlComponent(text: string): string {
2✔
289
    // SQLite doesn't have built-in URL encoding
×
290
    return `${text}`;
×
291
  }
×
292

293
  // DateTime Functions
2✔
294
  now(): string {
2✔
295
    // For generated columns, use the current timestamp at field creation time
×
296
    if (this.isGeneratedColumnContext) {
×
297
      const currentTimestamp = new Date()
×
298
        .toISOString()
×
299
        .replace('T', ' ')
×
300
        .replace('Z', '')
×
301
        .replace(/\.\d{3}$/, '');
×
302
      return `'${currentTimestamp}'`;
×
303
    }
×
304
    return "DATETIME('now')";
×
305
  }
×
306

307
  today(): string {
2✔
308
    // For generated columns, use the current date at field creation time
×
309
    if (this.isGeneratedColumnContext) {
×
310
      const currentDate = new Date().toISOString().split('T')[0];
×
311
      return `'${currentDate}'`;
×
312
    }
×
313
    return "DATE('now')";
×
314
  }
×
315

316
  private normalizeDateModifier(unitLiteral: string): {
2✔
317
    unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
318
    factor: number;
319
  } {
25✔
320
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
25✔
321
    switch (normalized) {
25✔
322
      case 'millisecond':
25✔
323
      case 'milliseconds':
25✔
324
      case 'ms':
25✔
325
        return { unit: 'seconds', factor: 0.001 };
3✔
326
      case 'second':
25✔
327
      case 'seconds':
25✔
328
      case 'sec':
25✔
329
      case 'secs':
25✔
330
        return { unit: 'seconds', factor: 1 };
4✔
331
      case 'minute':
25✔
332
      case 'minutes':
25✔
333
      case 'min':
25✔
334
      case 'mins':
25✔
335
        return { unit: 'minutes', factor: 1 };
4✔
336
      case 'hour':
25✔
337
      case 'hours':
25✔
338
      case 'hr':
25✔
339
      case 'hrs':
25✔
340
        return { unit: 'hours', factor: 1 };
4✔
341
      case 'week':
25✔
342
      case 'weeks':
25✔
343
        return { unit: 'days', factor: 7 };
2✔
344
      case 'month':
25✔
345
      case 'months':
25✔
346
        return { unit: 'months', factor: 1 };
2✔
347
      case 'quarter':
25✔
348
      case 'quarters':
25✔
349
        return { unit: 'months', factor: 3 };
2✔
350
      case 'year':
25✔
351
      case 'years':
25✔
352
        return { unit: 'years', factor: 1 };
2✔
353
      case 'day':
25✔
354
      case 'days':
25✔
355
      default:
25✔
356
        return { unit: 'days', factor: 1 };
2✔
357
    }
25✔
358
  }
25✔
359

360
  private normalizeDiffUnit(
2✔
361
    unitLiteral: string
19✔
362
  ): 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' {
19✔
363
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
19✔
364
    switch (normalized) {
19✔
365
      case 'millisecond':
19✔
366
      case 'milliseconds':
19✔
367
      case 'ms':
19✔
368
        return 'millisecond';
3✔
369
      case 'second':
19✔
370
      case 'seconds':
19✔
371
      case 'sec':
19✔
372
      case 'secs':
19✔
373
        return 'second';
4✔
374
      case 'minute':
19✔
375
      case 'minutes':
19✔
376
      case 'min':
19✔
377
      case 'mins':
19✔
378
        return 'minute';
4✔
379
      case 'hour':
19✔
380
      case 'hours':
19✔
381
      case 'hr':
19✔
382
      case 'hrs':
19✔
383
        return 'hour';
4✔
384
      case 'week':
19✔
385
      case 'weeks':
19✔
386
        return 'week';
2✔
387
      case 'month':
19!
388
      case 'months':
19!
UNCOV
389
        return 'month';
×
390
      case 'quarter':
19!
391
      case 'quarters':
19!
UNCOV
392
        return 'quarter';
×
393
      case 'year':
19!
394
      case 'years':
19!
UNCOV
395
        return 'year';
×
396
      default:
19✔
397
        return 'day';
2✔
398
    }
19✔
399
  }
19✔
400

401
  private normalizeTruncateFormat(unitLiteral: string): string {
2✔
402
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
23✔
403
    switch (normalized) {
23✔
404
      case 'millisecond':
23✔
405
      case 'milliseconds':
23✔
406
      case 'ms':
23✔
407
      case 'second':
23✔
408
      case 'seconds':
23✔
409
      case 'sec':
23✔
410
      case 'secs':
23✔
411
        return '%Y-%m-%d %H:%M:%S';
7✔
412
      case 'minute':
23✔
413
      case 'minutes':
23✔
414
      case 'min':
23✔
415
      case 'mins':
23✔
416
        return '%Y-%m-%d %H:%M';
4✔
417
      case 'hour':
23✔
418
      case 'hours':
23✔
419
      case 'hr':
23✔
420
      case 'hrs':
23✔
421
        return '%Y-%m-%d %H';
4✔
422
      case 'week':
23✔
423
      case 'weeks':
23✔
424
        return '%Y-%W';
2✔
425
      case 'month':
23✔
426
      case 'months':
23✔
427
        return '%Y-%m';
2✔
428
      case 'year':
23✔
429
      case 'years':
23✔
430
        return '%Y';
2✔
431
      case 'day':
23✔
432
      case 'days':
23✔
433
      default:
23✔
434
        return '%Y-%m-%d';
2✔
435
    }
23✔
436
  }
23✔
437

438
  dateAdd(date: string, count: string, unit: string): string {
2✔
439
    const { unit: cleanUnit, factor } = this.normalizeDateModifier(unit);
25✔
440
    const scaledCount = factor === 1 ? `(${count})` : `(${count}) * ${factor}`;
25✔
441
    return `DATETIME(${date}, (${scaledCount}) || ' ${cleanUnit}')`;
25✔
442
  }
25✔
443

444
  datestr(date: string): string {
2✔
UNCOV
445
    return `DATE(${date})`;
×
UNCOV
446
  }
×
447

448
  private buildMonthDiff(startDate: string, endDate: string): string {
2✔
UNCOV
449
    const startYear = `CAST(STRFTIME('%Y', ${startDate}) AS INTEGER)`;
×
UNCOV
450
    const endYear = `CAST(STRFTIME('%Y', ${endDate}) AS INTEGER)`;
×
UNCOV
451
    const startMonth = `CAST(STRFTIME('%m', ${startDate}) AS INTEGER)`;
×
UNCOV
452
    const endMonth = `CAST(STRFTIME('%m', ${endDate}) AS INTEGER)`;
×
UNCOV
453
    const startDay = `CAST(STRFTIME('%d', ${startDate}) AS INTEGER)`;
×
UNCOV
454
    const endDay = `CAST(STRFTIME('%d', ${endDate}) AS INTEGER)`;
×
UNCOV
455
    const startLastDay = `CAST(STRFTIME('%d', DATE(${startDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
UNCOV
456
    const endLastDay = `CAST(STRFTIME('%d', DATE(${endDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
457

UNCOV
458
    const baseMonths = `((${startYear} - ${endYear}) * 12 + (${startMonth} - ${endMonth}))`;
×
459
    const adjustDown = `(CASE WHEN ${baseMonths} > 0 AND ${startDay} < ${endDay} AND ${startDay} < ${startLastDay} THEN 1 ELSE 0 END)`;
×
460
    const adjustUp = `(CASE WHEN ${baseMonths} < 0 AND ${startDay} > ${endDay} AND ${endDay} < ${endLastDay} THEN 1 ELSE 0 END)`;
×
461

462
    return `(${baseMonths} - ${adjustDown} + ${adjustUp})`;
×
463
  }
×
464

465
  datetimeDiff(startDate: string, endDate: string, unit: string): string {
2✔
466
    const baseDiffDays = `(JULIANDAY(${startDate}) - JULIANDAY(${endDate}))`;
19✔
467
    switch (this.normalizeDiffUnit(unit)) {
19✔
468
      case 'millisecond':
19✔
469
        return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;
3✔
470
      case 'second':
19✔
471
        return `(${baseDiffDays}) * 24.0 * 60 * 60`;
4✔
472
      case 'minute':
19✔
473
        return `(${baseDiffDays}) * 24.0 * 60`;
4✔
474
      case 'hour':
19✔
475
        return `(${baseDiffDays}) * 24.0`;
4✔
476
      case 'week':
19✔
477
        return `(${baseDiffDays}) / 7.0`;
2✔
478
      case 'month':
19!
479
        return this.buildMonthDiff(startDate, endDate);
×
480
      case 'quarter':
19!
UNCOV
481
        return `${this.buildMonthDiff(startDate, endDate)} / 3.0`;
×
482
      case 'year': {
19!
483
        const monthDiff = this.buildMonthDiff(startDate, endDate);
×
484
        return `CAST((${monthDiff}) / 12.0 AS INTEGER)`;
×
485
      }
×
486
      case 'day':
19✔
487
      default:
19✔
488
        return `${baseDiffDays}`;
2✔
489
    }
19✔
490
  }
19✔
491

492
  datetimeFormat(date: string, format: string): string {
2✔
UNCOV
493
    // Convert common format patterns to SQLite STRFTIME format
×
UNCOV
494
    const cleanFormat = format.replace(/^'|'$/g, '');
×
495
    const sqliteFormat = cleanFormat
×
496
      .replace(/YYYY/g, '%Y')
×
UNCOV
497
      .replace(/MM/g, '%m')
×
UNCOV
498
      .replace(/DD/g, '%d')
×
499
      .replace(/HH/g, '%H')
×
500
      .replace(/mm/g, '%M')
×
UNCOV
501
      .replace(/ss/g, '%S');
×
502

UNCOV
503
    return `STRFTIME('${sqliteFormat}', ${date})`;
×
UNCOV
504
  }
×
505

506
  datetimeParse(dateString: string, _format?: string): string {
2✔
UNCOV
507
    // SQLite doesn't have direct parsing with custom format
×
UNCOV
508
    return `DATETIME(${dateString})`;
×
509
  }
×
510

511
  day(date: string): string {
2✔
512
    return `CAST(STRFTIME('%d', ${date}) AS INTEGER)`;
×
513
  }
×
514

515
  fromNow(date: string): string {
2✔
516
    // For generated columns, use the current timestamp at field creation time
×
517
    if (this.isGeneratedColumnContext) {
×
UNCOV
518
      const currentTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '');
×
UNCOV
519
      return `(JULIANDAY('${currentTimestamp}') - JULIANDAY(${date})) * 24 * 60 * 60`;
×
520
    }
×
521
    return `(JULIANDAY('now') - JULIANDAY(${date})) * 24 * 60 * 60`;
×
UNCOV
522
  }
×
523

524
  hour(date: string): string {
2✔
525
    return `CAST(STRFTIME('%H', ${date}) AS INTEGER)`;
×
UNCOV
526
  }
×
527

528
  isAfter(date1: string, date2: string): string {
2✔
529
    return `DATETIME(${date1}) > DATETIME(${date2})`;
×
UNCOV
530
  }
×
531

532
  isBefore(date1: string, date2: string): string {
2✔
533
    return `DATETIME(${date1}) < DATETIME(${date2})`;
×
UNCOV
534
  }
×
535

536
  isSame(date1: string, date2: string, unit?: string): string {
2✔
537
    if (unit) {
23✔
538
      const trimmed = unit.trim();
23✔
539
      if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
23✔
540
        const format = this.normalizeTruncateFormat(trimmed.slice(1, -1));
23✔
541
        return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
23✔
542
      }
23!
UNCOV
543
      const format = this.normalizeTruncateFormat(unit);
×
UNCOV
544
      return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
×
545
    }
×
546
    return `DATETIME(${date1}) = DATETIME(${date2})`;
×
UNCOV
547
  }
×
548

549
  lastModifiedTime(): string {
2✔
550
    return '__last_modified_time';
×
551
  }
×
552

553
  minute(date: string): string {
2✔
554
    return `CAST(STRFTIME('%M', ${date}) AS INTEGER)`;
×
555
  }
×
556

557
  month(date: string): string {
2✔
558
    return `CAST(STRFTIME('%m', ${date}) AS INTEGER)`;
×
559
  }
×
560

561
  second(date: string): string {
2✔
562
    return `CAST(STRFTIME('%S', ${date}) AS INTEGER)`;
×
563
  }
×
564

565
  timestr(date: string): string {
2✔
566
    return `TIME(${date})`;
×
567
  }
×
568

569
  toNow(date: string): string {
2✔
570
    // For generated columns, use the current timestamp at field creation time
×
571
    if (this.isGeneratedColumnContext) {
×
572
      const currentTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '');
×
573
      return `(JULIANDAY(${date}) - JULIANDAY('${currentTimestamp}')) * 24 * 60 * 60`;
×
574
    }
×
575
    return `(JULIANDAY(${date}) - JULIANDAY('now')) * 24 * 60 * 60`;
×
576
  }
×
577

578
  weekNum(date: string): string {
2✔
UNCOV
579
    return `CAST(STRFTIME('%W', ${date}) AS INTEGER)`;
×
UNCOV
580
  }
×
581

582
  weekday(date: string): string {
2✔
583
    // Convert SQLite's 0-based weekday (0=Sunday) to 1-based (1=Sunday)
×
584
    return `(CAST(STRFTIME('%w', ${date}) AS INTEGER) + 1)`;
×
UNCOV
585
  }
×
586

587
  workday(startDate: string, days: string): string {
2✔
588
    return `DATE(${startDate}, '+' || ${days} || ' days')`;
×
UNCOV
589
  }
×
590

591
  workdayDiff(startDate: string, endDate: string): string {
2✔
592
    return `CAST(JULIANDAY(${endDate}) - JULIANDAY(${startDate}) AS INTEGER)`;
×
UNCOV
593
  }
×
594

595
  year(date: string): string {
2✔
596
    return `CAST(STRFTIME('%Y', ${date}) AS INTEGER)`;
×
UNCOV
597
  }
×
598

599
  createdTime(): string {
2✔
600
    return '__created_time';
×
601
  }
×
602

603
  private normalizeBooleanCondition(condition: string): string {
2✔
604
    const wrapped = `(${condition})`;
×
605
    const valueType = `TYPEOF${wrapped}`;
×
606
    return `CASE
×
607
      WHEN ${wrapped} IS NULL THEN 0
×
608
      WHEN ${valueType} = 'integer' OR ${valueType} = 'real' THEN (${wrapped}) != 0
×
UNCOV
609
      WHEN ${valueType} = 'text' THEN (${wrapped} != '' AND LOWER(${wrapped}) != 'null')
×
UNCOV
610
      ELSE (${wrapped}) IS NOT NULL AND ${wrapped} != 'null'
×
611
    END`;
×
612
  }
×
613

614
  // Logical Functions
2✔
615
  if(condition: string, valueIfTrue: string, valueIfFalse: string): string {
2✔
616
    const booleanCondition = this.normalizeBooleanCondition(condition);
×
617
    return `CASE WHEN (${booleanCondition}) THEN ${valueIfTrue} ELSE ${valueIfFalse} END`;
×
618
  }
×
619

620
  and(params: string[]): string {
2✔
621
    return `(${this.joinParams(params, ' AND ')})`;
×
622
  }
×
623

624
  or(params: string[]): string {
2✔
UNCOV
625
    return `(${this.joinParams(params, ' OR ')})`;
×
626
  }
×
627

628
  not(value: string): string {
2✔
629
    return `NOT (${value})`;
×
630
  }
×
631

632
  xor(params: string[]): string {
2✔
633
    // SQLite doesn't have built-in XOR for multiple values
×
634
    if (params.length === 2) {
×
UNCOV
635
      return `((${params[0]}) AND NOT (${params[1]})) OR (NOT (${params[0]}) AND (${params[1]}))`;
×
636
    }
×
637
    // For multiple values, count true values and check if odd
×
638
    return `(${this.joinParams(
×
UNCOV
639
      params.map((p) => `CASE WHEN ${p} THEN 1 ELSE 0 END`),
×
640
      ' + '
×
641
    )}) % 2 = 1`;
×
642
  }
×
643

644
  blank(): string {
2✔
UNCOV
645
    return 'NULL';
×
646
  }
×
647

648
  error(_message: string): string {
2✔
UNCOV
649
    // ERROR function in SQLite generated columns should return NULL
×
UNCOV
650
    // since we can't throw actual errors in generated columns
×
651
    return 'NULL';
×
652
  }
×
653

654
  isError(value: string): string {
2✔
UNCOV
655
    // SQLite doesn't have a direct ISERROR function
×
656
    return `CASE WHEN ${value} IS NULL THEN 1 ELSE 0 END`;
×
657
  }
×
658

659
  switch(
2✔
UNCOV
660
    expression: string,
×
661
    cases: Array<{ case: string; result: string }>,
×
662
    defaultResult?: string
×
663
  ): string {
×
664
    let caseStatement = 'CASE';
×
665

666
    for (const caseItem of cases) {
×
667
      caseStatement += ` WHEN ${expression} = ${caseItem.case} THEN ${caseItem.result}`;
×
668
    }
×
669

UNCOV
670
    if (defaultResult) {
×
671
      caseStatement += ` ELSE ${defaultResult}`;
×
672
    }
×
673

UNCOV
674
    caseStatement += ' END';
×
675
    return caseStatement;
×
676
  }
×
677

678
  // Array Functions
2✔
679
  count(params: string[]): string {
2✔
680
    // Count non-null values
×
UNCOV
681
    return `(${params.map((p) => `CASE WHEN ${p} IS NOT NULL THEN 1 ELSE 0 END`).join(' + ')})`;
×
682
  }
×
683

684
  countA(params: string[]): string {
2✔
UNCOV
685
    // Count non-empty values (excluding empty strings)
×
686
    return `(${params.map((p) => `CASE WHEN ${p} IS NOT NULL AND ${p} <> '' THEN 1 ELSE 0 END`).join(' + ')})`;
×
687
  }
×
688

689
  countAll(value: string): string {
2✔
690
    // For single values, return 1 if not null, 0 if null
×
691
    return `CASE WHEN ${value} IS NULL THEN 0 ELSE 1 END`;
×
UNCOV
692
  }
×
693

694
  arrayJoin(array: string, separator?: string): string {
2✔
UNCOV
695
    // SQLite generated columns don't support subqueries, so we'll use simple string manipulation
×
UNCOV
696
    // This assumes arrays are stored as JSON strings like ["a","b","c"] or ["a", "b", "c"]
×
697
    const sep = separator ? this.stringLiteral(separator) : this.stringLiteral(', ');
×
698
    return `(
×
699
      CASE
UNCOV
700
        WHEN json_valid(${array}) AND json_type(${array}) = 'array' THEN
×
701
          REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(${array}, '[', ''), ']', ''), '"', ''), ', ', ','), ',', ${sep})
×
702
        WHEN ${array} IS NOT NULL THEN CAST(${array} AS TEXT)
×
703
        ELSE NULL
704
      END
705
    )`;
×
UNCOV
706
  }
×
707

708
  arrayUnique(array: string): string {
2✔
709
    // SQLite generated columns don't support complex operations for uniqueness
×
710
    // For now, return the array as-is (this is a limitation)
×
UNCOV
711
    return `(
×
712
      CASE
713
        WHEN json_valid(${array}) AND json_type(${array}) = 'array' THEN ${array}
×
714
        ELSE ${array}
×
715
      END
UNCOV
716
    )`;
×
717
  }
×
718

719
  arrayFlatten(array: string): string {
2✔
720
    // For SQLite generated columns, flattening is complex without subqueries
×
721
    // Return the array as-is (this is a limitation)
×
722
    return `(
×
723
      CASE
UNCOV
724
        WHEN json_valid(${array}) AND json_type(${array}) = 'array' THEN ${array}
×
UNCOV
725
        ELSE ${array}
×
726
      END
727
    )`;
×
728
  }
×
729

730
  arrayCompact(array: string): string {
2✔
UNCOV
731
    // SQLite generated columns don't support complex filtering without subqueries
×
UNCOV
732
    // For now, return the array as-is (this is a limitation)
×
733
    return `(
×
734
      CASE
UNCOV
735
        WHEN json_valid(${array}) AND json_type(${array}) = 'array' THEN ${array}
×
UNCOV
736
        ELSE ${array}
×
737
      END
738
    )`;
×
UNCOV
739
  }
×
740

741
  // System Functions
2✔
742
  recordId(): string {
2✔
UNCOV
743
    return '__id';
×
UNCOV
744
  }
×
745

746
  autoNumber(): string {
2✔
UNCOV
747
    return '__auto_number';
×
UNCOV
748
  }
×
749

750
  textAll(value: string): string {
2✔
751
    // Use same logic as t() function to handle integer formatting
×
UNCOV
752
    return `CASE
×
UNCOV
753
      WHEN ${value} = CAST(${value} AS INTEGER) THEN CAST(${value} AS INTEGER)
×
UNCOV
754
      ELSE CAST(${value} AS TEXT)
×
755
    END`;
×
756
  }
×
757

758
  // Field Reference - SQLite uses backticks for identifiers
2✔
759
  fieldReference(_fieldId: string, columnName: string): string {
2✔
760
    // For regular field references, return the column reference
×
761
    // Note: Expansion is handled at the expression level, not at individual field reference level
×
UNCOV
762
    return `\`${columnName}\``;
×
UNCOV
763
  }
×
764

765
  // Override some base implementations for SQLite-specific syntax
2✔
766
  castToNumber(value: string): string {
2✔
UNCOV
767
    return `CAST(${value} AS REAL)`;
×
UNCOV
768
  }
×
769

770
  castToString(value: string): string {
2✔
UNCOV
771
    return `CAST(${value} AS TEXT)`;
×
UNCOV
772
  }
×
773

774
  castToBoolean(value: string): string {
2✔
UNCOV
775
    return `CAST(${value} AS INTEGER)`;
×
UNCOV
776
  }
×
777

778
  castToDate(value: string): string {
2✔
UNCOV
779
    return `DATETIME(${value})`;
×
UNCOV
780
  }
×
781

782
  // SQLite uses square brackets for identifiers with special characters
2✔
783
  protected escapeIdentifier(identifier: string): string {
2✔
UNCOV
784
    return `[${identifier.replace(/\]/g, ']]')}]`;
×
UNCOV
785
  }
×
786

787
  // Override binary operations to handle SQLite-specific behavior
2✔
788
  modulo(left: string, right: string): string {
2✔
UNCOV
789
    return `(${left} % ${right})`;
×
UNCOV
790
  }
×
791

792
  // SQLite uses different boolean literals
2✔
793
  booleanLiteral(value: boolean): string {
2✔
UNCOV
794
    return value ? '1' : '0';
×
UNCOV
795
  }
×
796
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc