uql-orm 0.67.1 → 0.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/browser/http/http.js +5 -8
  2. package/dist/browser/querier/httpQuerier.d.ts +9 -9
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +9 -8
  5. package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
  6. package/dist/cockroachdb/cockroachDialect.js +7 -3
  7. package/dist/d1/d1Querier.d.ts +5 -5
  8. package/dist/d1/d1QuerierPool.d.ts +3 -3
  9. package/dist/d1/d1SqliteDialect.d.ts +1 -1
  10. package/dist/d1/d1SqliteDialect.js +1 -1
  11. package/dist/dialect/abstractSqlDialect.d.ts +107 -97
  12. package/dist/dialect/abstractSqlDialect.js +250 -293
  13. package/dist/dialect/hydrateColumn.js +33 -30
  14. package/dist/dialect/jsonSql.d.ts +32 -23
  15. package/dist/dialect/jsonSql.js +42 -31
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +19 -23
  17. package/dist/dialect/mysqlLikeSqlDialect.js +34 -51
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +24 -17
  19. package/dist/dialect/pgLikeSqlDialect.js +53 -26
  20. package/dist/dialect/vectorCast.d.ts +2 -0
  21. package/dist/dialect/vectorCast.js +25 -5
  22. package/dist/dialect/vectorSqlDialect.d.ts +8 -8
  23. package/dist/dialect/vectorSqlDialect.js +11 -11
  24. package/dist/http/query.d.ts +10 -2
  25. package/dist/http/query.js +26 -1
  26. package/dist/maria/mariaDialect.d.ts +16 -11
  27. package/dist/maria/mariaDialect.js +23 -19
  28. package/dist/migrate/ddl/indexDdl.d.ts +3 -1
  29. package/dist/migrate/ddl/indexDdl.js +5 -1
  30. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +7 -2
  31. package/dist/migrate/ddl/mysqlIndexDdl.js +28 -6
  32. package/dist/migrate/ddl/pgIndexDdl.d.ts +0 -9
  33. package/dist/migrate/ddl/pgIndexDdl.js +2 -6
  34. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
  35. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -3
  36. package/dist/migrate/introspection/mssqlIntrospector.d.ts +2 -9
  37. package/dist/migrate/introspection/mssqlIntrospector.js +2 -9
  38. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -9
  39. package/dist/migrate/introspection/mysqlIntrospector.js +2 -8
  40. package/dist/mongo/mongoDialect.d.ts +22 -10
  41. package/dist/mongo/mongoDialect.js +86 -38
  42. package/dist/mssql/mssqlDialect.d.ts +22 -18
  43. package/dist/mssql/mssqlDialect.js +54 -43
  44. package/dist/mysql/mysqlDialect.d.ts +16 -1
  45. package/dist/mysql/mysqlDialect.js +18 -2
  46. package/dist/sqlite/sqliteDialect.d.ts +17 -16
  47. package/dist/sqlite/sqliteDialect.js +35 -39
  48. package/dist/type/dialect.d.ts +0 -4
  49. package/dist/type/entity.d.ts +12 -7
  50. package/dist/type/query.d.ts +29 -24
  51. package/dist/type/queryWhere.d.ts +20 -20
  52. package/dist/type/universalQuerier.d.ts +10 -10
  53. package/dist/type/vector.d.ts +5 -7
  54. package/dist/type/wire.d.ts +9 -0
  55. package/dist/util/dialect.util.d.ts +2 -0
  56. package/dist/util/dialect.util.js +6 -2
  57. package/dist/util/object.util.d.ts +2 -4
  58. package/dist/util/object.util.js +4 -9
  59. package/package.json +2 -2
  60. package/dist/dialect/jsonArrayElemMatchUtils.d.ts +0 -2
  61. package/dist/dialect/jsonArrayElemMatchUtils.js +0 -7
  62. package/dist/dialect/pgVectorMetrics.d.ts +0 -13
  63. package/dist/dialect/pgVectorMetrics.js +0 -17
  64. package/dist/maria/mariaVectorMetrics.d.ts +0 -8
  65. package/dist/maria/mariaVectorMetrics.js +0 -10
@@ -1,7 +1,7 @@
1
1
  import { relationTermKey } from '../dialect/abstractSqlDialect.js';
