• 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

26.56
/apps/nestjs-backend/src/db-provider/select-query/sqlite/select-query.sqlite.ts
1
import type { ISelectFormulaConversionContext } from '../../../features/record/query-builder/sql-conversion.visitor';
2
import { isTextLikeParam, resolveFormulaParamInfo } from '../../utils/formula-param-metadata.util';
3
import { SelectQueryAbstract } from '../select-query.abstract';
4

5
/**
6
 * SQLite-specific implementation of SELECT query functions
7
 * Converts Teable formula functions to SQLite SQL expressions suitable
8
 * for use in SELECT statements. Unlike generated columns, these can use
9
 * more functions and have different optimization strategies.
10
 */
366✔
11
export class SelectQuerySqlite extends SelectQueryAbstract {
12
  private get tableAlias(): string | undefined {
13
    const ctx = this.context as ISelectFormulaConversionContext | undefined;
4✔
14
    return ctx?.tableAlias;
4✔
15
  }
16

17
  private getParamInfo(index?: number) {
18
    return resolveFormulaParamInfo(this.currentCallMetadata, index);
4✔
19
  }
20

21
  private isStringLiteral(value: string): boolean {
22
    const trimmed = value.trim();
×
23
    return /^'.*'$/.test(trimmed);
×
24
  }
25

26
  private qualifySystemColumn(column: string): string {
27
    const quoted = `"${column}"`;
×
28
    const alias = this.tableAlias;
×
29
    return alias ? `"${alias}".${quoted}` : quoted;
×
30
  }
31

32
  private isEmptyStringLiteral(value: string): boolean {
33
    return value.trim() === "''";
×
34
  }
35

36
  private normalizeBlankComparable(value: string): string {
37
    return `COALESCE(NULLIF(CAST((${value}) AS TEXT), ''), '')`;
×
38
  }
39

40
  private buildBlankAwareComparison(operator: '=' | '<>', left: string, right: string): string {
41
    const leftIsEmptyLiteral = this.isEmptyStringLiteral(left);
×
42
    const rightIsEmptyLiteral = this.isEmptyStringLiteral(right);
×
43
    const leftInfo = this.getParamInfo(0);
×
44
    const rightInfo = this.getParamInfo(1);
×
45
    const shouldNormalize =
46
      leftIsEmptyLiteral ||
×
47
      rightIsEmptyLiteral ||
48
      this.isStringLiteral(left) ||
49
      this.isStringLiteral(right) ||
50
      isTextLikeParam(leftInfo) ||
51
      isTextLikeParam(rightInfo);
52

53
    if (!shouldNormalize) {
×
54
      return `(${left} ${operator} ${right})`;
×
55
    }
56

57
    const normalize = (value: string, isEmptyLiteral: boolean) =>
×
58
      isEmptyLiteral ? "''" : this.normalizeBlankComparable(value);
×
59

60
    return `(${normalize(left, leftIsEmptyLiteral)} ${operator} ${normalize(right, rightIsEmptyLiteral)})`;
×
61
  }
62

63
  private coalesceNumeric(expr: string): string {
64
    return `COALESCE(CAST((${expr}) AS REAL), 0)`;
10✔
65
  }
66

67
  // Numeric Functions
68
  sum(params: string[]): string {
69
    if (params.length === 0) {
4✔
70
      return '0';
×
71
    }
72
    const terms = params.map((param) => this.coalesceNumeric(param));
10✔
73
    if (terms.length === 1) {
4✔
74
      return terms[0];
×
75
    }
76
    return `(${terms.join(' + ')})`;
4✔
77
  }
78

79
  average(params: string[]): string {
80
    if (params.length === 0) {
2✔
81
      return '0';
×
82
    }
83
    const numerator = this.sum(params);
2✔
84
    return `(${numerator}) / ${params.length}`;
2✔
85
  }
86

87
  max(params: string[]): string {
88
    return `MAX(${this.joinParams(params)})`;
×
89
  }
90

91
  min(params: string[]): string {
92
    return `MIN(${this.joinParams(params)})`;
×
93
  }
94

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

102
  roundUp(value: string, precision?: string): string {
103
    // SQLite doesn't have CEIL with precision, implement manually
104
    if (precision) {
×
105
      return `CAST(CEIL(${value} * POWER(10, ${precision})) / POWER(10, ${precision}) AS REAL)`;
×
106
    }
107
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
108
  }
109

110
  roundDown(value: string, precision?: string): string {
111
    // SQLite doesn't have FLOOR with precision, implement manually
112
    if (precision) {
×
113
      return `CAST(FLOOR(${value} * POWER(10, ${precision})) / POWER(10, ${precision}) AS REAL)`;
×
114
    }
115
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
116
  }
117

118
  ceiling(value: string): string {
119
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
120
  }
121

122
  floor(value: string): string {
123
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
124
  }
125

126
  even(value: string): string {
127
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 0 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
128
  }
129

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

134
  int(value: string): string {
135
    return `CAST(${value} AS INTEGER)`;
×
136
  }
137

138
  abs(value: string): string {
139
    return `ABS(${value})`;
×
140
  }
141

142
  sqrt(value: string): string {
143
    return `SQRT(${value})`;
×
144
  }
145

146
  power(base: string, exponent: string): string {
147
    return `POWER(${base}, ${exponent})`;
×
148
  }
149

150
  exp(value: string): string {
151
    return `EXP(${value})`;
×
152
  }
153

154
  log(value: string, base?: string): string {
155
    if (base) {
×
156
      // SQLite LOG is base-10, convert to natural log: ln(value) / ln(base)
157
      return `(LOG(${value}) * 2.302585092994046 / (LOG(${base}) * 2.302585092994046))`;
×
158
    }
159
    // SQLite LOG is base-10, convert to natural log: LOG(value) * ln(10)
160
    return `(LOG(${value}) * 2.302585092994046)`;
×
161
  }
162

163
  mod(dividend: string, divisor: string): string {
164
    return `(${dividend} % ${divisor})`;
×
165
  }
166

167
  value(text: string): string {
168
    return `CAST(${text} AS REAL)`;
×
169
  }
170

171
  // Text Functions
172
  concatenate(params: string[]): string {
173
    return `(${params.map((p) => `COALESCE(${p}, '')`).join(' || ')})`;
×
174
  }
175

176
  stringConcat(left: string, right: string): string {
177
    return `(COALESCE(${left}, '') || COALESCE(${right}, ''))`;
×
178
  }
179

180
  find(searchText: string, withinText: string, startNum?: string): string {
181
    if (startNum) {
×
182
      return `CASE WHEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) > 0 THEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) + ${startNum} - 1 ELSE 0 END`;
×
183
    }
184
    return `INSTR(${withinText}, ${searchText})`;
×
185
  }
186

187
  search(searchText: string, withinText: string, startNum?: string): string {
188
    // Case-insensitive search
189
    if (startNum) {
×
190
      return `CASE WHEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) > 0 THEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) + ${startNum} - 1 ELSE 0 END`;
×
191
    }
192
    return `INSTR(UPPER(${withinText}), UPPER(${searchText}))`;
×
193
  }
194

195
  mid(text: string, startNum: string, numChars: string): string {
196
    return `SUBSTR(${text}, ${startNum}, ${numChars})`;
×
197
  }
198

199
  left(text: string, numChars: string): string {
200
    return `SUBSTR(${text}, 1, ${numChars})`;
×
201
  }
202

203
  right(text: string, numChars: string): string {
204
    return `SUBSTR(${text}, -${numChars})`;
×
205
  }
206

207
  replace(oldText: string, startNum: string, numChars: string, newText: string): string {
208
    return `(SUBSTR(${oldText}, 1, ${startNum} - 1) || ${newText} || SUBSTR(${oldText}, ${startNum} + ${numChars}))`;
×
209
  }
210

211
  regexpReplace(text: string, pattern: string, replacement: string): string {
212
    // SQLite has limited regex support, use REPLACE for simple cases
213
    return `REPLACE(${text}, ${pattern}, ${replacement})`;
×
214
  }
215

216
  substitute(text: string, oldText: string, newText: string, instanceNum?: string): string {
217
    // SQLite doesn't support replacing specific instances easily
218
    return `REPLACE(${text}, ${oldText}, ${newText})`;
×
219
  }
220

221
  lower(text: string): string {
222
    return `LOWER(${text})`;
×
223
  }
224

225
  upper(text: string): string {
226
    return `UPPER(${text})`;
×
227
  }
228

229
  rept(text: string, numTimes: string): string {
230
    // SQLite doesn't have REPEAT, implement with recursive CTE or simple approach
231
    return `REPLACE(HEX(ZEROBLOB(${numTimes})), '00', ${text})`;
×
232
  }
233

234
  trim(text: string): string {
235
    return `TRIM(${text})`;
×
236
  }
237

238
  len(text: string): string {
239
    return `LENGTH(${text})`;
×
240
  }
241

242
  t(value: string): string {
243
    // SQLite T function should return numbers as numbers, not strings
244
    return `CASE WHEN ${value} IS NULL THEN '' WHEN typeof(${value}) = 'text' THEN ${value} ELSE ${value} END`;
×
245
  }
246

247
  encodeUrlComponent(text: string): string {
248
    // SQLite doesn't have built-in URL encoding
249
    return `${text}`;
×
250
  }
251

252
  // DateTime Functions - More flexible in SELECT context
253
  now(): string {
254
    return `DATETIME('now')`;
×
255
  }
256

257
  private normalizeDateModifier(unitLiteral: string): {
258
    unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
259
    factor: number;
260
  } {
261
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
52✔
262
    switch (normalized) {
52✔
263
      case 'millisecond':
264
      case 'milliseconds':
265
      case 'ms':
266
        return { unit: 'seconds', factor: 0.001 };
6✔
267
      case 'second':
268
      case 'seconds':
269
      case 's':
270
      case 'sec':
271
      case 'secs':
272
        return { unit: 'seconds', factor: 1 };
8✔
273
      case 'minute':
274
      case 'minutes':
275
      case 'min':
276
      case 'mins':
277
        return { unit: 'minutes', factor: 1 };
8✔
278
      case 'hour':
279
      case 'hours':
280
      case 'h':
281
      case 'hr':
282
      case 'hrs':
283
        return { unit: 'hours', factor: 1 };
10✔
284
      case 'week':
285
      case 'weeks':
286
        return { unit: 'days', factor: 7 };
4✔
287
      case 'month':
288
      case 'months':
289
        return { unit: 'months', factor: 1 };
4✔
290
      case 'quarter':
291
      case 'quarters':
292
        return { unit: 'months', factor: 3 };
4✔
293
      case 'year':
294
      case 'years':
295
        return { unit: 'years', factor: 1 };
4✔
296
      case 'day':
297
      case 'days':
298
      default:
299
        return { unit: 'days', factor: 1 };
4✔
300
    }
301
  }
302

303
  private normalizeDiffUnit(
304
    unitLiteral: string
305
  ): 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' {
306
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
52✔
307
    switch (normalized) {
52✔
308
      case 'millisecond':
309
      case 'milliseconds':
310
      case 'ms':
311
        return 'millisecond';
6✔
312
      case 'second':
313
      case 'seconds':
314
      case 's':
315
      case 'sec':
316
      case 'secs':
317
        return 'second';
12✔
318
      case 'minute':
319
      case 'minutes':
320
      case 'min':
321
      case 'mins':
322
        return 'minute';
8✔
323
      case 'hour':
324
      case 'hours':
325
      case 'h':
326
      case 'hr':
327
      case 'hrs':
328
        return 'hour';
12✔
329
      case 'week':
330
      case 'weeks':
331
        return 'week';
4✔
332
      case 'month':
333
      case 'months':
334
        return 'month';
×
335
      case 'quarter':
336
      case 'quarters':
337
        return 'quarter';
×
338
      case 'year':
339
      case 'years':
340
        return 'year';
×
341
      default:
342
        return 'day';
10✔
343
    }
344
  }
