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

teableio / teable / 21892749536

11 Feb 2026 04:28AM UTC coverage: 64.087% (-0.05%) from 64.133%
21892749536

push

github

web-flow
[sync] feat(observability): add OTEL metrics instrumentation and dashboards (T1991) (#1230) (#2586)

Synced from teableio/teable-ee@797b30c

Co-authored-by: teable-bot <bot@teable.io>

4978 of 6515 branches covered (76.41%)

1106 of 1794 new or added lines in 83 files covered. (61.65%)

46 existing lines in 11 files now uncovered.

22942 of 35798 relevant lines covered (64.09%)

9133.46 hits per line

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

8.33
/apps/nestjs-backend/src/db-provider/generated-column-query/sqlite/generated-column-query.sqlite.ts
1
/* eslint-disable sonarjs/no-identical-functions */
2
import { isTextLikeParam, resolveFormulaParamInfo } from '../../utils/formula-param-metadata.util';
3
import { GeneratedColumnQueryAbstract } from '../generated-column-query.abstract';
4

5
/**
6
 * SQLite-specific implementation of generated column query functions
7
 * Converts Teable formula functions to SQLite SQL expressions suitable
8
 * for use in generated columns. All generated SQL must be immutable.
9
 */
366✔
10
export class GeneratedColumnQuerySqlite extends GeneratedColumnQueryAbstract {
11
  private getParamInfo(index?: number) {
12
    return resolveFormulaParamInfo(this.currentCallMetadata, index);
4✔
13
  }
14

15
  private isStringLiteral(value: string): boolean {
16
    const trimmed = value.trim();
×
17
    return /^'.*'$/.test(trimmed);
×
18
  }
19

20
  private isEmptyStringLiteral(value: string): boolean {
21
    return value.trim() === "''";
×
22
  }
23

24
  private normalizeBlankComparable(value: string): string {
25
    // Treat NULL and empty strings as empty text for comparison parity with interpreter
26
    return `COALESCE(NULLIF(CAST((${value}) AS TEXT), ''), '')`;
×
27
  }
28

29
  private buildBlankAwareComparison(operator: '=' | '<>', left: string, right: string): string {
30
    const leftIsEmptyLiteral = this.isEmptyStringLiteral(left);
×
31
    const rightIsEmptyLiteral = this.isEmptyStringLiteral(right);
×
32
    const leftInfo = this.getParamInfo(0);
×
33
    const rightInfo = this.getParamInfo(1);
×
34
    const textComparison =
35
      leftIsEmptyLiteral ||
×
36
      rightIsEmptyLiteral ||
37
      this.isStringLiteral(left) ||
38
      this.isStringLiteral(right) ||
39
      isTextLikeParam(leftInfo) ||
40
      isTextLikeParam(rightInfo);
41

42
    if (!textComparison) {
×
43
      return `(${left} ${operator} ${right})`;
×
44
    }
45

46
    const normalize = (value: string, isEmptyLiteral: boolean) =>
×
47
      isEmptyLiteral ? "''" : this.normalizeBlankComparable(value);
×
48

49
    return `(${normalize(left, leftIsEmptyLiteral)} ${operator} ${normalize(right, rightIsEmptyLiteral)})`;
×
50
  }
51

52
  // Numeric Functions
53
  sum(params: string[]): string {
54
    if (params.length === 0) {
×
55
      return 'NULL';
×
56
    }
57
    if (params.length === 1) {
×
58
      return `${params[0]}`;
×
59
    }
60
    // SQLite doesn't have SUM() for multiple values, use addition
61
    return `(${this.joinParams(params, ' + ')})`;
×
62
  }
63

64
  average(params: string[]): string {
65
    if (params.length === 0) {
×
66
      return 'NULL';
×
67
    }
68
    if (params.length === 1) {
×
69
      return `${params[0]}`;
×
70
    }
71
    // Calculate average as sum divided by count
72
    return `((${this.joinParams(params, ' + ')}) / ${params.length})`;
×
73
  }
74

75
  max(params: string[]): string {
76
    if (params.length === 0) {
×
77
      return 'NULL';
×
78
    }
79
    if (params.length === 1) {
×
80
      return `${params[0]}`;
×
81
    }
82
    // Use nested MAX functions for multiple values
83
    return params.reduce((acc, param) => `MAX(${acc}, ${param})`);
×
84
  }
85

86
  min(params: string[]): string {
87
    if (params.length === 0) {
×
88
      return 'NULL';
×
89
    }
90
    if (params.length === 1) {
×
91
      return `${params[0]}`;
×
92
    }
93
    // Use nested MIN functions for multiple values
94
    return params.reduce((acc, param) => `MIN(${acc}, ${param})`);
×
95
  }
96

97
  round(value: string, precision?: string): string {
98
    if (precision) {
×
99
      return `ROUND(${value}, ${precision})`;
×
100
    }
101
    return `ROUND(${value})`;
×
102
  }
103

104
  roundUp(value: string, precision?: string): string {
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(CEIL(${value} * ${factor}) / ${factor} AS REAL)`;
×
118
    }
119
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
120
  }
121

122
  roundDown(value: string, precision?: string): string {
123
    if (precision) {
×
124
      // Use manual power calculation for 10^precision (common cases)
125
      const factor = `(
×
126
        CASE
127
          WHEN ${precision} = 0 THEN 1
128
          WHEN ${precision} = 1 THEN 10
129
          WHEN ${precision} = 2 THEN 100
130
          WHEN ${precision} = 3 THEN 1000
131
          WHEN ${precision} = 4 THEN 10000
132
          ELSE 1
133
        END
134
      )`;
135
      return `CAST(FLOOR(${value} * ${factor}) / ${factor} AS REAL)`;
×
136
    }
137
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
138
  }
139

140
  ceiling(value: string): string {
141
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
142
  }
143

144
  floor(value: string): string {
145
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
146
  }
147

148
  even(value: string): string {
149
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 0 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
150
  }
151

152
  odd(value: string): string {
153
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 1 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
154
  }
155

156
  int(value: string): string {
157
    return `CAST(${value} AS INTEGER)`;
×
158
  }
159

160
  abs(value: string): string {
161
    return `ABS(${value})`;
×
162
  }
163

164
  sqrt(value: string): string {
165
    // SQLite doesn't have SQRT function, use Newton's method approximation
166
    // One iteration of Newton's method: (x/2 + x/(x/2)) / 2
167
    return `(
×
168
      CASE
169
        WHEN ${value} <= 0 THEN 0
170
        ELSE (${value} / 2.0 + ${value} / (${value} / 2.0)) / 2.0
171
      END
172
    )`;
173
  }
174

175
  power(base: string, exponent: string): string {
176
    // SQLite doesn't have POWER function, implement for common cases
177
    return `(
×
178
      CASE
179
        WHEN ${exponent} = 0 THEN 1
180
        WHEN ${exponent} = 1 THEN ${base}
181
        WHEN ${exponent} = 2 THEN ${base} * ${base}
182
        WHEN ${exponent} = 3 THEN ${base} * ${base} * ${base}
183
        WHEN ${exponent} = 4 THEN ${base} * ${base} * ${base} * ${base}
184
        WHEN ${exponent} = 0.5 THEN
185
          -- Square root case using Newton's method
186
          CASE
187
            WHEN ${base} <= 0 THEN 0
188
            ELSE (${base} / 2.0 + ${base} / (${base} / 2.0)) / 2.0
189
          END
190
        ELSE 1
191
      END
192
    )`;
193
  }
194

195
  exp(value: string): string {
196
    return `EXP(${value})`;
×
197
  }
198

199
  log(value: string, base?: string): string {
200
    if (base) {
×
201
      return `(LOG(${value}) / LOG(${base}))`;
×
202
    }
203
    // SQLite LOG is base 10, but formula LOG should be natural log (base e)
204
    return `LN(${value})`;
×
205
  }
206

207
  mod(dividend: string, divisor: string): string {
208
    return `(${dividend} % ${divisor})`;
×
209
  }
210

211
  value(text: string): string {
212
    return `CAST(${text} AS REAL)`;
×
213
  }
214

215
  // Text Functions
216
  concatenate(params: string[]): string {
217
    // Handle NULL values by converting them to empty strings for CONCATENATE function
218
    // This mirrors the behavior of the formula evaluation engine
219
    const nullSafeParams = params.map((param) => `COALESCE(${param}, '')`);
×
220
    return `(${this.joinParams(nullSafeParams, ' || ')})`;
×
221
  }
222

223
  // String concatenation for + operator (treats NULL as empty string)
224
  stringConcat(left: string, right: string): string {
225
    return `(COALESCE(${left}, '') || COALESCE(${right}, ''))`;
×
226
  }
227

228
  equal(left: string, right: string): string {
229
    return this.buildBlankAwareComparison('=', left, right);
×
230
  }
231

232
  notEqual(left: string, right: string): string {
233
    return this.buildBlankAwareComparison('<>', left, right);
×
234
  }
235

236
  find(searchText: string, withinText: string, startNum?: string): string {
237
    if (startNum) {
×
238
      return `CASE WHEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) > 0 THEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) + ${startNum} - 1 ELSE 0 END`;
×
239
    }
240
    return `INSTR(${withinText}, ${searchText})`;
×
241
  }
242

243
  search(searchText: string, withinText: string, startNum?: string): string {
244
    // SQLite INSTR is case-sensitive, so we use UPPER for case-insensitive search
245
    if (startNum) {
×
246
      return `CASE WHEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) > 0 THEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) + ${startNum} - 1 ELSE 0 END`;
×
247
    }
248
    return `INSTR(UPPER(${withinText}), UPPER(${searchText}))`;
×
249
  }
250

251
  mid(text: string, startNum: string, numChars: string): string {
252
    return `SUBSTR(${text}, ${startNum}, ${numChars})`;
×
253
  }
254

255
  left(text: string, numChars: string): string {
256
    return `SUBSTR(${text}, 1, ${numChars})`;
×
257
  }
258

259
  right(text: string, numChars: string): string {
260
    return `SUBSTR(${text}, -${numChars})`;
×
261
  }
262

263
  replace(oldText: string, startNum: string, numChars: string, newText: string): string {
264
    return `SUBSTR(${oldText}, 1, ${startNum} - 1) || ${newText} || SUBSTR(${oldText}, ${startNum} + ${numChars})`;
×
265
  }
266

267
  regexpReplace(text: string, pattern: string, replacement: string): string {
268
    // SQLite doesn't have built-in regex replace, would need extension
269
    return `REPLACE(${text}, ${pattern}, ${replacement})`;
×
270
  }
271

272
  substitute(text: string, oldText: string, newText: string, instanceNum?: string): string {
273
    // SQLite REPLACE replaces all instances, no direct support for specific instance
274
    return `REPLACE(${text}, ${oldText}, ${newText})`;
×
275
  }
276

277
  lower(text: string): string {
278
    return `LOWER(${text})`;
×
279
  }
280

281
  upper(text: string): string {
282
    return `UPPER(${text})`;
×
283
  }
284

285
  rept(text: string, numTimes: string): string {
286
    // SQLite doesn't have REPEAT function, need to use recursive CTE or custom function
287
    return `REPLACE(HEX(ZEROBLOB(${numTimes})), '00', ${text})`;
×
288
  }
289

290
  trim(text: string): string {
291
    return `TRIM(${text})`;
×
292
  }
293

294
  len(text: string): string {
295
    return `LENGTH(${text})`;
×
296
  }
297

298
  t(value: string): string {
299
    return `CASE