2
- import { COUNT_ALIAS, JSON_ELEM_ALIAS } from '../dialect/aliases.js';
2
+ import { COUNT_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
3
3
  import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
4
- import { jsonPath } from '../dialect/jsonSql.js';
4
+ import { jsonArraySlotArgs, jsonPath, jsonSlotArgs } from '../dialect/jsonSql.js';
5
5
  import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
6
6
  import { getMeta } from '../entity/index.js';
7
7
  import { fieldOptionsToCanonical } from '../schema/canonicalType.js';
@@ -34,12 +34,17 @@ const MSSQL_FEATURES = {
34
34
  rowLockOf: true,
35
35
  orderedUpsertReturning: false,
36
36
  orderedJsonAggregates: true,
37
- partialJsonContainment: false,
38
- typedJsonElements: false,
39
37
  narrowVectorTypes: false,
40
38
  vectorTuningNeedsTransaction: false,
41
39
  serialDeclaresPrimaryKey: false,
42
40
  };
41
+ /** The `type` `OPENJSON` reports for the JSON scalar an element is compared with; anything else binds as a string. */
42
+ function openJsonType(value) {
43
+ if (typeof value === 'number') {
44
+ return 2;
45
+ }
46
+ return typeof value === 'boolean' ? 3 : 1;
47
+ }
43
48
  /** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
44
49
  export class MsSqlDialect extends MergeSqlDialect {
45
50
  features = MSSQL_FEATURES;
@@ -234,25 +239,19 @@ export class MsSqlDialect extends MergeSqlDialect {
234
239
  return `TRY_CAST(${expr} AS FLOAT)`;
235
240
  }
236
241
  /**
237
- * `JSON_VALUE` returns `NVARCHAR(4000)` and, in the lax mode that is the default, answers NULL
238
- * rather than erroring for anything longer - so a long string read through it disappears without a
239
- * word. `OPENJSON` has no such bound, so the path is split and its last segment matched as a key.
242
+ * `OPENJSON` at the path's parent, matching its last segment as a key, in either reading: `JSON_VALUE`
243
+ * answers NULL for text past 4000 characters, and `JSON_QUERY` for a scalar. A value reads back as
244
+ * text, which {@link jsonScalarParam} binds its operand as, and an array or object as its own JSON.
240
245
  */
241
- getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
242
- const dot = jsonPathStr.lastIndexOf('.');
243
- const parent = dot === -1 ? '$' : `$.${jsonPathStr.slice(0, dot).split('.').map(escapeSingleQuotes).join('.')}`;
244
- const leaf = escapeSingleQuotes(jsonPathStr.slice(dot + 1));
246
+ jsonPathReading(escapedColumn, path) {
247
+ if (!path) {
248
+ return escapedColumn;
249
+ }
250
+ const dot = path.lastIndexOf('.');
251
+ const parent = dot === -1 ? '$' : `$.${path.slice(0, dot).split('.').map(escapeSingleQuotes).join('.')}`;
252
+ const leaf = escapeSingleQuotes(path.slice(dot + 1));
245
253
  return `(SELECT ${this.#elem.value} FROM OPENJSON(${escapedColumn}, '${parent}') WHERE ${this.#elem.key} = N'${leaf}')`;
246
254
  }
247
- /**
248
- * The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
249
- * an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
250
- * `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
251
- * text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
252
- */
253
- getJsonPathJsonbExpr(escapedColumn, jsonPathStr) {
254
- return this.getJsonPathScalarExpr(escapedColumn, jsonPathStr);
255
- }
256
255
  /**
257
256
  * A value compared against a JSON path, which reads back as text, so only a boolean needs spelling as
258
257
  * `'true'`; SQL Server has no cast that parses text as JSON.
@@ -286,24 +285,37 @@ export class MsSqlDialect extends MergeSqlDialect {
286
285
  }
287
286
  return `JSON_QUERY(${this.addValue(ctx, JSON.stringify(value))})`;
288
287
  }
289
- jsonElemFrom(jsonField, _fields, alias) {
290
- return `OPENJSON(${jsonField}) ${alias}`;
288
+ jsonElemFrom(slot, alias) {
289
+ return `OPENJSON(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))}) ${alias}`;
290
+ }
291
+ /** `JSON_QUERY` answers an array or an object as written, and a scalar as NULL. */
292
+ jsonIsArray(slot) {
293
+ return `LEFT(JSON_QUERY(${jsonSlotArgs(slot)}), 1) = '['`;
291
294
  }
292
- /** `JSON_VALUE`'s 4000-character bound applies to an element's field, unlike a whole column. */
293
- jsonElemRef(alias, field) {
294
- return field === undefined
295
- ? `${alias}.${this.#elem.value}`
296
- : `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
295
+ /** An object element's `value` is its JSON text, which its fields are paths into. */
296
+ jsonElemDoc(alias) {
297
+ return `${alias}.${this.#elem.value}`;
297
298
  }
298
- jsonAll(ctx, jsonField, value) {
299
- const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
300
- const conditions = value.map((val) => `EXISTS (SELECT 1 FROM OPENJSON(${jsonField}) ${alias} WHERE ${alias}.${this.#elem.value} = ${this.jsonScalarParam(ctx, val)})`);
301
- return `(${conditions.join(' AND ')})`;
299
+ /**
300
+ * An element of the value's own JSON type: `OPENJSON` reads a string and a number back as the same text,
301
+ * so the `type` it reports is what tells `'5'` from `5`. A number compares by value, cast on both sides:
302
+ * text against a numeric parameter converts implicitly, which throws on an element that is no number.
303
+ */
304
+ jsonElemEquals(ctx, _slot, alias, value) {
305
+ const type = `${alias}.${this.#elem.type}`;
306
+ if (value === null) {
307
+ return `${type} = 0`;
308
+ }
309
+ const elem = this.jsonElemDoc(alias);
310
+ const param = this.jsonScalarParam(ctx, value);
311
+ const equal = typeof value === 'number' ? `${this.numericCast(elem)} = ${this.numericCast(param)}` : `${elem} = ${param}`;
312
+ return `${equal} AND ${type} = ${openJsonType(value)}`;
302
313
  }
303
- jsonSize(ctx, jsonField, value) {
304
- const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
305
- return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
314
+ jsonLength(slot) {
315
+ return `(SELECT COUNT(*) FROM OPENJSON(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))}))`;
306
316
  }
317
+ /** SQL Server orders JSON as the text `OPENJSON` reads, so a number sorts by its value first. */
318
+ jsonSortModes = ['numeric', 'text'];
307
319
  /** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
308
320
  jsonSet(ctx, expr, set, _field) {
309
321
  for (const [key, value] of Object.entries(set)) {
@@ -323,25 +335,24 @@ export class MsSqlDialect extends MergeSqlDialect {
323
335
  }
324
336
  /**
325
337
  * The surviving elements are re-aggregated into an array and written back whole - there is no
326
- * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
338
+ * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string. Only an
339
+ * array is rewritten: `JSON_MODIFY` would create an absent key, and any other value stays as it is.
327
340
  */
328
341
  jsonPullKey(ctx, expr, escapedCol, key, value) {
329
- const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
330
- const val = `${alias}.${this.#elem.value}`;
342
+ const slot = { base: escapedCol, path: key };
343
+ const val = this.jsonElemDoc(JSON_PULL_ALIAS);
331
344
  // `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
332
345
  // re-encoded from its reported `type` before the array is put back together - concatenated raw,
333
346
  // the result is text the engine then refuses to parse as JSON.
334
- const encoded = `CASE ${alias}.${this.#elem.type}` +
347
+ const encoded = `CASE ${JSON_PULL_ALIAS}.${this.#elem.type}` +
335
348
  ` WHEN 0 THEN 'null'` +
336
349
  ` WHEN 1 THEN '"' + STRING_ESCAPE(${val}, 'json') + '"'` +
337
350
  ` ELSE ${val} END`;
338
351
  // `IS NULL OR` because a JSON null element reads back as SQL NULL, and `<>` against one is
339
352
  // unknown rather than true - which silently dropped every null from the array it rebuilt.
340
- const kept = `SELECT '[' + STRING_AGG(${encoded}, ',') + ']' FROM OPENJSON(${escapedCol}, ${jsonPath(key)}) ${alias}` +
353
+ const kept = `SELECT '[' + STRING_AGG(${encoded}, ',') + ']' FROM ${this.jsonElemFrom(slot, JSON_PULL_ALIAS)}` +
341
354
  ` WHERE ${val} IS NULL OR ${val} <> ${this.jsonScalarParam(ctx, value)}`;
342
- // `JSON_MODIFY` creates a path it does not find, so a `$pull` against an absent key would add an
343
- // empty array where the other engines leave the document alone.
344
- const path = jsonPath(key);
345
- return `CASE WHEN JSON_QUERY(${escapedCol}, ${path}) IS NULL THEN ${expr} ELSE JSON_MODIFY(${expr}, ${path}, JSON_QUERY(COALESCE((${kept}), '[]'))) END`;
355
+ const pulled = `JSON_MODIFY(${expr}, ${jsonPath(key)}, JSON_QUERY(COALESCE((${kept}), '[]')))`;
356
+ return `CASE WHEN ${this.jsonIsArray(slot)} THEN ${pulled} ELSE ${expr} END`;
346
357
  }
347
358
  }
@@ -1,4 +1,6 @@
1
+ import type { JsonSlot } from '../dialect/jsonSql.js';
1
2
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
3
+ import type { QueryContext } from '../type/index.js';
2
4
  export declare class MySqlDialect extends MysqlLikeSqlDialect {
3
5
  readonly dialectName = "mysql";
4
6
  /**
@@ -6,6 +8,19 @@ export declare class MySqlDialect extends MysqlLikeSqlDialect {
6
8
  * "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
7
9
  */
8
10
  protected readonly upsertNewRowAlias = "_uql_new";
9
- /** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
11
+ /**
12
+ * 26.7 may plan the correlated `JSON_TABLE` as a semijoin materialized once for the whole table, which
13
+ * answers every row with one row's elements. Verified in `mysqlJsonElemMatch.test.ts`.
14
+ */
15
+ protected readonly jsonElemHint = "/*+ NO_SEMIJOIN() */";
16
+ /**
17
+ * `JSON_OVERLAPS`, which a multi-valued index serves: verified in `mysqlJsonArrayIndex.test.ts`. It reads
18
+ * a scalar as an array of one, so the value must be an array.
19
+ */
20
+ protected jsonAny(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
21
+ /**
22
+ * A `SET_VAR` hint, which MySQL reads only right after the statement's own `SELECT` keyword - before
23
+ * `DISTINCT`, and never in a subquery - so it is anchored to the start rather than found.
24
+ */
10
25
  protected applySettings(sql: string, settings: readonly string[]): string;
11
26
  }
@@ -7,8 +7,24 @@ export class MySqlDialect extends MysqlLikeSqlDialect {
7
7
  * "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
8
8
  */
9
9
  upsertNewRowAlias = UPSERT_NEW_ROW_ALIAS;
10
- /** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
10
+ /**
11
+ * 26.7 may plan the correlated `JSON_TABLE` as a semijoin materialized once for the whole table, which
12
+ * answers every row with one row's elements. Verified in `mysqlJsonElemMatch.test.ts`.
13
+ */
14
+ jsonElemHint = '/*+ NO_SEMIJOIN() */';
15
+ /**
16
+ * `JSON_OVERLAPS`, which a multi-valued index serves: verified in `mysqlJsonArrayIndex.test.ts`. It reads
17
+ * a scalar as an array of one, so the value must be an array.
18
+ */
19
+ jsonAny(ctx, slot, values) {
20
+ const overlaps = `JSON_OVERLAPS(${this.jsonValue(slot)}, ${this.addValue(ctx, JSON.stringify(values))})`;
21
+ return `(${this.jsonIsArray(slot)} AND ${overlaps})`;
22
+ }
23
+ /**
24
+ * A `SET_VAR` hint, which MySQL reads only right after the statement's own `SELECT` keyword - before
25
+ * `DISTINCT`, and never in a subquery - so it is anchored to the start rather than found.
26
+ */
11
27
  applySettings(sql, settings) {
12
- return sql.replace('SELECT ', `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
28
+ return sql.replace(/^SELECT /, `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
13
29
  }
14
30
  }
@@ -1,5 +1,6 @@
1
1
  import { AbstractSqlDialect, type DerivedRelation, type HydrateKind, type RelationRows } from '../dialect/abstractSqlDialect.js';
2
- import type { EntityMeta, FieldOptions, QueryContext, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
2
+ import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
3
+ import type { EntityMeta, FieldOptions, QueryContext, QueryPager, QueryTextSearchOptions, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
4
  /** What SQLite and the engines derived from it have. */
4
5
  export declare const SQLITE_FEATURES: SqlDialectFeatures;
5
6
  export declare class SqliteDialect extends AbstractSqlDialect {
@@ -58,25 +59,25 @@ export declare class SqliteDialect extends AbstractSqlDialect {
58
59
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
59
60
  */
60
61
  protected appendTextSearch<E>(ctx: QueryContext, entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
62
+ protected jsonLength(slot: JsonSlot): string;
63
+ /** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
64
+ protected jsonElemFrom(slot: JsonSlot, alias: string): string;
65
+ protected jsonIsArray(slot: JsonSlot): string;
66
+ /** An object element's `value` is its JSON text, which its fields are paths into. */
67
+ protected jsonElemDoc(alias: string): string;
61
68
  /**
62
- * Each element is read back as JSON text through `->` at its own `fullkey`, so it compares
63
- * correctly whatever its type. `JSON_EACH`'s `value` column would not: it unquotes strings (`a`
64
- * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
69
+ * A scalar element's `value` is already typed, so a number and a string compare as such. Its JSON form
70
+ * is read back from the column at the element's own `fullkey`, since `value` flattens a boolean to 0/1.
65
71
  */
66
- protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
67
- protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
68
- /** `JSON_EACH` yields both scalar and object elements, so one form covers each case. */
69
- protected jsonElemFrom(jsonField: string, _fields: readonly string[], alias: string): string;
70
- protected jsonElemRef(alias: string, field?: string, asJson?: boolean): string;
71
- protected getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
72
+ protected jsonElemValue(slot: JsonSlot, alias: string, mode: JsonAccessMode): string;
73
+ /** `JSON_EXTRACT` already answers a number as one, and orders it by value. */
74
+ protected readonly jsonSortModes: readonly JsonAccessMode[];
75
+ /** `->` for the JSON value, and `JSON_EXTRACT` for SQLite's own: a number as a number, a string as text. */
76
+ protected jsonPathReading(escapedColumn: string, path: string, mode: 'json' | 'text'): string;
72
77
  protected numericCast(expr: string): string;
73
78
  protected jsonCast(operand: string): string;
74
- /**
75
- * `JSON_REPLACE` leaves an absent key (and a NULL column) untouched. Elements are read back
76
- * through `->` at their own `fullkey` so each keeps its JSON type - `JSON_EACH`'s `value` would
77
- * flatten booleans to 0/1 and stringify objects.
78
- */
79
- protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
79
+ /** `JSON` keeps each element's JSON type in the array, which answers `[]` for no rows. */
80
+ protected jsonArrayOf(elem: string): string;
80
81
  protected jsonSet(ctx: QueryContext, expr: string, set: Record<string, unknown>, field?: FieldOptions): string;
81
82
  /** `[#]` appends, creating the array where it is absent: `JSON_SET`, since Turso's `JSON_INSERT` will not touch an existing array. */
82
83
  protected jsonPush(ctx: QueryContext, expr: string, push: Record<string, unknown>): string;
@@ -1,7 +1,6 @@
1
1
  import { AbstractSqlDialect, } from '../dialect/abstractSqlDialect.js';
2
- import { JSON_ELEM_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
3
2
  import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
4
- import { chainedCall, groupsPerCall, jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
3
+ import { chainedCall, groupsPerCall, jsonSetCall, jsonPath, jsonArraySlotArgs, jsonSlotArgs, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
5
4
  import { textSearchFields } from '../util/dialect.util.js';
6
5
  import { columnFamily, isIntegerColumn } from '../util/field.util.js';
7
6
  /** What SQLite and the engines derived from it have. */
@@ -25,8 +24,6 @@ export const SQLITE_FEATURES = {
25
24
  rowLockOf: true,
26
25
  orderedUpsertReturning: true,
27
26
  orderedJsonAggregates: true,
28
- partialJsonContainment: false,
29
- typedJsonElements: true,
30
27
  narrowVectorTypes: false,
31
28
  vectorTuningNeedsTransaction: false,
32
29
  serialDeclaresPrimaryKey: true,
@@ -126,32 +123,38 @@ export class SqliteDialect extends AbstractSqlDialect {
126
123
  ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
127
124
  ctx.addValue(search.$value);
128
125
  }
126
+ jsonLength(slot) {
127
+ return `JSON_ARRAY_LENGTH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))})`;
128
+ }
129
+ /** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
130
+ jsonElemFrom(slot, alias) {
131
+ return `JSON_EACH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))}) ${alias}`;
132
+ }
133
+ jsonIsArray(slot) {
134
+ return `JSON_TYPE(${jsonSlotArgs(slot)}) = 'array'`;
135
+ }
136
+ /** An object element's `value` is its JSON text, which its fields are paths into. */
137
+ jsonElemDoc(alias) {
138
+ return `${alias}.value`;
139
+ }
129
140
  /**
130
- * Each element is read back as JSON text through `->` at its own `fullkey`, so it compares
131
- * correctly whatever its type. `JSON_EACH`'s `value` column would not: it unquotes strings (`a`
132
- * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
141
+ * A scalar element's `value` is already typed, so a number and a string compare as such. Its JSON form
142
+ * is read back from the column at the element's own `fullkey`, since `value` flattens a boolean to 0/1.
133
143
  */
134
- jsonAll(ctx, jsonField, value) {
135
- const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
136
- const from = this.jsonElemFrom(jsonField, [], alias);
137
- const conditions = value.map((val) => jsonElemExists(from, [`${jsonField} -> ${alias}.fullkey = ${this.jsonScalarParam(ctx, val)}`]));
138
- return `(${conditions.join(' AND ')})`;
139
- }
140
- jsonSize(ctx, jsonField, value) {
141
- return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`JSON_ARRAY_LENGTH(${jsonField})`), value));
142
- }
143
- /** `JSON_EACH` yields both scalar and object elements, so one form covers each case. */
144
- jsonElemFrom(jsonField, _fields, alias) {
145
- return `JSON_EACH(${jsonField}) ${alias}`;
146
- }
147
- jsonElemRef(alias, field, asJson = false) {
148
- if (field === undefined) {
149
- return `${alias}.value`;
144
+ jsonElemValue(slot, alias, mode) {
145
+ if (mode === 'json') {
146
+ return `${slot.base} -> ${alias}.fullkey`;
150
147
  }
151
- return asJson ? `${alias}.value -> ${jsonPath(field)}` : `JSON_EXTRACT(${alias}.value, ${jsonPath(field)})`;
148
+ const value = this.jsonElemDoc(alias);
149
+ return mode === 'numeric' ? this.numericCast(value) : value;
152
150
  }
153
- getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
154
- return `JSON_EXTRACT(${escapedColumn}, ${jsonPath(jsonPathStr)})`;
151
+ /** `JSON_EXTRACT` already answers a number as one, and orders it by value. */
152
+ jsonSortModes = ['text'];
153
+ /** `->` for the JSON value, and `JSON_EXTRACT` for SQLite's own: a number as a number, a string as text. */
154
+ jsonPathReading(escapedColumn, path, mode) {
155
+ return mode === 'json'
156
+ ? `(${escapedColumn} -> ${jsonPath(path)})`
157
+ : `JSON_EXTRACT(${escapedColumn}, ${jsonPath(path)})`;
155
158
  }
156
159
  numericCast(expr) {
157
160
  return `CAST(${expr} AS REAL)`;
@@ -159,25 +162,18 @@ export class SqliteDialect extends AbstractSqlDialect {
159
162
  jsonCast(operand) {
160
163
  return `JSON(${operand})`;
161
164
  }
162
- /**
163
- * `JSON_REPLACE` leaves an absent key (and a NULL column) untouched. Elements are read back
164
- * through `->` at their own `fullkey` so each keeps its JSON type - `JSON_EACH`'s `value` would
165
- * flatten booleans to 0/1 and stringify objects.
166
- */
167
- jsonPullKey(ctx, expr, escapedCol, key, value) {
168
- const path = jsonPath(key);
169
- const elem = `${escapedCol} -> ${JSON_PULL_ALIAS}.fullkey`;
170
- const kept = `SELECT JSON_GROUP_ARRAY(JSON(${elem})) FROM JSON_EACH(${escapedCol}, ${path}) ${JSON_PULL_ALIAS} WHERE ${elem} <> ${this.jsonScalarParam(ctx, value)}`;
171
- return `JSON_REPLACE(${expr}, ${path}, (${kept}))`;
165
+ /** `JSON` keeps each element's JSON type in the array, which answers `[]` for no rows. */
166
+ jsonArrayOf(elem) {
167
+ return `JSON_GROUP_ARRAY(JSON(${elem}))`;
172
168
  }
173
169
  jsonSet(ctx, expr, set, field) {
174
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
170
+ return jsonSetCall((value) => this.jsonScalarParam(ctx, value), jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
175
171
  }
176
172
  /** `[#]` appends, creating the array where it is absent: `JSON_SET`, since Turso's `JSON_INSERT` will not touch an existing array. */
177
173
  jsonPush(ctx, expr, push) {
178
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, this.maxFunctionArgs, '[#]');
174
+ return jsonSetCall((value) => this.jsonScalarParam(ctx, value), expr, push, this.maxFunctionArgs, '[#]');
179
175
  }
180
176
  jsonUnset(_ctx, expr, unset) {
181
- return jsonRemoveCall('JSON_REMOVE', expr, unset, this.maxFunctionArgs);
177
+ return jsonRemoveCall(expr, unset, this.maxFunctionArgs);
182
178
  }
183
179
  }
@@ -135,10 +135,6 @@ export interface SqlDialectFeatures extends DialectFeatures {
135
135
  readonly orderedUpsertReturning: boolean;
136
136
  /** Whether a JSON aggregate takes an `ORDER BY` of its own; where not, a relation's rows keep their derived table's order. */
137
137
  readonly orderedJsonAggregates: boolean;
138
- /** Whether JSON array containment matches an object element that merely includes the given keys, as `@>` does. */
139
- readonly partialJsonContainment: boolean;
140
- /** Whether an exploded scalar JSON element keeps its SQL type, as SQLite's `JSON_EACH` does. */
141
- readonly typedJsonElements: boolean;
142
138
  /** Whether the engine has pgvector's `halfvec` and `sparsevec`; elsewhere both map onto `vector`. */
143
139
  readonly narrowVectorTypes: boolean;
144
140
  /** Whether an ANN index's tuning `SET` applies only inside a transaction, as `SET LOCAL` does. */
@@ -84,7 +84,7 @@ export type JsonUpdateOp<T = unknown> = {
84
84
  */
85
85
  type JsonUpdateOpFor<V, T = UnwrapJson<NonNullable<V>>> = [T] extends [never] ? never : IsMany<T> extends true ? never : JsonUpdateOp<T>;
86
86
  /** What an update takes beyond the value: `null` to clear an optional member, `raw` SQL, and JSON operators. */
87
- type UpdateExtra<V> = (undefined extends V ? null : never) | QueryRaw | JsonUpdateOpFor<V>;
87
+ type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | Raw | JsonUpdateOpFor<V>;
88
88
  /**
89
89
  * What a whole-record write persists: the fields and relations with their declared optionality, a
90
90
  * related row's alike, and no methods. Two mapped types, since asking each key costs a conditional.
@@ -97,10 +97,10 @@ export type EntityData<E, F extends keyof E = FieldKey<E>, R extends keyof E = R
97
97
  /** A relation's value as its rows' {@link EntityData}. */
98
98
  type RelationData<V> = V extends readonly (infer T)[] ? EntityData<T>[] : V extends object ? EntityData<V> : never;
99
99
  /** {@link EntityData} made partial, each member also taking its {@link UpdateExtra}. */
100
- export type UpdatePayload<E, F extends keyof E = FieldKey<E>, R extends keyof E = RelationKey<E>> = {
101
- [P in F]?: E[P] | UpdateExtra<E[P]>;
100
+ export type UpdatePayload<E, Raw = QueryRaw, F extends keyof E = FieldKey<E>, R extends keyof E = RelationKey<E>> = {
101
+ [P in F]?: E[P] | UpdateExtra<E[P], Raw>;
102
102
  } & {
103
- [P in R]?: E[P] | RelationData<E[P]> | UpdateExtra<E[P]>;
103
+ [P in R]?: E[P] | RelationData<E[P]> | UpdateExtra<E[P], Raw>;
104
104
  };
105
105
  /** The key's name where the entity states it, by the `idKey` brand or a conventional name; `never` otherwise. */
106
106
  export type NamedIdKey<E> = E extends {
@@ -181,7 +181,10 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
181
181
  readonly type?: FieldType;
182
182
  /** A vector column's dimensions: `@Field({ type: 'vector', dimensions: 1536 })`. */
183
183
  readonly dimensions?: number;
184
- /** The metric a vector search on this field uses unless it names its own `$distance`; `'cosine'` by default. */
184
+ /**
185
+ * The metric a vector search on this field uses unless it names its own `$distance`; by default its
186
+ * vector index's metric, else `'cosine'`.
187
+ */
185
188
  readonly distance?: VectorDistance;
186
189
  /** The entity this column is a foreign key to. */
187
190
  readonly references?: EntityGetter;
@@ -445,10 +448,12 @@ export type IndexJsonPath = {
445
448
  readonly path: string;
446
449
  /** How the value is read, matching what the queries over it compare against. */
447
450
  readonly type: FieldType;
451
+ /** Length of a string value, which MySQL keys as `CHAR(n)` and so requires. */
452
+ readonly length?: number;
448
453
  };
449
454
  /**
450
- * MySQL's multi-valued index, one key per element of the JSON array at `path`, what `$all` and
451
- * `$elemMatch` containment use; refused on any other engine.
455
+ * MySQL's multi-valued index, one key per element of the JSON array at `path`, which `$all` and an
456
+ * `$elemMatch` on one value or several use; refused on any other engine.
452
457
  * @example `@Index((user) => [{ column: user.tags, jsonArray: { type: String, length: 64 } }])`
453
458
  */
454
459
  export type IndexJsonArray = {
@@ -37,7 +37,7 @@ export type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {
37
37
  * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`
38
38
  * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.
39
39
  */
40
- export type QuerySelectValue<E> = QuerySelect<E> | readonly QueryRaw[];
40
+ export type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];
41
41
  /**
42
42
  * Fields to exclude from the query result - `{ name: true }` blacklists fields.
43
43
  * Mutually exclusive with positive field selections in `$select`.
@@ -46,8 +46,8 @@ export type QueryExclude<E> = QuerySelect<E>;
46
46
  /**
47
47
  * relation population map.
48
48
  */
49
- export type QueryPopulate<E, R extends keyof E = RelationKey<E>> = {
50
- [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;
49
+ export type QueryPopulate<E, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {
50
+ [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;
51
51
  };
52
52
  /**
53
53
  * The key a read carries its relation tallies under. One spelling for the type and the runtime that
@@ -59,8 +59,8 @@ export declare const COUNT_RESULT_KEY = "_count";
59
59
  * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes
60
60
  * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.
61
61
  */
62
- export type QueryCount<E, R extends keyof E = ToManyRelationKey<E>> = {
63
- [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;
62
+ export type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {
63
+ [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;
64
64
  };
65
65
  /**
66
66
  * query conflict paths - subset of field keys used to detect upsert conflicts.
@@ -69,7 +69,7 @@ export type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;
69
69
  /**
70
70
  * Options to populate a relation declared as `V`, by its cardinality.
71
71
  */
72
- export type QueryPopulateRelationOptions<V> = IsMany<V> extends true ? RelationQuery<RelationTarget<V>> : QueryUnique<RelationTarget<V>> & {
72
+ export type QueryPopulateRelationOptions<V, Raw = QueryRaw> = IsMany<V> extends true ? RelationQuery<RelationTarget<V>, Raw> : QueryUnique<RelationTarget<V>, Raw> & {
73
73
  $required?: boolean;
74
74
  };
75
75
  /**
@@ -158,24 +158,24 @@ export type QueryPager = {
158
158
  /**
159
159
  * Which rows a statement addresses.
160
160
  */
161
- export type QueryFilter<E> = {
161
+ export type QueryFilter<E, Raw = QueryRaw> = {
162
162
  /**
163
163
  * filtering options.
164
164
  */
165
- $where?: QueryWhere<E>;
165
+ $where?: QueryWhere<E, Raw>;
166
166
  };
167
167
  /**
168
168
  * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never
169
169
  * how many, so a count that accepted one would promise an influence it cannot have.
170
170
  */
171
- export type QueryPage<E> = QueryFilter<E> & QueryPager;
171
+ export type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;
172
172
  /**
173
173
  * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the
174
174
  * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a
175
175
  * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the
176
176
  * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.
177
177
  */
178
- export type QuerySearch<E> = QueryPage<E> & {
178
+ export type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {
179
179
  /**
180
180
  * sorting options.
181
181
  */
@@ -184,21 +184,21 @@ export type QuerySearch<E> = QueryPage<E> & {
184
184
  /**
185
185
  * query options.
186
186
  */
187
- export type Query<E> = {
187
+ export type Query<E, Raw = QueryRaw> = {
188
188
  /**
189
189
  * field selection - `{ name: true }` whitelists fields, or raw SQL projections
190
190
  * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).
191
191
  * Mutually exclusive with `$exclude`.
192
192
  */
193
- $select?: QuerySelectValue<E>;
193
+ $select?: QuerySelectValue<E, Raw>;
194
194
  /**
195
195
  * relation population options.
196
196
  */
197
- $populate?: QueryPopulate<E>;
197
+ $populate?: QueryPopulate<E, Raw>;
198
198
  /**
199
199
  * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.
200
200
  */
201
- $count?: QueryCount<E>;
201
+ $count?: QueryCount<E, Raw>;
202
202
  /**
203
203
  * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.
204
204
  * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept
@@ -227,7 +227,7 @@ export type Query<E> = {
227
227
  /**
228
228
  * filtering options.
229
229
  */
230
- $where?: QueryWhere<E>;
230
+ $where?: QueryWhere<E, Raw>;
231
231
  /**
232
232
  * Index from where start the search
233
233
  */
@@ -237,6 +237,11 @@ export type Query<E> = {
237
237
  */
238
238
  $limit?: number;
239
239
  };
240
+ /**
241
+ * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,
242
+ * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.
243
+ */
244
+ export type WireQuery<E> = Query<E, never>;
240
245
  /**
241
246
  * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query
242
247
  * check alike; `satisfies` keeps them in step with `Query`.
@@ -262,36 +267,36 @@ type RelationClause = (typeof QUERY_OBJECT_CLAUSES | typeof QUERY_NUMBER_CLAUSES
262
267
  * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot
263
268
  * drift, and a clause added to {@link Query} stays off it until it joins one of them.
264
269
  */
265
- export type RelationQuery<E = object> = Pick<Query<E>, RelationClause> & {
270
+ export type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {
266
271
  $required?: boolean;
267
272
  };
268
273
  /**
269
274
  * options to get a single record.
270
275
  */
271
- export type QueryOne<E> = Except<Query<E>, '$limit'>;
276
+ export type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;
272
277
  /**
273
278
  * options to get an unique record.
274
279
  */
275
- export type QueryUnique<E> = Pick<QueryOne<E>, '$select' | '$exclude' | '$populate' | '$where'>;
280
+ export type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;
276
281
  /**
277
282
  * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips
278
283
  * excess-property checks, while a key set fails its own constraint on a typo.
279
284
  * @internal
280
285
  */
281
- type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = {
282
- $select?: QuerySelect<E, S, V> | readonly QueryRaw[];
286
+ type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>, Raw = QueryRaw> = {
287
+ $select?: QuerySelect<E, S, V> | readonly Raw[];
283
288
  $exclude?: QuerySelect<E, X, V>;
284
- $populate?: QueryPopulate<E, P>;
285
- $count?: QueryCount<E, C & ToManyRelationKey<E>>;
289
+ $populate?: QueryPopulate<E, Raw, P>;
290
+ $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;
286
291
  };
287
292
  /**
288
293
  * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.
289
294
  */
290
- export type QueryProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never> = Query<E> & QueryProjection<E, S, V, X, P, C>;
295
+ export type QueryProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never, Raw = QueryRaw> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;
291
296
  /**
292
297
  * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.
293
298
  */
294
- export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never> = QueryOne<E> & QueryProjection<E, S, V, X, P, C>;
299
+ export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never, Raw = QueryRaw> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;
295
300
  /**
296
301
  * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every
297
302
  * field minus what `$select` or `$exclude` subtracts, plus the populated relations.