345

346
  private normalizeTruncateFormat(unitLiteral: string): string {
347
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
50✔
348
    switch (normalized) {
50✔
349
      case 'millisecond':
350
      case 'milliseconds':
351
      case 'ms':
352
      case 'second':
353
      case 'seconds':
354
      case 's':
355
      case 'sec':
356
      case 'secs':
357
        return '%Y-%m-%d %H:%M:%S';
16✔
358
      case 'minute':
359
      case 'minutes':
360
      case 'min':
361
      case 'mins':
362
        return '%Y-%m-%d %H:%M';
8✔
363
      case 'hour':
364
      case 'hours':
365
      case 'h':
366
      case 'hr':
367
      case 'hrs':
368
        return '%Y-%m-%d %H';
10✔
369
      case 'week':
370
      case 'weeks':
371
        return '%Y-%W';
4✔
372
      case 'month':
373
      case 'months':
374
        return '%Y-%m';
4✔
375
      case 'year':
376
      case 'years':
377
        return '%Y';
4✔
378
      case 'day':
379
      case 'days':
380
      default:
381
        return '%Y-%m-%d';
4✔
382
    }
383
  }
384

385
  today(): string {
386
    return `DATE('now')`;
×
387
  }
388

389
  dateAdd(date: string, count: string, unit: string): string {
390
    const { unit: modifierUnit, factor } = this.normalizeDateModifier(unit);
52✔
391
    const scaledCount = factor === 1 ? `(${count})` : `(${count}) * ${factor}`;
52✔
392
    return `DATETIME(${date}, (${scaledCount}) || ' ${modifierUnit}')`;
52✔
393
  }
