• 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

47.39
/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✔
2
import { SelectQueryAbstract } from '../select-query.abstract';
3

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

16
  private qualifySystemColumn(column: string): string {
2✔
17
    const quoted = `"${column}"`;
×
18
    const alias = this.tableAlias;
×
19
    return alias ? `"${alias}".${quoted}` : quoted;
×
20
  }
×
21

22
  private isEmptyStringLiteral(value: string): boolean {
2✔
23
    return value.trim() === "''";
×
24
  }
×
25

26
  private normalizeBlankComparable(value: string): string {
2✔
27
    return `COALESCE(NULLIF(CAST((${value}) AS TEXT), ''), '')`;
×
28
  }
×
29

30
  private buildBlankAwareComparison(operator: '=' | '<>', left: string, right: string): string {
2✔
31
    const shouldNormalize = this.isEmptyStringLiteral(left) || this.isEmptyStringLiteral(right);
×
32
    if (!shouldNormalize) {
×
33
      return `(${left} ${operator} ${right})`;
×
34
    }
×
35

36
    const normalizedLeft = this.isEmptyStringLiteral(left)
×
37
      ? "''"
×
38
      : this.normalizeBlankComparable(left);
×
39
    const normalizedRight = this.isEmptyStringLiteral(right)
×
40
      ? "''"
×
41
      : this.normalizeBlankComparable(right);
×
42

43
    return `(${normalizedLeft} ${operator} ${normalizedRight})`;
×
44
  }
×
45

46
  private coalesceNumeric(expr: string): string {
2✔
47
    return `COALESCE(CAST((${expr}) AS REAL), 0)`;
5✔
48
  }
5✔
49

50
  // Numeric Functions
2✔
51
  sum(params: string[]): string {
2✔
52
    if (params.length === 0) {
2!
53
      return '0';
×
54
    }
×
55
    const terms = params.map((param) => this.coalesceNumeric(param));
2✔
56
    if (terms.length === 1) {
2!
57
      return terms[0];
×
58
    }
×
59
    return `(${terms.join(' + ')})`;
2✔
60
  }
2✔
61

62
  average(params: string[]): string {
2✔
63
    if (params.length === 0) {
1!
64
      return '0';
×
65
    }
×
66
    const numerator = this.sum(params);
1✔
67
    return `(${numerator}) / ${params.length}`;
1✔
68
  }
1✔
69

70
  max(params: string[]): string {
2✔
71
    return `MAX(${this.joinParams(params)})`;
×
72
  }
×
73

74
  min(params: string[]): string {
2✔
75
    return `MIN(${this.joinParams(params)})`;
×
76
  }
×
77

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

85
  roundUp(value: string, precision?: string): string {
2✔
86
    // SQLite doesn't have CEIL with precision, implement manually
×
87
    if (precision) {
×
88
      return `CAST(CEIL(${value} * POWER(10, ${precision})) / POWER(10, ${precision}) AS REAL)`;
×
89
    }
×
90
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
91
  }
×
92

93
  roundDown(value: string, precision?: string): string {
2✔
94
    // SQLite doesn't have FLOOR with precision, implement manually
×
95
    if (precision) {
×
96
      return `CAST(FLOOR(${value} * POWER(10, ${precision})) / POWER(10, ${precision}) AS REAL)`;
×
97
    }
×
98
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
99
  }
×
100

101
  ceiling(value: string): string {
2✔
102
    return `CAST(CEIL(${value}) AS INTEGER)`;
×
103
  }
×
104

105
  floor(value: string): string {
2✔
106
    return `CAST(FLOOR(${value}) AS INTEGER)`;
×
107
  }
×
108

109
  even(value: string): string {
2✔
110
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 0 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
111
  }
×
112

113
  odd(value: string): string {
2✔
114
    return `CASE WHEN CAST(${value} AS INTEGER) % 2 = 1 THEN CAST(${value} AS INTEGER) ELSE CAST(${value} AS INTEGER) + 1 END`;
×
115
  }
