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

rogerpadilla / uql / 29120937283

10 Jul 2026 08:17PM UTC coverage: 94.931% (+0.01%) from 94.921%
29120937283

push

github

rogerpadilla
feat(uql-orm)!: reliable insertOne/insertMany IDs, heterogeneous batches, auto-chunking

insertOne/insertMany now return correct IDs on every database (undefined instead of a
fabricated value when the driver reports none), accept records with different column sets
in one statement, auto-chunk oversized batches, use native MariaDB RETURNING, and detect
clustered MySQL auto_increment_increment.

BREAKING CHANGE: dialect `insertIdStrategy` ('first'|'last') becomes `insertIdSource`
('returning'|'firstId'|'lastId'); dialect features moved from a constructor argument to the
overridable `featureDefaults` property; the `mergeDialectFeatures` helper and unused
`features.returning` flag were removed. Only custom dialect subclasses are affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

3251 of 3600 branches covered (90.31%)

Branch coverage included in aggregate %.

67 of 69 new or added lines in 12 files covered. (97.1%)

5644 of 5770 relevant lines covered (97.82%)

395.52 hits per line

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

97.92
/packages/uql-orm/src/sqlite/sqliteDialect.ts
1
import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
2
import { buildElemMatchConditions } from '../dialect/jsonArrayElemMatchUtils.js';
3
import { getMeta } from '../entity/index.js';
4
import type {
5
  DialectFeatures,
6
  FieldKey,
7
  FieldOptions,
8
  JsonUpdateOp,
9
  QueryComparisonOptions,
10
  QueryConflictPaths,
11
  QueryContext,
12
  QuerySizeComparisonOps,
13
  QueryTextSearchOptions,
14
  QueryWhereFieldOperatorMap,
15
  Type,
16
  VectorDistance,
17
} from '../type/index.js';
18
import { escapeAnsiSqlLiteral } from '../util/ansiSqlLiteral.js';
19
import { hasKeys } from '../util/index.js';
20

