uql-orm 0.55.0 → 0.57.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 (109) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/bunSql/bunSql.util.js +1 -1
  5. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  6. package/dist/cockroachdb/cockroachDialect.js +2 -10
  7. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  8. package/dist/d1/d1SqliteDialect.js +2 -0
  9. package/dist/dialect/abstractSqlDialect.d.ts +195 -32
  10. package/dist/dialect/abstractSqlDialect.js +406 -199
  11. package/dist/dialect/aliases.d.ts +10 -7
  12. package/dist/dialect/aliases.js +12 -7
  13. package/dist/dialect/hydrateColumn.d.ts +8 -2
  14. package/dist/dialect/hydrateColumn.js +33 -1
  15. package/dist/dialect/jsonSql.d.ts +13 -5
  16. package/dist/dialect/jsonSql.js +24 -7
  17. package/dist/dialect/mysqlLikeSqlDialect.d.ts +30 -2
  18. package/dist/dialect/mysqlLikeSqlDialect.js +58 -7
  19. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  20. package/dist/dialect/pgLikeSqlDialect.js +25 -50
  21. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  22. package/dist/dialect/pgVectorMetrics.js +17 -0
  23. package/dist/dialect/queryContext.d.ts +3 -7
  24. package/dist/dialect/queryContext.js +13 -8
  25. package/dist/dialect/queryJoins.d.ts +8 -4
  26. package/dist/dialect/queryJoins.js +27 -15
  27. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  28. package/dist/dialect/vectorSqlDialect.js +2 -3
  29. package/dist/entity/index.d.ts +1 -1
  30. package/dist/entity/index.js +1 -1
  31. package/dist/entity/metadata/definition.d.ts +4 -2
  32. package/dist/entity/metadata/definition.js +27 -29
  33. package/dist/maria/mariaDialect.d.ts +13 -6
  34. package/dist/maria/mariaDialect.js +29 -9
  35. package/dist/migrate/builder/splitSqlStatements.js +2 -2
  36. package/dist/migrate/cli.d.ts +2 -3
  37. package/dist/migrate/cli.js +4 -11
  38. package/dist/migrate/codegen/fieldOptionsSource.js +1 -1
  39. package/dist/migrate/ddl/index.d.ts +1 -5
  40. package/dist/migrate/ddl/index.js +14 -25
  41. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  42. package/dist/migrate/ddl/indexDdl.js +17 -1
  43. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  44. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  45. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  46. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  47. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -3
  48. package/dist/migrate/ddl/pgIndexDdl.js +29 -3
  49. package/dist/migrate/drift/driftDetector.js +21 -8
  50. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  51. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +1 -1
  52. package/dist/migrate/introspection/baseSqlIntrospector.js +65 -76
  53. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  54. package/dist/migrate/introspection/mysqlIntrospector.js +5 -8
  55. package/dist/migrate/introspection/sqliteIntrospector.js +2 -5
  56. package/dist/migrate/migrator.d.ts +7 -4
  57. package/dist/migrate/migrator.js +9 -14
  58. package/dist/migrate/schemaGenerator.d.ts +3 -3
  59. package/dist/migrate/schemaGenerator.js +7 -13
  60. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  61. package/dist/mongo/mongoDialect.d.ts +31 -18
  62. package/dist/mongo/mongoDialect.js +147 -108
  63. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  64. package/dist/mongo/mongodbQuerier.js +34 -108
  65. package/dist/mssql/mssqlDialect.d.ts +16 -0
  66. package/dist/mssql/mssqlDialect.js +26 -4
  67. package/dist/mysql/mysqlDialect.d.ts +2 -0
  68. package/dist/mysql/mysqlDialect.js +4 -0
  69. package/dist/querier/abstractQuerier.d.ts +20 -36
  70. package/dist/querier/abstractQuerier.js +44 -143
  71. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  72. package/dist/querier/abstractSqlQuerier.d.ts +11 -22
  73. package/dist/querier/abstractSqlQuerier.js +49 -53
  74. package/dist/schema/canonicalType.js +4 -6
  75. package/dist/schema/dependencyGraph.js +2 -4
  76. package/dist/schema/indexDifferences.js +5 -5
  77. package/dist/schema/schemaASTBuilder.js +34 -12
  78. package/dist/schema/schemaASTDiffer.d.ts +10 -2
  79. package/dist/schema/schemaASTDiffer.js +17 -16
  80. package/dist/schema/types.d.ts +1 -1
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +40 -8
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +2 -2
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +2 -1
  88. package/dist/type/migratorDialect.d.ts +4 -0
  89. package/dist/type/querier.d.ts +6 -6
  90. package/dist/type/query.d.ts +25 -48
  91. package/dist/type/query.js +10 -5
  92. package/dist/type/queryAggregate.d.ts +10 -10
  93. package/dist/type/queryAggregate.js +1 -1
  94. package/dist/type/universalQuerier.d.ts +4 -4
  95. package/dist/util/dialect.util.d.ts +8 -2
  96. package/dist/util/dialect.util.js +19 -0
  97. package/dist/util/field.util.d.ts +5 -0
  98. package/dist/util/field.util.js +19 -0
  99. package/dist/util/logger.d.ts +10 -1
  100. package/dist/util/logger.js +18 -0
  101. package/dist/util/object.util.d.ts +4 -0
  102. package/dist/util/object.util.js +8 -0
  103. package/dist/util/relationQuery.util.d.ts +15 -68
  104. package/dist/util/relationQuery.util.js +35 -83
  105. package/dist/util/rowKey.util.d.ts +1 -11
  106. package/dist/util/rowKey.util.js +1 -13
  107. package/package.json +1 -1
  108. package/dist/querier/relationCount.d.ts +0 -16
  109. package/dist/querier/relationCount.js +0 -121
