uql-orm 0.45.1 → 0.47.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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/bunSql/bunSqlQuerier.d.ts +7 -0
  3. package/dist/bunSql/bunSqlQuerier.js +7 -0
  4. package/dist/bunSql/bunSqlQuerierPool.d.ts +1 -0
  5. package/dist/bunSql/bunSqlQuerierPool.js +6 -1
  6. package/dist/bunSql/index.d.ts +1 -0
  7. package/dist/bunSql/index.js +1 -0
  8. package/dist/dialect/abstractSqlDialect.d.ts +22 -2
  9. package/dist/dialect/abstractSqlDialect.js +55 -18
  10. package/dist/dialect/aliases.d.ts +4 -0
  11. package/dist/dialect/aliases.js +4 -0
  12. package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
  13. package/dist/dialect/pgLikeSqlDialect.d.ts +14 -0
  14. package/dist/dialect/pgLikeSqlDialect.js +42 -2
  15. package/dist/entity/metadata/definition.js +8 -1
  16. package/dist/migrate/builder/columnBuilder.d.ts +14 -0
  17. package/dist/migrate/builder/columnBuilder.js +28 -9
  18. package/dist/migrate/builder/tableBuilder.js +8 -19
  19. package/dist/migrate/builder/types.d.ts +16 -33
  20. package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
  21. package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
  22. package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
  23. package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
  24. package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
  25. package/dist/migrate/codegen/sourceLiteral.js +17 -0
  26. package/dist/migrate/generator/definitionToNode.d.ts +21 -0
  27. package/dist/migrate/generator/definitionToNode.js +47 -25
  28. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
  29. package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
  30. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  31. package/dist/migrate/introspection/mongoIntrospector.js +2 -2
  32. package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
  33. package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
  34. package/dist/migrate/schemaGenerator.d.ts +48 -2
  35. package/dist/migrate/schemaGenerator.js +109 -36
  36. package/dist/mongo/mongoDialect.js +2 -1
  37. package/dist/mongo/mongodbQuerier.d.ts +16 -0
  38. package/dist/mongo/mongodbQuerier.js +50 -1
  39. package/dist/querier/abstractQuerier.d.ts +18 -1
  40. package/dist/querier/abstractQuerier.js +50 -14
  41. package/dist/querier/abstractSqlQuerier.d.ts +14 -0
  42. package/dist/querier/abstractSqlQuerier.js +17 -0
  43. package/dist/schema/schemaAST.d.ts +34 -3
  44. package/dist/schema/schemaAST.js +4 -9
  45. package/dist/schema/schemaASTBuilder.js +5 -2
  46. package/dist/schema/schemaASTDiffer.js +8 -4
  47. package/dist/schema/types.d.ts +2 -0
  48. package/dist/sqlite/sqliteDialect.js +2 -1
  49. package/dist/type/dialect.d.ts +21 -2
  50. package/dist/type/entity.d.ts +21 -0
  51. package/dist/type/migration.d.ts +11 -18
  52. package/dist/util/dialect.util.js +5 -4
  53. package/dist/util/field.util.d.ts +23 -0
  54. package/dist/util/field.util.js +28 -0
  55. package/dist/util/fieldOption.util.d.ts +16 -3
  56. package/dist/util/fieldOption.util.js +23 -4
  57. package/dist/util/relationQuery.util.d.ts +34 -1
  58. package/dist/util/relationQuery.util.js +40 -3
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -53,7 +53,7 @@ The query is just JSON: build it dynamically, store it, diff it, or send it from
53
53
  - **One API, everywhere it runs.** PostgreSQL, PGlite, CockroachDB, MySQL, MariaDB, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun's native SQL, and even MongoDB. The same code on Node 24+, Bun, Deno, [Cloudflare Workers](https://uql-orm.dev/cloudflare-d1), [AWS Lambda and Vercel](https://uql-orm.dev/serverless), and [the browser](https://uql-orm.dev/browser), with no native binaries on the `fetch`-based drivers.
