uql-orm 0.55.0 → 0.56.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 (48) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +3 -3
  3. package/dist/bunSql/bunSql.util.js +1 -1
  4. package/dist/dialect/mysqlLikeSqlDialect.js +2 -3
  5. package/dist/dialect/pgLikeSqlDialect.js +2 -2
  6. package/dist/dialect/queryJoins.js +2 -4
  7. package/dist/entity/index.d.ts +1 -1
  8. package/dist/entity/index.js +1 -1
  9. package/dist/entity/metadata/definition.d.ts +4 -2
  10. package/dist/entity/metadata/definition.js +24 -26
  11. package/dist/migrate/builder/splitSqlStatements.js +2 -2
  12. package/dist/migrate/cli.js +2 -9
  13. package/dist/migrate/codegen/fieldOptionsSource.js +1 -1
  14. package/dist/migrate/ddl/pgIndexDdl.d.ts +5 -0
  15. package/dist/migrate/ddl/pgIndexDdl.js +9 -0
  16. package/dist/migrate/drift/driftDetector.js +21 -8
  17. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  18. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +1 -1
  19. package/dist/migrate/introspection/baseSqlIntrospector.js +65 -76
  20. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  21. package/dist/migrate/introspection/mysqlIntrospector.js +5 -8
  22. package/dist/migrate/introspection/sqliteIntrospector.js +2 -5
  23. package/dist/migrate/migrator.d.ts +5 -2
  24. package/dist/migrate/migrator.js +9 -14
  25. package/dist/migrate/schemaGenerator.js +2 -6
  26. package/dist/mongo/mongoDialect.d.ts +1 -1
  27. package/dist/mongo/mongoDialect.js +4 -7
  28. package/dist/mongo/mongodbQuerier.js +6 -6
  29. package/dist/querier/abstractQuerier.js +17 -22
  30. package/dist/querier/abstractSqlQuerier.d.ts +10 -8
  31. package/dist/querier/abstractSqlQuerier.js +33 -27
  32. package/dist/schema/canonicalType.js +0 -2
  33. package/dist/schema/dependencyGraph.js +2 -4
  34. package/dist/schema/indexDifferences.js +2 -2
  35. package/dist/schema/schemaASTBuilder.js +3 -10
  36. package/dist/schema/schemaASTDiffer.d.ts +10 -2
  37. package/dist/schema/schemaASTDiffer.js +12 -11
  38. package/dist/schema/types.d.ts +1 -1
  39. package/dist/sqlite/sqliteDialect.js +2 -1
  40. package/dist/util/dialect.util.d.ts +7 -1
  41. package/dist/util/dialect.util.js +19 -0
  42. package/dist/util/logger.d.ts +10 -1
  43. package/dist/util/logger.js +18 -0
  44. package/dist/util/object.util.d.ts +2 -0
  45. package/dist/util/object.util.js +4 -0
  46. package/dist/util/relationQuery.util.d.ts +3 -3
  47. package/dist/util/relationQuery.util.js +8 -2
  48. package/package.json +1 -1
