@mikro-orm/sql 7.0.0-dev.335 → 7.0.0-dev.336
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/AbstractSqlPlatform.d.ts +25 -0
- package/AbstractSqlPlatform.js +32 -3
- package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
- package/dialects/mysql/BaseMySqlPlatform.js +17 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +6 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +10 -6
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +3 -0
- package/package.json +2 -2
- package/query/CriteriaNodeFactory.js +15 -3
- package/query/QueryBuilderHelper.d.ts +5 -0
- package/query/QueryBuilderHelper.js +156 -1
- package/query/enums.d.ts +2 -0
- package/query/enums.js +2 -0
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
26
26
|
quoteValue(value: any): string;
|
|
27
27
|
getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
|
|
28
28
|
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
|
|
29
|
+
/**
|
|
30
|
+
* Quotes a key for use inside a JSON path expression (e.g. `$.key`).
|
|
31
|
+
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
quoteJsonKey(key: string): string;
|
|
29
35
|
getJsonIndexDefinition(index: IndexDef): string[];
|
|
30
36
|
supportsUnionWhere(): boolean;
|
|
31
37
|
supportsSchemas(): boolean;
|
|
@@ -42,6 +48,25 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
42
48
|
quoteCollation(collation: string): string;
|
|
43
49
|
/** @internal */
|
|
44
50
|
protected validateCollationName(collation: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Returns FROM clause for JSON array iteration.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
getJsonArrayFromSQL(column: string, alias: string, _properties: {
|
|
56
|
+
name: string;
|
|
57
|
+
type: string;
|
|
58
|
+
}[]): string;
|
|
59
|
+
/**
|
|
60
|
+
* Returns SQL expression to access an element's property within a JSON array iteration.
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
getJsonArrayElementPropertySQL(alias: string, property: string, _type: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Wraps JSON array FROM clause and WHERE condition into a full EXISTS condition.
|
|
66
|
+
* MySQL overrides this because `json_table` doesn't support correlated subqueries.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
getJsonArrayExistsSQL(from: string, where: string): string;
|
|
45
70
|
/**
|
|
46
71
|
* Maps a runtime type name (e.g. 'string', 'number') to a driver-specific bind type constant.
|
|
47
72
|
* Used by NativeQueryBuilder for output bindings.
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -64,11 +64,18 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
64
64
|
}
|
|
65
65
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
66
66
|
const [a, ...b] = path;
|
|
67
|
-
const quoteKey = (key) => (/^[a-z]\w*$/i.exec(key) ? key : `"${key}"`);
|
|
68
67
|
if (aliased) {
|
|
69
|
-
return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(
|
|
68
|
+
return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
|
|
70
69
|
}
|
|
71
|
-
return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(
|
|
70
|
+
return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Quotes a key for use inside a JSON path expression (e.g. `$.key`).
|
|
74
|
+
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
quoteJsonKey(key) {
|
|
78
|
+
return /^[a-z]\w*$/i.exec(key) ? key : `"${key}"`;
|
|
72
79
|
}
|
|
73
80
|
getJsonIndexDefinition(index) {
|
|
74
81
|
return index.columnNames.map(column => {
|
|
@@ -116,6 +123,28 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
116
123
|
throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
|
|
117
124
|
}
|
|
118
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Returns FROM clause for JSON array iteration.
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
getJsonArrayFromSQL(column, alias, _properties) {
|
|
131
|
+
return `json_each(${column}) as ${this.quoteIdentifier(alias)}`;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns SQL expression to access an element's property within a JSON array iteration.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
getJsonArrayElementPropertySQL(alias, property, _type) {
|
|
138
|
+
return `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(property)}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Wraps JSON array FROM clause and WHERE condition into a full EXISTS condition.
|
|
142
|
+
* MySQL overrides this because `json_table` doesn't support correlated subqueries.
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
145
|
+
getJsonArrayExistsSQL(from, where) {
|
|
146
|
+
return `exists (select 1 from ${from} where ${where})`;
|
|
147
|
+
}
|
|
119
148
|
/**
|
|
120
149
|
* Maps a runtime type name (e.g. 'string', 'number') to a driver-specific bind type constant.
|
|
121
150
|
* Used by NativeQueryBuilder for output bindings.
|
|
@@ -5,6 +5,7 @@ import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
|
|
|
5
5
|
import type { IndexDef } from '../../typings.js';
|
|
6
6
|
import { MySqlNativeQueryBuilder } from './MySqlNativeQueryBuilder.js';
|
|
7
7
|
export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
8
|
+
#private;
|
|
8
9
|
protected readonly schemaHelper: MySqlSchemaHelper;
|
|
9
10
|
protected readonly exceptionConverter: MySqlExceptionConverter;
|
|
10
11
|
protected readonly ORDER_BY_NULLS_TRANSLATE: {
|
|
@@ -42,5 +43,10 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
42
43
|
getFullTextWhereClause(): string;
|
|
43
44
|
getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string;
|
|
44
45
|
getOrderByExpression(column: string, direction: string, collation?: string): string[];
|
|
46
|
+
getJsonArrayFromSQL(column: string, alias: string, properties: {
|
|
47
|
+
name: string;
|
|
48
|
+
type: string;
|
|
49
|
+
}[]): string;
|
|
50
|
+
getJsonArrayExistsSQL(from: string, where: string): string;
|
|
45
51
|
getDefaultClientUrl(): string;
|
|
46
52
|
}
|
|
@@ -6,6 +6,12 @@ import { MySqlNativeQueryBuilder } from './MySqlNativeQueryBuilder.js';
|
|
|
6
6
|
export class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
7
7
|
schemaHelper = new MySqlSchemaHelper(this);
|
|
8
8
|
exceptionConverter = new MySqlExceptionConverter();
|
|
9
|
+
#jsonTypeCasts = {
|
|
10
|
+
string: 'text',
|
|
11
|
+
number: 'double',
|
|
12
|
+
bigint: 'bigint',
|
|
13
|
+
boolean: 'unsigned',
|
|
14
|
+
};
|
|
9
15
|
ORDER_BY_NULLS_TRANSLATE = {
|
|
10
16
|
[QueryOrder.asc_nulls_first]: 'is not null',
|
|
11
17
|
[QueryOrder.asc_nulls_last]: 'is null',
|
|
@@ -114,6 +120,17 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
114
120
|
ret.push(`${col} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`);
|
|
115
121
|
return ret;
|
|
116
122
|
}
|
|
123
|
+
getJsonArrayFromSQL(column, alias, properties) {
|
|
124
|
+
const columns = properties
|
|
125
|
+
.map(p => `${this.quoteIdentifier(p.name)} ${this.#jsonTypeCasts[p.type] ?? 'text'} path '$.${this.quoteJsonKey(p.name)}'`)
|
|
126
|
+
.join(', ');
|
|
127
|
+
return `json_table(${column}, '$[*]' columns (${columns})) as ${this.quoteIdentifier(alias)}`;
|
|
128
|
+
}
|
|
129
|
+
// MySQL does not support correlated json_table inside EXISTS subqueries,
|
|
130
|
+
// so we use a semi-join via the comma-join pattern instead.
|
|
131
|
+
getJsonArrayExistsSQL(from, where) {
|
|
132
|
+
return `(select 1 from ${from} where ${where} limit 1) is not null`;
|
|
133
|
+
}
|
|
117
134
|
getDefaultClientUrl() {
|
|
118
135
|
return 'mysql://root@127.0.0.1:3306';
|
|
119
136
|
}
|
|
@@ -5,6 +5,7 @@ import { PostgreSqlNativeQueryBuilder } from './PostgreSqlNativeQueryBuilder.js'
|
|
|
5
5
|
import { PostgreSqlSchemaHelper } from './PostgreSqlSchemaHelper.js';
|
|
6
6
|
import { PostgreSqlExceptionConverter } from './PostgreSqlExceptionConverter.js';
|
|
7
7
|
export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
8
|
+
#private;
|
|
8
9
|
protected readonly schemaHelper: PostgreSqlSchemaHelper;
|
|
9
10
|
protected readonly exceptionConverter: PostgreSqlExceptionConverter;
|
|
10
11
|
createNativeQueryBuilder(): PostgreSqlNativeQueryBuilder;
|
|
@@ -102,5 +103,10 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
102
103
|
castColumn(prop?: {
|
|
103
104
|
columnTypes?: string[];
|
|
104
105
|
}): string;
|
|
106
|
+
getJsonArrayFromSQL(column: string, alias: string, _properties: {
|
|
107
|
+
name: string;
|
|
108
|
+
type: string;
|
|
109
|
+
}[]): string;
|
|
110
|
+
getJsonArrayElementPropertySQL(alias: string, property: string, type: string): string;
|
|
105
111
|
getDefaultClientUrl(): string;
|
|
106
112
|
}
|
|
@@ -7,6 +7,8 @@ import { FullTextType } from './FullTextType.js';
|
|
|
7
7
|
export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
8
8
|
schemaHelper = new PostgreSqlSchemaHelper(this);
|
|
9
9
|
exceptionConverter = new PostgreSqlExceptionConverter();
|
|
10
|
+
/** Maps JS runtime type names to PostgreSQL cast types for JSON property access. @internal */
|
|
11
|
+
#jsonTypeCasts = { number: 'float8', bigint: 'int8', boolean: 'bool' };
|
|
10
12
|
createNativeQueryBuilder() {
|
|
11
13
|
return new PostgreSqlNativeQueryBuilder(this);
|
|
12
14
|
}
|
|
@@ -215,12 +217,7 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
215
217
|
const last = path.pop();
|
|
216
218
|
const root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${first}` : first);
|
|
217
219
|
type = typeof type === 'string' ? this.getMappedType(type).runtimeType : String(type);
|
|
218
|
-
const
|
|
219
|
-
number: 'float8',
|
|
220
|
-
bigint: 'int8',
|
|
221
|
-
boolean: 'bool',
|
|
222
|
-
};
|
|
223
|
-
const cast = (key) => raw(type in types ? `(${key})::${types[type]}` : key);
|
|
220
|
+
const cast = (key) => raw(type in this.#jsonTypeCasts ? `(${key})::${this.#jsonTypeCasts[type]}` : key);
|
|
224
221
|
let lastOperator = '->>';
|
|
225
222
|
// force `->` for operator payloads with array values
|
|
226
223
|
if (Utils.isPlainObject(value) &&
|
|
@@ -352,6 +349,13 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
352
349
|
return '';
|
|
353
350
|
}
|
|
354
351
|
}
|
|
352
|
+
getJsonArrayFromSQL(column, alias, _properties) {
|
|
353
|
+
return `jsonb_array_elements(${column}) as ${this.quoteIdentifier(alias)}`;
|
|
354
|
+
}
|
|
355
|
+
getJsonArrayElementPropertySQL(alias, property, type) {
|
|
356
|
+
const expr = `${this.quoteIdentifier(alias)}->>${this.quoteValue(property)}`;
|
|
357
|
+
return type in this.#jsonTypeCasts ? `(${expr})::${this.#jsonTypeCasts[type]}` : expr;
|
|
358
|
+
}
|
|
355
359
|
getDefaultClientUrl() {
|
|
356
360
|
return 'postgresql://postgres@127.0.0.1:5432';
|
|
357
361
|
}
|
|
@@ -75,5 +75,6 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
75
75
|
convertVersionValue(value: Date | number, prop: EntityProperty): number | {
|
|
76
76
|
$in: (string | number)[];
|
|
77
77
|
};
|
|
78
|
+
getJsonArrayElementPropertySQL(alias: string, property: string, _type: string): string;
|
|
78
79
|
quoteValue(value: any): string;
|
|
79
80
|
}
|
|
@@ -133,6 +133,9 @@ export class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
133
133
|
}
|
|
134
134
|
return value;
|
|
135
135
|
}
|
|
136
|
+
getJsonArrayElementPropertySQL(alias, property, _type) {
|
|
137
|
+
return `json_extract(${this.quoteIdentifier(alias)}.value, '$.${this.quoteJsonKey(property)}')`;
|
|
138
|
+
}
|
|
136
139
|
quoteValue(value) {
|
|
137
140
|
if (value instanceof Date) {
|
|
138
141
|
return '' + +value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.0.0-dev.
|
|
3
|
+
"version": "7.0.0-dev.336",
|
|
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": "^6.6.9"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.0.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.0.0-dev.336"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
|
@@ -2,6 +2,7 @@ import { GroupOperator, isRaw, JsonType, RawQueryFragment, ReferenceKind, Utils,
|
|
|
2
2
|
import { ObjectCriteriaNode } from './ObjectCriteriaNode.js';
|
|
3
3
|
import { ArrayCriteriaNode } from './ArrayCriteriaNode.js';
|
|
4
4
|
import { ScalarCriteriaNode } from './ScalarCriteriaNode.js';
|
|
5
|
+
import { EMBEDDABLE_ARRAY_OPS } from './enums.js';
|
|
5
6
|
/**
|
|
6
7
|
* @internal
|
|
7
8
|
*/
|
|
@@ -69,15 +70,26 @@ export class CriteriaNodeFactory {
|
|
|
69
70
|
}, {});
|
|
70
71
|
return this.createNode(metadata, entityName, map, node, key, validate);
|
|
71
72
|
}
|
|
73
|
+
// For array embeddeds stored as real columns, route property-level queries
|
|
74
|
+
// as scalar nodes so QueryBuilderHelper generates EXISTS subqueries with
|
|
75
|
+
// JSON array iteration. Keys containing `~` indicate the property lives
|
|
76
|
+
// inside a parent's object-mode JSON column (MetadataDiscovery uses `~` as
|
|
77
|
+
// the glue for object embeds), where JSON path access is used instead.
|
|
78
|
+
if (prop.array && !String(key).includes('~')) {
|
|
79
|
+
const keys = Object.keys(val);
|
|
80
|
+
const hasOnlyArrayOps = keys.every(k => EMBEDDABLE_ARRAY_OPS.includes(k));
|
|
81
|
+
if (!hasOnlyArrayOps) {
|
|
82
|
+
return this.createScalarNode(metadata, entityName, val, node, key, validate);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
72
85
|
// array operators can be used on embedded properties
|
|
73
|
-
const
|
|
74
|
-
const operator = Object.keys(val).some(f => Utils.isOperator(f) && !allowedOperators.includes(f));
|
|
86
|
+
const operator = Object.keys(val).some(f => Utils.isOperator(f) && !EMBEDDABLE_ARRAY_OPS.includes(f));
|
|
75
87
|
if (operator) {
|
|
76
88
|
throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload);
|
|
77
89
|
}
|
|
78
90
|
const map = Object.keys(val).reduce((oo, k) => {
|
|
79
91
|
const embeddedProp = prop.embeddedProps[k] ?? Object.values(prop.embeddedProps).find(p => p.name === k);
|
|
80
|
-
if (!embeddedProp && !
|
|
92
|
+
if (!embeddedProp && !EMBEDDABLE_ARRAY_OPS.includes(k)) {
|
|
81
93
|
throw ValidationError.invalidEmbeddableQuery(entityName, k, prop.type);
|
|
82
94
|
}
|
|
83
95
|
if (embeddedProp) {
|
|
@@ -56,6 +56,11 @@ export declare class QueryBuilderHelper {
|
|
|
56
56
|
private fieldName;
|
|
57
57
|
getProperty(field: string, alias?: string): EntityProperty | undefined;
|
|
58
58
|
isTableNameAliasRequired(type: QueryType): boolean;
|
|
59
|
+
private processEmbeddedArrayCondition;
|
|
60
|
+
private buildJsonArrayExists;
|
|
61
|
+
private resolveEmbeddedProp;
|
|
62
|
+
private buildEmbeddedArrayWhere;
|
|
63
|
+
private buildEmbeddedArrayOperatorCondition;
|
|
59
64
|
processOnConflictCondition(cond: FilterQuery<any>, schema?: string): FilterQuery<any>;
|
|
60
65
|
createFormulaTable(alias: string, meta: EntityMetadata, schema?: string): FormulaTable;
|
|
61
66
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
2
|
-
import { JoinType, QueryType } from './enums.js';
|
|
2
|
+
import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
@@ -12,6 +12,8 @@ export class QueryBuilderHelper {
|
|
|
12
12
|
#subQueries;
|
|
13
13
|
#driver;
|
|
14
14
|
#tptAliasMap;
|
|
15
|
+
/** Monotonically increasing counter for unique JSON array iteration aliases within a single query. */
|
|
16
|
+
#jsonAliasCounter = 0;
|
|
15
17
|
constructor(entityName, alias, aliasMap, subQueries, driver, tptAliasMap = {}) {
|
|
16
18
|
this.#entityName = entityName;
|
|
17
19
|
this.#alias = alias;
|
|
@@ -436,6 +438,17 @@ export class QueryBuilderHelper {
|
|
|
436
438
|
params.push(this.getRegExpParam(cond[key]));
|
|
437
439
|
return { sql: parts.join(' and '), params };
|
|
438
440
|
}
|
|
441
|
+
if (Utils.isPlainObject(cond[key]) && !Raw.isKnownFragmentSymbol(key)) {
|
|
442
|
+
const [a, f] = this.splitField(key);
|
|
443
|
+
const prop = this.getProperty(f, a);
|
|
444
|
+
if (prop?.kind === ReferenceKind.EMBEDDED && prop.array) {
|
|
445
|
+
const keys = Object.keys(cond[key]);
|
|
446
|
+
const hasOnlyArrayOps = keys.every((k) => EMBEDDABLE_ARRAY_OPS.includes(k));
|
|
447
|
+
if (!hasOnlyArrayOps) {
|
|
448
|
+
return this.processEmbeddedArrayCondition(cond[key], prop, a);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
439
452
|
if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) {
|
|
440
453
|
return this.processObjectSubCondition(cond, key, type);
|
|
441
454
|
}
|
|
@@ -827,6 +840,148 @@ export class QueryBuilderHelper {
|
|
|
827
840
|
isTableNameAliasRequired(type) {
|
|
828
841
|
return [QueryType.SELECT, QueryType.COUNT].includes(type);
|
|
829
842
|
}
|
|
843
|
+
processEmbeddedArrayCondition(cond, prop, alias) {
|
|
844
|
+
const fieldName = prop.fieldNames[0];
|
|
845
|
+
const column = this.#platform.quoteIdentifier(`${alias}.${fieldName}`);
|
|
846
|
+
const parts = [];
|
|
847
|
+
const allParams = [];
|
|
848
|
+
// Top-level $not generates NOT EXISTS (no element matches the inner condition).
|
|
849
|
+
const { $not, ...rest } = cond;
|
|
850
|
+
if (Utils.hasObjectKeys(rest)) {
|
|
851
|
+
const result = this.buildJsonArrayExists(rest, prop, column, false);
|
|
852
|
+
if (result) {
|
|
853
|
+
parts.push(result.sql);
|
|
854
|
+
allParams.push(...result.params);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if ($not != null) {
|
|
858
|
+
if (!Utils.isPlainObject($not)) {
|
|
859
|
+
throw new ValidationError(`Invalid query: $not in embedded array queries expects an object value`);
|
|
860
|
+
}
|
|
861
|
+
const result = this.buildJsonArrayExists($not, prop, column, true);
|
|
862
|
+
if (result) {
|
|
863
|
+
parts.push(result.sql);
|
|
864
|
+
allParams.push(...result.params);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (parts.length === 0) {
|
|
868
|
+
return { sql: '1 = 1', params: [] };
|
|
869
|
+
}
|
|
870
|
+
return { sql: parts.join(' and '), params: allParams };
|
|
871
|
+
}
|
|
872
|
+
buildJsonArrayExists(cond, prop, column, negate) {
|
|
873
|
+
const jeAlias = `__je${this.#jsonAliasCounter++}`;
|
|
874
|
+
const referencedProps = new Map();
|
|
875
|
+
const { sql: whereSql, params } = this.buildEmbeddedArrayWhere(cond, prop, jeAlias, referencedProps);
|
|
876
|
+
if (!whereSql) {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
const from = this.#platform.getJsonArrayFromSQL(column, jeAlias, [...referencedProps.values()]);
|
|
880
|
+
const exists = this.#platform.getJsonArrayExistsSQL(from, whereSql);
|
|
881
|
+
return { sql: negate ? `not ${exists}` : exists, params };
|
|
882
|
+
}
|
|
883
|
+
resolveEmbeddedProp(prop, key) {
|
|
884
|
+
const embProp = prop.embeddedProps[key] ?? Object.values(prop.embeddedProps).find(p => p.name === key);
|
|
885
|
+
if (!embProp) {
|
|
886
|
+
throw ValidationError.invalidEmbeddableQuery(this.#entityName, key, prop.type);
|
|
887
|
+
}
|
|
888
|
+
const prefix = `${prop.fieldNames[0]}~`;
|
|
889
|
+
const raw = embProp.fieldNames[0];
|
|
890
|
+
const jsonPropName = raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
|
|
891
|
+
return { embProp, jsonPropName };
|
|
892
|
+
}
|
|
893
|
+
buildEmbeddedArrayWhere(cond, prop, jeAlias, referencedProps) {
|
|
894
|
+
const parts = [];
|
|
895
|
+
const params = [];
|
|
896
|
+
for (const k of Object.keys(cond)) {
|
|
897
|
+
if (k === '$and' || k === '$or') {
|
|
898
|
+
const items = cond[k];
|
|
899
|
+
if (items.length === 0) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const subParts = [];
|
|
903
|
+
for (const item of items) {
|
|
904
|
+
const sub = this.buildEmbeddedArrayWhere(item, prop, jeAlias, referencedProps);
|
|
905
|
+
if (sub.sql) {
|
|
906
|
+
subParts.push(sub.sql);
|
|
907
|
+
params.push(...sub.params);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (subParts.length > 0) {
|
|
911
|
+
const joiner = k === '$or' ? ' or ' : ' and ';
|
|
912
|
+
parts.push(`(${subParts.join(joiner)})`);
|
|
913
|
+
}
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
// Within $or/$and scope, $not provides element-level negation:
|
|
917
|
+
// "this element does not match the condition".
|
|
918
|
+
if (k === '$not') {
|
|
919
|
+
const sub = this.buildEmbeddedArrayWhere(cond[k], prop, jeAlias, referencedProps);
|
|
920
|
+
if (sub.sql) {
|
|
921
|
+
parts.push(`not (${sub.sql})`);
|
|
922
|
+
params.push(...sub.params);
|
|
923
|
+
}
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const { embProp, jsonPropName } = this.resolveEmbeddedProp(prop, k);
|
|
927
|
+
referencedProps.set(k, { name: jsonPropName, type: embProp.runtimeType ?? 'string' });
|
|
928
|
+
const lhs = this.#platform.getJsonArrayElementPropertySQL(jeAlias, jsonPropName, embProp.runtimeType ?? 'string');
|
|
929
|
+
const value = cond[k];
|
|
930
|
+
if (Utils.isPlainObject(value)) {
|
|
931
|
+
// Validate that all keys are operators — nested embeddables within array elements are not supported.
|
|
932
|
+
const valueKeys = Object.keys(value);
|
|
933
|
+
if (valueKeys.some(vk => !Utils.isOperator(vk))) {
|
|
934
|
+
throw ValidationError.invalidEmbeddableQuery(this.#entityName, k, prop.type);
|
|
935
|
+
}
|
|
936
|
+
const sub = this.buildEmbeddedArrayOperatorCondition(lhs, value, params);
|
|
937
|
+
parts.push(sub);
|
|
938
|
+
}
|
|
939
|
+
else if (value === null) {
|
|
940
|
+
parts.push(`${lhs} is null`);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
parts.push(`${lhs} = ?`);
|
|
944
|
+
params.push(value);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return { sql: parts.join(' and '), params };
|
|
948
|
+
}
|
|
949
|
+
buildEmbeddedArrayOperatorCondition(lhs, value, params) {
|
|
950
|
+
const supported = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$not', '$like', '$exists']);
|
|
951
|
+
const parts = [];
|
|
952
|
+
// Clone to avoid getOperatorReplacement mutating the original (it sets value[op] = null for $exists).
|
|
953
|
+
value = { ...value };
|
|
954
|
+
for (const op of Object.keys(value)) {
|
|
955
|
+
if (!supported.has(op)) {
|
|
956
|
+
throw new ValidationError(`Operator ${op} is not supported in embedded array queries`);
|
|
957
|
+
}
|
|
958
|
+
const replacement = this.getOperatorReplacement(op, value);
|
|
959
|
+
const val = value[op];
|
|
960
|
+
if (['$in', '$nin'].includes(op)) {
|
|
961
|
+
if (!Array.isArray(val)) {
|
|
962
|
+
throw new ValidationError(`Invalid query: ${op} operator expects an array value`);
|
|
963
|
+
}
|
|
964
|
+
else if (val.length === 0) {
|
|
965
|
+
parts.push(`1 = ${op === '$in' ? 0 : 1}`);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
val.forEach((v) => params.push(v));
|
|
969
|
+
parts.push(`${lhs} ${replacement} (${val.map(() => '?').join(', ')})`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
else if (op === '$exists') {
|
|
973
|
+
parts.push(`${lhs} ${replacement} null`);
|
|
974
|
+
}
|
|
975
|
+
else if (val === null) {
|
|
976
|
+
parts.push(`${lhs} ${replacement} null`);
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
parts.push(`${lhs} ${replacement} ?`);
|
|
980
|
+
params.push(val);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return parts.join(' and ');
|
|
984
|
+
}
|
|
830
985
|
processOnConflictCondition(cond, schema) {
|
|
831
986
|
const meta = this.#metadata.get(this.#entityName);
|
|
832
987
|
const tableName = meta.tableName;
|
package/query/enums.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare enum QueryType {
|
|
|
7
7
|
DELETE = "DELETE",
|
|
8
8
|
UPSERT = "UPSERT"
|
|
9
9
|
}
|
|
10
|
+
/** Operators that apply to the embedded array column itself, not to individual elements. */
|
|
11
|
+
export declare const EMBEDDABLE_ARRAY_OPS: string[];
|
|
10
12
|
export declare enum JoinType {
|
|
11
13
|
leftJoin = "left join",
|
|
12
14
|
innerJoin = "inner join",
|
package/query/enums.js
CHANGED
|
@@ -8,6 +8,8 @@ export var QueryType;
|
|
|
8
8
|
QueryType["DELETE"] = "DELETE";
|
|
9
9
|
QueryType["UPSERT"] = "UPSERT";
|
|
10
10
|
})(QueryType || (QueryType = {}));
|
|
11
|
+
/** Operators that apply to the embedded array column itself, not to individual elements. */
|
|
12
|
+
export const EMBEDDABLE_ARRAY_OPS = ['$contains', '$contained', '$overlap'];
|
|
11
13
|
export var JoinType;
|
|
12
14
|
(function (JoinType) {
|
|
13
15
|
JoinType["leftJoin"] = "left join";
|