54
54
  - **Relations without N+1.** [`$populate`](https://uql-orm.dev/querying/relations) loads a to-many with one query for all parents, not one per parent. Nothing is lazy, so nothing fires behind your back in a serializer.
55
55
  - **Migrations you read before they run.** Edit an entity, run `uql-migrate generate:entities`, review the SQL in the PR like any other file. [`drift:check`](https://uql-orm.dev/migrations) catches a database that no longer matches.
56
- - **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [virtual fields](https://uql-orm.dev/entities/virtual-fields) are sub-queries you can filter on, and a migration can be plain SQL.
56
+ - **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [computed fields](https://uql-orm.dev/entities/computed-fields) are expressions you can filter on, and a migration can be plain SQL.
57
57
  - **Light.** Zero runtime dependencies, under 280 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
58
58
  - **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
59
59
  - **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), by 2.6-3x over the next closest and roughly 10x over the slowest, on Bun, Node and Deno alike. The same benchmark [scores the types](https://github.com/rogerpadilla/ts-orm-benchmark#type-safety) by writing ten ordinary mistakes in six ORMs' APIs and compiling them: UQL is the only one that catches all ten.
@@ -2,6 +2,13 @@ import type { ReservedSQL, SQL } from 'bun';
2
2
  import type { AbstractSqlDialect } from '../dialect/index.js';
3
3
  import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
4
4
  import type { ExtraOptions } from '../type/index.js';
5
+ /**
6
+ * Querier for `bun:sql`, Bun's built-in driver for Postgres, MySQL, MariaDB, CockroachDB and SQLite.
7
+ *
8
+ * @remarks Deliberately does not override `internalStream`, which every other SQL driver here does:
9
+ * Bun's `SQL.Query` is a `Promise` with no cursor or async-iterator API, so `findManyStream` falls back
10
+ * to the base class buffering the whole result, as `PgliteQuerier` does for the same reason.
11
+ */
5
12
  export declare class BunSqlQuerier extends AbstractPoolQuerier<ReservedSQL> {
6
13
  readonly sql: SQL;
7
14
  readonly extra?: ExtraOptions | undefined;
@@ -1,5 +1,12 @@
1
1
  import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
2
2
  import { getAffectedRows, getInsertId, isReservedConnection, normalizeRows } from './bunSql.util.js';
3
+ /**
4
+ * Querier for `bun:sql`, Bun's built-in driver for Postgres, MySQL, MariaDB, CockroachDB and SQLite.
5
+ *
6
+ * @remarks Deliberately does not override `internalStream`, which every other SQL driver here does:
7
+ * Bun's `SQL.Query` is a `Promise` with no cursor or async-iterator API, so `findManyStream` falls back
8
+ * to the base class buffering the whole result, as `PgliteQuerier` does for the same reason.
9
+ */
3
10
  export class BunSqlQuerier extends AbstractPoolQuerier {
4
11
  sql;
5
12
  extra;
@@ -7,6 +7,7 @@ export declare class BunSqlQuerierPool extends AbstractSqlQuerierPool<BunSqlQuer
7
7
  readonly config: SQL.Options;
8
8
  readonly sql: SQL;
9
9
  readonly sqlDialectName: SqlDialectName;
10
+ private foreignKeysOn?;
10
11
  constructor(config: SQL.Options, extra?: ExtraOptions);
11
12
  /**
12
13
  * Provides a pg-compatible interface for libraries like connect-pg-simple.
@@ -19,6 +19,7 @@ export class BunSqlQuerierPool extends AbstractSqlQuerierPool {
19
19
  config;
20
20
  sql;
21
21
  sqlDialectName;
22
+ foreignKeysOn;
22
23
  constructor(config, extra) {
23
24
  const dialectName = inferDialectName(config);
24
25
  super(new DialectMap[dialectName](dialectOptionsFrom(extra)), extra);
@@ -44,8 +45,12 @@ export class BunSqlQuerierPool extends AbstractSqlQuerierPool {
44
45
  }
45
46
  async getQuerier() {
46
47
  const connFactory = async () => {
47
- // Bun's SQLite adapter does not support connection reservation (it's unpooled).
48
+ // Bun's SQLite adapter does not support connection reservation (it's unpooled), and leaves
49
+ // `foreign_keys` off as `bun:sqlite` does, so without the pragma the constraints uql emits in
50
+ // its own DDL are decorative. One connection means one pragma, issued on the first acquisition.
48
51
  if (!isPoolableDialect(this.sqlDialectName)) {
52
+ this.foreignKeysOn ??= this.sql.unsafe('PRAGMA foreign_keys = ON');
53
+ await this.foreignKeysOn;
49
54
  return this.sql;
50
55
  }
51
56
  return this.sql.reserve();
@@ -1,4 +1,5 @@
1
1
  export * from './bunSqliteDialect.js';
2
+ export * from './bunSqlCockroachDialect.js';
2
3
  export * from './bunSqlPostgresDialect.js';
3
4
  export * from './bunSqlQuerier.js';
4
5
  export * from './bunSqlQuerierPool.js';
@@ -1,4 +1,5 @@
1
1
  export * from './bunSqliteDialect.js';
2
+ export * from './bunSqlCockroachDialect.js';
2
3
  export * from './bunSqlPostgresDialect.js';
3
4
  export * from './bunSqlQuerier.js';
4
5
  export * from './bunSqlQuerierPool.js';
@@ -1,4 +1,5 @@
1
1
  import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryFilter, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorNear, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
2
+ import { type ParentPartition } from '../util/index.js';
2
3
  import type { HydrateKind } from './hydrateColumn.js';
3
4
  import { type JsonAccessMode } from './jsonSql.js';
4
5
  import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
@@ -52,6 +53,18 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
52
53
  */
53
54
  readonly maxBindValues: number;
54
55
  getBeginTransactionStatements(isolationLevel?: IsolationLevel): string[];
56
+ /**
57
+ * Every parent's own bounded page in one statement: a subquery per parent, each filtered to that
58
+ * parent alone and carrying its own `ORDER BY`, `LIMIT` and `OFFSET`. Universal, and reads
59
+ * `parents x (skip + limit)` rows where a `ROW_NUMBER` window reads every matching child.
60
+ * [The design](../../../../architecture/populate-limits.md).
61
+ *
62
+ * Each branch is a wrapped derived table rather than a bare parenthesised select: SQLite rejects
63
+ * `ORDER BY`/`LIMIT` on the latter, and the wrapper costs nothing elsewhere.
64
+ */
65
+ findPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, partition: ParentPartition): void;
66
+ /** The shape {@link findPerParent} emits, which the Postgres family replaces with a `LATERAL` join. */
67
+ protected appendPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, { joins, parents }: ParentPartition): void;
55
68
  createContext(): QueryContext;
56
69
  /**
57
70
  * Builds SQL text in isolation via `build`, so the caller can embed it inline (e.g.
@@ -171,6 +184,13 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
171
184
  * `NOT (... <=> ...)` - instead of having to fall back to a form that takes none.
172
185
  */
173
186
  protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string;
187
+ /**
188
+ * The expression an inlined computed field stands for, or nothing when the field is a real column.
189
+ *
190
+ * Every clause that names such a field needs the expression itself, never the output alias: an
191
+ * alias exists only when the field was also selected, which `$where` and `$sort` cannot assume.
192
+ */
193
+ private inlinedOperand;
174
194
  compareFieldOperator<E, K extends keyof QueryWhereFieldOperatorMap<E>>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, op: K, val: QueryWhereFieldOperatorMap<E>[K], opts?: QueryOptions): void;
175
195
  /**
176
196
  * `<operand> <op> <value>` for every operator that needs nothing but its left-hand SQL, or
@@ -261,8 +281,8 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
261
281
  */
262
282
  private collectSortTerms;
263
283
  /**
264
- * The `ORDER BY` operand for one key. A key that is not a column of `meta` - a virtual field, a
265
- * `raw()` projection - is an output alias, which is never table-qualified and needs no resolving.
284
+ * The `ORDER BY` operand for one key. A key that is not a field of `meta` - a `raw()` projection, a
285
+ * `$agg` alias - is an output alias, which is never table-qualified and needs no resolving.
266
286
  */
267
287
  private sortColumn;
268
288
  pager(ctx: QueryContext, opts: QueryPager): void;
@@ -1,8 +1,9 @@
1
1
  import { getMeta, soleIdOf } from '../entity/index.js';
2
2
  import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
3
- import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
3
+ import { computedExpression, isInlinedExpression } from '../util/field.util.js';
4
+ import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, queryChildrenOf, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
4
5
  import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
5
- import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX } from './aliases.js';
6
+ import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX, PER_PARENT_BRANCH_ALIAS } from './aliases.js';
6
7
  import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
7
8
  import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
8
9
  import { SqlQueryContext } from './queryContext.js';
@@ -54,6 +55,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
54
55
  // 'set-before' - MySQL/MariaDB pattern
55
56
  return [`SET TRANSACTION ISOLATION LEVEL ${level}`, this.beginTransactionCommand];
56
57
  }
