@mikro-orm/sql 7.1.0-dev.3 → 7.1.0-dev.31
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 +2 -2
- package/AbstractSqlDriver.d.ts +19 -1
- package/AbstractSqlDriver.js +215 -16
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.js +13 -2
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- 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 +9 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +72 -6
- 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 +1 -0
- 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 +36 -0
- package/query/QueryBuilder.js +63 -1
- 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
|
@@ -60,7 +60,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
60
60
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
61
61
|
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
|
|
62
62
|
/** Executes a SQL query and returns an async iterable that yields results row by row. */
|
|
63
|
-
stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions): AsyncIterableIterator<T>;
|
|
63
|
+
stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
|
|
64
64
|
/** @inheritDoc */
|
|
65
65
|
executeDump(dump: string): Promise<void>;
|
|
66
66
|
protected getSql(query: string, formatted: string, context?: LogContext): string;
|
package/AbstractSqlConnection.js
CHANGED
|
@@ -190,7 +190,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
190
190
|
}, { ...q, ...loggerContext });
|
|
191
191
|
}
|
|
192
192
|
/** Executes a SQL query and returns an async iterable that yields results row by row. */
|
|
193
|
-
async *stream(query, params = [], ctx, loggerContext) {
|
|
193
|
+
async *stream(query, params = [], ctx, loggerContext, chunkSize) {
|
|
194
194
|
await this.ensureConnection();
|
|
195
195
|
const q = this.prepareQuery(query, params);
|
|
196
196
|
const sql = this.getSql(q.query, q.formatted, loggerContext);
|
|
@@ -203,7 +203,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
203
203
|
parameters: [],
|
|
204
204
|
};
|
|
205
205
|
try {
|
|
206
|
-
const res = (ctx ?? this.getClient()).getExecutor().stream(compiled,
|
|
206
|
+
const res = (ctx ?? this.getClient()).getExecutor().stream(compiled, chunkSize ?? 100);
|
|
207
207
|
this.logQuery(sql, {
|
|
208
208
|
sql,
|
|
209
209
|
params,
|
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
31
31
|
protected findFromVirtual<T extends object>(entityName: EntityName<T>, where: ObjectQuery<T>, options: FindOptions<T, any> | CountOptions<T, any>, type: QueryType): Promise<EntityData<T>[] | number>;
|
|
32
32
|
protected streamFromVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any>): AsyncIterableIterator<EntityData<T>>;
|
|
33
33
|
protected wrapVirtualExpressionInSubquery<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any>, type: QueryType): Promise<T[] | number>;
|
|
34
|
-
protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options:
|
|
34
|
+
protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
|
|
35
35
|
/**
|
|
36
36
|
* Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
|
|
37
37
|
* Force balanced strategy to load to-many relations via separate queries.
|
|
@@ -83,11 +83,22 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
83
83
|
* Uses single query with join via virtual relation on pivot.
|
|
84
84
|
*/
|
|
85
85
|
protected loadPolymorphicPivotInverseSide<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>): Promise<Dictionary<T[]>>;
|
|
86
|
+
/**
|
|
87
|
+
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
88
|
+
* Each pivot row's discriminator column selects which target table to hydrate.
|
|
89
|
+
*/
|
|
90
|
+
protected loadFromUnionTargetPolymorphicPivotTable<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where?: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean): Promise<Dictionary<T[]>>;
|
|
86
91
|
/**
|
|
87
92
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
88
93
|
*/
|
|
89
94
|
private buildPivotResultMap;
|
|
90
95
|
private wrapPopulateFilter;
|
|
96
|
+
/**
|
|
97
|
+
* The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
|
|
98
|
+
* representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
|
|
99
|
+
* to JS format for consistent key hashing in `buildPivotResultMap`.
|
|
100
|
+
*/
|
|
101
|
+
private convertOwnerPksForPivotQuery;
|
|
91
102
|
private getPivotOrderBy;
|
|
92
103
|
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
|
|
93
104
|
stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
|
|
@@ -136,6 +147,13 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
136
147
|
mapPropToFieldNames<T extends object>(qb: AnyQueryBuilder<T>, prop: EntityProperty<T>, tableAlias: string, meta: EntityMetadata<T>, schema?: string, explicitFields?: readonly InternalField<T>[]): InternalField<T>[];
|
|
137
148
|
/** @internal */
|
|
138
149
|
createQueryBuilder<T extends object>(entityName: EntityName<T> | AnyQueryBuilder<T>, ctx?: Transaction, preferredConnectionType?: ConnectionType, convertCustomTypes?: boolean, loggerContext?: LoggingOptions, alias?: string, em?: SqlEntityManager): AnyQueryBuilder<T>;
|
|
150
|
+
/**
|
|
151
|
+
* Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
|
|
152
|
+
* without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
|
|
153
|
+
* Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
|
|
154
|
+
* `@Unique`. Strings are returned unchanged.
|
|
155
|
+
*/
|
|
156
|
+
renderPartialIndexWhere<T extends object>(entityName: EntityName<T>, where: string | FilterQuery<T>): string;
|
|
139
157
|
protected resolveConnectionType(args: {
|
|
140
158
|
ctx?: Transaction;
|
|
141
159
|
connectionType?: ConnectionType;
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -34,6 +34,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
34
34
|
return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
|
|
35
35
|
}
|
|
36
36
|
validateSqlOptions(options) {
|
|
37
|
+
if (options.using && !options.indexHint) {
|
|
38
|
+
const names = Utils.asArray(options.using);
|
|
39
|
+
const hint = this.platform.formatIndexHint(names);
|
|
40
|
+
if (hint) {
|
|
41
|
+
options.indexHint = hint;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
37
44
|
if (options.collation != null && typeof options.collation !== 'string') {
|
|
38
45
|
throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
|
|
39
46
|
}
|
|
@@ -50,7 +57,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
50
57
|
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
|
|
51
58
|
const joinedProps = this.joinedProps(meta, populate, options);
|
|
52
59
|
const schema = this.getSchemaName(meta, options);
|
|
53
|
-
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
|
|
60
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
|
|
61
|
+
.withSchema(schema)
|
|
62
|
+
.cache(false);
|
|
54
63
|
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
55
64
|
const orderBy = this.buildOrderBy(qb, meta, populate, options);
|
|
56
65
|
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
@@ -88,6 +97,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
88
97
|
if (options.em) {
|
|
89
98
|
await qb.applyJoinedFilters(options.em, options.filters);
|
|
90
99
|
}
|
|
100
|
+
if (options._partitionLimit) {
|
|
101
|
+
qb.setPartitionLimit(options._partitionLimit);
|
|
102
|
+
}
|
|
91
103
|
return qb;
|
|
92
104
|
}
|
|
93
105
|
async find(entityName, where, options = {}) {
|
|
@@ -221,7 +233,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
221
233
|
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
|
|
222
234
|
const query = native.compile();
|
|
223
235
|
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);
|
|
236
|
+
const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext, options.chunkSize);
|
|
225
237
|
for await (const row of res) {
|
|
226
238
|
yield this.mapResult(row, meta);
|
|
227
239
|
}
|
|
@@ -1099,8 +1111,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1099
1111
|
const pks = wrapped.getPrimaryKeys(true);
|
|
1100
1112
|
const snap = coll.getSnapshot();
|
|
1101
1113
|
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
1102
|
-
|
|
1103
|
-
|
|
1114
|
+
// For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
|
|
1115
|
+
// persister can write it alongside the FK id. Memoized per sync-run because a collection can
|
|
1116
|
+
// hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
|
|
1117
|
+
// chain + re-scans Object.entries each call.
|
|
1118
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
|
|
1119
|
+
const classToDisc = new Map();
|
|
1120
|
+
const toDiff = (item) => {
|
|
1121
|
+
const keys = helper(item).getPrimaryKeys(true);
|
|
1122
|
+
if (!isUnionTargetMN) {
|
|
1123
|
+
return keys;
|
|
1124
|
+
}
|
|
1125
|
+
let disc = classToDisc.get(item.constructor);
|
|
1126
|
+
if (!classToDisc.has(item.constructor)) {
|
|
1127
|
+
disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
|
|
1128
|
+
if (disc === undefined) {
|
|
1129
|
+
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.`);
|
|
1130
|
+
}
|
|
1131
|
+
classToDisc.set(item.constructor, disc);
|
|
1132
|
+
}
|
|
1133
|
+
return [disc, ...keys];
|
|
1134
|
+
};
|
|
1135
|
+
const snapshot = snap ? snap.map(toDiff) : [];
|
|
1136
|
+
const current = coll.getItems(false).map(toDiff);
|
|
1104
1137
|
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
1105
1138
|
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
1106
1139
|
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
@@ -1172,21 +1205,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1172
1205
|
return {};
|
|
1173
1206
|
}
|
|
1174
1207
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1208
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
1209
|
+
return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1210
|
+
}
|
|
1175
1211
|
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1176
1212
|
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1177
1213
|
}
|
|
1178
1214
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1179
1215
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1180
1216
|
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
|
-
}
|
|
1217
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1190
1218
|
const cond = {
|
|
1191
1219
|
[pivotProp2.name]: { $in: ownerPks },
|
|
1192
1220
|
};
|
|
@@ -1201,7 +1229,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1201
1229
|
const fields = pivotJoin
|
|
1202
1230
|
? [pivotProp1.name, pivotProp2.name]
|
|
1203
1231
|
: [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
1204
|
-
const
|
|
1232
|
+
const pivotFindOptions = {
|
|
1205
1233
|
ctx,
|
|
1206
1234
|
...options,
|
|
1207
1235
|
fields,
|
|
@@ -1217,10 +1245,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1217
1245
|
},
|
|
1218
1246
|
],
|
|
1219
1247
|
populateWhere: undefined,
|
|
1220
|
-
// @ts-ignore
|
|
1221
1248
|
_populateWhere: 'infer',
|
|
1222
1249
|
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
1223
|
-
}
|
|
1250
|
+
};
|
|
1251
|
+
if (pivotFindOptions._partitionLimit) {
|
|
1252
|
+
pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
|
|
1253
|
+
}
|
|
1254
|
+
const res = await this.find(pivotMeta.class, where, pivotFindOptions);
|
|
1224
1255
|
// Convert result FK values back to JS format so key hashing
|
|
1225
1256
|
// in buildPivotResultMap is consistent with the owner keys.
|
|
1226
1257
|
if (needsConversion) {
|
|
@@ -1340,6 +1371,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1340
1371
|
});
|
|
1341
1372
|
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1342
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
1376
|
+
* Each pivot row's discriminator column selects which target table to hydrate.
|
|
1377
|
+
*/
|
|
1378
|
+
async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
|
|
1379
|
+
// :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
|
|
1380
|
+
// class per item, but the ref-mode map only carries flat PK values and would hydrate every
|
|
1381
|
+
// row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
|
|
1382
|
+
if (pivotJoin) {
|
|
1383
|
+
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.`);
|
|
1384
|
+
}
|
|
1385
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1386
|
+
const targets = prop.polymorphTargets;
|
|
1387
|
+
const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
|
|
1388
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
1389
|
+
const ownerMeta = ownerProp.targetMeta;
|
|
1390
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1391
|
+
const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
|
|
1392
|
+
ctx,
|
|
1393
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1394
|
+
fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
|
|
1395
|
+
populateWhere: undefined,
|
|
1396
|
+
// @ts-ignore
|
|
1397
|
+
_populateWhere: 'infer',
|
|
1398
|
+
}));
|
|
1399
|
+
/* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1400
|
+
if (needsConversion) {
|
|
1401
|
+
for (const item of pivotRows) {
|
|
1402
|
+
const fk = item[ownerProp.name];
|
|
1403
|
+
if (fk != null) {
|
|
1404
|
+
item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const classMeta = new Map(targets.map(t => [t.class, t]));
|
|
1409
|
+
const rowsByTarget = new Map();
|
|
1410
|
+
for (const row of pivotRows) {
|
|
1411
|
+
const discValue = row[discriminatorColumn];
|
|
1412
|
+
const targetClass = prop.discriminatorMap[discValue];
|
|
1413
|
+
const targetMeta = classMeta.get(targetClass);
|
|
1414
|
+
/* v8 ignore next 3 - defensive: unknown discriminator value */
|
|
1415
|
+
if (!targetMeta) {
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const list = rowsByTarget.get(targetMeta) ?? [];
|
|
1419
|
+
list.push(row);
|
|
1420
|
+
rowsByTarget.set(targetMeta, list);
|
|
1421
|
+
}
|
|
1422
|
+
// Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
|
|
1423
|
+
// to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
|
|
1424
|
+
// populateFilter is a filter on the populated collection; since union-target splits the pivot
|
|
1425
|
+
// and target queries, we merge it into the target-level `where` instead of wrapping it on the
|
|
1426
|
+
// pivot query (where joins to target tables aren't available).
|
|
1427
|
+
// Hoisted above the loop since `options` doesn't change per target.
|
|
1428
|
+
const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
|
|
1429
|
+
const populate = options.populate ?? [];
|
|
1430
|
+
const orphanedRows = new Set();
|
|
1431
|
+
for (const [targetMeta, rows] of rowsByTarget) {
|
|
1432
|
+
const targetIds = rows.map(r => r[prop.discriminator]);
|
|
1433
|
+
// Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
|
|
1434
|
+
// metadata validation time, so a single primary key column is guaranteed here.
|
|
1435
|
+
const pkCol = targetMeta.primaryKeys[0];
|
|
1436
|
+
let cond = { [pkCol]: { $in: targetIds } };
|
|
1437
|
+
if (!Utils.isEmpty(where)) {
|
|
1438
|
+
cond = { $and: [cond, where] };
|
|
1439
|
+
}
|
|
1440
|
+
if (!Utils.isEmpty(populateFilter)) {
|
|
1441
|
+
cond = { $and: [cond, populateFilter] };
|
|
1442
|
+
}
|
|
1443
|
+
const results = (await this.find(targetMeta.class, cond, {
|
|
1444
|
+
ctx,
|
|
1445
|
+
...childOptions,
|
|
1446
|
+
populate: populate,
|
|
1447
|
+
}));
|
|
1448
|
+
const byPk = new Map();
|
|
1449
|
+
for (const row of results) {
|
|
1450
|
+
Object.defineProperty(row, 'constructor', {
|
|
1451
|
+
value: targetMeta.class,
|
|
1452
|
+
enumerable: false,
|
|
1453
|
+
configurable: true,
|
|
1454
|
+
});
|
|
1455
|
+
byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
|
|
1456
|
+
}
|
|
1457
|
+
for (const row of rows) {
|
|
1458
|
+
const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
|
|
1459
|
+
const entity = byPk.get(pkHash);
|
|
1460
|
+
if (entity == null) {
|
|
1461
|
+
orphanedRows.add(row);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
row[prop.discriminator] = entity;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
|
|
1468
|
+
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
|
|
1469
|
+
}
|
|
1343
1470
|
/**
|
|
1344
1471
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1345
1472
|
*/
|
|
@@ -1364,6 +1491,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1364
1491
|
}
|
|
1365
1492
|
return undefined;
|
|
1366
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
|
|
1496
|
+
* representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
|
|
1497
|
+
* to JS format for consistent key hashing in `buildPivotResultMap`.
|
|
1498
|
+
*/
|
|
1499
|
+
convertOwnerPksForPivotQuery(owners, ownerMeta) {
|
|
1500
|
+
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1501
|
+
const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1502
|
+
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1503
|
+
/* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1504
|
+
if (needsConversion) {
|
|
1505
|
+
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1506
|
+
}
|
|
1507
|
+
return { ownerPks, needsConversion, pkProp };
|
|
1508
|
+
}
|
|
1367
1509
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1368
1510
|
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1369
1511
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
@@ -1430,6 +1572,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1430
1572
|
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
1431
1573
|
return true;
|
|
1432
1574
|
}
|
|
1575
|
+
// Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
|
|
1576
|
+
// target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
|
|
1577
|
+
// Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
|
|
1578
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1433
1581
|
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1434
1582
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1435
1583
|
return false;
|
|
@@ -1798,6 +1946,57 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1798
1946
|
}
|
|
1799
1947
|
return qb;
|
|
1800
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
|
|
1951
|
+
* without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
|
|
1952
|
+
* Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
|
|
1953
|
+
* `@Unique`. Strings are returned unchanged.
|
|
1954
|
+
*/
|
|
1955
|
+
renderPartialIndexWhere(entityName, where) {
|
|
1956
|
+
if (typeof where === 'string') {
|
|
1957
|
+
return where;
|
|
1958
|
+
}
|
|
1959
|
+
const name = Utils.className(entityName);
|
|
1960
|
+
if (where == null || (Utils.isPlainObject(where) && Object.keys(where).length === 0)) {
|
|
1961
|
+
throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`);
|
|
1962
|
+
}
|
|
1963
|
+
const alias = '__p';
|
|
1964
|
+
const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias);
|
|
1965
|
+
qb.where(where);
|
|
1966
|
+
const sql = qb.getFormattedQuery();
|
|
1967
|
+
// Relation traversal produces join clauses whose aliased identifiers can't be inlined
|
|
1968
|
+
// into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL.
|
|
1969
|
+
if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) {
|
|
1970
|
+
throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`);
|
|
1971
|
+
}
|
|
1972
|
+
// Anchor at end-of-string only — the synthetic QB has no top-level order by / limit /
|
|
1973
|
+
// group by / having / offset, so any such keyword inside the captured predicate is
|
|
1974
|
+
// inside a subquery and must not terminate the match.
|
|
1975
|
+
const match = /\bwhere\s+([\s\S]+)$/i.exec(sql);
|
|
1976
|
+
if (!match) {
|
|
1977
|
+
throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`);
|
|
1978
|
+
}
|
|
1979
|
+
const quote = (s) => this.platform.quoteIdentifier(s);
|
|
1980
|
+
const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g');
|
|
1981
|
+
const stripped = match[1].replace(aliasPrefix, '').trim();
|
|
1982
|
+
// Any qualified column reference remaining after the alias strip points at another table or
|
|
1983
|
+
// subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both
|
|
1984
|
+
// QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs
|
|
1985
|
+
// (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside
|
|
1986
|
+
// them (e.g. JSON path operands like `'$.path'`) don't trip the guard.
|
|
1987
|
+
// Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls
|
|
1988
|
+
// (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not
|
|
1989
|
+
// followed by `(` is treated as a cross-table column reference.
|
|
1990
|
+
const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''");
|
|
1991
|
+
const quotedIdent = String.raw `(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`;
|
|
1992
|
+
const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`;
|
|
1993
|
+
const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`);
|
|
1994
|
+
const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/;
|
|
1995
|
+
if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) {
|
|
1996
|
+
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.`);
|
|
1997
|
+
}
|
|
1998
|
+
return stripped;
|
|
1999
|
+
}
|
|
1801
2000
|
resolveConnectionType(args) {
|
|
1802
2001
|
if (args.ctx) {
|
|
1803
2002
|
return 'write';
|
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)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QueryHelper, } from '@mikro-orm/core';
|
|
1
2
|
class InsertStatement {
|
|
2
3
|
order;
|
|
3
4
|
#keys;
|
|
@@ -106,6 +107,16 @@ export class PivotCollectionPersister {
|
|
|
106
107
|
buildPivotKeysAndData(prop, fks, pks, deleteAll = false) {
|
|
107
108
|
let data;
|
|
108
109
|
let keys;
|
|
110
|
+
// Union-target polymorphic M:N prepends the per-row discriminator to `fks` in syncCollections;
|
|
111
|
+
// Rails-style polymorphic M:N uses a static discriminatorValue on the prop. Normalize to a single
|
|
112
|
+
// "current row's discriminator" value so the prepend block below handles both cases uniformly.
|
|
113
|
+
let rowDiscriminator;
|
|
114
|
+
if (QueryHelper.isUnionTargetPolymorphic(prop) && !deleteAll && fks.length > 0) {
|
|
115
|
+
[rowDiscriminator, ...fks] = fks;
|
|
116
|
+
}
|
|
117
|
+
else if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
118
|
+
rowDiscriminator = prop.discriminatorValue;
|
|
119
|
+
}
|
|
109
120
|
if (deleteAll) {
|
|
110
121
|
data = pks;
|
|
111
122
|
keys = prop.joinColumns;
|
|
@@ -116,8 +127,8 @@ export class PivotCollectionPersister {
|
|
|
116
127
|
? [...prop.inverseJoinColumns, ...prop.joinColumns]
|
|
117
128
|
: [...prop.joinColumns, ...prop.inverseJoinColumns];
|
|
118
129
|
}
|
|
119
|
-
if (
|
|
120
|
-
data = [
|
|
130
|
+
if (rowDiscriminator !== undefined) {
|
|
131
|
+
data = [rowDiscriminator, ...data];
|
|
121
132
|
keys = [prop.discriminatorColumn, ...keys];
|
|
122
133
|
}
|
|
123
134
|
return { data, keys };
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
|
|
3
3
|
</h1>
|
|
4
4
|
|
|
5
|
-
TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
|
|
5
|
+
TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
|
|
6
6
|
|
|
7
7
|
> Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
|
|
8
8
|
|
|
@@ -19,6 +19,7 @@ Install a driver package for your database:
|
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
21
|
npm install @mikro-orm/postgresql # PostgreSQL
|
|
22
|
+
npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
|
|
22
23
|
npm install @mikro-orm/mysql # MySQL
|
|
23
24
|
npm install @mikro-orm/mariadb # MariaDB
|
|
24
25
|
npm install @mikro-orm/sqlite # SQLite
|
package/SqlEntityManager.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type EntityData, type EntityName, type EntityRepository, type
|
|
1
|
+
import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type CountByOptions, type Dictionary, type EntityData, type EntityKey, type EntityName, type EntityRepository, type FilterQuery, type GetRepository, type LoggingOptions, type QueryResult, type RawQueryFragment } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlDriver } from './AbstractSqlDriver.js';
|
|
3
3
|
import type { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
4
4
|
import type { QueryBuilder } from './query/QueryBuilder.js';
|
|
@@ -29,6 +29,10 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
|
|
|
29
29
|
getKysely<TDB = undefined, TOptions extends GetKyselyOptions = GetKyselyOptions>(options?: TOptions): Kysely<TDB extends undefined ? InferKyselyDB<EntitiesFromManager<this>, TOptions> & InferClassEntityDB<AllEntitiesFromManager<this>, TOptions> : TDB>;
|
|
30
30
|
/** Executes a raw SQL query, using the current transaction context if available. */
|
|
31
31
|
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* @inheritDoc
|
|
34
|
+
*/
|
|
35
|
+
countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
|
|
32
36
|
getRepository<T extends object, U extends EntityRepository<T> = SqlEntityRepository<T>>(entityName: EntityName<T>): GetRepository<T, U>;
|
|
33
37
|
protected applyDiscriminatorCondition<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<Entity>): FilterQuery<Entity>;
|
|
34
38
|
}
|
package/SqlEntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityManager, } from '@mikro-orm/core';
|
|
1
|
+
import { EntityManager, raw, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { MikroKyselyPlugin } from './plugin/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* @inheritDoc
|
|
@@ -35,6 +35,41 @@ export class SqlEntityManager extends EntityManager {
|
|
|
35
35
|
async execute(query, params = [], method = 'all', loggerContext) {
|
|
36
36
|
return this.getDriver().execute(query, params, method, this.getContext(false).getTransactionContext(), loggerContext);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* @inheritDoc
|
|
40
|
+
*/
|
|
41
|
+
async countBy(entityName, groupBy, options = {}) {
|
|
42
|
+
const em = this.getContext(false);
|
|
43
|
+
options = { ...options };
|
|
44
|
+
em.prepareOptions(options);
|
|
45
|
+
const meta = em.getMetadata().find(entityName);
|
|
46
|
+
const fields = Utils.asArray(groupBy);
|
|
47
|
+
const { where: rawWhere, ...countOptions } = options;
|
|
48
|
+
await em.tryFlush(entityName, options);
|
|
49
|
+
const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
|
|
50
|
+
const qb = em.createQueryBuilder(meta.class);
|
|
51
|
+
qb
|
|
52
|
+
.select([...fields, raw('count(*) as cnt')])
|
|
53
|
+
.where(where)
|
|
54
|
+
.groupBy(fields);
|
|
55
|
+
if (countOptions.having) {
|
|
56
|
+
qb.having(countOptions.having);
|
|
57
|
+
}
|
|
58
|
+
if (countOptions.schema) {
|
|
59
|
+
qb.withSchema(countOptions.schema);
|
|
60
|
+
}
|
|
61
|
+
const rows = await qb.execute('all', { mapResults: false });
|
|
62
|
+
const results = {};
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const keyParts = fields.map(f => {
|
|
65
|
+
const col = meta.properties[f]?.fieldNames?.[0] ?? f;
|
|
66
|
+
return String(row[col]);
|
|
67
|
+
});
|
|
68
|
+
const key = keyParts.join(Utils.PK_SEPARATOR);
|
|
69
|
+
results[key] = +row.cnt;
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
38
73
|
getRepository(entityName) {
|
|
39
74
|
return super.getRepository(entityName);
|
|
40
75
|
}
|
package/SqlMikroORM.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AnyEntity, type EntityClass, type EntityManager, type EntityManagerType, type EntitySchema, type IDatabaseDriver, MikroORM, type Options } from '@mikro-orm/core';
|
|
2
|
+
import type { AbstractSqlDriver } from './AbstractSqlDriver.js';
|
|
3
|
+
import type { SqlEntityManager } from './SqlEntityManager.js';
|
|
4
|
+
/** Configuration options shared by all SQL drivers. */
|
|
5
|
+
export type SqlOptions<D extends AbstractSqlDriver = AbstractSqlDriver, EM extends SqlEntityManager<D> = SqlEntityManager<D>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> = Partial<Options<D, EM, Entities>>;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a type-safe configuration object for any SQL driver. The driver class
|
|
8
|
+
* must be passed via `options.driver` (e.g. `SqliteDriver`, `MySqlDriver`, …).
|
|
9
|
+
*/
|
|
10
|
+
export declare function defineSqlConfig<D extends AbstractSqlDriver = AbstractSqlDriver, EM extends SqlEntityManager<D> = SqlEntityManager<D>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]>(options: Partial<Options<D, EM, Entities>>): Partial<Options<D, EM, Entities>>;
|
|
11
|
+
/**
|
|
12
|
+
* Generic entry point for SQL drivers. Use this when consuming `@mikro-orm/sql`
|
|
13
|
+
* directly with a Kysely dialect; for the bundled driver packages prefer
|
|
14
|
+
* `@mikro-orm/sqlite`, `@mikro-orm/postgresql`, etc.
|
|
15
|
+
*
|
|
16
|
+
* @inheritDoc
|
|
17
|
+
*/
|
|
18
|
+
export declare class SqlMikroORM<D extends AbstractSqlDriver = AbstractSqlDriver, EM extends SqlEntityManager<D> = SqlEntityManager<D>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> extends MikroORM<D, EM, Entities> {
|
|
19
|
+
/**
|
|
20
|
+
* @inheritDoc
|
|
21
|
+
*/
|
|
22
|
+
static init<D extends IDatabaseDriver = AbstractSqlDriver, EM extends EntityManager<D> = D[typeof EntityManagerType] & EntityManager<D>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]>(options: Partial<Options<D, EM, Entities>>): Promise<MikroORM<D, EM, Entities>>;
|
|
23
|
+
}
|