@mikro-orm/sql 7.2.7-dev.6 → 7.2.7-dev.8
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/AbstractSqlDriver.d.ts +4 -0
- package/AbstractSqlDriver.js +35 -20
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +1 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +3 -0
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +1 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +8 -3
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
- package/dialects/sqlite/SqlitePlatform.js +9 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -1
- package/package.json +2 -2
- package/query/CriteriaNode.d.ts +1 -0
- package/query/CriteriaNode.js +11 -2
- package/query/ObjectCriteriaNode.js +2 -14
- package/query/QueryBuilder.d.ts +8 -3
- package/query/QueryBuilder.js +45 -13
- package/query/QueryBuilderHelper.js +21 -9
- package/schema/DatabaseTable.js +4 -0
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
18
18
|
evaluateFormula(formula: (...args: any[]) => string | Raw, columns: any, table: FormulaTable): string;
|
|
19
19
|
/** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
|
|
20
20
|
protected getTableProps<T extends object>(meta: EntityMetadata<T>): EntityProperty<T>[];
|
|
21
|
+
/** Columns reloaded by INSERT: generated/default columns any row leaves to the database, and explicit `returning` hints. */
|
|
22
|
+
protected getInsertReturningProperties<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[]): EntityProperty<T>[];
|
|
21
23
|
/** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
|
|
22
24
|
protected getUpdateReturningProperties<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[]): EntityProperty<T>[];
|
|
23
25
|
/** Column expression for the `returning` clause of batch updates, applying `convertToJSValueSQL` of custom types. */
|
|
@@ -63,6 +65,8 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
63
65
|
private buildCloneFields;
|
|
64
66
|
private getCloneableProps;
|
|
65
67
|
nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
|
|
68
|
+
/** Resolves the primary key of every inserted row from the supplied data or the returned rows, and reports them via `insertedIds` when all are known. */
|
|
69
|
+
protected resolveInsertedPrimaryKeys<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[], res: QueryResult<T>): any[][];
|
|
66
70
|
nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T> & UpsertOptions<T>): Promise<QueryResult<T>>;
|
|
67
71
|
nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T> & UpsertManyOptions<T>, transform?: (sql: string, params: any[]) => string): Promise<QueryResult<T>>;
|
|
68
72
|
nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T> | string | any, options?: DeleteOptions<T>): Promise<QueryResult<T>>;
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -49,6 +49,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
49
49
|
getTableProps(meta) {
|
|
50
50
|
return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
|
|
51
51
|
}
|
|
52
|
+
/** Columns reloaded by INSERT: generated/default columns any row leaves to the database, and explicit `returning` hints. */
|
|
53
|
+
getInsertReturningProperties(meta, data) {
|
|
54
|
+
return this.getTableProps(meta).filter(prop => prop.returning ||
|
|
55
|
+
(((prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated) &&
|
|
56
|
+
data.some(d => !(prop.name in d) || isRaw(d[prop.name]))));
|
|
57
|
+
}
|
|
52
58
|
/** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
|
|
53
59
|
getUpdateReturningProperties(meta, data) {
|
|
54
60
|
const returning = new Set();
|
|
@@ -801,10 +807,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
801
807
|
? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')'
|
|
802
808
|
: `(${this.platform.quoteIdentifier(pks[0])})`;
|
|
803
809
|
if (this.platform.usesOutputStatement()) {
|
|
804
|
-
const
|
|
805
|
-
(((prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated) &&
|
|
806
|
-
(!(prop.name in data[0]) || isRaw(data[0][prop.name]))));
|
|
807
|
-
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
810
|
+
const returningFields = Utils.flatten(this.getInsertReturningProperties(meta, data).map(prop => prop.fieldNames));
|
|
808
811
|
sql +=
|
|
809
812
|
returningFields.length > 0
|
|
810
813
|
? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`
|
|
@@ -931,10 +934,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
931
934
|
.join(', ');
|
|
932
935
|
}
|
|
933
936
|
if (meta && this.platform.usesReturningStatement()) {
|
|
934
|
-
const
|
|
935
|
-
(((prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated) &&
|
|
936
|
-
(!(prop.name in data[0]) || isRaw(data[0][prop.name]))));
|
|
937
|
-
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
937
|
+
const returningFields = Utils.flatten(this.getInsertReturningProperties(meta, data).map(prop => prop.fieldNames));
|
|
938
938
|
/* v8 ignore next */
|
|
939
939
|
sql +=
|
|
940
940
|
returningFields.length > 0
|
|
@@ -945,22 +945,35 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
945
945
|
sql = transform(sql);
|
|
946
946
|
}
|
|
947
947
|
const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
|
|
948
|
-
|
|
949
|
-
if (pks.length > 1) {
|
|
950
|
-
// owner has composite pk
|
|
951
|
-
pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
res.row ??= {};
|
|
955
|
-
res.rows ??= [];
|
|
956
|
-
pk = data.map((d, i) => d[pks[0]] ?? res.rows[i]?.[pks[0]]).map(d => [d]);
|
|
957
|
-
res.insertId = res.insertId || res.row[pks[0]];
|
|
958
|
-
}
|
|
948
|
+
const pk = this.resolveInsertedPrimaryKeys(meta, data, res);
|
|
959
949
|
for (let i = 0; i < collections.length; i++) {
|
|
960
950
|
await this.processManyToMany(meta, pk[i], collections[i], false, options);
|
|
961
951
|
}
|
|
962
952
|
return res;
|
|
963
953
|
}
|
|
954
|
+
/** Resolves the primary key of every inserted row from the supplied data or the returned rows, and reports them via `insertedIds` when all are known. */
|
|
955
|
+
resolveInsertedPrimaryKeys(meta, data, res) {
|
|
956
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
957
|
+
if (pks.length > 1) {
|
|
958
|
+
// owner has composite pk
|
|
959
|
+
const pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
|
|
960
|
+
res.insertedIds = pk;
|
|
961
|
+
return pk;
|
|
962
|
+
}
|
|
963
|
+
res.row ??= {};
|
|
964
|
+
res.rows ??= [];
|
|
965
|
+
const ids = data.map((d, i) => {
|
|
966
|
+
// a raw pk expression is resolved by the database, its value only exists in the returned row
|
|
967
|
+
const supplied = d[meta.primaryKeys[0]] ?? d[pks[0]];
|
|
968
|
+
return supplied == null || isRaw(supplied) ? res.rows[i]?.[pks[0]] : supplied;
|
|
969
|
+
});
|
|
970
|
+
res.insertId = res.insertId || res.row[pks[0]];
|
|
971
|
+
// drivers without `returning` leave the ids unknown here, keep the `insertId` fallback for them
|
|
972
|
+
if (ids.every(id => id != null)) {
|
|
973
|
+
res.insertedIds = ids;
|
|
974
|
+
}
|
|
975
|
+
return ids.map(id => [id]);
|
|
976
|
+
}
|
|
964
977
|
async nativeUpdate(entityName, where, data, options = {}) {
|
|
965
978
|
options.convertCustomTypes ??= true;
|
|
966
979
|
const meta = this.metadata.get(entityName);
|
|
@@ -2544,7 +2557,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2544
2557
|
if (ret.length > 0 && !hasExplicitFields && addFormulas) {
|
|
2545
2558
|
// `toString()` must yield the quoted alias like `QueryBuilder.finalize()`, which dedupes formulas by rendered SQL
|
|
2546
2559
|
const quotedAlias = this.platform.quoteIdentifier(alias);
|
|
2547
|
-
|
|
2560
|
+
// TPT parent aliases are only known once the parent joins exist
|
|
2561
|
+
qb.ensureTPTJoins();
|
|
2562
|
+
const columns = meta.createColumnMappingObject(p => qb.helper.getTPTAliasForProperty(p.name, alias), quotedAlias.toString());
|
|
2548
2563
|
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
2549
2564
|
for (const prop of meta.props) {
|
|
2550
2565
|
if (lazyProps.includes(prop)) {
|
|
@@ -59,6 +59,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
59
59
|
};
|
|
60
60
|
isBigIntProperty(prop: EntityProperty): boolean;
|
|
61
61
|
getArrayDeclarationSQL(): string;
|
|
62
|
+
supportsArrayOverlap(): boolean;
|
|
62
63
|
getFloatDeclarationSQL(): string;
|
|
63
64
|
getDoubleDeclarationSQL(): string;
|
|
64
65
|
getEnumTypeDeclarationSQL(column: {
|
|
@@ -25,6 +25,7 @@ export class PostgreSqlExceptionConverter extends ExceptionConverter {
|
|
|
25
25
|
break;
|
|
26
26
|
case '23502':
|
|
27
27
|
return new NotNullConstraintViolationException(exception);
|
|
28
|
+
case '23001':
|
|
28
29
|
case '23503':
|
|
29
30
|
return new ForeignKeyConstraintViolationException(exception);
|
|
30
31
|
case '23505':
|
|
@@ -409,14 +409,19 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
409
409
|
const key = this.getTableKey(col);
|
|
410
410
|
ret[key] ??= [];
|
|
411
411
|
let type = col.data_type.toLowerCase() === 'array' ? col.udt_name.replace(/^_(.*)$/, '$1[]') : col.udt_name;
|
|
412
|
+
// `udt_name` drops array element typmods (`_numeric` for `numeric(12,2)[]`), recover them from `format_type`
|
|
413
|
+
const arrayModifier = type.endsWith('[]')
|
|
414
|
+
? /^[^(]*(\([^)]*\))[^(]*\[\]$/.exec(col.format_type ?? '')?.[1]
|
|
415
|
+
: undefined;
|
|
416
|
+
if (arrayModifier) {
|
|
417
|
+
type = `${type.slice(0, -2)}${arrayModifier}[]`;
|
|
418
|
+
}
|
|
412
419
|
if (col.data_type === 'USER-DEFINED' &&
|
|
413
420
|
col.udt_schema &&
|
|
414
421
|
col.udt_schema !== this.platform.getDefaultSchemaName()) {
|
|
415
422
|
type = `${col.udt_schema}.${type}`;
|
|
416
423
|
}
|
|
417
|
-
|
|
418
|
-
type = 'char';
|
|
419
|
-
}
|
|
424
|
+
type = type.replace(/^bpchar\b/, 'char');
|
|
420
425
|
// postgis stores the geometry/geography typmod (`geometry(point,4326)`) in atttypmod, which
|
|
421
426
|
// information_schema does not expose — recover the full type via format_type so it round-trips
|
|
422
427
|
if ((col.udt_name === 'geometry' || col.udt_name === 'geography') && col.format_type) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntityProperty, type IsolationLevel } from '@mikro-orm/core';
|
|
1
|
+
import { type EntityMetadata, type EntityProperty, type IsolationLevel } from '@mikro-orm/core';
|
|
2
2
|
import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
|
|
3
3
|
import { SqliteNativeQueryBuilder } from './SqliteNativeQueryBuilder.js';
|
|
4
4
|
import { SqliteSchemaHelper } from './SqliteSchemaHelper.js';
|
|
@@ -74,6 +74,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
74
74
|
*/
|
|
75
75
|
supportsSchemas(): boolean;
|
|
76
76
|
getDefaultSchemaName(): string | undefined;
|
|
77
|
+
validateMetadata(meta: EntityMetadata): void;
|
|
77
78
|
getFullTextWhereClause(): string;
|
|
78
79
|
escape(value: any): string;
|
|
79
80
|
convertVersionValue(value: Date | number, prop: EntityProperty): number | {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MetadataError } from '@mikro-orm/core';
|
|
1
2
|
import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
|
|
2
3
|
import { SqliteNativeQueryBuilder } from './SqliteNativeQueryBuilder.js';
|
|
3
4
|
import { SqliteSchemaHelper } from './SqliteSchemaHelper.js';
|
|
@@ -110,6 +111,14 @@ export class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
110
111
|
// Return 'main' only when schema support is active (i.e., databases are attached)
|
|
111
112
|
return this.supportsSchemas() ? 'main' : undefined;
|
|
112
113
|
}
|
|
114
|
+
validateMetadata(meta) {
|
|
115
|
+
super.validateMetadata(meta);
|
|
116
|
+
// `main` always exists and `*` resolves at runtime; anything else fails later as `unknown database` unless attached
|
|
117
|
+
const attached = this.config.get('attachDatabases')?.some(db => db.name === meta.schema);
|
|
118
|
+
if (meta.schema && !['main', '*'].includes(meta.schema) && !attached) {
|
|
119
|
+
throw new MetadataError(`Entity ${meta.className} declares schema '${meta.schema}', but SQLite cannot address it: attach it via \`attachDatabases\` or remove the schema in \`discovery.onMetadata\` when sharing entities between drivers`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
113
122
|
getFullTextWhereClause() {
|
|
114
123
|
return `:column: match :query`;
|
|
115
124
|
}
|
|
@@ -242,7 +242,8 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
242
242
|
}
|
|
243
243
|
for (let i = cols.length - 1; i >= 0; i--) {
|
|
244
244
|
const col = cols[i];
|
|
245
|
-
|
|
245
|
+
// the first column is not preceded by a comma
|
|
246
|
+
const re = ` *(?:^|,) *[\`"']?${col.name}[\`"']? (.*)`;
|
|
246
247
|
const columnDef = new RegExp(re, 'i').exec(columnsDef);
|
|
247
248
|
if (columnDef) {
|
|
248
249
|
columns[col.name] = { name: col.name, definition: columnDef[1] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.2.7-dev.
|
|
3
|
+
"version": "7.2.7-dev.8",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@mikro-orm/core": "^7.2.6"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.7-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.7-dev.8"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/query/CriteriaNode.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
|
|
|
27
27
|
parentPath?: string;
|
|
28
28
|
}): string;
|
|
29
29
|
private isPivotJoin;
|
|
30
|
+
protected isOperatorPayload(payload: any): boolean;
|
|
30
31
|
getPivotPath(path: string): string;
|
|
31
32
|
aliased(field: string, alias?: string): string;
|
|
32
33
|
isStrict(): boolean;
|
package/query/CriteriaNode.js
CHANGED
|
@@ -125,8 +125,17 @@ export class CriteriaNode {
|
|
|
125
125
|
this.payload instanceof RegExp ||
|
|
126
126
|
this.payload instanceof Date ||
|
|
127
127
|
rawField;
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || this.isOperatorPayload(this.payload));
|
|
129
|
+
}
|
|
130
|
+
isOperatorPayload(payload) {
|
|
131
|
+
return (Utils.isObject(payload) &&
|
|
132
|
+
Utils.getObjectQueryKeys(payload).every(k => {
|
|
133
|
+
if (k === '$not' && payload[k] instanceof CriteriaNode && Utils.isPlainObject(payload[k].payload)) {
|
|
134
|
+
// `$not` wrapping entity conditions is a condition on the target entity, not on the relation itself
|
|
135
|
+
return Utils.getObjectQueryKeys(payload[k].payload).every(ik => Utils.isOperator(ik, false));
|
|
136
|
+
}
|
|
137
|
+
return Utils.isOperator(k, false);
|
|
138
|
+
}));
|
|
130
139
|
}
|
|
131
140
|
getPivotPath(path) {
|
|
132
141
|
return `${path}[pivot]`;
|
|
@@ -283,18 +283,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
283
283
|
const embeddable = this.prop.kind === ReferenceKind.EMBEDDED;
|
|
284
284
|
const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) ||
|
|
285
285
|
(this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner);
|
|
286
|
-
const operatorKeys = knownKey &&
|
|
287
|
-
keys.every(key => {
|
|
288
|
-
if (key === '$not') {
|
|
289
|
-
// $not wraps conditions like $and/$or, check if it wraps entity property conditions (needs auto-join)
|
|
290
|
-
// vs simple operator conditions on the FK (doesn't need auto-join)
|
|
291
|
-
const childPayload = this.payload[key].payload;
|
|
292
|
-
if (Utils.isPlainObject(childPayload)) {
|
|
293
|
-
return Utils.getObjectQueryKeys(childPayload).every(k => Utils.isOperator(k, false));
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return Utils.isOperator(key, false);
|
|
297
|
-
});
|
|
286
|
+
const operatorKeys = knownKey && this.isOperatorPayload(this.payload);
|
|
298
287
|
const primaryKeys = knownKey &&
|
|
299
288
|
keys.every(key => {
|
|
300
289
|
if (key === '$not') {
|
|
@@ -322,8 +311,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
322
311
|
this.payload instanceof RegExp ||
|
|
323
312
|
this.payload instanceof Date ||
|
|
324
313
|
rawField;
|
|
325
|
-
const operator = Utils.isPlainObject(this.payload) &&
|
|
326
|
-
Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false));
|
|
314
|
+
const operator = Utils.isPlainObject(this.payload) && this.isOperatorPayload(this.payload);
|
|
327
315
|
const field = `${alias}.${this.prop.name}`;
|
|
328
316
|
const method = qb.hasFlag(QueryFlag.INFER_POPULATE) ? 'joinAndSelect' : 'join';
|
|
329
317
|
const path = this.getPath();
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -821,6 +821,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
821
821
|
* ```
|
|
822
822
|
*/
|
|
823
823
|
stream(options?: QBStreamOptions): AsyncIterableIterator<Loaded<Entity, Hint, Fields>>;
|
|
824
|
+
private convertStreamExceptions;
|
|
824
825
|
/**
|
|
825
826
|
* Alias for `qb.getResultList()`
|
|
826
827
|
*/
|
|
@@ -1003,6 +1004,10 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
1003
1004
|
processPopulateHint(): void;
|
|
1004
1005
|
private processPopulateWhere;
|
|
1005
1006
|
private mergeOnConditions;
|
|
1007
|
+
/**
|
|
1008
|
+
* `$not` can be moved to a join's `on` clause as a whole only when all of its conditions target that join.
|
|
1009
|
+
*/
|
|
1010
|
+
private getNotConditionAlias;
|
|
1006
1011
|
/**
|
|
1007
1012
|
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
1008
1013
|
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
@@ -1012,9 +1017,9 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
1012
1017
|
private canDistributeOrBranches;
|
|
1013
1018
|
private getOrBranchAliases;
|
|
1014
1019
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
* can reference their aliases.
|
|
1020
|
+
* A condition that cannot be distributed (an entity filter's `$or`, or a `$not` spanning several joins)
|
|
1021
|
+
* is applied intact to the `on` clause of the outermost join common to all targeted joins, nesting
|
|
1022
|
+
* the targeted joins under it so the clause can reference their aliases.
|
|
1018
1023
|
*/
|
|
1019
1024
|
private mergeFilterOrCondition;
|
|
1020
1025
|
/**
|
package/query/QueryBuilder.js
CHANGED
|
@@ -1080,12 +1080,19 @@ export class QueryBuilder {
|
|
|
1080
1080
|
const conn = this.getConnection();
|
|
1081
1081
|
// outside a transaction, wrap in a short implicit one so RLS `set local` session context applies (no-op when unset)
|
|
1082
1082
|
const sessionContext = this.context ? undefined : this.em?.getTransactionSessionContext();
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1083
|
+
let res;
|
|
1084
|
+
// a QueryBuilder used directly must honour the same `convertException` contract as the driver methods
|
|
1085
|
+
try {
|
|
1086
|
+
res = await (sessionContext
|
|
1087
|
+
? conn.transactional(trx => conn.execute(query.sql, query.params, method, trx, loggerContext), {
|
|
1088
|
+
sessionContext,
|
|
1089
|
+
loggerContext,
|
|
1090
|
+
})
|
|
1091
|
+
: conn.execute(query.sql, query.params, method, this.context, loggerContext));
|
|
1092
|
+
}
|
|
1093
|
+
catch (e) {
|
|
1094
|
+
throw this.driver.convertException(e);
|
|
1095
|
+
}
|
|
1089
1096
|
const meta = this.mainAlias.meta;
|
|
1090
1097
|
if (!options.mapResults || !meta) {
|
|
1091
1098
|
await this.em?.storeCache(this.#state.cache, cached, res);
|
|
@@ -1146,7 +1153,7 @@ export class QueryBuilder {
|
|
|
1146
1153
|
const chunkSize = options.chunkSize ?? 100;
|
|
1147
1154
|
const query = this.toQuery();
|
|
1148
1155
|
const loggerContext = { id: this.em?.id, ...this.loggerContext, ...this.#abortOptions };
|
|
1149
|
-
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext, chunkSize);
|
|
1156
|
+
const res = this.convertStreamExceptions(this.getConnection().stream(query.sql, query.params, this.context, loggerContext, chunkSize));
|
|
1150
1157
|
const meta = this.mainAlias.meta;
|
|
1151
1158
|
if (options.rawResults || !meta) {
|
|
1152
1159
|
yield* res;
|
|
@@ -1180,6 +1187,14 @@ export class QueryBuilder {
|
|
|
1180
1187
|
}
|
|
1181
1188
|
}
|
|
1182
1189
|
}
|
|
1190
|
+
async *convertStreamExceptions(res) {
|
|
1191
|
+
try {
|
|
1192
|
+
yield* res;
|
|
1193
|
+
}
|
|
1194
|
+
catch (e) {
|
|
1195
|
+
throw this.driver.convertException(e);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1183
1198
|
/**
|
|
1184
1199
|
* Alias for `qb.getResultList()`
|
|
1185
1200
|
*/
|
|
@@ -1558,7 +1573,10 @@ export class QueryBuilder {
|
|
|
1558
1573
|
field = asMatch[1].trim();
|
|
1559
1574
|
customAlias = asMatch[2];
|
|
1560
1575
|
}
|
|
1561
|
-
|
|
1576
|
+
// an aliased field is always a column, never a relation to expand, even when an FK column is named like its relation
|
|
1577
|
+
const join = customAlias
|
|
1578
|
+
? undefined
|
|
1579
|
+
: Object.keys(this.#state.joins).find(k => field === k.substring(0, k.indexOf('#')));
|
|
1562
1580
|
if (join && type === 'where') {
|
|
1563
1581
|
ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[join]));
|
|
1564
1582
|
return;
|
|
@@ -2102,7 +2120,13 @@ export class QueryBuilder {
|
|
|
2102
2120
|
}
|
|
2103
2121
|
mergeOnConditions(joins, cond, filter, op) {
|
|
2104
2122
|
for (const k of Object.keys(cond)) {
|
|
2105
|
-
|
|
2123
|
+
const notAlias = k === '$not' ? this.getNotConditionAlias(cond[k], joins) : undefined;
|
|
2124
|
+
if (k === '$not' && !notAlias) {
|
|
2125
|
+
// splitting a `$not` across joins would drop the negation, so it is kept intact on their common ancestor join
|
|
2126
|
+
this.mergeFilterOrCondition([{ $not: cond[k] }], joins);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
if (Utils.isOperator(k) && !notAlias) {
|
|
2106
2130
|
if (Array.isArray(cond[k])) {
|
|
2107
2131
|
if (k === '$or' && !this.canDistributeOrBranches(cond[k], joins)) {
|
|
2108
2132
|
// entity filters have no other sink, so their `$or` is kept intact on the outermost targeted join instead
|
|
@@ -2116,7 +2140,7 @@ export class QueryBuilder {
|
|
|
2116
2140
|
/* v8 ignore next */
|
|
2117
2141
|
this.mergeOnConditions(joins, cond[k], filter, k);
|
|
2118
2142
|
}
|
|
2119
|
-
const
|
|
2143
|
+
const alias = notAlias ?? this.helper.splitField(k)[0];
|
|
2120
2144
|
const join = joins.find(j => j.alias === alias);
|
|
2121
2145
|
if (join) {
|
|
2122
2146
|
const parentJoin = joins.find(j => j.alias === join.ownerAlias);
|
|
@@ -2145,6 +2169,14 @@ export class QueryBuilder {
|
|
|
2145
2169
|
}
|
|
2146
2170
|
}
|
|
2147
2171
|
}
|
|
2172
|
+
/**
|
|
2173
|
+
* `$not` can be moved to a join's `on` clause as a whole only when all of its conditions target that join.
|
|
2174
|
+
*/
|
|
2175
|
+
getNotConditionAlias(cond, joins) {
|
|
2176
|
+
const aliases = this.getOrBranchAliases([cond]);
|
|
2177
|
+
const [alias] = aliases;
|
|
2178
|
+
return aliases.size === 1 && joins.some(j => j.alias === alias) ? alias : undefined;
|
|
2179
|
+
}
|
|
2148
2180
|
/**
|
|
2149
2181
|
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
2150
2182
|
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
@@ -2173,9 +2205,9 @@ export class QueryBuilder {
|
|
|
2173
2205
|
return aliases;
|
|
2174
2206
|
}
|
|
2175
2207
|
/**
|
|
2176
|
-
*
|
|
2177
|
-
*
|
|
2178
|
-
* can reference their aliases.
|
|
2208
|
+
* A condition that cannot be distributed (an entity filter's `$or`, or a `$not` spanning several joins)
|
|
2209
|
+
* is applied intact to the `on` clause of the outermost join common to all targeted joins, nesting
|
|
2210
|
+
* the targeted joins under it so the clause can reference their aliases.
|
|
2179
2211
|
*/
|
|
2180
2212
|
mergeFilterOrCondition(branches, joins) {
|
|
2181
2213
|
const aliases = this.getOrBranchAliases(branches);
|
|
@@ -623,6 +623,19 @@ export class QueryBuilderHelper {
|
|
|
623
623
|
parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`);
|
|
624
624
|
params.push(...query.params);
|
|
625
625
|
}
|
|
626
|
+
else if (['$in', '$nin'].includes(op) &&
|
|
627
|
+
prop?.customType instanceof ArrayType &&
|
|
628
|
+
Array.isArray(opValue) &&
|
|
629
|
+
!opValue.every((v) => Array.isArray(v))) {
|
|
630
|
+
// a flat list on an array column matches any of its elements, like in mongo
|
|
631
|
+
if (!this.#platform.supportsArrayOverlap()) {
|
|
632
|
+
throw new ValidationError(`${op} on array property ${prop.name} expects a list of arrays, use $eq for an exact match`);
|
|
633
|
+
}
|
|
634
|
+
const mappedKey = this.mapper(key, type, opValue, null);
|
|
635
|
+
const sql = `${this.#platform.quoteIdentifier(mappedKey)} && ?`;
|
|
636
|
+
parts.push(op === '$in' ? sql : `not (${sql})`);
|
|
637
|
+
params.push(prop.customType.convertToDatabaseValue(opValue, this.#platform, { fromQuery: true, key: op, mode: 'query' }));
|
|
638
|
+
}
|
|
626
639
|
else {
|
|
627
640
|
const mappedKey = this.mapper(key, type, opValue, null);
|
|
628
641
|
const val = this.getValueReplacement(fields, opValue, params, op, prop, rowValues);
|
|
@@ -641,16 +654,15 @@ export class QueryBuilderHelper {
|
|
|
641
654
|
}
|
|
642
655
|
return `(${tmp.join(', ')})`;
|
|
643
656
|
}
|
|
644
|
-
// array custom types
|
|
645
|
-
// array literal, so we emit a single bound parameter — not one per element wrapped in a tuple
|
|
657
|
+
// array custom types convert a whole array into one literal, so each candidate array binds a single parameter
|
|
646
658
|
if (prop?.customType instanceof ArrayType) {
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
mode: 'query'
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return
|
|
659
|
+
const list = key === '$in' || key === '$nin';
|
|
660
|
+
const items = list ? value : [value];
|
|
661
|
+
for (const item of items) {
|
|
662
|
+
params.push(prop.customType.convertToDatabaseValue(item, this.#platform, { fromQuery: true, key, mode: 'query' }));
|
|
663
|
+
}
|
|
664
|
+
const sql = items.map(() => '?').join(', ');
|
|
665
|
+
return list ? `(${sql})` : sql;
|
|
654
666
|
}
|
|
655
667
|
value.forEach(p => params.push(p));
|
|
656
668
|
return `(${value.map(() => '?').join(', ')})`;
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -760,6 +760,10 @@ export class DatabaseTable {
|
|
|
760
760
|
!idx.primary &&
|
|
761
761
|
!idx.where &&
|
|
762
762
|
!this.hasAdvancedIndexOptions(idx));
|
|
763
|
+
// `getMappedType('timestamptz(3)[]')` resolves to the element type so diffing keeps the modifier, but the property is still an array
|
|
764
|
+
if (column.type?.endsWith('[]') && !column.enumItems?.length) {
|
|
765
|
+
column = { ...column, mappedType: Type.getType(t.array) };
|
|
766
|
+
}
|
|
763
767
|
const kind = this.getReferenceKind(fk, unique);
|
|
764
768
|
const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
|
|
765
769
|
const type = fk
|