uql-orm 0.53.0 → 0.54.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/dist/browser/uql-browser.min.js.map +1 -1
- package/dist/cockroachdb/cockroachDialect.d.ts +2 -5
- package/dist/cockroachdb/cockroachDialect.js +2 -5
- package/dist/dialect/abstractSqlDialect.d.ts +6 -0
- package/dist/dialect/abstractSqlDialect.js +9 -3
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
- package/dist/entity/decorator/members.d.ts +3 -10
- package/dist/entity/metadata/definition.js +0 -4
- package/dist/migrate/builder/migrationBuilder.js +0 -4
- package/dist/migrate/migrator.d.ts +3 -7
- package/dist/migrate/migrator.js +3 -7
- package/dist/migrate/schemaGenerator.d.ts +0 -13
- package/dist/migrate/schemaGenerator.js +0 -13
- package/dist/migrate/storage/databaseStorage.d.ts +2 -2
- package/dist/migrate/storage/databaseStorage.js +2 -2
- package/dist/mongo/mongodbQuerier.d.ts +3 -5
- package/dist/mongo/mongodbQuerier.js +18 -19
- package/dist/mssql/mssqlDialect.d.ts +2 -0
- package/dist/mssql/mssqlDialect.js +2 -0
- package/dist/querier/abstractQuerier.d.ts +17 -22
- package/dist/querier/abstractQuerier.js +80 -57
- package/dist/querier/abstractSqlQuerier.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.js +68 -84
- package/dist/schema/schemaASTBuilder.js +7 -7
- package/dist/type/entity.d.ts +6 -11
- package/dist/type/migration.d.ts +0 -3
- package/dist/type/query.d.ts +12 -12
- package/dist/type/query.js +0 -6
- package/dist/type/universalQuerier.d.ts +3 -3
- package/dist/util/dialect.util.d.ts +3 -2
- package/dist/util/dialect.util.js +1 -0
- package/dist/util/field.util.d.ts +4 -16
- package/dist/util/field.util.js +6 -19
- package/dist/util/fieldOption.util.d.ts +1 -4
- package/dist/util/fieldOption.util.js +0 -2
- package/dist/util/logger.d.ts +3 -3
- package/dist/util/logger.js +3 -0
- package/dist/util/raw.d.ts +3 -10
- package/dist/util/sql.util.js +2 -2
- package/package.json +1 -1
|
@@ -64,6 +64,8 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
64
64
|
maxBindValues = 2100;
|
|
65
65
|
/** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
|
|
66
66
|
returningPosition = 'after-target';
|
|
67
|
+
/** Microsoft documents no row order for a `MERGE ... OUTPUT`. */
|
|
68
|
+
upsertReturningOrdered = false;
|
|
67
69
|
insertIdSource = 'returning';
|
|
68
70
|
/** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
|
|
69
71
|
mergeTargetHint = ' WITH (HOLDLOCK)';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
1
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
2
2
|
import { LoggerWrapper, type ParentJoin, type ParentPartition } from '../util/index.js';
|
|
3
|
+
/** Base class for all database queriers. */
|
|
3
4
|
export declare abstract class AbstractQuerier implements Querier {
|
|
4
5
|
readonly extra?: ExtraOptions | undefined;
|
|
5
6
|
/**
|
|
@@ -109,11 +110,12 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
109
110
|
abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
110
111
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
|
|
111
112
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
113
|
+
* The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
|
|
114
|
+
* same rows the statement wrote.
|
|
114
115
|
*/
|
|
115
116
|
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
116
|
-
|
|
117
|
+
/** Writes `rows`, and onto each one the key the database generated for it, where it can tell. */
|
|
118
|
+
protected abstract internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
|
|
117
119
|
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
118
120
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
119
121
|
protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
@@ -150,20 +152,9 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
150
152
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
151
153
|
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
|
|
152
154
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* Whether a row names its key decides which statement it takes, never whether the row exists: an
|
|
157
|
-
* id the caller invented is not proof of anything, and a stale one used to issue an `UPDATE` that
|
|
158
|
-
* matched nothing and reported success. A named row upserts on its own key, so it is written
|
|
159
|
-
* either way and no read can go stale between deciding and writing. An unnamed one inserts, and
|
|
160
|
-
* the database assigns the key.
|
|
161
|
-
*
|
|
162
|
-
* A composite key is always supplied by the caller, so it always takes the upsert branch - which
|
|
163
|
-
* is why nothing here special-cases one, and why this is the method that stopped refusing them.
|
|
164
|
-
*
|
|
165
|
-
* The hooks follow the statement: a named row fires `beforeUpsert`/`afterUpsert`, never the
|
|
166
|
-
* update pair, because the database picks the branch as the statement runs.
|
|
155
|
+
* Whether a row names its key decides its statement, never whether the row exists: a named row
|
|
156
|
+
* upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
|
|
157
|
+
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
167
158
|
*/
|
|
168
159
|
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
169
160
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
@@ -217,14 +208,18 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
217
208
|
/** Whether anything at all - a global listener or the entity itself - handles `event`. */
|
|
218
209
|
private hasHook;
|
|
219
210
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
211
|
+
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
212
|
+
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
213
|
+
* `undefined`: a missing id is honest where a guessed one is not.
|
|
222
214
|
*/
|
|
215
|
+
protected idsByConflict<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, rows: EntityData<E>[]): Promise<(PrimaryKey | undefined)[]>;
|
|
223
216
|
/**
|
|
224
|
-
* Runs `write` between the event's `before`/`after` pair.
|
|
225
|
-
*
|
|
217
|
+
* Runs `write` between the event's `before`/`after` pair. The before hooks get the caller's rows, so
|
|
218
|
+
* what they assign is written; `write` and the after hooks get a copy, which by then carries what
|
|
219
|
+
* the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
|
|
226
220
|
*/
|
|
227
221
|
private hooked;
|
|
222
|
+
/** Fires the global listeners first, then the entity's own hooks. */
|
|
228
223
|
private emitHook;
|
|
229
224
|
/**
|
|
230
225
|
* Runs `task` after everything already queued on this querier, one at a time.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, childrenOf, clone, dataKeyed, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
2
|
+
import { asSelectMap, childrenOf, clone, dataKeyed, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -41,22 +41,27 @@ function soleParentColumn(relOpts) {
|
|
|
41
41
|
return parentJoins(relOpts, 1)[0].joined;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* The id each written row is named by, in payload order. Read off the rows as written, so a key the
|
|
45
|
+
* database generated or the ORM filled is there, and a composite is named by every column of it.
|
|
46
46
|
*/
|
|
47
|
+
function writtenIds(meta, rows) {
|
|
48
|
+
return rows.map((row) => (namesKey(meta, row) ? idOf(meta, row) : undefined));
|
|
49
|
+
}
|
|
47
50
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* statement reports one. A sole key takes what the statement reported when it spoke for every row,
|
|
52
|
-
* and otherwise falls back to the key the caller supplied - a `firstId` dialect reports nothing for
|
|
53
|
-
* a batch, which is not the same as those rows having no id.
|
|
51
|
+
* Writes the key an upsert reported onto each row that named none - only the key, since an `onInsert`
|
|
52
|
+
* value was never written to a row the upsert updated. A report aligns with the rows only when it
|
|
53
|
+
* speaks for every one: a `firstId` dialect reports nothing for a batch.
|
|
54
54
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
function adoptReportedIds(meta, rows, reported) {
|
|
56
|
+
if (meta.ids.length !== 1 || reported?.length !== rows.length) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const [idKey] = meta.ids;
|
|
60
|
+
for (let index = 0; index < rows.length; index++) {
|
|
61
|
+
rows[index][idKey] ??= reported[index];
|
|
62
|
+
}
|
|
59
63
|
}
|
|
64
|
+
/** Base class for all database queriers. */
|
|
60
65
|
export class AbstractQuerier {
|
|
61
66
|
extra;
|
|
62
67
|
/**
|
|
@@ -182,15 +187,18 @@ export class AbstractQuerier {
|
|
|
182
187
|
return id;
|
|
183
188
|
}
|
|
184
189
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
190
|
+
* The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
|
|
191
|
+
* same rows the statement wrote.
|
|
187
192
|
*/
|
|
188
193
|
async insertMany(entity, payload) {
|
|
194
|
+
if (!payload?.length) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
189
197
|
const meta = getMeta(entity);
|
|
190
|
-
return this.hooked(entity, 'Insert', payload, async () => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return
|
|
198
|
+
return this.hooked(entity, 'Insert', payload, async (rows) => {
|
|
199
|
+
fillOnFields(meta, rows, 'onInsert');
|
|
200
|
+
await this.internalInsertMany(entity, rows);
|
|
201
|
+
return writtenIds(meta, rows);
|
|
194
202
|
});
|
|
195
203
|
}
|
|
196
204
|
async updateOneById(entity, id, payload, opts) {
|
|
@@ -198,7 +206,10 @@ export class AbstractQuerier {
|
|
|
198
206
|
return this.updateMany(entity, { $where: whereIds(getMeta(entity), id) }, payload, opts);
|
|
199
207
|
}
|
|
200
208
|
async updateMany(entity, q, payload, opts) {
|
|
201
|
-
return this.hooked(entity, 'Update', [payload], () =>
|
|
209
|
+
return this.hooked(entity, 'Update', [payload], ([row]) => {
|
|
210
|
+
fillOnFields(getMeta(entity), [row], 'onUpdate');
|
|
211
|
+
return this.internalUpdateMany(entity, q, row, opts);
|
|
212
|
+
});
|
|
202
213
|
}
|
|
203
214
|
async restoreOneById(entity, id) {
|
|
204
215
|
assertIdValue(entity, id);
|
|
@@ -221,16 +232,20 @@ export class AbstractQuerier {
|
|
|
221
232
|
* was how an `@Id({ onInsert })` or an audit trail silently skipped this path.
|
|
222
233
|
*/
|
|
223
234
|
async upsertOne(entity, conflictPaths, payload) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
235
|
+
const meta = getMeta(entity);
|
|
236
|
+
return this.hooked(entity, 'Upsert', [payload], async (rows) => {
|
|
237
|
+
const { ids, changes, created } = await this.internalUpsertOne(entity, conflictPaths, rows[0]);
|
|
238
|
+
adoptReportedIds(meta, rows, ids);
|
|
239
|
+
const [id] = writtenIds(meta, rows);
|
|
227
240
|
return { id, changes, created };
|
|
228
241
|
});
|
|
229
242
|
}
|
|
230
243
|
async upsertMany(entity, conflictPaths, payload) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
const meta = getMeta(entity);
|
|
245
|
+
return this.hooked(entity, 'Upsert', payload, async (rows) => {
|
|
246
|
+
const { ids, changes } = await this.internalUpsertMany(entity, conflictPaths, rows);
|
|
247
|
+
adoptReportedIds(meta, rows, ids);
|
|
248
|
+
return { ids: writtenIds(meta, rows), changes };
|
|
234
249
|
});
|
|
235
250
|
}
|
|
236
251
|
async deleteOneById(entity, id, opts) {
|
|
@@ -278,20 +293,9 @@ export class AbstractQuerier {
|
|
|
278
293
|
return id;
|
|
279
294
|
}
|
|
280
295
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* Whether a row names its key decides which statement it takes, never whether the row exists: an
|
|
285
|
-
* id the caller invented is not proof of anything, and a stale one used to issue an `UPDATE` that
|
|
286
|
-
* matched nothing and reported success. A named row upserts on its own key, so it is written
|
|
287
|
-
* either way and no read can go stale between deciding and writing. An unnamed one inserts, and
|
|
288
|
-
* the database assigns the key.
|
|
289
|
-
*
|
|
290
|
-
* A composite key is always supplied by the caller, so it always takes the upsert branch - which
|
|
291
|
-
* is why nothing here special-cases one, and why this is the method that stopped refusing them.
|
|
292
|
-
*
|
|
293
|
-
* The hooks follow the statement: a named row fires `beforeUpsert`/`afterUpsert`, never the
|
|
294
|
-
* update pair, because the database picks the branch as the statement runs.
|
|
296
|
+
* Whether a row names its key decides its statement, never whether the row exists: a named row
|
|
297
|
+
* upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
|
|
298
|
+
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
295
299
|
*/
|
|
296
300
|
async saveMany(entity, payload) {
|
|
297
301
|
const meta = getMeta(entity);
|
|
@@ -320,15 +324,15 @@ export class AbstractQuerier {
|
|
|
320
324
|
const write = async () => {
|
|
321
325
|
if (toInsert.length) {
|
|
322
326
|
const inserted = await this.insertMany(entity, toInsert.map((index) => payload[index]));
|
|
323
|
-
toInsert.
|
|
324
|
-
ids[
|
|
325
|
-
}
|
|
327
|
+
for (let position = 0; position < toInsert.length; position++) {
|
|
328
|
+
ids[toInsert[position]] = inserted[position];
|
|
329
|
+
}
|
|
326
330
|
}
|
|
327
331
|
if (toUpsert.length) {
|
|
328
332
|
const conflictPaths = Object.fromEntries(meta.ids.map((key) => [key, true]));
|
|
329
|
-
await this.upsertMany(entity, conflictPaths, toUpsert.map((index) => payload[index]));
|
|
330
|
-
for (
|
|
331
|
-
ids[
|
|
333
|
+
const { ids: upserted } = await this.upsertMany(entity, conflictPaths, toUpsert.map((index) => payload[index]));
|
|
334
|
+
for (let position = 0; position < toUpsert.length; position++) {
|
|
335
|
+
ids[toUpsert[position]] = upserted[position];
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
338
|
};
|
|
@@ -519,16 +523,15 @@ export class AbstractQuerier {
|
|
|
519
523
|
// unless this has run - and this method is `protected`, so a caller can arrive without them.
|
|
520
524
|
assertSoleId(meta, 'saving a relation');
|
|
521
525
|
const relEntity = relOpts.entity();
|
|
522
|
-
const relPayload = relValue;
|
|
523
526
|
switch (relOpts.cardinality) {
|
|
524
527
|
case '1m':
|
|
525
528
|
case 'mm':
|
|
526
|
-
return this.saveToMany(relOpts, relEntity, ids,
|
|
529
|
+
return this.saveToMany(relOpts, relEntity, ids, relValue, isUpdate);
|
|
527
530
|
case '11':
|
|
528
|
-
return this.saveOneToOne(relEntity, relOpts, ids,
|
|
531
|
+
return this.saveOneToOne(relEntity, relOpts, ids, relValue, isUpdate);
|
|
529
532
|
case 'm1':
|
|
530
|
-
if (
|
|
531
|
-
return this.saveManyToOne(entity, relEntity, relOpts, ids,
|
|
533
|
+
if (relValue)
|
|
534
|
+
return this.saveManyToOne(entity, relEntity, relOpts, ids, relValue);
|
|
532
535
|
}
|
|
533
536
|
}
|
|
534
537
|
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
@@ -628,19 +631,39 @@ export class AbstractQuerier {
|
|
|
628
631
|
return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
|
|
629
632
|
}
|
|
630
633
|
/**
|
|
631
|
-
*
|
|
632
|
-
*
|
|
634
|
+
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
635
|
+
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
636
|
+
* `undefined`: a missing id is honest where a guessed one is not.
|
|
633
637
|
*/
|
|
638
|
+
async idsByConflict(entity, conflictPaths, rows) {
|
|
639
|
+
const meta = getMeta(entity);
|
|
640
|
+
const [idKey] = meta.ids;
|
|
641
|
+
const keys = getKeys(conflictPaths);
|
|
642
|
+
const q = {
|
|
643
|
+
$select: Object.fromEntries([idKey, ...keys].map((key) => [key, true])),
|
|
644
|
+
$where: { $or: rows.map((row) => Object.fromEntries(keys.map((key) => [key, row[key]]))) },
|
|
645
|
+
};
|
|
646
|
+
const found = await this.internalFindMany(entity, q, { filters: withoutSoftDeleteFilter(undefined) });
|
|
647
|
+
const byConflict = new Map();
|
|
648
|
+
for (const row of found) {
|
|
649
|
+
const key = rowKey(row, keys);
|
|
650
|
+
byConflict.set(key, byConflict.has(key) ? undefined : row[idKey]);
|
|
651
|
+
}
|
|
652
|
+
return rows.map((row) => byConflict.get(rowKey(row, keys)));
|
|
653
|
+
}
|
|
634
654
|
/**
|
|
635
|
-
* Runs `write` between the event's `before`/`after` pair.
|
|
636
|
-
*
|
|
655
|
+
* Runs `write` between the event's `before`/`after` pair. The before hooks get the caller's rows, so
|
|
656
|
+
* what they assign is written; `write` and the after hooks get a copy, which by then carries what
|
|
657
|
+
* the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
|
|
637
658
|
*/
|
|
638
659
|
async hooked(entity, event, payloads, write) {
|
|
639
660
|
await this.emitHook(entity, `before${event}`, payloads);
|
|
640
|
-
const
|
|
641
|
-
await
|
|
661
|
+
const rows = clone(payloads);
|
|
662
|
+
const result = await write(rows);
|
|
663
|
+
await this.emitHook(entity, `after${event}`, rows);
|
|
642
664
|
return result;
|
|
643
665
|
}
|
|
666
|
+
/** Fires the global listeners first, then the entity's own hooks. */
|
|
644
667
|
async emitHook(entity, event, payloads) {
|
|
645
668
|
if (!this.hasHook(entity, event))
|
|
646
669
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, ExtraOptions,
|
|
2
|
+
import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
import { type ParentPartition } from '../util/index.js';
|
|
4
4
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
5
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
@@ -71,6 +71,13 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
71
71
|
* query, so neither can reach here.
|
|
72
72
|
*/
|
|
73
73
|
protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
74
|
+
/**
|
|
75
|
+
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
76
|
+
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
77
|
+
* before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
|
|
78
|
+
* pair outright. Both then cost a second statement; only the counting differs.
|
|
79
|
+
*/
|
|
80
|
+
private countedSeparately;
|
|
74
81
|
/**
|
|
75
82
|
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
76
83
|
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
@@ -82,13 +89,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
82
89
|
* answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
|
|
83
90
|
* too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
|
|
84
91
|
*/
|
|
85
|
-
/**
|
|
86
|
-
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
87
|
-
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
88
|
-
* before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
|
|
89
|
-
* pair outright. Both then cost a second statement; only the counting differs.
|
|
90
|
-
*/
|
|
91
|
-
private countedSeparately;
|
|
92
92
|
protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
93
93
|
private selectRows;
|
|
94
94
|
private hydrateRows;
|
|
@@ -119,7 +119,7 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
119
119
|
protected internalCount<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
120
120
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
121
121
|
protected internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
122
|
-
internalInsertMany<E extends object>(entity: Type<E>,
|
|
122
|
+
internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
|
|
123
123
|
internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
124
124
|
/** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
|
|
125
125
|
private settleIds;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
3
|
-
import { getMeta, idOf,
|
|
3
|
+
import { getMeta, idOf, namesKey } from '../entity/index.js';
|
|
4
4
|
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
6
|
import { enrichError } from './queryError.js';
|
|
@@ -22,50 +22,36 @@ function partitionBySuppliedId(payload, idKey) {
|
|
|
22
22
|
return supplied.length && generated.length ? [supplied, generated] : [supplied.length ? supplied : generated];
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* they all carry the same ones, which is the batch as it stands and needs no grouping at all.
|
|
25
|
+
* Row indexes grouped by the columns their rows carry, payload order kept within each group.
|
|
27
26
|
*
|
|
28
27
|
* Deliberately finer than {@link partitionBySuppliedId}: an upsert's `DO UPDATE SET` is one
|
|
29
28
|
* assignment list for the whole statement, so rows of different shapes cannot share one at all.
|
|
30
29
|
*/
|
|
31
30
|
function groupByInsertShape(meta, payload) {
|
|
32
|
-
const
|
|
33
|
-
let index =
|
|
34
|
-
|
|
35
|
-
index++;
|
|
36
|
-
}
|
|
37
|
-
if (index === payload.length) {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
const groups = new Map([[first, payload.slice(0, index)]]);
|
|
41
|
-
for (; index < payload.length; index++) {
|
|
42
|
-
const row = payload[index];
|
|
43
|
-
const shape = insertShapeOf(meta, row);
|
|
31
|
+
const groups = new Map();
|
|
32
|
+
for (let index = 0; index < payload.length; index++) {
|
|
33
|
+
const shape = insertShapeOf(meta, payload[index]);
|
|
44
34
|
const group = groups.get(shape);
|
|
45
35
|
if (group) {
|
|
46
|
-
group.push(
|
|
36
|
+
group.push(index);
|
|
47
37
|
}
|
|
48
38
|
else {
|
|
49
|
-
groups.set(shape, [
|
|
39
|
+
groups.set(shape, [index]);
|
|
50
40
|
}
|
|
51
41
|
}
|
|
52
42
|
return [...groups.values()];
|
|
53
43
|
}
|
|
54
44
|
/**
|
|
55
|
-
*
|
|
56
|
-
* parameter, so fields-per-record is a safe upper bound. Every multi-row
|
|
57
|
-
* allows 100 binds, which a couple of dozen rows reach.
|
|
45
|
+
* A group's row indexes split into statements within the dialect's bind budget, payload order kept.
|
|
46
|
+
* `DEFAULT` cells bind no parameter, so fields-per-record is a safe upper bound. Every multi-row
|
|
47
|
+
* write splits on this: D1 allows 100 binds, which a couple of dozen rows reach.
|
|
58
48
|
*/
|
|
59
|
-
function
|
|
60
|
-
const fieldsPerRecord = getInsertFieldKeys(meta,
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
/** `rows` split into statement-sized slices, payload order kept. */
|
|
64
|
-
function chunkByBindBudget(meta, rows, maxBindValues) {
|
|
65
|
-
const size = bindBudgetChunkSize(meta, rows, maxBindValues);
|
|
49
|
+
function chunkByBindBudget(meta, payload, group, maxBindValues) {
|
|
50
|
+
const fieldsPerRecord = getInsertFieldKeys(meta, group.map((index) => payload[index])).length;
|
|
51
|
+
const size = Math.max(1, Math.floor(maxBindValues / (fieldsPerRecord || 1)));
|
|
66
52
|
const chunks = [];
|
|
67
|
-
for (let start = 0; start <
|
|
68
|
-
chunks.push(
|
|
53
|
+
for (let start = 0; start < group.length; start += size) {
|
|
54
|
+
chunks.push(group.slice(start, start + size));
|
|
69
55
|
}
|
|
70
56
|
return chunks;
|
|
71
57
|
}
|
|
@@ -186,17 +172,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
186
172
|
this.dialect.findPerParent(ctx, entity, q, partition);
|
|
187
173
|
return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
|
|
188
174
|
}
|
|
189
|
-
/**
|
|
190
|
-
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
191
|
-
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
192
|
-
* the end, or a filter nothing matched.
|
|
193
|
-
*
|
|
194
|
-
* A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
|
|
195
|
-
* is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
|
|
196
|
-
* engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
|
|
197
|
-
* answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
|
|
198
|
-
* too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
|
|
199
|
-
*/
|
|
200
175
|
/**
|
|
201
176
|
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
202
177
|
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
@@ -212,6 +187,17 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
212
187
|
}
|
|
213
188
|
return undefined;
|
|
214
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
192
|
+
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
193
|
+
* the end, or a filter nothing matched.
|
|
194
|
+
*
|
|
195
|
+
* A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
|
|
196
|
+
* is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
|
|
197
|
+
* engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
|
|
198
|
+
* answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
|
|
199
|
+
* too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
|
|
200
|
+
*/
|
|
215
201
|
async internalFindManyAndCount(entity, q, opts) {
|
|
216
202
|
const separately = this.countedSeparately(entity, q, opts);
|
|
217
203
|
if (separately) {
|
|
@@ -359,24 +345,15 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
359
345
|
}
|
|
360
346
|
return res;
|
|
361
347
|
}
|
|
362
|
-
async internalInsertMany(entity,
|
|
363
|
-
if (!payload?.length) {
|
|
364
|
-
return [];
|
|
365
|
-
}
|
|
366
|
-
payload = clone(payload);
|
|
348
|
+
async internalInsertMany(entity, rows) {
|
|
367
349
|
const meta = getMeta(entity);
|
|
368
|
-
// What comes back is one column's value, so
|
|
369
|
-
//
|
|
370
|
-
// is several columns.
|
|
350
|
+
// What comes back is one column's value, so nothing is read back for a composite: its rows
|
|
351
|
+
// already carry every column of it. `sole` is what keeps every id path off such a key.
|
|
371
352
|
const [idKey] = meta.ids;
|
|
372
353
|
const sole = meta.ids.length === 1;
|
|
373
354
|
const idField = sole ? meta.fields[idKey] : undefined;
|
|
374
355
|
const generatedKey = !!idField && isAutoIncrement(idField, true);
|
|
375
|
-
const
|
|
376
|
-
for (const group of partitionBySuppliedId(payload, idKey)) {
|
|
377
|
-
// Per group, not per batch: the two carry different columns - one names the key, one does not -
|
|
378
|
-
// so a budget taken over their union would under-fill the statement that is missing one.
|
|
379
|
-
const chunkSize = bindBudgetChunkSize(meta, group.map((index) => payload[index]), this.dialect.maxBindValues);
|
|
356
|
+
for (const group of partitionBySuppliedId(rows, idKey)) {
|
|
380
357
|
// RETURNING-based ids are exact per row. Header-derived ones (LAST_INSERT_ID / lastInsertRowid
|
|
381
358
|
// arithmetic) are only sound when the key is database-generated and every row *in this
|
|
382
359
|
// statement* left it to the database. That is a property of the statement, not of the batch:
|
|
@@ -385,28 +362,26 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
385
362
|
// most one extra statement and keeps each of them inferable.
|
|
386
363
|
const idsReliable = sole &&
|
|
387
364
|
(this.dialect.insertIdSource === 'returning' ||
|
|
388
|
-
(generatedKey && group.every((index) =>
|
|
365
|
+
(generatedKey && group.every((index) => rows[index][idKey] === undefined)));
|
|
389
366
|
// Inferring multiple ids from the single header id (MySQL) assumes a known stride; a clustered
|
|
390
367
|
// server may set `auto_increment_increment` > 1, so probe it (once, cached) before inferring.
|
|
391
368
|
if (idsReliable && group.length > 1 && this.dialect.insertIdSource === 'firstId') {
|
|
392
369
|
this.#insertIdIncrement ??= await this.loadInsertIdIncrement();
|
|
393
370
|
}
|
|
394
|
-
|
|
395
|
-
|
|
371
|
+
// Per group, not per batch: the two carry different columns - one names the key, one does not -
|
|
372
|
+
// so a budget taken over their union would under-fill the statement that is missing one.
|
|
373
|
+
for (const indexes of chunkByBindBudget(meta, rows, group, this.dialect.maxBindValues)) {
|
|
396
374
|
const ctx = this.dialect.createContext();
|
|
397
|
-
this.dialect.insert(ctx, entity, indexes.map((index) =>
|
|
375
|
+
this.dialect.insert(ctx, entity, indexes.map((index) => rows[index]));
|
|
398
376
|
const { ids = [] } = await this.run(ctx.sql, ctx.values);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
it[idKey] ??= ids[position];
|
|
377
|
+
if (idsReliable) {
|
|
378
|
+
for (let position = 0; position < indexes.length; position++) {
|
|
379
|
+
rows[indexes[position]][idKey] ??= ids[position];
|
|
403
380
|
}
|
|
404
|
-
|
|
405
|
-
});
|
|
381
|
+
}
|
|
406
382
|
}
|
|
407
383
|
}
|
|
408
|
-
await this.insertRelations(entity,
|
|
409
|
-
return payloadIds;
|
|
384
|
+
await this.insertRelations(entity, rows);
|
|
410
385
|
}
|
|
411
386
|
async internalUpdateMany(entity, q, payload, opts) {
|
|
412
387
|
payload = clone(payload);
|
|
@@ -446,8 +421,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
446
421
|
// One statement per shape, each split again to stay inside the bind budget. Grouping first is
|
|
447
422
|
// what makes the budget arithmetic right: every row of a group carries the same columns, so the
|
|
448
423
|
// `DO UPDATE SET` resolves to non-binding `EXCLUDED` references rather than inlined values.
|
|
449
|
-
const
|
|
450
|
-
const statements = (groups ?? [payload]).flatMap((group) => chunkByBindBudget(meta, group, this.dialect.maxBindValues));
|
|
424
|
+
const statements = groupByInsertShape(meta, payload).flatMap((group) => chunkByBindBudget(meta, payload, group, this.dialect.maxBindValues));
|
|
451
425
|
if (statements.length === 1) {
|
|
452
426
|
return this.runUpsert(entity, conflictPaths, payload);
|
|
453
427
|
}
|
|
@@ -459,31 +433,41 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
459
433
|
// than one statement; `transaction` is re-entrant, so this is free inside a caller's own.
|
|
460
434
|
return this.transaction(async () => {
|
|
461
435
|
let changes = 0;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
436
|
+
// Placed by index, since grouping by shape reorders the rows. A statement reporting fewer ids
|
|
437
|
+
// than it wrote places none.
|
|
438
|
+
const ids = new Array(payload.length);
|
|
439
|
+
for (const indexes of statements) {
|
|
440
|
+
const { changes: written = 0, ids: reported } = await this.runUpsert(entity, conflictPaths, indexes.map((index) => payload[index]));
|
|
441
|
+
changes += written;
|
|
442
|
+
if (reported?.length === indexes.length) {
|
|
443
|
+
for (let position = 0; position < indexes.length; position++) {
|
|
444
|
+
ids[indexes[position]] = reported[position];
|
|
445
|
+
}
|
|
468
446
|
}
|
|
469
447
|
}
|
|
470
|
-
// No `created
|
|
471
|
-
return
|
|
448
|
+
// No `created`: it speaks for a single statement, and there were several.
|
|
449
|
+
return { changes, ids };
|
|
472
450
|
});
|
|
473
451
|
}
|
|
474
452
|
async runUpsert(entity, conflictPaths, payload) {
|
|
453
|
+
const meta = getMeta(entity);
|
|
454
|
+
// Asked first: the statement fills an `onInsert` key into these rows whether it inserts them or not.
|
|
455
|
+
const unnamed = meta.ids.length === 1 && payload.some((row) => !namesKey(meta, row));
|
|
475
456
|
const ctx = this.dialect.createContext();
|
|
476
457
|
this.dialect.upsert(ctx, entity, conflictPaths, payload);
|
|
477
458
|
const result = await this.run(ctx.sql, ctx.values);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
459
|
+
const ordered = payload.length === 1 || (this.dialect.insertIdSource === 'returning' && this.dialect.upsertReturningOrdered);
|
|
460
|
+
if (ordered && result.ids?.length === payload.length) {
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
// The statement's ids name its rows only in order and for every one. A MySQL batch reports a
|
|
464
|
+
// weighted count (1=insert, 2=update) instead, CockroachDB and SQL Server answer out of order, and
|
|
465
|
+
// `DO NOTHING` skips rows, so there the ids are read back by the conflict columns.
|
|
466
|
+
const { changes } = result;
|
|
467
|
+
const created = payload.length === 1 ? result.created : undefined;
|
|
468
|
+
return unnamed
|
|
469
|
+
? { changes, created, ids: await this.idsByConflict(entity, conflictPaths, payload) }
|
|
470
|
+
: { changes, created };
|
|
487
471
|
}
|
|
488
472
|
async internalDeleteMany(entity, q, opts) {
|
|
489
473
|
const meta = getMeta(entity);
|