58
+ /**
59
+ * Every parent's own bounded page in one statement: a subquery per parent, each filtered to that
60
+ * parent alone and carrying its own `ORDER BY`, `LIMIT` and `OFFSET`. Universal, and reads
61
+ * `parents x (skip + limit)` rows where a `ROW_NUMBER` window reads every matching child.
62
+ * [The design](../../../../architecture/populate-limits.md).
63
+ *
64
+ * Each branch is a wrapped derived table rather than a bare parenthesised select: SQLite rejects
65
+ * `ORDER BY`/`LIMIT` on the latter, and the wrapper costs nothing elsewhere.
66
+ */
67
+ findPerParent(ctx, entity, q, partition) {
68
+ if (!partition.parents.length) {
69
+ // Guarded on the contract rather than in either shape: this is the end that would otherwise
70
+ // append nothing and hand the driver an empty statement, and both shapes owe the same promise.
71
+ throw new TypeError('cannot read a bounded relation for no parents at all');
72
+ }
73
+ this.appendPerParent(ctx, entity, q, partition);
74
+ }
75
+ /** The shape {@link findPerParent} emits, which the Postgres family replaces with a `LATERAL` join. */
76
+ appendPerParent(ctx, entity, q, { joins, parents }) {
77
+ parents.forEach((parent, index) => {
78
+ if (index) {
79
+ ctx.append(' UNION ALL ');
80
+ }
81
+ ctx.append('SELECT * FROM (');
82
+ this.find(ctx, entity, queryChildrenOf(q, joins, parent));
83
+ ctx.append(`) ${this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS))}`);
84
+ });
85
+ }
57
86
  createContext() {
58
87
  return new SqlQueryContext(this);
59
88
  }