21
export class SqliteDialect extends AbstractSqlDialect {
22
  /** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
23
  protected override readonly featureDefaults: DialectFeatures = {
134✔
24
    explicitJsonCast: false,
25
    nativeArrays: false,
26
    supportsJsonb: false,
27
    ifNotExists: true,
28
    indexIfNotExists: true,
29
    dropTableCascade: false,
30
    renameColumn: true,
31
    foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
32
    columnComment: false, // SQLite does not support column comments
33
    vectorIndexStyle: 'create',
34
    vectorSupportsLength: false,
35
    supportsTimestamptz: false,
36
    defaultStringAsText: true,
37
  };
38

39
  override readonly dialectName = 'sqlite';
134✔
40

41
  override readonly quoteChar = '`';
134✔
42

43
  override readonly serialPrimaryKey = 'INTEGER PRIMARY KEY AUTOINCREMENT';
134✔
44

45
  override readonly tableOptions = '';
134✔
46

47
  override readonly beginTransactionCommand = 'BEGIN TRANSACTION';
134✔
48

49
  override readonly commitTransactionCommand = 'COMMIT';
134✔
50

51
  override readonly rollbackTransactionCommand = 'ROLLBACK';
134✔
52

53
  override readonly isolationLevelStrategy = 'none';
134✔
54

55
  override readonly alterColumnSyntax = 'none';
134✔
56

57
  override readonly booleanLiteral = 'integer';
134✔
58

59
  override readonly insertIdSource = 'lastId';
134✔
60

61
  protected override readonly vectorDistanceFns: Partial<Record<VectorDistance, string>> = {
134✔
62
    cosine: 'vec_distance_cosine',
63
    l2: 'vec_distance_L2',
64
    hamming: 'vec_distance_hamming',
65
  };
66

67
  /**
68
   * SQLite does not support the `DEFAULT` keyword inside `VALUES`. Inline the metadata default
69
   * when declared, else `NULL` (which is also how SQLite auto-generates INTEGER PRIMARY KEYs).
70
   */
71
  protected override appendDefaultInsertValue(ctx: QueryContext, field: FieldOptions | undefined): void {
72
    if (field?.defaultValue !== undefined) {
7!
NEW
73
      this.formatPersistableValue(ctx, field, field.defaultValue);
×
74
    } else {
75
      ctx.append('NULL');
7✔
76
    }
77
  }
78

79
  protected override ilikeExpr(f: string, ph: string): string {
80
    return `${f} LIKE ${ph}`;
16✔
81
  }
82

83
  protected override get neOp(): string {
84
    return 'IS NOT';
17✔
85
  }
86

87
  override normalizeValue(value: unknown): unknown {
88
    if (value instanceof Date) return value.getTime();
2,373✔
89
    return super.normalizeValue(value);
2,371✔
90
  }
91

92
  override compare<E>(
93
    ctx: QueryContext,
94
    entity: Type<E>,
95
    key: string,
96
    val: unknown,
97
    opts?: QueryComparisonOptions,
98
  ): void {
99
    if (key === '$text') {
688✔
100
      const meta = getMeta(entity);
2✔
101
      const search = val as QueryTextSearchOptions<E>;
2✔
102
      const fields = search.$fields!.map((fKey) => {
2✔
103
        const field = meta.fields[fKey];
3✔
104
        const columnName = this.resolveColumnName(fKey, field!);
3✔
105
        return this.escapeId(columnName);
3✔
106
      });
107
      const tableName = this.resolveTableName(entity, meta);
2✔
108
      ctx.append(`${this.escapeId(tableName)} MATCH {${fields.join(' ')}} : `);
2✔
109
      ctx.addValue(search.$value);
2✔
110
      return;
2✔
111
    }
112
    super.compare(ctx, entity, key, val, opts);
686✔
113
  }
114

115
  override compareFieldOperator<E, K extends keyof QueryWhereFieldOperatorMap<E>>(
116
    ctx: QueryContext,
117
    entity: Type<E>,
118
    key: FieldKey<E>,
119
    op: K,
120
    val: QueryWhereFieldOperatorMap<E>[K],
121
    opts: QueryComparisonOptions = {},
659✔
122
  ): void {
123
    switch (op) {
659✔
124
      case '$elemMatch':
125
        this.buildElemMatchCondition(ctx, entity, key, val as Record<string, unknown>, opts);
6✔
126
        break;
6✔
127
      case '$all': {
128
        // SQLite: Check JSON array contains all values using multiple json_each subqueries
129
        const values = val as unknown[];
1✔
130
        const conditions = values
1✔
131
          .map((v) => {
132
            ctx.pushValue(JSON.stringify(v));
2✔
133
            return `EXISTS (SELECT 1 FROM json_each(${this.escapeId(key)}) WHERE value = json(?))`;
2✔
134
          })
135
          .join(' AND ');
136
        ctx.append(`(${conditions})`);
1✔
137
        break;
1✔
138
      }
139
      case '$size':
140
        // SQLite: Check JSON array length
141
        // e.g., json_array_length(roles) = 3, or json_array_length(roles) >= 2
142
        this.buildSizeComparison(
4✔
143
          ctx,
144
          () => {
145
            ctx.append('json_array_length(');
5✔
146
            this.getComparisonKey(ctx, entity, key, opts);
5✔
147
            ctx.append(')');
5✔
148
          },
149
          val as number | QuerySizeComparisonOps,
150
        );
151
        break;
4✔
152
      default:
153
        super.compareFieldOperator(ctx, entity, key, op, val, opts);
648✔
154
    }
155
  }
156

157
  /**
158
   * Build $elemMatch condition for SQLite JSON arrays.
159
   * Uses EXISTS with json_each and supports nested operators.
160
   */
161
  private buildElemMatchCondition<E>(
162
    ctx: QueryContext,
163
    _entity: Type<E>,
164
    key: FieldKey<E>,
165
    match: Record<string, unknown>,
166
    opts: QueryComparisonOptions,
167
  ): void {
168
    ctx.append('EXISTS (SELECT 1 FROM json_each(');
6✔
169
    this.getComparisonKey(ctx, _entity, key, opts);
6✔
170
    ctx.append(') WHERE ');
6✔
171

172
    const conditions = buildElemMatchConditions(
6✔
173
      match,
174
      (field, op, opVal) =>
175
        this.buildJsonFieldCondition(ctx, (f) => `json_extract(value, '$.${this.escapeJsonKey(f)}')`, field, op, opVal),
15✔
176
      (field, value) => {
177
        // Keep SQLite's placeholder behavior consistent with prior implementation.
178
        ctx.pushValue(value);
2✔
179
        return `json_extract(value, '$.${this.escapeJsonKey(field)}') = ?`;
2✔
180
      },
181
    );
182

183
    ctx.append(conditions.join(' AND '));
6✔
184
    ctx.append(')');
6✔
185
  }
186

187
  protected override getJsonPathScalarExpr(escapedColumn: string, jsonPath: string): string {
188
    return `json_extract(${escapedColumn}, '$.${this.escapeJsonKey(jsonPath)}')`;
7✔
189
  }
190

191
  protected override numericCast(expr: string): string {
192
    return `CAST(${expr} AS REAL)`;
4✔
193
  }
194

195
  override upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void {
196
    this.onConflictUpsert(ctx, entity, conflictPaths, payload, this.insert.bind(this));
9✔
197
  }
198

199
  protected override formatJsonUpdate<E>(ctx: QueryContext, escapedCol: string, value: JsonUpdateOp<E>): void {
200
    let expr = escapedCol;
12✔
201
    if (hasKeys(value.$merge)) {
12✔
202
      const merge = value.$merge as Record<string, unknown>;
7✔
203
      expr = `json_set(COALESCE(${escapedCol}, '{}')`;
7✔
204
      for (const [key, v] of Object.entries(merge)) {
7✔
205
        expr += `, '$.${this.escapeJsonKey(key)}', json(?)`;
7✔
206
        ctx.pushValue(JSON.stringify(v));
7✔
207
      }
208
      expr += ')';
7✔
209
    }
210
    if (hasKeys(value.$push)) {
12✔
211
      const push = value.$push as Record<string, unknown>;
5✔
212
      expr = `json_insert(${expr}`;
5✔
213
      for (const [key, v] of Object.entries(push)) {
5✔
214
        expr += `, '$.${this.escapeJsonKey(key)}[#]', json(?)`;
5✔
215
        ctx.pushValue(JSON.stringify(v));
5✔
216
      }
217
      expr += ')';
5✔
218
    }
219
    if (value.$unset?.length) {
12✔
220
      const paths = value.$unset.map((k) => `'$.${this.escapeJsonKey(k)}'`).join(', ');
5✔
221
      expr = `json_remove(${expr}, ${paths})`;
4✔
222
    }
223
    ctx.append(`${escapedCol} = ${expr}`);
12✔
224
  }
225

226
  override escape(value: unknown): string {
227
    return escapeAnsiSqlLiteral(value);
1✔
228
  }
229
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc