uql-orm 0.55.0 → 0.57.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/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/bunSql/bunSql.util.js +1 -1
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +195 -32
- package/dist/dialect/abstractSqlDialect.js +406 -199
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +30 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +58 -7
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +25 -50
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +27 -15
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +4 -2
- package/dist/entity/metadata/definition.js +27 -29
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/builder/splitSqlStatements.js +2 -2
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +4 -11
- package/dist/migrate/codegen/fieldOptionsSource.js +1 -1
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -3
- package/dist/migrate/ddl/pgIndexDdl.js +29 -3
- package/dist/migrate/drift/driftDetector.js +21 -8
- package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/baseSqlIntrospector.js +65 -76
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
- package/dist/migrate/introspection/mysqlIntrospector.js +5 -8
- package/dist/migrate/introspection/sqliteIntrospector.js +2 -5
- package/dist/migrate/migrator.d.ts +7 -4
- package/dist/migrate/migrator.js +9 -14
- package/dist/migrate/schemaGenerator.d.ts +3 -3
- package/dist/migrate/schemaGenerator.js +7 -13
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +147 -108
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +34 -108
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +44 -143
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +11 -22
- package/dist/querier/abstractSqlQuerier.js +49 -53
- package/dist/schema/canonicalType.js +4 -6
- package/dist/schema/dependencyGraph.js +2 -4
- package/dist/schema/indexDifferences.js +5 -5
- package/dist/schema/schemaASTBuilder.js +34 -12
- package/dist/schema/schemaASTDiffer.d.ts +10 -2
- package/dist/schema/schemaASTDiffer.js +17 -16
- package/dist/schema/types.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +40 -8
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +2 -2
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +2 -1
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/query.d.ts +25 -48
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +10 -10
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +8 -2
- package/dist/util/dialect.util.js +19 -0
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/logger.d.ts +10 -1
- package/dist/util/logger.js +18 -0
- package/dist/util/object.util.d.ts +4 -0
- package/dist/util/object.util.js +8 -0
- package/dist/util/relationQuery.util.d.ts +15 -68
- package/dist/util/relationQuery.util.js +35 -83
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- package/dist/querier/relationCount.js +0 -121
|
@@ -2,7 +2,7 @@ import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
3
|
import { fieldOf, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
|
-
import { clone, getKeys,
|
|
5
|
+
import { clone, getKeys, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
8
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -11,12 +11,6 @@ import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys,
|
|
|
11
11
|
function asksForNoRows(q) {
|
|
12
12
|
return q.$limit === 0;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
* What MongoDB accepts in one pipeline. Bisected against a real server: 1000 top-level stages are
|
|
16
|
-
* accepted and 1001 refused (`Pipeline length must be no longer than 1000 stages`), and a
|
|
17
|
-
* `$unionWith`'s own sub-pipeline stages do not count toward it.
|
|
18
|
-
*/
|
|
19
|
-
const MAX_PIPELINE_STAGES = 1000;
|
|
20
14
|
export class MongodbQuerier extends AbstractQuerier {
|
|
21
15
|
dialect;
|
|
22
16
|
conn;
|
|
@@ -34,112 +28,23 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
34
28
|
});
|
|
35
29
|
}
|
|
36
30
|
async internalFindMany(entity, q, opts) {
|
|
37
|
-
this.dialect.assertNoLock(q);
|
|
38
31
|
if (asksForNoRows(q)) {
|
|
39
32
|
return [];
|
|
40
33
|
}
|
|
41
34
|
return this.timed('internalFindMany', undefined, async () => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
let documents;
|
|
45
|
-
if (vectorSort) {
|
|
46
|
-
const pipeline = this.buildVectorPipeline(entity, q, vectorSort, opts);
|
|
47
|
-
documents = await this.runPipeline(entity, meta, pipeline);
|
|
48
|
-
// to-many relations need their own query, exactly as in the non-vector path
|
|
49
|
-
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
// A relation condition needs `$lookup`, so it forces the aggregation path just like populating
|
|
53
|
-
// one does - and so does ordering by a relation, which reads what a lookup produced, and
|
|
54
|
-
// `$distinct`, which is a `$group`. A plain `find` cursor can express none of the four.
|
|
55
|
-
if (q.$distinct ||
|
|
56
|
-
populatesRelations(meta, q.$populate) ||
|
|
57
|
-
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
58
|
-
this.dialect.sortsRelations(entity, q.$sort)) {
|
|
59
|
-
const pipeline = this.dialect.aggregationPipeline(entity, q, opts);
|
|
60
|
-
documents = await this.runPipeline(entity, meta, pipeline);
|
|
61
|
-
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
const cursor = this.buildFindCursor(entity, q, opts);
|
|
65
|
-
documents = await this.execute(() => cursor.toArray());
|
|
66
|
-
documents = this.dialect.normalizeIds(meta, documents) || [];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return documents;
|
|
35
|
+
const cursor = this.readCursor(entity, q, opts);
|
|
36
|
+
return this.dialect.normalizeIds(getMeta(entity), await this.execute(() => cursor.toArray()));
|
|
70
37
|
});
|
|
71
38
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
|
|
74
|
-
* one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
|
|
75
|
-
* -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
|
|
76
|
-
* rather than N concurrent ones.
|
|
77
|
-
*
|
|
78
|
-
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
79
|
-
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
80
|
-
* [The design](../../../../architecture/populate-limits.md).
|
|
81
|
-
*/
|
|
82
|
-
async internalFindManyPerParent(entity, q, { joins, parents }) {
|
|
83
|
-
const queries = parents.map((parent) => queryChildrenOf(q, joins, parent));
|
|
84
|
-
// A vector sort is not a degraded fallback here, it is the only expressible form: `$vectorSearch`
|
|
85
|
-
// has to be the first stage of a pipeline, so it cannot be one of N `$unionWith` branches. Read a
|
|
86
|
-
// parent at a time it stays correct, because `buildVectorSearchStage` passes the query's `$where`
|
|
87
|
-
// - which carries this parent's key - into the search as its filter, so each parent gets its own
|
|
88
|
-
// nearest rather than a share of the collection's.
|
|
89
|
-
if (this.dialect.extractVectorSort(q.$sort)) {
|
|
90
|
-
return this.readEachInTurn(entity, queries);
|
|
91
|
-
}
|
|
92
|
-
const pipelines = queries.map((it) => this.dialect.aggregationPipeline(entity, it));
|
|
93
|
-
// Counted, not estimated: the leading branch's own length grows with every `$lookup` a populate
|
|
94
|
-
// adds, so a fixed parent budget would let a richer query overflow at the server instead.
|
|
95
|
-
const stages = (pipelines[0]?.length ?? 0) + pipelines.length - 1;
|
|
96
|
-
return stages > MAX_PIPELINE_STAGES
|
|
97
|
-
? this.readEachInTurn(entity, queries)
|
|
98
|
-
: this.readInOnePipeline(entity, q, pipelines);
|
|
99
|
-
}
|
|
100
|
-
/** Every parent's page as one `$unionWith` pipeline. */
|
|
101
|
-
async readInOnePipeline(entity, q, pipelines) {
|
|
102
|
-
const meta = getMeta(entity);
|
|
103
|
-
const [first, ...rest] = pipelines;
|
|
104
|
-
const documents = await this.runPipeline(entity, meta, [
|
|
105
|
-
...first,
|
|
106
|
-
...rest.map((pipeline) => ({ $unionWith: { coll: meta.name, pipeline } })),
|
|
107
|
-
]);
|
|
108
|
-
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
109
|
-
return documents;
|
|
110
|
-
}
|
|
111
|
-
/** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
|
|
112
|
-
async readEachInTurn(entity, queries) {
|
|
113
|
-
const documents = [];
|
|
114
|
-
for (const query of queries) {
|
|
115
|
-
documents.push(...(await this.internalFindMany(entity, query)));
|
|
116
|
-
}
|
|
117
|
-
return documents;
|
|
118
|
-
}
|
|
119
39
|
async *internalFindManyStream(entity, q, opts) {
|
|
120
40
|
if (asksForNoRows(q)) {
|
|
121
41
|
return;
|
|
122
42
|
}
|
|
123
43
|
const meta = getMeta(entity);
|
|
124
|
-
const
|
|
125
|
-
if (joinableKeys.length || toManyKeys.length) {
|
|
126
|
-
const parts = [];
|
|
127
|
-
if (joinableKeys.length)
|
|
128
|
-
parts.push(`joinable: ${joinableKeys.join(', ')}`);
|
|
129
|
-
if (toManyKeys.length)
|
|
130
|
-
parts.push(`toMany: ${toManyKeys.join(', ')}`);
|
|
131
|
-
throw new TypeError(`findManyStream does not load relations on MongoDB (${parts.join('; ')}). Use findMany with $populate (or legacy relation keys in $select) so aggregation and fill logic can run.`);
|
|
132
|
-
}
|
|
133
|
-
// An ordering that names a relation reads a field only a `$lookup` produces, and a stream has no
|
|
134
|
-
// pipeline to add one: MongoDB ranks every document equal and hands back natural order. `findMany`
|
|
135
|
-
// takes the aggregation path for exactly this case, so it is the one that can honor the clause.
|
|
136
|
-
if (this.dialect.sortsRelations(entity, q.$sort)) {
|
|
137
|
-
throw new TypeError('findManyStream does not order by a relation on MongoDB. Use findMany, whose aggregation pipeline adds the $lookup the ordering reads.');
|
|
138
|
-
}
|
|
139
|
-
const cursor = this.buildFindCursor(entity, q, opts);
|
|
44
|
+
const cursor = this.readCursor(entity, q, opts);
|
|
140
45
|
try {
|
|
141
46
|
for await (const doc of cursor) {
|
|
142
|
-
const [normalized] = this.dialect.normalizeIds(meta, [doc])
|
|
47
|
+
const [normalized] = this.dialect.normalizeIds(meta, [doc]);
|
|
143
48
|
yield normalized;
|
|
144
49
|
}
|
|
145
50
|
}
|
|
@@ -147,6 +52,32 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
147
52
|
throw enrichError(err, this.logger, 'internalFindManyStream');
|
|
148
53
|
}
|
|
149
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The cursor a read runs on: the aggregation pipeline for a clause only a stage can express - a
|
|
57
|
+
* lookup, a grouping, a vector search - and the plain `find` cursor for everything else. One routing
|
|
58
|
+
* for a read and a stream alike, so both load the same relations.
|
|
59
|
+
*/
|
|
60
|
+
readCursor(entity, q, opts) {
|
|
61
|
+
this.dialect.assertNoLock(q);
|
|
62
|
+
const vectorSort = this.dialect.extractVectorSort(q.$sort);
|
|
63
|
+
const pipeline = vectorSort
|
|
64
|
+
? this.buildVectorPipeline(entity, q, vectorSort, opts)
|
|
65
|
+
: this.readsThroughPipeline(entity, q) && this.dialect.aggregationPipeline(entity, q, opts);
|
|
66
|
+
return pipeline
|
|
67
|
+
? this.collection(entity).aggregate(pipeline, { session: this.session })
|
|
68
|
+
: this.buildFindCursor(entity, q, opts);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
72
|
+
* or order by a relation, and the grouping `$distinct` is.
|
|
73
|
+
*/
|
|
74
|
+
readsThroughPipeline(entity, q) {
|
|
75
|
+
return (!!q.$distinct ||
|
|
76
|
+
hasKeys(q.$count) ||
|
|
77
|
+
populatesRelations(getMeta(entity), q.$populate) ||
|
|
78
|
+
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
79
|
+
this.dialect.sortsRelations(entity, q.$sort));
|
|
80
|
+
}
|
|
150
81
|
buildScalarProjection(entity, q) {
|
|
151
82
|
return this.dialect.select(entity, q.$select, q.$exclude);
|
|
152
83
|
}
|
|
@@ -174,11 +105,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
174
105
|
}
|
|
175
106
|
return cursor;
|
|
176
107
|
}
|
|
177
|
-
/** Execute an aggregation pipeline and normalize `_id` → `id`. */
|
|
178
|
-
async runPipeline(entity, meta, pipeline) {
|
|
179
|
-
const documents = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
180
|
-
return this.dialect.normalizeIds(meta, documents) || [];
|
|
181
|
-
}
|
|
182
108
|
/**
|
|
183
109
|
* Build an aggregation pipeline for vector similarity search.
|
|
184
110
|
* `$vectorSearch` is always the first stage; `$where` is merged into its `filter`.
|
|
@@ -191,7 +117,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
191
117
|
// that follow treat it like any other - and a query with no projection keeps its own columns.
|
|
192
118
|
...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
|
|
193
119
|
// `$vectorSearch` has already applied `$limit`, so the pager is its own.
|
|
194
|
-
...this.dialect.readStages(entity, q,
|
|
120
|
+
...this.dialect.readStages(entity, q, {
|
|
195
121
|
sort: this.dialect.sort(entity, vectorSort.regularSort, q.$populate),
|
|
196
122
|
project: scoreAlias ? { [scoreAlias]: 1 } : undefined,
|
|
197
123
|
}),
|
|
@@ -257,7 +183,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
257
183
|
const founds = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
258
184
|
// `normalizeIds` has already spread a compound `_id` back into its columns, so the settled rows
|
|
259
185
|
// are named the same way every other driver names them.
|
|
260
|
-
return
|
|
186
|
+
return this.dialect.normalizeIds(meta, founds).map((found) => idOf(meta, found));
|
|
261
187
|
}
|
|
262
188
|
async internalInsertMany(entity, rows) {
|
|
263
189
|
return this.timed('internalInsertMany', undefined, async () => {
|
|
@@ -356,7 +282,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
356
282
|
};
|
|
357
283
|
});
|
|
358
284
|
const res = await this.execute((session) => this.collection(entity).bulkWrite(operations, { session }));
|
|
359
|
-
const changes =
|
|
285
|
+
const changes = res.upsertedCount + res.modifiedCount;
|
|
360
286
|
// `upsertedIds` names only the documents inserted, keyed by operation index, so each lands on
|
|
361
287
|
// its own row; an updated document's `_id` is read back by the conflict fields instead.
|
|
362
288
|
const reported = payload.map((_, index) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
1
2
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
2
3
|
import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
4
|
/**
|
|
@@ -65,6 +66,21 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
65
66
|
* with room for the sign and the point.
|
|
66
67
|
*/
|
|
67
68
|
protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
|
|
69
|
+
/**
|
|
70
|
+
* The rows read as they are, `FOR JSON PATH` making the array. It nests a dotted key and leaves a
|
|
71
|
+
* null out, which is what unflattening a row with a joined column does, so a row with none keeps its
|
|
72
|
+
* nulls. `JSON_QUERY` keeps the array JSON inside a parent's own `FOR JSON`.
|
|
73
|
+
*/
|
|
74
|
+
protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
|
|
75
|
+
/**
|
|
76
|
+
* What JSON would round or misread crosses it as text: a number exactly, style 3 keeping a float's
|
|
77
|
+
* 17 digits, bytes as hex, and a date in UTC with its offset, which is how `tedious` reads one.
|
|
78
|
+
*/
|
|
79
|
+
protected readonly carriedFields: {
|
|
80
|
+
numeric: (expr: string) => string;
|
|
81
|
+
blob: (expr: string) => string;
|
|
82
|
+
date: (expr: string) => string;
|
|
83
|
+
};
|
|
68
84
|
/** Named parameters, which `tedious` binds by name rather than by position. */
|
|
69
85
|
placeholder(index: number): string;
|
|
70
86
|
/** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relationTermKey } from '../dialect/abstractSqlDialect.js';
|
|
2
|
+
import { COUNT_ALIAS, JSON_ELEM_ALIAS } from '../dialect/aliases.js';
|
|
3
|
+
import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
2
4
|
import { jsonPath } from '../dialect/jsonSql.js';
|
|
3
5
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
4
6
|
import { getMeta } from '../entity/index.js';
|
|
@@ -127,6 +129,26 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
127
129
|
const exactDecimal = field.type === String && fieldOptionsToCanonical(field).category === 'decimal';
|
|
128
130
|
return exactDecimal ? `CONVERT(NVARCHAR(41), ${escapedColumn})` : escapedColumn;
|
|
129
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The rows read as they are, `FOR JSON PATH` making the array. It nests a dotted key and leaves a
|
|
134
|
+
* null out, which is what unflattening a row with a joined column does, so a row with none keeps its
|
|
135
|
+
* nulls. `JSON_QUERY` keeps the array JSON inside a parent's own `FOR JSON`.
|
|
136
|
+
*/
|
|
137
|
+
appendRelationArray(ctx, rows) {
|
|
138
|
+
const rowsCtx = ctx.createFragment();
|
|
139
|
+
const { terms } = this.read(rowsCtx, rows.entity, rows.query, { alias: rows.alias, json: true }, rows.joins);
|
|
140
|
+
const nulls = terms.some((term) => relationTermKey(term).includes('.')) ? '' : ', INCLUDE_NULL_VALUES';
|
|
141
|
+
ctx.append(`JSON_QUERY(COALESCE((${rowsCtx.sql} FOR JSON PATH${nulls}), '[]'))`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* What JSON would round or misread crosses it as text: a number exactly, style 3 keeping a float's
|
|
145
|
+
* 17 digits, bytes as hex, and a date in UTC with its offset, which is how `tedious` reads one.
|
|
146
|
+
*/
|
|
147
|
+
carriedFields = {
|
|
148
|
+
numeric: (expr) => `CONVERT(VARCHAR(40), ${expr}, 3)`,
|
|
149
|
+
blob: (expr) => `${this.escape(BYTES_PREFIX)} + CONVERT(VARCHAR(MAX), ${expr}, 2)`,
|
|
150
|
+
date: (expr) => `CONVERT(VARCHAR(33), CAST(${expr} AS DATETIMEOFFSET), 127)`,
|
|
151
|
+
};
|
|
130
152
|
/** Named parameters, which `tedious` binds by name rather than by position. */
|
|
131
153
|
placeholder(index) {
|
|
132
154
|
return `@p${index}`;
|
|
@@ -293,12 +315,12 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
293
315
|
: `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
|
|
294
316
|
}
|
|
295
317
|
jsonAll(ctx, jsonField, value) {
|
|
296
|
-
const alias = ctx.
|
|
318
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
297
319
|
const conditions = value.map((val) => `EXISTS (SELECT 1 FROM OPENJSON(${jsonField}) ${alias} WHERE ${alias}.${this.#elem.value} = ${this.jsonScalarParam(ctx, val)})`);
|
|
298
320
|
return `(${conditions.join(' AND ')})`;
|
|
299
321
|
}
|
|
300
322
|
jsonSize(ctx, jsonField, value) {
|
|
301
|
-
const alias = ctx.
|
|
323
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
302
324
|
return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
|
|
303
325
|
}
|
|
304
326
|
/** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
|
|
@@ -323,7 +345,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
323
345
|
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
|
|
324
346
|
*/
|
|
325
347
|
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
326
|
-
const alias = ctx.
|
|
348
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
327
349
|
const val = `${alias}.${this.#elem.value}`;
|
|
328
350
|
// `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
|
|
329
351
|
// re-encoded from its reported `type` before the array is put back together - concatenated raw,
|
|
@@ -6,4 +6,6 @@ export declare class MySqlDialect extends MysqlLikeSqlDialect {
|
|
|
6
6
|
* "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
|
|
7
7
|
*/
|
|
8
8
|
protected readonly upsertNewRowAlias = "_uql_new";
|
|
9
|
+
/** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
|
|
10
|
+
protected applySettings(sql: string, settings: readonly string[]): string;
|
|
9
11
|
}
|
|
@@ -7,4 +7,8 @@ export class MySqlDialect extends MysqlLikeSqlDialect {
|
|
|
7
7
|
* "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
|
|
8
8
|
*/
|
|
9
9
|
upsertNewRowAlias = UPSERT_NEW_ROW_ALIAS;
|
|
10
|
+
/** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
|
|
11
|
+
applySettings(sql, settings) {
|
|
12
|
+
return sql.replace('SELECT ', `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
|
|
13
|
+
}
|
|
10
14
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage,
|
|
2
|
-
import { LoggerWrapper
|
|
1
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
2
|
+
import { LoggerWrapper } from '../util/index.js';
|
|
3
3
|
/** Base class for all database queriers. */
|
|
4
4
|
export declare abstract class AbstractQuerier implements Querier {
|
|
5
5
|
readonly extra?: ExtraOptions | undefined;
|
|
@@ -44,31 +44,20 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
44
44
|
$entity: Type<E>;
|
|
45
45
|
}, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
46
46
|
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
47
|
-
protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
48
47
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* **SQL:** Joinable relations (e.g. m1 / one-to-one) are still emitted in the streamed SQL; **to-many**
|
|
53
|
-
* relations are not filled (no second query) - requesting them throws a clear `TypeError`.
|
|
54
|
-
*
|
|
55
|
-
* **MongoDB:** Relation loading uses aggregation + follow-up queries in `findMany`; **streams use a plain
|
|
56
|
-
* find cursor**, so any requested relation keys in `$select` / `$populate` throw a `TypeError`.
|
|
57
|
-
*
|
|
58
|
-
* No `afterLoad` hooks on streamed rows.
|
|
48
|
+
* The rows matching `q`, each populated relation and `$count` read with them in the same statement
|
|
49
|
+
* or pipeline. [The design](../../../../architecture/relations-in-one-statement.md).
|
|
59
50
|
*/
|
|
60
|
-
|
|
61
|
-
$entity: Type<E>;
|
|
62
|
-
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
63
|
-
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
51
|
+
protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
64
52
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Abstract rather than defaulted: a default would be N queries, which is the N+1 that batched
|
|
69
|
-
* population exists to prevent, and it would be invisible to whichever backend forgot to override.
|
|
53
|
+
* Stream records as an async iterable, in both the entity-as-argument and entity-as-field patterns.
|
|
54
|
+
* Each row streams with its populated relations and counts, read by the same statement or pipeline
|
|
55
|
+
* `findMany` runs. No `afterLoad` hooks on streamed rows.
|
|
70
56
|
*/
|
|
71
|
-
|
|
57
|
+
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P, C> & {
|
|
58
|
+
$entity: Type<E>;
|
|
59
|
+
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
|
|
60
|
+
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
|
|
72
61
|
protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
|
|
73
62
|
/**
|
|
74
63
|
* Find multiple records and return both the records and total count.
|
|
@@ -157,19 +146,6 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
157
146
|
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
158
147
|
*/
|
|
159
148
|
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
160
|
-
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
161
|
-
private fillToManyThroughRelation;
|
|
162
|
-
private fillToManyOneToMany;
|
|
163
|
-
/**
|
|
164
|
-
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
165
|
-
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
166
|
-
* list, which is both correct and cheaper.
|
|
167
|
-
*
|
|
168
|
-
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
169
|
-
* what they query, never in how the page is spread over its parents.
|
|
170
|
-
*/
|
|
171
|
-
private findChildrenOf;
|
|
172
|
-
protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
|
|
173
149
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
174
150
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
175
151
|
/**
|
|
@@ -207,6 +183,14 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
207
183
|
transaction<T>(callback: () => Promise<T>, opts?: TransactionOptions): Promise<T>;
|
|
208
184
|
/** Whether anything at all - a global listener or the entity itself - handles `event`. */
|
|
209
185
|
private hasHook;
|
|
186
|
+
/** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
|
|
187
|
+
private listensForLoad;
|
|
188
|
+
/**
|
|
189
|
+
* `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
|
|
190
|
+
* parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
|
|
191
|
+
* listens.
|
|
192
|
+
*/
|
|
193
|
+
private emitLoaded;
|
|
210
194
|
/**
|
|
211
195
|
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
212
196
|
* not report them in payload order. A row that no read row matches, or that two do, keeps
|