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,15 +1,32 @@
1
- import { fieldOf, getMeta, soleIdOf } from '../entity/index.js';
2
- import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
1
+ import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
2
+ import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
3
3
  import { isInlinedExpression } from '../util/field.util.js';
4
- import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, queryChildrenOf, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
4
+ import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, columnFamily, countedRelations, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
6
- import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX, PER_PARENT_BRANCH_ALIAS } from './aliases.js';
6
+ import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS, relationSortColumn } from './aliases.js';
7
7
  import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
8
8
  import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
9
9
  import { SqlQueryContext } from './queryContext.js';
10
10
  import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
11
11
  import { resolveVectorCast } from './vectorCast.js';
12
12
  import { VectorSqlDialect } from './vectorSqlDialect.js';
13
+ /** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
14
+ export function relationTermKey({ sql, key }) {
15
+ if (key === undefined) {
16
+ throw new TypeError(`a raw $select in a populated relation needs an alias, the key its value lands under: ${sql}`);
17
+ }
18
+ return key;
19
+ }
20
+ /**
21
+ * What a projection reads: its raw expressions, or its fields past any `$exclude`, and every field where
22
+ * none is left of a row crossing JSON, which answers only under keys.
23
+ */
24
+ function projectedKeys(meta, select, exclude, json) {
25
+ const selected = Array.isArray(select)
26
+ ? select
27
+ : normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
28
+ return selected.length || !json ? selected : normalizeScalarFieldSelection(meta);
29
+ }
13
30
  /** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
14
31
  function inOperands(op, value) {
15
32
  if (!Array.isArray(value)) {
@@ -49,6 +66,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
49
66
  * `insertMany` splits larger batches into multiple statements based on this limit.
50
67
  */
51
68
  maxBindValues = 32766;
69
+ /**
70
+ * The most arguments one SQL function call takes. A variadic call past it, a wide relation row or JSON
71
+ * update, is spread over nested calls. No cap binds unless a dialect declares one.
72
+ */
73
+ maxFunctionArgs = Infinity;
52
74
  getBeginTransactionStatements(isolationLevel) {
53
75
  const level = isolationLevel?.toUpperCase();
54
76
  const strategy = this.isolationLevelStrategy;
@@ -61,34 +83,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
61
83
  // 'set-before' - MySQL/MariaDB pattern
62
84
  return [`SET TRANSACTION ISOLATION LEVEL ${level}`, this.beginTransactionCommand];
63
85
  }
64
- /**
65
- * Every parent's own bounded page in one statement: a subquery per parent, each filtered to that
66
- * parent alone and carrying its own `ORDER BY`, `LIMIT` and `OFFSET`. Universal, and reads
67
- * `parents x (skip + limit)` rows where a `ROW_NUMBER` window reads every matching child.
68
- * [The design](../../../../architecture/populate-limits.md).
69
- *
70
- * Each branch is a wrapped derived table rather than a bare parenthesised select: SQLite rejects
71
- * `ORDER BY`/`LIMIT` on the latter, and the wrapper costs nothing elsewhere.
72
- */
73
- findPerParent(ctx, entity, q, partition) {
74
- if (!partition.parents.length) {
75
- // Guarded on the contract rather than in either shape: this is the end that would otherwise
76
- // append nothing and hand the driver an empty statement, and both shapes owe the same promise.
77
- throw new TypeError('cannot read a bounded relation for no parents at all');
78
- }
79
- this.appendPerParent(ctx, entity, q, partition);
80
- }
81
- /** The shape {@link findPerParent} emits, which the Postgres family replaces with a `LATERAL` join. */
82
- appendPerParent(ctx, entity, q, { joins, parents }) {
83
- parents.forEach((parent, index) => {
84
- if (index) {
85
- ctx.append(' UNION ALL ');
86
- }
87
- ctx.append('SELECT * FROM (');
88
- this.find(ctx, entity, queryChildrenOf(q, joins, parent));
89
- ctx.append(`) ${this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS))}`);
90
- });
91
- }
92
86
  createContext() {
93
87
  return new SqlQueryContext(this);
94
88
  }
