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
@@ -1,7 +1,7 @@
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 { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, getRelationRequestSummary, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { AbstractQuerier } from './abstractQuerier.js';
6
6
  import { enrichError } from './queryError.js';
7
7
  /**
@@ -223,8 +223,11 @@ export class AbstractSqlQuerier extends AbstractQuerier {
223
223
  return this.all(ctx.sql, ctx.values);
224
224
  }
225
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);
226
+ const founds = unflatObjects(rows);
227
+ this.hydrateAll(entity, founds);
228
+ if (q.$populate) {
229
+ await this.fillToManyRelations(entity, founds, q.$populate);
230
+ }
228
231
  return founds;
229
232
  }
230
233
  async *internalFindManyStream(entity, q, opts) {
@@ -245,11 +248,14 @@ export class AbstractSqlQuerier extends AbstractQuerier {
245
248
  // context holds was normalized as it was bound.
246
249
  const ctx = this.dialect.createContext();
247
250
  this.dialect.find(ctx, entity, q, opts);
251
+ const fields = this.dialect.hydratableFields(entity);
248
252
  let attrsPaths;
249
253
  try {
250
254
  for await (const row of this.internalStream(ctx.sql, ctx.values)) {
251
255
  attrsPaths ??= obtainAttrsPaths(row);
252
- yield this.hydrateFields(entity, unflatObject(row, attrsPaths));
256
+ const found = unflatObject(row, attrsPaths);
257
+ this.hydrateFields(meta, fields, found);
258
+ yield found;
253
259
  }
254
260
  }
255
261
  catch (err) {
@@ -265,33 +271,35 @@ export class AbstractSqlQuerier extends AbstractQuerier {
265
271
  yield* await this.internalAll(query, values);
266
272
  }
267
273
  /**
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.
274
+ * Turn what a driver returned back into the types the entity declares, for every row and everything
275
+ * populated under them. Which columns, and as what, is `hydratableFields`, resolved once for all the
276
+ * rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
277
+ * only sparse on Postgres.
278
+ */
279
+ hydrateAll(entity, dtos, visited) {
280
+ const meta = getMeta(entity);
281
+ const fields = this.dialect.hydratableFields(entity);
282
+ for (const dto of dtos) {
283
+ this.hydrateFields(meta, fields, dto, visited);
284
+ }
285
+ }
286
+ /**
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.
276
290
  */
277
- hydrateFields(entity, dto, visited) {
291
+ hydrateFields(meta, fields, dto, visited) {
278
292
  if (!dto || typeof dto !== 'object' || visited?.has(dto)) {
279
- return dto;
293
+ return;
280
294
  }
281
- const meta = getMeta(entity);
295
+ visited?.add(dto);
282
296
  const row = dto;
283
- for (const [key, kind] of this.dialect.hydratableFields(entity)) {
297
+ for (const [key, kind] of fields) {
284
298
  const value = row[key];
285
299
  if (value != null) {
286
300
  row[key] = decodeColumn(value, kind);
287
301
  }
288
302
  }
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();
293
- }
294
- visited?.add(dto);
295
303
  // The value is read before the relation's target is resolved: a query that populated nothing
296
304
  // still walks every relation the entity declares, and `rel.entity()` is a call per row per
297
305
  // relation that only the populated ones need.
@@ -302,16 +310,14 @@ export class AbstractSqlQuerier extends AbstractQuerier {
302
310
  const rel = meta.relations[key];
303
311
  if (!rel)
304
312
  continue;
313
+ visited ??= new WeakSet([dto]);
305
314
  const relEntity = rel.entity();
306
315
  if (Array.isArray(value)) {
307
- for (const it of value) {
308
- this.hydrateFields(relEntity, it, visited);
309
- }
316
+ this.hydrateAll(relEntity, value, visited);
310
317
  continue;
311
318
  }
312
- this.hydrateFields(relEntity, value, visited);
319
+ this.hydrateFields(getMeta(relEntity), this.dialect.hydratableFields(relEntity), value, visited);
313
320
  }
314
- return dto;
315
321
  }
316
322
  /**
317
323
  * Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
@@ -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)) {
@@ -64,8 +64,8 @@ export function describeIndexDifferences(source, target, facets) {
64
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
71
  differences.push(`type: ${target.type ?? 'btree'} → ${source.type ?? 'btree'}`);
@@ -10,6 +10,7 @@ import { ddlText } from '../util/ddlExpression.util.js';
10
10
  import { isInlinedExpression } from '../util/field.util.js';
11
11
  import { isSoleIdField } from '../util/field.util.js';
12
12
  import { isAutoIncrement } from '../util/field.util.js';
13
+ import { definedEntries } from '../util/object.util.js';
13
14
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
14
15
  import { fieldOptionsToCanonical } from './canonicalType.js';
15
16
  import { createTableNode, SchemaAST } from './schemaAST.js';
@@ -79,11 +80,7 @@ function addTableFromEntity(ctx, meta) {
79
80
  const { columns, primaryKey } = table;
80
81
  table.checks?.push(...(meta.checks ?? []));
81
82
  // Add columns from fields
82
- const fields = meta.fields;
83
- for (const key of Object.keys(fields)) {
84
- const field = fields[key];
85
- if (!field)
86
- continue;
83
+ for (const [key, field] of definedEntries(meta.fields)) {
87
84
  // An inlined expression has no column; a stored one is a column like any other.
88
85
  if (isInlinedExpression(field))
89
86
  continue;
@@ -126,11 +123,7 @@ function addRelationshipsFromEntity(ctx, meta) {
126
123
  const table = tableOf(ctx, meta);
127
124
  if (!table)
128
125
  return;
129
- const relations = meta.relations;
130
- for (const key of Object.keys(relations)) {
131
- const relation = relations[key];
132
- if (!relation)
133
- continue;
126
+ for (const [key, relation] of definedEntries(meta.relations)) {
134
127
  const relatedMeta = getMeta(relation.entity());
135
128
  const relatedTable = tableOf(ctx, relatedMeta);
136
129
  if (!relatedTable)
@@ -10,7 +10,7 @@
10
10
  import { type IndexFacet } from './indexDifferences.js';
11
11
  import type { SchemaAST } from './schemaAST.js';
12
12
  import type { CanonicalType } from './types.js';
13
- import type { RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
13
+ import type { ColumnDiff, ForeignKeyAction, IndexDiff, RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
14
14
  /**
15
15
  * Options for schema diffing.
16
16
  */
@@ -59,7 +59,10 @@ export declare function diffSchemas(source: SchemaAST, target: SchemaAST, option
59
59
  * against the one the database reported, then projects the result into a `SchemaDiff`. One
60
60
  * comparison serves both, so drift and migrations can no longer disagree about what has changed.
61
61
  */
62
- export declare function diffTable(source: TableNode, target: TableNode, options?: DiffOptions): TableDiff | undefined;
62
+ export declare function diffTable(source: TableNode, target: TableNode, options?: DiffOptions): (TableDiff & {
63
+ readonly columnDiffs: ColumnDiff[];
64
+ readonly indexDiffs: IndexDiff[];
65
+ }) | undefined;
63
66
  /**
64
67
  * Compare two lists of relationships.
65
68
  *
@@ -70,3 +73,8 @@ export declare function diffTable(source: TableNode, target: TableNode, options?
70
73
  * pairing on names would report every hand-named one as a drop and an add.
71
74
  */
72
75
  export declare function diffRelationshipNodes(source: readonly RelationshipNode[], target: readonly RelationshipNode[], opts?: DiffOptions): RelationshipDiff[];
76
+ /** A relationship's `ON DELETE` and `ON UPDATE`, an unstated one read as the action the database applies. */
77
+ export declare function referentialActions(rel: RelationshipNode): {
78
+ readonly onDelete: ForeignKeyAction;
79
+ readonly onUpdate: ForeignKeyAction;
80
+ };
@@ -61,8 +61,8 @@ export function diffSchemas(source, target, options = {}) {
61
61
  const tablesToAlter = matched
62
62
  .map(([sourceTable, targetTable]) => diffTable(sourceTable, targetTable, opts))
63
63
  .filter((tableDiff) => tableDiff !== undefined);
64
- const columnDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.columnDiffs ?? []);
65
- const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs ?? []);
64
+ const columnDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.columnDiffs);
65
+ const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs);
66
66
  const primaryKeyDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.primaryKeyDiff ?? []);
