uql-orm 0.27.0 → 0.28.1
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 +6 -6
- package/dist/dialect/abstractSqlDialect.d.ts +69 -44
- package/dist/dialect/abstractSqlDialect.js +278 -315
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +1 -3
- package/dist/dialect/queryContext.d.ts +7 -5
- package/dist/dialect/queryContext.js +11 -6
- package/dist/dialect/queryJoins.d.ts +53 -0
- package/dist/dialect/queryJoins.js +91 -0
- package/dist/dialect/vectorSqlDialect.d.ts +1 -1
- package/dist/entity/metadata/definition.js +2 -2
- package/dist/mongo/mongoDialect.d.ts +38 -8
- package/dist/mongo/mongoDialect.js +103 -64
- package/dist/mongo/mongodbQuerier.js +19 -42
- package/dist/sqlite/sqliteDialect.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.js +3 -3
- package/dist/type/dialect.d.ts +11 -3
- package/dist/type/query.d.ts +3 -3
- package/dist/util/dialect.util.d.ts +1 -2
- package/dist/util/dialect.util.js +0 -3
- package/dist/util/relationQuery.util.d.ts +17 -2
- package/dist/util/relationQuery.util.js +35 -2
- package/dist/util/sql.util.d.ts +1 -2
- package/dist/util/sql.util.js +1 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
<a href="https://uql-orm.dev">
|
|
4
4
|
<picture>
|
|
5
5
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rogerpadilla/uql/main/assets/logo-dark.svg">
|
|
6
|
-
<img src="https://raw.githubusercontent.com/rogerpadilla/uql/main/assets/logo.svg" alt="UQL" width="
|
|
6
|
+
<img src="https://raw.githubusercontent.com/rogerpadilla/uql/main/assets/logo.svg" alt="UQL" width="72" height="72">
|
|
7
7
|
</picture>
|
|
8
8
|
</a>
|
|
9
9
|
|
|
10
|
-
<h3>The
|
|
10
|
+
<h3>The JSON-native TypeScript ORM</h3>
|
|
11
11
|
|
|
12
|
-
<p>
|
|
12
|
+
<p>Queries are plain JSON, typed to the leaf. Unified across SQL databases and MongoDB.</p>
|
|
13
13
|
|
|
14
14
|
<p>
|
|
15
15
|
<a href="https://uql-orm.dev"><b>Website</b></a> ·
|
|
@@ -56,9 +56,9 @@ from the browser to the server. The same object runs on every supported database
|
|
|
56
56
|
- **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.
|
|
57
57
|
- **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.
|
|
58
58
|
- **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.
|
|
59
|
-
- **Light.** Zero runtime dependencies,
|
|
59
|
+
- **Light.** Zero runtime dependencies, 269 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
|
|
60
60
|
- **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).
|
|
61
|
-
- **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):
|
|
61
|
+
- **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): 206µs, against 644µs for the next closest and 2,236µs for the slowest.
|
|
62
62
|
|
|
63
63
|
## Get started
|
|
64
64
|
|
|
@@ -77,4 +77,4 @@ Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/ma
|
|
|
77
77
|
|
|
78
78
|
Built something? [Open a PR](https://github.com/rogerpadilla/uql/blob/main/CONTRIBUTING.md) and add it here.
|
|
79
79
|
|
|
80
|
-
[](https://uql-orm.dev)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager,
|
|
1
|
+
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, 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
2
|
import type { HydrateKind } from './hydrateColumn.js';
|
|
3
3
|
import { IndexSqlDialect } from './indexSqlDialect.js';
|
|
4
|
+
import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
|
|
4
5
|
/** How a column's values are bound: see {@link AbstractSqlDialect.persistKind}. */
|
|
5
6
|
type PersistKind = 'plain' | 'json' | 'vector';
|
|
6
7
|
/** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
|
|
@@ -34,7 +35,8 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
34
35
|
* straight into `ctx`'s own values array - shared by reference, not copied - so `addValue` numbers
|
|
35
36
|
* its placeholder correctly against the real query from the start; a fresh, empty array would
|
|
36
37
|
* instead number from `1` regardless of how many values `ctx` already has, misnumbering every
|
|
37
|
-
* bound value on `$n`-placeholder dialects once `ctx` isn't otherwise empty.
|
|
38
|
+
* bound value on `$n`-placeholder dialects once `ctx` isn't otherwise empty. Generated aliases are
|
|
39
|
+
* shared for the same reason - see {@link SqlQueryContext}.
|
|
38
40
|
*/
|
|
39
41
|
protected buildFragment(ctx: QueryContext, build: (fragmentCtx: QueryContext) => void): string;
|
|
40
42
|
addValue(values: unknown[], value: unknown): string;
|
|
@@ -51,7 +53,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
51
53
|
normalizeValues(values: unknown[] | undefined): unknown[] | undefined;
|
|
52
54
|
placeholder(_index: number): string;
|
|
53
55
|
returningId<E>(entity: Type<E>): string;
|
|
54
|
-
search<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions): void;
|
|
56
|
+
search<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, joins?: QueryJoins): void;
|
|
55
57
|
selectFields<E>(ctx: QueryContext, entity: Type<E>, select: QuerySelectValue<E> | undefined, opts?: QuerySelectOptions, exclude?: QueryExclude<E>): void;
|
|
56
58
|
/**
|
|
57
59
|
* The expression a scalar field is read through, the plain column by default. MariaDB reads a
|
|
@@ -65,19 +67,11 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
65
67
|
* than inheriting another engine's syntax.
|
|
66
68
|
*/
|
|
67
69
|
protected appendTextSearch<E>(_ctx: QueryContext, _entity: Type<E>, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>): void;
|
|
68
|
-
select<E>(ctx: QueryContext, entity: Type<E>,
|
|
70
|
+
select<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions, joins?: QueryJoins): void;
|
|
71
|
+
/** Columns are alias-qualified once anything else is in play: a join, or a to-many being filled. */
|
|
69
72
|
private resolveRelationAwarePrefix;
|
|
70
|
-
protected selectRelationFields
|
|
71
|
-
|
|
72
|
-
}): void;
|
|
73
|
-
protected selectRelationJoins<E>(ctx: QueryContext, entity: Type<E>, select: QuerySelect<E> | undefined, populate: QueryPopulate<E> | undefined, opts?: {
|
|
74
|
-
prefix?: string;
|
|
75
|
-
}): void;
|
|
76
|
-
/**
|
|
77
|
-
* Iterates over joinable (11/m1) relations for a given select, resolving shared metadata.
|
|
78
|
-
* Used by both `selectRelationFields` and `selectRelationJoins` to avoid duplicated iteration logic.
|
|
79
|
-
*/
|
|
80
|
-
private forEachJoinableRelation;
|
|
73
|
+
protected selectRelationFields(ctx: QueryContext, joins: QueryJoins): void;
|
|
74
|
+
protected selectRelationJoins<E>(ctx: QueryContext, meta: EntityMeta<E>, tableName: string, joins: QueryJoins): void;
|
|
81
75
|
where<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
82
76
|
/** Renders a `$where` tree without applying entity filters (used for same-scope `$and`/`$or` recursion). */
|
|
83
77
|
protected renderWhere<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
@@ -87,39 +81,57 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
87
81
|
private readonly escapedColumns;
|
|
88
82
|
private static readonly NEGATE_OP_MAP;
|
|
89
83
|
private static readonly COMPARE_OP_MAP;
|
|
90
|
-
private static readonly LIKE_OP_MAP;
|
|
91
84
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
85
|
+
* Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
|
|
86
|
+
* Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
|
|
87
|
+
* two can never drift apart - and neither one decides case folding, which is
|
|
88
|
+
* {@link caseInsensitiveMatch}'s single call.
|
|
89
|
+
*/
|
|
90
|
+
private static readonly LIKE_OPS;
|
|
91
|
+
/**
|
|
92
|
+
* How this engine matches case-insensitively. One decision, not two: folding the pattern while the
|
|
93
|
+
* comparison leaves the column alone matches neither case, which is what `$istartsWith: 'Some'`
|
|
94
|
+
* used to do wherever `LIKE` is case-sensitive.
|
|
95
|
+
*
|
|
96
|
+
* - `ilike`: the engine has a case-insensitive operator (`ILIKE`), so the pattern goes through as written.
|
|
97
|
+
* - `native`: plain `LIKE` already ignores case (SQLite, for ASCII). Folding the pattern in JS would
|
|
98
|
+
* only break the non-ASCII characters the engine cannot fold anyway - `'É'` would become an `'é'`
|
|
99
|
+
* that matches nothing.
|
|
100
|
+
* - `fold`: nothing ignores case on its own, so both sides are lowered explicitly. Not indexable as
|
|
101
|
+
* such; an expression index over `LOWER(column)` is what makes it so.
|
|
95
102
|
*/
|
|
96
|
-
|
|
97
|
-
/**
|
|
103
|
+
protected readonly caseInsensitiveMatch: 'ilike' | 'native' | 'fold';
|
|
104
|
+
/**
|
|
105
|
+
* A `$like`-family condition, or `undefined` when `op` is not one of them. Shared by columns and
|
|
106
|
+
* JSON paths, and the only place a pattern is folded - always together with the column it is
|
|
107
|
+
* compared against.
|
|
108
|
+
*/
|
|
109
|
+
protected likeCondition(ctx: QueryContext, operand: string, op: string, val: unknown): string | undefined;
|
|
110
|
+
/** Builds `prefix.column` from an already-resolved field, through the same memo writes use. */
|
|
98
111
|
private columnWithPrefix;
|
|
99
112
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
113
|
+
* The SQL a field comparison reads its left-hand side from. A virtual field builds its expression
|
|
114
|
+
* as text rather than appending it, so every operator gets a real operand to wrap - `LOWER(...)`,
|
|
115
|
+
* `NOT (... <=> ...)` - instead of having to fall back to a form that takes none.
|
|
102
116
|
*/
|
|
103
|
-
protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string
|
|
104
|
-
private appendFieldSql;
|
|
117
|
+
protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string;
|
|
105
118
|
compareFieldOperator<E, K extends keyof QueryWhereFieldOperatorMap<E>>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, op: K, val: QueryWhereFieldOperatorMap<E>[K], opts?: QueryOptions): void;
|
|
106
119
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
120
|
+
* `<operand> <op> <value>` for every operator that needs nothing but its left-hand SQL, or
|
|
121
|
+
* `undefined` when `op` is not one of them.
|
|
109
122
|
*
|
|
110
123
|
* One implementation for three callers that each had their own: a WHERE column, a HAVING aggregate
|
|
111
|
-
* expression, and a `$size` count (
|
|
112
|
-
*
|
|
113
|
-
* `unsupported HAVING operator` on the `$like` that `QueryHavingMap` accepts, and neither of
|
|
114
|
-
* other two turned `$eq: null` into `IS NULL` the way the WHERE path does.
|
|
124
|
+
* expression, and a `$size` count (whose expression is already in the context, so it passes an
|
|
125
|
+
* empty operand). They previously disagreed - HAVING carried a second comparison-operator map and
|
|
126
|
+
* threw `unsupported HAVING operator` on the `$like` that `QueryHavingMap` accepts, and neither of
|
|
127
|
+
* the other two turned `$eq: null` into `IS NULL` the way the WHERE path does.
|
|
115
128
|
*
|
|
116
129
|
* The operators kept out are the ones that need more than an operand: `$not` recurses through the
|
|
117
130
|
* entity, and `$all`/`$size`/`$elemMatch` address a JSON document.
|
|
118
131
|
*/
|
|
119
|
-
protected
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
private appendInNin;
|
|
132
|
+
protected operatorCondition(ctx: QueryContext, operand: string, op: string, val: unknown): string | undefined;
|
|
133
|
+
/** {@link operatorCondition}, appended; `false` when `op` needs more than an operand. */
|
|
134
|
+
protected appendOperatorCondition(ctx: QueryContext, operand: string, op: string, val: unknown): boolean;
|
|
123
135
|
/**
|
|
124
136
|
* Build a comparison condition for a JSON field.
|
|
125
137
|
* Used by both `$elemMatch` and dot-notation paths.
|
|
@@ -181,23 +193,34 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
181
193
|
* dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
182
194
|
*/
|
|
183
195
|
protected jsonScalarParam(ctx: QueryContext, value: unknown): string;
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
/** {@link resolveOperandField}, appended. */
|
|
197
|
+
getComparisonKey<E>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, opts?: QueryOptions): void;
|
|
198
|
+
sort<E>(ctx: QueryContext, entity: Type<E>, sort: QuerySortMap<E> | undefined, opts?: QuerySortOptions): void;
|
|
199
|
+
/**
|
|
200
|
+
* Walks `$sort` against the metadata of the entity each level addresses, rather than flattening it
|
|
201
|
+
* to dotted strings and reading every key off the root: only that way does a related column resolve
|
|
202
|
+
* through its own `@Field({ name })`, and only that way is `tax.category` the one alias the join
|
|
203
|
+
* carries instead of two quoted identifiers.
|
|
204
|
+
*/
|
|
205
|
+
private collectSortTerms;
|
|
206
|
+
/**
|
|
207
|
+
* The `ORDER BY` operand for one key. A key that is not a column of `meta` - a virtual field, a
|
|
208
|
+
* `raw()` projection - is an output alias, which is never table-qualified and needs no resolving.
|
|
209
|
+
*/
|
|
210
|
+
private sortColumn;
|
|
186
211
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
187
212
|
/** Whether this engine has row locks at all. The SQLite family locks the database instead. */
|
|
188
213
|
readonly supportsRowLocks: boolean;
|
|
189
214
|
/** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
|
|
190
215
|
readonly supportsLockOf: boolean;
|
|
191
|
-
/** Whether this statement joins, which is what forces the lock to be narrowed to one table. */
|
|
192
|
-
private joinsRelations;
|
|
193
216
|
/** Validated before the querier checks for a transaction, so the clearer error wins. */
|
|
194
|
-
assertLockSupported<E>(entity: Type<E>, q: Query<E
|
|
217
|
+
assertLockSupported<E>(entity: Type<E>, q: Query<E>, joins?: QueryJoins): void;
|
|
195
218
|
/**
|
|
196
219
|
* The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
|
|
197
220
|
* joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
|
|
198
221
|
* and the other engines quietly widen the lock to the joined rows.
|
|
199
222
|
*/
|
|
200
|
-
protected appendLock<E>(ctx: QueryContext, entity: Type<E>, q: Query<E
|
|
223
|
+
protected appendLock<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, joins?: QueryJoins): void;
|
|
201
224
|
count<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
|
|
202
225
|
/** `$group` aggregate operator → SQL function name. An allowlist, not a formatter: the op key
|
|
203
226
|
* comes from query data, so anything outside this map must be rejected rather than passed
|
|
@@ -205,7 +228,9 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
205
228
|
private static readonly AGGREGATE_FN_MAP;
|
|
206
229
|
aggregate<E, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(ctx: QueryContext, entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): void;
|
|
207
230
|
/**
|
|
208
|
-
* ORDER BY for aggregate queries - handles both entity-field and alias references.
|
|
231
|
+
* ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
|
|
232
|
+
* statement has no joins to address, so a relation key is rejected rather than emitted as an alias
|
|
233
|
+
* nothing defines.
|
|
209
234
|
*/
|
|
210
235
|
private aggregateSort;
|
|
211
236
|
protected having(ctx: QueryContext, having: QueryHavingMap, aggregateExpressions: Record<string, string>): void;
|
|
@@ -253,7 +278,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
253
278
|
protected getUpsertUpdateAssignments<E>(ctx: QueryContext, meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], callback?: (columnName: string) => string): string;
|
|
254
279
|
protected getUpsertConflictPathsStr<E>(meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>): string;
|
|
255
280
|
delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
|
|
256
|
-
escapeId(val: string, forbidQualified?: boolean, addDot?: boolean): string;
|
|
281
|
+
escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
257
282
|
/**
|
|
258
283
|
* Bind one persisted value, classifying its column on the spot. Dialects override
|
|
259
284
|
* {@link appendJsonValue} and {@link appendVectorValue} rather than this, so the chain runs once per
|
|
@@ -386,6 +411,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
386
411
|
* metadata is shared between dialects while this result is not, since `escapeIdChar` and the naming
|
|
387
412
|
* strategy differ. Weakly keyed so a transient entity's metadata stays collectable.
|
|
388
413
|
*/
|
|
414
|
+
private escapedColumnOf;
|
|
389
415
|
private escapedColumnName;
|
|
390
416
|
private escapedColumn;
|
|
391
417
|
/** As {@link escapedColumn}, but qualified by the query alias when the parent is nested. */
|
|
@@ -428,7 +454,6 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
428
454
|
*/
|
|
429
455
|
protected get neOp(): string;
|
|
430
456
|
protected neExpr(field: string, ph: string): string;
|
|
431
|
-
protected ilikeExpr(f: string, ph: string): string;
|
|
432
457
|
/**
|
|
433
458
|
* Formats an IN/NOT IN expression, binding each value individually.
|
|
434
459
|
* Postgres overrides to use `= ANY($1)` / `<> ALL($1)` with a single array parameter.
|