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

teableio / teable / 19880339057

03 Dec 2025 02:32AM UTC coverage: 71.865% (-2.2%) from 74.057%
19880339057

push

github

web-flow
chore: ensure convertField transaction safety (#2197)

11258 of 12263 branches covered (91.8%)

181 of 191 new or added lines in 5 files covered. (94.76%)

4405 existing lines in 54 files now uncovered.

55511 of 77243 relevant lines covered (71.87%)

4342.48 hits per line

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

45.58
/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 { isTextLikeParam, resolveFormulaParamInfo } from '../../utils/formula-param-metadata.util';
3
import { SelectQueryAbstract } from '../select-query.abstract';
4

5
/**
2✔
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
 */
2✔
11
export class SelectQuerySqlite extends SelectQueryAbstract {
2✔
12
  private get tableAlias(): string | undefined {
2✔
13
    const ctx = this.context as ISelectFormulaConversionContext | undefined;
×
14
    return ctx?.tableAlias;
×
UNCOV
15
  }
×
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

154
  log(value: string, base?: string): string {
2✔
UNCOV
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))`;
×
UNCOV
158
    }
×
UNCOV
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 {
2✔
164
    return `(${dividend} % ${divisor})`;
×
165
  }
×
166

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

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

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

180
  find(searchText: string, withinText: string, startNum?: string): string {
2✔
UNCOV
181
    if (startNum) {
×
UNCOV
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})`;
×
UNCOV
185
  }
×
186

187
  search(searchText: string, withinText: string, startNum?: string): string {
2✔
188
    // Case-insensitive search
×
UNCOV
189
    if (startNum) {
×
UNCOV
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}))`;
×
UNCOV
193
  }
×
194

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

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

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

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

211
  regexpReplace(text: string, pattern: string, replacement: string): string {
2✔
UNCOV
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 {
2✔
UNCOV
217
    // SQLite doesn't support replacing specific instances easily
×
218
    return `REPLACE(${text}, ${oldText}, ${newText})`;
×
219
  }
×
220

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

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

229
  rept(text: string, numTimes: string): string {
2✔
UNCOV
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 {
2✔
UNCOV
235
    return `TRIM(${text})`;
×
UNCOV
236
  }
×
237

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

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

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

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

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

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

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

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

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

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

399
  private buildMonthDiff(startDate: string, endDate: string): string {
2✔
UNCOV
400
    const startYear = `CAST(STRFTIME('%Y', ${startDate}) AS INTEGER)`;
×
UNCOV
401
    const endYear = `CAST(STRFTIME('%Y', ${endDate}) AS INTEGER)`;
×
UNCOV
402
    const startMonth = `CAST(STRFTIME('%m', ${startDate}) AS INTEGER)`;
×
UNCOV
403
    const endMonth = `CAST(STRFTIME('%m', ${endDate}) AS INTEGER)`;
×
UNCOV
404
    const startDay = `CAST(STRFTIME('%d', ${startDate}) AS INTEGER)`;
×
UNCOV
405
    const endDay = `CAST(STRFTIME('%d', ${endDate}) AS INTEGER)`;
×
UNCOV
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}))`;
×
UNCOV
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})`;
×
UNCOV
414
  }
×
415

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

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

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

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

456
  fromNow(date: string): string {
2✔
457
    return `CAST((JULIANDAY('now') - JULIANDAY(${date})) * 86400 AS INTEGER)`;
×
458
  }
×
459

460
  hour(date: string): string {
2✔
UNCOV
461
    return `CAST(STRFTIME('%H', ${date}) AS INTEGER)`;
×
UNCOV
462
  }
×
463

464
  isAfter(date1: string, date2: string): string {
2✔
UNCOV
465
    return `DATETIME(${date1}) > DATETIME(${date2})`;
×
UNCOV
466
  }
×
467

468
  isBefore(date1: string, date2: string): string {
2✔
UNCOV
469
    return `DATETIME(${date1}) < DATETIME(${date2})`;
×
UNCOV
470
  }
×
471

472
  isSame(date1: string, date2: string, unit?: string): string {
2✔
473
    if (unit) {
25✔
474
      const trimmed = unit.trim();
25✔
475
      if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
25✔
476
        const format = this.normalizeTruncateFormat(trimmed.slice(1, -1));
25✔
477
        return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
25✔
478
      }
25!
479
      const format = this.normalizeTruncateFormat(unit);
×
480
      return `STRFTIME('${format}', ${date1}) = STRFTIME('${format}', ${date2})`;
×
UNCOV
481
    }
×
UNCOV
482
    return `DATETIME(${date1}) = DATETIME(${date2})`;
×
483
  }
×
484

485
  lastModifiedTime(): string {
2✔
UNCOV
486
    return this.qualifySystemColumn('__last_modified_time');
×
487
  }
×
488

489
  minute(date: string): string {
2✔
UNCOV
490
    return `CAST(STRFTIME('%M', ${date}) AS INTEGER)`;
×
491
  }
×
492

493
  month(date: string): string {
2✔
UNCOV
494
    return `CAST(STRFTIME('%m', ${date}) AS INTEGER)`;
×
UNCOV
495
  }
×
496

497
  second(date: string): string {
2✔
498
    return `CAST(STRFTIME('%S', ${date}) AS INTEGER)`;
×
UNCOV
499
  }
×
500

501
  timestr(date: string): string {
2✔
502
    return `TIME(${date})`;
×
UNCOV
503
  }
×
504

505
  toNow(date: string): string {
2✔
506
    return `CAST((JULIANDAY(${date}) - JULIANDAY('now')) * 86400 AS INTEGER)`;
×
UNCOV
507
  }
×
508

509
  weekNum(date: string): string {
2✔
510
    return `CAST(STRFTIME('%W', ${date}) AS INTEGER)`;
×
UNCOV
511
  }
×
512

513
  weekday(date: string): string {
2✔
514
    // SQLite STRFTIME('%w') returns 0-6 (Sunday=0), but we need 1-7 (Sunday=1)
×
515
    return `CAST(STRFTIME('%w', ${date}) AS INTEGER) + 1`;
×
516
  }
×
517

518
  workday(startDate: string, days: string): string {
2✔
519
    // Simplified implementation
×
520
    return `DATE(${startDate}, '+' || ${days} || ' days')`;
×
521
  }
×
522

523
  workdayDiff(startDate: string, endDate: string): string {
2✔
UNCOV
524
    return `CAST((JULIANDAY(${endDate}) - JULIANDAY(${startDate})) AS INTEGER)`;
×
525
  }
×
526

527
  year(date: string): string {
2✔
UNCOV
528
    return `CAST(STRFTIME('%Y', ${date}) AS INTEGER)`;
×
UNCOV
529
  }
×
530

531
  createdTime(): string {
2✔
UNCOV
532
    return this.qualifySystemColumn('__created_time');
×
UNCOV
533
  }
×
534

535
  // Logical Functions
2✔
536
  private truthinessScore(value: string): string {
2✔
UNCOV
537
    const wrapped = `(${value})`;
×
538
    const valueType = `TYPEOF${wrapped}`;
×
539
    return `CASE