×
116

117
  int(value: string): string {
2✔
118
    return `CAST(${value} AS INTEGER)`;
×
119
  }
×
120

121
  abs(value: string): string {
2✔
122
    return `ABS(${value})`;
×
123
  }
×
124

125
  sqrt(value: string): string {
2✔
126
    return `SQRT(${value})`;
×
127
  }
×
128

129
  power(base: string, exponent: string): string {
2✔
130
    return `POWER(${base}, ${exponent})`;
×
131
  }
×
132

133
  exp(value: string): string {
2✔
134
    return `EXP(${value})`;
×
135
  }
×
136

137
  log(value: string, base?: string): string {
2✔
138
    if (base) {
×
139
      // SQLite LOG is base-10, convert to natural log: ln(value) / ln(base)
×
140
      return `(LOG(${value}) * 2.302585092994046 / (LOG(${base}) * 2.302585092994046))`;
×
141
    }
×
142
    // SQLite LOG is base-10, convert to natural log: LOG(value) * ln(10)
×
143
    return `(LOG(${value}) * 2.302585092994046)`;
×
144
  }
×
145

146
  mod(dividend: string, divisor: string): string {
2✔
147
    return `(${dividend} % ${divisor})`;
×
148
  }
×
149

150
  value(text: string): string {
2✔
151
    return `CAST(${text} AS REAL)`;
×
152
  }
×
153

154
  // Text Functions
2✔
155
  concatenate(params: string[]): string {
2✔
156
    return `(${params.map((p) => `COALESCE(${p}, '')`).join(' || ')})`;
×
157
  }
×
158

159
  stringConcat(left: string, right: string): string {
2✔
160
    return `(COALESCE(${left}, '') || COALESCE(${right}, ''))`;
×
161
  }
×
162

163
  find(searchText: string, withinText: string, startNum?: string): string {
2✔
164
    if (startNum) {
×
165
      return `CASE WHEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) > 0 THEN INSTR(SUBSTR(${withinText}, ${startNum}), ${searchText}) + ${startNum} - 1 ELSE 0 END`;
×
166
    }
×
167
    return `INSTR(${withinText}, ${searchText})`;
×
168
  }
×
169

170
  search(searchText: string, withinText: string, startNum?: string): string {
2✔
171
    // Case-insensitive search
×
172
    if (startNum) {
×
173
      return `CASE WHEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) > 0 THEN INSTR(UPPER(SUBSTR(${withinText}, ${startNum})), UPPER(${searchText})) + ${startNum} - 1 ELSE 0 END`;
×
174
    }
×
175
    return `INSTR(UPPER(${withinText}), UPPER(${searchText}))`;
×
176
  }
×
177

178
  mid(text: string, startNum: string, numChars: string): string {
2✔
179
    return `SUBSTR(${text}, ${startNum}, ${numChars})`;
×
180
  }
×
181

182
  left(text: string, numChars: string): string {
2✔
183
    return `SUBSTR(${text}, 1, ${numChars})`;
×
184
  }
×
185

186
  right(text: string, numChars: string): string {
2✔
187
    return `SUBSTR(${text}, -${numChars})`;
×
188
  }
×
189

190
  replace(oldText: string, startNum: string, numChars: string, newText: string): string {
2✔
191
    return `(SUBSTR(${oldText}, 1, ${startNum} - 1) || ${newText} || SUBSTR(${oldText}, ${startNum} + ${numChars}))`;
×
192
  }
×
193

194
  regexpReplace(text: string, pattern: string, replacement: string): string {
2✔
195
    // SQLite has limited regex support, use REPLACE for simple cases
×
196
    return `REPLACE(${text}, ${pattern}, ${replacement})`;
×
197
  }
×
198

