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.
- package/README.md +1 -1
- package/dist/bunSql/bunSqlQuerier.d.ts +7 -0
- package/dist/bunSql/bunSqlQuerier.js +7 -0
- package/dist/bunSql/bunSqlQuerierPool.d.ts +1 -0
- package/dist/bunSql/bunSqlQuerierPool.js +6 -1
- package/dist/bunSql/index.d.ts +1 -0
- package/dist/bunSql/index.js +1 -0
- package/dist/dialect/abstractSqlDialect.d.ts +22 -2
- package/dist/dialect/abstractSqlDialect.js +55 -18
- package/dist/dialect/aliases.d.ts +4 -0
- package/dist/dialect/aliases.js +4 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +14 -0
- package/dist/dialect/pgLikeSqlDialect.js +42 -2
- package/dist/entity/metadata/definition.js +8 -1
- package/dist/migrate/builder/columnBuilder.d.ts +14 -0
- package/dist/migrate/builder/columnBuilder.js +28 -9
- package/dist/migrate/builder/tableBuilder.js +8 -19
- package/dist/migrate/builder/types.d.ts +16 -33
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
- package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
- package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
- package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
- package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
- package/dist/migrate/codegen/sourceLiteral.js +17 -0
- package/dist/migrate/generator/definitionToNode.d.ts +21 -0
- package/dist/migrate/generator/definitionToNode.js +47 -25
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +2 -2
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
- package/dist/migrate/schemaGenerator.d.ts +48 -2
- package/dist/migrate/schemaGenerator.js +109 -36
- package/dist/mongo/mongoDialect.js +2 -1
- package/dist/mongo/mongodbQuerier.d.ts +16 -0
- package/dist/mongo/mongodbQuerier.js +50 -1
- package/dist/querier/abstractQuerier.d.ts +18 -1
- package/dist/querier/abstractQuerier.js +50 -14
- package/dist/querier/abstractSqlQuerier.d.ts +14 -0
- package/dist/querier/abstractSqlQuerier.js +17 -0
- package/dist/schema/schemaAST.d.ts +34 -3
- package/dist/schema/schemaAST.js +4 -9
- package/dist/schema/schemaASTBuilder.js +5 -2
- package/dist/schema/schemaASTDiffer.js +8 -4
- package/dist/schema/types.d.ts +2 -0
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/type/dialect.d.ts +21 -2
- package/dist/type/entity.d.ts +21 -0
- package/dist/type/migration.d.ts +11 -18
- package/dist/util/dialect.util.js +5 -4
- package/dist/util/field.util.d.ts +23 -0
- package/dist/util/field.util.js +28 -0
- package/dist/util/fieldOption.util.d.ts +16 -3
- package/dist/util/fieldOption.util.js +23 -4
- package/dist/util/relationQuery.util.d.ts +34 -1
- package/dist/util/relationQuery.util.js +40 -3
- 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, [
|
|
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();
|
package/dist/bunSql/index.d.ts
CHANGED
package/dist/bunSql/index.js
CHANGED
|
@@ -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
|
|
265
|
-
* `
|
|
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 {
|
|
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
|
|
192
|
+
if (isInlinedExpression(field)) {
|
|
164
193
|
this.getRawValue(ctx, {
|
|
165
|
-
value: field.
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
775
|
-
* `
|
|
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
|
|
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}. */
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 {
|