@@ -43,90 +43,79 @@ export class BaseSqlIntrospector {
43
43
  */
44
44
  buildAST(tableSchemas) {
45
45
  const ast = new SchemaAST();
46
- const tableNodes = new Map();
47
- this.buildTables(ast, tableNodes, tableSchemas);
48
- this.buildRelationships(ast, tableNodes, tableSchemas);
49
- this.buildIndexes(ast, tableNodes, tableSchemas);
46
+ // Every table first, so a foreign key reaches a table introspected after the one it leaves from.
47
+ const tables = tableSchemas.map((schema) => ({ schema, table: this.buildTable(schema) }));
48
+ const tableNodes = new Map(tables.map(({ schema, table }) => [schema.name, table]));
49
+ for (const { table } of tables) {
50
+ ast.addTable(table);
51
+ }
52
+ for (const { schema, table } of tables) {
53
+ this.buildRelationships(ast, tableNodes, schema, table);
54
+ this.buildIndexes(ast, schema, table);
55
+ }
50
56
  return ast;
51
57
  }
52
- buildTables(ast, tableNodes, tableSchemas) {
53
- for (const schema of tableSchemas) {
54
- const table = createTableNode(schema.name, this.schema);
55
- const { columns } = table;
56
- for (const col of schema.columns) {
57
- // Spread, not field by field: a `ColumnSchema` is a `ColumnNode` minus the graph links, so
58
- // everything but the type crosses unchanged and a field either shape gains cannot be dropped
59
- // here. Listed by hand this had already lost `enum` and `generatedAs`.
60
- const { type, length: _length, precision: _precision, scale: _scale, ...rest } = col;
61
- const column = {
62
- ...rest,
63
- type: canonicalColumnType(type, col),
64
- table,
65
- referencedBy: [],
66
- };
67
- columns.set(col.name, column);
68
- }
69
- // From the ordered list the query returned, not from the per-column flags: `(a, b)` is a
70
- // different key from `(b, a)`, and a flag says only that a column is *in* the key. Falls back
71
- // to the flags for an introspector that reports no key of its own.
72
- const keyColumns = schema.primaryKey ?? schema.columns.filter((col) => col.isPrimaryKey).map((col) => col.name);
73
- table.primaryKey.push(...keyColumns.flatMap((name) => columns.get(name) ?? []));
74
- table.primaryKeyName = schema.primaryKeyName;
75
- tableNodes.set(schema.name, table);
76
- ast.addTable(table);
58
+ buildTable(schema) {
59
+ const table = createTableNode(schema.name, this.schema);
60
+ const { columns } = table;
61
+ for (const col of schema.columns) {
62
+ // Spread, not field by field: a `ColumnSchema` is a `ColumnNode` minus the graph links, so
63
+ // everything but the type crosses unchanged and a field either shape gains cannot be dropped
64
+ // here. Listed by hand this had already lost `enum` and `generatedAs`.
65
+ const { type, length: _length, precision: _precision, scale: _scale, ...rest } = col;
66
+ const column = {
67
+ ...rest,
68
+ type: canonicalColumnType(type, col),
69
+ table,
70
+ referencedBy: [],
71
+ };
72
+ columns.set(col.name, column);
77
73
  }
74
+ // From the ordered list the query returned, not from the per-column flags: `(a, b)` is a
75
+ // different key from `(b, a)`, and a flag says only that a column is *in* the key. Falls back
76
+ // to the flags for an introspector that reports no key of its own.
77
+ const keyColumns = schema.primaryKey ?? schema.columns.filter((col) => col.isPrimaryKey).map((col) => col.name);
78
+ table.primaryKey.push(...keyColumns.flatMap((name) => columns.get(name) ?? []));
79
+ table.primaryKeyName = schema.primaryKeyName;
80
+ return table;
78
81
  }
79
- buildRelationships(ast, tableNodes, tableSchemas) {
80
- for (const schema of tableSchemas) {
81
- if (!schema.foreignKeys)
82
+ buildRelationships(ast, tableNodes, schema, fromTable) {
83
+ for (const fk of schema.foreignKeys ?? []) {
84
+ const toTable = tableNodes.get(fk.references.table);
85
+ if (!toTable)
82
86
  continue;
83
- const fromTable = tableNodes.get(schema.name);
84
- if (!fromTable)
85
- continue;
86
- for (const fk of schema.foreignKeys) {
87
- const toTable = tableNodes.get(fk.references.table);
88
- if (!toTable)
89
- continue;
90
- const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
91
- const toColumns = fk.references.columns.flatMap((name) => toTable.columns.get(name) ?? []);
92
- if (fromColumns.length > 0 && toColumns.length > 0) {
93
- const rel = {
94
- name: fk.name ?? derivedForeignKeyName(schema.name, fk.columns),
95
- type: fromColumns[0].isUnique ? 'OneToOne' : 'ManyToOne',
96
- from: { table: fromTable, columns: fromColumns },
97
- to: { table: toTable, columns: toColumns },
98
- onDelete: fk.onDelete || 'NO ACTION',
99
- onUpdate: fk.onUpdate || 'NO ACTION',
100
- };
101
- ast.addRelationship(rel);
102
- }
87
+ const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
88
+ const toColumns = fk.references.columns.flatMap((name) => toTable.columns.get(name) ?? []);
89
+ if (fromColumns.length > 0 && toColumns.length > 0) {
90
+ const rel = {
91
+ name: fk.name ?? derivedForeignKeyName(schema.name, fk.columns),
92
+ type: fromColumns[0].isUnique ? 'OneToOne' : 'ManyToOne',
93
+ from: { table: fromTable, columns: fromColumns },
94
+ to: { table: toTable, columns: toColumns },
95
+ onDelete: fk.onDelete || 'NO ACTION',
96
+ onUpdate: fk.onUpdate || 'NO ACTION',
97
+ };
98
+ ast.addRelationship(rel);
103
99
  }
104
100
  }
105
101
  }
106
- buildIndexes(ast, tableNodes, tableSchemas) {
107
- for (const schema of tableSchemas) {
108
- if (!schema.indexes)
109
- continue;
110
- const table = tableNodes.get(schema.name);
111
- if (!table)
112
- continue;
113
- for (const idx of schema.indexes) {
114
- // An expression has no column to resolve. Dropping the entries that name a column this table
115
- // does not have, and the index if that leaves none, is what the entity side does too.
116
- const entries = idx.entries.filter((entry) => entry.expression || table.columns.has(entry.column));
117
- if (entries.length > 0) {
118
- const index = {
119
- name: idx.name,
120
- table,
121
- entries,
122
- unique: idx.unique,
123
- type: idx.type,
124
- where: idx.where,
125
- include: idx.include,
126
- source: 'database',
127
- };
128
- ast.addIndex(index);
129
- }
102
+ buildIndexes(ast, schema, table) {
103
+ for (const idx of schema.indexes ?? []) {
104
+ // An expression has no column to resolve. Dropping the entries that name a column this table
105
+ // does not have, and the index if that leaves none, is what the entity side does too.
106
+ const entries = idx.entries.filter((entry) => entry.expression || table.columns.has(entry.column));
107
+ if (entries.length > 0) {
108
+ const index = {
109
+ name: idx.name,
110
+ table,
111
+ entries,
112
+ unique: idx.unique,
113
+ type: idx.type,
114
+ where: idx.where,
115
+ include: idx.include,
116
+ source: 'database',
117
+ };
118
+ ast.addIndex(index);
130
119
  }
131
120
  }
132
121
  }
@@ -27,7 +27,8 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
27
27
  `;
28
28
  }
29
29
  parseTableExistsResult(results) {
30
- return (this.toNumber(results[0]?.count) ?? 0) > 0;
30
+ // No row, or no count in it, reads as `NaN`, which is not above zero.
31
+ return Number(results[0]?.count) > 0;
31
32
  }
32
33
  /**
33
34
  * From `sys` rather than `INFORMATION_SCHEMA`, which has no identity flag and no per-column view of
@@ -24,11 +24,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
24
24
  `;
25
25
  }
26
26
  parseTableExistsResult(results) {
27
- const row = results[0];
28
- if (row?.count !== undefined) {
29
- return (this.toNumber(row.count) ?? 0) > 0;
30
- }
31
- return false;
27
+ // No row, or no count in it, reads as `NaN`, which is not above zero.
28
+ return Number(results[0]?.count) > 0;
32
29
  }
33
30
  getColumnsQuery(_tableName) {
34
31
  return /*sql*/ `
@@ -97,7 +94,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
97
94
  async mapColumnsResult(_read, _tableName, results) {
98
95
  return results.map((row) => ({
99
96
  name: row.column_name,
100
- type: (row.column_type || '').toUpperCase(),
97
+ type: row.column_type.toUpperCase(),
101
98
  nullable: row.is_nullable === 'YES',
102
99
  defaultValue: this.parseDefaultValue(row.column_default),
103
100
  isPrimaryKey: row.column_key === 'PRI',
@@ -122,8 +119,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
122
119
  async mapForeignKeysResult(_read, _tableName, results) {
123
120
  return results.map((row) => ({
124
121
  name: row.constraint_name,
125
- columns: (row.columns || '').split(','),
126
- references: { table: row.referenced_table, columns: (row.referenced_columns || '').split(',') },
122
+ columns: row.columns.split(','),
123
+ references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
127
124
  onDelete: this.normalizeReferentialAction(row.delete_rule),
128
125
  onUpdate: this.normalizeReferentialAction(row.update_rule),
129
126
  }));
@@ -22,11 +22,8 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
22
22
  `;
23
23
  }
24
24
  parseTableExistsResult(results) {
25
- const row = results[0];
26
- if (row?.count !== undefined) {
27
- return (this.toNumber(row.count) ?? 0) > 0;
28
- }
29
- return false;
25
+ // No row, or no count in it, reads as `NaN`, which is not above zero.
26
+ return Number(results[0]?.count) > 0;
30
27
  }
31
28
  /**
32
29
  * `table_xinfo`, not `table_info`: the latter omits generated columns entirely, so a table carrying
@@ -19,8 +19,11 @@ export declare class Migrator {
19
19
  private readonly _defaultForeignKeyAction?;
20
20
  private _mongoSchemaLoadPromise?;
21
21
  constructor(pool: QuerierPool, options?: MigratorOptions);
22
- /** Loads MongoDB schema generator on first use; SQL generators are set in the constructor (or via {@link setSchemaGenerator}). */
23
- private ensureSchemaGenerator;
22
+ /**
23
+ * Loads MongoDB's schema generator on first use, so the optional `mongodb` peer loads only then. SQL
24
+ * generators are set in the constructor (or via {@link setSchemaGenerator}).
25
+ */
26
+ ensureSchemaGenerator(): Promise<void>;
24
27
  /**
25
28
  * Set the schema generator for DDL operations
26
29
  */
@@ -10,7 +10,6 @@ import { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPT
10
10
  import { runMongoCommand } from './generator/mongoCommand.js';
11
11
  import { introspectorFor } from './introspection/registry.js';
12
12
  import { createSchemaGenerator } from './schemaGenerator.js';
13
- import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
14
13
  import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
15
14
  /**
16
15
  * Main class for managing database migrations
@@ -37,7 +36,7 @@ export class Migrator {
37
36
  _mongoSchemaLoadPromise;
38
37
  constructor(pool, options = {}) {
39
38
  this.pool = pool;
40
- this.dialectName = pool.dialect.dialectName ?? 'postgres';
39
+ this.dialectName = pool.dialect.dialectName;
41
40
  this._defaultForeignKeyAction = options.defaultForeignKeyAction;
42
41
  this.storage =
43
42
  options.storage ??
@@ -51,18 +50,17 @@ export class Migrator {
51
50
  this.schemaGenerator =
52
51
  options.schemaGenerator ?? (this.dialectName === 'mongodb' ? undefined : this.createGenerator());
53
52
  }
54
- /** Loads MongoDB schema generator on first use; SQL generators are set in the constructor (or via {@link setSchemaGenerator}). */
53
+ /**
54
+ * Loads MongoDB's schema generator on first use, so the optional `mongodb` peer loads only then. SQL
55
+ * generators are set in the constructor (or via {@link setSchemaGenerator}).
56
+ */
55
57
  async ensureSchemaGenerator() {
56
58
  if (this.schemaGenerator || this.dialectName !== 'mongodb') {
57
59
  return;
58
60
  }
59
- if (!this._mongoSchemaLoadPromise) {
60
- this._mongoSchemaLoadPromise = createSchemaGeneratorAsync(this.pool.dialect, this._defaultForeignKeyAction).then((gen) => {
61
- if (gen) {
62
- this.schemaGenerator = gen;
63
- }
64
- });
65
- }
61
+ this._mongoSchemaLoadPromise ??= import('./generator/mongoSchemaGenerator.js').then(({ MongoSchemaGenerator }) => {
62
+ this.schemaGenerator = new MongoSchemaGenerator(this.pool.dialect.namingStrategy, this._defaultForeignKeyAction);
63
+ });
66
64
  await this._mongoSchemaLoadPromise;
67
65
  }
68
66
  /**
@@ -302,12 +300,9 @@ export class Migrator {
302
300
  }
303
301
  async findEntityForTable(tableName) {
304
302
  await this.ensureSchemaGenerator();
305
- if (!this.schemaGenerator) {
306
- return undefined;
307
- }
308
303
  for (const entity of this.entities) {
309
304
  const meta = getMeta(entity);
310
- const name = this.schemaGenerator.resolveTableName(meta);
305
+ const name = this.generator.resolveTableName(meta);
311
306
  if (name === tableName) {
312
307
  return entity;
313
308
  }
@@ -474,8 +474,6 @@ export class SqlSchemaGenerator {
474
474
  if (current == null || desired == null)
475
475
  return current == null && desired == null;
476
476
  const normalize = (value) => {
477
- if (value === null)
478
- return 'null';
479
477
  // Render first: the desired side may be a symbolic expression, the current side is always the
480
478
  // engine's own text, and `{"kind":"now"}` matches no spelling of `CURRENT_TIMESTAMP`.
481
479
  const val = SqlExpression.isExpression(value) ? formatDefaultValue(value, this.dialect) : value;
@@ -518,10 +516,8 @@ export class SqlSchemaGenerator {
518
516
  constraints.push(`CONSTRAINT ${this.escapeId(name)} CHECK (${check.expression})`);
519
517
  });
520
518
  for (const rel of table.outgoingRelations) {
521
- if (rel.from.columns.length > 0) {
522
- const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
523
- constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
524
- }
519
+ const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
520
+ constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
525
521
  }
526
522
  const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
527
523
  let createSql = `CREATE TABLE ${ifNotExists}${this.dialect.escapeQualifiedId(table.name, table.schema)} (\n`;
@@ -187,7 +187,7 @@ export declare class MongoDialect extends AbstractDialect {
187
187
  /** `[column, key]` for the fields whose stored name differs from their property name, memoized per entity. */
188
188
  private renamedColumns;
189
189
  private referenceKeys;
190
- normalizeIds<E extends Document>(meta: EntityMeta<E>, docs: Document[] | undefined): E[] | undefined;
190
+ normalizeIds<E extends Document>(meta: EntityMeta<E>, docs: Document[]): E[];
191
191
  /** `doc` is the wire shape - `_id`, stored names, `ObjectId`s - and what comes back is the code's. */
192
192
  normalizeId<E extends Document>(meta: EntityMeta<E>, doc: Document | undefined): E | undefined;
193
193
  /**
@@ -2,7 +2,7 @@ import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
3
  import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
4
4
  import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
5
- import { assertSoleId, getMeta, soleIdOf } from '../entity/index.js';
5
+ import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
6
6
  import { QueryRaw } from '../type/queryRaw.js';
7
7
  import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
8
8
  /** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
@@ -722,7 +722,7 @@ export class MongoDialect extends AbstractDialect {
722
722
  #renamedColumns = new WeakMap();
723
723
  #referenceKeys = new WeakMap();
724
724
  normalizeIds(meta, docs) {
725
- return docs?.map((doc) => this.normalizeId(meta, doc));
725
+ return docs.map((doc) => this.normalizeId(meta, doc));
726
726
  }
727
727
  /** `doc` is the wire shape - `_id`, stored names, `ObjectId`s - and what comes back is the code's. */
728
728
  normalizeId(meta, doc) {
@@ -755,10 +755,7 @@ export class MongoDialect extends AbstractDialect {
755
755
  }
756
756
  const relKeys = getKeys(meta.relations).filter((key) => res[key]);
757
757
  for (const relKey of relKeys) {
758
- const relOpts = meta.relations[relKey];
759
- if (!relOpts)
760
- continue;
761
- const relMeta = getMeta(relOpts.entity());
758
+ const relMeta = getMeta(relationOf(meta, relKey).entity());
762
759
  res[relKey] = Array.isArray(res[relKey])
763
760
  ? this.normalizeIds(relMeta, res[relKey])
764
761
  : this.normalizeId(relMeta, res[relKey]);
@@ -894,7 +891,7 @@ export class MongoDialect extends AbstractDialect {
894
891
  const named = inserting && it[idKey] != null;
895
892
  if (inserting && !named) {
896
893
  // Nothing named the key, so the database is being asked to mint one.
897
- this.assertMintableKey(meta, meta.fields[idKey]);
894
+ this.assertMintableKey(meta, fieldOf(meta, idKey));
898
895
  }
899
896
  const doc = named ? { [MongoDialect.ID_KEY]: this.toWireId(it[idKey]) } : {};
900
897
  for (const key of filterFieldKeys(meta, it, callbackKey)) {
@@ -63,7 +63,7 @@ export class MongodbQuerier extends AbstractQuerier {
63
63
  else {
64
64
  const cursor = this.buildFindCursor(entity, q, opts);
65
65
  documents = await this.execute(() => cursor.toArray());
66
- documents = this.dialect.normalizeIds(meta, documents) || [];
66
+ documents = this.dialect.normalizeIds(meta, documents);
67
67
  }
68
68
  }
69
69
  return documents;
@@ -92,7 +92,7 @@ export class MongodbQuerier extends AbstractQuerier {
92
92
  const pipelines = queries.map((it) => this.dialect.aggregationPipeline(entity, it));
93
93
  // Counted, not estimated: the leading branch's own length grows with every `$lookup` a populate
94
94
  // adds, so a fixed parent budget would let a richer query overflow at the server instead.
95
- const stages = (pipelines[0]?.length ?? 0) + pipelines.length - 1;
95
+ const stages = pipelines[0].length + pipelines.length - 1;
96
96
  return stages > MAX_PIPELINE_STAGES
97
97
  ? this.readEachInTurn(entity, queries)
98
98
  : this.readInOnePipeline(entity, q, pipelines);
@@ -139,7 +139,7 @@ export class MongodbQuerier extends AbstractQuerier {
139
139
  const cursor = this.buildFindCursor(entity, q, opts);
140
140
  try {
141
141
  for await (const doc of cursor) {
142
- const [normalized] = this.dialect.normalizeIds(meta, [doc]) || [doc];
142
+ const [normalized] = this.dialect.normalizeIds(meta, [doc]);
143
143
  yield normalized;
144
144
  }
145
145
  }
@@ -177,7 +177,7 @@ export class MongodbQuerier extends AbstractQuerier {
177
177
  /** Execute an aggregation pipeline and normalize `_id` → `id`. */
178
178
  async runPipeline(entity, meta, pipeline) {
179
179
  const documents = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
180
- return this.dialect.normalizeIds(meta, documents) || [];
180
+ return this.dialect.normalizeIds(meta, documents);
181
181
  }
182
182
  /**
183
183
  * Build an aggregation pipeline for vector similarity search.
@@ -257,7 +257,7 @@ export class MongodbQuerier extends AbstractQuerier {
257
257
  const founds = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
258
258
  // `normalizeIds` has already spread a compound `_id` back into its columns, so the settled rows
259
259
  // are named the same way every other driver names them.
260
- return (this.dialect.normalizeIds(meta, founds) || []).map((found) => idOf(meta, found));
260
+ return this.dialect.normalizeIds(meta, founds).map((found) => idOf(meta, found));
261
261
  }
262
262
  async internalInsertMany(entity, rows) {
263
263
  return this.timed('internalInsertMany', undefined, async () => {
@@ -356,7 +356,7 @@ export class MongodbQuerier extends AbstractQuerier {
356
356
  };
357
357
  });
358
358
  const res = await this.execute((session) => this.collection(entity).bulkWrite(operations, { session }));
359
- const changes = (res.upsertedCount ?? 0) + (res.modifiedCount ?? 0);
359
+ const changes = res.upsertedCount + res.modifiedCount;
360
360
  // `upsertedIds` names only the documents inserted, keyed by operation index, so each lands on
361
361
  // its own row; an updated document's `_id` is read back by the conflict fields instead.
362
362
  const reported = payload.map((_, index) => {
@@ -1,5 +1,5 @@
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 { asSelectMap, childrenOf, clone, dataKeyed, entityName, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
4
  import { fillRelationCounts, withIdForCounts } from './relationCount.js';
5
5
  /**
@@ -78,13 +78,10 @@ export class AbstractQuerier {
78
78
  logger;
79
79
  constructor(extra) {
80
80
  this.extra = extra;
81
- this.logger = new LoggerWrapper(extra?.logger, {
82
- logValues: extra?.logValues,
83
- slowQuery: extra?.slowQuery,
84
- });
81
+ this.logger = queryLoggerFor(extra);
85
82
  }
86
83
  validateProjectionQuery(entity, q) {
87
- this.validateProjectionQueryRecursive(entity, q, getMeta(entity).name ?? entity.name);
84
+ this.validateProjectionQueryRecursive(entity, q, entityName(getMeta(entity)));
88
85
  }
89
86
  validateProjectionQueryRecursive(entity, q, path) {
90
87
  const meta = getMeta(entity);
@@ -96,9 +93,7 @@ export class AbstractQuerier {
96
93
  }
97
94
  }
98
95
  forEachRequestedRelation(meta, q.$populate, (relKey, relValue) => {
99
- const relOpts = meta.relations[relKey];
100
- if (!relOpts)
101
- return;
96
+ const relOpts = relationOf(meta, relKey);
102
97
  const relEntity = relOpts.entity();
103
98
  const parsed = parseRelationQueryValue(relValue);
104
99
  if (parsed.nested) {
@@ -136,8 +131,14 @@ export class AbstractQuerier {
136
131
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
137
132
  this.validateProjectionQuery(entity, q);
138
133
  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);
134
+ // Guarded here rather than only inside: awaiting a call that returns at once still costs every read
135
+ // a promise and a turn of the microtask queue, and most reads count nothing and hook nothing.
136
+ if (q.$count) {
137
+ await fillRelationCounts(this, entity, founds, q.$count);
138
+ }
139
+ if (this.hasHook(entity, 'afterLoad')) {
140
+ await this.emitHook(entity, 'afterLoad', founds);
141
+ }
141
142
  return founds;
142
143
  }
143
144
  findManyStream(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
@@ -348,9 +349,7 @@ export class AbstractQuerier {
348
349
  const meta = getMeta(entity);
349
350
  const relKeys = getRelationRequestSummary(meta, populate).toManyKeys;
350
351
  for (const relKey of relKeys) {
351
- const relOpts = meta.relations[relKey];
352
- if (!relOpts)
353
- continue;
352
+ const relOpts = relationOf(meta, relKey);
354
353
  const relEntity = relOpts.entity();
355
354
  const relationQuery = clone(parseRelationAtKey(relKey, populate).query);
356
355
  if (relOpts.through) {
@@ -498,9 +497,7 @@ export class AbstractQuerier {
498
497
  const relKeys = filterPersistableRelationKeys(meta, meta.relations, 'delete');
499
498
  // Cascade forwards `opts` (including `hardDelete`); each child soft-deletes only if it can.
500
499
  for (const relKey of relKeys) {
501
- const relOpts = meta.relations[relKey];
502
- if (!relOpts)
503
- continue;
500
+ const relOpts = relationOf(meta, relKey);
504
501
  const relEntity = relOpts.entity();
505
502
  const target = relOpts.through ? relOpts.through() : relEntity;
506
503
  const where = childrenOf(parentJoins(relOpts, meta.ids.length), ids);
@@ -514,9 +511,7 @@ export class AbstractQuerier {
514
511
  */
515
512
  async saveRelation(entity, ids, relValue, relKey, isUpdate) {
516
513
  const meta = getMeta(entity);
517
- const relOpts = meta.relations[relKey];
518
- if (!relOpts)
519
- return;
514
+ const relOpts = relationOf(meta, relKey);
520
515
  // Here rather than only in the callers below: writing the parent's key into a child is one column
521
516
  // per key, so a composite takes a statement per parent. `soleParentColumn` and the sole
522
517
  // `targetKeyColumns` under it read the *first* pair, which is a real column of a wrong pairing
@@ -684,7 +679,7 @@ export class AbstractQuerier {
684
679
  * another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
685
680
  * sequential rather than nested.
686
681
  */
687
- async serialize(task) {
682
+ serialize(task) {
688
683
  const res = this.taskQueue.then(task);
689
684
  this.taskQueue = res.catch(() => { });
690
685
  return res;
@@ -100,14 +100,16 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
100
100
  */
101
101
  protected internalStream<T>(query: string, values?: unknown[]): AsyncIterable<T>;
102
102
  /**
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.
103
+ * Turn what a driver returned back into the types the entity declares, for every row and everything
104
+ * populated under them. Which columns, and as what, is `hydratableFields`, resolved once for all the
105
+ * rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
106
+ * only sparse on Postgres.
107
+ */
108
+ private hydrateAll;
109
+ /**
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.
111
113
  */
112
114
  private hydrateFields;
113
115
  /**