@@ -164,69 +158,70 @@ export class AbstractSqlDialect extends VectorSqlDialect {
164
158
  const [idKey] = meta.ids;
165
159
  return meta.ids.length === 1 ? `${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
166
160
  }
167
- search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS) {
161
+ search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS, order) {
168
162
  const meta = getMeta(entity);
169
163
  const prefix = this.resolveRelationAwarePrefix(this.resolveTableAlias(meta), meta, opts, q.$populate, joins);
170
164
  if (opts.prefix !== prefix) {
171
165
  opts = { ...opts, prefix };
172
166
  }
173
167
  this.where(ctx, entity, q.$where, opts);
174
- const sorted = this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
168
+ const sorted = order
169
+ ? this.orderCarried(ctx, q, order)
170
+ : this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
175
171
  this.pager(ctx, q, sorted);
176
172
  }
177
- selectFields(ctx, entity, select, opts = {}, exclude) {
178
- const meta = getMeta(entity);
179
- const prefix = opts.prefix ? opts.prefix + '.' : '';
180
- const escapedPrefix = this.escapeId(opts.prefix, true, true);
181
- const scalars = Array.isArray(select)
182
- ? select // raw SQL projections passed as QueryRaw[]
183
- : normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
184
- // A prefix means relations are in play: rows arrive keyed by the id and `fillToManyRelations`
185
- // groups children by it, so it outlives any subtraction - `$exclude` or falsy `$select` alike.
186
- // Every key of a composite, since grouping by part of one would gather the wrong rows together.
187
- const missingIds = opts.prefix ? meta.ids.filter((key) => !scalars.includes(key)) : [];
188
- const selectArr = missingIds.length ? [...missingIds, ...scalars] : scalars;
189
- if (!selectArr.length) {
190
- ctx.append(escapedPrefix + '*');
191
- return;
173
+ /**
174
+ * A relation's rows ordered by the columns their sort terms were carried out in, where they are
175
+ * paged: otherwise the aggregate reading them orders them, and sorting them first is wasted work.
176
+ */
177
+ orderCarried(ctx, q, order) {
178
+ if (!order.length || (q.$limit === undefined && q.$skip === undefined)) {
179
+ return false;
192
180
  }
193
- selectArr.forEach((key, index) => {
194
- if (index > 0)
195
- ctx.append(', ');
196
- if (key instanceof QueryRaw) {
197
- this.getRawValue(ctx, {
198
- value: key,
199
- prefix: opts.prefix,
200
- escapedPrefix,
201
- autoPrefixAlias: opts.autoPrefixAlias,
202
- });
203
- }
204
- else {
205
- const field = fieldOf(meta, key);
206
- if (isInlinedExpression(field)) {
207
- // Qualified even when nothing else in this statement is: the expression is spliced in, and
208
- // one that opens a correlated subquery has the inner table's columns in scope, so a bare
209
- // `"id"` would bind to *that* table instead of this one. `SELECT "Item"."id" FROM "Item"`
210
- // is valid on every engine, so naming the table costs nothing where it is not needed.
211
- const qualified = opts.prefix ?? this.resolveTableAlias(meta);
212
- this.getRawValue(ctx, {
213
- value: field.computed.as(key),
214
- prefix: qualified,
215
- escapedPrefix: this.escapeId(qualified, true, true),
216
- autoPrefixAlias: opts.autoPrefixAlias,
217
- });
218
- return;
219
- }
220
- const columnName = this.resolveColumnName(key, field);
221
- const column = escapedPrefix + this.escapeId(columnName);
222
- const expr = this.selectFieldExpr(column, field);
223
- ctx.append(expr);
224
- // An expression needs the alias too, or the row comes back keyed by the expression text.
225
- if (expr !== column || columnName !== key || opts.autoPrefixAlias) {
226
- ctx.append(' ' + this.escapeId(prefix + key, true));
227
- }
228
- }
229
- });
181
+ ctx.append(` ORDER BY ${order.map(({ ref, direction }) => ref + direction).join(', ')}`);
182
+ return true;
183
+ }
184
+ /**
185
+ * The columns a projection reads: each field under its key, a raw expression under its alias, and
186
+ * `*` where nothing is left, or every field in a row crossing JSON, which answers only under keys. A
187
+ * joined row keeps its key, every column of a composite past any subtraction: it is what tells a
188
+ * matched row from no match.
189
+ */
190
+ selectTerms(ctx, entity, select, opts = {}, exclude) {
191
+ const meta = getMeta(entity);
192
+ const selected = projectedKeys(meta, select, exclude, opts.json);
193
+ const missingIds = opts.joined ? meta.ids.filter((key) => !selected.includes(key)) : [];
194
+ const keys = missingIds.length ? [...missingIds, ...selected] : selected;
195
+ if (!keys.length) {
196
+ return [{ sql: `${this.escapeId(opts.prefix, true, true)}*`, bare: true }];
197
+ }
198
+ return keys.map((key) => key instanceof QueryRaw
199
+ ? { sql: this.rawSql(ctx, key, opts.prefix), key: key[RAW_ALIAS] }
200
+ : this.fieldTerm(ctx, meta, key, opts));
201
+ }
202
+ /** One field's column, or the expression an inlined one stands for, as the projection reads it. */
203
+ fieldTerm(ctx, meta, key, opts) {
204
+ const field = fieldOf(meta, key);
205
+ if (isInlinedExpression(field)) {
206
+ // Qualified even when nothing else in this statement is: the expression is spliced in, and one
207
+ // that opens a correlated subquery has the inner table's columns in scope, so a bare `"id"`
208
+ // would bind to *that* table instead of this one.
209
+ const sql = this.rawSql(ctx, field.computed, opts.prefix ?? this.resolveTableAlias(meta));
210
+ return { sql: opts.json ? this.carried(`(${sql})`, field) : sql, key };
211
+ }
212
+ const columnName = this.resolveColumnName(key, field);
213
+ const column = this.escapeId(opts.prefix, true, true) + this.escapeId(columnName);
214
+ const sql = opts.json ? this.carried(column, field) : this.selectFieldExpr(column, field);
215
+ return { sql, key, bare: sql === column && columnName === key };
216
+ }
217
+ /** A `raw()` rendered where it stands, without the alias a projection writes for it. */
218
+ rawSql(ctx, value, prefix) {
219
+ return this.buildFragment(ctx, (fragmentCtx) => value.render({
220
+ ctx: fragmentCtx,
221
+ dialect: this,
222
+ prefix: prefix ?? '',
223
+ escapedPrefix: this.escapeId(prefix, true, true),
224
+ }));
230
225
  }
231
226
  /**
232
227
  * What follows `SELECT` before the projection. Empty everywhere but SQL Server, whose `FETCH` will
@@ -236,8 +231,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
236
231
  return '';
237
232
  }
238
233
  /**
239
- * The expression a scalar field is read through, the plain column by default. MariaDB reads a
240
- * vector column back with `VEC_ToText`, since selecting it raw yields its binary form.
234
+ * The expression a scalar field is read through in the statement's own rows, the plain column by
235
+ * default. MariaDB reads a vector column back with `VEC_ToText`, since selecting it raw yields its
236
+ * binary form. A related row's column crosses JSON through {@link carriedFields} instead.
241
237
  */
242
238
  selectFieldExpr(escapedColumn, _field) {
243
239
  return escapedColumn;
@@ -253,26 +249,68 @@ export class AbstractSqlDialect extends VectorSqlDialect {
253
249
  }
254
250
  select(ctx, entity, q, opts = {}, joins = NO_JOINS, totalAlias) {
255
251
  const meta = getMeta(entity);
256
- const { alias, ref } = this.tableRef(meta);
252
+ const { alias, ref } = this.tableRef(meta, opts.alias);
257
253
  const prefix = this.resolveRelationAwarePrefix(alias, meta, opts, q.$populate, joins);
258
- ctx.append(q.$distinct ? 'SELECT DISTINCT ' : 'SELECT ');
259
- ctx.append(this.selectModifier(q));
260
- this.selectFields(ctx, entity, q.$select, { prefix }, q.$exclude);
261
- // Add related fields BEFORE FROM clause
262
- this.selectRelationFields(ctx, joins);
263
- // Inject vector distance projections when $project is set
264
- for (const [key, val] of Object.entries(q.$sort ?? {})) {
265
- if (isVectorSearch(val) && val.$project) {
266
- ctx.append(', ');
267
- this.appendVectorProjection(ctx, meta, key, val);
268
- }
269
- }
254
+ const terms = this.projection(ctx, entity, q, { prefix, json: opts.json }, joins);
255
+ const carried = opts.carried ? this.carrySort(ctx, meta, q, { prefix, joins, distinct: q.$distinct }) : undefined;
256
+ const columns = carried ? [...terms, ...carried.columns] : [...terms];
270
257
  if (totalAlias) {
271
- ctx.append(`, ${this.totalOverExpr} ${this.escapeId(totalAlias, true)}`);
258
+ columns.push({ sql: this.totalOverExpr, key: totalAlias });
272
259
  }
260
+ ctx.append(q.$distinct ? 'SELECT DISTINCT ' : 'SELECT ');
261
+ ctx.append(this.selectModifier(q));
262
+ ctx.append(columns.map((term) => this.termSql(term)).join(', '));
273
263
  ctx.append(` FROM ${ref}${this.lockHint(q)}`);
274
- // Add JOINs AFTER FROM clause
275
264
  this.selectRelationJoins(ctx, meta, alias, joins);
265
+ return { terms, order: carried?.order };
266
+ }
267
+ /**
268
+ * Everything a read's rows answer under, in order: its own fields, each joined row's fields and to-many
269
+ * relations under its path, its own to-many relations and `$count`, and each vector distance a `$sort`
270
+ * projects.
271
+ */
272
+ projection(ctx, entity, q, opts, joins) {
273
+ const meta = getMeta(entity);
274
+ const parent = opts.prefix ?? this.resolveTableAlias(meta);
275
+ const distinct = !!q.$distinct;
276
+ return [
277
+ ...this.selectTerms(ctx, entity, q.$select, opts, q.$exclude),
278
+ ...this.selectJoinedRows(ctx, joins, opts.json, distinct),
279
+ ...this.selectToManyRelations(ctx, meta, q.$populate, parent, distinct),
280
+ ...this.selectRelationCounts(ctx, meta, q.$count, parent),
281
+ ...this.selectVectorProjections(ctx, meta, q),
282
+ ];
283
+ }
284
+ /** A term as a projection writes it, aliased unless its SQL already answers under its key. */
285
+ termSql({ sql, key, bare }) {
286
+ return bare || key === undefined ? sql : `${sql} ${this.escapeId(key, true)}`;
287
+ }
288
+ /** The distance each vector `$sort` projects, under the name it asked for. */
289
+ selectVectorProjections(ctx, meta, q) {
290
+ return Object.entries(q.$sort ?? {}).flatMap(([key, value]) => isVectorSearch(value) && value.$project
291
+ ? [
292
+ {
293
+ sql: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorProjection(fragmentCtx, meta, key, value)),
294
+ key: value.$project,
295
+ },
296
+ ]
297
+ : []);
298
+ }
299
+ /**
300
+ * A relation's sort terms carried out beside its rows as columns, for the aggregate reading them to
301
+ * order by: a term that names a column of theirs already is ordered by that one.
302
+ */
303
+ carrySort(ctx, meta, q, opts) {
304
+ const columns = [];
305
+ const order = this.sortTerms(ctx, meta, q.$sort, opts).map(({ key, expr, direction, output }) => {
306
+ if (output) {
307
+ return { ref: expr, direction };
308
+ }
309
+ const column = relationSortColumn(key);
310
+ columns.push({ sql: expr, key: column });
311
+ return { ref: this.escapeId(column, true), direction };
312
+ });
313
+ return { columns, order };
276
314
  }
277
315
  /**
278
316
  * The table as a statement writes it, each part escaped on its own rather than as one dotted
@@ -282,35 +320,49 @@ export class AbstractSqlDialect extends VectorSqlDialect {
282
320
  return this.escapeQualifiedId(this.resolveTableAlias(meta), this.resolveSchema(meta));
283
321
  }
284
322
  /**
285
- * A FROM or JOIN operand plus the alias to prefix its columns by, aliased only once a schema puts
286
- * something in front of the name. See {@link resolveTableAlias} for why the prefix cannot be the
287
- * qualified path.
323
+ * A FROM or JOIN operand plus the alias to prefix its columns by, aliased once a schema puts something
324
+ * in front of the name, or the read takes an alias of its own. See {@link resolveTableAlias} for why
325
+ * the prefix cannot be the qualified path.
288
326
  */
289
- tableRef(meta) {
290
- const alias = this.resolveTableAlias(meta);
327
+ tableRef(meta, alias = this.resolveTableAlias(meta)) {
291
328
  const name = this.escapedTableName(meta);
292
- const schema = this.resolveSchema(meta);
293
- return { alias, ref: schema ? `${name} ${this.escapeId(alias, true)}` : name };
329
+ const aliased = !!this.resolveSchema(meta) || alias !== this.resolveTableAlias(meta);
330
+ return { alias, ref: aliased ? `${name} ${this.escapeId(alias, true)}` : name };
294
331
  }
295
- /** Columns are alias-qualified once anything else is in play: a join, or a to-many being filled. */
332
+ /**
333
+ * Columns are qualified once anything else is in play: an alias of the read's own, a join, or a
334
+ * relation being read.
335
+ */
296
336
  resolveRelationAwarePrefix(tableName, meta, opts, populate, joins) {
337
+ if (opts.alias) {
338
+ return opts.alias;
339
+ }
297
340
  return (opts.prefix ?? (opts.autoPrefix || joins.size > 0 || populatesRelations(meta, populate)))
298
341
  ? tableName
299
342
  : undefined;
300
343
  }
301
- selectRelationFields(ctx, joins) {
344
+ /** Each joined row's columns and to-many relations under its path, which is what unflattens it. */
345
+ selectJoinedRows(ctx, joins, json, distinct) {
346
+ const terms = [];
302
347
  for (const join of joins.values()) {
303
348
  // A join `$sort` asked for adds no columns: it orders the rows, it does not widen them.
304
349
  if (!join.projected)
305
350
  continue;
306
- ctx.append(', ');
307
- this.selectFields(ctx, join.entity, join.query.$select, { prefix: join.path, autoPrefixAlias: true }, join.query.$exclude);
351
+ const opts = { prefix: join.alias, json, joined: true };
352
+ const row = [
353
+ ...this.selectTerms(ctx, join.entity, join.query.$select, opts, join.query.$exclude),
354
+ ...this.selectToManyRelations(ctx, join.meta, join.query.$populate, join.alias, distinct),
355
+ ];
356
+ for (const term of row) {
357
+ terms.push({ sql: term.sql, key: `${join.path}.${relationTermKey(term)}` });
358
+ }
308
359
  }
360
+ return terms;
309
361
  }
310
362
  selectRelationJoins(ctx, meta, rootAlias, joins) {
311
363
  for (const join of joins.values()) {
312
- const joinAlias = this.escapeId(join.path, true);
313
- const parentAlias = join.parent ? this.escapeId(join.parent.path, true) : this.escapeId(rootAlias, true);
364
+ const joinAlias = this.escapeId(join.alias, true);
365
+ const parentAlias = this.escapeId(join.parent ? join.parent.alias : rootAlias, true);
314
366
  ctx.append(` ${join.required ? 'INNER' : 'LEFT'} JOIN ${this.escapedTableName(join.meta)} ${joinAlias} ON `);
315
367
  join.relation.references.forEach((reference, index) => {
316
368
  if (index > 0)
@@ -325,7 +377,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
325
377
  // particular `security: true` ones) must apply even to a bare `$populate: { rel: true }`
326
378
  // with no explicit `$where` - and equally to a join `$sort` brought in on its own.
327
379
  // `where()` -> `renderWhere()` no-ops cleanly (appends nothing) when there is nothing to add.
328
- this.where(ctx, join.entity, join.query.$where ?? {}, { prefix: join.path, clause: 'AND' });
380
+ this.where(ctx, join.entity, join.query.$where ?? {}, { prefix: join.alias, clause: 'AND' });
329
381
  }
330
382
  }
331
383
  where(ctx, entity, where = {}, opts = {}) {
@@ -360,9 +412,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
360
412
  if (val instanceof QueryRaw) {
361
413
  if (key === '$exists' || key === '$nexists') {
362
414
  ctx.append(key === '$exists' ? 'EXISTS (' : 'NOT EXISTS (');
363
- // The alias: the enclosing statement declares one, and Postgres forbids reaching past it
364
- // to the qualified name it aliased.
365
- const alias = this.resolveTableAlias(meta);
415
+ // The read's alias: the enclosing statement declares one, and Postgres forbids reaching past
416
+ // it to the qualified name it aliased.
417
+ const alias = opts.prefix ?? this.resolveTableAlias(meta);
366
418
  this.getRawValue(ctx, {
367
419
  value: val,
368
420
  prefix: alias,
@@ -397,10 +449,10 @@ export class AbstractSqlDialect extends VectorSqlDialect {
397
449
  if (rel) {
398
450
  const sizeVal = parseRelationSize(val);
399
451
  if (sizeVal !== undefined) {
400
- this.compareRelationSize(ctx, entity, sizeVal, rel, opts);
452
+ this.compareRelationSize(ctx, entity, key, sizeVal, rel, opts);
401
453
  return;
402
454
  }
403
- this.compareRelation(ctx, entity, val, rel, opts);
455
+ this.compareRelation(ctx, entity, key, val, rel, opts);
404
456
  return;
405
457
  }
406
458
  const value = this.normalizeWhereValue(val);
@@ -700,7 +752,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
700
752
  if (isOperatorOnlyObject(match)) {
701
753
  const entries = Object.entries(match);
702
754
  const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) => isJsonbOp(op, val));
703
- const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
755
+ const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
704
756
  const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, this.elemAccessor(alias, asJson), '', op, val, asJson));
705
757
  return jsonElemExists(this.jsonElemFrom(jsonField, [], alias, asJson), conditions);
706
758
  }
@@ -712,7 +764,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
712
764
  if (this.jsonContainmentIsPartial && !someValue(match, isOperatorObject)) {
713
765
  return this.jsonAll(ctx, jsonField, [match]);
714
766
  }
715
- const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
767
+ const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
716
768
  const conditions = buildElemMatchConditions(match, (field, op, opVal) => {
717
769
  const asJson = isJsonbOp(op, opVal);
718
770
  return this.buildJsonFieldCondition(ctx, this.elemAccessor(alias, asJson), field, op, opVal, asJson);
@@ -749,19 +801,24 @@ export class AbstractSqlDialect extends VectorSqlDialect {
749
801
  /** Appends the `ORDER BY`, reporting whether there was one - which {@link pager} needs on the
750
802
  * engines that refuse to page an unordered statement. */
751
803
  sort(ctx, entity, sort, opts = {}) {
804
+ const terms = this.sortTerms(ctx, getMeta(entity), sort, opts);
805
+ if (terms.length) {
806
+ ctx.append(` ORDER BY ${terms.map(({ expr, direction }) => expr + direction).join(', ')}`);
807
+ }
808
+ return terms.length > 0;
809
+ }
810
+ /**
811
+ * The terms of an `ORDER BY`, collected before anything is appended so an unorderable key is reported
812
+ * instead of half a clause, and because a vector distance is the primary ordering wherever it appears.
813
+ */
814
+ sortTerms(ctx, meta, sort, opts) {
752
815
  if (!hasKeys(sort)) {
753
- return false;
816
+ return [];
754
817
  }
755
- // Collected before anything is appended so an unorderable key is reported instead of half a
756
- // clause, and because a vector distance is the primary ordering wherever it appears in the map.
757
818
  const vectors = [];
758
819
  const columns = [];
759
- this.collectSortTerms(ctx, getMeta(entity), sort, opts, vectors, columns);
760
- const terms = [...vectors, ...columns];
761
- if (terms.length) {
762
- ctx.append(` ORDER BY ${terms.join(', ')}`);
763
- }
764
- return terms.length > 0;
820
+ this.collectSortTerms(ctx, meta, sort, opts, vectors, columns);
821
+ return [...vectors, ...columns];
765
822
  }
766
823
  /**
767
824
  * Walks `$sort` against the metadata of the entity each level addresses, rather than flattening it
@@ -769,30 +826,35 @@ export class AbstractSqlDialect extends VectorSqlDialect {
769
826
  * through its own `@Field({ name })`, and only that way is `tax.category` the one alias the join
770
827
  * carries instead of two quoted identifiers.
771
828
  */
772
- collectSortTerms(ctx, meta, sort, opts, vectors, columns, path = '') {
773
- // Below the first level the alias a column is qualified by *is* the path walked to reach it.
774
- const prefix = path || opts.prefix;
829
+ collectSortTerms(ctx, meta, sort, opts, vectors, columns, path = '',
830
+ // Below the first level, the alias of the join the path walked to.
831
+ prefix = opts.prefix) {
775
832
  for (const [key, value] of Object.entries(sort)) {
776
833
  const relation = meta.relations[key];
834
+ const keyPath = path ? `${path}.${key}` : key;
777
835
  if (relation) {
778
- const relPath = path ? `${path}.${key}` : key;
779
836
  const countDirection = parseSortByCount(value);
780
837
  if (countDirection !== undefined) {
781
838
  // A correlated count, not a join: a parent has many of these, so what is being ordered by
782
839
  // is how many, and `SELECT DISTINCT` cannot order by an expression it did not select.
783
840
  if (opts.distinct) {
784
- throw new TypeError(`cannot $sort by '${relPath}.$count' with $distinct: it is not a selected column`);
841
+ throw new TypeError(`cannot $sort by '${keyPath}.$count' with $distinct: it is not a selected column`);
785
842
  }
786
- columns.push(this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, relation, { prefix }, 'COUNT(*)', {})) + this.resolveSortDirection(countDirection));
843
+ columns.push({
844
+ key: keyPath,
845
+ expr: this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, key, relation, { prefix }, 'COUNT(*)', {})),
846
+ direction: this.resolveSortDirection(countDirection),
847
+ output: false,
848
+ });
787
849
  continue;
788
850
  }
789
- const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, value, opts.joins ?? NO_JOINS, `cannot $sort by relation '${relPath}': this statement joins no relations`);
851
+ const { join, sort: relationSort } = resolveSortableJoin(relation, keyPath, value, opts.joins ?? NO_JOINS, `cannot $sort by relation '${keyPath}': this statement joins no relations`);
790
852
  // `SELECT DISTINCT` can only order by what it selected, on every engine here, so a join
791
853
  // brought in for the sort alone has nothing to order by. Populating it selects its columns.
792
854
  if (opts.distinct && !join.projected) {
793
- throw new TypeError(`cannot $sort by relation '${relPath}' with $distinct unless '${relPath}' is populated: SELECT DISTINCT orders only by selected columns`);
855
+ throw new TypeError(`cannot $sort by relation '${keyPath}' with $distinct unless '${keyPath}' is populated: SELECT DISTINCT orders only by selected columns`);
794
856
  }
795
- this.collectSortTerms(ctx, join.meta, relationSort, opts, vectors, columns, relPath);
857
+ this.collectSortTerms(ctx, join.meta, relationSort, opts, vectors, columns, keyPath, join.alias);
796
858
  continue;
797
859
  }
798
860
  if (isVectorSearch(value)) {
@@ -801,11 +863,20 @@ export class AbstractSqlDialect extends VectorSqlDialect {
801
863
  }
802
864
  // Already projected in the SELECT list: order by that alias rather than recomputing it.
803
865
  vectors.push(value.$project
804
- ? this.escapeId(value.$project)
805
- : this.buildFragment(ctx, (fragmentCtx) => this.appendVectorSort(fragmentCtx, meta, key, value)));
866
+ ? { key: keyPath, expr: this.escapeId(value.$project), direction: '', output: true }
867
+ : {
868
+ key: keyPath,
869
+ expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorSort(fragmentCtx, meta, key, value)),
870
+ direction: '',
871
+ output: false,
872
+ });
806
873
  continue;
807
874
  }
808
- columns.push(this.sortColumn(ctx, meta, key, prefix) + this.resolveSortDirection(value));
875
+ columns.push({
876
+ key: keyPath,
877
+ ...this.sortColumn(ctx, meta, key, prefix),
878
+ direction: this.resolveSortDirection(value),
879
+ });
809
880
  }
810
881
  }
811
882
  /**
@@ -815,11 +886,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
815
886
  sortColumn(ctx, meta, key, prefix) {
816
887
  const field = meta.fields[key];
817
888
  if (field) {
818
- return (this.inlinedOperand(ctx, field, prefix ?? this.resolveTableAlias(meta)) ??
819
- this.columnWithPrefix(key, field, prefix));
889
+ const expr = this.inlinedOperand(ctx, field, prefix ?? this.resolveTableAlias(meta)) ??
890
+ this.columnWithPrefix(key, field, prefix);
891
+ return { expr, output: false };
820
892
  }
821
893
  const json = this.resolveJsonDotPath(meta, key, prefix);
822
- return json ? this.jsonPathExpr(json.column, json.jsonPath, 'text') : this.escapeId(key);
894
+ return json
895
+ ? { expr: this.jsonPathExpr(json.column, json.jsonPath, 'text'), output: false }
896
+ : { expr: this.escapeId(key), output: true };
823
897
  }
824
898
  /**
825
899
  * `LIMIT`/`OFFSET`. `sorted` says whether an `ORDER BY` was emitted just before, which
@@ -870,7 +944,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
870
944
  * joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
871
945
  * and the other engines quietly widen the lock to the joined rows.
872
946
  */
873
- appendLock(ctx, entity, q, joins = NO_JOINS) {
947
+ appendLock(ctx, entity, q, joins = NO_JOINS, alias) {
874
948
  const wait = parseQueryLock(q.$lock);
875
949
  if (!wait) {
876
950
  return;
@@ -878,7 +952,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
878
952
  this.assertLockSupported(entity, q, joins);
879
953
  const meta = getMeta(entity);
880
954
  // `OF` names the alias in the FROM, never the schema-qualified path it was aliased from.
881
- const target = joins.size > 0 ? ` OF ${this.escapeId(this.resolveTableAlias(meta), true)}` : '';
955
+ const target = joins.size > 0 ? ` OF ${this.escapeId(alias ?? this.resolveTableAlias(meta), true)}` : '';
882
956
  const suffix = wait === 'skip' ? ' SKIP LOCKED' : wait === 'nowait' ? ' NOWAIT' : '';
883
957
  ctx.append(` FOR UPDATE${target}${suffix}`);
884
958
  }
@@ -901,9 +975,10 @@ export class AbstractSqlDialect extends VectorSqlDialect {
901
975
  * many rows there are beyond the page it already has.
902
976
  */
903
977
  countDistinct(ctx, entity, q, opts) {
978
+ const read = this.readOptions(ctx, getMeta(entity), opts);
904
979
  ctx.append(`SELECT COUNT(*) ${this.escapeId(COUNT_ALIAS, true)} FROM (`);
905
- this.select(ctx, entity, q, opts);
906
- this.search(ctx, entity, { $where: q.$where }, opts);
980
+ this.select(ctx, entity, q, read);
981
+ this.search(ctx, entity, { $where: q.$where }, read);
907
982
  ctx.append(`) ${this.escapeId(DISTINCT_DERIVED_ALIAS, true)}`);
908
983
  }
909
984
  /**
@@ -1025,14 +1100,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1025
1100
  */
1026
1101
  totalOverExpr = 'COUNT(*) OVER ()';
1027
1102
  find(ctx, entity, q = {}, opts, totalAlias) {
1103
+ const meta = getMeta(entity);
1104
+ const read = this.readOptions(ctx, meta, opts);
1028
1105
  // The one statement that can join, so the one that resolves the join set; everything else renders
1029
- // against `NO_JOINS` and rejects a `$sort` that would need one.
1030
- const joins = resolveQueryJoins(getMeta(entity), q);
1031
- this.select(ctx, entity, q, opts, joins, totalAlias);
1032
- this.search(ctx, entity, q, opts, joins);
1033
- // Appended here rather than in `search`, which `count`/`update`/`delete` share: a lock belongs
1034
- // to a SELECT alone. Every engine spells it after LIMIT/OFFSET, so it goes last.
1035
- this.appendLock(ctx, entity, q, joins);
1106
+ // against `NO_JOINS` and rejects a `$sort` that would need one. The joins claim their aliases after
1107
+ // the table's own.
1108
+ this.read(ctx, entity, q, read, resolveQueryJoins(meta, q, (path) => ctx.claimAlias(path)), totalAlias);
1109
+ }
1110
+ /**
1111
+ * A read's whole statement. The lock is appended here rather than in `search`, which `count`,
1112
+ * `update` and `delete` share: it belongs to a SELECT alone, and every engine spells it last.
1113
+ */
1114
+ read(ctx, entity, q, opts, joins, totalAlias) {
1115
+ const projection = this.select(ctx, entity, q, opts, joins, totalAlias);
1116
+ this.search(ctx, entity, q, opts, joins, projection.order);
1117
+ this.appendLock(ctx, entity, q, joins, opts.alias);
1118
+ return projection;
1119
+ }
1120
+ /**
1121
+ * `opts` with the alias the read's table claims: its own name, which needs no alias written, unless
1122
+ * another table of the statement took it first.
1123
+ */
1124
+ readOptions(ctx, meta, opts = {}) {
1125
+ if (opts.alias !== undefined) {
1126
+ return opts;
1127
+ }
1128
+ const name = this.resolveTableAlias(meta);
1129
+ const alias = ctx.claimAlias(name);
1130
+ return alias === name ? opts : { ...opts, alias };
1036
1131
  }
1037
1132
  insert(ctx, entity, payload, opts) {
1038
1133
  // Every engine whose ids come back from the statement itself wants the same clause, so it is
@@ -1340,7 +1435,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1340
1435
  return decoded;
1341
1436
  }
1342
1437
  /**
1343
- * The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode.
1438
+ * The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode. A
1439
+ * date and bytes decode because a related row crosses JSON, which spells both as text.
1344
1440
  *
1345
1441
  * `BigInt` is asked first because it shares the numeric family with `Number`: let the switch answer
1346
1442
  * it and every `type: BigInt` property silently decodes to a JS number again.
@@ -1359,6 +1455,10 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1359
1455
  return 'boolean';
1360
1456
  case 'numeric':
1361
1457
  return 'number';
1458
+ case 'date':
1459
+ return 'date';
1460
+ case 'blob':
1461
+ return 'bytes';
1362
1462
  default:
1363
1463
  return undefined;
1364
1464
  }
@@ -1432,7 +1532,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1432
1532
  return Object.entries(pull).reduce((acc, [key, value]) => this.jsonPullKey(ctx, acc, escapedCol, key, value), expr);
1433
1533
  }
1434
1534
  getRawValue(ctx, opts) {
1435
- const { value, prefix = '', escapedPrefix, autoPrefixAlias } = opts;
1535
+ const { value, prefix = '', escapedPrefix } = opts;
1436
1536
  value.render({
1437
1537
  ...opts,
1438
1538
  ctx,
@@ -1442,8 +1542,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1442
1542
  });
1443
1543
  const alias = value[RAW_ALIAS];
1444
1544
  if (alias) {
1445
- const fullAlias = autoPrefixAlias && prefix ? `${prefix}.${alias}` : alias;
1446
- ctx.append(' ' + this.escapeId(fullAlias, true));
1545
+ ctx.append(' ' + this.escapeId(alias, true));
1447
1546
  }
1448
1547
  }
1449
1548
  /**
@@ -1558,65 +1657,173 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1558
1657
  escapedColumn(table, meta, key) {
1559
1658
  return this.escapeId(table, false, true) + this.escapedColumnName(meta, key);
1560
1659
  }
1561
- /** As {@link escapedColumn}, but qualified by the query alias when the parent is nested. */
1562
- escapedParentColumn(parentTable, meta, opts, key) {
1563
- return opts.prefix
1564
- ? this.escapeId(opts.prefix, true, true) + this.escapedColumnName(meta, key)
1565
- : this.escapedColumn(parentTable, meta, key);
1566
- }
1567
1660
  /**
1568
1661
  * The single path from a relation operator to its target, so none can emit an unscoped subquery:
1569
1662
  * the target's `$where` is merged with its active filters, making a trashed or out-of-scope row
1570
1663
  * invisible here just as it is to a joined `$populate`. The caller's filter bypass is deliberately
1571
1664
  * not propagated (`withDeleted()` does not reach into relations), matching `selectRelationJoins`.
1572
1665
  */
1573
- appendRelationSubquery(ctx, meta, rel, opts, projection, val) {
1574
- // Aliases, not paths, everywhere a column is prefixed; `tableRef` declares them in the FROM.
1575
- const parentAlias = this.resolveTableAlias(meta);
1666
+ appendRelationSubquery(ctx, meta, relKey, rel, opts, projection, val) {
1576
1667
  const relatedEntity = rel.entity();
1577
1668
  const relatedMeta = getMeta(relatedEntity);
1578
- const { alias: relatedAlias, ref: relatedRef } = this.tableRef(relatedMeta);
1579
- // Resolved before any SQL is emitted: it also decides whether the mm form reaches the target.
1669
+ const parent = opts.prefix ?? this.resolveTableAlias(meta);
1670
+ // Resolved before any SQL is emitted: it also decides whether the junction form reaches the target.
1580
1671
  const targetWhere = this.scopedWhere(relatedMeta, val);
1581
1672
  ctx.append(`(SELECT ${projection} FROM `);
1582
- // One equality per key of the parent, anded: a composite correlates on every column, and matching
1583
- // on part of one would find the rows of a different parent. `parentJoins` is what keeps the two
1584
- // ends the right way round, whether the join lands on the junction or on the target.
1585
- const correlation = (alias, joinedMeta) => parentJoins(rel, meta.ids.length)
1586
- .map(({ parent, joined }) => `${this.escapedColumn(alias, joinedMeta, joined)} = ${this.escapedParentColumn(parentAlias, meta, opts, parent)}`)
1587
- .join(' AND ');
1588
- if (rel.cardinality === 'mm' && rel.through) {
1589
- const throughEntity = rel.through();
1590
- const throughMeta = getMeta(throughEntity);
1591
- const { alias: throughAlias, ref: throughRef } = this.tableRef(throughMeta);
1592
- ctx.append(throughRef);
1593
- ctx.append(` WHERE ${correlation(throughAlias, throughMeta)}`);
1594
- // The junction is a row being read too: a soft-deleted link is not a link.
1595
- this.where(ctx, throughEntity, {}, { prefix: throughAlias, clause: 'AND' });
1673
+ if (rel.through) {
1674
+ const junction = this.junctionRows(ctx, meta, rel, rel.through(), parent);
1675
+ ctx.append(junction.from);
1596
1676
  if (hasKeys(targetWhere)) {
1677
+ const related = this.tableRef(relatedMeta, ctx.claimAlias(relKey));
1597
1678
  const targetKey = soleIdOf(relatedMeta, 'a many-to-many target');
1598
- const [targetColumn] = targetKeyColumns(rel, meta.ids.length);
1599
- ctx.append(` AND ${this.escapedColumn(throughAlias, throughMeta, targetColumn)} IN (`);
1600
- ctx.append(`SELECT ${this.escapedColumn(relatedAlias, relatedMeta, targetKey)} FROM ${relatedRef}`);
1601
- this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: relatedAlias, clause: 'WHERE' });
1679
+ ctx.append(` AND ${junction.target} IN (`);
1680
+ ctx.append(`SELECT ${this.escapedColumn(related.alias, relatedMeta, targetKey)} FROM ${related.ref}`);
1681
+ this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: related.alias, clause: 'WHERE' });
1602
1682
  ctx.append(')');
1603
1683
  }
1604
1684
  }
1605
1685
  else {
1606
- ctx.append(relatedRef);
1607
- ctx.append(` WHERE ${correlation(relatedAlias, relatedMeta)}`);
1608
- this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: relatedAlias, clause: 'AND' });
1686
+ const related = this.tableRef(relatedMeta, ctx.claimAlias(relKey, parent));
1687
+ ctx.append(related.ref);
1688
+ ctx.append(` WHERE ${this.correlation(meta, rel, parent, related.alias, relatedMeta)}`);
1689
+ this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: related.alias, clause: 'AND' });
1609
1690
  }
1610
1691
  ctx.append(')');
1611
1692
  }
1693
+ /**
1694
+ * One equality per key of the parent, anded: a composite correlates on every column, and matching on
1695
+ * part of one would find the rows of a different parent. `parentJoins` keeps the two ends the right
1696
+ * way round, whether the join lands on the junction or on the target.
1697
+ */
1698
+ correlation(meta, rel, parent, alias, joinedMeta) {
1699
+ const escapedParent = this.escapeId(parent, true, true);
1700
+ return parentJoins(rel, meta.ids.length)
1701
+ .map(({ parent: key, joined }) => `${this.escapedColumn(alias, joinedMeta, joined)} = ${escapedParent}${this.escapedColumnName(meta, key)}`)
1702
+ .join(' AND ');
1703
+ }
1704
+ /**
1705
+ * Each to-many a row populates, as a subquery of the statement's select list correlated to `parent`,
1706
+ * the row's alias. [The design](../../../../architecture/relations-in-one-statement.md).
1707
+ */
1708
+ selectToManyRelations(ctx, meta, populate, parent, distinct) {
1709
+ return getRelationRequestSummary(meta, populate).toManyKeys.map((relKey) => {
1710
+ const { query } = parseRelationAtKey(relKey, populate);
1711
+ const sql = this.buildFragment(ctx, (fragmentCtx) => this.appendToManyRelation(fragmentCtx, meta, relKey, query, parent, distinct));
1712
+ return { sql, key: relKey };
1713
+ });
1714
+ }
1715
+ /** Each relation a read's `$count` tallies, as a correlated count of its select list. */
1716
+ selectRelationCounts(ctx, meta, count, parent) {
1717
+ return countedRelations(meta, count).map(({ relKey, relation, where }) => {
1718
+ const sql = this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, relKey, relation, { prefix: parent }, 'COUNT(*)', where));
1719
+ return { sql, key: `${COUNT_RESULT_KEY}.${relKey}` };
1720
+ });
1721
+ }
1722
+ /**
1723
+ * A to-many's rows as one JSON array: an ordinary read of the related entity under the relation's
1724
+ * name, narrowed to the parent's rows, in the engine's own spelling.
1725
+ */
1726
+ appendToManyRelation(ctx, meta, relKey, query, parent, distinct) {
1727
+ const relation = relationOf(meta, relKey);
1728
+ const entity = relation.entity();
1729
+ const relMeta = getMeta(entity);
1730
+ this.assertDistinctSort(relMeta, relKey, query);
1731
+ const alias = ctx.claimAlias(relKey, parent);
1732
+ const correlation = raw(({ ctx: rowsCtx }) => this.appendCorrelation(rowsCtx, meta, relation, parent, alias));
1733
+ const rows = { ...query, $where: { ...query.$where, $and: [...(query.$where?.$and ?? []), correlation] } };
1734
+ const joins = resolveQueryJoins(relMeta, rows, (path) => ctx.claimAlias(path));
1735
+ this.appendRelationArray(ctx, { entity, query: rows, alias, joins, distinct });
1736
+ }
1737
+ /**
1738
+ * A relation that deduplicates its rows is sorted only by what it selects: `SELECT DISTINCT` orders by
1739
+ * nothing else, and a column carrying a sort term out would join the set it deduplicates on.
1740
+ */
1741
+ assertDistinctSort(meta, relKey, query) {
1742
+ if (!query.$distinct || !query.$sort) {
1743
+ return;
1744
+ }
1745
+ const selected = projectedKeys(meta, query.$select, query.$exclude, true);
1746
+ for (const key of getKeys(query.$sort)) {
1747
+ if (!selected.includes(key)) {
1748
+ throw new TypeError(`cannot $sort the $distinct relation '${relKey}' by '${key}', which it does not select`);
1749
+ }
1750
+ }
1751
+ }
1752
+ /**
1753
+ * What makes a relation's row one of the parent's: its foreign key on the parent's key, or, through a
1754
+ * junction, a pairing of the two that the junction's own filters let through.
1755
+ */
1756
+ appendCorrelation(ctx, meta, rel, parent, alias) {
1757
+ const relMeta = getMeta(rel.entity());
1758
+ if (!rel.through) {
1759
+ ctx.append(this.correlation(meta, rel, parent, alias, relMeta));
1760
+ return;
1761
+ }
1762
+ const junction = this.junctionRows(ctx, meta, rel, rel.through(), parent);
1763
+ const targetKey = soleIdOf(relMeta, 'a many-to-many target');
1764
+ ctx.append(`${this.escapedColumn(alias, relMeta, targetKey)} IN (SELECT ${junction.target} FROM ${junction.from})`);
1765
+ }
1766
+ /**
1767
+ * A junction's rows pairing the parent with the relation's targets, as far as its own filters let them
1768
+ * through, since a soft-deleted link is not a link; and the column naming each row's target.
1769
+ */
1770
+ junctionRows(ctx, meta, rel, junction, parent) {
1771
+ const junctionMeta = getMeta(junction);
1772
+ const { alias, ref } = this.tableRef(junctionMeta, ctx.claimAlias(this.resolveTableAlias(junctionMeta), parent));
1773
+ const scope = this.buildFragment(ctx, (fragmentCtx) => this.where(fragmentCtx, junction, {}, { prefix: alias, clause: 'AND' }));
1774
+ const [target] = targetKeyColumns(rel, meta.ids.length);
1775
+ return {
1776
+ from: `${ref} WHERE ${this.correlation(meta, rel, parent, alias, junctionMeta)}${scope}`,
1777
+ target: this.escapedColumn(alias, junctionMeta, target),
1778
+ };
1779
+ }
1780
+ /**
1781
+ * Whether the engine's JSON aggregate takes an `ORDER BY` of its own. Where it does not, a relation's
1782
+ * rows carry no sort term out and keep their own order, which a derived table hands its aggregate.
1783
+ */
1784
+ orderedAggregates = true;
1785
+ /**
1786
+ * The rows read as a derived table, their values crossing JSON and, where the aggregate orders, each
1787
+ * sort term carried out beside them for it to order by, since a derived table's order is not promised
1788
+ * past it. The table takes the relation's name, which nothing inside it can see.
1789
+ */
1790
+ derivedRelation(ctx, rows) {
1791
+ const rowsCtx = ctx.createFragment();
1792
+ const readOpts = { alias: rows.alias, json: true, carried: this.orderedAggregates };
1793
+ const { terms, order = [] } = this.read(rowsCtx, rows.entity, rows.query, readOpts, rows.joins);
1794
+ const alias = this.escapeId(rows.alias, true);
1795
+ return {
1796
+ from: `(${rowsCtx.sql}) ${alias}`,
1797
+ pairs: terms.map((term) => {
1798
+ const key = relationTermKey(term);
1799
+ return [key, `${alias}.${this.escapeId(key, true)}`];
1800
+ }),
1801
+ order: order.map(({ ref, direction }) => `${alias}.${ref}${direction}`).join(', '),
1802
+ };
1803
+ }
1804
+ /**
1805
+ * How each column family crosses JSON inside its parent's statement, where JSON would round it or
1806
+ * cannot spell it: as text, which the field's hydrate kind decodes back, and bytes as `\x` and hex,
1807
+ * the `BYTES_PREFIX`. A family missing here crosses as it is.
1808
+ */
1809
+ carriedFields = {};
1810
+ /** `expr` as it crosses JSON, by its field's family: see {@link carriedFields}. */
1811
+ carried(expr, field) {
1812
+ const family = columnFamily(field.columnType ?? field.type);
1813
+ return (family && this.carriedFields[family]?.(expr, field)) ?? expr;
1814
+ }
1815
+ /** `'key', column, ...`: the arguments of a JSON object call over `pairs`. */
1816
+ jsonObjectArgs(pairs) {
1817
+ return pairs.map(([key, sql]) => `${this.escape(key)}, ${sql}`).join(', ');
1818
+ }
1612
1819
  /** Filter by relation: a parent matches when {@link appendRelationSubquery} finds one target row. */
1613
- compareRelation(ctx, entity, val, rel, opts) {
1820
+ compareRelation(ctx, entity, relKey, val, rel, opts) {
1614
1821
  ctx.append('EXISTS ');
1615
- this.appendRelationSubquery(ctx, getMeta(entity), rel, opts, '1', val);
1822
+ this.appendRelationSubquery(ctx, getMeta(entity), relKey, rel, opts, '1', val);
1616
1823
  }
1617
1824
  /** Filter by relation size: the same subquery, counting instead of testing for existence. */
1618
- compareRelationSize(ctx, entity, sizeVal, rel, opts) {
1619
- this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, getMeta(entity), rel, opts, 'COUNT(*)', {}), sizeVal);
1825
+ compareRelationSize(ctx, entity, relKey, sizeVal, rel, opts) {
1826
+ this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, getMeta(entity), relKey, rel, opts, 'COUNT(*)', {}), sizeVal);
1620
1827
  }
1621
1828
  /**
1622
1829
  * `<expr> <op> <value>` for each operator, AND-joined and parenthesized when there is more than