@@ -1,7 +1,6 @@
1
- import { assertSoleId, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
2
- import { asSelectMap, childrenOf, clone, dataKeyed, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
1
+ import { assertSoleId, getMeta, idOf, namesKey, relationOf, soleIdOf } from '../entity/index.js';
2
+ import { childrenOf, clone, entityName, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
- import { fillRelationCounts, withIdForCounts } from './relationCount.js';
5
4
  /**
6
5
  * Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
7
6
  * `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
@@ -78,13 +77,10 @@ export class AbstractQuerier {
78
77
  logger;
79
78
  constructor(extra) {
80
79
  this.extra = extra;
81
- this.logger = new LoggerWrapper(extra?.logger, {
82
- logValues: extra?.logValues,
83
- slowQuery: extra?.slowQuery,
84
- });
80
+ this.logger = queryLoggerFor(extra);
85
81
  }
86
82
  validateProjectionQuery(entity, q) {
87
- this.validateProjectionQueryRecursive(entity, q, getMeta(entity).name ?? entity.name);
83
+ this.validateProjectionQueryRecursive(entity, q, entityName(getMeta(entity)));
88
84
  }
89
85
  validateProjectionQueryRecursive(entity, q, path) {
90
86
  const meta = getMeta(entity);
@@ -96,9 +92,7 @@ export class AbstractQuerier {
96
92
  }
97
93
  }
98
94
  forEachRequestedRelation(meta, q.$populate, (relKey, relValue) => {
99
- const relOpts = meta.relations[relKey];
100
- if (!relOpts)
101
- return;
95
+ const relOpts = relationOf(meta, relKey);
102
96
  const relEntity = relOpts.entity();
103
97
  const parsed = parseRelationQueryValue(relValue);
104
98
  if (parsed.nested) {
@@ -135,9 +129,12 @@ export class AbstractQuerier {
135
129
  async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
136
130
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
137
131
  this.validateProjectionQuery(entity, q);
138
- const founds = await this.internalFindMany(entity, withIdForCounts(entity, q), opts);
139
- await fillRelationCounts(this, entity, founds, q.$count);
140
- await this.emitHook(entity, 'afterLoad', founds);
132
+ const founds = await this.internalFindMany(entity, q, opts);
133
+ // Guarded here rather than only inside: awaiting a call that returns at once still costs every read
134
+ // a promise and a turn of the microtask queue, and most reads hook nothing.
135
+ if (this.listensForLoad(entity, q.$populate)) {
136
+ await this.emitLoaded(entity, founds, q.$populate);
137
+ }
141
138
  return founds;
142
139
  }
143
140
  findManyStream(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
@@ -148,9 +145,10 @@ export class AbstractQuerier {
148
145
  async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
149
146
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
150
147
  this.validateProjectionQuery(entity, q);
151
- const [founds, count] = await this.internalFindManyAndCount(entity, withIdForCounts(entity, q), opts);
152
- await fillRelationCounts(this, entity, founds, q.$count);
153
- await this.emitHook(entity, 'afterLoad', founds);
148
+ const [founds, count] = await this.internalFindManyAndCount(entity, q, opts);
149
+ if (this.listensForLoad(entity, q.$populate)) {
150
+ await this.emitLoaded(entity, founds, q.$populate);
151
+ }
154
152
  return [founds, count];
155
153
  }
156
154
  /**
@@ -341,125 +339,6 @@ export class AbstractQuerier {
341
339
  await (toInsert.length && toUpsert.length ? this.transaction(write) : write());
342
340
  return ids;
343
341
  }
344
- async fillToManyRelations(entity, payload, populate) {
345
- if (!payload.length) {
346
- return;
347
- }
348
- const meta = getMeta(entity);
349
- const relKeys = getRelationRequestSummary(meta, populate).toManyKeys;
350
- for (const relKey of relKeys) {
351
- const relOpts = meta.relations[relKey];
352
- if (!relOpts)
353
- continue;
354
- const relEntity = relOpts.entity();
355
- const relationQuery = clone(parseRelationAtKey(relKey, populate).query);
356
- if (relOpts.through) {
357
- await this.fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery);
358
- }
359
- else if (relOpts.cardinality === '1m') {
360
- await this.fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity);
361
- }
362
- }
363
- }
364
- async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
365
- const joins = parentJoins(relOpts, meta.ids.length);
366
- const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
367
- const throughEntity = relOpts.through();
368
- const throughMeta = getMeta(throughEntity);
369
- const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
370
- if (!targetRelKey) {
371
- // Asserted rather than assumed: used as a key regardless, it spells the literal string
372
- // `undefined`, and the statement asks the junction for a relation of that name.
373
- throw new TypeError(`'${meta.name}.${relKey}' goes through '${throughMeta.name}', which declares no relation on its ` +
374
- `'${targetColumn}' column. Give it one, so the target's rows can be read through it.`);
375
- }
376
- // A relation query names the target's columns, not the junction's, so its projection and filter
377
- // belong on the populate below, resolved against the entity that has them. Spread onto the
378
- // junction query instead they asked `ItemTag` for `Tag`'s columns and failed with "no such
379
- // column".
380
- //
381
- // Ordering and paging split the other way: they describe the statement with one row per pairing,
382
- // which is the junction's. Left on the populate they reached a to-one join, which rejects all
383
- // four by name - so a many-to-many carrying any of them threw rather than paging.
384
- //
385
- // Those four are not a coincidence: they are exactly the clauses a joined relation rejects, for
386
- // the same reason - each needs a statement with many rows per parent, which only the junction's
387
- // is. The `satisfies` ties the two lists together, so a fifth clause added there fails to compile
388
- // here rather than quietly staying on the populate and throwing again.
389
- const { $sort, $limit, $skip, $distinct, ...targetQuery } = relationQuery;
390
- const junctionClauses = {
391
- $limit,
392
- $skip,
393
- $distinct,
394
- // Qualified by the relation that reaches them, since the columns it names are the target's.
395
- $sort: $sort && { [targetRelKey]: $sort },
396
- };
397
- const junctionQuery = {
398
- $select: joinedColumns(joins),
399
- ...junctionClauses,
400
- $populate: {
401
- [targetRelKey]: {
402
- ...targetQuery,
403
- $required: true,
404
- },
405
- },
406
- };
407
- const throughFounds = await this.findChildrenOf(throughEntity, junctionQuery, joins, payload, meta.fields);
408
- // The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
409
- // reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
410
- const founds = throughFounds.map((it) => ({
411
- ...it[targetRelKey],
412
- ...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
413
- }));
414
- this.putChildrenInParents(payload, founds, joins, relKey);
415
- }
416
- async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
417
- const joins = parentJoins(relOpts, meta.ids.length);
418
- // The FK is what putChildrenInParents groups on, so it outlives the relation's projection
419
- // either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
420
- // dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
421
- const select = asSelectMap(relationQuery.$select);
422
- const exclude = relationQuery.$exclude;
423
- for (const { joined } of joins) {
424
- if (select && !select[joined]) {
425
- select[joined] = true;
426
- }
427
- delete exclude?.[joined];
428
- }
429
- this.putChildrenInParents(payload, await this.findChildrenOf(relEntity, relationQuery, joins, payload, meta.fields), joins, relKey);
430
- }
431
- /**
432
- * The children of a whole page of parents, however the relation asked for them: one bounded branch
433
- * per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
434
- * list, which is both correct and cheaper.
435
- *
436
- * The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
437
- * what they query, never in how the page is spread over its parents.
438
- */
439
- async findChildrenOf(entity, query, joins, parents, parentFields) {
440
- const founds = isBoundedPerParent(query)
441
- ? await this.internalFindManyPerParent(entity, query, { joins, parents, parentFields })
442
- : await this.findMany(entity, queryChildrenOfAll(query, joins, parents));
443
- // Read back as rows rather than as the entity they hydrate to: what follows regroups them by the
444
- // join columns, which a projected entity type does not carry.
445
- return founds;
446
- }
447
- putChildrenInParents(parents, children, joins, relKey) {
448
- const childrenByParentId = dataKeyed();
449
- // Every joined column, so two children agreeing on one column of a composite key are not
450
- // gathered under the same parent. Both column lists are read once, not once per row.
451
- const joinedKeys = keyColumns(joins, 'joined');
452
- const parentKeys = keyColumns(joins, 'parent');
453
- for (const child of children) {
454
- (childrenByParentId[rowKey(child, joinedKeys)] ??= []).push(child);
455
- }
456
- for (const parent of parents) {
457
- // `[]` rather than nothing for a parent with no children: a populated to-many is a list the
458
- // caller asked for, so it maps and counts without a guard, and its type can say so. An
459
- // unpopulated one stays absent, which is what tells the two apart.
460
- parent[relKey] = (childrenByParentId[rowKey(parent, parentKeys)] ?? []);
461
- }
462
- }
463
342
  async insertRelations(entity, payload) {
464
343
  const meta = getMeta(entity);
465
344
  const entries = payload.reduce((acc, it) => {
@@ -498,9 +377,7 @@ export class AbstractQuerier {
498
377
  const relKeys = filterPersistableRelationKeys(meta, meta.relations, 'delete');
499
378
  // Cascade forwards `opts` (including `hardDelete`); each child soft-deletes only if it can.
500
379
  for (const relKey of relKeys) {
501
- const relOpts = meta.relations[relKey];
502
- if (!relOpts)
503
- continue;
380
+ const relOpts = relationOf(meta, relKey);
504
381
  const relEntity = relOpts.entity();
505
382
  const target = relOpts.through ? relOpts.through() : relEntity;
506
383
  const where = childrenOf(parentJoins(relOpts, meta.ids.length), ids);
@@ -514,9 +391,7 @@ export class AbstractQuerier {
514
391
  */
515
392
  async saveRelation(entity, ids, relValue, relKey, isUpdate) {
516
393
  const meta = getMeta(entity);
517
- const relOpts = meta.relations[relKey];
518
- if (!relOpts)
519
- return;
394
+ const relOpts = relationOf(meta, relKey);
520
395
  // Here rather than only in the callers below: writing the parent's key into a child is one column
521
396
  // per key, so a composite takes a statement per parent. `soleParentColumn` and the sole
522
397
  // `targetKeyColumns` under it read the *first* pair, which is a real column of a wrong pairing
@@ -630,6 +505,32 @@ export class AbstractQuerier {
630
505
  hasHook(entity, event) {
631
506
  return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
632
507
  }
508
+ /** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
509
+ listensForLoad(entity, populate) {
510
+ if (this.hasHook(entity, 'afterLoad')) {
511
+ return true;
512
+ }
513
+ const meta = getMeta(entity);
514
+ return getRelationRequestSummary(meta, populate).requestedKeys.some((relKey) => this.listensForLoad(relationOf(meta, relKey).entity(), parseRelationAtKey(relKey, populate).query.$populate));
515
+ }
516
+ /**
517
+ * `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
518
+ * parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
519
+ * listens.
520
+ */
521
+ async emitLoaded(entity, rows, populate) {
522
+ const meta = getMeta(entity);
523
+ for (const relKey of getRelationRequestSummary(meta, populate).requestedKeys) {
524
+ const relEntity = relationOf(meta, relKey).entity();
525
+ const relPopulate = parseRelationAtKey(relKey, populate).query.$populate;
526
+ if (this.listensForLoad(relEntity, relPopulate)) {
527
+ // A to-many holds a list and a to-one a row, which `flatMap` takes alike; an absent one adds none.
528
+ const loaded = rows.flatMap((row) => row[relKey] ?? []);
529
+ await this.emitLoaded(relEntity, loaded, relPopulate);
530
+ }
531
+ }
532
+ await this.emitHook(entity, 'afterLoad', rows);
533
+ }
633
534
  /**
634
535
  * The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
635
536
  * not report them in payload order. A row that no read row matches, or that two do, keeps
@@ -684,7 +585,7 @@ export class AbstractQuerier {
684
585
  * another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
685
586
  * sequential rather than nested.
686
587
  */
687
- async serialize(task) {
588
+ serialize(task) {
688
589
  const res = this.taskQueue.then(task);
689
590
  this.taskQueue = res.catch(() => { });
690
591
  return res;
@@ -1,5 +1,5 @@
1
1
  import type { AbstractDialect } from '../dialect/index.js';
2
- import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
2
+ import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
3
3
  /**
4
4
  * Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
5
5
  */
@@ -31,7 +31,7 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
31
31
  * The connection outlives the call here: it is held until the iterator is drained or closed by a
32
32
  * `break`/`throw`. Abandoning the iterator instead leaks it until GC, so consume it in a `for await`.
33
33
  */
34
- findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P>>;
34
+ findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P, C>>;
35
35
  findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
36
36
  count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
37
37
  exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
@@ -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
@@ -100,14 +86,17 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
100
86
  */
101
87
  protected internalStream<T>(query: string, values?: unknown[]): AsyncIterable<T>;
102
88
  /**
103
- * Turn what a driver returned back into the types the entity declares, for the row and everything
104
- * populated under it. Which columns, and as what, is `hydratableFields`; the per-cell decode is
105
- * `decodeColumn`. Both live with the dialect, because a `sparsevec` is only sparse on Postgres.
106
- *
107
- * `visited` guards a populated graph that points back at itself, and makes a node two paths reach
108
- * decode once. Only a relation can lead the walk back somewhere it has been, so an entity that
109
- * declares none skips the guard rather than allocating a set per row to hold a single object -
110
- * which cost more than the decoding it guards, on a flat read.
89
+ * Turn what a driver returned back into the types the entity declares, for every row and everything
90
+ * populated under them. Which columns, and as what, is `hydratableFields`, resolved once for all the
91
+ * rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
92
+ * only sparse on Postgres.
93
+ */
94
+ private hydrateAll;
95
+ /**
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.
111
100
  */
112
101
  private hydrateFields;
113
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, hasKeys, 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,9 +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) {
226
- const founds = unflatObjects(rows).map((row) => this.hydrateFields(entity, row));
227
- await this.fillToManyRelations(entity, founds, q.$populate);
209
+ hydrateRows(entity, rows) {
210
+ const founds = unflatObjects(rows);
211
+ this.hydrateAll(entity, founds);
228
212
  return founds;
229
213
  }
230
214
  async *internalFindManyStream(entity, q, opts) {
@@ -234,10 +218,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
234
218
  await this.applyVectorTuning(entity, q);
235
219
  }
236
220
  const meta = getMeta(entity);
237
- const { toManyKeys } = getRelationRequestSummary(meta, q.$populate);
238
- if (toManyKeys.length) {
239
- 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.`);
240
- }
241
221
  // The one path that does not go through `all`/`run`, so it connects on its own: streaming first on
242
222
  // a freshly acquired querier used to reach `getConn()` with nothing acquired.
243
223
  await this.lazyConnect();
@@ -245,11 +225,14 @@ export class AbstractSqlQuerier extends AbstractQuerier {
245
225
  // context holds was normalized as it was bound.
246
226
  const ctx = this.dialect.createContext();
247
227
  this.dialect.find(ctx, entity, q, opts);
228
+ const fields = this.dialect.hydratableFields(entity);
248
229
  let attrsPaths;
249
230
  try {
250
231
  for await (const row of this.internalStream(ctx.sql, ctx.values)) {
251
232
  attrsPaths ??= obtainAttrsPaths(row);
252
- yield this.hydrateFields(entity, unflatObject(row, attrsPaths));
233
+ const found = unflatObject(row, attrsPaths);
234
+ this.hydrateFields(meta, fields, found);
235
+ yield found;
253
236
  }
254
237
  }
255
238
  catch (err) {
@@ -265,53 +248,66 @@ export class AbstractSqlQuerier extends AbstractQuerier {
265
248
  yield* await this.internalAll(query, values);
266
249
  }
267
250
  /**
268
- * Turn what a driver returned back into the types the entity declares, for the row and everything
269
- * populated under it. Which columns, and as what, is `hydratableFields`; the per-cell decode is
270
- * `decodeColumn`. Both live with the dialect, because a `sparsevec` is only sparse on Postgres.
271
- *
272
- * `visited` guards a populated graph that points back at itself, and makes a node two paths reach
273
- * decode once. Only a relation can lead the walk back somewhere it has been, so an entity that
274
- * declares none skips the guard rather than allocating a set per row to hold a single object -
275
- * which cost more than the decoding it guards, on a flat read.
251
+ * Turn what a driver returned back into the types the entity declares, for every row and everything
252
+ * populated under them. Which columns, and as what, is `hydratableFields`, resolved once for all the
253
+ * rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
254
+ * only sparse on Postgres.
276
255
  */
277
- hydrateFields(entity, dto, visited) {
278
- if (!dto || typeof dto !== 'object' || visited?.has(dto)) {
279
- return dto;
280
- }
256
+ hydrateAll(entity, dtos) {
281
257
  const meta = getMeta(entity);
258
+ const fields = this.dialect.hydratableFields(entity);
259
+ for (const dto of dtos) {
260
+ this.hydrateFields(meta, fields, dto);
261
+ }
262
+ }
263
+ /**
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.
268
+ */
269
+ hydrateFields(meta, fields, dto) {
282
270
  const row = dto;
283
- for (const [key, kind] of this.dialect.hydratableFields(entity)) {
271
+ for (const [key, kind] of fields) {
284
272
  const value = row[key];
285
273
  if (value != null) {
286
274
  row[key] = decodeColumn(value, kind);
287
275
  }
288
276
  }
289
- // Allocated only where the walk can continue: an entity declaring no relation cannot lead back
290
- // to a node already decoded, and the loop below is a no-op for it anyway.
291
- if (hasKeys(meta.relations)) {
292
- visited ??= new WeakSet();
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
+ }
293
283
  }
294
- visited?.add(dto);
295
284
  // The value is read before the relation's target is resolved: a query that populated nothing
296
285
  // still walks every relation the entity declares, and `rel.entity()` is a call per row per
297
286
  // relation that only the populated ones need.
298
287
  for (const key in meta.relations) {
299
288
  const value = row[key];
300
- if (!value || typeof value !== 'object')
289
+ if (!value)
301
290
  continue;
302
291
  const rel = meta.relations[key];
303
292
  if (!rel)
304
293
  continue;
305
294
  const relEntity = rel.entity();
306
- if (Array.isArray(value)) {
307
- for (const it of value) {
308
- this.hydrateFields(relEntity, it, visited);
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);
309
308
  }
310
- continue;
311
309
  }
312
- this.hydrateFields(relEntity, value, visited);
313
310
  }
314
- return dto;
315
311
  }
316
312
  /**
317
313
  * Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
@@ -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':
@@ -492,7 +492,5 @@ export function canonicalToColumnType(type) {
492
492
  return 'halfvec';
493
493
  case 'sparsevec':
494
494
  return 'sparsevec';
495
- default:
496
- return 'varchar';
497
495
  }
498
496
  }
@@ -38,11 +38,9 @@ export function findCycles(nodes, dependenciesOf) {
38
38
  const visited = new Set();
39
39
  const onPath = new Set();
40
40
  const visit = (node, path) => {
41
+ // A node on the walk was handed down in `path` too, so the cycle closes where it first appears.
41
42
  if (onPath.has(node)) {
42
- const start = path.indexOf(node);
43
- if (start !== -1) {
44
- cycles.push(path.slice(start));
45
- }
43
+ cycles.push(path.slice(path.indexOf(node)));
46
44
  return;
47
45
  }
48
46
  if (visited.has(node)) {
@@ -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
- if ((source.unique ?? false) !== (target.unique ?? false)) {
68
- differences.push(`unique: ${target.unique ?? false} → ${source.unique ?? false}`);
67
+ if (source.unique !== target.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;