199
  substitute(text: string, oldText: string, newText: string, instanceNum?: string): string {
2✔
200
    // SQLite doesn't support replacing specific instances easily
×
201
    return `REPLACE(${text}, ${oldText}, ${newText})`;
×
202
  }
×
203

204
  lower(text: string): string {
2✔
205
    return `LOWER(${text})`;
×
206
  }
×
207

208
  upper(text: string): string {
2✔
209
    return `UPPER(${text})`;
×
210
  }
×
211

212
  rept(text: string, numTimes: string): string {
2✔
213
    // SQLite doesn't have REPEAT, implement with recursive CTE or simple approach
×
214
    return `REPLACE(HEX(ZEROBLOB(${numTimes})), '00', ${text})`;
×
215
  }
×
216

217
  trim(text: string): string {
2✔
218
    return `TRIM(${text})`;
×
219
  }
×
220

221
  len(text: string): string {
2✔
222
    return `LENGTH(${text})`;
×
223
  }
×
224

225
  t(value: string): string {
2✔
226
    // SQLite T function should return numbers as numbers, not strings
×
227
    return `CASE WHEN ${value} IS NULL THEN '' WHEN typeof(${value}) = 'text' THEN ${value} ELSE ${value} END`;
×
228
  }
×
229

230
  encodeUrlComponent(text: string): string {
2✔
231
    // SQLite doesn't have built-in URL encoding
×
232
    return `${text}`;
×
233
  }
×
234

235
  // DateTime Functions - More flexible in SELECT context
2✔
236
  now(): string {
2✔
237
    return `DATETIME('now')`;
×
238
  }
×
239

240
  private normalizeDateModifier(unitLiteral: string): {
2✔
241
    unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
242
    factor: number;
243
  } {
25✔
244
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
25✔
245
    switch (normalized) {
25✔
246
      case 'millisecond':
25✔
247
      case 'milliseconds':
25✔
248
      case 'ms':
25✔
249
        return { unit: 'seconds', factor: 0.001 };
3✔
250
      case 'second':
25✔
251
      case 'seconds':
25✔
252
      case 'sec':
25✔
253
      case 'secs':
25✔
254
        return { unit: 'seconds', factor: 1 };
4✔
255
      case 'minute':
25✔
256
      case 'minutes':
25✔
257
      case 'min':
25✔
258
      case 'mins':
25✔
259
        return { unit: 'minutes', factor: 1 };
4✔
260
      case 'hour':
25✔
261
      case 'hours':
25✔
262
      case 'hr':
25✔
263
      case 'hrs':
25✔
264
        return { unit: 'hours', factor: 1 };
4✔
265
      case 'week':
25✔
266
      case 'weeks':
25✔
267
        return { unit: 'days', factor: 7 };
2✔
268
      case 'month':
25✔
269
      case 'months':
25✔
270
        return { unit: 'months', factor: 1 };
2✔
271
      case 'quarter':
25✔
272
      case 'quarters':
25✔
273
        return { unit: 'months', factor: 3 };
2✔
274
      case 'year':
25✔
275
      case 'years':
25✔
276
        return { unit: 'years', factor: 1 };
2✔
277
      case 'day':
25✔
278
      case 'days':
25✔
279
      default:
25✔
280
        return { unit: 'days', factor: 1 };
2✔
281
    }
25✔
282
  }
25✔
283

284
  private normalizeDiffUnit(
2✔
285
    unitLiteral: string
19✔
286
  ): 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' {
