uql-orm 0.69.0 → 0.70.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/dist/browser/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/type/clientQuerier.d.ts +5 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.js +2 -2
- package/dist/dialect/abstractDialect.d.ts +8 -2
- package/dist/dialect/abstractDialect.js +17 -1
- package/dist/dialect/abstractSqlDialect.d.ts +16 -5
- package/dist/dialect/abstractSqlDialect.js +82 -44
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +10 -7
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/dialect/queryJoins.d.ts +8 -1
- package/dist/dialect/queryJoins.js +33 -10
- package/dist/entity/decorator/members.d.ts +25 -11
- package/dist/entity/metadata/definition.d.ts +8 -3
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
- package/dist/migrate/migrator.d.ts +2 -1
- package/dist/migrate/migrator.js +5 -3
- package/dist/mongo/mongoDialect.d.ts +31 -27
- package/dist/mongo/mongoDialect.js +172 -112
- package/dist/mongo/mongodbQuerier.d.ts +0 -2
- package/dist/mongo/mongodbQuerier.js +12 -11
- package/dist/mssql/mssqlDialect.js +3 -2
- package/dist/postgres/postgresDialect.js +2 -2
- package/dist/querier/abstractQuerier.d.ts +16 -11
- package/dist/querier/abstractQuerier.js +28 -8
- package/dist/querier/abstractQuerierPool.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +3 -3
- package/dist/sqlite/sqliteDialect.js +1 -0
- package/dist/turso/tursoDialect.d.ts +1 -1
- package/dist/turso/tursoDialect.js +6 -2
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +31 -2
- package/dist/type/migration.d.ts +7 -0
- package/dist/type/query.d.ts +6 -0
- package/dist/type/queryAggregate.d.ts +77 -42
- package/dist/type/queryAggregate.js +4 -21
- package/dist/type/universalQuerier.d.ts +9 -9
- package/dist/util/dialect.util.d.ts +6 -2
- package/dist/util/dialect.util.js +19 -5
- package/package.json +1 -1
|
@@ -3,10 +3,10 @@ import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEY
|
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
4
|
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
|
-
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS,
|
|
6
|
+
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
|
|
7
7
|
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
8
8
|
import { SqlQueryContext } from './queryContext.js';
|
|
9
|
-
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
9
|
+
import { groupPathField, NO_JOINS, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
10
10
|
import { resolveVectorCast } from './vectorCast.js';
|
|
11
11
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
12
12
|
/** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
|
|
@@ -916,7 +916,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
916
916
|
count(ctx, entity, q, opts) {
|
|
917
917
|
const { $where, $skip, $limit } = q;
|
|
918
918
|
if ($skip === undefined && $limit === undefined) {
|
|
919
|
-
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(
|
|
919
|
+
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(AGGREGATE_VALUE_ALIAS)] });
|
|
920
920
|
this.search(ctx, entity, { $where }, opts);
|
|
921
921
|
return;
|
|
922
922
|
}
|
|
@@ -936,9 +936,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
936
936
|
}
|
|
937
937
|
/** `SELECT COUNT(*)` over the rows `rows` appends, as a derived table. */
|
|
938
938
|
countRows(ctx, rows) {
|
|
939
|
-
ctx.append(`SELECT COUNT(*) ${this.escapeId(
|
|
939
|
+
ctx.append(`SELECT COUNT(*) ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM (`);
|
|
940
940
|
rows();
|
|
941
|
-
ctx.append(`) ${this.escapeId(
|
|
941
|
+
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
942
942
|
}
|
|
943
943
|
/**
|
|
944
944
|
* The statistic the engine already keeps, as a `count` column. Overridden by the dialects that
|
|
@@ -958,35 +958,40 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
958
958
|
};
|
|
959
959
|
aggregate(ctx, entity, q, opts = {}) {
|
|
960
960
|
const meta = getMeta(entity);
|
|
961
|
-
const
|
|
961
|
+
const entries = parseGroupMap(q.$group, q.$select);
|
|
962
|
+
if (!entries.length) {
|
|
963
|
+
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
964
|
+
}
|
|
965
|
+
const table = this.tableRef(meta, this.readOptions(ctx, meta).alias);
|
|
966
|
+
const joins = resolveGroupJoins(meta, q.$group, (path) => ctx.claimAlias(path));
|
|
967
|
+
const prefix = joins.size ? table.alias : undefined;
|
|
968
|
+
const reads = entries.map((entry) => ({ entry, value: this.aggregateValue(ctx, entity, entry, joins, prefix) }));
|
|
969
|
+
// Only bare columns are read inline: SQL Server refuses a subquery inside an aggregate or a
|
|
970
|
+
// `GROUP BY`, and a filter's bound values would repeat in `HAVING`. A derived table answers by alias.
|
|
971
|
+
const derived = reads.some(({ value }) => !value.bare);
|
|
972
|
+
const named = (sql, alias) => sql === this.escapeId(alias) ? sql : `${sql} ${this.escapeId(alias)}`;
|
|
962
973
|
const groupKeys = [];
|
|
963
974
|
const selectParts = [];
|
|
964
975
|
// Every column the statement emits, mapped to the SQL that references it. `$having` and `$sort`
|
|
965
976
|
// may name these and nothing else, so one map answers both "is this legal?" and "what do I
|
|
966
977
|
// emit for it?".
|
|
967
978
|
const emittedColumns = {};
|
|
968
|
-
for (const entry of
|
|
979
|
+
for (const { entry, value } of reads) {
|
|
980
|
+
const column = derived ? this.escapeId(entry.alias) : value.sql;
|
|
981
|
+
const expr = entry.kind === 'key' ? column : this.aggregateFn(entry.op, value.sql === '*' ? '*' : column, entry.distinct);
|
|
969
982
|
if (entry.kind === 'key') {
|
|
970
|
-
|
|
971
|
-
const columnName = this.resolveColumnName(entry.alias, field);
|
|
972
|
-
const escaped = this.escapeId(columnName);
|
|
973
|
-
groupKeys.push(escaped);
|
|
974
|
-
emittedColumns[entry.alias] = escaped;
|
|
975
|
-
selectParts.push(columnName !== entry.alias ? `${escaped} ${this.escapeId(entry.alias)}` : escaped);
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
const sqlFn = AbstractSqlDialect.AGGREGATE_FN[entry.op];
|
|
979
|
-
const sqlArg = entry.fieldRef === '*' ? '*' : this.escapeId(this.columnOf(meta, entry.fieldRef));
|
|
980
|
-
const expr = `${sqlFn}(${entry.distinct ? 'DISTINCT ' : ''}${sqlArg})`;
|
|
981
|
-
emittedColumns[entry.alias] = expr;
|
|
982
|
-
selectParts.push(`${expr} ${this.escapeId(entry.alias)}`);
|
|
983
|
+
groupKeys.push(expr);
|
|
983
984
|
}
|
|
985
|
+
emittedColumns[entry.alias] = expr;
|
|
986
|
+
selectParts.push(named(expr, entry.alias));
|
|
984
987
|
}
|
|
985
|
-
|
|
986
|
-
|
|
988
|
+
const columns = reads.flatMap(({ entry, value }) => (value.sql === '*' ? [] : [named(value.sql, entry.alias)]));
|
|
989
|
+
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${derived ? `(SELECT ${columns.join(', ')} FROM ` : ''}${table.ref}`);
|
|
990
|
+
this.selectRelationJoins(ctx, meta, table.alias, joins);
|
|
991
|
+
this.where(ctx, entity, q.$where, { ...opts, prefix });
|
|
992
|
+
if (derived) {
|
|
993
|
+
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
987
994
|
}
|
|
988
|
-
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${tableName}`);
|
|
989
|
-
this.where(ctx, entity, q.$where, opts);
|
|
990
995
|
if (groupKeys.length) {
|
|
991
996
|
ctx.append(` GROUP BY ${groupKeys.join(', ')}`);
|
|
992
997
|
}
|
|
@@ -996,6 +1001,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
996
1001
|
const sorted = this.aggregateSort(ctx, q.$sort, emittedColumns);
|
|
997
1002
|
this.pager(ctx, q, sorted);
|
|
998
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* What one entry reads: a grouped field, through the join its path passes, or an aggregate's argument,
|
|
1006
|
+
* narrowed by its own `$where` to `CASE WHEN … THEN … END`. Bare where it is a column or `'*'`.
|
|
1007
|
+
*/
|
|
1008
|
+
aggregateValue(ctx, entity, entry, joins, prefix) {
|
|
1009
|
+
if (entry.kind === 'key') {
|
|
1010
|
+
const { key, join } = groupPathField(joins, entry.path);
|
|
1011
|
+
return join
|
|
1012
|
+
? this.aggregateOperand(ctx, join.entity, key, join.alias)
|
|
1013
|
+
: this.aggregateOperand(ctx, entity, key, prefix);
|
|
1014
|
+
}
|
|
1015
|
+
const arg = entry.fieldRef === '*' ? undefined : this.aggregateOperand(ctx, entity, entry.fieldRef, prefix);
|
|
1016
|
+
const { where } = entry;
|
|
1017
|
+
const condition = where &&
|
|
1018
|
+
this.buildFragment(ctx, (fragment) => this.renderWhere(fragment, entity, where, { clause: false, prefix }));
|
|
1019
|
+
if (!condition) {
|
|
1020
|
+
return arg ?? { sql: '*', bare: true };
|
|
1021
|
+
}
|
|
1022
|
+
return { sql: `CASE WHEN ${condition} THEN ${arg?.sql ?? '1'} END`, bare: false };
|
|
1023
|
+
}
|
|
1024
|
+
/** A field as an aggregate reads it: its column, or the expression an inlined one stands for. */
|
|
1025
|
+
aggregateOperand(ctx, entity, key, prefix) {
|
|
1026
|
+
const field = getMeta(entity).fields[key];
|
|
1027
|
+
return {
|
|
1028
|
+
sql: this.resolveOperandField(ctx, entity, key, { prefix }),
|
|
1029
|
+
bare: field === undefined || !isInlinedExpression(field),
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
999
1032
|
/**
|
|
1000
1033
|
* ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
|
|
1001
1034
|
* statement has no joins to address, so a relation key is rejected rather than emitted as an alias
|
|
@@ -1296,7 +1329,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1296
1329
|
}
|
|
1297
1330
|
const decoded = [];
|
|
1298
1331
|
for (const [key, field] of Object.entries(meta.fields)) {
|
|
1299
|
-
const kind = this.
|
|
1332
|
+
const kind = this.fieldKind(meta, field);
|
|
1300
1333
|
if (kind) {
|
|
1301
1334
|
decoded.push([key, kind]);
|
|
1302
1335
|
}
|
|
@@ -1306,13 +1339,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1306
1339
|
}
|
|
1307
1340
|
/** The same for an aggregate's row, per query: each alias decodes by {@link aggregateKind}. */
|
|
1308
1341
|
hydratableAggregates(entity, q) {
|
|
1309
|
-
const
|
|
1342
|
+
const meta = getMeta(entity);
|
|
1343
|
+
const joins = resolveGroupJoins(meta, q.$group);
|
|
1310
1344
|
const decoded = [];
|
|
1311
1345
|
for (const entry of parseGroupMap(q.$group, q.$select)) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
const kind = entry.kind === 'fn' ? this.aggregateKind(entry.op, field) : this.hydrateKind(field);
|
|
1346
|
+
const kind = entry.kind === 'fn'
|
|
1347
|
+
? this.aggregateKind(entry.op, this.fieldKind(meta, meta.fields[entry.fieldRef]))
|
|
1348
|
+
: this.groupedKind(meta, joins, entry.path);
|
|
1316
1349
|
if (kind) {
|
|
1317
1350
|
decoded.push([entry.alias, kind]);
|
|
1318
1351
|
}
|
|
@@ -1327,17 +1360,22 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1327
1360
|
*
|
|
1328
1361
|
* One rule for both readers: a relation aggregate a field declares, and an aggregate a query names.
|
|
1329
1362
|
*/
|
|
1330
|
-
aggregateKind(op,
|
|
1331
|
-
return op === '$count' || op === '$avg' ? 'number' :
|
|
1363
|
+
aggregateKind(op, fieldKind) {
|
|
1364
|
+
return op === '$count' || op === '$avg' ? 'number' : fieldKind;
|
|
1365
|
+
}
|
|
1366
|
+
/** What a grouped path decodes as: the kind of the field it reaches, through its join or on the entity. */
|
|
1367
|
+
groupedKind(meta, joins, path) {
|
|
1368
|
+
const { key, join } = groupPathField(joins, path);
|
|
1369
|
+
return join ? this.fieldKind(join.meta, join.meta.fields[key]) : this.fieldKind(meta, meta.fields[key]);
|
|
1332
1370
|
}
|
|
1333
|
-
/** What a
|
|
1334
|
-
|
|
1371
|
+
/** What a field decodes as: its column's kind, or a relation aggregate's {@link aggregateKind} over the target's column. */
|
|
1372
|
+
fieldKind(meta, field) {
|
|
1335
1373
|
const spec = aggregateOf(field);
|
|
1336
1374
|
if (!spec) {
|
|
1337
|
-
return
|
|
1375
|
+
return this.hydrateKind(field);
|
|
1338
1376
|
}
|
|
1339
1377
|
const target = getMeta(relationOf(meta, spec.relation).entity());
|
|
1340
|
-
return this.aggregateKind(spec.op, spec.field ? target.fields[spec.field] : undefined);
|
|
1378
|
+
return this.aggregateKind(spec.op, spec.field ? this.fieldKind(target, target.fields[spec.field]) : undefined);
|
|
1341
1379
|
}
|
|
1342
1380
|
/** What one column decodes as, the inverse of {@link persistKind}. `BigInt` first, since it shares the numeric family. */
|
|
1343
1381
|
hydrateKind(field) {
|
|
@@ -1395,8 +1433,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1395
1433
|
* `$pull` reads the column and every later one its expression once, so no value binds twice.
|
|
1396
1434
|
*/
|
|
1397
1435
|
formatJsonUpdate(ctx, escapedCol, value, field) {
|
|
1398
|
-
// Centralizes the one narrowing cast: the payload's keys are typed against the entity's JSON
|
|
1399
|
-
// payload, which the dialects do not need - they only build SQL from keys and values.
|
|
1400
1436
|
const { $pull, $set, $push, $unset } = value;
|
|
1401
1437
|
let expr = escapedCol;
|
|
1402
1438
|
if (hasKeys($pull)) {
|
|
@@ -1584,7 +1620,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1584
1620
|
const meta = getMeta(entity);
|
|
1585
1621
|
const rel = relationOf(meta, aggregate.relation);
|
|
1586
1622
|
const parent = prefix || this.resolveTableAlias(meta);
|
|
1587
|
-
|
|
1623
|
+
const { $limit, $skip } = aggregate.query ?? {};
|
|
1624
|
+
if ($limit === undefined && $skip === undefined) {
|
|
1588
1625
|
this.appendRelationSubquery(ctx, meta, aggregate.relation, rel, { prefix: parent }, aggregate);
|
|
1589
1626
|
return;
|
|
1590
1627
|
}
|
|
@@ -1616,14 +1653,15 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1616
1653
|
const joins = resolveQueryJoins(getMeta(entity), query, (path) => ctx.claimAlias(path));
|
|
1617
1654
|
this.read(ctx, entity, query, { alias }, joins);
|
|
1618
1655
|
}
|
|
1619
|
-
/**
|
|
1656
|
+
/** A relation aggregate over an operand, reading `0` on a parent with no rows where its type says so. */
|
|
1620
1657
|
aggregateCall(op, operand) {
|
|
1621
|
-
|
|
1622
|
-
return 'COUNT(*)';
|
|
1623
|
-
}
|
|
1624
|
-
const call = `${AbstractSqlDialect.AGGREGATE_FN[op]}(${operand})`;
|
|
1658
|
+
const call = this.aggregateFn(op, op === '$count' ? '*' : operand);
|
|
1625
1659
|
return op === '$sum' ? `COALESCE(${call}, 0)` : call;
|
|
1626
1660
|
}
|
|
1661
|
+
/** One aggregate function call, the one spelling every statement that aggregates writes. */
|
|
1662
|
+
aggregateFn(op, operand, distinct) {
|
|
1663
|
+
return `${AbstractSqlDialect.AGGREGATE_FN[op]}(${distinct ? 'DISTINCT ' : ''}${operand})`;
|
|
1664
|
+
}
|
|
1627
1665
|
/**
|
|
1628
1666
|
* One equality per key of the parent, anded: a composite correlates on every column, and matching on
|
|
1629
1667
|
* part of one would find the rows of a different parent. `parentJoins` keeps the two ends the right
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
2
|
-
export declare const COUNT_ALIAS = "_uql_count";
|
|
3
1
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
4
2
|
export declare const TOTAL_ALIAS = "_uql_total";
|
|
5
|
-
/**
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The derived table a statement wraps the rows it counts or aggregates in: a page, a `$distinct` set, or
|
|
5
|
+
* rows computing an inlined field an aggregate names. MySQL requires the alias.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ROWS_ALIAS = "_uql_rows";
|
|
7
8
|
/** The derived table a capped relation aggregate reads: the page is taken first, then aggregated over. */
|
|
8
9
|
export declare const AGGREGATE_PAGE_ALIAS = "_uql_page";
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
* and the field a MongoDB `$group` or `$count`
|
|
12
|
-
* are written and read here.
|
|
11
|
+
* The one value a statement that aggregates answers under: a `COUNT(*)` or an estimate, the column a
|
|
12
|
+
* capped page carries out for the aggregate wrapping it, and the field a MongoDB `$group` or `$count`
|
|
13
|
+
* leaves its result in. One name, since both ends of each are written and read here.
|
|
13
14
|
*/
|
|
14
15
|
export declare const AGGREGATE_VALUE_ALIAS = "_uql_value";
|
|
16
|
+
/** Beside each MongoDB `$sum`, how many values it read: `$sum` answers 0 over none, where SQL answers null. */
|
|
17
|
+
export declare const SUM_COUNT_ALIAS = "_uql_count";
|
|
15
18
|
/** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
|
|
16
19
|
export declare const RELATION_ROW_ALIAS = "_uql_row";
|
|
17
20
|
/** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
// Every identifier UQL invents, `_uql`-prefixed to stay off a user's own, collected in one place:
|
|
2
2
|
// the ends writing and reading one sit in different modules, and a drift between them fails silently.
|
|
3
|
-
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
4
|
-
export const COUNT_ALIAS = '_uql_count';
|
|
5
3
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
6
4
|
export const TOTAL_ALIAS = '_uql_total';
|
|
7
|
-
/**
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The derived table a statement wraps the rows it counts or aggregates in: a page, a `$distinct` set, or
|
|
7
|
+
* rows computing an inlined field an aggregate names. MySQL requires the alias.
|
|
8
|
+
*/
|
|
9
|
+
export const ROWS_ALIAS = '_uql_rows';
|
|
9
10
|
/** The derived table a capped relation aggregate reads: the page is taken first, then aggregated over. */
|
|
10
11
|
export const AGGREGATE_PAGE_ALIAS = '_uql_page';
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
* and the field a MongoDB `$group` or `$count`
|
|
14
|
-
* are written and read here.
|
|
13
|
+
* The one value a statement that aggregates answers under: a `COUNT(*)` or an estimate, the column a
|
|
14
|
+
* capped page carries out for the aggregate wrapping it, and the field a MongoDB `$group` or `$count`
|
|
15
|
+
* leaves its result in. One name, since both ends of each are written and read here.
|
|
15
16
|
*/
|
|
16
17
|
export const AGGREGATE_VALUE_ALIAS = '_uql_value';
|
|
18
|
+
/** Beside each MongoDB `$sum`, how many values it read: `$sum` answers 0 over none, where SQL answers null. */
|
|
19
|
+
export const SUM_COUNT_ALIAS = '_uql_count';
|
|
17
20
|
/** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
|
|
18
21
|
export const RELATION_ROW_ALIAS = '_uql_row';
|
|
19
22
|
/** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
|
|
@@ -2,7 +2,7 @@ import { getMeta } from '../entity/index.js';
|
|
|
2
2
|
import { textSearchFields } from '../util/index.js';
|
|
3
3
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect, } from './abstractSqlDialect.js';
|
|
5
|
-
import {
|
|
5
|
+
import { AGGREGATE_VALUE_ALIAS } from './aliases.js';
|
|
6
6
|
import { BYTES_PREFIX } from './hydrateColumn.js';
|
|
7
7
|
import { jsonSetCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
|
|
8
8
|
import { aggregatesRelations } from './queryJoins.js';
|
|
@@ -27,6 +27,7 @@ export const MYSQL_FEATURES = {
|
|
|
27
27
|
stringSizing: 'varchar',
|
|
28
28
|
supportsUnsigned: true,
|
|
29
29
|
serverSideCursors: false,
|
|
30
|
+
correlatedWrites: true,
|
|
30
31
|
rowLocks: true,
|
|
31
32
|
rowLockWithWindow: true,
|
|
32
33
|
rowLockOf: true,
|
|
@@ -49,7 +50,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
49
50
|
estimatedCount(ctx, entity) {
|
|
50
51
|
const meta = getMeta(entity);
|
|
51
52
|
const schema = this.resolveSchema(meta);
|
|
52
|
-
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(
|
|
53
|
+
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM information_schema.TABLES WHERE TABLE_SCHEMA = `);
|
|
53
54
|
if (schema) {
|
|
54
55
|
ctx.addValue(schema);
|
|
55
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Query, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
|
|
4
4
|
* `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
|
|
@@ -39,6 +39,13 @@ export type QuerySortOptions = {
|
|
|
39
39
|
* columns, the `ORDER BY` and the lock agree. `claimAlias` names each join's table, parents first.
|
|
40
40
|
*/
|
|
41
41
|
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
|
|
42
|
+
/** What an aggregate joins: each to-one relation a `$group` path passes through. */
|
|
43
|
+
export declare function resolveGroupJoins<E>(meta: EntityMeta<E>, group: QueryGroupMap<E> | undefined, claimAlias?: (path: string) => string): QueryJoins;
|
|
44
|
+
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
45
|
+
export declare function groupPathField(joins: QueryJoins, path: readonly string[]): {
|
|
46
|
+
readonly key: string;
|
|
47
|
+
readonly join: QueryJoin | undefined;
|
|
48
|
+
};
|
|
42
49
|
/**
|
|
43
50
|
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
44
51
|
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMeta, relationOf } from '../entity/index.js';
|
|
2
|
-
import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
2
|
+
import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
3
3
|
export const NO_JOINS = new Map();
|
|
4
4
|
/**
|
|
5
5
|
* What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
|
|
@@ -11,9 +11,31 @@ export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
|
|
|
11
11
|
}
|
|
12
12
|
const joins = new Map();
|
|
13
13
|
addPopulateJoins(joins, claimAlias, meta, q.$populate);
|
|
14
|
-
|
|
14
|
+
addPathJoins(joins, claimAlias, meta, q.$sort);
|
|
15
15
|
return joins;
|
|
16
16
|
}
|
|
17
|
+
/** What an aggregate joins: each to-one relation a `$group` path passes through. */
|
|
18
|
+
export function resolveGroupJoins(meta, group, claimAlias = (path) => path) {
|
|
19
|
+
const joins = new Map();
|
|
20
|
+
for (const ref of Object.values(group ?? {})) {
|
|
21
|
+
if (isRecord(ref)) {
|
|
22
|
+
addPathJoins(joins, claimAlias, meta, ref);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return joins;
|
|
26
|
+
}
|
|
27
|
+
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
28
|
+
export function groupPathField(joins, path) {
|
|
29
|
+
const key = path[path.length - 1];
|
|
30
|
+
if (path.length === 1) {
|
|
31
|
+
return { key, join: undefined };
|
|
32
|
+
}
|
|
33
|
+
const join = joins.get(path.slice(0, -1).join('.'));
|
|
34
|
+
if (!join) {
|
|
35
|
+
throw new TypeError(`cannot $group by '${path.join('.')}': only a to-one relation's field groups, since a to-many multiplies the rows it joins`);
|
|
36
|
+
}
|
|
37
|
+
return { key, join };
|
|
38
|
+
}
|
|
17
39
|
/**
|
|
18
40
|
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
19
41
|
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
@@ -71,21 +93,22 @@ function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
|
|
|
71
93
|
addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
/** The to-one relations a nested map of fields passes through, a `$sort` or a `$group` path, as joins adding no columns. */
|
|
97
|
+
function addPathJoins(joins, claimAlias, meta, map, parent) {
|
|
98
|
+
if (!map) {
|
|
76
99
|
return;
|
|
77
100
|
}
|
|
78
|
-
for (const key of getKeys(
|
|
101
|
+
for (const key of getKeys(map)) {
|
|
79
102
|
const relation = meta.relations[key];
|
|
80
|
-
const value =
|
|
103
|
+
const value = map[key];
|
|
81
104
|
// A to-many, or a value that is not a map of the relation's own fields, cannot be joined and is
|
|
82
|
-
// reported where the
|
|
105
|
+
// reported where the statement names it - the one place that knows how to.
|
|
83
106
|
if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
|
|
84
107
|
continue;
|
|
85
108
|
}
|
|
86
109
|
const join = addJoin(joins, claimAlias, parent, key, relation, {}, false, false);
|
|
87
|
-
// `E` stated: inferred from
|
|
88
|
-
|
|
110
|
+
// `E` stated: inferred from the nested map, it lands on the nested relation's target.
|
|
111
|
+
addPathJoins(joins, claimAlias, join.meta, value, join);
|
|
89
112
|
}
|
|
90
113
|
}
|
|
91
114
|
/** The join a sort may address at `path` with the relation's own sort map, or why it may not; `unjoinable` is the dialect's remedy. */
|
|
@@ -102,7 +125,7 @@ export function resolveSortableJoin(relation, path, value, joins, unjoinable) {
|
|
|
102
125
|
}
|
|
103
126
|
return { join, sort: value };
|
|
104
127
|
}
|
|
105
|
-
/** A nested
|
|
128
|
+
/** A nested map of fields, as opposed to a `$sort` direction or vector search, or a `$group` field's `true`. */
|
|
106
129
|
function isSortMap(value) {
|
|
107
130
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && !('$vector' in value);
|
|
108
131
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import type { AggregateValue, ComputedRefs, EntityAggregate, EntityGetter, Except, FieldOptions, FieldType, HasCompositeKey, IdValue, NamedIdKey, RejectKeys, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, RelationAggregate, TsTypeOf } from '../../type/index.js';
|
|
1
|
+
import type { AggregateValue, ComputedRefs, EntityAggregate, EntityGetter, Except, FieldOptions, FieldType, HasCompositeKey, IdValue, NamedIdKey, RejectKeys, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, RelationAggregate, TsTypeOf, Writable } from '../../type/index.js';
|
|
2
2
|
import type { RejectIncompatible } from '../../util/index.js';
|
|
3
3
|
/** A member decorator that also constrains the property it may be applied to, on a class `O`. */
|
|
4
4
|
type MemberDecorator<V, O = unknown> = (value: undefined, context: ClassFieldDecoratorContext<O, V>) => void;
|
|
5
|
+
/**
|
|
6
|
+
* A {@link MemberDecorator} whose property has to admit `M` as well as hold `V`. A decorator context is
|
|
7
|
+
* covariant in its value, so on its own it takes a property narrower than the field reads: a `string`
|
|
8
|
+
* where a nullable column reads `string | null`, a `number` where `max()` reads `number | null`.
|
|
9
|
+
*
|
|
10
|
+
* `M` is what each arm insists on rather than the whole of `V`, because a column type that names a
|
|
11
|
+
* family - `jsonb`, whose document the property shapes, or `numeric`, either number kind - is meant to
|
|
12
|
+
* be narrowed. A column insists on its `null`; an aggregate, whose value is exact, insists on all of it.
|
|
13
|
+
*/
|
|
14
|
+
type AdmittingDecorator<V, M, O> = <P extends V | undefined>(value: undefined, context: ClassFieldDecoratorContext<O, P> & ([Writable<M>] extends [Writable<P>] ? unknown : {
|
|
15
|
+
readonly __propertyMustAdmit: M;
|
|
16
|
+
})) => void;
|
|
5
17
|
/**
|
|
6
18
|
* The property type a set of field options describes: the declared `type`, narrowed by `enum` to the
|
|
7
19
|
* values that type admits (so `enum: [2]` stays off a `String`), or else the referenced key's own type,
|
|
@@ -16,6 +28,16 @@ type DeclaredValue<O> = O extends {
|
|
|
16
28
|
} ? HasCompositeKey<E> extends true ? {
|
|
17
29
|
readonly __compositeKeyNeedsAColumnPerKey: true;
|
|
18
30
|
} : IdValue<E> : never;
|
|
31
|
+
/**
|
|
32
|
+
* The `null` a column reads back: every one holds it unless `nullable: false` says otherwise, so the
|
|
33
|
+
* property admits it too. A key holds none, whether `@Id` or `@Field({ isId: true })` declares it, since
|
|
34
|
+
* it is NOT NULL on every engine.
|
|
35
|
+
*/
|
|
36
|
+
type NullOf<O> = O extends {
|
|
37
|
+
readonly nullable: false;
|
|
38
|
+
} | {
|
|
39
|
+
readonly isId: true;
|
|
40
|
+
} ? never : null;
|
|
19
41
|
/** The enum's members, or a named complaint where they widened for lack of `as const`, which would check nothing. */
|
|
20
42
|
type EnumValue<Members, Declared> = Declared extends Members ? {
|
|
21
43
|
readonly __enumNeedsAsConst: true;
|
|
@@ -29,14 +51,14 @@ export declare function Field<This, O extends FieldOptions<DeclaredValue<O>, Thi
|
|
|
29
51
|
type: FieldType;
|
|
30
52
|
} | {
|
|
31
53
|
references: EntityGetter;
|
|
32
|
-
}) & RejectKeys<Exclude<keyof O, keyof FieldOptions>> & RejectIncompatible<O>>(opts: O):
|
|
54
|
+
}) & RejectKeys<Exclude<keyof O, keyof FieldOptions>> & RejectIncompatible<O>>(opts: O): AdmittingDecorator<DeclaredValue<O> | NullOf<O>, NullOf<O>, This>;
|
|
33
55
|
/**
|
|
34
56
|
* Declares a field a relation aggregate computes, `@Field({ computed: (user) => user.resources.count() })`.
|
|
35
57
|
* The aggregate says what the field holds, so it takes no `type`, and only `count` and `sum` - the two a
|
|
36
58
|
* row change turns into a delta - may be `stored`.
|
|
37
59
|
* @example `@Field({ computed: (user) => user.resources.count() }) readonly resourceCount?: number;`
|
|
38
60
|
*/
|
|
39
|
-
export declare function Field<This, O extends AggregateOptions<This> & RejectKeys<Exclude<keyof O, keyof FieldOptions>>>(opts: O):
|
|
61
|
+
export declare function Field<This, O extends AggregateOptions<This> & RejectKeys<Exclude<keyof O, keyof FieldOptions>>>(opts: O): AdmittingDecorator<AggregateValue<O>, AggregateValue<O>, This>;
|
|
40
62
|
/**
|
|
41
63
|
* A field the aggregate itself types: `stored: true` only where a trigger could keep it, and every other
|
|
42
64
|
* option as a column takes it.
|
|
@@ -50,14 +72,6 @@ type AggregateOptions<E> = (Except<FieldOptions<never, E>, 'computed' | 'stored'
|
|
|
50
72
|
}['agg'];
|
|
51
73
|
readonly stored: true;
|
|
52
74
|
});
|
|
53
|
-
/**
|
|
54
|
-
* {@link MemberDecorator} for a field an aggregate types, which the property has to hold *exactly*: a
|
|
55
|
-
* decorator context takes a property narrower than its value, so nothing else would stop a `number`
|
|
56
|
-
* from holding what `max()` reads, which is `number | null` on a parent with no rows.
|
|
57
|
-
*/
|
|
58
|
-
type AggregateDecorator<V, O> = <P extends V | undefined>(value: undefined, context: ClassFieldDecoratorContext<O, P> & ([V] extends [P] ? unknown : {
|
|
59
|
-
readonly __propertyMustAdmit: V;
|
|
60
|
-
})) => void;
|
|
61
75
|
/**
|
|
62
76
|
* A key the type level cannot name, reported on each `@Id` that leaves it unnamed. Where no `idKey`
|
|
63
77
|
* brand and no conventional name applies, `IdKey` falls back to every field, and `IdValue`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterName, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, Type, WrittenId } from '../../type/index.js';
|
|
2
2
|
export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
|
|
3
3
|
export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
|
|
4
4
|
/** `T` is the relation's target, independent of the owner `E`. */
|
|
@@ -39,10 +39,14 @@ export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>
|
|
|
39
39
|
export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
|
|
40
40
|
/** The relation `key` names, for a caller that took `key` from the metadata itself. */
|
|
41
41
|
export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
|
|
42
|
+
/** A row of `E` as far as reading its key goes: a record or a write, whatever its values. */
|
|
43
|
+
type KeyedRow<E> = {
|
|
44
|
+
readonly [K in keyof E]?: unknown;
|
|
45
|
+
};
|
|
42
46
|
/** Whether the row names every column of its primary key, `0` and `''` included. */
|
|
43
|
-
export declare function namesKey<E>(meta: EntityMeta<E>, row:
|
|
47
|
+
export declare function namesKey<E>(meta: EntityMeta<E>, row: KeyedRow<E>): boolean;
|
|
44
48
|
/** A row's primary key: its value, or a map of every column on a composite, checked at run time. */
|
|
45
|
-
export declare function idOf<E>(meta: EntityMeta<E>, row:
|
|
49
|
+
export declare function idOf<E>(meta: EntityMeta<E>, row: KeyedRow<E>): WrittenId<E>;
|
|
46
50
|
/**
|
|
47
51
|
* Forgets an entity, and reports whether there was one - for a registry that grows at runtime, where a
|
|
48
52
|
* deleted content type would otherwise keep its metadata for the life of the process. Nothing rewrites
|
|
@@ -58,3 +62,4 @@ export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
|
|
|
58
62
|
* schema build constrains and a junction joins by, settling the relations holding them first.
|
|
59
63
|
*/
|
|
60
64
|
export declare function foreignKeysOf<E>(meta: EntityMeta<E>): RelationMeta[];
|
|
65
|
+
export {};
|
|
@@ -156,9 +156,11 @@ export class EntityCodeGenerator {
|
|
|
156
156
|
const fieldOptions = this.buildFieldOptions(col, propertyName);
|
|
157
157
|
lines.push(` @Field(${fieldOptions})`);
|
|
158
158
|
}
|
|
159
|
-
// Property
|
|
159
|
+
// Property. A generated column is the database's to write, so it is `readonly`: a write payload
|
|
160
|
+
// leaves those out, and one naming it would be dropped rather than persisted.
|
|
160
161
|
const nullable = col.nullable ? '?' : '';
|
|
161
|
-
|
|
162
|
+
const written = col.generatedAs === undefined ? '' : 'readonly ';
|
|
163
|
+
lines.push(` ${written}${propertyName}${nullable}: ${tsType};`);
|
|
162
164
|
return lines.join('\n');
|
|
163
165
|
}
|
|
164
166
|
/**
|
|
@@ -49,7 +49,8 @@ export declare class Migrator {
|
|
|
49
49
|
/** Runs the list narrowed by `to`/`step`, stopping at the first failure: `up` over the pending, `down` over the executed reversed. */
|
|
50
50
|
private runInOrder;
|
|
51
51
|
/**
|
|
52
|
-
* Run a single migration,
|
|
52
|
+
* Run a single migration, in a transaction where the dialect has one for it and the migration has not
|
|
53
|
+
* declared `transaction: false` - the opt-out a statement an engine refuses inside one needs.
|
|
53
54
|
*/
|
|
54
55
|
runMigration(migration: Migration<Querier>, direction: 'up' | 'down'): Promise<MigrationResult>;
|
|
55
56
|
/**
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -112,14 +112,15 @@ export class Migrator {
|
|
|
112
112
|
return results;
|
|
113
113
|
}
|
|
114
114
|
/**
|
|
115
|
-
* Run a single migration,
|
|
115
|
+
* Run a single migration, in a transaction where the dialect has one for it and the migration has not
|
|
116
|
+
* declared `transaction: false` - the opt-out a statement an engine refuses inside one needs.
|
|
116
117
|
*/
|
|
117
118
|
async runMigration(migration, direction) {
|
|
118
119
|
const startTime = Date.now();
|
|
119
120
|
return this.target.withSession(async ({ querier, transaction }) => {
|
|
120
121
|
try {
|
|
121
122
|
this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
|
|
122
|
-
|
|
123
|
+
const work = async () => {
|
|
123
124
|
if (direction === 'up') {
|
|
124
125
|
await migration.up(querier);
|
|
125
126
|
await this.storage.logWithQuerier(querier, migration.name);
|
|
@@ -128,7 +129,8 @@ export class Migrator {
|
|
|
128
129
|
await migration.down(querier);
|
|
129
130
|
await this.storage.unlogWithQuerier(querier, migration.name);
|
|
130
131
|
}
|
|
131
|
-
}
|
|
132
|
+
};
|
|
133
|
+
await (migration.transaction === false ? work() : transaction(work));
|
|
132
134
|
const duration = Date.now() - startTime;
|
|
133
135
|
this.logger.logMigration(`Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`);
|
|
134
136
|
return {
|