@@ -160,9 +189,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
160
189
  const field = meta.fields[key];
161
190
  if (!field)
162
191
  return;
163
- if (field.virtual) {
192
+ if (isInlinedExpression(field)) {
164
193
  this.getRawValue(ctx, {
165
- value: field.virtual.as(key),
194
+ value: computedExpression(field).as(key),
166
195
  prefix: opts.prefix,
167
196
  escapedPrefix,
168
197
  autoPrefixAlias: opts.autoPrefixAlias,
@@ -485,15 +514,23 @@ export class AbstractSqlDialect extends VectorSqlDialect {
485
514
  */
486
515
  resolveOperandField(ctx, entity, key, opts) {
487
516
  const field = getMeta(entity).fields[key];
488
- const virtual = field?.virtual;
489
- if (virtual) {
490
- return this.buildFragment(ctx, (fragmentCtx) => this.getRawValue(fragmentCtx, {
491
- value: virtual,
492
- prefix: opts.prefix,
493
- escapedPrefix: this.escapeId(opts.prefix, true, true),
494
- }));
495
- }
496
- return this.columnWithPrefix(key, field, opts.prefix);
517
+ return this.inlinedOperand(ctx, field, opts.prefix) ?? this.columnWithPrefix(key, field, opts.prefix);
518
+ }
519
+ /**
520
+ * The expression an inlined computed field stands for, or nothing when the field is a real column.
521
+ *
522
+ * Every clause that names such a field needs the expression itself, never the output alias: an
523
+ * alias exists only when the field was also selected, which `$where` and `$sort` cannot assume.
524
+ */
525
+ inlinedOperand(ctx, field, prefix) {
526
+ const inlined = field && isInlinedExpression(field) ? computedExpression(field) : undefined;
527
+ return inlined
528
+ ? this.buildFragment(ctx, (fragmentCtx) => this.getRawValue(fragmentCtx, {
529
+ value: inlined,
530
+ prefix,
531
+ escapedPrefix: this.escapeId(prefix, true, true),
532
+ }))
533
+ : undefined;
497
534
  }
498
535
  compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
499
536
  const field = this.resolveOperandField(ctx, entity, key, opts);
@@ -767,17 +804,17 @@ export class AbstractSqlDialect extends VectorSqlDialect {
767
804
  : this.buildFragment(ctx, (fragmentCtx) => this.appendVectorSort(fragmentCtx, meta, key, value)));
768
805
  continue;
769
806
  }
770
- columns.push(this.sortColumn(meta, key, prefix) + this.resolveSortDirection(value));
807
+ columns.push(this.sortColumn(ctx, meta, key, prefix) + this.resolveSortDirection(value));
771
808
  }
772
809
  }
773
810
  /**
774
- * The `ORDER BY` operand for one key. A key that is not a column of `meta` - a virtual field, a
775
- * `raw()` projection - is an output alias, which is never table-qualified and needs no resolving.
811
+ * The `ORDER BY` operand for one key. A key that is not a field of `meta` - a `raw()` projection, a
812
+ * `$agg` alias - is an output alias, which is never table-qualified and needs no resolving.
776
813
  */
777
- sortColumn(meta, key, prefix) {
814
+ sortColumn(ctx, meta, key, prefix) {
778
815
  const field = meta.fields[key];
779
816
  if (field) {
780
- return field.virtual ? this.escapeId(key) : this.columnWithPrefix(key, field, prefix);
817
+ return this.inlinedOperand(ctx, field, prefix) ?? this.columnWithPrefix(key, field, prefix);
781
818
  }
782
819
  const json = this.resolveJsonDotPath(meta, key, prefix);
783
820
  return json ? this.jsonPathExpr(json.column, json.jsonPath, 'text') : this.escapeId(key);
@@ -14,6 +14,10 @@ export declare const COUNT_ALIAS = "_uql_count";
14
14
  export declare const TOTAL_ALIAS = "_uql_total";
15
15
  /** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
16
16
  export declare const DISTINCT_DERIVED_ALIAS = "_uql_distinct";
17
+ /** Prefix for the derived table each branch of a per-parent bounded read is wrapped in. */
18
+ export declare const PER_PARENT_BRANCH_ALIAS = "_uql_p";
19
+ /** The row source a `LATERAL` per-parent read correlates each of its branches against. */
20
+ export declare const PER_PARENT_KEYS_ALIAS = "_uql_keys";
17
21
  /** Prefix for the alias an exploded JSON array element is read through. */
18
22
  export declare const JSON_ELEM_ALIAS_PREFIX = "_uql_elem";
19
23
  /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
@@ -14,6 +14,10 @@ export const COUNT_ALIAS = '_uql_count';
14
14
  export const TOTAL_ALIAS = '_uql_total';
15
15
  /** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
16
16
  export const DISTINCT_DERIVED_ALIAS = '_uql_distinct';
17
+ /** Prefix for the derived table each branch of a per-parent bounded read is wrapped in. */
18
+ export const PER_PARENT_BRANCH_ALIAS = '_uql_p';
19
+ /** The row source a `LATERAL` per-parent read correlates each of its branches against. */
20
+ export const PER_PARENT_KEYS_ALIAS = '_uql_keys';
17
21
  /** Prefix for the alias an exploded JSON array element is read through. */
18
22
  export const JSON_ELEM_ALIAS_PREFIX = '_uql_elem';
19
23
  /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
@@ -32,7 +32,8 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
32
32
  renameColumn: true,
33
33
  foreignKeyAlter: true,
34
34
  primaryKeyAlter: true,
35
- columnComment: true,
35
+ generatedColumnAdd: true,
36
+ commentSyntax: 'inline',
36
37
  vectorIndexRequiresNotNull: false,
37
38
  vectorSupportsLength: false,
38
39
  supportsTimestamptz: false,
@@ -1,4 +1,5 @@
1
1
  import { type DialectFeatures, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QuerySizeComparisonOps, type QueryTextSearchOptions, type Type, type VectorDistance, type VectorOperatorMetric } from '../type/index.js';
2
+ import { type ParentPartition } from '../util/relationQuery.util.js';
2
3
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
3
4
  /**
4
5
  * Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
@@ -21,6 +22,19 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
21
22
  readonly commitTransactionCommand = "COMMIT";
22
23
  readonly rollbackTransactionCommand = "ROLLBACK";
23
24
  readonly alterColumnStrategy = "separate-clauses";
25
+ /**
26
+ * One `LATERAL` branch correlated against an array of the parent keys, in place of the base
27
+ * `UNION ALL` of a subquery per parent. Same rows and the same `parents x (skip + limit)` read, but
28
+ * the planner sees one correlated index loop rather than N branches to plan: flat in page size where
29
+ * `UNION ALL` is linear, and the statement's text stops changing with the number of parents, so one
30
+ * prepared statement serves every page.
31
+ *
32
+ * Postgres, CockroachDB, PGlite, Neon and bun-sql inherit it together. **MySQL has `LATERAL` and must
33
+ * not use it** - it does not plan this as a correlated index loop and measured slower than both its
34
+ * own `UNION ALL` and a query per parent, which is why this is an override rather than a capability
35
+ * flag. [The design](../../../../architecture/populate-limits.md).
36
+ */
37
+ protected appendPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, { joins, parents, parentFields }: ParentPartition): void;
24
38
  /** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
25
39
  protected readonly upsertUpdateBindsInPlace = true;
26
40
  readonly insertIdSource = "returning";
@@ -1,8 +1,11 @@
1
+ import { canonicalToSql, fieldOptionsToCanonical } from '../schema/canonicalType.js';
1
2
  import { QueryRaw, } from '../type/index.js';
2
3
  import { hasVectorNear } from '../util/dialect.util.js';
4
+ import { raw } from '../util/raw.js';
5
+ import { queryNarrowedTo } from '../util/relationQuery.util.js';
3
6
  import { escapeSingleQuotes } from '../util/sqlLiteral.js';
4
7
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
5
- import { JSON_PULL_ALIAS } from './aliases.js';
8
+ import { JSON_PULL_ALIAS, PER_PARENT_BRANCH_ALIAS, PER_PARENT_KEYS_ALIAS } from './aliases.js';
6
9
  import { jsonSetTarget } from './jsonSql.js';
7
10
  import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
8
11
  /**
@@ -29,7 +32,8 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
29
32
  renameColumn: true,
30
33
  foreignKeyAlter: true,
31
34
  primaryKeyAlter: true,
32
- columnComment: false,
35
+ generatedColumnAdd: true,
36
+ commentSyntax: 'statement',
33
37
  vectorIndexRequiresNotNull: false,
34
38
  vectorSupportsLength: true,
35
39
  supportsTimestamptz: true,
@@ -46,6 +50,42 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
46
50
  commitTransactionCommand = 'COMMIT';
47
51
  rollbackTransactionCommand = 'ROLLBACK';
48
52
  alterColumnStrategy = 'separate-clauses';
53
+ /**
54
+ * One `LATERAL` branch correlated against an array of the parent keys, in place of the base
55
+ * `UNION ALL` of a subquery per parent. Same rows and the same `parents x (skip + limit)` read, but
56
+ * the planner sees one correlated index loop rather than N branches to plan: flat in page size where
57
+ * `UNION ALL` is linear, and the statement's text stops changing with the number of parents, so one
58
+ * prepared statement serves every page.
59
+ *
60
+ * Postgres, CockroachDB, PGlite, Neon and bun-sql inherit it together. **MySQL has `LATERAL` and must
61
+ * not use it** - it does not plan this as a correlated index loop and measured slower than both its
62
+ * own `UNION ALL` and a query per parent, which is why this is an override rather than a capability
63
+ * flag. [The design](../../../../architecture/populate-limits.md).
64
+ */
65
+ appendPerParent(ctx, entity, q, { joins, parents, parentFields }) {
66
+ const keys = this.escapeId(ctx.nextAlias(PER_PARENT_KEYS_ALIAS));
67
+ const branch = this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS));
68
+ const column = (index) => `${keys}.k${index}`;
69
+ // `unnest` resolves an uncast parameter to `unknown` and refuses it ("function unnest(unknown) is
70
+ // not unique"), so the array says its type. It comes from the parent's key column, which always
71
+ // declares one, rather than the child's foreign key, which would have to be resolved through the
72
+ // reference it takes its own type from.
73
+ const sources = joins.map(({ parent }) => {
74
+ const field = parentFields[parent];
75
+ if (!field) {
76
+ throw new TypeError(`cannot page a relation per parent: '${parent}' is not a field of the parent entity`);
77
+ }
78
+ const values = parents.map((it) => it[parent]);
79
+ return `${this.addValue(ctx.values, values)}::${canonicalToSql(fieldOptionsToCanonical(field), this)}[]`;
80
+ });
81
+ const rowSource = `unnest(${sources.join(', ')}) AS ${keys}(${joins.map((_, index) => `k${index}`).join(', ')})`;
82
+ // The keys come from the row source rather than as values, which is the whole point of correlating:
83
+ // one branch, planned once, instead of one per parent.
84
+ const correlated = Object.fromEntries(joins.map(({ joined }, index) => [joined, raw(({ ctx: inner }) => inner.append(column(index)))]));
85
+ ctx.append(`SELECT ${branch}.* FROM ${rowSource} JOIN LATERAL (`);
86
+ this.find(ctx, entity, queryNarrowedTo(q, correlated));
87
+ ctx.append(`) ${branch} ON TRUE`);
88
+ }
49
89
  /** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
50
90
  upsertUpdateBindsInPlace = true;
51
91
  insertIdSource = 'returning';
@@ -1,4 +1,5 @@
1
1
  import { SOFT_DELETE_FILTER } from '../../type/index.js';
2
+ import { isInlinedExpression } from '../../util/field.util.js';
2
3
  import { entityName, fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
3
4
  import { ownRegistrations } from '../decorator/bag.js';
4
5
  /**
@@ -16,7 +17,13 @@ function globalMap(key) {
16
17
  const metas = globalMap('uql-orm/entity/metadata/v1');
17
18
  export function defineField(entity, key, opts = {}) {
18
19
  const meta = ensureWritableMeta(entity);
19
- if (!opts.type && !opts.references && !opts.virtual) {
20
+ if (opts.virtual !== undefined && opts.computed !== undefined) {
21
+ throw new TypeError(`'${entity.name}.${key}' gives both 'virtual' and 'computed'. They are one option under two names - ` +
22
+ "keep 'computed'; 'npx uql-codemod' rewrites the other.");
23
+ }
24
+ // A stored computed column is a real column and still needs a type; only an inlined one is exempt,
25
+ // its expression being spliced in rather than declared.
26
+ if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
20
27
  throw new TypeError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
21
28
  "at another entity with 'references', which resolves the column type from its primary key.");
22
29
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Fluent API for defining columns in migrations.
5
5
  */
6
+ import type { EnumValues } from '../../schema/types.js';
6
7
  import type { CanonicalType, ForeignKeyAction } from '../../schema/types.js';
7
8
  import type { BaseColumnOptions, FullColumnDefinition, IColumnBuilder, IForeignKeyBuilder } from './types.js';
8
9
  /**
@@ -17,6 +18,8 @@ export declare class ColumnBuilder implements IColumnBuilder, IForeignKeyBuilder
17
18
  private _primaryKey;
18
19
  private _autoIncrement;
19
20
  private _unique;
21
+ private _enum?;
22
+ private _generatedAs?;
20
23
  private _comment?;
21
24
  private _index?;
22
25
  private _foreignKey?;
@@ -45,6 +48,17 @@ export declare class ColumnBuilder implements IColumnBuilder, IForeignKeyBuilder
45
48
  * Add a unique constraint.
46
49
  */
47
50
  unique(): this;
51
+ /**
52
+ * Make the column one the database computes: `GENERATED ALWAYS AS (<sql>) STORED`.
53
+ *
54
+ * Takes the SQL as text, since a `CREATE TABLE` has nowhere to bind a value into - the same reason
55
+ * a check expression and a partial-index predicate do.
56
+ */
57
+ computed(sql: string): this;
58
+ /**
59
+ * Constrain the column to these values, as a `CHECK (col IN (...))` - what `@Field({ enum })` emits.
60
+ */
61
+ enum(values: EnumValues): this;
48
62
  /**
49
63
  * Add a comment to the column.
50
64
  */
@@ -15,6 +15,8 @@ export class ColumnBuilder {
15
15
  _primaryKey;
16
16
  _autoIncrement;
17
17
  _unique;
18
+ _enum;
19
+ _generatedAs;
18
20
  _comment;
19
21
  _index;
20
22
  _foreignKey;
@@ -35,8 +37,7 @@ export class ColumnBuilder {
35
37
  // Handle inline references option
36
38
  if (options.references) {
37
39
  this._foreignKey = {
38
- table: options.references.table,
39
- columns: [options.references.column ?? 'id'],
40
+ references: { table: options.references.table, columns: [options.references.column ?? 'id'] },
40
41
  onDelete: options.references.onDelete ?? 'NO ACTION',
41
42
  onUpdate: options.references.onUpdate ?? 'NO ACTION',
42
43
  };
@@ -85,6 +86,23 @@ export class ColumnBuilder {
85
86
  this._unique = true;
86
87
  return this;
87
88
  }
89
+ /**
90
+ * Make the column one the database computes: `GENERATED ALWAYS AS (<sql>) STORED`.
91
+ *
92
+ * Takes the SQL as text, since a `CREATE TABLE` has nowhere to bind a value into - the same reason
93
+ * a check expression and a partial-index predicate do.
94
+ */
95
+ computed(sql) {
96
+ this._generatedAs = sql;
97
+ return this;
98
+ }
99
+ /**
100
+ * Constrain the column to these values, as a `CHECK (col IN (...))` - what `@Field({ enum })` emits.
101
+ */
102
+ enum(values) {
103
+ this._enum = values;
104
+ return this;
105
+ }
88
106
  /**
89
107
  * Add a comment to the column.
90
108
  */
@@ -113,8 +131,7 @@ export class ColumnBuilder {
113
131
  */
114
132
  references(table, column = 'id') {
115
133
  this._foreignKey = {
116
- table,
117
- columns: [column],
134
+ references: { table, columns: [column] },
118
135
  onDelete: 'NO ACTION',
119
136
  onUpdate: 'NO ACTION',
120
137
  };
@@ -125,7 +142,7 @@ export class ColumnBuilder {
125
142
  */
126
143
  onDelete(action) {
127
144
  if (this._foreignKey) {
128
- this._foreignKey.onDelete = action;
145
+ this._foreignKey = { ...this._foreignKey, onDelete: action };
129
146
  }
130
147
  return this;
131
148
  }
@@ -134,7 +151,7 @@ export class ColumnBuilder {
134
151
  */
135
152
  onUpdate(action) {
136
153
  if (this._foreignKey) {
137
- this._foreignKey.onUpdate = action;
154
+ this._foreignKey = { ...this._foreignKey, onUpdate: action };
138
155
  }
139
156
  return this;
140
157
  }
@@ -147,9 +164,11 @@ export class ColumnBuilder {
147
164
  type: this._type,
148
165
  nullable: this._nullable,
149
166
  defaultValue: this._defaultValue,
150
- primaryKey: this._primaryKey,
151
- autoIncrement: this._autoIncrement,
152
- unique: this._unique,
167
+ isPrimaryKey: this._primaryKey,
168
+ isAutoIncrement: this._autoIncrement,
169
+ isUnique: this._unique,
170
+ enum: this._enum,
171
+ generatedAs: this._generatedAs,
153
172
  comment: this._comment,
154
173
  index: this._index,
155
174
  foreignKey: this._foreignKey,
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { ddlText, normalizeIndexColumn } from '../../util/index.js';
7
7
  import { derivedIndexName } from '../../util/sql.util.js';
8
+ import { columnForeignKey, columnIndex } from '../generator/definitionToNode.js';
8
9
  import { ColumnBuilder } from './columnBuilder.js';
9
10
  import { expr } from './expressions.js';
10
11
  /**
@@ -193,18 +194,11 @@ export class TableBuilder {
193
194
  build() {
194
195
  // Build all columns from builders
195
196
  const columns = this._columnBuilders.map((cb) => cb.build());
196
- // Collect column-level indexes
197
+ // Collect column-level indexes, skipping any a table-level one already names.
197
198
  for (const col of columns) {
198
- if (col.index) {
199
- const indexName = typeof col.index === 'string' ? col.index : derivedIndexName(this._name, [col.name]);
200
- // Only add if not already in table-level indexes
201
- if (!this._indexes.some((idx) => idx.name === indexName)) {
202
- this._indexes.push({
203
- name: indexName,
204
- entries: [{ column: col.name }],
205
- unique: col.unique,
206
- });
207
- }
199
+ const index = columnIndex(this._name, col);
200
+ if (index && !this._indexes.some((idx) => idx.name === index.name)) {
201
+ this._indexes.push(index);
208
202
  }
209
203
  }
210
204
  // Build foreign keys
@@ -213,14 +207,9 @@ export class TableBuilder {
213
207
  .filter((fk) => fk !== undefined);
214
208
  // Collect column-level foreign keys
215
209
  for (const col of columns) {
216
- if (col.foreignKey) {
217
- foreignKeys.push({
218
- name: col.foreignKey.name,
219
- columns: [col.name],
220
- references: { table: col.foreignKey.table, columns: col.foreignKey.columns },
221
- onDelete: col.foreignKey.onDelete,
222
- onUpdate: col.foreignKey.onUpdate,
223
- });
210
+ const foreignKey = columnForeignKey(col);
211
+ if (foreignKey) {
212
+ foreignKeys.push(foreignKey);
224
213
  }
225
214
  }
226
215
  return {