67
67
  // Relationships span tables, so they are compared over the whole schema rather than per table.
68
68
  const relationshipDiffs = opts.compareRelationships
@@ -265,16 +265,17 @@ export function diffRelationshipNodes(source, target, opts = {}) {
265
265
  .filter((diff) => diff !== undefined),
266
266
  ];
267
267
  }
268
- /**
269
- * Compare two relationships.
270
- */
268
+ /** A relationship's `ON DELETE` and `ON UPDATE`, an unstated one read as the action the database applies. */
269
+ export function referentialActions(rel) {
270
+ return {
271
+ onDelete: rel.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION,
272
+ onUpdate: rel.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION,
273
+ };
274
+ }
275
+ /** Two relationships over the same columns differ only in what they do when the row they point at goes. */
271
276
  function diffRelationship(source, target) {
272
- // Compare on delete/update actions (normalizing defaults)
273
- const sDelete = source.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION;
274
- const tDelete = target.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION;
275
- const sUpdate = source.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION;
276
- const tUpdate = target.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION;
277
- if (sDelete !== tDelete || sUpdate !== tUpdate) {
277
+ const [expected, actual] = [referentialActions(source), referentialActions(target)];
278
+ if (expected.onDelete !== actual.onDelete || expected.onUpdate !== actual.onUpdate) {
278
279
  return { ...relationEnds(source), type: 'alter', expected: source, actual: target };
279
280
  }
280
281
  return undefined;
@@ -342,7 +342,7 @@ export type DriftSeverity = 'critical' | 'warning' | 'info';
342
342
  /**
343
343
  * Type of schema drift.
344
344
  */
345
- export type DriftType = 'missing_table' | 'unexpected_table' | 'missing_column' | 'unexpected_column' | 'type_mismatch' | 'constraint_mismatch' | 'missing_index' | 'unexpected_index' | 'index_mismatch' | 'missing_relationship' | 'unexpected_relationship';
345
+ export type DriftType = 'missing_table' | 'unexpected_table' | 'missing_column' | 'unexpected_column' | 'type_mismatch' | 'constraint_mismatch' | 'missing_index' | 'unexpected_index' | 'index_mismatch' | 'missing_relationship' | 'unexpected_relationship' | 'relationship_mismatch';
346
346
  /**
347
347
  * A single schema drift issue.
348
348
  */
@@ -1,6 +1,7 @@
1
1
  import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
2
2
  import { JSON_ELEM_ALIAS_PREFIX, JSON_PULL_ALIAS } from '../dialect/aliases.js';
3
3
  import { jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget } from '../dialect/jsonSql.js';
4
+ import { textSearchFields } from '../util/dialect.util.js';
4
5
  export class SqliteDialect extends AbstractSqlDialect {
5
6
  /** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
6
7
  featureDefaults = {
@@ -86,7 +87,7 @@ export class SqliteDialect extends AbstractSqlDialect {
86
87
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
87
88
  */
88
89
  appendTextSearch(ctx, entity, meta, search) {
89
- const columns = search.$fields.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
90
+ const columns = textSearchFields(meta, search).map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
90
91
  ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
91
92
  ctx.addValue(search.$value);
92
93
  }
@@ -1,4 +1,4 @@
1
- import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
1
+ import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
2
2
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
3
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
4
4
  /** Appends `record`'s not-yet-`seen` insertable keys (real, caller-written, defined value) to `keys`. */
@@ -171,3 +171,9 @@ export declare function assertNonNegativeInteger(value: number, clause: string):
171
171
  export declare function throwUnknownAggregateColumn(key: string, clause: string): never;
172
172
  /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
173
173
  export declare function assertAggregateColumns(clauseMap: object, emitted: ReadonlySet<string>, clause: string): void;
174
+ /**
175
+ * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
176
+ * the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
177
+ * where neither says, rather than guessed: every engine answers a guess with an error of its own.
178
+ */
179
+ export declare function textSearchFields<E>(meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): readonly string[];
@@ -424,3 +424,22 @@ export function assertAggregateColumns(clauseMap, emitted, clause) {
424
424
  }
425
425
  }
426
426
  }
427
+ /**
428
+ * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
429
+ * the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
430
+ * where neither says, rather than guessed: every engine answers a guess with an error of its own.
431
+ */
432
+ export function textSearchFields(meta, search) {
433
+ if (search.$fields?.length) {
434
+ return search.$fields;
435
+ }
436
+ const fulltext = (meta.indexes ?? []).filter((index) => index.type === 'fulltext');
437
+ if (fulltext.length === 1) {
438
+ return fulltext[0].columns.map((entry) => entry.column);
439
+ }
440
+ const name = entityName(meta);
441
+ const declared = fulltext.length
442
+ ? `${fulltext.length} fulltext indexes to choose from`
443
+ : 'no fulltext index to search';
444
+ throw new TypeError(`$text on '${name}' names no $fields, and '${name}' declares ${declared}. Name them with $fields.`);
445
+ }
@@ -34,7 +34,7 @@ export declare class LoggerWrapper implements Logger {
34
34
  private readonly loggerFunction?;
35
35
  private readonly logValues;
36
36
  private readonly slowQuery?;
37
- constructor(options: LoggingOptions, config?: LoggerWrapperConfig);
37
+ constructor(options?: LoggingOptions, config?: LoggerWrapperConfig);
38
38
  /** Whether `logQuery` would ever actually surface bound values, given the configured levels/slowQuery/logValues. */
39
39
  willLogValues(): boolean;
40
40
  logQuery(query: string, values?: unknown[], duration?: number): void;
@@ -46,6 +46,15 @@ export declare class LoggerWrapper implements Logger {
46
46
  logSkippedMigration(message: string): void;
47
47
  private log;
48
48
  }
49
+ /**
50
+ * The wrapper a querier logs through, one per options object and read once: a pool builds a querier
51
+ * for every statement, and all of them log by the same options.
52
+ */
53
+ export declare function queryLoggerFor(extra?: {
54
+ readonly logger?: LoggingOptions;
55
+ readonly logValues?: boolean;
56
+ readonly slowQuery?: number;
57
+ }): LoggerWrapper;
49
58
  /**
50
59
  * Structural type for any EventEmitter-like connection pool that emits an
51
60
  * `'error'` event on a dropped connection (node-postgres, `mariadb`, etc.).
@@ -136,6 +136,24 @@ export class LoggerWrapper {
136
136
  }
137
137
  }
138
138
  }
139
+ const wrappersByOptions = new WeakMap();
140
+ let unconfigured;
141
+ /**
142
+ * The wrapper a querier logs through, one per options object and read once: a pool builds a querier
143
+ * for every statement, and all of them log by the same options.
144
+ */
145
+ export function queryLoggerFor(extra) {
146
+ if (!extra) {
147
+ unconfigured ??= new LoggerWrapper();
148
+ return unconfigured;
149
+ }
150
+ let wrapper = wrappersByOptions.get(extra);
151
+ if (!wrapper) {
152
+ wrapper = new LoggerWrapper(extra.logger, { logValues: extra.logValues, slowQuery: extra.slowQuery });
153
+ wrappersByOptions.set(extra, wrapper);
154
+ }
155
+ return wrapper;
156
+ }
139
157
  /**
140
158
  * Attaches an error listener to a connection pool so a dropped connection is logged instead of left
141
159
  * unhandled - which crashes the process for drivers that don't guard against it themselves
@@ -20,6 +20,8 @@ export declare function isOperatorObject(value: unknown): value is Record<string
20
20
  /** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */
21
21
  export declare function isOperatorOnlyObject(value: unknown): value is Record<string, unknown>;
22
22
  export declare function getKeys<T extends object>(obj: T): (keyof T & string)[];
23
+ /** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */
24
+ export declare function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][];
23
25
  /**
24
26
  * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so
25
27
  * the fallback is what an entity that named no table is called - which is why the sites spelling this
@@ -52,6 +52,10 @@ export function isOperatorOnlyObject(value) {
52
52
  export function getKeys(obj) {
53
53
  return obj ? Object.keys(obj) : [];
54
54
  }
55
+ /** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */
56
+ export function definedEntries(record) {
57
+ return Object.entries(record).filter((entry) => entry[1] !== undefined);
58
+ }
55
59
  /**
56
60
  * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so
57
61
  * the fallback is what an entity that named no table is called - which is why the sites spelling this
@@ -1,8 +1,8 @@
1
1
  import type { EntityMeta, FieldMeta, Except, Query, QueryPopulate, RelationKey, RelationMeta } from '../type/index.js';
2
2
  export type RelationRequestSummary<E> = {
3
- readonly requestedKeys: RelationKey<E>[];
4
- readonly joinableKeys: RelationKey<E>[];
5
- readonly toManyKeys: RelationKey<E>[];
3
+ readonly requestedKeys: readonly RelationKey<E>[];
4
+ readonly joinableKeys: readonly RelationKey<E>[];
5
+ readonly toManyKeys: readonly RelationKey<E>[];
6
6
  };
7
7
  /**
8
8
  * Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
@@ -1,5 +1,11 @@
1
1
  import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, } from '../type/query.js';
2
2
  import { getKeys, someKey } from './object.util.js';
3
+ /** What a query populating nothing requests, shared: most reads populate nothing, and ask on every one. */
4
+ const NOTHING_REQUESTED = Object.freeze({
5
+ requestedKeys: Object.freeze([]),
6
+ joinableKeys: Object.freeze([]),
7
+ toManyKeys: Object.freeze([]),
8
+ });
3
9
  /**
4
10
  * Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
5
11
  * the one field it reads, so it answers for a relation being declared as well as for a resolved one.
@@ -142,11 +148,11 @@ function assertJoinableRelationQuery(relKey, value) {
142
148
  }
143
149
  }
144
150
  export function getRelationRequestSummary(meta, populate) {
151
+ if (!populate)
152
+ return NOTHING_REQUESTED;
145
153
  const requestedKeys = [];
146
154
  const joinableKeys = [];
147
155
  const toManyKeys = [];
148
- if (!populate)
149
- return { requestedKeys, joinableKeys, toManyKeys };
150
156
  for (const key of getKeys(populate)) {
151
157
  if (!populate[key])
152
158
  continue;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.55.0",
6
+ "version": "0.56.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"