uql-orm 0.56.0 → 0.58.0

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 (110) hide show
  1. package/README.md +7 -9
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  5. package/dist/cockroachdb/cockroachDialect.js +2 -10
  6. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  7. package/dist/d1/d1SqliteDialect.js +2 -0
  8. package/dist/dialect/abstractSqlDialect.d.ts +196 -33
  9. package/dist/dialect/abstractSqlDialect.js +410 -203
  10. package/dist/dialect/aliases.d.ts +10 -7
  11. package/dist/dialect/aliases.js +12 -7
  12. package/dist/dialect/hydrateColumn.d.ts +8 -2
  13. package/dist/dialect/hydrateColumn.js +33 -1
  14. package/dist/dialect/jsonSql.d.ts +13 -5
  15. package/dist/dialect/jsonSql.js +24 -7
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
  17. package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  19. package/dist/dialect/pgLikeSqlDialect.js +23 -48
  20. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  21. package/dist/dialect/pgVectorMetrics.js +17 -0
  22. package/dist/dialect/queryContext.d.ts +3 -7
  23. package/dist/dialect/queryContext.js +13 -8
  24. package/dist/dialect/queryJoins.d.ts +8 -4
  25. package/dist/dialect/queryJoins.js +26 -11
  26. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  27. package/dist/dialect/vectorSqlDialect.js +2 -3
  28. package/dist/entity/decorator/bag.d.ts +2 -2
  29. package/dist/entity/decorator/entity.d.ts +8 -9
  30. package/dist/entity/decorator/entity.js +6 -7
  31. package/dist/entity/decorator/members.d.ts +7 -6
  32. package/dist/entity/decorator/members.js +2 -1
  33. package/dist/entity/metadata/definition.d.ts +16 -11
  34. package/dist/entity/metadata/definition.js +54 -42
  35. package/dist/http/handler.d.ts +2 -2
  36. package/dist/http/handler.js +0 -1
  37. package/dist/maria/mariaDialect.d.ts +13 -6
  38. package/dist/maria/mariaDialect.js +29 -9
  39. package/dist/migrate/cli.d.ts +2 -3
  40. package/dist/migrate/cli.js +2 -2
  41. package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
  42. package/dist/migrate/codegen/entityTypes.d.ts +1 -1
  43. package/dist/migrate/codegen/entityTypes.js +4 -3
  44. package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
  45. package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
  46. package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
  47. package/dist/migrate/codegen/sourceLiteral.js +4 -0
  48. package/dist/migrate/ddl/index.d.ts +1 -5
  49. package/dist/migrate/ddl/index.js +14 -25
  50. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  51. package/dist/migrate/ddl/indexDdl.js +17 -1
  52. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  53. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  54. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  55. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  56. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
  57. package/dist/migrate/ddl/pgIndexDdl.js +29 -12
  58. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
  59. package/dist/migrate/migrator.d.ts +4 -4
  60. package/dist/migrate/schemaGenerator.d.ts +8 -8
  61. package/dist/migrate/schemaGenerator.js +5 -7
  62. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  63. package/dist/mongo/mongoDialect.d.ts +31 -18
  64. package/dist/mongo/mongoDialect.js +146 -104
  65. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  66. package/dist/mongo/mongodbQuerier.js +31 -106
  67. package/dist/mssql/mssqlDialect.d.ts +16 -0
  68. package/dist/mssql/mssqlDialect.js +26 -4
  69. package/dist/mysql/mysqlDialect.d.ts +2 -0
  70. package/dist/mysql/mysqlDialect.js +4 -0
  71. package/dist/querier/abstractQuerier.d.ts +20 -36
  72. package/dist/querier/abstractQuerier.js +35 -129
  73. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  74. package/dist/querier/abstractSqlQuerier.d.ts +4 -17
  75. package/dist/querier/abstractSqlQuerier.js +40 -50
  76. package/dist/schema/canonicalType.js +4 -4
  77. package/dist/schema/indexDifferences.js +4 -4
  78. package/dist/schema/schemaASTBuilder.d.ts +3 -3
  79. package/dist/schema/schemaASTBuilder.js +32 -3
  80. package/dist/schema/schemaASTDiffer.js +5 -5
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +38 -7
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +3 -3
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +110 -69
  88. package/dist/type/migration.d.ts +7 -7
  89. package/dist/type/migratorDialect.d.ts +4 -0
  90. package/dist/type/querier.d.ts +6 -6
  91. package/dist/type/querierPool.d.ts +2 -2
  92. package/dist/type/query.d.ts +41 -72
  93. package/dist/type/query.js +10 -5
  94. package/dist/type/queryAggregate.d.ts +43 -34
  95. package/dist/type/queryAggregate.js +1 -1
  96. package/dist/type/queryWhere.d.ts +12 -9
  97. package/dist/type/universalQuerier.d.ts +4 -4
  98. package/dist/util/dialect.util.d.ts +4 -4
  99. package/dist/util/dialect.util.js +24 -15
  100. package/dist/util/field.util.d.ts +5 -0
  101. package/dist/util/field.util.js +19 -0
  102. package/dist/util/object.util.d.ts +2 -0
  103. package/dist/util/object.util.js +4 -0
  104. package/dist/util/relationQuery.util.d.ts +12 -65
  105. package/dist/util/relationQuery.util.js +27 -81
  106. package/dist/util/rowKey.util.d.ts +1 -11
  107. package/dist/util/rowKey.util.js +1 -13
  108. package/package.json +1 -1
  109. package/dist/querier/relationCount.d.ts +0 -16
  110. package/dist/querier/relationCount.js +0 -121
