uql-orm 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -9
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +196 -33
- package/dist/dialect/abstractSqlDialect.js +410 -203
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +23 -48
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +26 -11
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/decorator/bag.d.ts +2 -2
- package/dist/entity/decorator/entity.d.ts +8 -9
- package/dist/entity/decorator/entity.js +6 -7
- package/dist/entity/decorator/members.d.ts +7 -6
- package/dist/entity/decorator/members.js +2 -1
- package/dist/entity/metadata/definition.d.ts +16 -11
- package/dist/entity/metadata/definition.js +54 -42
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +0 -1
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +2 -2
- package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
- package/dist/migrate/codegen/entityTypes.d.ts +1 -1
- package/dist/migrate/codegen/entityTypes.js +4 -3
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
- package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
- package/dist/migrate/ddl/pgIndexDdl.js +29 -12
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
- package/dist/migrate/migrator.d.ts +4 -4
- package/dist/migrate/schemaGenerator.d.ts +8 -8
- package/dist/migrate/schemaGenerator.js +5 -7
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +146 -104
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +31 -106
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +35 -129
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +4 -17
- package/dist/querier/abstractSqlQuerier.js +40 -50
- package/dist/schema/canonicalType.js +4 -4
- package/dist/schema/indexDifferences.js +4 -4
- package/dist/schema/schemaASTBuilder.d.ts +3 -3
- package/dist/schema/schemaASTBuilder.js +32 -3
- package/dist/schema/schemaASTDiffer.js +5 -5
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +38 -7
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +3 -3
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +110 -69
- package/dist/type/migration.d.ts +7 -7
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/querierPool.d.ts +2 -2
- package/dist/type/query.d.ts +41 -72
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +43 -34
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/queryWhere.d.ts +12 -9
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +4 -4
- package/dist/util/dialect.util.js +24 -15
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- package/dist/util/relationQuery.util.d.ts +12 -65
- package/dist/util/relationQuery.util.js +27 -81
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- 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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
240
|
-
* vector column back with `VEC_ToText`, since selecting it raw yields its
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
|
286
|
-
*
|
|
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
|
|
293
|
-
return { alias, ref:
|
|
329
|
+
const aliased = !!this.resolveSchema(meta) || alias !== this.resolveTableAlias(meta);
|
|
330
|
+
return { alias, ref: aliased ? `${name} ${this.escapeId(alias, true)}` : name };
|
|
294
331
|
}
|
|
295
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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.
|
|
313
|
-
const parentAlias = join.parent ?
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
760
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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 '${
|
|
841
|
+
throw new TypeError(`cannot $sort by '${keyPath}.$count' with $distinct: it is not a selected column`);
|
|
785
842
|
}
|
|
786
|
-
columns.push(
|
|
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,
|
|
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 '${
|
|
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,
|
|
857
|
+
this.collectSortTerms(ctx, join.meta, relationSort, opts, vectors, columns, keyPath, join.alias);
|
|
796
858
|
continue;
|
|
797
859
|
}
|
|
798
860
|
if (isVectorSearch(value)) {
|
|
@@ -801,25 +863,37 @@ 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
|
-
:
|
|
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(
|
|
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
|
/**
|
|
812
883
|
* The `ORDER BY` operand for one key. A key that is not a field of `meta` - a `raw()` projection, a
|
|
813
|
-
* `$
|
|
884
|
+
* `$select` alias - is an output alias, which is never table-qualified and needs no resolving.
|
|
814
885
|
*/
|
|
815
886
|
sortColumn(ctx, meta, key, prefix) {
|
|
816
887
|
const field = meta.fields[key];
|
|
817
888
|
if (field) {
|
|
818
|
-
|
|
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
|
|
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,
|
|
906
|
-
this.search(ctx, entity, { $where: q.$where },
|
|
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
|
/**
|
|
@@ -931,7 +1006,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
931
1006
|
// may name these and nothing else, so one map answers both "is this legal?" and "what do I
|
|
932
1007
|
// emit for it?".
|
|
933
1008
|
const emittedColumns = {};
|
|
934
|
-
for (const entry of parseGroupMap(q.$group, q.$
|
|
1009
|
+
for (const entry of parseGroupMap(q.$group, q.$select)) {
|
|
935
1010
|
if (entry.kind === 'key') {
|
|
936
1011
|
const field = meta.fields[entry.alias];
|
|
937
1012
|
const columnName = this.resolveColumnName(entry.alias, field);
|
|
@@ -949,7 +1024,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
949
1024
|
}
|
|
950
1025
|
}
|
|
951
1026
|
if (!selectParts.length) {
|
|
952
|
-
throw new TypeError('aggregate requires at least one $group column or $
|
|
1027
|
+
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
953
1028
|
}
|
|
954
1029
|
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${tableName}`);
|
|
955
1030
|
this.where(ctx, entity, q.$where, opts);
|
|
@@ -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
|
-
|
|
1031
|
-
this.
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
@@ -1324,7 +1419,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1324
1419
|
hydratableAggregates(entity, q) {
|
|
1325
1420
|
const { fields } = getMeta(entity);
|
|
1326
1421
|
const decoded = [];
|
|
1327
|
-
for (const entry of parseGroupMap(q.$group, q.$
|
|
1422
|
+
for (const entry of parseGroupMap(q.$group, q.$select)) {
|
|
1328
1423
|
if (entry.kind === 'fn' && entry.op !== '$min' && entry.op !== '$max') {
|
|
1329
1424
|
decoded.push([entry.alias, 'number']);
|
|
1330
1425
|
continue;
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1579
|
-
// Resolved before any SQL is emitted: it also decides whether the
|
|
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
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
-
|
|
1599
|
-
ctx.append(`
|
|
1600
|
-
|
|
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.
|
|
1607
|
-
ctx.append(
|
|
1608
|
-
this.
|
|
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
|