×
UNCOV
540
      WHEN ${wrapped} IS NULL THEN 0
×
UNCOV
541
      WHEN ${valueType} = 'integer' OR ${valueType} = 'real' THEN (${wrapped}) != 0
×
542
      WHEN ${valueType} = 'text' THEN (${wrapped} != '' AND LOWER(${wrapped}) != 'null')
×
543
      ELSE (${wrapped}) IS NOT NULL AND ${wrapped} != 'null'
×
544
    END`;
×
545
  }
×
546

547
  if(condition: string, valueIfTrue: string, valueIfFalse: string): string {
2✔
UNCOV
548
    const truthiness = this.truthinessScore(condition);
×
549
    return `CASE WHEN (${truthiness}) = 1 THEN ${valueIfTrue} ELSE ${valueIfFalse} END`;
×
550
  }
×
551

552
  and(params: string[]): string {
2✔
UNCOV
553
    return `(${params.map((p) => `(${p})`).join(' AND ')})`;
×
554
  }
×
555

556
  or(params: string[]): string {
2✔
UNCOV
557
    return `(${params.map((p) => `(${p})`).join(' OR ')})`;
×
UNCOV
558
  }
×
559

560
  not(value: string): string {
2✔
UNCOV
561
    return `NOT (${value})`;
×
UNCOV
562
  }
×
563

564
  xor(params: string[]): string {
2✔
565
    if (params.length === 2) {
×
566
      return `((${params[0]}) AND NOT (${params[1]})) OR (NOT (${params[0]}) AND (${params[1]}))`;
×
567
    }
×
568
    return `(${params.map((p) => `CASE WHEN ${p} THEN 1 ELSE 0 END`).join(' + ')}) % 2 = 1`;
×
569
  }
×
570

571
  blank(): string {
2✔
572
    // SQLite BLANK function should return null instead of empty string
×
573
    return `NULL`;
×
574
  }
×
575

576
  error(_message: string): string {
2✔
UNCOV
577
    // SQLite doesn't have a direct error function, use a failing expression
×
UNCOV
578
    return `(1/0)`;
×
UNCOV
579
  }
×
580

581
  isError(_value: string): string {
2✔
UNCOV
582
    return `0`;
×
UNCOV
583
  }
×
584

585
  switch(
2✔
UNCOV
586
    expression: string,
×
UNCOV
587
    cases: Array<{ case: string; result: string }>,
×
588
    defaultResult?: string
×
589
  ): string {
×
UNCOV
590
    let sql = `CASE ${expression}`;
×
UNCOV
591
    for (const caseItem of cases) {
×
592
      sql += ` WHEN ${caseItem.case} THEN ${caseItem.result}`;
×
593
    }
×
594
    if (defaultResult) {
×
595
      sql += ` ELSE ${defaultResult}`;
×
UNCOV
596
    }
×
UNCOV
597
    sql += ` END`;
×
598
    return sql;
×
599
  }
×
600

601
  // Array Functions - Limited in SQLite
2✔
602
  count(params: string[]): string {
2✔
603
    return `COUNT(${this.joinParams(params)})`;
×
604
  }
×
605

606
  countA(params: string[]): string {
2✔
UNCOV
607
    return `COUNT(${this.joinParams(params.map((p) => `CASE WHEN ${p} IS NOT NULL THEN 1 END`))})`;
×
608
  }
×
609

610
  countAll(_value: string): string {
2✔
UNCOV
611
    return `COUNT(*)`;
×
UNCOV
612
  }
×
613

614
  private buildJsonArrayUnion(
2✔
615
    arrays: string[],
×
UNCOV
616
    opts?: { filterNulls?: boolean; withOrdinal?: boolean }
×
UNCOV
617
  ): string {
×
618
    const selects = arrays.map((array, index) => {
×
619
      const base = `SELECT value, ${index} AS arg_index, CAST(key AS INTEGER) AS ord FROM json_each(COALESCE(${array}, '[]'))`;
×
UNCOV
620
      const whereClause = opts?.filterNulls
×
UNCOV
621
        ? " WHERE value IS NOT NULL AND value != 'null' AND value != ''"
×
622
        : '';
×
623
      return `${base}${whereClause}`;
×
UNCOV
624
    });
×
625

UNCOV
626
    if (selects.length === 0) {
×
627
      return 'SELECT NULL AS value, 0 AS arg_index, 0 AS ord WHERE 0';
×
628
    }
×
629

UNCOV
630
    return selects.join(' UNION ALL ');
×
631
  }
×
632

633
  arrayJoin(array: string, separator?: string): string {
2✔
UNCOV
634
    const sep = separator || ',';
×
635
    // SQLite JSON array join using json_each with stable ordering by key
×
636
    return `(SELECT GROUP_CONCAT(value, ${sep}) FROM json_each(${array}) ORDER BY key)`;
×
UNCOV
637
  }
×
638

639
  arrayUnique(arrays: string[]): string {
2✔
640
    const unionQuery = this.buildJsonArrayUnion(arrays, { withOrdinal: true, filterNulls: true });
×
UNCOV
641
    return `COALESCE(
×
642
      '[' || (
643
        SELECT GROUP_CONCAT(json_quote(value))
644
        FROM (
645
          SELECT value, ROW_NUMBER() OVER (PARTITION BY value ORDER BY arg_index, ord) AS rn, arg_index, ord
UNCOV
646
          FROM (${unionQuery}) AS combined
×
647
        )
648
        WHERE rn = 1
649
        ORDER BY arg_index, ord
650
      ) || ']',
651
      '[]'
652
    )`;
×
653
  }
×
654

655
  arrayFlatten(arrays: string[]): string {
2✔
656
    const unionQuery = this.buildJsonArrayUnion(arrays, { withOrdinal: true });
×
657
    return `COALESCE(
×
658
      '[' || (
659
        SELECT GROUP_CONCAT(json_quote(value))
660
        FROM (${unionQuery}) AS combined
×
661
        ORDER BY arg_index, ord
662
      ) || ']',
663
      '[]'
664
    )`;
×
665
  }
×
666

667
  arrayCompact(arrays: string[]): string {
2✔
668
    const unionQuery = this.buildJsonArrayUnion(arrays, {
×
669
      filterNulls: true,
×
UNCOV
670
      withOrdinal: true,
×
UNCOV
671
    });
×
UNCOV
672
    return `COALESCE(
×
673
      '[' || (
674
        SELECT GROUP_CONCAT(json_quote(value))
UNCOV
675
        FROM (${unionQuery}) AS combined
×
676
        ORDER BY arg_index, ord
677
      ) || ']',
678
      '[]'
UNCOV
679
    )`;
×
UNCOV
680
  }
×
681

682
  // System Functions
2✔
683
  recordId(): string {
2✔
UNCOV
684
    return this.qualifySystemColumn('__id');
×
UNCOV
685
  }
×
686

687
  autoNumber(): string {
2✔
UNCOV
688
    return this.qualifySystemColumn('__auto_number');
×
UNCOV
689
  }
×
690

691
  textAll(value: string): string {
2✔
692
    return `CAST(${value} AS TEXT)`;
×
UNCOV
693
  }
×
694

695
  // Binary Operations
2✔
696
  add(left: string, right: string): string {
2✔
697
    return `(${left} + ${right})`;
×
UNCOV
698
  }
×
699

700
  subtract(left: string, right: string): string {
2✔
701
    return `(${left} - ${right})`;
×
UNCOV
702
  }
×
703

704
  multiply(left: string, right: string): string {
2✔
705
    return `(${left} * ${right})`;
×
UNCOV
706
  }
×
707

708
  divide(left: string, right: string): string {
2✔
709
    return `(${left} / ${right})`;
×
UNCOV
710
  }
×
711

712
  modulo(left: string, right: string): string {
2✔
713
    return `(${left} % ${right})`;
×
714
  }
×
715

716
  // Comparison Operations
2✔
717
  equal(left: string, right: string): string {
2✔
718
    return this.buildBlankAwareComparison('=', left, right);
×
UNCOV
719
  }
×
720

721
  notEqual(left: string, right: string): string {
2✔
722
    return this.buildBlankAwareComparison('<>', left, right);
×
UNCOV
723
  }
×
724

725
  greaterThan(left: string, right: string): string {
2✔
726
    return `(${left} > ${right})`;
×
UNCOV
727
  }
×
728

729
  lessThan(left: string, right: string): string {
2✔
730
    return `(${left} < ${right})`;
×
731
  }
×
732

733
  greaterThanOrEqual(left: string, right: string): string {
2✔
734
    return `(${left} >= ${right})`;
×
735
  }
×
736

737
  lessThanOrEqual(left: string, right: string): string {
2✔
UNCOV
738
    return `(${left} <= ${right})`;
×
739
  }
×
740

741
  // Logical Operations
2✔
742
  logicalAnd(left: string, right: string): string {
2✔
UNCOV
743
    return `(${left} AND ${right})`;
×
UNCOV
744
  }
×
745

746
  logicalOr(left: string, right: string): string {
2✔
UNCOV
747
    return `(${left} OR ${right})`;
×
UNCOV
748
  }
×
749

750
  bitwiseAnd(left: string, right: string): string {
2✔
UNCOV
751
    return `(${left} & ${right})`;
×
UNCOV
752
  }
×
753

754
  // Unary Operations
2✔
755
  unaryMinus(value: string): string {
2✔
UNCOV
756
    return `(-${value})`;
×
UNCOV
757
  }
×
758

759
  // Field Reference
2✔
760
  fieldReference(_fieldId: string, columnName: string): string {
2✔
UNCOV
761
    return `"${columnName}"`;
×
UNCOV
762
  }
×
763

764
  // Literals
2✔
765
  stringLiteral(value: string): string {
2✔
UNCOV
766
    return `'${value.replace(/'/g, "''")}'`;