394

395
  datestr(date: string): string {
396
    return `DATE(${date})`;
×
397
  }
398

399
  private buildMonthDiff(startDate: string, endDate: string): string {
400
    const startYear = `CAST(STRFTIME('%Y', ${startDate}) AS INTEGER)`;
×
401
    const endYear = `CAST(STRFTIME('%Y', ${endDate}) AS INTEGER)`;
×
402
    const startMonth = `CAST(STRFTIME('%m', ${startDate}) AS INTEGER)`;
×
403
    const endMonth = `CAST(STRFTIME('%m', ${endDate}) AS INTEGER)`;
×
404
    const startDay = `CAST(STRFTIME('%d', ${startDate}) AS INTEGER)`;
×
405
    const endDay = `CAST(STRFTIME('%d', ${endDate}) AS INTEGER)`;
×
406
    const startLastDay = `CAST(STRFTIME('%d', DATE(${startDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
407
    const endLastDay = `CAST(STRFTIME('%d', DATE(${endDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
408

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

413
    return `(${baseMonths} - ${adjustDown} + ${adjustUp})`;
×
414
  }
415

416
  datetimeDiff(startDate: string, endDate: string, unit: string): string {
417
    const baseDiffDays = `(JULIANDAY(${startDate}) - JULIANDAY(${endDate}))`;
42✔
418
    switch (this.normalizeDiffUnit(unit)) {
42✔
419
      case 'millisecond':
420
        return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;
6✔
421
      case 'second':
422
        return `(${baseDiffDays}) * 24.0 * 60 * 60`;
10✔
423
      case 'minute':
424
        return `(${baseDiffDays}) * 24.0 * 60`;
8✔
425
      case 'hour':
426
        return `(${baseDiffDays}) * 24.0`;
10✔
427
      case 'week':
428
        return `(${baseDiffDays}) / 7.0`;
4✔
429
      case 'month':
430
        return this.buildMonthDiff(startDate, endDate);
×
431
      case 'quarter':
432
        return `${this.buildMonthDiff(startDate, endDate)} / 3.0`;
×
433
      case 'year': {
434
        const monthDiff = this.buildMonthDiff(startDate, endDate);
×
435
        return `CAST((${monthDiff}) / 12.0 AS INTEGER)`;
×
436
      }
437
      case 'day':
438
      default:
439
        return `${baseDiffDays}`;
4✔
440
    }
441
  }
442

443
  datetimeFormat(date: string, format: string): string {
444
    return `STRFTIME(${format}, ${date})`;
×
445
  }
446

447
  datetimeParse(dateString: string, _format?: string): string {
448
    // SQLite doesn't have direct parsing with custom formats
449
    return `DATETIME(${dateString})`;
×
450
  }
451

452
  day(date: string): string {
453
    return `CAST(STRFTIME('%d', ${date}) AS INTEGER)`;
×
454
  }
455

456
  private buildNowDiffByUnit(nowExpr: string, dateExpr: string, unit: string): string {
457
    const baseDiffDays = `(JULIANDAY(${nowExpr}) - JULIANDAY(${dateExpr}))`;
10✔
458
    switch (this.normalizeDiffUnit(unit)) {
10✔
459
      case 'millisecond':
NEW
460
        return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;
×
461
      case 'second':
462
        return `(${baseDiffDays}) * 24.0 * 60 * 60`;
2✔
463
      case 'minute':
NEW
464
        return `(${baseDiffDays}) * 24.0 * 60`;
×
465
      case 'hour':
466
        return `(${baseDiffDays}) * 24.0`;
2✔
467
      case 'week':
NEW
468
        return `(${baseDiffDays}) / 7.0`;
×
469
      case 'month':
NEW
470
        return this.buildMonthDiff(nowExpr, dateExpr);
×
471
      case 'quarter':
NEW
472
        return `${this.buildMonthDiff(nowExpr, dateExpr)} / 3.0`;
×
473
      case 'year': {
NEW
474
        const monthDiff = this.buildMonthDiff(nowExpr, dateExpr);
×
NEW
475
        return `CAST((${monthDiff}) / 12.0 AS INTEGER)`;
×
476
      }
477
      case 'day':
478
      default:
479
        return `${baseDiffDays}`;
6✔
480
    }
481
  }
482

483
  fromNow(date: string, unit = 'day'): string {
484
    return this.buildNowDiffByUnit("'now'", `DATETIME(${date})`, unit);
10✔
485
  }
486

487
  hour(date: string): string {
488
    return `CAST(STRFTIME('%H', ${date}) AS INTEGER)`;
×
489
  }
490

491
  isAfter(date1: string, date2: string): string {
492
    return `DATETIME(${date1}) > DATETIME(${date2})`;
×
493
  }
494

495
  isBefore(date1: string, date2: string): string {
496
    return `DATETIME(${date1}) < DATETIME(${date2})`;
×
497
  }
498

499
  isSame(date1: string, date2: string, unit?: string): string {
500
    if (unit) {
50✔
501
      const trimmed = unit.trim();
50✔
502
      if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
50✔
503
        const format = this.normalizeTruncateFormat(trimmed.slice(1, -1));
50✔
504
        return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
50✔
505
      }
506
      const format = this.normalizeTruncateFormat(unit);
×
507
      return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
×
508
    }
509
    return `DATETIME(${date1}) = DATETIME(${date2})`;
×
510
  }
511

512
  lastModifiedTime(): string {
513
    return this.qualifySystemColumn('__last_modified_time');
×
514
  }
515

516
  minute(date: string): string {
517
    return `CAST(STRFTIME('%M', ${date}) AS INTEGER)`;
×
518
  }
519

520
  month(date: string): string {
521
    return `CAST(STRFTIME('%m', ${date}) AS INTEGER)`;
×
522
  }
523

524
  second(date: string): string {
525
    return `CAST(STRFTIME('%S', ${date}) AS INTEGER)`;
×
526
  }
527

528
  timestr(date: string): string {
529
    return `TIME(${date})`;
×
530
  }
531

532
  toNow(date: string, unit = 'day'): string {
533
    return this.fromNow(date, unit);
2✔
534
  }
535

536
  weekNum(date: string): string {
537
    return `CAST(STRFTIME('%W', ${date}) AS INTEGER)`;
×
538
  }
539

540
  weekday(date: string, startDayOfWeek?: string): string {
541
    // SQLite STRFTIME('%w') returns 0-6 (Sunday=0), but we need 1-7 (Sunday=1)
NEW
542
    const weekdaySql = `CAST(STRFTIME('%w', ${date}) AS INTEGER) + 1`;
×
NEW
543
    if (!startDayOfWeek) {
×
NEW
544
      return weekdaySql;
×
545
    }
546

NEW
547
    const normalizedStartDay = `LOWER(TRIM(COALESCE(CAST(${startDayOfWeek} AS TEXT), '')))`;
×
NEW
548
    const mondayWeekdaySql = `(CASE WHEN (${weekdaySql}) = 1 THEN 7 ELSE (${weekdaySql}) - 1 END)`;
×
NEW
549
    return `CASE WHEN ${normalizedStartDay} = 'monday' THEN ${mondayWeekdaySql} ELSE ${weekdaySql} END`;
×
550
  }
551

552
  workday(startDate: string, days: string): string {
553
    // Simplified implementation
554
    return `DATE(${startDate}, '+' || ${days} || ' days')`;
×
555
  }
556

557
  workdayDiff(startDate: string, endDate: string): string {
558
    return `CAST((JULIANDAY(${endDate}) - JULIANDAY(${startDate})) AS INTEGER)`;
×
559
  }
560

561
  year(date: string): string {
562
    return `CAST(STRFTIME('%Y', ${date}) AS INTEGER)`;
×
563
  }
564

565
  createdTime(): string {
566
    return this.qualifySystemColumn('__created_time');
×
567
  }
568

569
  // Logical Functions
570
  private truthinessScore(value: string): string {
571
    const wrapped = `(${value})`;
×
572
    const valueType = `TYPEOF${wrapped}`;
×
573
    return `CASE
×
574
      WHEN ${wrapped} IS NULL THEN 0
575
      WHEN ${valueType} = 'integer' OR ${valueType} = 'real' THEN (${wrapped}) != 0
576
      WHEN ${valueType} = 'text' THEN (${wrapped} != '' AND LOWER(${wrapped}) != 'null')
577
      ELSE (${wrapped}) IS NOT NULL AND ${wrapped} != 'null'
578
    END`;
579
  }
580

581
  if(condition: string, valueIfTrue: string, valueIfFalse: string): string {
582
    const truthiness = this.truthinessScore(condition);
×
583
    return `CASE WHEN (${truthiness}) = 1 THEN ${valueIfTrue} ELSE ${valueIfFalse} END`;
×
584
  }
585

586
  and(params: string[]): string {
587
    return `(${params.map((p) => `(${p})`).join(' AND ')})`;
×
588
  }
589

590
  or(params: string[]): string {
591
    return `(${params.map((p) => `(${p})`).join(' OR ')})`;
×
592
  }
593

594
  not(value: string): string {
595
    return `NOT (${value})`;
×
596
  }
597

598
  xor(params: string[]): string {
599
    if (params.length === 2) {
×
600
      return `((${params[0]}) AND NOT (${params[1]})) OR (NOT (${params[0]}) AND (${params[1]}))`;
×
601
    }
602
    return `(${params.map((p) => `CASE WHEN ${p} THEN 1 ELSE 0 END`).join(' + ')}) % 2 = 1`;
×
603
  }
604

605
  blank(): string {
606
    // SQLite BLANK function should return null instead of empty string
607
    return `NULL`;
×
608
  }
609

610
  error(_message: string): string {
611
    // SQLite doesn't have a direct error function, use a failing expression
612
    return `(1/0)`;
×
613
  }
614

615
  isError(_value: string): string {
616
    return `0`;
×
617
  }
618

619
  switch(
620
    expression: string,
621
    cases: Array<{ case: string; result: string }>,
622
    defaultResult?: string
623
  ): string {
624
    let sql = `CASE ${expression}`;
×
625
    for (const caseItem of cases) {
×
626
      sql += ` WHEN ${caseItem.case} THEN ${caseItem.result}`;
×
627
    }
628
    if (defaultResult) {
×
629
      sql += ` ELSE ${defaultResult}`;
×
630
    }
631
    sql += ` END`;
×
632
    return sql;
×
633
  }
634

635
  // Array Functions - Limited in SQLite
636
  count(params: string[]): string {
637
    return `COUNT(${this.joinParams(params)})`;
×
638
  }
639

640
  countA(params: string[]): string {
641
    return `COUNT(${this.joinParams(params.map((p) => `CASE WHEN ${p} IS NOT NULL THEN 1 END`))})`;
×
642
  }
643

644
  countAll(value: string): string {
645
    const paramInfo = this.getParamInfo(0);
4✔
646
    if (paramInfo.isJsonField || paramInfo.isMultiValueField) {
4✔
647
      const baseExpr =
648
        paramInfo.isFieldReference && paramInfo.fieldDbName
2✔
649
          ? this.tableAlias
650
            ? `"${this.tableAlias}"."${paramInfo.fieldDbName}"`
651
            : `"${paramInfo.fieldDbName}"`
652
          : value;
653
      return `CASE
2✔
654
        WHEN ${baseExpr} IS NULL THEN 0
655
        WHEN json_valid(${baseExpr}) AND json_type(${baseExpr}) = 'array' THEN COALESCE(json_array_length(${baseExpr}), 0)
656
        WHEN json_valid(${baseExpr}) AND json_type(${baseExpr}) = 'null' THEN 0
657
        ELSE 1
658
      END`;
659
    }
660

661
    return `CASE WHEN ${value} IS NULL THEN 0 ELSE 1 END`;
2✔
662
  }
663

664
  private buildJsonArrayUnion(
665
    arrays: string[],
666
    opts?: { filterNulls?: boolean; withOrdinal?: boolean }
667
  ): string {
668
    const selects = arrays.map((array, index) => {
×
669
      const base = `SELECT value, ${index} AS arg_index, CAST(key AS INTEGER) AS ord FROM json_each(COALESCE(${array}, '[]'))`;
×
670
      const whereClause = opts?.filterNulls
×
671
        ? " WHERE value IS NOT NULL AND value != 'null' AND value != ''"
672
        : '';
673
      return `${base}${whereClause}`;
×
674
    });
675

676
    if (selects.length === 0) {
×
677
      return 'SELECT NULL AS value, 0 AS arg_index, 0 AS ord WHERE 0';
×
678
    }
679

680
    return selects.join(' UNION ALL ');
×
681
  }
682

683
  arrayJoin(array: string, separator?: string): string {
684
    const sep = separator || ',';
×
685
    // SQLite JSON array join using json_each with stable ordering by key
686
    return `(SELECT GROUP_CONCAT(value, ${sep}) FROM json_each(${array}) ORDER BY key)`;
×
687
  }
688

689
  arrayUnique(arrays: string[]): string {
690
    const unionQuery = this.buildJsonArrayUnion(arrays, { withOrdinal: true, filterNulls: true });
×
691
    return `COALESCE(
×
692
      '[' || (
693
        SELECT GROUP_CONCAT(json_quote(value))
694
        FROM (
695
          SELECT value, ROW_NUMBER() OVER (PARTITION BY value ORDER BY arg_index, ord) AS rn, arg_index, ord
696
          FROM (${unionQuery}) AS combined
697
        )
698
        WHERE rn = 1
699
        ORDER BY arg_index, ord
700
      ) || ']',
701
      '[]'
702
    )`;
703
  }
704

705
  arrayFlatten(arrays: string[]): string {
706
    const unionQuery = this.buildJsonArrayUnion(arrays, { withOrdinal: true });
×
707
    return `COALESCE(
×
708
      '[' || (
709
        SELECT GROUP_CONCAT(json_quote(value))
710
        FROM (${unionQuery}) AS combined
711
        ORDER BY arg_index, ord
712
      ) || ']',
713
      '[]'
714
    )`;
715
  }
716

717
  arrayCompact(arrays: string[]): string {
718
    const unionQuery = this.buildJsonArrayUnion(arrays, {
×
719
      filterNulls: true,
720
      withOrdinal: true,
721
    });
722
    return `COALESCE(
×
723
      '[' || (
724
        SELECT GROUP_CONCAT(json_quote(value))
725
        FROM (${unionQuery}) AS combined
726
        ORDER BY arg_index, ord
727
      ) || ']',
728
      '[]'
729
    )`;
730
  }
731

732
  // System Functions
733
  recordId(): string {
734
    return this.qualifySystemColumn('__id');
×
735
  }
736

737
  autoNumber(): string {
738
    return this.qualifySystemColumn('__auto_number');
×
739
  }
740

741
  textAll(value: string): string {
742
    return `CAST(${value} AS TEXT)`;
×
743
  }
744

745
  // Binary Operations
746
  add(left: string, right: string): string {
747
    return `(${left} + ${right})`;
×
748
  }
749

750
  subtract(left: string, right: string): string {
751
    return `(${left} - ${right})`;
×
752
  }
753

754
  multiply(left: string, right: string): string {
755
    return `(${left} * ${right})`;
×
756
  }
757

758
  divide(left: string, right: string): string {
759
    return `(${left} / ${right})`;
×
760
  }
761

762
  modulo(left: string, right: string): string {
763
    return `(${left} % ${right})`;
×
764
  }
765

766
  // Comparison Operations
767
  equal(left: string, right: string): string {
768
    return this.buildBlankAwareComparison('=', left, right);
×
769
  }
770

771
  notEqual(left: string, right: string): string {
772
    return this.buildBlankAwareComparison('<>', left, right);
×
773
  }
774

775
  greaterThan(left: string, right: string): string {
776
    return `(${left} > ${right})`;
×
777
  }
778

779
  lessThan(left: string, right: string): string {
780
    return `(${left} < ${right})`;
×
781
  }
782

783
  greaterThanOrEqual(left: string, right: string): string {
784
    return `(${left} >= ${right})`;
×
785
  }
786

787
  lessThanOrEqual(left: string, right: string): string {
788
    return `(${left} <= ${right})`;
×
789
  }
790

791
  // Logical Operations
792
  logicalAnd(left: string, right: string): string {
793
    return `(${left} AND ${right})`;
×
794
  }
795

796
  logicalOr(left: string, right: string): string {
797
    return `(${left} OR ${right})`;
×
798
  }
799

800
  bitwiseAnd(left: string, right: string): string {
801
    return `(${left} & ${right})`;
×
802
  }
803

804
  // Unary Operations
805
  unaryMinus(value: string): string {
806
    return `(-${value})`;
×
807
  }
808

809
  // Field Reference
810
  fieldReference(_fieldId: string, columnName: string): string {
811
    return `"${columnName}"`;
×
812
  }
813

814
  // Literals
815
  stringLiteral(value: string): string {
816
    return `'${value.replace(/'/g, "''")}'`;
×
817
  }
818

819
  numberLiteral(value: number): string {
820
    return value.toString();
×
821
  }
822

823
  booleanLiteral(value: boolean): string {
824
    return value ? '1' : '0';
×
825
  }
826

827
  nullLiteral(): string {
828
    return 'NULL';
×
829
  }
830

831
  // Utility methods for type conversion and validation
832
  castToNumber(value: string): string {
833
    return `CAST(${value} AS REAL)`;
×
834
  }
835

836
  castToString(value: string): string {
837
    return `CAST(${value} AS TEXT)`;
×
838
  }
839

840
  castToBoolean(value: string): string {
841
    return `CASE WHEN ${value} THEN 1 ELSE 0 END`;
×
842
  }
843

844
  castToDate(value: string): string {
845
    return `DATETIME(${value})`;
×
846
  }
847

848
  // Handle null values and type checking
849
  isNull(value: string): string {
850
    return `${value} IS NULL`;
×
851
  }
852

853
  coalesce(params: string[]): string {
854
    return `COALESCE(${this.joinParams(params)})`;
×
855
  }
856

857
  // Parentheses for grouping
858
  parentheses(expression: string): string {
859
    return `(${expression})`;
×
860
  }
861
}
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