×
300
      WHEN ${value} IS NULL THEN ''
301
      WHEN ${value} = CAST(${value} AS INTEGER) THEN CAST(${value} AS INTEGER)
302
      ELSE CAST(${value} AS TEXT)
303
    END`;
304
  }
305

306
  encodeUrlComponent(text: string): string {
307
    // SQLite doesn't have built-in URL encoding
308
    return `${text}`;
×
309
  }
310

311
  // DateTime Functions
312
  now(): string {
313
    // For generated columns, use the current timestamp at field creation time
314
    if (this.isGeneratedColumnContext) {
×
315
      const currentTimestamp = new Date()
×
316
        .toISOString()
317
        .replace('T', ' ')
318
        .replace('Z', '')
319
        .replace(/\.\d{3}$/, '');
320
      return `'${currentTimestamp}'`;
×
321
    }
322
    return "DATETIME('now')";
×
323
  }
324

325
  today(): string {
326
    // For generated columns, use the current date at field creation time
327
    if (this.isGeneratedColumnContext) {
×
328
      const currentDate = new Date().toISOString().split('T')[0];
×
329
      return `'${currentDate}'`;
×
330
    }
331
    return "DATE('now')";
×
332
  }
333

334
  private normalizeDateModifier(unitLiteral: string): {
335
    unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
336
    factor: number;
337
  } {
338
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
×
339
    switch (normalized) {
×
340
      case 'millisecond':
341
      case 'milliseconds':
342
      case 'ms':
343
        return { unit: 'seconds', factor: 0.001 };
×
344
      case 'second':
345
      case 'seconds':
346
      case 's':
347
      case 'sec':
348
      case 'secs':
349
        return { unit: 'seconds', factor: 1 };
×
350
      case 'minute':
351
      case 'minutes':
352
      case 'min':
353
      case 'mins':
354
        return { unit: 'minutes', factor: 1 };
×
355
      case 'hour':
356
      case 'hours':
357
      case 'h':
358
      case 'hr':
359
      case 'hrs':
360
        return { unit: 'hours', factor: 1 };
×
361
      case 'week':
362
      case 'weeks':
363
        return { unit: 'days', factor: 7 };
×
364
      case 'month':
365
      case 'months':
366
        return { unit: 'months', factor: 1 };
×
367
      case 'quarter':
368
      case 'quarters':
369
        return { unit: 'months', factor: 3 };
×
370
      case 'year':
371
      case 'years':
372
        return { unit: 'years', factor: 1 };
×
373
      case 'day':
374
      case 'days':
375
      default:
376
        return { unit: 'days', factor: 1 };
×
377
    }
378
  }
379

380
  private normalizeDiffUnit(
381
    unitLiteral: string
382
  ): 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' {
383
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
10✔
384
    switch (normalized) {
10✔
385
      case 'millisecond':
386
      case 'milliseconds':
387
      case 'ms':
388
        return 'millisecond';
×
389
      case 'second':
390
      case 'seconds':
391
      case 's':
392
      case 'sec':
393
      case 'secs':
394
        return 'second';
2✔
395
      case 'minute':
396
      case 'minutes':
397
      case 'min':
398
      case 'mins':
399
        return 'minute';
×
400
      case 'hour':
401
      case 'hours':
402
      case 'h':
403
      case 'hr':
404
      case 'hrs':
405
        return 'hour';
2✔
406
      case 'week':
407
      case 'weeks':
408
        return 'week';
×
409
      case 'month':
410
      case 'months':
411
        return 'month';
×
412
      case 'quarter':
413
      case 'quarters':
414
        return 'quarter';
×
415
      case 'year':
416
      case 'years':
417
        return 'year';
×
418
      default:
419
        return 'day';
6✔
420
    }
421
  }
422

423
  private normalizeTruncateFormat(unitLiteral: string): string {
424
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
×
425
    switch (normalized) {
×
426
      case 'millisecond':
427
      case 'milliseconds':
428
      case 'ms':
429
      case 'second':
430
      case 'seconds':
431
      case 's':
432
      case 'sec':
433
      case 'secs':
434
        return '%Y-%m-%d %H:%M:%S';
×
435
      case 'minute':
436
      case 'minutes':
437
      case 'min':
438
      case 'mins':
439
        return '%Y-%m-%d %H:%M';
×
440
      case 'hour':
441
      case 'hours':
442
      case 'h':
443
      case 'hr':
444
      case 'hrs':
445
        return '%Y-%m-%d %H';
×
446
      case 'week':
447
      case 'weeks':
448
        return '%Y-%W';
×
449
      case 'month':
450
      case 'months':
451
        return '%Y-%m';
×
452
      case 'year':
453
      case 'years':
454
        return '%Y';
×
455
      case 'day':
456
      case 'days':
457
      default:
458
        return '%Y-%m-%d';
×
459
    }
460
  }
461

462
  dateAdd(date: string, count: string, unit: string): string {
463
    const { unit: cleanUnit, factor } = this.normalizeDateModifier(unit);
×
464
    const scaledCount = factor === 1 ? `(${count})` : `(${count}) * ${factor}`;
×
465
    return `DATETIME(${date}, (${scaledCount}) || ' ${cleanUnit}')`;
×
466
  }
467

468
  datestr(date: string): string {
469
    return `DATE(${date})`;
×
470
  }
471

472
  private buildMonthDiff(startDate: string, endDate: string): string {
473
    const startYear = `CAST(STRFTIME('%Y', ${startDate}) AS INTEGER)`;
×
474
    const endYear = `CAST(STRFTIME('%Y', ${endDate}) AS INTEGER)`;
×
475
    const startMonth = `CAST(STRFTIME('%m', ${startDate}) AS INTEGER)`;
×
476
    const endMonth = `CAST(STRFTIME('%m', ${endDate}) AS INTEGER)`;
×
477
    const startDay = `CAST(STRFTIME('%d', ${startDate}) AS INTEGER)`;
×
478
    const endDay = `CAST(STRFTIME('%d', ${endDate}) AS INTEGER)`;
×
479
    const startLastDay = `CAST(STRFTIME('%d', DATE(${startDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
480
    const endLastDay = `CAST(STRFTIME('%d', DATE(${endDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
481

482
    const baseMonths = `((${startYear} - ${endYear}) * 12 + (${startMonth} - ${endMonth}))`;
×
483
    const adjustDown = `(CASE WHEN ${baseMonths} > 0 AND ${startDay} < ${endDay} AND ${startDay} < ${startLastDay} THEN 1 ELSE 0 END)`;
×
484
    const adjustUp = `(CASE WHEN ${baseMonths} < 0 AND ${startDay} > ${endDay} AND ${endDay} < ${endLastDay} THEN 1 ELSE 0 END)`;
×
485

486
    return `(${baseMonths} - ${adjustDown} + ${adjustUp})`;
×
487
  }
488

489
  datetimeDiff(startDate: string, endDate: string, unit: string): string {
490
    const baseDiffDays = `(JULIANDAY(${startDate}) - JULIANDAY(${endDate}))`;
×
491
    switch (this.normalizeDiffUnit(unit)) {
×
492
      case 'millisecond':
493
        return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;
×
494
      case 'second':
495
        return `(${baseDiffDays}) * 24.0 * 60 * 60`;
×
496
      case 'minute':
497
        return `(${baseDiffDays}) * 24.0 * 60`;
×
498
      case 'hour':
499
        return `(${baseDiffDays}) * 24.0`;
×
500
      case 'week':
501
        return `(${baseDiffDays}) / 7.0`;
×
502
      case 'month':
503
        return this.buildMonthDiff(startDate, endDate);
×
504
      case 'quarter':
505
        return `${this.buildMonthDiff(startDate, endDate)} / 3.0`;
×
506
      case 'year': {
507
        const monthDiff = this.buildMonthDiff(startDate, endDate);
×
508
        return `CAST((${monthDiff}) / 12.0 AS INTEGER)`;
×
509
      }
510
      case 'day':
511
      default:
512
        return `${baseDiffDays}`;
×
513
    }
514
  }
515

516
  datetimeFormat(date: string, format: string): string {
517
    // Convert common format patterns to SQLite STRFTIME format
518
    const cleanFormat = format.replace(/^'|'$/g, '');
×
519
    const sqliteFormat = cleanFormat
×
520
      .replace(/YYYY/g, '%Y')
521
      .replace(/MM/g, '%m')
522
      .replace(/DD/g, '%d')
523
      .replace(/HH/g, '%H')
524
      .replace(/mm/g, '%M')
525
      .replace(/ss/g, '%S');
526

527
    return `STRFTIME('${sqliteFormat}', ${date})`;
×
528
  }
529

530
  datetimeParse(dateString: string, _format?: string): string {
531
    // SQLite doesn't have direct parsing with custom format
532
    return `DATETIME(${dateString})`;
×
533
  }
534

535
  day(date: string): string {
536
    return `CAST(STRFTIME('%d', ${date}) AS INTEGER)`;
×
537
  }
538

539
  private buildNowDiffByUnit(nowExpr: string, dateExpr: string, unit: string): string {
540
    const diffUnit = this.normalizeDiffUnit(unit);
10✔
541
    const baseDiffDays = `(JULIANDAY(${nowExpr}) - JULIANDAY(${dateExpr}))`;
10✔
542
    switch (diffUnit) {
10✔
543
      case 'millisecond':
NEW
544
        return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;
×
545
      case 'second':
546
        return `(${baseDiffDays}) * 24.0 * 60 * 60`;
2✔
547
      case 'minute':
NEW
548
        return `(${baseDiffDays}) * 24.0 * 60`;
×
549
      case 'hour':
550
        return `(${baseDiffDays}) * 24.0`;
2✔
551
      case 'week':
NEW
552
        return `(${baseDiffDays}) / 7.0`;
×
553
      case 'month':
NEW
554
        return this.buildMonthDiff(nowExpr, dateExpr);
×
555
      case 'quarter':
NEW
556
        return `${this.buildMonthDiff(nowExpr, dateExpr)} / 3.0`;
×
557
      case 'year': {
NEW
558
        const monthDiff = this.buildMonthDiff(nowExpr, dateExpr);
×
NEW
559
        return `CAST((${monthDiff}) / 12.0 AS INTEGER)`;
×
560
      }
561
      case 'day':
562
      default:
563
        return `${baseDiffDays}`;
6✔
564
    }
565
  }
566

567
  fromNow(date: string, unit = 'day'): string {
568
    // For generated columns, use the current timestamp at field creation time
569
    const dateExpr = `DATETIME(${date})`;
10✔
570
    if (this.isGeneratedColumnContext) {
10✔
571
      const currentTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '');
×
NEW
572
      return this.buildNowDiffByUnit(`'${currentTimestamp}'`, dateExpr, unit);
×
573
    }
574
    return this.buildNowDiffByUnit("'now'", dateExpr, unit);
10✔
575
  }
576

577
  hour(date: string): string {
578
    return `CAST(STRFTIME('%H', ${date}) AS INTEGER)`;
×
579
  }
580

581
  isAfter(date1: string, date2: string): string {
582
    return `DATETIME(${date1}) > DATETIME(${date2})`;
×
583
  }
584

585
  isBefore(date1: string, date2: string): string {
586
    return `DATETIME(${date1}) < DATETIME(${date2})`;
×
587
  }
588

589
  isSame(date1: string, date2: string, unit?: string): string {
590
    if (unit) {
×
591
      const trimmed = unit.trim();
×
592
      if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
×
593
        const format = this.normalizeTruncateFormat(trimmed.slice(1, -1));
×
594
        return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
×
595
      }
596
      const format = this.normalizeTruncateFormat(unit);
×
597
      return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
×
598
    }
599
    return `DATETIME(${date1}) = DATETIME(${date2})`;
×
600
  }
601

602
  lastModifiedTime(): string {
603
    return '__last_modified_time';
×
604
  }
605

606
  minute(date: string): string {
607
    return `CAST(STRFTIME('%M', ${date}) AS INTEGER)`;
×
608
  }
609

610
  month(date: string): string {
611
    return `CAST(STRFTIME('%m', ${date}) AS INTEGER)`;
×
612
  }
613

614
  second(date: string): string {
615
    return `CAST(STRFTIME('%S', ${date}) AS INTEGER)`;
×
616
  }
617

618
  timestr(date: string): string {
619
    return `TIME(${date})`;
×
620
  }
621

622
  toNow(date: string, unit = 'day'): string {
623
    return this.fromNow(date, unit);
2✔
624
  }
625

626
  weekNum(date: string): string {
627
    return `CAST(STRFTIME('%W', ${date}) AS INTEGER)`;
×
628
  }
629

630
  weekday(date: string, _startDayOfWeek?: string): string {
631
    // Convert SQLite's 0-based weekday (0=Sunday) to 1-based (1=Sunday)
632
    return `(CAST(STRFTIME('%w', ${date}) AS INTEGER) + 1)`;
×
633
  }
634

635
  workday(startDate: string, days: string): string {
636
    return `DATE(${startDate}, '+' || ${days} || ' days')`;
×
637
  }
638

639
  workdayDiff(startDate: string, endDate: string): string {
640
    return `CAST(JULIANDAY(${endDate}) - JULIANDAY(${startDate}) AS INTEGER)`;
×
641
  }
642

643
  year(date: string): string {
644
    return `CAST(STRFTIME('%Y', ${date}) AS INTEGER)`;
×
645
  }
646

647
  createdTime(): string {
648
    return '__created_time';
×
649
  }
650

651
  private normalizeBooleanCondition(condition: string): string {
652
    const wrapped = `(${condition})`;
×
653
    const valueType = `TYPEOF${wrapped}`;
×
654
    return `CASE
×
655
      WHEN ${wrapped} IS NULL THEN 0
656
      WHEN ${valueType} = 'integer' OR ${valueType} = 'real' THEN (${wrapped}) != 0
657
      WHEN ${valueType} = 'text' THEN (${wrapped} != '' AND LOWER(${wrapped}) != 'null')
658
      ELSE (${wrapped}) IS NOT NULL AND ${wrapped} != 'null'
659
    END`;
660
  }
661

662
  // Logical Functions
663
  if(condition: string, valueIfTrue: string, valueIfFalse: string): string {
664
    const booleanCondition = this.normalizeBooleanCondition(condition);
×
665
    return `CASE WHEN (${booleanCondition}) THEN ${valueIfTrue} ELSE ${valueIfFalse} END`;
×
666
  }
667

668
  and(params: string[]): string {
669
    return `(${this.joinParams(params, ' AND ')})`;
×
670
  }
671

672
  or(params: string[]): string {
673
    return `(${this.joinParams(params, ' OR ')})`;
×
674
  }
675

676
  not(value: string): string {
677
    return `NOT (${value})`;
×
678
  }
679

680
  xor(params: string[]): string {
681
    // SQLite doesn't have built-in XOR for multiple values
682
    if (params.length === 2) {
×
683
      return `((${params[0]}) AND NOT (${params[1]})) OR (NOT (${params[0]}) AND (${params[1]}))`;
×
684
    }
685
    // For multiple values, count true values and check if odd
686
    return `(${this.joinParams(
×
687
      params.map((p) => `CASE WHEN ${p} THEN 1 ELSE 0 END`),
×
688
      ' + '
689
    )}) % 2 = 1`;
690
  }
691

692
  blank(): string {
693
    return 'NULL';
×
694
  }
695

696
  error(_message: string): string {
697
    // ERROR function in SQLite generated columns should return NULL
698
    // since we can't throw actual errors in generated columns
699
    return 'NULL';
×
700
  }
701

702
  isError(value: string): string {
703
    // SQLite doesn't have a direct ISERROR function
704
    return `CASE WHEN ${value} IS NULL THEN 1 ELSE 0 END`;
×
705
  }
706

707
  switch(
708
    expression: string,
709
    cases: Array<{ case: string; result: string }>,
710
    defaultResult?: string
711
  ): string {
712
    let caseStatement = 'CASE';
×
713

714
    for (const caseItem of cases) {
×
715
      caseStatement += ` WHEN ${expression} = ${caseItem.case} THEN ${caseItem.result}`;
×
716
    }
717

718
    if (defaultResult) {
×
719
      caseStatement += ` ELSE ${defaultResult}`;
×
720
    }
721

722
    caseStatement += ' END';
×
723
    return caseStatement;
×
724
  }
725

726
  // Array Functions
727
  count(params: string[]): string {
728
    // Count non-null values
729
    return `(${params.map((p) => `CASE WHEN ${p} IS NOT NULL THEN 1 ELSE 0 END`).join(' + ')})`;
×
730
  }
731

732
  countA(params: string[]): string {
733
    // Count non-empty values (excluding empty strings)
734
    return `(${params.map((p) => `CASE WHEN ${p} IS NOT NULL AND ${p} <> '' THEN 1 ELSE 0 END`).join(' + ')})`;
×
735
  }
736

737
  countAll(value: string): string {
738
    const paramInfo = this.getParamInfo(0);
4✔
739
    if (paramInfo.isJsonField || paramInfo.isMultiValueField) {
4✔
740
      return `CASE
2✔
741
        WHEN ${value} IS NULL THEN 0
742
        WHEN json_valid(${value}) AND json_type(${value}) = 'array' THEN COALESCE(json_array_length(${value}), 0)
743
        WHEN json_valid(${value}) AND json_type(${value}) = 'null' THEN 0
744
        ELSE 1
745
      END`;
746
    }
747

748
    // For single values, return 1 if not null, 0 if null.
749
    return `CASE WHEN ${value} IS NULL THEN 0 ELSE 1 END`;
2✔
750
  }
751

752
  private buildJsonArrayUnion(
753
    arrays: string[],
754
    opts?: { filterNulls?: boolean; withOrdinal?: boolean }
755
  ): string {
756
    const selects = arrays.map((array, index) => {
×
757
      const base = `SELECT value, ${index} AS arg_index, CAST(key AS INTEGER) AS ord FROM json_each(COALESCE(${array}, '[]'))`;
×
758
      const whereClause = opts?.filterNulls
×
759
        ? " WHERE value IS NOT NULL AND value != 'null' AND value != ''"
760
        : '';
761
      return `${base}${whereClause}`;
×
762
    });
763

764
    if (selects.length === 0) {
×
765
      return 'SELECT NULL AS value, 0 AS arg_index, 0 AS ord WHERE 0';
×
766
    }
767

768
    return selects.join(' UNION ALL ');
×
769
  }
770

771
  arrayJoin(array: string, separator?: string): string {
772
    // SQLite generated columns don't support subqueries, so we'll use simple string manipulation
773
    // This assumes arrays are stored as JSON strings like ["a","b","c"] or ["a", "b", "c"]
774
    const sep = separator ? this.stringLiteral(separator) : this.stringLiteral(', ');
×
775
    return `(
×
776
      CASE
777
        WHEN json_valid(${array}) AND json_type(${array}) = 'array' THEN
778
          REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(${array}, '[', ''), ']', ''), '"', ''), ', ', ','), ',', ${sep})
779
        WHEN ${array} IS NOT NULL THEN CAST(${array} AS TEXT)
780
        ELSE NULL
781
      END
782
    )`;
783
  }
784

785
  arrayUnique(arrays: string[]): string {
786
    const unionQuery = this.buildJsonArrayUnion(arrays, { withOrdinal: true, filterNulls: true });
×
787
    return `COALESCE(
×
788
      '[' || (
789
        SELECT GROUP_CONCAT(json_quote(value))
790
        FROM (
791
          SELECT value, ROW_NUMBER() OVER (PARTITION BY value ORDER BY arg_index, ord) AS rn, arg_index, ord
792
          FROM (${unionQuery}) AS combined
793
        )
794
        WHERE rn = 1
795
        ORDER BY arg_index, ord
796
      ) || ']',
797
      '[]'
798
    )`;
799
  }
800

801
  arrayFlatten(arrays: string[]): string {
802
    const unionQuery = this.buildJsonArrayUnion(arrays, { withOrdinal: true });
×
803
    return `COALESCE(
×
804
      '[' || (
805
        SELECT GROUP_CONCAT(json_quote(value))
806
        FROM (${unionQuery}) AS combined
807
        ORDER BY arg_index, ord
808
      ) || ']',
809
      '[]'
810
    )`;
811
  }
812

813
  arrayCompact(arrays: string[]): string {
814
    const unionQuery = this.buildJsonArrayUnion(arrays, {
×
815
      filterNulls: true,
816
      withOrdinal: true,
817
    });
818
    return `COALESCE(
×
819
      '[' || (
820
        SELECT GROUP_CONCAT(json_quote(value))
821
        FROM (${unionQuery}) AS combined
822
        ORDER BY arg_index, ord
823
      ) || ']',
824
      '[]'
825
    )`;
826
  }
827

828
  // System Functions
829
  recordId(): string {
830
    return '__id';
×
831
  }
832

833
  autoNumber(): string {
834
    return '__auto_number';
×
835
  }
836

837
  textAll(value: string): string {
838
    // Use same logic as t() function to handle integer formatting
839
    return `CASE