19✔
287
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
19✔
288
    switch (normalized) {
19✔
289
      case 'millisecond':
19✔
290
      case 'milliseconds':
19✔
291
      case 'ms':
19✔
292
        return 'millisecond';
3✔
293
      case 'second':
19✔
294
      case 'seconds':
19✔
295
      case 'sec':
19✔
296
      case 'secs':
19✔
297
        return 'second';
4✔
298
      case 'minute':
19✔
299
      case 'minutes':
19✔
300
      case 'min':
19✔
301
      case 'mins':
19✔
302
        return 'minute';
4✔
303
      case 'hour':
19✔
304
      case 'hours':
19✔
305
      case 'hr':
19✔
306
      case 'hrs':
19✔
307
        return 'hour';
4✔
308
      case 'week':
19✔
309
      case 'weeks':
19✔
310
        return 'week';
2✔
311
      case 'month':
19!
312
      case 'months':
19!
UNCOV
313
        return 'month';
×
314
      case 'quarter':
19!
315
      case 'quarters':
19!
UNCOV
316
        return 'quarter';
×
317
      case 'year':
19!
318
      case 'years':
19!
UNCOV
319
        return 'year';
×
320
      default:
19✔
321
        return 'day';
2✔
322
    }
19✔
323
  }
19✔
324

325
  private normalizeTruncateFormat(unitLiteral: string): string {
2✔
326
    const normalized = unitLiteral.replace(/^'|'$/g, '').trim().toLowerCase();
23✔
327
    switch (normalized) {
23✔
328
      case 'millisecond':
23✔
329
      case 'milliseconds':
23✔
330
      case 'ms':
23✔
331
      case 'second':
23✔
332
      case 'seconds':
23✔
333
      case 'sec':
23✔
334
      case 'secs':
23✔
335
        return '%Y-%m-%d %H:%M:%S';
7✔
336
      case 'minute':
23✔
337
      case 'minutes':
23✔
338
      case 'min':
23✔
339
      case 'mins':
23✔
340
        return '%Y-%m-%d %H:%M';
4✔
341
      case 'hour':
23✔
342
      case 'hours':
23✔
343
      case 'hr':
23✔
344
      case 'hrs':
23✔
345
        return '%Y-%m-%d %H';
4✔
346
      case 'week':
23✔
347
      case 'weeks':
23✔
348
        return '%Y-%W';
2✔
349
      case 'month':
23✔
350
      case 'months':
23✔
351
        return '%Y-%m';
2✔
352
      case 'year':
23✔
353
      case 'years':
23✔
354
        return '%Y';
2✔
355
      case 'day':
23✔
356
      case 'days':
23✔
357
      default:
23✔
358
        return '%Y-%m-%d';
2✔
359
    }
23✔
360
  }
23✔
361

362
  today(): string {
2✔
UNCOV
363
    return `DATE('now')`;
×
364
  }
×
365

366
  dateAdd(date: string, count: string, unit: string): string {
2✔
367
    const { unit: modifierUnit, factor } = this.normalizeDateModifier(unit);
25✔
368
    const scaledCount = factor === 1 ? `(${count})` : `(${count}) * ${factor}`;
25✔
369
    return `DATETIME(${date}, (${scaledCount}) || ' ${modifierUnit}')`;
25✔
370
  }
25✔
371

372
  datestr(date: string): string {
2✔
UNCOV
373
    return `DATE(${date})`;
×
UNCOV
374
  }
×
375

