@mikro-orm/sql 7.2.7-dev.6 → 7.2.7-dev.7
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/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 +7 -3
- package/query/QueryBuilder.js +23 -6
- 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) {
|
|
@@ -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.7",
|
|
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.7"
|
|
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
|
@@ -1003,6 +1003,10 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
1003
1003
|
processPopulateHint(): void;
|
|
1004
1004
|
private processPopulateWhere;
|
|
1005
1005
|
private mergeOnConditions;
|
|
1006
|
+
/**
|
|
1007
|
+
* `$not` can be moved to a join's `on` clause as a whole only when all of its conditions target that join.
|
|
1008
|
+
*/
|
|
1009
|
+
private getNotConditionAlias;
|
|
1006
1010
|
/**
|
|
1007
1011
|
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
1008
1012
|
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
@@ -1012,9 +1016,9 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
1012
1016
|
private canDistributeOrBranches;
|
|
1013
1017
|
private getOrBranchAliases;
|
|
1014
1018
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
* can reference their aliases.
|
|
1019
|
+
* A condition that cannot be distributed (an entity filter's `$or`, or a `$not` spanning several joins)
|
|
1020
|
+
* is applied intact to the `on` clause of the outermost join common to all targeted joins, nesting
|
|
1021
|
+
* the targeted joins under it so the clause can reference their aliases.
|
|
1018
1022
|
*/
|
|
1019
1023
|
private mergeFilterOrCondition;
|
|
1020
1024
|
/**
|
package/query/QueryBuilder.js
CHANGED
|
@@ -1558,7 +1558,10 @@ export class QueryBuilder {
|
|
|
1558
1558
|
field = asMatch[1].trim();
|
|
1559
1559
|
customAlias = asMatch[2];
|
|
1560
1560
|
}
|
|
1561
|
-
|
|
1561
|
+
// an aliased field is always a column, never a relation to expand, even when an FK column is named like its relation
|
|
1562
|
+
const join = customAlias
|
|
1563
|
+
? undefined
|
|
1564
|
+
: Object.keys(this.#state.joins).find(k => field === k.substring(0, k.indexOf('#')));
|
|
1562
1565
|
if (join && type === 'where') {
|
|
1563
1566
|
ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[join]));
|
|
1564
1567
|
return;
|
|
@@ -2102,7 +2105,13 @@ export class QueryBuilder {
|
|
|
2102
2105
|
}
|
|
2103
2106
|
mergeOnConditions(joins, cond, filter, op) {
|
|
2104
2107
|
for (const k of Object.keys(cond)) {
|
|
2105
|
-
|
|
2108
|
+
const notAlias = k === '$not' ? this.getNotConditionAlias(cond[k], joins) : undefined;
|
|
2109
|
+
if (k === '$not' && !notAlias) {
|
|
2110
|
+
// splitting a `$not` across joins would drop the negation, so it is kept intact on their common ancestor join
|
|
2111
|
+
this.mergeFilterOrCondition([{ $not: cond[k] }], joins);
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
if (Utils.isOperator(k) && !notAlias) {
|
|
2106
2115
|
if (Array.isArray(cond[k])) {
|
|
2107
2116
|
if (k === '$or' && !this.canDistributeOrBranches(cond[k], joins)) {
|
|
2108
2117
|
// entity filters have no other sink, so their `$or` is kept intact on the outermost targeted join instead
|
|
@@ -2116,7 +2125,7 @@ export class QueryBuilder {
|
|
|
2116
2125
|
/* v8 ignore next */
|
|
2117
2126
|
this.mergeOnConditions(joins, cond[k], filter, k);
|
|
2118
2127
|
}
|
|
2119
|
-
const
|
|
2128
|
+
const alias = notAlias ?? this.helper.splitField(k)[0];
|
|
2120
2129
|
const join = joins.find(j => j.alias === alias);
|
|
2121
2130
|
if (join) {
|
|
2122
2131
|
const parentJoin = joins.find(j => j.alias === join.ownerAlias);
|
|
@@ -2145,6 +2154,14 @@ export class QueryBuilder {
|
|
|
2145
2154
|
}
|
|
2146
2155
|
}
|
|
2147
2156
|
}
|
|
2157
|
+
/**
|
|
2158
|
+
* `$not` can be moved to a join's `on` clause as a whole only when all of its conditions target that join.
|
|
2159
|
+
*/
|
|
2160
|
+
getNotConditionAlias(cond, joins) {
|
|
2161
|
+
const aliases = this.getOrBranchAliases([cond]);
|
|
2162
|
+
const [alias] = aliases;
|
|
2163
|
+
return aliases.size === 1 && joins.some(j => j.alias === alias) ? alias : undefined;
|
|
2164
|
+
}
|
|
2148
2165
|
/**
|
|
2149
2166
|
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
2150
2167
|
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
@@ -2173,9 +2190,9 @@ export class QueryBuilder {
|
|
|
2173
2190
|
return aliases;
|
|
2174
2191
|
}
|
|
2175
2192
|
/**
|
|
2176
|
-
*
|
|
2177
|
-
*
|
|
2178
|
-
* can reference their aliases.
|
|
2193
|
+
* A condition that cannot be distributed (an entity filter's `$or`, or a `$not` spanning several joins)
|
|
2194
|
+
* is applied intact to the `on` clause of the outermost join common to all targeted joins, nesting
|
|
2195
|
+
* the targeted joins under it so the clause can reference their aliases.
|
|
2179
2196
|
*/
|
|
2180
2197
|
mergeFilterOrCondition(branches, joins) {
|
|
2181
2198
|
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
|