uql-orm 0.41.1 → 0.42.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/dist/browser/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/querier/httpQuerier.js +21 -6
- package/dist/browser/type/clientQuerier.d.ts +3 -3
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +32 -3
- package/dist/dialect/abstractSqlDialect.js +58 -22
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/entity/decorator/members.d.ts +10 -2
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +18 -1
- package/dist/entity/metadata/definition.js +91 -36
- package/dist/http/handler.js +4 -4
- package/dist/maria/mariaDialect.d.ts +2 -2
- package/dist/maria/mariaDialect.js +3 -2
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +16 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
- package/dist/migrate/migrator.js +6 -0
- package/dist/migrate/schemaGenerator.d.ts +44 -34
- package/dist/migrate/schemaGenerator.js +158 -141
- package/dist/mongo/mongoDialect.js +39 -14
- package/dist/mongo/mongodbQuerier.js +6 -3
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.d.ts +16 -12
- package/dist/querier/abstractQuerier.js +88 -40
- package/dist/querier/abstractQuerierPool.d.ts +8 -8
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +12 -7
- package/dist/querier/relationCount.js +44 -33
- package/dist/schema/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +38 -31
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +54 -18
- package/dist/schema/types.d.ts +46 -7
- package/dist/sqlite/sqliteDialect.d.ts +2 -1
- package/dist/sqlite/sqliteDialect.js +4 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +57 -14
- package/dist/type/migration.d.ts +19 -0
- package/dist/type/queryWhere.d.ts +7 -2
- package/dist/type/universalQuerier.d.ts +8 -8
- package/dist/util/dialect.util.js +15 -14
- package/dist/util/field.util.d.ts +11 -1
- package/dist/util/field.util.js +12 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.d.ts +6 -0
- package/dist/util/object.util.js +18 -0
- package/dist/util/relationQuery.util.d.ts +48 -7
- package/dist/util/relationQuery.util.js +70 -9
- package/dist/util/rowKey.util.d.ts +9 -0
- package/dist/util/rowKey.util.js +24 -0
- package/dist/util/sql.util.d.ts +24 -7
- package/dist/util/sql.util.js +75 -10
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@ import { ObjectId } from 'mongodb';
|
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
3
|
import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
4
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
5
|
-
import { getMeta } from '../entity/index.js';
|
|
5
|
+
import { assertSoleId, getMeta, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
7
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, someKey, } from '../util/index.js';
|
|
7
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
8
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
9
9
|
export const mongoDialectFeatures = {
|
|
10
10
|
explicitJsonCast: false,
|
|
@@ -16,6 +16,7 @@ export const mongoDialectFeatures = {
|
|
|
16
16
|
dropTableCascade: false,
|
|
17
17
|
renameColumn: false,
|
|
18
18
|
foreignKeyAlter: false,
|
|
19
|
+
primaryKeyAlter: false,
|
|
19
20
|
columnComment: false,
|
|
20
21
|
vectorIndexRequiresNotNull: false,
|
|
21
22
|
vectorSupportsLength: false,
|
|
@@ -44,7 +45,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
44
45
|
* the document does not use.
|
|
45
46
|
*/
|
|
46
47
|
columnOf(meta, key) {
|
|
47
|
-
if (key === MongoDialect.ID_KEY
|
|
48
|
+
if (key === MongoDialect.ID_KEY) {
|
|
49
|
+
return MongoDialect.ID_KEY;
|
|
50
|
+
}
|
|
51
|
+
if (meta.fields[key]?.isId) {
|
|
52
|
+
// A document has one `_id`. A composite key is a different document shape - a sub-document,
|
|
53
|
+
// whose field order decides equality - rather than a translation, so it is refused here, which
|
|
54
|
+
// every read reaches, and in `getPersistables`, which every write reaches.
|
|
55
|
+
assertSoleId(meta, 'MongoDB');
|
|
48
56
|
return MongoDialect.ID_KEY;
|
|
49
57
|
}
|
|
50
58
|
return super.columnOf(meta, key);
|
|
@@ -161,7 +169,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
161
169
|
*/
|
|
162
170
|
relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts) {
|
|
163
171
|
return relOpts.cardinality === 'mm' && relOpts.through
|
|
164
|
-
? this.junctionLookup(relOpts, relMeta,
|
|
172
|
+
? this.junctionLookup(meta, relOpts, relMeta, targetScope, temp, tail, opts)
|
|
165
173
|
: {
|
|
166
174
|
$lookup: {
|
|
167
175
|
from: this.resolveTableName(relMeta),
|
|
@@ -175,22 +183,29 @@ export class MongoDialect extends AbstractDialect {
|
|
|
175
183
|
* ManyToMany counts/tests junction rows, so the target is reached from inside the junction's own
|
|
176
184
|
* lookup - the junction's filters apply too, since a soft-deleted link is not a link.
|
|
177
185
|
*/
|
|
178
|
-
junctionLookup(relOpts, relMeta,
|
|
186
|
+
junctionLookup(meta, relOpts, relMeta, targetScope, temp, tail, opts) {
|
|
179
187
|
const throughEntity = relOpts.through();
|
|
180
188
|
const throughMeta = getMeta(throughEntity);
|
|
181
189
|
const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
|
|
182
190
|
const nested = REL_NESTED_KEY;
|
|
191
|
+
// Both ends are one column here - each `$lookup` matches one field against `_id` - so both sides
|
|
192
|
+
// must be sole-keyed. Sliced rather than indexed positionally: `references[1]` is the parent's
|
|
193
|
+
// *second* column on a composite, a real column of the wrong side.
|
|
194
|
+
assertSoleId(meta, 'MongoDB');
|
|
195
|
+
assertSoleId(relMeta, 'MongoDB');
|
|
196
|
+
const [parentJoin] = parentJoins(relOpts, meta.ids.length);
|
|
197
|
+
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
183
198
|
return {
|
|
184
199
|
$lookup: {
|
|
185
200
|
from: this.resolveTableName(throughMeta),
|
|
186
201
|
localField: MongoDialect.ID_KEY,
|
|
187
|
-
foreignField: this.columnOf(throughMeta,
|
|
202
|
+
foreignField: this.columnOf(throughMeta, parentJoin.joined),
|
|
188
203
|
pipeline: [
|
|
189
204
|
...(hasKeys(junctionScope) ? [{ $match: junctionScope }] : []),
|
|
190
205
|
{
|
|
191
206
|
$lookup: {
|
|
192
207
|
from: this.resolveTableName(relMeta),
|
|
193
|
-
localField: this.columnOf(throughMeta,
|
|
208
|
+
localField: this.columnOf(throughMeta, targetColumn),
|
|
194
209
|
foreignField: MongoDialect.ID_KEY,
|
|
195
210
|
pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), { $limit: 1 }],
|
|
196
211
|
as: nested,
|
|
@@ -250,7 +265,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
250
265
|
*/
|
|
251
266
|
assertKnownPathRoot(meta, key) {
|
|
252
267
|
const root = key.includes('.') ? key.slice(0, key.indexOf('.')) : key;
|
|
253
|
-
if (root === MongoDialect.ID_KEY ||
|
|
268
|
+
if (root === MongoDialect.ID_KEY || meta.fields[root]) {
|
|
254
269
|
return;
|
|
255
270
|
}
|
|
256
271
|
throw new TypeError(`path ${key} does not exist in ${entityName(meta)}`);
|
|
@@ -372,7 +387,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
372
387
|
// MongoDB returns `_id` unless it is explicitly excluded, so subtracting the primary key needs
|
|
373
388
|
// `_id: 0` - the one inclusion/exclusion mix MongoDB allows - or `$exclude: { id: true }` would
|
|
374
389
|
// have no effect at all.
|
|
375
|
-
if (this.subtractsKey(meta
|
|
390
|
+
if (this.subtractsKey(soleIdOf(meta, 'MongoDB'), selectMap, exclude)) {
|
|
376
391
|
projection[MongoDialect.ID_KEY] = 0;
|
|
377
392
|
}
|
|
378
393
|
return projection;
|
|
@@ -652,9 +667,15 @@ export class MongoDialect extends AbstractDialect {
|
|
|
652
667
|
* direction joins the parent's `_id` to the target's FK.
|
|
653
668
|
*/
|
|
654
669
|
joinKeys(meta, relMeta, relOpts) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
670
|
+
if (relOpts.cardinality === 'm1') {
|
|
671
|
+
// The target's side, not this entity's: a lookup matches one `localField` against one
|
|
672
|
+
// `foreignField`, so a composite target would join on its first column alone and gather the
|
|
673
|
+
// rows of every key that agrees on it. The other branch is refused by `columnOf` below, whose
|
|
674
|
+
// key *is* an id column; this one names a plain foreign key, so it has to say so itself.
|
|
675
|
+
assertSoleId(relMeta, 'MongoDB');
|
|
676
|
+
return { localField: this.columnOf(meta, relOpts.references[0].local), foreignField: MongoDialect.ID_KEY };
|
|
677
|
+
}
|
|
678
|
+
return { localField: MongoDialect.ID_KEY, foreignField: this.columnOf(relMeta, relOpts.references[0].foreign) };
|
|
658
679
|
}
|
|
659
680
|
/** `[column, key]` for the fields whose stored name differs from their property name, memoized per entity. */
|
|
660
681
|
renamedColumns(meta) {
|
|
@@ -679,8 +700,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
679
700
|
const res = doc;
|
|
680
701
|
const _id = MongoDialect.ID_KEY;
|
|
681
702
|
if (res[_id]) {
|
|
682
|
-
|
|
683
|
-
|
|
703
|
+
const idKey = soleIdOf(meta, 'MongoDB');
|
|
704
|
+
res[idKey] = res[_id];
|
|
705
|
+
if (idKey !== _id) {
|
|
684
706
|
delete res[_id];
|
|
685
707
|
}
|
|
686
708
|
}
|
|
@@ -797,6 +819,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
797
819
|
return [{ $set: assignments }, ...(unset.size > 0 ? [{ $unset: [...unset] }] : [])];
|
|
798
820
|
}
|
|
799
821
|
getPersistables(meta, payload, callbackKey) {
|
|
822
|
+
// Not `columnOf`, which maps the primary key to `_id`: an update may not touch `_id` at all, and
|
|
823
|
+
// an insert leaves it to the driver. What that mapping refuses, a write has to refuse too.
|
|
824
|
+
assertSoleId(meta, 'MongoDB');
|
|
800
825
|
const payloads = fillOnFields(meta, payload, callbackKey);
|
|
801
826
|
// Keys are resolved per document so heterogeneous payloads keep every provided field.
|
|
802
827
|
return payloads.map((it) => filterFieldKeys(meta, it, callbackKey).reduce((acc, key) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
|
-
import { getMeta } from '../entity/index.js';
|
|
3
|
+
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
5
|
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
@@ -202,7 +202,9 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
202
202
|
const meta = getMeta(entity);
|
|
203
203
|
const pipeline = this.dialect.aggregationPipeline(entity, idOnlyQuery(meta, q), opts);
|
|
204
204
|
const founds = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
205
|
-
|
|
205
|
+
// `normalizeIds` has already spread a compound `_id` back into its columns, so the settled rows
|
|
206
|
+
// are named the same way every other driver names them.
|
|
207
|
+
return (this.dialect.normalizeIds(meta, founds) || []).map((found) => idOf(meta, found));
|
|
206
208
|
}
|
|
207
209
|
async internalInsertMany(entity, payloads) {
|
|
208
210
|
return this.timed('internalInsertMany', undefined, async () => {
|
|
@@ -214,8 +216,9 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
214
216
|
const persistables = this.dialect.getPersistables(meta, payloads, 'onInsert');
|
|
215
217
|
const { insertedIds } = await this.execute((session) => this.collection(entity).insertMany(persistables, { session }));
|
|
216
218
|
const ids = Object.values(insertedIds);
|
|
219
|
+
const idKey = soleIdOf(meta, 'insert');
|
|
217
220
|
for (const [index, it] of payloads.entries()) {
|
|
218
|
-
it[
|
|
221
|
+
it[idKey] = ids[index];
|
|
219
222
|
}
|
|
220
223
|
await this.insertRelations(entity, payloads);
|
|
221
224
|
return ids;
|
|
@@ -15,7 +15,7 @@ export class PostgresDialect extends PgLikeSqlDialect {
|
|
|
15
15
|
hasNarrowVectorTypes = true;
|
|
16
16
|
upsert(ctx, entity, conflictPaths, payload) {
|
|
17
17
|
// The xmax system column is 0 for a newly inserted row and non-zero for an updated one (MVCC).
|
|
18
|
-
super.upsert(ctx, entity, conflictPaths, payload,
|
|
18
|
+
super.upsert(ctx, entity, conflictPaths, payload, `(xmax = 0) AS ${this.escapeId('_created')}`);
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* `to_regclass` rather than a `::regclass` cast: it answers `NULL` for a table that does not exist
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EntityData, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
-
import { LoggerWrapper } from '../util/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, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import { LoggerWrapper, type ParentJoin } from '../util/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all database queriers.
|
|
5
5
|
* It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
|
|
@@ -30,7 +30,7 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
30
30
|
protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
|
|
31
31
|
$entity: Type<E>;
|
|
32
32
|
}), maybeQueryOrOpts?: Q | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, Q, QueryOptions | undefined];
|
|
33
|
-
findOneById<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>, id:
|
|
33
|
+
findOneById<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>, id: EntityId<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
34
34
|
/**
|
|
35
35
|
* Find a single record matching the query.
|
|
36
36
|
* Supports both entity-as-argument and entity-as-field patterns.
|
|
@@ -104,16 +104,16 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
104
104
|
/** Abstract outright: nothing is shared to do around it. See {@link UniversalQuerier.estimatedCount}. */
|
|
105
105
|
abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
106
106
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
107
|
-
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
108
|
-
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
109
|
-
updateOneById<E extends object>(entity: Type<E>, id:
|
|
107
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
108
|
+
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
109
|
+
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
110
110
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
111
111
|
protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
112
|
-
restoreOneById<E extends object>(entity: Type<E>, id:
|
|
112
|
+
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
113
113
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
114
114
|
abstract upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
115
115
|
abstract upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
116
|
-
deleteOneById<E extends object>(entity: Type<E>, id:
|
|
116
|
+
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
117
117
|
/**
|
|
118
118
|
* Delete records matching the query. Soft-deletes when the entity has a soft-delete field (unless
|
|
119
119
|
* `opts.hardDelete`), otherwise removes the rows. Supports both entity-as-argument and entity-as-field patterns.
|
|
@@ -132,15 +132,19 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
132
132
|
*/
|
|
133
133
|
private findDoomed;
|
|
134
134
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
135
|
-
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E
|
|
136
|
-
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
135
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
136
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
137
137
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
138
138
|
private fillToManyThroughRelation;
|
|
139
139
|
private fillToManyOneToMany;
|
|
140
|
-
protected putChildrenInParents<E>(parents: E[], children: RawRow[],
|
|
140
|
+
protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
|
|
141
141
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
142
142
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
143
|
-
|
|
143
|
+
/**
|
|
144
|
+
* `EntityId` because a settled set names composite rows as objects, which is also what the parent's
|
|
145
|
+
* own delete takes - and what {@link childrenOf} reads each child's foreign key columns out of.
|
|
146
|
+
*/
|
|
147
|
+
protected deleteRelations<E extends object>(entity: Type<E>, ids: EntityId<E>[], opts?: QueryOptions): Promise<void>;
|
|
144
148
|
/**
|
|
145
149
|
* Persists `relValue` against every id in `ids`, which an update hands the whole page of rows it
|
|
146
150
|
* settled: the value is the same for all of them, so the statements are per relation rather than per
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper,
|
|
1
|
+
import { assertSoleId, getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
+
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, joinedRowKey, LoggerWrapper, parentJoins, parentRowKey, parentsIn, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -15,6 +15,29 @@ function assertIdValue(entity, id) {
|
|
|
15
15
|
if (id === undefined || id === null) {
|
|
16
16
|
throw new TypeError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
|
|
17
17
|
}
|
|
18
|
+
if (isScalarId(id)) {
|
|
19
|
+
// One value names one column, which `buildQueryWhereAsMap` refuses on a composite.
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Every key, or the `$where` names only some of the columns and addresses each row that agrees on
|
|
23
|
+
// those - which for an update or a delete is silently far more than the one row asked for. `{}`
|
|
24
|
+
// reaches here too, and would otherwise be an unfiltered statement over the whole table.
|
|
25
|
+
const given = id;
|
|
26
|
+
const { ids } = getMeta(entity);
|
|
27
|
+
const missing = ids.filter((key) => given[key] == null);
|
|
28
|
+
if (missing.length) {
|
|
29
|
+
throw new TypeError(`'${entity.name}' is addressed by an object carrying every key of its primary key (${ids.join(', ')}); missing ${missing.join(', ')}.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The one column a write path matches a parent's key against.
|
|
34
|
+
*
|
|
35
|
+
* Every caller reaches its parents through a sole id - `soleIdOf` refused a composite before any of
|
|
36
|
+
* them - so the parent contributes exactly one column here, whether the relation goes through a
|
|
37
|
+
* junction or straight to the child.
|
|
38
|
+
*/
|
|
39
|
+
function soleParentColumn(relOpts) {
|
|
40
|
+
return parentJoins(relOpts, 1)[0].joined;
|
|
18
41
|
}
|
|
19
42
|
/**
|
|
20
43
|
* Base class for all database queriers.
|
|
@@ -187,7 +210,11 @@ export class AbstractQuerier {
|
|
|
187
210
|
// Where a snapshot was taken, the statement names those rows rather than resolving `q` a second
|
|
188
211
|
// time. Two reads of one `$limit` with no total order are free to disagree, which would fire the
|
|
189
212
|
// hooks for one row and delete another; naming them also spares the second read.
|
|
190
|
-
|
|
213
|
+
let target = q;
|
|
214
|
+
if (doomed) {
|
|
215
|
+
const meta = getMeta(entity);
|
|
216
|
+
target = { $where: doomed.map((it) => idOf(meta, it)) };
|
|
217
|
+
}
|
|
191
218
|
await this.emitHook(entity, 'beforeDelete', doomed ?? []);
|
|
192
219
|
const changes = await this.internalDeleteMany(entity, target, opts);
|
|
193
220
|
await this.emitHook(entity, 'afterDelete', doomed ?? []);
|
|
@@ -218,12 +245,16 @@ export class AbstractQuerier {
|
|
|
218
245
|
const toInsert = [];
|
|
219
246
|
const toUpdate = [];
|
|
220
247
|
const existingIds = [];
|
|
248
|
+
// Save reads an id as proof the row exists, and inserts the rest. A composite key is supplied by
|
|
249
|
+
// the caller on every row, insert included, so that proof does not exist for one: telling the two
|
|
250
|
+
// apart takes a read, which is `upsertMany`'s job and not this one's.
|
|
251
|
+
const idKey = soleIdOf(meta, 'saving a row');
|
|
221
252
|
for (const it of payload) {
|
|
222
|
-
const id = it[
|
|
253
|
+
const id = it[idKey];
|
|
223
254
|
if (!id) {
|
|
224
255
|
toInsert.push(it);
|
|
225
256
|
}
|
|
226
|
-
else if (!someKey(it, (key) => key !==
|
|
257
|
+
else if (!someKey(it, (key) => key !== idKey)) {
|
|
227
258
|
existingIds.push(id);
|
|
228
259
|
}
|
|
229
260
|
else {
|
|
@@ -233,9 +264,9 @@ export class AbstractQuerier {
|
|
|
233
264
|
const [insertedIds, updatedIds] = await Promise.all([
|
|
234
265
|
toInsert.length ? this.insertMany(entity, toInsert) : [],
|
|
235
266
|
Promise.all(toUpdate.map(async (it) => {
|
|
236
|
-
const id = it[
|
|
267
|
+
const id = it[idKey];
|
|
237
268
|
const data = { ...it };
|
|
238
|
-
delete data[
|
|
269
|
+
delete data[idKey];
|
|
239
270
|
await this.updateOneById(entity, id, data);
|
|
240
271
|
return id;
|
|
241
272
|
})),
|
|
@@ -263,11 +294,11 @@ export class AbstractQuerier {
|
|
|
263
294
|
}
|
|
264
295
|
}
|
|
265
296
|
async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
|
|
266
|
-
const
|
|
297
|
+
const joins = parentJoins(relOpts, meta.ids.length);
|
|
298
|
+
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
267
299
|
const throughEntity = relOpts.through();
|
|
268
300
|
const throughMeta = getMeta(throughEntity);
|
|
269
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local ===
|
|
270
|
-
const ids = payload.map((it) => it[meta.id]);
|
|
301
|
+
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
|
|
271
302
|
// A relation query names the target's columns, not the join table's, so the projection and the
|
|
272
303
|
// filter belong on the populate below - resolved there against the entity that has them. Spread
|
|
273
304
|
// onto the through query they asked `ItemTag` for `Tag`'s columns: `$where`/`$sort` failed with
|
|
@@ -275,53 +306,52 @@ export class AbstractQuerier {
|
|
|
275
306
|
const { $select: _select, $exclude: _exclude, $where: _where, ...throughQuery } = relationQuery;
|
|
276
307
|
const throughFounds = await this.findMany(throughEntity, {
|
|
277
308
|
...throughQuery,
|
|
278
|
-
$select:
|
|
279
|
-
[localField]: true,
|
|
280
|
-
},
|
|
309
|
+
$select: joinedColumns(joins),
|
|
281
310
|
$populate: {
|
|
282
311
|
[targetRelKey]: {
|
|
283
312
|
...relationQuery,
|
|
284
313
|
$required: true,
|
|
285
314
|
},
|
|
286
315
|
},
|
|
287
|
-
$where:
|
|
288
|
-
[localField]: ids,
|
|
289
|
-
},
|
|
316
|
+
$where: parentsIn(joins, payload),
|
|
290
317
|
});
|
|
318
|
+
// The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
|
|
319
|
+
// reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
|
|
291
320
|
const founds = throughFounds.map((it) => ({
|
|
292
321
|
...it[targetRelKey],
|
|
293
|
-
[
|
|
322
|
+
...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
|
|
294
323
|
}));
|
|
295
|
-
this.putChildrenInParents(payload, founds,
|
|
324
|
+
this.putChildrenInParents(payload, founds, joins, relKey);
|
|
296
325
|
}
|
|
297
326
|
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
298
|
-
const
|
|
327
|
+
const joins = parentJoins(relOpts, meta.ids.length);
|
|
299
328
|
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
300
329
|
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
301
330
|
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
302
331
|
const select = asSelectMap(relationQuery.$select);
|
|
303
|
-
|
|
304
|
-
|
|
332
|
+
const exclude = relationQuery.$exclude;
|
|
333
|
+
for (const { joined } of joins) {
|
|
334
|
+
if (select && !select[joined]) {
|
|
335
|
+
select[joined] = true;
|
|
336
|
+
}
|
|
337
|
+
delete exclude?.[joined];
|
|
305
338
|
}
|
|
306
|
-
|
|
307
|
-
const ids = payload.map((it) => it[meta.id]);
|
|
308
|
-
relationQuery.$where = { ...relationQuery.$where, [foreignField]: ids };
|
|
339
|
+
relationQuery.$where = { ...relationQuery.$where, ...parentsIn(joins, payload) };
|
|
309
340
|
const founds = await this.findMany(relEntity, relationQuery);
|
|
310
|
-
this.putChildrenInParents(payload, founds,
|
|
341
|
+
this.putChildrenInParents(payload, founds, joins, relKey);
|
|
311
342
|
}
|
|
312
|
-
putChildrenInParents(parents, children,
|
|
343
|
+
putChildrenInParents(parents, children, joins, relKey) {
|
|
313
344
|
const childrenByParentId = {};
|
|
314
345
|
for (const child of children) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
childrenByParentId[parentId].push(child);
|
|
346
|
+
// Every joined column, so two children agreeing on one column of a composite key are not
|
|
347
|
+
// gathered under the same parent.
|
|
348
|
+
(childrenByParentId[joinedRowKey(joins, child)] ??= []).push(child);
|
|
319
349
|
}
|
|
320
350
|
for (const parent of parents) {
|
|
321
351
|
// `[]` rather than nothing for a parent with no children: a populated to-many is a list the
|
|
322
352
|
// caller asked for, so it maps and counts without a guard, and its type can say so. An
|
|
323
353
|
// unpopulated one stays absent, which is what tells the two apart.
|
|
324
|
-
parent[relKey] = (childrenByParentId[
|
|
354
|
+
parent[relKey] = (childrenByParentId[parentRowKey(joins, parent)] ?? []);
|
|
325
355
|
}
|
|
326
356
|
}
|
|
327
357
|
async insertRelations(entity, payload) {
|
|
@@ -334,7 +364,8 @@ export class AbstractQuerier {
|
|
|
334
364
|
}, []);
|
|
335
365
|
if (!entries.length)
|
|
336
366
|
return;
|
|
337
|
-
|
|
367
|
+
const idKey = soleIdOf(meta, 'saving a relation');
|
|
368
|
+
await Promise.all(entries.map(({ it, relKeys }) => Promise.all(relKeys.map((relKey) => this.saveRelation(entity, [it[idKey]], it[relKey], relKey)))));
|
|
338
369
|
}
|
|
339
370
|
async updateRelations(entity, q, payload, opts) {
|
|
340
371
|
const meta = getMeta(entity);
|
|
@@ -342,8 +373,9 @@ export class AbstractQuerier {
|
|
|
342
373
|
if (!relKeys.length) {
|
|
343
374
|
return;
|
|
344
375
|
}
|
|
376
|
+
const idKey = soleIdOf(meta, 'saving a relation');
|
|
345
377
|
const founds = await this.findMany(entity, idOnlyQuery(meta, q), opts);
|
|
346
|
-
const ids = founds.map((found) => found[
|
|
378
|
+
const ids = founds.map((found) => found[idKey]);
|
|
347
379
|
if (!ids.length) {
|
|
348
380
|
return;
|
|
349
381
|
}
|
|
@@ -351,6 +383,10 @@ export class AbstractQuerier {
|
|
|
351
383
|
await this.saveRelation(entity, ids, payload[relKey], relKey, true);
|
|
352
384
|
}
|
|
353
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* `EntityId` because a settled set names composite rows as objects, which is also what the parent's
|
|
388
|
+
* own delete takes - and what {@link childrenOf} reads each child's foreign key columns out of.
|
|
389
|
+
*/
|
|
354
390
|
async deleteRelations(entity, ids, opts) {
|
|
355
391
|
const meta = getMeta(entity);
|
|
356
392
|
const relKeys = filterPersistableRelationKeys(meta, meta.relations, 'delete');
|
|
@@ -360,9 +396,9 @@ export class AbstractQuerier {
|
|
|
360
396
|
if (!relOpts)
|
|
361
397
|
continue;
|
|
362
398
|
const relEntity = relOpts.entity();
|
|
363
|
-
const parentKey = parentKeyColumn(relOpts);
|
|
364
399
|
const target = relOpts.through ? relOpts.through() : relEntity;
|
|
365
|
-
|
|
400
|
+
const where = childrenOf(parentJoins(relOpts, meta.ids.length), ids);
|
|
401
|
+
await this.deleteMany(target, { $where: where }, opts);
|
|
366
402
|
}
|
|
367
403
|
}
|
|
368
404
|
/**
|
|
@@ -375,6 +411,11 @@ export class AbstractQuerier {
|
|
|
375
411
|
const relOpts = meta.relations[relKey];
|
|
376
412
|
if (!relOpts)
|
|
377
413
|
return;
|
|
414
|
+
// Here rather than only in the callers below: writing the parent's key into a child is one column
|
|
415
|
+
// per key, so a composite takes a statement per parent. `soleParentColumn` and the sole
|
|
416
|
+
// `targetKeyColumns` under it read the *first* pair, which is a real column of a wrong pairing
|
|
417
|
+
// unless this has run - and this method is `protected`, so a caller can arrive without them.
|
|
418
|
+
assertSoleId(meta, 'saving a relation');
|
|
378
419
|
const relEntity = relOpts.entity();
|
|
379
420
|
const relPayload = relValue;
|
|
380
421
|
switch (relOpts.cardinality) {
|
|
@@ -391,7 +432,8 @@ export class AbstractQuerier {
|
|
|
391
432
|
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
392
433
|
const { through } = relOpts;
|
|
393
434
|
if (through) {
|
|
394
|
-
const localField =
|
|
435
|
+
const localField = soleParentColumn(relOpts);
|
|
436
|
+
const [targetColumn] = targetKeyColumns(relOpts, 1);
|
|
395
437
|
const throughEntity = through();
|
|
396
438
|
if (isUpdate) {
|
|
397
439
|
await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
|
|
@@ -401,12 +443,18 @@ export class AbstractQuerier {
|
|
|
401
443
|
// would link every parent to a single shared row instead.
|
|
402
444
|
for (const id of ids) {
|
|
403
445
|
const savedIds = await this.saveMany(relEntity, relPayload);
|
|
404
|
-
|
|
446
|
+
// A link needs the target's id, and a driver that cannot report one (a MySQL batch mixing
|
|
447
|
+
// supplied and generated keys) would otherwise write a row pointing at `undefined`.
|
|
448
|
+
if (savedIds.some((relId) => relId === undefined)) {
|
|
449
|
+
throw new TypeError(`'${relEntity.name}' rows saved through '${throughEntity.name}' reported no id, so they cannot be linked. ` +
|
|
450
|
+
'Insert them with their own ids, or save the relation in its own statement.');
|
|
451
|
+
}
|
|
452
|
+
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetColumn]: relId })));
|
|
405
453
|
}
|
|
406
454
|
}
|
|
407
455
|
return;
|
|
408
456
|
}
|
|
409
|
-
const foreignField =
|
|
457
|
+
const foreignField = soleParentColumn(relOpts);
|
|
410
458
|
if (isUpdate) {
|
|
411
459
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
412
460
|
}
|
|
@@ -415,7 +463,7 @@ export class AbstractQuerier {
|
|
|
415
463
|
}
|
|
416
464
|
}
|
|
417
465
|
async saveOneToOne(relEntity, relOpts, ids, relPayload) {
|
|
418
|
-
const foreignField =
|
|
466
|
+
const foreignField = soleParentColumn(relOpts);
|
|
419
467
|
if (relPayload === null) {
|
|
420
468
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
421
469
|
return;
|
|
@@ -423,7 +471,7 @@ export class AbstractQuerier {
|
|
|
423
471
|
await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
|
|
424
472
|
}
|
|
425
473
|
async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
|
|
426
|
-
// Not `
|
|
474
|
+
// Not `soleParentColumn`: a many-to-one points the other way, so this is the *parent's* own column
|
|
427
475
|
// holding the child's id - the one place `references[0].local` does not name a key of the parent.
|
|
428
476
|
const localField = relOpts.references[0].local;
|
|
429
477
|
// Per parent: each gets its own reference row, so each `SET` carries a different value.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
|
|
5
5
|
*/
|
|
@@ -24,7 +24,7 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
24
24
|
withQuerier<T>(callback: (querier: Q) => Promise<T>, opts?: PoolRunOptions): Promise<T>;
|
|
25
25
|
/** Run `fn` under `context` (an enclosing {@link withContext}) when provided, else run it as-is. */
|
|
26
26
|
private runScoped;
|
|
27
|
-
findOneById<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>, id:
|
|
27
|
+
findOneById<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>, id: EntityId<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
28
28
|
findOne<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: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
29
29
|
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>[]>;
|
|
30
30
|
/**
|
|
@@ -38,16 +38,16 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
38
38
|
aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
39
39
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
40
40
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
41
|
-
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
42
|
-
updateOneById<E extends object>(entity: Type<E>, id:
|
|
41
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
42
|
+
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
43
43
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
44
44
|
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
45
45
|
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
46
|
-
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E
|
|
47
|
-
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
48
|
-
deleteOneById<E extends object>(entity: Type<E>, id:
|
|
46
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
47
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
48
|
+
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
49
49
|
deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
50
|
-
restoreOneById<E extends object>(entity: Type<E>, id:
|
|
50
|
+
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
51
51
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
52
52
|
/**
|
|
53
53
|
* end the pool.
|
|
@@ -105,7 +105,7 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
105
105
|
protected internalCount<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
106
106
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
107
107
|
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>[]>;
|
|
108
|
-
internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
108
|
+
internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
109
109
|
internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
110
110
|
/** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
|
|
111
111
|
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 } from '../entity/index.js';
|
|
3
|
+
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
6
|
import { enrichError } from './queryError.js';
|
|
@@ -283,14 +283,19 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
283
283
|
}
|
|
284
284
|
payload = clone(payload);
|
|
285
285
|
const meta = getMeta(entity);
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
// A composite key is supplied whole by the caller, so nothing is generated and nothing comes
|
|
287
|
+
// back: the rows insert, and their ids are `undefined` for the reason MySQL's are below - there
|
|
288
|
+
// is no id to report. `idOf` names such a row for the caller that wants one.
|
|
289
|
+
const [idKey] = meta.ids;
|
|
290
|
+
const sole = meta.ids.length === 1;
|
|
291
|
+
const idField = sole ? meta.fields[idKey] : undefined;
|
|
288
292
|
// RETURNING-based IDs are exact per row. Header-derived IDs (LAST_INSERT_ID /
|
|
289
293
|
// lastInsertRowid arithmetic) are only sound when the primary key is database-generated
|
|
290
294
|
// and no record supplies an explicit ID (a mixed batch shifts the positional mapping and
|
|
291
295
|
// MySQL stops guaranteeing consecutive values); otherwise generated IDs stay `undefined`.
|
|
292
|
-
const idsReliable =
|
|
293
|
-
(
|
|
296
|
+
const idsReliable = sole &&
|
|
297
|
+
(this.dialect.insertIdSource === 'returning' ||
|
|
298
|
+
(!!idField && isAutoIncrement(idField, true) && payload.every((it) => it[idKey] === undefined)));
|
|
294
299
|
// Inferring multiple ids from the single header id (MySQL) assumes a known stride; a clustered
|
|
295
300
|
// server may set `auto_increment_increment` > 1, so probe it (once, cached) before inferring.
|
|
296
301
|
if (idsReliable && payload.length > 1 && this.dialect.insertIdSource === 'firstId') {
|
|
@@ -309,7 +314,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
309
314
|
if (idsReliable) {
|
|
310
315
|
it[idKey] ??= ids[index];
|
|
311
316
|
}
|
|
312
|
-
payloadIds.push(it[idKey]);
|
|
317
|
+
payloadIds.push(sole ? it[idKey] : undefined);
|
|
313
318
|
});
|
|
314
319
|
}
|
|
315
320
|
await this.insertRelations(entity, payload);
|
|
@@ -339,7 +344,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
339
344
|
const ctx = this.dialect.createContext();
|
|
340
345
|
this.dialect.find(ctx, entity, idOnlyQuery(meta, q), opts);
|
|
341
346
|
const founds = await this.all(ctx.sql, ctx.values);
|
|
342
|
-
return founds.map((
|
|
347
|
+
return founds.map((found) => idOf(meta, found));
|
|
343
348
|
}
|
|
344
349
|
async upsertOne(entity, conflictPaths, payload) {
|
|
345
350
|
return this.upsertMany(entity, conflictPaths, [payload]);
|