@@ -1,6 +1,5 @@
1
1
  import type { AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
- import { type ParentPartition } from '../util/index.js';
4
3
  import type { BuildUpdateResultPayload } from '../util/sql.util.js';
5
4
  import { AbstractQuerier } from './abstractQuerier.js';
6
5
  export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
@@ -58,19 +57,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
58
57
  */
59
58
  private applyVectorTuning;
60
59
  protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
61
- /**
62
- * One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
63
- * `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
64
- * `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
65
- *
66
- * Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
67
- * compound branch, and the wrapper costs nothing elsewhere.
68
- *
69
- * Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
70
- * `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
71
- * query, so neither can reach here.
72
- */
73
- protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
74
60
  /**
75
61
  * How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
76
62
  * `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
@@ -107,9 +93,10 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
107
93
  */
108
94
  private hydrateAll;
109
95
  /**
110
- * One row of {@link hydrateAll}. `visited` guards a populated graph that points back at itself, and
111
- * makes a node two paths reach decode once. Only a populated relation can lead the walk back, so the
112
- * guard is created at the first one a row carries: rows that populated nothing never allocate one.
96
+ * One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
97
+ * joined and unflattened, there only when its key is, since an unmatched join still fills a computed
98
+ * column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
99
+ * Each is an object of its own, so the walk reaches none twice.
113
100
  */
114
101
  private hydrateFields;
115
102
  /**
@@ -1,7 +1,8 @@
1
1
  import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
2
2
  import { decodeColumn } from '../dialect/hydrateColumn.js';
3
3
  import { getMeta, idOf, namesKey } from '../entity/index.js';
4
- import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, getRelationRequestSummary, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
4
+ import { COUNT_RESULT_KEY } from '../type/index.js';
5
+ import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, idOnlyQuery, isAutoIncrement, isPagedQuery, isRecord, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
5
6
  import { AbstractQuerier } from './abstractQuerier.js';
6
7
  import { enrichError } from './queryError.js';
7
8
  /**
@@ -153,24 +154,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
153
154
  }
154
155
  }
155
156
  async internalFindMany(entity, q, opts) {
156
- return this.hydrateRows(entity, q, await this.selectRows(entity, q, opts));
157
- }
158
- /**
159
- * One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
160
- * `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
161
- * `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
162
- *
163
- * Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
164
- * compound branch, and the wrapper costs nothing elsewhere.
165
- *
166
- * Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
167
- * `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
168
- * query, so neither can reach here.
169
- */
170
- async internalFindManyPerParent(entity, q, partition) {
171
- const ctx = this.dialect.createContext();
172
- this.dialect.findPerParent(ctx, entity, q, partition);
173
- return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
157
+ return this.hydrateRows(entity, await this.selectRows(entity, q, opts));
174
158
  }
175
159
  /**
176
160
  * How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
@@ -208,7 +192,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
208
192
  for (const row of rows) {
209
193
  delete row[TOTAL_ALIAS];
210
194
  }
211
- return [await this.hydrateRows(entity, q, rows), total];
195
+ return [this.hydrateRows(entity, rows), total];
212
196
  }
213
197
  async selectRows(entity, q, opts, totalAlias) {
214
198
  this.assertLockable(entity, q);
@@ -222,12 +206,9 @@ export class AbstractSqlQuerier extends AbstractQuerier {
222
206
  this.dialect.find(ctx, entity, q, opts, totalAlias);
223
207
  return this.all(ctx.sql, ctx.values);
224
208
  }
225
- async hydrateRows(entity, q, rows) {
209
+ hydrateRows(entity, rows) {
226
210
  const founds = unflatObjects(rows);
227
211
  this.hydrateAll(entity, founds);
228
- if (q.$populate) {
229
- await this.fillToManyRelations(entity, founds, q.$populate);
230
- }
231
212
  return founds;
232
213
  }
233
214
  async *internalFindManyStream(entity, q, opts) {
@@ -237,10 +218,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
237
218
  await this.applyVectorTuning(entity, q);
238
219
  }
239
220
  const meta = getMeta(entity);
240
- const { toManyKeys } = getRelationRequestSummary(meta, q.$populate);
241
- if (toManyKeys.length) {
242
- throw new TypeError(`findManyStream does not load to-many relations (${toManyKeys.join(', ')}). Use findMany so fillToManyRelations can run, or omit those keys from the stream query.`);
243
- }
244
221
  // The one path that does not go through `all`/`run`, so it connects on its own: streaming first on
245
222
  // a freshly acquired querier used to reach `getConn()` with nothing acquired.
246
223
  await this.lazyConnect();
@@ -276,23 +253,20 @@ export class AbstractSqlQuerier extends AbstractQuerier {
276
253
  * rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
277
254
  * only sparse on Postgres.
278
255
  */
279
- hydrateAll(entity, dtos, visited) {
256
+ hydrateAll(entity, dtos) {
280
257
  const meta = getMeta(entity);
281
258
  const fields = this.dialect.hydratableFields(entity);
282
259
  for (const dto of dtos) {
283
- this.hydrateFields(meta, fields, dto, visited);
260
+ this.hydrateFields(meta, fields, dto);
284
261
  }
285
262
  }
286
263
  /**
287
- * One row of {@link hydrateAll}. `visited` guards a populated graph that points back at itself, and
288
- * makes a node two paths reach decode once. Only a populated relation can lead the walk back, so the
289
- * guard is created at the first one a row carries: rows that populated nothing never allocate one.
264
+ * One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
265
+ * joined and unflattened, there only when its key is, since an unmatched join still fills a computed
266
+ * column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
267
+ * Each is an object of its own, so the walk reaches none twice.
290
268
  */
291
- hydrateFields(meta, fields, dto, visited) {
292
- if (!dto || typeof dto !== 'object' || visited?.has(dto)) {
293
- return;
294
- }
295
- visited?.add(dto);
269
+ hydrateFields(meta, fields, dto) {
296
270
  const row = dto;
297
271
  for (const [key, kind] of fields) {
298
272
  const value = row[key];
@@ -300,23 +274,39 @@ export class AbstractSqlQuerier extends AbstractQuerier {
300
274
  row[key] = decodeColumn(value, kind);
301
275
  }
302
276
  }
277
+ // A tally read inside the statement comes back as its driver reads a COUNT, which on some is text.
278
+ const counts = row[COUNT_RESULT_KEY];
279
+ if (isRecord(counts)) {
280
+ for (const relKey in counts) {
281
+ counts[relKey] = Number(counts[relKey]);
282
+ }
283
+ }
303
284
  // The value is read before the relation's target is resolved: a query that populated nothing
304
285
  // still walks every relation the entity declares, and `rel.entity()` is a call per row per
305
286
  // relation that only the populated ones need.
306
287
  for (const key in meta.relations) {
307
288
  const value = row[key];
308
- if (!value || typeof value !== 'object')
289
+ if (!value)
309
290
  continue;
310
291
  const rel = meta.relations[key];
311
292
  if (!rel)
312
293
  continue;
313
- visited ??= new WeakSet([dto]);
314
294
  const relEntity = rel.entity();
315
- if (Array.isArray(value)) {
316
- this.hydrateAll(relEntity, value, visited);
317
- continue;
295
+ if (typeof value === 'string' || Array.isArray(value)) {
296
+ // A to-many's rows, as flat as a statement's own.
297
+ const rows = unflatObjects(typeof value === 'string' ? JSON.parse(value) : value);
298
+ row[key] = rows;
299
+ this.hydrateAll(relEntity, rows);
300
+ }
301
+ else if (isRecord(value)) {
302
+ const relMeta = getMeta(relEntity);
303
+ if (value[relMeta.ids[0]] == null) {
304
+ delete row[key];
305
+ }
306
+ else {
307
+ this.hydrateFields(relMeta, this.dialect.hydratableFields(relEntity), value);
308
+ }
318
309
  }
319
- this.hydrateFields(getMeta(relEntity), this.dialect.hydratableFields(relEntity), value, visited);
320
310
  }
321
311
  }
322
312
  /**
@@ -339,17 +329,17 @@ export class AbstractSqlQuerier extends AbstractQuerier {
339
329
  async internalAggregate(entity, q, opts) {
340
330
  const ctx = this.dialect.createContext();
341
331
  this.dialect.aggregate(ctx, entity, q, opts);
342
- // oxlint-disable-next-line typescript/no-explicit-any -- raw DB rows satisfy QueryAggregateResult at runtime but TS can't verify
343
- const res = await this.all(ctx.sql, ctx.values);
332
+ const rows = await this.all(ctx.sql, ctx.values);
344
333
  const hydratable = this.dialect.hydratableAggregates(entity, q);
345
- for (const row of res) {
334
+ for (const row of rows) {
335
+ const cells = row;
346
336
  for (const [alias, kind] of hydratable) {
347
- if (row[alias] != null) {
348
- row[alias] = decodeColumn(row[alias], kind);
337
+ if (cells[alias] != null) {
338
+ cells[alias] = decodeColumn(cells[alias], kind);
349
339
  }
350
340
  }
351
341
  }
352
- return res;
342
+ return rows;
353
343
  }
354
344
  async internalInsertMany(entity, rows) {
355
345
  const meta = getMeta(entity);
@@ -6,7 +6,7 @@
6
6
  * - Canonical types (dialect-agnostic)
7
7
  * - TypeScript types (for entity generation)
8
8
  */
9
- import { columnFamily } from '../util/field.util.js';
9
+ import { columnFamily, isIntegerColumn } from '../util/field.util.js';
10
10
  /** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
11
11
  export function isVectorCategory(category) {
12
12
  return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
@@ -395,9 +395,9 @@ export function fieldOptionsToCanonical(options) {
395
395
  case 'numeric':
396
396
  // BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
397
397
  // to happen, and the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
398
- return type === Number && (options.precision || options.scale)
399
- ? { category: 'decimal', precision: options.precision, scale: options.scale }
400
- : { category: 'integer', size: 'big' };
398
+ return isIntegerColumn(options)
399
+ ? { category: 'integer', size: 'big' }
400
+ : { category: 'decimal', precision: options.precision, scale: options.scale };
401
401
  case 'boolean':
402
402
  return { category: 'boolean' };
403
403
  case 'date':
@@ -61,20 +61,20 @@ export function describeIndexDifferences(source, target, facets) {
61
61
  if (comparableEntries) {
62
62
  const [sourceColumns, targetColumns] = [source.entries, target.entries].map((entries) => entries.map((entry) => entrySignature(entry, facets)).join(', '));
63
63
  if (sourceColumns !== targetColumns) {
64
- differences.push(`columns: (${targetColumns}) → (${sourceColumns})`);
64
+ differences.push(`columns: (${targetColumns}) -> (${sourceColumns})`);
65
65
  }
66
66
  }
67
67
  if (source.unique !== target.unique) {
68
- differences.push(`unique: ${target.unique} → ${source.unique}`);
68
+ differences.push(`unique: ${target.unique} -> ${source.unique}`);
69
69
  }
70
70
  if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
71
- differences.push(`type: ${target.type ?? 'btree'} → ${source.type ?? 'btree'}`);
71
+ differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
72
72
  }
73
73
  if (facets.has('include')) {
74
74
  // Order carries no meaning in an `INCLUDE` list, so it is compared as a set.
75
75
  const [sourceInclude, targetInclude] = [source.include ?? [], target.include ?? []].map((columns) => [...columns].sort().join(', '));
76
76
  if (sourceInclude !== targetInclude) {
77
- differences.push(`include: (${targetInclude}) → (${sourceInclude})`);
77
+ differences.push(`include: (${targetInclude}) -> (${sourceInclude})`);
78
78
  }
79
79
  }
80
80
  return differences;
@@ -15,9 +15,9 @@ import { type CanonicalType, type ForeignKeyAction } from './types.js';
15
15
  */
16
16
  export interface BuildSchemaASTOptions {
17
17
  /** Custom resolver for a table's own name, unqualified. */
18
- resolveTableName?: (meta: EntityMeta<unknown>) => string;
18
+ resolveTableName?: (meta: EntityMeta<object>) => string;
19
19
  /** Custom resolver for the schema a table lives in; `undefined` leaves it unqualified. */
20
- resolveSchema?: (meta: EntityMeta<unknown>) => string | undefined;
20
+ resolveSchema?: (meta: EntityMeta<object>) => string | undefined;
21
21
  /** Custom column name resolver */
22
22
  resolveColumnName?: (key: string, field: FieldOptions) => string;
23
23
  /** Naming strategy to use */
@@ -31,7 +31,7 @@ export interface BuildSchemaASTOptions {
31
31
  * Three passes, because each needs the one before it to have finished for *every* entity: a relation
32
32
  * resolves against a table another entity declares, and an index against the columns of its own.
33
33
  */
34
- export declare function buildSchemaAST(entities: readonly Type<unknown>[], options?: BuildSchemaASTOptions): SchemaAST;
34
+ export declare function buildSchemaAST(entities: readonly Type<object>[], options?: BuildSchemaASTOptions): SchemaAST;
35
35
  /**
36
36
  * Resolve the canonical type for a field, inheriting from the referenced
37
37
  * entity's primary key when the field is a foreign-key reference
@@ -168,8 +168,8 @@ function addRelationshipsFromEntity(ctx, meta) {
168
168
  }
169
169
  }
170
170
  /**
171
- * Add indexes from field options (`@Field({ index })`) and from `@Index([...])`, which have nothing
172
- * in common beyond their target table.
171
+ * Add indexes from field options (`@Field({ index })`), from `@Index`, and for every foreign
172
+ * key none of those already serves.
173
173
  */
174
174
  function addIndexesFromEntity(ctx, meta) {
175
175
  const table = tableOf(ctx, meta);
@@ -194,6 +194,35 @@ function addIndexesFromEntity(ctx, meta) {
194
194
  for (const idxMeta of meta.indexes ?? []) {
195
195
  addCompositeIndex(ctx, table, meta, idxMeta);
196
196
  }
197
+ addForeignKeyIndexes(ctx, meta, table);
198
+ }
199
+ /**
200
+ * An index over each foreign key the table owns, unless one already leads with its columns or a field
201
+ * of it says `index: false`. A relation looks its rows up by these columns, and MySQL alone indexes
202
+ * them on its own. Last, so it sees every index the entity declared.
203
+ */
204
+ function addForeignKeyIndexes(ctx, meta, table) {
205
+ const optedOut = new Set(definedEntries(meta.fields).flatMap(([key, field]) => field.index === false ? [ctx.resolveColumnName(key, field)] : []));
206
+ for (const { from } of table.outgoingRelations) {
207
+ const columns = from.columns.map((column) => column.name);
208
+ if (columns.some((column) => optedOut.has(column)) || isIndexedBy(table, columns))
209
+ continue;
210
+ ctx.ast.addIndex({
211
+ name: derivedIndexName(table.name, columns),
212
+ table,
213
+ entries: columns.map((column) => ({ column })),
214
+ unique: false,
215
+ source: 'entity',
216
+ syncStatus: 'entity_only',
217
+ });
218
+ }
219
+ }
220
+ /** Whether the key, a unique column or an index already leads with `columns`, which is all a lookup needs. */
221
+ function isIndexedBy(table, columns) {
222
+ const leads = (indexed) => columns.every((column, at) => indexed[at] === column);
223
+ return (leads(table.primaryKey.map((column) => column.name)) ||
224
+ (columns.length === 1 && table.columns.get(columns[0])?.isUnique === true) ||
225
+ table.indexes.some((index) => leads(index.entries.map((entry) => entry.expression || entry.jsonPath || entry.jsonArray ? undefined : entry.column))));
197
226
  }
198
227
  /** An `include` column is named like any other, so a naming strategy has to reach it too. */
199
228
  function resolveIncludeColumn(ctx, meta, column) {
@@ -201,7 +230,7 @@ function resolveIncludeColumn(ctx, meta, column) {
201
230
  return field ? ctx.resolveColumnName(column, field) : column;
202
231
  }
203
232
  /**
204
- * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
233
+ * One `@Index`. Its entries keep the authored form (expression, prefix length, order) with
205
234
  * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
206
235
  * subset, which is what diffing and introspection compare.
207
236
  */
@@ -179,7 +179,7 @@ function diffColumn(tableName, source, target, opts) {
179
179
  const impliedNotNull = source.isPrimaryKey && target.isPrimaryKey;
180
180
  const typeChanged = !generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type));
181
181
  if (typeChanged) {
182
- differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
182
+ differences.push(`type: ${formatType(source.type)} -> ${formatType(target.type)}`);
183
183
  }
184
184
  // Signedness is the one thing compared on a generated key, because it is the one part of the serial
185
185
  // spelling that does round trip - and a key left unsigned refuses every foreign key pointing at it,
@@ -187,14 +187,14 @@ function diffColumn(tableName, source, target, opts) {
187
187
  // a database created before the serial became signed could never gain a foreign key.
188
188
  const signednessChanged = generatedType && !!source.type.unsigned !== !!target.type.unsigned;
189
189
  if (signednessChanged) {
190
- differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
190
+ differences.push(`type: ${formatType(source.type)} -> ${formatType(target.type)}`);
191
191
  }
192
192
  if (!impliedNotNull && source.nullable !== target.nullable) {
193
- differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
193
+ differences.push(`nullable: ${target.nullable} -> ${source.nullable}`);
194
194
  }
195
195
  // Compare unique constraint
196
196
  if (source.isUnique !== target.isUnique) {
197
- differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
197
+ differences.push(`unique: ${target.isUnique} -> ${source.isUnique}`);
198
198
  }
199
199
  // Four things are deliberately not compared, all for one reason: a difference here could only be
200
200
  // reported, never settled, because no statement this generator emits would change it.
@@ -206,7 +206,7 @@ function diffColumn(tableName, source, target, opts) {
206
206
  // The same rule `describeIndexDifferences` follows for what it cannot read.
207
207
  // Compare default values (if both defined)
208
208
  if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
209
- differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
209
+ differences.push(`default: ${target.defaultValue ?? 'NULL'} -> ${source.defaultValue ?? 'NULL'}`);
210
210
  }
211
211
  if (differences.length === 0) {
212
212
  return undefined;
@@ -1,4 +1,4 @@
1
- import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
1
+ import { AbstractSqlDialect, type DerivedRelation, type HydrateKind, type RelationRows } from '../dialect/abstractSqlDialect.js';
2
2
  import type { DialectFeatures, EntityMeta, FieldOptions, QueryContext, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
3
  export declare class SqliteDialect extends AbstractSqlDialect {
4
4
  /** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
@@ -16,6 +16,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
16
16
  /** SQLite locks the whole database, not rows, so `$lock` has nothing to map onto. */
17
17
  readonly supportsRowLocks = false;
18
18
  readonly booleanLiteral = "integer";
19
+ /** A function call takes 127 arguments before SQLite 3.48, as libSQL and Turso embed. */
20
+ readonly maxFunctionArgs: number;
19
21
  readonly insertIdSource = "returning";
20
22
  /**
21
23
  * The [sqlite-vec](https://github.com/asg017/sqlite-vec) functions, which need that extension
@@ -36,6 +38,23 @@ export declare class SqliteDialect extends AbstractSqlDialect {
36
38
  * SQLite's own spelling of "no limit", where the MySQL family uses its largest `BIGINT`.
37
39
  */
38
40
  pager(ctx: QueryContext, opts: QueryPager): void;
41
+ /** `json_group_array` of each row's object, ordered by the sort terms carried out beside them. */
42
+ protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
43
+ /**
44
+ * `json_object` of each key and its column. A row wider than one call takes inserts the rest into it,
45
+ * addressing each key as one path segment.
46
+ */
47
+ protected jsonObject(pairs: DerivedRelation['pairs']): string;
48
+ /**
49
+ * An integer crosses JSON as its exact text, which JSON would round past 2^53, and bytes as hex, which
50
+ * `json_object` cannot hold at all. A real stays a number, since SQLite writes one to text with 15 digits.
51
+ */
52
+ protected readonly carriedFields: {
53
+ numeric: (expr: string, field: FieldOptions) => string;
54
+ blob: (expr: string) => string;
55
+ };
56
+ /** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
57
+ protected hydrateKind(field: FieldOptions | undefined): HydrateKind | undefined;
39
58
  /**
40
59
  * FTS5 matches the table itself rather than its columns, so this only works when the table *is* an
41
60
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
@@ -1,7 +1,9 @@
1
- import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
2
- import { JSON_ELEM_ALIAS_PREFIX, JSON_PULL_ALIAS } from '../dialect/aliases.js';
3
- import { jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget } from '../dialect/jsonSql.js';
1
+ import { AbstractSqlDialect, } from '../dialect/abstractSqlDialect.js';
2
+ import { JSON_ELEM_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
3
+ import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
4
+ import { chainedCall, groupsPerCall, jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
4
5
  import { textSearchFields } from '../util/dialect.util.js';
6
+ import { columnFamily, isIntegerColumn } from '../util/field.util.js';
5
7
  export class SqliteDialect extends AbstractSqlDialect {
6
8
  /** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
7
9
  featureDefaults = {
@@ -37,6 +39,8 @@ export class SqliteDialect extends AbstractSqlDialect {
37
39
  /** SQLite locks the whole database, not rows, so `$lock` has nothing to map onto. */
38
40
  supportsRowLocks = false;
39
41
  booleanLiteral = 'integer';
42
+ /** A function call takes 127 arguments before SQLite 3.48, as libSQL and Turso embed. */
43
+ maxFunctionArgs = 127;
40
44
  // SQLite supports `RETURNING` (including on `INSERT ... ON CONFLICT`), so IDs are exact per row.
41
45
  insertIdSource = 'returning';
42
46
  /**
@@ -82,6 +86,33 @@ export class SqliteDialect extends AbstractSqlDialect {
82
86
  }
83
87
  super.pager(ctx, opts);
84
88
  }
89
+ /** `json_group_array` of each row's object, ordered by the sort terms carried out beside them. */
90
+ appendRelationArray(ctx, rows) {
91
+ const { from, pairs, order } = this.derivedRelation(ctx, rows);
92
+ ctx.append(`(SELECT json_group_array(${this.jsonObject(pairs)}${order ? ` ORDER BY ${order}` : ''}) FROM ${from})`);
93
+ }
94
+ /**
95
+ * `json_object` of each key and its column. A row wider than one call takes inserts the rest into it,
96
+ * addressing each key as one path segment.
97
+ */
98
+ jsonObject(pairs) {
99
+ const perCall = groupsPerCall(this.maxFunctionArgs, 2);
100
+ const object = `json_object(${this.jsonObjectArgs(pairs.slice(0, perCall))})`;
101
+ const inserts = pairs.slice(perCall).map(([key, sql]) => `${this.escape(`$."${key}"`)}, ${sql}`);
102
+ return chainedCall('json_insert', object, inserts, 2, this.maxFunctionArgs);
103
+ }
104
+ /**
105
+ * An integer crosses JSON as its exact text, which JSON would round past 2^53, and bytes as hex, which
106
+ * `json_object` cannot hold at all. A real stays a number, since SQLite writes one to text with 15 digits.
107
+ */
108
+ carriedFields = {
109
+ numeric: (expr, field) => (isIntegerColumn(field) ? `CAST(${expr} AS TEXT)` : expr),
110
+ blob: (expr) => `${this.escape(BYTES_PREFIX)} || hex(${expr})`,
111
+ };
112
+ /** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
113
+ hydrateKind(field) {
114
+ return columnFamily(field?.type) === 'date' ? undefined : super.hydrateKind(field);
115
+ }
85
116
  /**
86
117
  * FTS5 matches the table itself rather than its columns, so this only works when the table *is* an
87
118
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
@@ -104,7 +135,7 @@ export class SqliteDialect extends AbstractSqlDialect {
104
135
  * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
105
136
  */
106
137
  jsonAll(ctx, jsonField, value) {
107
- const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
138
+ const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
108
139
  const from = this.jsonElemFrom(jsonField, [], alias);
109
140
  const conditions = value.map((val) => jsonElemExists(from, [`${jsonField} -> ${alias}.fullkey = ${this.jsonScalarParam(ctx, val)}`]));
110
141
  return `(${conditions.join(' AND ')})`;
@@ -143,7 +174,7 @@ export class SqliteDialect extends AbstractSqlDialect {
143
174
  return `JSON_REPLACE(${expr}, ${path}, (${kept}))`;
144
175
  }
145
176
  jsonSet(ctx, expr, set, field) {
146
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set);
177
+ return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
147
178
  }
148
179
  /**
149
180
  * `[#]` appends, creating the array when the key is absent.
@@ -153,9 +184,9 @@ export class SqliteDialect extends AbstractSqlDialect {
153
184
  * so it silently drops the element when the array already exists.
154
185
  */
155
186
  jsonPush(ctx, expr, push) {
156
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, '[#]');
187
+ return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, this.maxFunctionArgs, '[#]');
157
188
  }
158
189
  jsonUnset(_ctx, expr, unset) {
159
- return jsonRemoveCall('JSON_REMOVE', expr, unset);
190
+ return jsonRemoveCall('JSON_REMOVE', expr, unset, this.maxFunctionArgs);
160
191
  }
161
192
  }
@@ -12,4 +12,6 @@ import type { VectorDistance, VectorMetric } from '../type/index.js';
12
12
  export declare class TursoDialect extends LibsqlDialect {
13
13
  /** The Rust engine adds a dot-product distance to libSQL's cosine and L2. */
14
14
  readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
15
+ /** The Rust engine takes no `ORDER BY` inside an aggregate. */
16
+ protected readonly orderedAggregates = false;
15
17
  }
@@ -15,4 +15,6 @@ export class TursoDialect extends LibsqlDialect {
15
15
  ['l2', { fn: 'vector_distance_l2' }],
16
16
  ['inner', { fn: 'vector_distance_dot' }],
17
17
  ]);
18
+ /** The Rust engine takes no `ORDER BY` inside an aggregate. */
19
+ orderedAggregates = false;
18
20
  }
@@ -1,5 +1,5 @@
1
- import type { AbstractDialect } from '../dialect/abstractDialect.js';
2
1
  import type { ForeignKeyAction } from '../schema/types.js';
2
+ import type { MigratorDialect } from './migratorDialect.js';
3
3
  import type { Querier } from './querier.js';
4
4
  import type { QuerierPool } from './querierPool.js';
5
5
  import type { Type } from './utility.js';
@@ -12,12 +12,12 @@ export interface Config {
12
12
  * This is required for both the application and the migrations CLI.
13
13
  * Must expose {@link QuerierPool.dialect}; migrations and the CLI read `pool.dialect.dialectName`.
14
14
  */
15
- pool: QuerierPool<Querier, AbstractDialect>;
15
+ pool: QuerierPool<Querier, MigratorDialect>;
16
16
  /**
17
17
  * List of entity classes to be managed by the ORM.
18
18
  * If omitted, classes that completed `defineEntity` (including via `@Entity()`) are discovered via `getEntities()`.
19
19
  */
20
- entities?: Type<unknown>[];
20
+ entities?: Type<object>[];
21
21
  /**
22
22
  * The directory where migration files are stored.
23
23
  * @default './migrations'
@@ -33,12 +33,11 @@ export interface QueryContext {
33
33
  addValue(value: unknown): this;
34
34
  pushValue(...values: unknown[]): this;
35
35
  /**
36
- * A fresh derived-table/subquery alias, unique within this query: `nextAlias('_uql_elem')` returns
37
- * `'_uql_elem_1'`, then `'_uql_elem_2'`, etc. Needed wherever a hook (e.g. exploding a JSON array)
38
- * might recurse into itself at a deeper nesting level within the same query - a fixed, reused
39
- * alias would let the inner occurrence shadow the outer one it needs to correlate against.
36
+ * An alias for a table or row source the statement reads: `name`, or `name_2`, `name_3`... - the
37
+ * first no other took, so a nested one never shadows what it correlates against, and never `parent`,
38
+ * the one a correlated subquery compares against. Compared without case, as MySQL does on macOS.
40
39
  */
41
- nextAlias(prefix: string): string;
40
+ claimAlias(name: string, parent?: string): string;
42
41
  /**
43
42
  * A context for a fragment of this same statement: it renders its own SQL in isolation while
44
43
  * sharing the bound values and the generated aliases, so both stay unique and correctly numbered