376
  private buildMonthDiff(startDate: string, endDate: string): string {
2✔
UNCOV
377
    const startYear = `CAST(STRFTIME('%Y', ${startDate}) AS INTEGER)`;
×
UNCOV
378
    const endYear = `CAST(STRFTIME('%Y', ${endDate}) AS INTEGER)`;
×
UNCOV
379
    const startMonth = `CAST(STRFTIME('%m', ${startDate}) AS INTEGER)`;
×
UNCOV
380
    const endMonth = `CAST(STRFTIME('%m', ${endDate}) AS INTEGER)`;
×
UNCOV
381
    const startDay = `CAST(STRFTIME('%d', ${startDate}) AS INTEGER)`;
×
UNCOV
382
    const endDay = `CAST(STRFTIME('%d', ${endDate}) AS INTEGER)`;
×
UNCOV
383
    const startLastDay = `CAST(STRFTIME('%d', DATE(${startDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
UNCOV
384
    const endLastDay = `CAST(STRFTIME('%d', DATE(${endDate}, 'start of month', '+1 month', '-1 day')) AS INTEGER)`;
×
385

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

UNCOV
390
    return `(${baseMonths} - ${adjustDown} + ${adjustUp})`;
×
391
  }
×
392

393
  datetimeDiff(startDate: string, endDate: string, unit: string): string {
2✔
394
    const baseDiffDays = `(JULIANDAY(${startDate}) - JULIANDAY(${endDate}))`;
19✔
395
    switch (this.normalizeDiffUnit(unit)) {
19✔
396
      case 'millisecond':
19✔
397
        return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;
3✔
398
      case 'second':
19✔
399
        return `(${baseDiffDays}) * 24.0 * 60 * 60`;
4✔
400
      case 'minute':
19✔
401
        return `(${baseDiffDays}) * 24.0 * 60`;
4✔
402
      case 'hour':
19✔
403
        return `(${baseDiffDays}) * 24.0`;
4✔
404
      case 'week':
19✔
405
        return `(${baseDiffDays}) / 7.0`;
2✔
406
      case 'month':
19!
UNCOV
407
        return this.buildMonthDiff(startDate, endDate);
×
408
      case 'quarter':
19!
409
        return `${this.buildMonthDiff(startDate, endDate)} / 3.0`;
×
410
      case 'year': {
19!
UNCOV
411
        const monthDiff = this.buildMonthDiff(startDate, endDate);
×
412
        return `CAST((${monthDiff}) / 12.0 AS INTEGER)`;
×
413
      }
×
414
      case 'day':
19✔
415
      default:
19✔
416
        return `${baseDiffDays}`;
2✔
417
    }
19✔
418
  }
19✔
419

420
  datetimeFormat(date: string, format: string): string {
2✔
UNCOV
421
    return `STRFTIME(${format}, ${date})`;
×
422
  }
×
423

424
  datetimeParse(dateString: string, _format?: string): string {
2✔
425
    // SQLite doesn't have direct parsing with custom formats
×
426
    return `DATETIME(${dateString})`;
×
UNCOV
427
  }
×
428

429
  day(date: string): string {
2✔
430
    return `CAST(STRFTIME('%d', ${date}) AS INTEGER)`;
×
UNCOV
431
  }
×
432

433
  fromNow(date: string): string {
2✔
434
    return `CAST((JULIANDAY('now') - JULIANDAY(${date})) * 86400 AS INTEGER)`;
×
UNCOV
435
  }
×
436

437
  hour(date: string): string {
2✔
438
    return `CAST(STRFTIME('%H', ${date}) AS INTEGER)`;
×
UNCOV
439
  }
×
440

441
  isAfter(date1: string, date2: string): string {
2✔
442
    return `DATETIME(${date1}) > DATETIME(${date2})`;
×
UNCOV
443
  }
×
444

445
  isBefore(date1: string, date2: string): string {
2✔
446
    return `DATETIME(${date1}) < DATETIME(${date2})`;
×
UNCOV
447
  }
×
448

449
  isSame(date1: string, date2: string, unit?: string): string {
2✔
450
    if (unit) {
23✔
451
      const trimmed = unit.trim();
23✔
452
      if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
23✔
453
        const format = this.normalizeTruncateFormat(trimmed.slice(1, -1));
23✔
454
        return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
23✔
455
      }
23!
UNCOV
456
      const format = this.normalizeTruncateFormat(unit);
×
457
      return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
×
458
    }
×
459
    return `DATETIME(${date1}) = DATETIME(${date2})`;
×
UNCOV
460
  }
×
461

462
  lastModifiedTime(): string {
2✔
463
    return this.qualifySystemColumn('__last_modified_time');
×
464
  }
×
465

466
  minute(date: string): string {
2✔
467
    return `CAST(STRFTIME('%M', ${date}) AS INTEGER)`;
×
468
  }
×
469

470
  month(date: string): string {
2✔
471
    return `CAST(STRFTIME('%m', ${date}) AS INTEGER)`;
×
472
  }
×
473

474
  second(date: string): string {
2✔
475
    return `CAST(STRFTIME('%S', ${date}) AS INTEGER)`;
×
476
  }
×
477

478
  timestr(date: string): string {
2✔
UNCOV
479
    return `TIME(${date})`;
×
480
  }
×
481

482
  toNow(date: string): string {
2✔
483
    return `CAST((JULIANDAY(${date}) - JULIANDAY('now')) * 86400 AS INTEGER)`;
×
484
  }
×
485

486
  weekNum(date: string): string {
2✔
487
    return `CAST(STRFTIME('%W', ${date}) AS INTEGER)`;
×
488
  }
×
489

490
  weekday(date: string): string {
2✔
491
    // SQLite STRFTIME('%w') returns 0-6 (Sunday=0), but we need 1-7 (Sunday=1)
×
492
    return `CAST(STRFTIME('%w', ${date}) AS INTEGER) + 1`;
×
493
  }
×
494

495
  workday(startDate: string, days: string): string {
2✔
496
    // Simplified implementation
×
497
    return `DATE(${startDate}, '+' || ${days} || ' days')`;
×
UNCOV
498
  }
×
499

500
  workdayDiff(startDate: string, endDate: string): string {
2✔
501
    return `CAST((JULIANDAY(${endDate}) - JULIANDAY(${startDate})) AS INTEGER)`;
×
UNCOV
502
  }
×
503

504
  year(date: string): string {
2✔
505
    return `CAST(STRFTIME('%Y', ${date}) AS INTEGER)`;
×
UNCOV
506
  }
×
507

508
  createdTime(): string {
2✔
509
    return this.qualifySystemColumn('__created_time');
×
510
  }
×
511

512
  // Logical Functions
2✔
513
  private truthinessScore(value: string): string {
2✔
UNCOV
514
    const wrapped = `(${value})`;
×
515
    const valueType = `TYPEOF${wrapped}`;
×
516
    return `CASE
×
517
      WHEN ${wrapped} IS NULL THEN 0
×
UNCOV
518
      WHEN ${valueType} = 'integer' OR ${valueType} = 'real' THEN (${wrapped}) != 0
×
UNCOV
519
      WHEN ${valueType} = 'text' THEN (${wrapped} != '' AND LOWER(${wrapped}) != 'null')
×
520
      ELSE (${wrapped}) IS NOT NULL AND ${wrapped} != 'null'
×
521
    END`;
×
522
  }
×
523

524
  if(condition: string, valueIfTrue: string, valueIfFalse: string): string {
2✔
525
    const truthiness = this.truthinessScore(condition);
×
526
    return `CASE WHEN (${truthiness}) = 1 THEN ${valueIfTrue} ELSE ${valueIfFalse} END`;
×
UNCOV
527
  }
×
528

529
  and(params: string[]): string {
2✔
530
    return `(${params.map((p) => `(${p})`).join(' AND ')})`;
×
531
  }
×
532

533
  or(params: string[]): string {
2✔
534
    return `(${params.map((p) => `(${p})`).join(' OR ')})`;
×
535
  }
×
536

537
  not(value: string): string {
2✔
538
    return `NOT (${value})`;
×
539
  }
×
540

541
  xor(params: string[]): string {
2✔
542
    if (params.length === 2) {
×
UNCOV
543
      return `((${params[0]}) AND NOT (${params[1]})) OR (NOT (${params[0]}) AND (${params[1]}))`;
×
UNCOV
544
    }
×
UNCOV
545
    return `(${params.map((p) => `CASE WHEN ${p} THEN 1 ELSE 0 END`).join(' + ')}) % 2 = 1`;
×
546
  }
×
547

548
  blank(): string {
2✔
UNCOV
549
    // SQLite BLANK function should return null instead of empty string
×
550
    return `NULL`;
×
551
  }
×
552

553
  error(_message: string): string {
2✔
554
    // SQLite doesn't have a direct error function, use a failing expression
×
555
    return `(1/0)`;
×
UNCOV
556
  }
×
557

558
  isError(_value: string): string {
2✔
559
    return `0`;
×
560
  }
×
561

562
  switch(
2✔
UNCOV
563
    expression: string,
×
564
    cases: Array<{ case: string; result: string }>,
×
565
    defaultResult?: string
×
566
  ): string {
×
UNCOV
567
    let sql = `CASE ${expression}`;
×
UNCOV
568
    for (const caseItem of cases) {
×
569
      sql += ` WHEN ${caseItem.case} THEN ${caseItem.result}`;
×
570
    }
×
571
    if (defaultResult) {
×
UNCOV
572
      sql += ` ELSE ${defaultResult}`;
×
UNCOV
573
    }
×
574
    sql += ` END`;
×
575
    return sql;
×
576
  }
×
577

578
  // Array Functions - Limited in SQLite
2✔
579
  count(params: string[]): string {
2✔
580
    return `COUNT(${this.joinParams(params)})`;
×
581
  }
×
582

583
  countA(params: string[]): string {
2✔
584
    return `COUNT(${this.joinParams(params.map((p) => `CASE WHEN ${p} IS NOT NULL THEN 1 END`))})`;
×
585
  }
×
586

587
  countAll(_value: string): string {
2✔
588
    return `COUNT(*)`;
×
589
  }
×
590

591
  arrayJoin(array: string, separator?: string): string {
2✔
UNCOV
592
    const sep = separator || ',';
×
593
    // SQLite JSON array join using json_each with stable ordering by key
×
594
    return `(SELECT GROUP_CONCAT(value, ${sep}) FROM json_each(${array}) ORDER BY key)`;
×
UNCOV
595
  }
×
596

597
  arrayUnique(array: string): string {
2✔
598
    // SQLite JSON array unique using json_each and DISTINCT
×
UNCOV
599
    return `'[' || (SELECT GROUP_CONCAT('"' || value || '"') FROM (SELECT DISTINCT value FROM json_each(${array}))) || ']'`;
×
UNCOV
600
  }
×
601

602
  arrayFlatten(array: string): string {
2✔
UNCOV
603
    // For JSON arrays, just return the array (already flat)
×
UNCOV
604
    return `${array}`;
×
605
  }
×
606

607
  arrayCompact(array: string): string {
2✔
UNCOV
608
    // Remove null values from JSON array
×
609
    return `'[' || (SELECT GROUP_CONCAT('"' || value || '"') FROM json_each(${array}) WHERE value IS NOT NULL AND value != 'null') || ']'`;
×
610
  }
×
611

612
  // System Functions
2✔
613
  recordId(): string {
2✔
614
    return this.qualifySystemColumn('__id');
×
615
  }
×
616

617
  autoNumber(): string {
2✔
618
    return this.qualifySystemColumn('__auto_number');
×
619
  }
×
620

621
  textAll(value: string): string {
2✔
622
    return `CAST(${value} AS TEXT)`;
×
623
  }
×
624

625
  // Binary Operations
2✔
626
  add(left: string, right: string): string {
2✔
627
    return `(${left} + ${right})`;
×
UNCOV
628
  }
×
629

630
  subtract(left: string, right: string): string {
2✔
631
    return `(${left} - ${right})`;
×
UNCOV
632
  }
×
633

634
  multiply(left: string, right: string): string {
2✔
635
    return `(${left} * ${right})`;
×
UNCOV
636
  }
×
637

638
  divide(left: string, right: string): string {
2✔
639
    return `(${left} / ${right})`;
×
640
  }
×
641

642
  modulo(left: string, right: string): string {
2✔
643
    return `(${left} % ${right})`;
×
644
  }
×
645

646
  // Comparison Operations
2✔
647
  equal(left: string, right: string): string {
2✔
648
    return this.buildBlankAwareComparison('=', left, right);
×
UNCOV
649
  }
×
650

651
  notEqual(left: string, right: string): string {
2✔
652
    return this.buildBlankAwareComparison('<>', left, right);
×
653
  }
×
654

655
  greaterThan(left: string, right: string): string {
2✔
UNCOV
656
    return `(${left} > ${right})`;
×
657
  }
×
658

659
  lessThan(left: string, right: string): string {
2✔
UNCOV
660
    return `(${left} < ${right})`;
×
UNCOV
661
  }
×
662

663
  greaterThanOrEqual(left: string, right: string): string {
2✔
UNCOV
664
    return `(${left} >= ${right})`;
×
UNCOV
665
  }
×
666

667
  lessThanOrEqual(left: string, right: string): string {
2✔
UNCOV
668
    return `(${left} <= ${right})`;
×
UNCOV
669
  }
×
670

671
  // Logical Operations
2✔
672
  logicalAnd(left: string, right: string): string {
2✔
UNCOV
673
    return `(${left} AND ${right})`;
×
674
  }
×
675

676
  logicalOr(left: string, right: string): string {
2✔
UNCOV
677
    return `(${left} OR ${right})`;
×
UNCOV
678
  }
×
679

680
  bitwiseAnd(left: string, right: string): string {
2✔
UNCOV
681
    return `(${left} & ${right})`;
×
UNCOV
682
  }
×
683

684
  // Unary Operations
2✔
685
  unaryMinus(value: string): string {
2✔
UNCOV
686
    return `(-${value})`;
×
687
  }
×
688

689
  // Field Reference
2✔
690
  fieldReference(_fieldId: string, columnName: string): string {
2✔
691
    return `"${columnName}"`;
×
692
  }
×
693

694
  // Literals
2✔
695
  stringLiteral(value: string): string {
2✔
696
    return `'${value.replace(/'/g, "''")}'`;
