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.
Files changed (44) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  2. package/dist/browser/type/clientQuerier.d.ts +5 -5
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +4 -4
  5. package/dist/cockroachdb/cockroachDialect.js +2 -2
  6. package/dist/dialect/abstractDialect.d.ts +8 -2
  7. package/dist/dialect/abstractDialect.js +17 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +16 -5
  9. package/dist/dialect/abstractSqlDialect.js +82 -44
  10. package/dist/dialect/aliases.d.ts +10 -7
  11. package/dist/dialect/aliases.js +10 -7
  12. package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
  13. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  14. package/dist/dialect/queryJoins.d.ts +8 -1
  15. package/dist/dialect/queryJoins.js +33 -10
  16. package/dist/entity/decorator/members.d.ts +25 -11
  17. package/dist/entity/metadata/definition.d.ts +8 -3
  18. package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
  19. package/dist/migrate/migrator.d.ts +2 -1
  20. package/dist/migrate/migrator.js +5 -3
  21. package/dist/mongo/mongoDialect.d.ts +31 -27
  22. package/dist/mongo/mongoDialect.js +172 -112
  23. package/dist/mongo/mongodbQuerier.d.ts +0 -2
  24. package/dist/mongo/mongodbQuerier.js +12 -11
  25. package/dist/mssql/mssqlDialect.js +3 -2
  26. package/dist/postgres/postgresDialect.js +2 -2
  27. package/dist/querier/abstractQuerier.d.ts +16 -11
  28. package/dist/querier/abstractQuerier.js +28 -8
  29. package/dist/querier/abstractQuerierPool.d.ts +9 -9
  30. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  31. package/dist/querier/abstractSqlQuerier.js +3 -3
  32. package/dist/sqlite/sqliteDialect.js +1 -0
  33. package/dist/turso/tursoDialect.d.ts +1 -1
  34. package/dist/turso/tursoDialect.js +6 -2
  35. package/dist/type/dialect.d.ts +6 -0
  36. package/dist/type/entity.d.ts +31 -2
  37. package/dist/type/migration.d.ts +7 -0
  38. package/dist/type/query.d.ts +6 -0
  39. package/dist/type/queryAggregate.d.ts +77 -42
  40. package/dist/type/queryAggregate.js +4 -21
  41. package/dist/type/universalQuerier.d.ts +9 -9
  42. package/dist/util/dialect.util.d.ts +6 -2
  43. package/dist/util/dialect.util.js +19 -5
  44. 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, COUNT_ALIAS, COUNTED_ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
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(COUNT_ALIAS)] });
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(COUNT_ALIAS, true)} FROM (`);
939
+ ctx.append(`SELECT COUNT(*) ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM (`);
940
940
  rows();
941
- ctx.append(`) ${this.escapeId(COUNTED_ROWS_ALIAS, true)}`);
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 tableName = this.escapedTableName(meta);
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 parseGroupMap(q.$group, q.$select)) {
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
- const field = meta.fields[entry.alias];
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
- if (!selectParts.length) {
986
- throw new TypeError('aggregate requires at least one $group column or $select function');
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.aggregateHydrateKind(meta, field) ?? this.hydrateKind(field);
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 { fields } = getMeta(entity);
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
- // A grouped column is the field itself; an aggregate reads the one it aggregates, `'*'` for a tally.
1313
- const key = entry.kind === 'fn' ? entry.fieldRef : entry.alias;
1314
- const field = fields[key];
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, field) {
1331
- return op === '$count' || op === '$avg' ? 'number' : this.hydrateKind(field);
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 relation aggregate a field declares decodes as: {@link aggregateKind} over the target's column. */
1334
- aggregateHydrateKind(meta, field) {
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 undefined;
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
- if (!aggregate.query?.$limit && aggregate.query?.$skip === undefined) {
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
- /** One aggregate over an operand, `COALESCE`d where the aggregate answers `0` on no rows rather than null. */
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
- if (op === '$count') {
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
- /** The derived table a count wraps the rows it counts in: a page, or a `$distinct` set. MySQL requires the alias. */
6
- export declare const COUNTED_ROWS_ALIAS = "_uql_rows";
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
- * What an aggregate answers under: the column a capped page carries out for the aggregate wrapping it,
11
- * and the field a MongoDB `$group` or `$count` leaves its value in. One name, since both ends of each
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. */
@@ -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
- /** The derived table a count wraps the rows it counts in: a page, or a `$distinct` set. MySQL requires the alias. */
8
- export const COUNTED_ROWS_ALIAS = '_uql_rows';
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
- * What an aggregate answers under: the column a capped page carries out for the aggregate wrapping it,
13
- * and the field a MongoDB `$group` or `$count` leaves its value in. One name, since both ends of each
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 { COUNT_ALIAS } from './aliases.js';
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(COUNT_ALIAS, true)} FROM information_schema.TABLES WHERE TABLE_SCHEMA = `);
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
  }
@@ -29,6 +29,7 @@ export const PG_FEATURES = {
29
29
  stringSizing: 'bounded-text',
30
30
  supportsUnsigned: false,
31
31
  serverSideCursors: true,
32
+ correlatedWrites: true,
32
33
  rowLocks: true,
33
34
  rowLockWithWindow: false,
34
35
  rowLockOf: true,
@@ -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
- addSortJoins(joins, claimAlias, meta, q.$sort);
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
- function addSortJoins(joins, claimAlias, meta, sort, parent) {
75
- if (!sort) {
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(sort)) {
101
+ for (const key of getKeys(map)) {
79
102
  const relation = meta.relations[key];
80
- const value = sort[key];
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 `ORDER BY` is rendered - the one place that knows how to name it.
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 a `QuerySortMap<object>`, it lands on the nested relation's target.
88
- addSortJoins(joins, claimAlias, join.meta, value, join);
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 `$sort` map, as opposed to a direction or a vector search. */
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): MemberDecorator<DeclaredValue<O> | undefined, This>;
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): AggregateDecorator<AggregateValue<O>, This>;
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 { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterName, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, Type, WrittenId } from '../../type/index.js';
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: EntityData<E>): boolean;
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: EntityData<E>): WrittenId<E>;
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
- lines.push(` ${propertyName}${nullable}: ${tsType};`);
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, within a transaction where the dialect has one for it
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
  /**
@@ -112,14 +112,15 @@ export class Migrator {
112
112
  return results;
113
113
  }
114
114
  /**
115
- * Run a single migration, within a transaction where the dialect has one for it
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
- await transaction(async () => {
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 {