×
840
      WHEN ${value} = CAST(${value} AS INTEGER) THEN CAST(${value} AS INTEGER)
841
      ELSE CAST(${value} AS TEXT)
842
    END`;
843
  }
844

845
  // Field Reference - SQLite uses backticks for identifiers
846
  fieldReference(_fieldId: string, columnName: string): string {
847
    // For regular field references, return the column reference
848
    // Note: Expansion is handled at the expression level, not at individual field reference level
849
    return `\`${columnName}\``;
×
850
  }
851

852
  // Override some base implementations for SQLite-specific syntax
853
  castToNumber(value: string): string {
854
    return `CAST(${value} AS REAL)`;
×
855
  }
856

857
  castToString(value: string): string {
858
    return `CAST(${value} AS TEXT)`;
×
859
  }
860

861
  castToBoolean(value: string): string {
862
    return `CAST(${value} AS INTEGER)`;
×
863
  }
864

865
  castToDate(value: string): string {
866
    return `DATETIME(${value})`;
×
867
  }
868

869
  // SQLite uses square brackets for identifiers with special characters
870
  protected escapeIdentifier(identifier: string): string {
871
    return `[${identifier.replace(/\]/g, ']]')}]`;
×
872
  }
873

874
  // Override binary operations to handle SQLite-specific behavior
875
  modulo(left: string, right: string): string {
876
    return `(${left} % ${right})`;
×
877
  }
878

879
  // SQLite uses different boolean literals
880
  booleanLiteral(value: boolean): string {
881
    return value ? '1' : '0';
×
882
  }
883
}
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