×
697
  }
×
698

699
  numberLiteral(value: number): string {
2✔
700
    return value.toString();
×
701
  }
×
702

703
  booleanLiteral(value: boolean): string {
2✔
UNCOV
704
    return value ? '1' : '0';
×
705
  }
×
706

707
  nullLiteral(): string {
2✔
UNCOV
708
    return 'NULL';
×
UNCOV
709
  }
×
710

711
  // Utility methods for type conversion and validation
2✔
712
  castToNumber(value: string): string {
2✔
UNCOV
713
    return `CAST(${value} AS REAL)`;
×
UNCOV
714
  }
×
715

716
  castToString(value: string): string {
2✔
UNCOV
717
    return `CAST(${value} AS TEXT)`;
×
UNCOV
718
  }
×
719

720
  castToBoolean(value: string): string {
2✔
UNCOV
721
    return `CASE WHEN ${value} THEN 1 ELSE 0 END`;
×
UNCOV
722
  }
×
723

724
  castToDate(value: string): string {
2✔
UNCOV
725
    return `DATETIME(${value})`;
×
UNCOV
726
  }
×
727

728
  // Handle null values and type checking
2✔
729
  isNull(value: string): string {
2✔
UNCOV
730
    return `${value} IS NULL`;
×
UNCOV
731
  }
×
732

733
  coalesce(params: string[]): string {
2✔
UNCOV
734
    return `COALESCE(${this.joinParams(params)})`;
×
UNCOV
735
  }
×
736

737
  // Parentheses for grouping
2✔
738
  parentheses(expression: string): string {
2✔
UNCOV
739
    return `(${expression})`;
×
UNCOV
740
  }
×
741
}
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