@mikro-orm/sql 7.1.0-dev.4 → 7.1.0-dev.40
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/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +27 -6
- package/AbstractSqlDriver.d.ts +26 -1
- package/AbstractSqlDriver.js +286 -35
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.d.ts +2 -2
- package/PivotCollectionPersister.js +19 -3
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +46 -3
- package/SqlEntityManager.js +77 -7
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +4 -5
- package/dialects/mysql/BaseMySqlPlatform.js +9 -10
- package/dialects/mysql/MySqlSchemaHelper.d.ts +13 -3
- package/dialects/mysql/MySqlSchemaHelper.js +145 -21
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
- package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +11 -5
- package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +230 -5
- package/dialects/postgresql/index.d.ts +2 -0
- package/dialects/postgresql/index.js +2 -0
- package/dialects/postgresql/typeOverrides.d.ts +14 -0
- package/dialects/postgresql/typeOverrides.js +12 -0
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
- package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +4 -4
- package/plugin/transformer.d.ts +11 -3
- package/plugin/transformer.js +138 -29
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +42 -1
- package/query/QueryBuilder.js +78 -7
- package/schema/DatabaseSchema.js +26 -4
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +182 -31
- package/schema/SchemaComparator.d.ts +10 -0
- package/schema/SchemaComparator.js +104 -1
- package/schema/SchemaHelper.d.ts +63 -1
- package/schema/SchemaHelper.js +235 -6
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +16 -9
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +34 -2
package/AbstractSqlDriver.js
CHANGED
|
@@ -3,6 +3,28 @@ import { QueryBuilder } from './query/QueryBuilder.js';
|
|
|
3
3
|
import { JoinType, QueryType } from './query/enums.js';
|
|
4
4
|
import { SqlEntityManager } from './SqlEntityManager.js';
|
|
5
5
|
import { PivotCollectionPersister } from './PivotCollectionPersister.js';
|
|
6
|
+
/** Extracts cancellation controls from any options bag that extends `AbortQueryOptions`. */
|
|
7
|
+
function pickAbortOptions(options) {
|
|
8
|
+
if (!options || (options.signal == null && options.inflightQueryAbortStrategy == null)) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
signal: options.signal,
|
|
13
|
+
inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns a `loggerContext` payload that carries the abort fields alongside any existing
|
|
18
|
+
* context. The connection layer strips them before logging — this avoids widening the public
|
|
19
|
+
* `Connection.execute()` signature.
|
|
20
|
+
*/
|
|
21
|
+
function withAbortContext(loggerContext, options) {
|
|
22
|
+
const abort = pickAbortOptions(options);
|
|
23
|
+
if (!abort) {
|
|
24
|
+
return loggerContext;
|
|
25
|
+
}
|
|
26
|
+
return { ...loggerContext, ...abort };
|
|
27
|
+
}
|
|
6
28
|
/** Base class for SQL database drivers, implementing find/insert/update/delete using QueryBuilder. */
|
|
7
29
|
export class AbstractSqlDriver extends DatabaseDriver {
|
|
8
30
|
[EntityManagerType];
|
|
@@ -34,6 +56,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
34
56
|
return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
|
|
35
57
|
}
|
|
36
58
|
validateSqlOptions(options) {
|
|
59
|
+
if (options.using && !options.indexHint) {
|
|
60
|
+
const names = Utils.asArray(options.using);
|
|
61
|
+
const hint = this.platform.formatIndexHint(names);
|
|
62
|
+
if (hint) {
|
|
63
|
+
options.indexHint = hint;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
37
66
|
if (options.collation != null && typeof options.collation !== 'string') {
|
|
38
67
|
throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
|
|
39
68
|
}
|
|
@@ -50,7 +79,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
50
79
|
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
|
|
51
80
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
52
81
|
const schema = this.getSchemaName(meta, options);
|
|
53
|
-
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
|
|
82
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
|
|
83
|
+
.withSchema(schema)
|
|
84
|
+
.cache(false);
|
|
85
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
54
86
|
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
55
87
|
const orderBy = this.buildOrderBy(qb, meta, populate, options);
|
|
56
88
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
@@ -88,6 +120,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
88
120
|
if (options.em) {
|
|
89
121
|
await qb.applyJoinedFilters(options.em, options.filters);
|
|
90
122
|
}
|
|
123
|
+
if (options._partitionLimit) {
|
|
124
|
+
qb.setPartitionLimit(options._partitionLimit);
|
|
125
|
+
}
|
|
91
126
|
return qb;
|
|
92
127
|
}
|
|
93
128
|
async find(entityName, where, options = {}) {
|
|
@@ -204,7 +239,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
204
239
|
const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
|
|
205
240
|
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
|
|
206
241
|
const query = native.compile();
|
|
207
|
-
const res = await this.execute(query.sql, query.params, 'all', options.ctx);
|
|
242
|
+
const res = await this.execute(query.sql, query.params, 'all', options.ctx, withAbortContext(options.loggerContext, options));
|
|
208
243
|
if (type === QueryType.COUNT) {
|
|
209
244
|
return res[0].count;
|
|
210
245
|
}
|
|
@@ -221,7 +256,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
221
256
|
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
|
|
222
257
|
const query = native.compile();
|
|
223
258
|
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
|
|
224
|
-
const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
|
|
259
|
+
const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, withAbortContext(options.loggerContext, options), options.chunkSize);
|
|
225
260
|
for await (const row of res) {
|
|
226
261
|
yield this.mapResult(row, meta);
|
|
227
262
|
}
|
|
@@ -529,6 +564,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
529
564
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
530
565
|
const schema = this.getSchemaName(meta, options);
|
|
531
566
|
const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
|
|
567
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
532
568
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
533
569
|
if (meta && !Utils.isEmpty(populate)) {
|
|
534
570
|
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
@@ -554,6 +590,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
554
590
|
const meta = this.metadata.get(entityName);
|
|
555
591
|
const collections = this.extractManyToMany(meta, data);
|
|
556
592
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
593
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
557
594
|
const res = await this.rethrow(qb.insert(data).execute('run', false));
|
|
558
595
|
res.row = res.row || {};
|
|
559
596
|
let pk;
|
|
@@ -586,9 +623,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
586
623
|
const props = this.getCloneableProps(meta);
|
|
587
624
|
const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
|
|
588
625
|
const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
|
|
626
|
+
const abort = pickAbortOptions(options);
|
|
589
627
|
const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
590
628
|
selectQb.select(selectFields).where(where);
|
|
629
|
+
selectQb.setAbortOptions(abort);
|
|
591
630
|
const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
631
|
+
insertQb.setAbortOptions(abort);
|
|
592
632
|
return this.rethrow(insertQb
|
|
593
633
|
.insertFrom(selectQb, { columns: insertColumns })
|
|
594
634
|
.execute('run', false));
|
|
@@ -618,9 +658,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
618
658
|
}
|
|
619
659
|
}
|
|
620
660
|
const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
|
|
661
|
+
const abort = pickAbortOptions(options);
|
|
621
662
|
const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
|
|
622
663
|
selectQb.select(selectFields).where(sourceWhere);
|
|
664
|
+
selectQb.setAbortOptions(abort);
|
|
623
665
|
const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
|
|
666
|
+
insertQb.setAbortOptions(abort);
|
|
624
667
|
const res = await this.rethrow(insertQb
|
|
625
668
|
.insertFrom(selectQb, { columns: insertColumns })
|
|
626
669
|
.execute('run', false));
|
|
@@ -828,7 +871,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
828
871
|
if (transform) {
|
|
829
872
|
sql = transform(sql);
|
|
830
873
|
}
|
|
831
|
-
const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
|
|
874
|
+
const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
|
|
832
875
|
let pk;
|
|
833
876
|
/* v8 ignore next */
|
|
834
877
|
if (pks.length > 1) {
|
|
@@ -861,6 +904,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
861
904
|
}
|
|
862
905
|
if (Utils.hasObjectKeys(data)) {
|
|
863
906
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
907
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
864
908
|
if (options.upsert) {
|
|
865
909
|
/* v8 ignore next */
|
|
866
910
|
const uniqueFields = options.onConflictFields ??
|
|
@@ -906,6 +950,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
906
950
|
? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
|
|
907
951
|
: meta.primaryKeys);
|
|
908
952
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
953
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
909
954
|
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
910
955
|
qb.insert(data)
|
|
911
956
|
.onConflict(uniqueFields)
|
|
@@ -1051,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1051
1096
|
if (transform) {
|
|
1052
1097
|
sql = transform(sql, params);
|
|
1053
1098
|
}
|
|
1054
|
-
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
|
|
1099
|
+
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
|
|
1055
1100
|
for (let i = 0; i < collections.length; i++) {
|
|
1056
1101
|
await this.processManyToMany(meta, where[i], collections[i], false, options);
|
|
1057
1102
|
}
|
|
@@ -1069,6 +1114,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1069
1114
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
1070
1115
|
.delete(where)
|
|
1071
1116
|
.withSchema(this.getSchemaName(meta, options));
|
|
1117
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
1072
1118
|
return this.rethrow(qb.execute('run', false));
|
|
1073
1119
|
}
|
|
1074
1120
|
/**
|
|
@@ -1099,8 +1145,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1099
1145
|
const pks = wrapped.getPrimaryKeys(true);
|
|
1100
1146
|
const snap = coll.getSnapshot();
|
|
1101
1147
|
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
1102
|
-
|
|
1103
|
-
|
|
1148
|
+
// For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
|
|
1149
|
+
// persister can write it alongside the FK id. Memoized per sync-run because a collection can
|
|
1150
|
+
// hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
|
|
1151
|
+
// chain + re-scans Object.entries each call.
|
|
1152
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
|
|
1153
|
+
const classToDisc = new Map();
|
|
1154
|
+
const toDiff = (item) => {
|
|
1155
|
+
const keys = helper(item).getPrimaryKeys(true);
|
|
1156
|
+
if (!isUnionTargetMN) {
|
|
1157
|
+
return keys;
|
|
1158
|
+
}
|
|
1159
|
+
let disc = classToDisc.get(item.constructor);
|
|
1160
|
+
if (!classToDisc.has(item.constructor)) {
|
|
1161
|
+
disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
|
|
1162
|
+
if (disc === undefined) {
|
|
1163
|
+
throw new Error(`Cannot resolve discriminator value for ${item.constructor.name} in ${coll.property.name}; the class is not part of the union target list.`);
|
|
1164
|
+
}
|
|
1165
|
+
classToDisc.set(item.constructor, disc);
|
|
1166
|
+
}
|
|
1167
|
+
return [disc, ...keys];
|
|
1168
|
+
};
|
|
1169
|
+
const snapshot = snap ? snap.map(toDiff) : [];
|
|
1170
|
+
const current = coll.getItems(false).map(toDiff);
|
|
1104
1171
|
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
1105
1172
|
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
1106
1173
|
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
@@ -1118,6 +1185,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1118
1185
|
if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
|
|
1119
1186
|
const cols = coll.property.referencedColumnNames;
|
|
1120
1187
|
const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
|
|
1188
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
1121
1189
|
if (coll.getSnapshot() === undefined) {
|
|
1122
1190
|
if (coll.property.orphanRemoval) {
|
|
1123
1191
|
const query = qb
|
|
@@ -1159,7 +1227,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1159
1227
|
schema = this.config.get('schema');
|
|
1160
1228
|
}
|
|
1161
1229
|
const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
|
|
1162
|
-
const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
|
|
1230
|
+
const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext, pickAbortOptions(options)));
|
|
1163
1231
|
persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
|
|
1164
1232
|
}
|
|
1165
1233
|
for (const persister of Utils.values(groups)) {
|
|
@@ -1172,21 +1240,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1172
1240
|
return {};
|
|
1173
1241
|
}
|
|
1174
1242
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1243
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
1244
|
+
return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1245
|
+
}
|
|
1175
1246
|
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1176
1247
|
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1177
1248
|
}
|
|
1178
1249
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1179
1250
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1180
1251
|
const ownerMeta = pivotProp2.targetMeta;
|
|
1181
|
-
|
|
1182
|
-
// convert owner PKs to DB format for the query and convert result FKs back to
|
|
1183
|
-
// JS format for consistent key hashing in buildPivotResultMap.
|
|
1184
|
-
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1185
|
-
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1186
|
-
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1187
|
-
if (needsConversion) {
|
|
1188
|
-
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1189
|
-
}
|
|
1252
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1190
1253
|
const cond = {
|
|
1191
1254
|
[pivotProp2.name]: { $in: ownerPks },
|
|
1192
1255
|
};
|
|
@@ -1201,7 +1264,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1201
1264
|
const fields = pivotJoin
|
|
1202
1265
|
? [pivotProp1.name, pivotProp2.name]
|
|
1203
1266
|
: [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
1204
|
-
const
|
|
1267
|
+
const pivotFindOptions = {
|
|
1205
1268
|
ctx,
|
|
1206
1269
|
...options,
|
|
1207
1270
|
fields,
|
|
@@ -1217,10 +1280,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1217
1280
|
},
|
|
1218
1281
|
],
|
|
1219
1282
|
populateWhere: undefined,
|
|
1220
|
-
// @ts-ignore
|
|
1221
1283
|
_populateWhere: 'infer',
|
|
1222
1284
|
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
1223
|
-
}
|
|
1285
|
+
};
|
|
1286
|
+
if (pivotFindOptions._partitionLimit) {
|
|
1287
|
+
pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
|
|
1288
|
+
}
|
|
1289
|
+
const res = await this.find(pivotMeta.class, where, pivotFindOptions);
|
|
1224
1290
|
// Convert result FK values back to JS format so key hashing
|
|
1225
1291
|
// in buildPivotResultMap is consistent with the owner keys.
|
|
1226
1292
|
if (needsConversion) {
|
|
@@ -1340,6 +1406,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1340
1406
|
});
|
|
1341
1407
|
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1342
1408
|
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
1411
|
+
* Each pivot row's discriminator column selects which target table to hydrate.
|
|
1412
|
+
*/
|
|
1413
|
+
async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
|
|
1414
|
+
// :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
|
|
1415
|
+
// class per item, but the ref-mode map only carries flat PK values and would hydrate every
|
|
1416
|
+
// row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
|
|
1417
|
+
if (pivotJoin) {
|
|
1418
|
+
throw new Error(`The ':ref' populate hint is not supported for union-target polymorphic M:N on ${prop.name}. Use a regular populate hint instead.`);
|
|
1419
|
+
}
|
|
1420
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1421
|
+
const targets = prop.polymorphTargets;
|
|
1422
|
+
const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
|
|
1423
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
1424
|
+
const ownerMeta = ownerProp.targetMeta;
|
|
1425
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1426
|
+
const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
|
|
1427
|
+
ctx,
|
|
1428
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1429
|
+
fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
|
|
1430
|
+
populateWhere: undefined,
|
|
1431
|
+
// @ts-ignore
|
|
1432
|
+
_populateWhere: 'infer',
|
|
1433
|
+
}));
|
|
1434
|
+
/* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1435
|
+
if (needsConversion) {
|
|
1436
|
+
for (const item of pivotRows) {
|
|
1437
|
+
const fk = item[ownerProp.name];
|
|
1438
|
+
if (fk != null) {
|
|
1439
|
+
item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const classMeta = new Map(targets.map(t => [t.class, t]));
|
|
1444
|
+
const rowsByTarget = new Map();
|
|
1445
|
+
for (const row of pivotRows) {
|
|
1446
|
+
const discValue = row[discriminatorColumn];
|
|
1447
|
+
const targetClass = prop.discriminatorMap[discValue];
|
|
1448
|
+
const targetMeta = classMeta.get(targetClass);
|
|
1449
|
+
/* v8 ignore next 3 - defensive: unknown discriminator value */
|
|
1450
|
+
if (!targetMeta) {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const list = rowsByTarget.get(targetMeta) ?? [];
|
|
1454
|
+
list.push(row);
|
|
1455
|
+
rowsByTarget.set(targetMeta, list);
|
|
1456
|
+
}
|
|
1457
|
+
// Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
|
|
1458
|
+
// to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
|
|
1459
|
+
// populateFilter is a filter on the populated collection; since union-target splits the pivot
|
|
1460
|
+
// and target queries, we merge it into the target-level `where` instead of wrapping it on the
|
|
1461
|
+
// pivot query (where joins to target tables aren't available).
|
|
1462
|
+
// Hoisted above the loop since `options` doesn't change per target.
|
|
1463
|
+
const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
|
|
1464
|
+
const populate = options.populate ?? [];
|
|
1465
|
+
const orphanedRows = new Set();
|
|
1466
|
+
for (const [targetMeta, rows] of rowsByTarget) {
|
|
1467
|
+
const targetIds = rows.map(r => r[prop.discriminator]);
|
|
1468
|
+
// Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
|
|
1469
|
+
// metadata validation time, so a single primary key column is guaranteed here.
|
|
1470
|
+
const pkCol = targetMeta.primaryKeys[0];
|
|
1471
|
+
let cond = { [pkCol]: { $in: targetIds } };
|
|
1472
|
+
if (!Utils.isEmpty(where)) {
|
|
1473
|
+
cond = { $and: [cond, where] };
|
|
1474
|
+
}
|
|
1475
|
+
if (!Utils.isEmpty(populateFilter)) {
|
|
1476
|
+
cond = { $and: [cond, populateFilter] };
|
|
1477
|
+
}
|
|
1478
|
+
const results = (await this.find(targetMeta.class, cond, {
|
|
1479
|
+
ctx,
|
|
1480
|
+
...childOptions,
|
|
1481
|
+
populate: populate,
|
|
1482
|
+
}));
|
|
1483
|
+
const byPk = new Map();
|
|
1484
|
+
for (const row of results) {
|
|
1485
|
+
Object.defineProperty(row, 'constructor', {
|
|
1486
|
+
value: targetMeta.class,
|
|
1487
|
+
enumerable: false,
|
|
1488
|
+
configurable: true,
|
|
1489
|
+
});
|
|
1490
|
+
byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
|
|
1491
|
+
}
|
|
1492
|
+
for (const row of rows) {
|
|
1493
|
+
const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
|
|
1494
|
+
const entity = byPk.get(pkHash);
|
|
1495
|
+
if (entity == null) {
|
|
1496
|
+
orphanedRows.add(row);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
row[prop.discriminator] = entity;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
|
|
1503
|
+
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
|
|
1504
|
+
}
|
|
1343
1505
|
/**
|
|
1344
1506
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1345
1507
|
*/
|
|
@@ -1364,6 +1526,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1364
1526
|
}
|
|
1365
1527
|
return undefined;
|
|
1366
1528
|
}
|
|
1529
|
+
/**
|
|
1530
|
+
* The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
|
|
1531
|
+
* representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
|
|
1532
|
+
* to JS format for consistent key hashing in `buildPivotResultMap`.
|
|
1533
|
+
*/
|
|
1534
|
+
convertOwnerPksForPivotQuery(owners, ownerMeta) {
|
|
1535
|
+
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1536
|
+
const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1537
|
+
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1538
|
+
/* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1539
|
+
if (needsConversion) {
|
|
1540
|
+
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1541
|
+
}
|
|
1542
|
+
return { ownerPks, needsConversion, pkProp };
|
|
1543
|
+
}
|
|
1367
1544
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1368
1545
|
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1369
1546
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
@@ -1430,6 +1607,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1430
1607
|
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
1431
1608
|
return true;
|
|
1432
1609
|
}
|
|
1610
|
+
// Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
|
|
1611
|
+
// target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
|
|
1612
|
+
// Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
|
|
1613
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1433
1616
|
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1434
1617
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1435
1618
|
return false;
|
|
@@ -1544,6 +1727,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1544
1727
|
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
1545
1728
|
const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
|
|
1546
1729
|
qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
|
|
1730
|
+
// For polymorphic targets that are TPT child entities, INNER JOIN parent tables so that
|
|
1731
|
+
// filter conditions referencing parent-table columns resolve to the correct alias. The
|
|
1732
|
+
// INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
|
|
1733
|
+
// keeps the resulting query valid for rows pointing to other polymorphic targets.
|
|
1734
|
+
if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
|
|
1735
|
+
this.addTPTParentJoinsForRelation(qb, targetMeta, tableAlias, targetPath);
|
|
1736
|
+
}
|
|
1547
1737
|
// For polymorphic targets that are TPT base classes, also LEFT JOIN
|
|
1548
1738
|
// all descendant tables so child-specific fields can be selected.
|
|
1549
1739
|
if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptChildren?.length && !ref) {
|
|
@@ -1592,17 +1782,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1592
1782
|
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1593
1783
|
// For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
|
|
1594
1784
|
if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
|
|
1595
|
-
|
|
1596
|
-
let childMeta = meta2;
|
|
1597
|
-
while (childMeta.tptParent) {
|
|
1598
|
-
const parentMeta = childMeta.tptParent;
|
|
1599
|
-
const parentAlias = qb.getNextAlias(parentMeta.className);
|
|
1600
|
-
qb.createAlias(parentMeta.class, parentAlias);
|
|
1601
|
-
qb.state.tptAlias[`${tableAlias}:${parentMeta.className}`] = parentAlias;
|
|
1602
|
-
qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${path}.[tpt]${childMeta.className}`);
|
|
1603
|
-
childAlias = parentAlias;
|
|
1604
|
-
childMeta = parentMeta;
|
|
1605
|
-
}
|
|
1785
|
+
this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
|
|
1606
1786
|
}
|
|
1607
1787
|
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1608
1788
|
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
@@ -1650,6 +1830,25 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1650
1830
|
}
|
|
1651
1831
|
return fields;
|
|
1652
1832
|
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
1835
|
+
* Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
|
|
1836
|
+
* when filter conditions reference parent-table columns.
|
|
1837
|
+
* @internal
|
|
1838
|
+
*/
|
|
1839
|
+
addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
|
|
1840
|
+
let childAlias = leafAlias;
|
|
1841
|
+
let childMeta = leafMeta;
|
|
1842
|
+
while (childMeta.tptParent) {
|
|
1843
|
+
const parentMeta = childMeta.tptParent;
|
|
1844
|
+
const parentAlias = qb.getNextAlias(parentMeta.className);
|
|
1845
|
+
qb.createAlias(parentMeta.class, parentAlias);
|
|
1846
|
+
qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
|
|
1847
|
+
qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
|
|
1848
|
+
childAlias = parentAlias;
|
|
1849
|
+
childMeta = parentMeta;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1653
1852
|
/**
|
|
1654
1853
|
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1655
1854
|
* @internal
|
|
@@ -1761,18 +1960,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1761
1960
|
});
|
|
1762
1961
|
}
|
|
1763
1962
|
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
|
|
1963
|
+
const sourceAlias = qb.helper.getTPTAliasForProperty(prop.name, tableAlias);
|
|
1764
1964
|
if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
|
|
1765
1965
|
return prop.fieldNames.map((col, idx) => {
|
|
1766
1966
|
if (!prop.customTypes[idx]?.convertToJSValueSQL) {
|
|
1767
1967
|
return col;
|
|
1768
1968
|
}
|
|
1769
|
-
const prefixed = this.platform.quoteIdentifier(`${
|
|
1969
|
+
const prefixed = this.platform.quoteIdentifier(`${sourceAlias}.${col}`);
|
|
1770
1970
|
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${col}`);
|
|
1771
1971
|
return raw(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
|
|
1772
1972
|
});
|
|
1773
1973
|
}
|
|
1774
1974
|
if (prop.customType?.convertToJSValueSQL) {
|
|
1775
|
-
const prefixed = this.platform.quoteIdentifier(`${
|
|
1975
|
+
const prefixed = this.platform.quoteIdentifier(`${sourceAlias}.${prop.fieldNames[0]}`);
|
|
1776
1976
|
return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
|
|
1777
1977
|
}
|
|
1778
1978
|
if (prop.formula) {
|
|
@@ -1781,7 +1981,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1781
1981
|
const columns = meta.createColumnMappingObject(tableAlias);
|
|
1782
1982
|
return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
|
|
1783
1983
|
}
|
|
1784
|
-
const sourceAlias = qb.helper.getTPTAliasForProperty(prop.name, tableAlias);
|
|
1785
1984
|
return prop.fieldNames.map(fieldName => {
|
|
1786
1985
|
return raw('?? as ??', [`${sourceAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
|
|
1787
1986
|
});
|
|
@@ -1798,6 +1997,57 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1798
1997
|
}
|
|
1799
1998
|
return qb;
|
|
1800
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
|
|
2002
|
+
* without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
|
|
2003
|
+
* Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
|
|
2004
|
+
* `@Unique`. Strings are returned unchanged.
|
|
2005
|
+
*/
|
|
2006
|
+
renderPartialIndexWhere(entityName, where) {
|
|
2007
|
+
if (typeof where === 'string') {
|
|
2008
|
+
return where;
|
|
2009
|
+
}
|
|
2010
|
+
const name = Utils.className(entityName);
|
|
2011
|
+
if (where == null || (Utils.isPlainObject(where) && Object.keys(where).length === 0)) {
|
|
2012
|
+
throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`);
|
|
2013
|
+
}
|
|
2014
|
+
const alias = '__p';
|
|
2015
|
+
const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias);
|
|
2016
|
+
qb.where(where);
|
|
2017
|
+
const sql = qb.getFormattedQuery();
|
|
2018
|
+
// Relation traversal produces join clauses whose aliased identifiers can't be inlined
|
|
2019
|
+
// into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL.
|
|
2020
|
+
if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) {
|
|
2021
|
+
throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`);
|
|
2022
|
+
}
|
|
2023
|
+
// Anchor at end-of-string only — the synthetic QB has no top-level order by / limit /
|
|
2024
|
+
// group by / having / offset, so any such keyword inside the captured predicate is
|
|
2025
|
+
// inside a subquery and must not terminate the match.
|
|
2026
|
+
const match = /\bwhere\s+([\s\S]+)$/i.exec(sql);
|
|
2027
|
+
if (!match) {
|
|
2028
|
+
throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`);
|
|
2029
|
+
}
|
|
2030
|
+
const quote = (s) => this.platform.quoteIdentifier(s);
|
|
2031
|
+
const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g');
|
|
2032
|
+
const stripped = match[1].replace(aliasPrefix, '').trim();
|
|
2033
|
+
// Any qualified column reference remaining after the alias strip points at another table or
|
|
2034
|
+
// subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both
|
|
2035
|
+
// QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs
|
|
2036
|
+
// (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside
|
|
2037
|
+
// them (e.g. JSON path operands like `'$.path'`) don't trip the guard.
|
|
2038
|
+
// Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls
|
|
2039
|
+
// (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not
|
|
2040
|
+
// followed by `(` is treated as a cross-table column reference.
|
|
2041
|
+
const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''");
|
|
2042
|
+
const quotedIdent = String.raw `(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`;
|
|
2043
|
+
const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`;
|
|
2044
|
+
const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`);
|
|
2045
|
+
const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/;
|
|
2046
|
+
if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) {
|
|
2047
|
+
throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` references another table or subquery which cannot be inlined into a CREATE INDEX ... WHERE clause.`);
|
|
2048
|
+
}
|
|
2049
|
+
return stripped;
|
|
2050
|
+
}
|
|
1801
2051
|
resolveConnectionType(args) {
|
|
1802
2052
|
if (args.ctx) {
|
|
1803
2053
|
return 'write';
|
|
@@ -1824,7 +2074,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1824
2074
|
for (const prop of meta.relations) {
|
|
1825
2075
|
if (collections[prop.name]) {
|
|
1826
2076
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1827
|
-
const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
|
|
2077
|
+
const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext, pickAbortOptions(options));
|
|
1828
2078
|
persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
|
|
1829
2079
|
await this.rethrow(persister.execute());
|
|
1830
2080
|
}
|
|
@@ -1833,6 +2083,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1833
2083
|
async lockPessimistic(entity, options) {
|
|
1834
2084
|
const meta = helper(entity).__meta;
|
|
1835
2085
|
const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
|
|
2086
|
+
qb.setAbortOptions(pickAbortOptions(options));
|
|
1836
2087
|
const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
|
|
1837
2088
|
qb.select(raw('1'))
|
|
1838
2089
|
.where(cond)
|
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -30,7 +30,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
30
30
|
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
|
|
31
31
|
/**
|
|
32
32
|
* Quotes a key for use inside a JSON path expression (e.g. `$.key`).
|
|
33
|
-
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
|
|
33
|
+
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
|
|
34
|
+
* with embedded `\` and `"` escaped per the JSON path string syntax.
|
|
34
35
|
* @internal
|
|
35
36
|
*/
|
|
36
37
|
quoteJsonKey(key: string): string;
|
|
@@ -50,8 +51,19 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
50
51
|
* @internal
|
|
51
52
|
*/
|
|
52
53
|
quoteCollation(collation: string): string;
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
|
|
56
|
+
* so word-chars alone would reject valid real-world collations.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
validateCollationName(collation: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
|
|
62
|
+
* SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
|
|
63
|
+
* names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
caseInsensitiveCollationNames(): boolean;
|
|
55
67
|
/** @internal */
|
|
56
68
|
validateJsonPropertyName(name: string): void;
|
|
57
69
|
/**
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -66,18 +66,23 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
66
66
|
}
|
|
67
67
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
68
68
|
const [a, ...b] = path;
|
|
69
|
+
const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
|
|
69
70
|
if (aliased) {
|
|
70
|
-
return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)},
|
|
71
|
+
return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
|
|
71
72
|
}
|
|
72
|
-
return raw(`json_extract(${this.quoteIdentifier(a)},
|
|
73
|
+
return raw(`json_extract(${this.quoteIdentifier(a)}, ${jsonPath})`);
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
75
76
|
* Quotes a key for use inside a JSON path expression (e.g. `$.key`).
|
|
76
|
-
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
|
|
77
|
+
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
|
|
78
|
+
* with embedded `\` and `"` escaped per the JSON path string syntax.
|
|
77
79
|
* @internal
|
|
78
80
|
*/
|
|
79
81
|
quoteJsonKey(key) {
|
|
80
|
-
|
|
82
|
+
if (/^[a-z]\w*$/i.test(key)) {
|
|
83
|
+
return key;
|
|
84
|
+
}
|
|
85
|
+
return `"${key.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
81
86
|
}
|
|
82
87
|
getJsonIndexDefinition(index) {
|
|
83
88
|
return index.columnNames.map(column => {
|
|
@@ -123,12 +128,25 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
123
128
|
this.validateCollationName(collation);
|
|
124
129
|
return this.quoteIdentifier(collation);
|
|
125
130
|
}
|
|
126
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
|
|
133
|
+
* so word-chars alone would reject valid real-world collations.
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
127
136
|
validateCollationName(collation) {
|
|
128
|
-
if (!/^[\w]+$/.test(collation)) {
|
|
129
|
-
throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
|
|
137
|
+
if (!/^[\w\-.]+$/.test(collation)) {
|
|
138
|
+
throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters, hyphens, and dots.`);
|
|
130
139
|
}
|
|
131
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
|
|
143
|
+
* SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
|
|
144
|
+
* names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
caseInsensitiveCollationNames() {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
132
150
|
/** @internal */
|
|
133
151
|
validateJsonPropertyName(name) {
|
|
134
152
|
if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) {
|