×
UNCOV
767
  }
×
768

769
  numberLiteral(value: number): string {
2✔
UNCOV
770
    return value.toString();
×
UNCOV
771
  }
×
772

773
  booleanLiteral(value: boolean): string {
2✔
UNCOV
774
    return value ? '1' : '0';
×
UNCOV
775
  }
×
776

777
  nullLiteral(): string {
2✔
UNCOV
778
    return 'NULL';
×
UNCOV
779
  }
×
780

781
  // Utility methods for type conversion and validation
2✔
782
  castToNumber(value: string): string {
2✔
UNCOV
783
    return `CAST(${value} AS REAL)`;
×
UNCOV
784
  }
×
785

786
  castToString(value: string): string {
2✔
UNCOV
787
    return `CAST(${value} AS TEXT)`;
×
UNCOV
788
  }
×
789

790
  castToBoolean(value: string): string {
2✔
UNCOV
791
    return `CASE WHEN ${value} THEN 1 ELSE 0 END`;
×
UNCOV
792
  }
×
793

794
  castToDate(value: string): string {
2✔
UNCOV
795
    return `DATETIME(${value})`;
×
UNCOV
796
  }
×
797

798
  // Handle null values and type checking
2✔
799
  isNull(value: string): string {
2✔
UNCOV
800
    return `${value} IS NULL`;
×
UNCOV
801
  }
×
802

803
  coalesce(params: string[]): string {
2✔
UNCOV
804
    return `COALESCE(${this.joinParams(params)})`;
×
UNCOV
805
  }
×
806

807
  // Parentheses for grouping
2✔
808
  parentheses(expression: string): string {
2✔
UNCOV
809
    return `(${expression})`;
×
UNCOV
810
  }
×
811
}
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