uql-orm 0.67.0 → 0.68.0
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/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
- package/dist/cockroachdb/cockroachDialect.js +7 -3
- package/dist/d1/d1SqliteDialect.d.ts +1 -1
- package/dist/d1/d1SqliteDialect.js +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +107 -97
- package/dist/dialect/abstractSqlDialect.js +250 -293
- package/dist/dialect/hydrateColumn.js +33 -30
- package/dist/dialect/jsonSql.d.ts +32 -23
- package/dist/dialect/jsonSql.js +42 -31
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +19 -23
- package/dist/dialect/mysqlLikeSqlDialect.js +34 -51
- package/dist/dialect/pgLikeSqlDialect.d.ts +24 -17
- package/dist/dialect/pgLikeSqlDialect.js +53 -26
- package/dist/dialect/vectorCast.d.ts +2 -0
- package/dist/dialect/vectorCast.js +25 -5
- package/dist/dialect/vectorSqlDialect.d.ts +8 -8
- package/dist/dialect/vectorSqlDialect.js +11 -11
- package/dist/index.d.ts +1 -0
- package/dist/maria/mariaDialect.d.ts +16 -11
- package/dist/maria/mariaDialect.js +23 -19
- package/dist/migrate/ddl/indexDdl.d.ts +3 -1
- package/dist/migrate/ddl/indexDdl.js +5 -1
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +7 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +28 -6
- package/dist/migrate/ddl/pgIndexDdl.d.ts +0 -9
- package/dist/migrate/ddl/pgIndexDdl.js +2 -6
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -3
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +2 -9
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -9
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -9
- package/dist/migrate/introspection/mysqlIntrospector.js +2 -8
- package/dist/mongo/mongoDialect.d.ts +22 -10
- package/dist/mongo/mongoDialect.js +86 -38
- package/dist/mssql/mssqlDialect.d.ts +22 -18
- package/dist/mssql/mssqlDialect.js +54 -43
- package/dist/mysql/mysqlDialect.d.ts +16 -1
- package/dist/mysql/mysqlDialect.js +18 -2
- package/dist/sqlite/sqliteDialect.d.ts +17 -16
- package/dist/sqlite/sqliteDialect.js +35 -39
- package/dist/type/dialect.d.ts +0 -4
- package/dist/type/entity.d.ts +8 -3
- package/dist/type/vector.d.ts +5 -7
- package/dist/util/dialect.util.d.ts +2 -0
- package/dist/util/dialect.util.js +6 -2
- package/dist/util/object.util.d.ts +2 -4
- package/dist/util/object.util.js +4 -9
- package/package.json +1 -1
- package/dist/dialect/jsonArrayElemMatchUtils.d.ts +0 -2
- package/dist/dialect/jsonArrayElemMatchUtils.js +0 -7
- package/dist/dialect/pgVectorMetrics.d.ts +0 -13
- package/dist/dialect/pgVectorMetrics.js +0 -17
- package/dist/maria/mariaVectorMetrics.d.ts +0 -8
- package/dist/maria/mariaVectorMetrics.js +0 -10
|
@@ -100,10 +100,20 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
100
100
|
getPrimaryKeyParams(tableName) {
|
|
101
101
|
return [tableName];
|
|
102
102
|
}
|
|
103
|
-
/** The {@link ForeignKeyAction} a catalogue names, whatever its case. */
|
|
103
|
+
/** The {@link ForeignKeyAction} a catalogue names, whatever its case, and `SET_NULL` as SQL Server spells it. */
|
|
104
104
|
normalizeReferentialAction(action) {
|
|
105
|
-
const
|
|
106
|
-
return FOREIGN_KEY_ACTIONS.find((known) => known ===
|
|
105
|
+
const spelled = action.toUpperCase().replaceAll('_', ' ');
|
|
106
|
+
return FOREIGN_KEY_ACTIONS.find((known) => known === spelled);
|
|
107
|
+
}
|
|
108
|
+
/** Foreign keys read one row each, as {@link JoinedForeignKeyRow} lists them. */
|
|
109
|
+
joinedForeignKeys(rows) {
|
|
110
|
+
return rows.map((row) => ({
|
|
111
|
+
name: row.constraint_name,
|
|
112
|
+
columns: row.columns.split(','),
|
|
113
|
+
references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
|
|
114
|
+
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
115
|
+
onUpdate: this.normalizeReferentialAction(row.update_rule),
|
|
116
|
+
}));
|
|
107
117
|
}
|
|
108
118
|
/**
|
|
109
119
|
* Convert bigint/null values to number safely.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
|
|
2
|
-
import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
|
|
2
|
+
import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
|
|
3
3
|
/**
|
|
4
4
|
* SQL Server schema introspector.
|
|
5
5
|
*
|
|
@@ -31,14 +31,7 @@ export declare class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
|
|
|
31
31
|
columns: string;
|
|
32
32
|
is_unique: boolean;
|
|
33
33
|
}[]): Promise<IndexSchema[]>;
|
|
34
|
-
protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results:
|
|
35
|
-
constraint_name: string;
|
|
36
|
-
columns: string;
|
|
37
|
-
referenced_table: string;
|
|
38
|
-
referenced_columns: string;
|
|
39
|
-
delete_rule: string;
|
|
40
|
-
update_rule: string;
|
|
41
|
-
}[]): Promise<ForeignKeySchema[]>;
|
|
34
|
+
protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: JoinedForeignKeyRow[]): Promise<ForeignKeySchema[]>;
|
|
42
35
|
/**
|
|
43
36
|
* The engine reprints a default from its own parse tree: all of it in one pair of parentheses, and a
|
|
44
37
|
* number in a second, so `((0))`, `((1)+(2))`, `(N'x')` and `(getdate())`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
|
|
1
|
+
import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.js';
|
|
2
2
|
/**
|
|
3
3
|
* SQL Server schema introspector.
|
|
4
4
|
*
|
|
@@ -146,14 +146,7 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
146
146
|
}));
|
|
147
147
|
}
|
|
148
148
|
async mapForeignKeysResult(_read, _tableName, results) {
|
|
149
|
-
return
|
|
150
|
-
name: row.constraint_name,
|
|
151
|
-
columns: row.columns.split(','),
|
|
152
|
-
references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
|
|
153
|
-
// `sys` spells them with an underscore: `SET_NULL`, `NO_ACTION`.
|
|
154
|
-
onDelete: this.normalizeReferentialAction(row.delete_rule.replaceAll('_', ' ')),
|
|
155
|
-
onUpdate: this.normalizeReferentialAction(row.update_rule.replaceAll('_', ' ')),
|
|
156
|
-
}));
|
|
149
|
+
return this.joinedForeignKeys(results);
|
|
157
150
|
}
|
|
158
151
|
/**
|
|
159
152
|
* The engine reprints a default from its own parse tree: all of it in one pair of parentheses, and a
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
|
|
2
|
-
import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
|
|
2
|
+
import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
|
|
3
3
|
/**
|
|
4
4
|
* MySQL/MariaDB schema introspector.
|
|
5
5
|
* Works with both MySQL and MariaDB as they share the same information_schema structure.
|
|
@@ -21,14 +21,7 @@ export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
|
|
|
21
21
|
columns: string;
|
|
22
22
|
is_unique: number;
|
|
23
23
|
}[]): Promise<IndexSchema[]>;
|
|
24
|
-
protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results:
|
|
25
|
-
constraint_name: string;
|
|
26
|
-
columns: string;
|
|
27
|
-
referenced_table: string;
|
|
28
|
-
referenced_columns: string;
|
|
29
|
-
delete_rule: string;
|
|
30
|
-
update_rule: string;
|
|
31
|
-
}[]): Promise<ForeignKeySchema[]>;
|
|
24
|
+
protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: JoinedForeignKeyRow[]): Promise<ForeignKeySchema[]>;
|
|
32
25
|
/**
|
|
33
26
|
* MariaDB prints a string default as the literal it is (`'it''s'`). MySQL prints one bare, save an
|
|
34
27
|
* expression default (`DEFAULT ('x')`, what a `TEXT` column takes), which comes with a charset
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { unescapeMysqlString } from '../../util/sqlLiteral.js';
|
|
2
|
-
import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
|
|
2
|
+
import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.js';
|
|
3
3
|
/**
|
|
4
4
|
* MySQL/MariaDB schema introspector.
|
|
5
5
|
* Works with both MySQL and MariaDB as they share the same information_schema structure.
|
|
@@ -117,13 +117,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
117
117
|
}));
|
|
118
118
|
}
|
|
119
119
|
async mapForeignKeysResult(_read, _tableName, results) {
|
|
120
|
-
return
|
|
121
|
-
name: row.constraint_name,
|
|
122
|
-
columns: row.columns.split(','),
|
|
123
|
-
references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
|
|
124
|
-
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
125
|
-
onUpdate: this.normalizeReferentialAction(row.update_rule),
|
|
126
|
-
}));
|
|
120
|
+
return this.joinedForeignKeys(results);
|
|
127
121
|
}
|
|
128
122
|
/**
|
|
129
123
|
* MariaDB prints a string default as the literal it is (`'it''s'`). MySQL prints one bare, save an
|
|
@@ -55,12 +55,20 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
55
55
|
*/
|
|
56
56
|
private appendLogicalOperator;
|
|
57
57
|
/**
|
|
58
|
-
* Emits the correlated `$lookup` for one relation condition and
|
|
59
|
-
* result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
58
|
+
* Emits the correlated `$lookup` for one relation condition, and adds to `filter` the condition testing
|
|
59
|
+
* its result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
60
60
|
* `$size`. The target's (and, for ManyToMany, the junction's) own filters scope the lookup, so a
|
|
61
61
|
* relation subquery can no more read out-of-scope rows than a direct query on the target can.
|
|
62
62
|
*/
|
|
63
63
|
private appendRelationLookup;
|
|
64
|
+
/** Adds `expr` to `filter`'s `$expr`, `AND`ed with any already there. */
|
|
65
|
+
private static andExpr;
|
|
66
|
+
/**
|
|
67
|
+
* `$size` against bounds, which MongoDB's own `$size` takes only as a number: the array at `path` counted
|
|
68
|
+
* in an `$expr`, which no other value satisfies. `$and` may evaluate every operand, so the count reads an
|
|
69
|
+
* empty array in place of any other value.
|
|
70
|
+
*/
|
|
71
|
+
private static arraySize;
|
|
64
72
|
/**
|
|
65
73
|
* The correlated `$lookup` for the target rows of one relation `where` narrows, as `temp`: straight at
|
|
66
74
|
* the target, or for a many-to-many from inside its junction's rows. The caller's filter bypass is not
|
|
@@ -74,11 +82,8 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
74
82
|
* Each end is one field matched against one `_id`, so both sides must be sole-keyed.
|
|
75
83
|
*/
|
|
76
84
|
private junctionOf;
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
* the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
|
|
80
|
-
*/
|
|
81
|
-
private compareRelationCount;
|
|
85
|
+
/** `count` compared with `size`, a number or its bounds, as an aggregation expression. */
|
|
86
|
+
private static compareCount;
|
|
82
87
|
/** Whether a query subtracts `key` from the projection, via `$exclude` or a negative `$select`. */
|
|
83
88
|
private subtractsKey;
|
|
84
89
|
/**
|
|
@@ -104,10 +109,17 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
104
109
|
*/
|
|
105
110
|
private transformOperators;
|
|
106
111
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
112
|
+
* What a value holding `value` matches, as the SQL engines read it: an operator map tests it, an array
|
|
113
|
+
* holds each element by `$all`, an object each key by {@link containment}, and a scalar is equal.
|
|
114
|
+
*/
|
|
115
|
+
private held;
|
|
116
|
+
/**
|
|
117
|
+
* `$all` as one `$elemMatch` per value, since native `$all` never looks into an element that is an array:
|
|
118
|
+
* an array element holds each of its values alike, an object or operator map as {@link held} reads it.
|
|
109
119
|
*/
|
|
110
|
-
private
|
|
120
|
+
private allHolding;
|
|
121
|
+
/** An object's keys as {@link held} reads each, a nested object's by its dotted path rather than whole. */
|
|
122
|
+
private containment;
|
|
111
123
|
select<E extends Document>(entity: Type<E>, select?: QuerySelectValue<E>, exclude?: QueryExclude<E>): Record<string, 0 | 1>;
|
|
112
124
|
/**
|
|
113
125
|
* The `$sort` stage. A relation key reads the document a `$lookup` unwound onto the parent, so - as
|
|
@@ -5,7 +5,7 @@ import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js
|
|
|
5
5
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
7
7
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
8
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
9
9
|
/** Default {@link DialectFeatures} for MongoDB. */
|
|
10
10
|
export const mongoDialectFeatures = {
|
|
11
11
|
ifNotExists: false,
|
|
@@ -105,7 +105,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
105
105
|
if (!lookups) {
|
|
106
106
|
throw new TypeError(`filtering by relation '${key}' is not supported here on MongoDB`);
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
this.appendRelationLookup(filter, meta, key, val, lookups);
|
|
109
109
|
}
|
|
110
110
|
else {
|
|
111
111
|
this.assertNoRaw(val);
|
|
@@ -115,13 +115,21 @@ export class MongoDialect extends AbstractDialect {
|
|
|
115
115
|
if ((key === MongoDialect.ID_KEY || isReference) && !isOperatorObject(val)) {
|
|
116
116
|
val = this.toWireId(val);
|
|
117
117
|
}
|
|
118
|
-
if (isOperatorObject(val)) {
|
|
119
|
-
|
|
118
|
+
if (!isOperatorObject(val)) {
|
|
119
|
+
filter[key] = Array.isArray(val) ? { $in: val } : val;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// MongoDB's `$size` takes only a number, so bounds become an `$expr` beside the other operators.
|
|
123
|
+
const { $size: size, ...ops } = val;
|
|
124
|
+
if (!isRecord(size)) {
|
|
125
|
+
filter[key] = this.transformOperators(val);
|
|
120
126
|
}
|
|
121
|
-
else
|
|
122
|
-
|
|
127
|
+
else {
|
|
128
|
+
MongoDialect.andExpr(filter, MongoDialect.arraySize(key, size));
|
|
129
|
+
if (hasKeys(ops)) {
|
|
130
|
+
filter[key] = this.transformOperators(ops);
|
|
131
|
+
}
|
|
123
132
|
}
|
|
124
|
-
filter[key] = val;
|
|
125
133
|
}
|
|
126
134
|
}
|
|
127
135
|
return filter;
|
|
@@ -149,21 +157,38 @@ export class MongoDialect extends AbstractDialect {
|
|
|
149
157
|
filter['$nor'] = [...(filter['$nor'] ?? []), ...negated];
|
|
150
158
|
}
|
|
151
159
|
/**
|
|
152
|
-
* Emits the correlated `$lookup` for one relation condition and
|
|
153
|
-
* result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
160
|
+
* Emits the correlated `$lookup` for one relation condition, and adds to `filter` the condition testing
|
|
161
|
+
* its result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
154
162
|
* `$size`. The target's (and, for ManyToMany, the junction's) own filters scope the lookup, so a
|
|
155
163
|
* relation subquery can no more read out-of-scope rows than a direct query on the target can.
|
|
156
164
|
*/
|
|
157
|
-
appendRelationLookup(meta, relKey, val, lookups) {
|
|
165
|
+
appendRelationLookup(filter, meta, relKey, val, lookups) {
|
|
158
166
|
const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
|
|
159
167
|
const sizeVal = parseRelationSize(val);
|
|
160
168
|
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: COUNT_ALIAS }];
|
|
161
169
|
const where = (sizeVal === undefined ? val : {});
|
|
162
170
|
lookups.temps.push(temp);
|
|
163
171
|
lookups.stages.push(this.relationLookup(meta, meta.relations[relKey], where, temp, tail));
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
if (sizeVal === undefined) {
|
|
173
|
+
filter[`${temp}.0`] = { $exists: true };
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
MongoDialect.andExpr(filter, MongoDialect.compareCount(this.tally(temp), sizeVal));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Adds `expr` to `filter`'s `$expr`, `AND`ed with any already there. */
|
|
180
|
+
static andExpr(filter, expr) {
|
|
181
|
+
filter['$expr'] = filter['$expr'] ? { $and: [filter['$expr'], expr] } : expr;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* `$size` against bounds, which MongoDB's own `$size` takes only as a number: the array at `path` counted
|
|
185
|
+
* in an `$expr`, which no other value satisfies. `$and` may evaluate every operand, so the count reads an
|
|
186
|
+
* empty array in place of any other value.
|
|
187
|
+
*/
|
|
188
|
+
static arraySize(path, size) {
|
|
189
|
+
const value = `$${path}`;
|
|
190
|
+
const count = { $size: { $cond: [{ $isArray: value }, value, []] } };
|
|
191
|
+
return { $and: [{ $isArray: value }, MongoDialect.compareCount(count, size)] };
|
|
167
192
|
}
|
|
168
193
|
/**
|
|
169
194
|
* The correlated `$lookup` for the target rows of one relation `where` narrows, as `temp`: straight at
|
|
@@ -222,22 +247,18 @@ export class MongoDialect extends AbstractDialect {
|
|
|
222
247
|
target: this.columnOf(throughMeta, targetColumn),
|
|
223
248
|
};
|
|
224
249
|
}
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
compareRelationCount(temp, sizeVal) {
|
|
230
|
-
const count = this.tally(temp);
|
|
231
|
-
if (typeof sizeVal === 'number') {
|
|
232
|
-
return { $eq: [count, sizeVal] };
|
|
250
|
+
/** `count` compared with `size`, a number or its bounds, as an aggregation expression. */
|
|
251
|
+
static compareCount(count, size) {
|
|
252
|
+
if (typeof size === 'number') {
|
|
253
|
+
return { $eq: [count, size] };
|
|
233
254
|
}
|
|
234
|
-
const comparisons = Object.entries(
|
|
255
|
+
const comparisons = Object.entries(size)
|
|
235
256
|
.filter(([, bound]) => bound !== undefined)
|
|
236
|
-
.flatMap(([op, bound]) => op === '$between'
|
|
257
|
+
.flatMap(([op, bound]) => op === '$between' && Array.isArray(bound)
|
|
237
258
|
? [{ $gte: [count, bound[0]] }, { $lte: [count, bound[1]] }]
|
|
238
259
|
: [{ [op]: [count, bound] }]);
|
|
239
260
|
if (!comparisons.length) {
|
|
240
|
-
throw new TypeError('$size
|
|
261
|
+
throw new TypeError('$size needs at least one comparison');
|
|
241
262
|
}
|
|
242
263
|
return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
|
|
243
264
|
}
|
|
@@ -311,7 +332,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
311
332
|
// mapping - passing it through raw sends UQL-only operators (`$startsWith`, `$between`, ...)
|
|
312
333
|
// straight to the server, which rejects them as unknown.
|
|
313
334
|
if (op === '$elemMatch') {
|
|
314
|
-
result[op] = this.
|
|
335
|
+
result[op] = this.held(val);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
// An object or an array is matched by what it holds, as the SQL engines read it, where native `$all`
|
|
339
|
+
// compares the whole element. MongoDB takes `$elemMatch` there only when every value is one.
|
|
340
|
+
if (op === '$all' && Array.isArray(val) && val.some((value) => Array.isArray(value) || isOperatorMap(value))) {
|
|
341
|
+
result[op] = this.allHolding(val);
|
|
315
342
|
continue;
|
|
316
343
|
}
|
|
317
344
|
// Native MongoDB operators - pass through directly
|
|
@@ -354,14 +381,36 @@ export class MongoDialect extends AbstractDialect {
|
|
|
354
381
|
return result;
|
|
355
382
|
}
|
|
356
383
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
384
|
+
* What a value holding `value` matches, as the SQL engines read it: an operator map tests it, an array
|
|
385
|
+
* holds each element by `$all`, an object each key by {@link containment}, and a scalar is equal.
|
|
359
386
|
*/
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
return this.transformOperators(
|
|
387
|
+
held(value) {
|
|
388
|
+
if (Array.isArray(value)) {
|
|
389
|
+
return this.transformOperators({ $all: value });
|
|
363
390
|
}
|
|
364
|
-
|
|
391
|
+
if (!isOperatorMap(value)) {
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
return isOperatorObject(value) ? this.transformOperators(value) : this.containment(value);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* `$all` as one `$elemMatch` per value, since native `$all` never looks into an element that is an array:
|
|
398
|
+
* an array element holds each of its values alike, an object or operator map as {@link held} reads it.
|
|
399
|
+
*/
|
|
400
|
+
allHolding(values) {
|
|
401
|
+
return values.map((value) => {
|
|
402
|
+
if (Array.isArray(value)) {
|
|
403
|
+
return { $elemMatch: { $all: this.allHolding(value) } };
|
|
404
|
+
}
|
|
405
|
+
return { $elemMatch: isOperatorMap(value) ? this.held(value) : { $eq: value } };
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** An object's keys as {@link held} reads each, a nested object's by its dotted path rather than whole. */
|
|
409
|
+
containment(object, prefix = '') {
|
|
410
|
+
return Object.fromEntries(Object.entries(object).flatMap(([key, value]) => {
|
|
411
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
412
|
+
return isJsonObject(value) ? Object.entries(this.containment(value, path)) : [[path, this.held(value)]];
|
|
413
|
+
}));
|
|
365
414
|
}
|
|
366
415
|
select(entity, select, exclude) {
|
|
367
416
|
const meta = getMeta(entity);
|
|
@@ -832,16 +881,15 @@ export class MongoDialect extends AbstractDialect {
|
|
|
832
881
|
const groups = this.groupUpdateOperators(persistable);
|
|
833
882
|
const { set, push, pull, unset } = groups;
|
|
834
883
|
const exprKeys = [...Object.keys(pull), ...Object.keys(set), ...Object.keys(push)];
|
|
835
|
-
//
|
|
836
|
-
//
|
|
884
|
+
// Native `$pull` fails on a value that is no array, and MongoDB rejects two operators targeting one
|
|
885
|
+
// path in a single update document: either forces the pipeline form.
|
|
837
886
|
const allPaths = [...exprKeys, ...unset];
|
|
838
|
-
if (new Set(allPaths).size < allPaths.length) {
|
|
887
|
+
if (hasKeys(pull) || new Set(allPaths).size < allPaths.length) {
|
|
839
888
|
return this.getUpdatePipeline(groups, new Set(exprKeys));
|
|
840
889
|
}
|
|
841
890
|
return {
|
|
842
891
|
...(hasKeys(set) && { $set: set }),
|
|
843
892
|
...(hasKeys(push) && { $push: push }),
|
|
844
|
-
...(hasKeys(pull) && { $pull: pull }),
|
|
845
893
|
...(unset.size > 0 && { $unset: Object.fromEntries([...unset].map((path) => [path, ''])) }),
|
|
846
894
|
};
|
|
847
895
|
}
|
|
@@ -862,9 +910,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
862
910
|
if (path in push) {
|
|
863
911
|
expr = { $concatArrays: [expr, [{ $literal: push[path] }]] };
|
|
864
912
|
}
|
|
865
|
-
// Only `$set` and `$push` create a key. A `$pull` alone
|
|
866
|
-
//
|
|
867
|
-
assignments[path] = path in set || path in push ? expr : { $cond: [{ $isArray: `$${path}` }, expr,
|
|
913
|
+
// Only `$set` and `$push` create a key. A `$pull` alone leaves any value that is no array as it is,
|
|
914
|
+
// and an absent one absent, since a pipeline `$set` of a missing field adds none.
|
|
915
|
+
assignments[path] = path in set || path in push ? expr : { $cond: [{ $isArray: `$${path}` }, expr, `$${path}`] };
|
|
868
916
|
}
|
|
869
917
|
return [{ $set: assignments }, ...(unset.size > 0 ? [{ $unset: [...unset] }] : [])];
|
|
870
918
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
2
|
+
import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
|
|
2
3
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
3
|
-
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager,
|
|
4
|
+
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
4
5
|
/** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
|
|
5
6
|
export declare class MsSqlDialect extends MergeSqlDialect {
|
|
6
7
|
#private;
|
|
@@ -103,18 +104,11 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
103
104
|
estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
|
|
104
105
|
protected numericCast(expr: string): string;
|
|
105
106
|
/**
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* `OPENJSON` at the path's parent, matching its last segment as a key, in either reading: `JSON_VALUE`
|
|
108
|
+
* answers NULL for text past 4000 characters, and `JSON_QUERY` for a scalar. A value reads back as
|
|
109
|
+
* text, which {@link jsonScalarParam} binds its operand as, and an array or object as its own JSON.
|
|
109
110
|
*/
|
|
110
|
-
protected
|
|
111
|
-
/**
|
|
112
|
-
* The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
|
|
113
|
-
* an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
|
|
114
|
-
* `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
|
|
115
|
-
* text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
|
|
116
|
-
*/
|
|
117
|
-
protected getJsonPathJsonbExpr(escapedColumn: string, jsonPathStr: string): string;
|
|
111
|
+
protected jsonPathReading(escapedColumn: string, path: string): string;
|
|
118
112
|
/**
|
|
119
113
|
* A value compared against a JSON path, which reads back as text, so only a boolean needs spelling as
|
|
120
114
|
* `'true'`; SQL Server has no cast that parses text as JSON.
|
|
@@ -126,11 +120,20 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
126
120
|
* flattened a boolean to 1/0 for this engine's columns, so the cast is what restores it.
|
|
127
121
|
*/
|
|
128
122
|
protected jsonWriteParam(ctx: QueryContext, value: unknown): string;
|
|
129
|
-
protected jsonElemFrom(
|
|
130
|
-
/** `
|
|
131
|
-
protected
|
|
132
|
-
|
|
133
|
-
protected
|
|
123
|
+
protected jsonElemFrom(slot: JsonSlot, alias: string): string;
|
|
124
|
+
/** `JSON_QUERY` answers an array or an object as written, and a scalar as NULL. */
|
|
125
|
+
protected jsonIsArray(slot: JsonSlot): string;
|
|
126
|
+
/** An object element's `value` is its JSON text, which its fields are paths into. */
|
|
127
|
+
protected jsonElemDoc(alias: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* An element of the value's own JSON type: `OPENJSON` reads a string and a number back as the same text,
|
|
130
|
+
* so the `type` it reports is what tells `'5'` from `5`. A number compares by value, cast on both sides:
|
|
131
|
+
* text against a numeric parameter converts implicitly, which throws on an element that is no number.
|
|
132
|
+
*/
|
|
133
|
+
protected jsonElemEquals(ctx: QueryContext, _slot: JsonSlot, alias: string, value: unknown): string;
|
|
134
|
+
protected jsonLength(slot: JsonSlot): string;
|
|
135
|
+
/** SQL Server orders JSON as the text `OPENJSON` reads, so a number sorts by its value first. */
|
|
136
|
+
protected readonly jsonSortModes: readonly JsonAccessMode[];
|
|
134
137
|
/** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
|
|
135
138
|
protected jsonSet(ctx: QueryContext, expr: string, set: Record<string, unknown>, _field?: FieldOptions): string;
|
|
136
139
|
/** `'append '` prefixing the path extends the array there, creating it where there is none. */
|
|
@@ -139,7 +142,8 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
139
142
|
protected jsonUnset(_ctx: QueryContext, expr: string, unset: readonly string[]): string;
|
|
140
143
|
/**
|
|
141
144
|
* The surviving elements are re-aggregated into an array and written back whole - there is no
|
|
142
|
-
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
|
|
145
|
+
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string. Only an
|
|
146
|
+
* array is rewritten: `JSON_MODIFY` would create an absent key, and any other value stays as it is.
|
|
143
147
|
*/
|
|
144
148
|
protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
|
|
145
149
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relationTermKey } from '../dialect/abstractSqlDialect.js';
|
|
2
|
-
import { COUNT_ALIAS,
|
|
2
|
+
import { COUNT_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
|
|
3
3
|
import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
4
|
-
import { jsonPath } from '../dialect/jsonSql.js';
|
|
4
|
+
import { jsonArraySlotArgs, jsonPath, jsonSlotArgs } from '../dialect/jsonSql.js';
|
|
5
5
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
6
6
|
import { getMeta } from '../entity/index.js';
|
|
7
7
|
import { fieldOptionsToCanonical } from '../schema/canonicalType.js';
|
|
@@ -34,12 +34,17 @@ const MSSQL_FEATURES = {
|
|
|
34
34
|
rowLockOf: true,
|
|
35
35
|
orderedUpsertReturning: false,
|
|
36
36
|
orderedJsonAggregates: true,
|
|
37
|
-
partialJsonContainment: false,
|
|
38
|
-
typedJsonElements: false,
|
|
39
37
|
narrowVectorTypes: false,
|
|
40
38
|
vectorTuningNeedsTransaction: false,
|
|
41
39
|
serialDeclaresPrimaryKey: false,
|
|
42
40
|
};
|
|
41
|
+
/** The `type` `OPENJSON` reports for the JSON scalar an element is compared with; anything else binds as a string. */
|
|
42
|
+
function openJsonType(value) {
|
|
43
|
+
if (typeof value === 'number') {
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
return typeof value === 'boolean' ? 3 : 1;
|
|
47
|
+
}
|
|
43
48
|
/** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
|
|
44
49
|
export class MsSqlDialect extends MergeSqlDialect {
|
|
45
50
|
features = MSSQL_FEATURES;
|
|
@@ -234,25 +239,19 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
234
239
|
return `TRY_CAST(${expr} AS FLOAT)`;
|
|
235
240
|
}
|
|
236
241
|
/**
|
|
237
|
-
* `
|
|
238
|
-
*
|
|
239
|
-
*
|
|
242
|
+
* `OPENJSON` at the path's parent, matching its last segment as a key, in either reading: `JSON_VALUE`
|
|
243
|
+
* answers NULL for text past 4000 characters, and `JSON_QUERY` for a scalar. A value reads back as
|
|
244
|
+
* text, which {@link jsonScalarParam} binds its operand as, and an array or object as its own JSON.
|
|
240
245
|
*/
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
jsonPathReading(escapedColumn, path) {
|
|
247
|
+
if (!path) {
|
|
248
|
+
return escapedColumn;
|
|
249
|
+
}
|
|
250
|
+
const dot = path.lastIndexOf('.');
|
|
251
|
+
const parent = dot === -1 ? '$' : `$.${path.slice(0, dot).split('.').map(escapeSingleQuotes).join('.')}`;
|
|
252
|
+
const leaf = escapeSingleQuotes(path.slice(dot + 1));
|
|
245
253
|
return `(SELECT ${this.#elem.value} FROM OPENJSON(${escapedColumn}, '${parent}') WHERE ${this.#elem.key} = N'${leaf}')`;
|
|
246
254
|
}
|
|
247
|
-
/**
|
|
248
|
-
* The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
|
|
249
|
-
* an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
|
|
250
|
-
* `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
|
|
251
|
-
* text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
|
|
252
|
-
*/
|
|
253
|
-
getJsonPathJsonbExpr(escapedColumn, jsonPathStr) {
|
|
254
|
-
return this.getJsonPathScalarExpr(escapedColumn, jsonPathStr);
|
|
255
|
-
}
|
|
256
255
|
/**
|
|
257
256
|
* A value compared against a JSON path, which reads back as text, so only a boolean needs spelling as
|
|
258
257
|
* `'true'`; SQL Server has no cast that parses text as JSON.
|
|
@@ -286,24 +285,37 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
286
285
|
}
|
|
287
286
|
return `JSON_QUERY(${this.addValue(ctx, JSON.stringify(value))})`;
|
|
288
287
|
}
|
|
289
|
-
jsonElemFrom(
|
|
290
|
-
return `OPENJSON(${
|
|
288
|
+
jsonElemFrom(slot, alias) {
|
|
289
|
+
return `OPENJSON(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))}) ${alias}`;
|
|
290
|
+
}
|
|
291
|
+
/** `JSON_QUERY` answers an array or an object as written, and a scalar as NULL. */
|
|
292
|
+
jsonIsArray(slot) {
|
|
293
|
+
return `LEFT(JSON_QUERY(${jsonSlotArgs(slot)}), 1) = '['`;
|
|
291
294
|
}
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
return
|
|
295
|
-
? `${alias}.${this.#elem.value}`
|
|
296
|
-
: `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
|
|
295
|
+
/** An object element's `value` is its JSON text, which its fields are paths into. */
|
|
296
|
+
jsonElemDoc(alias) {
|
|
297
|
+
return `${alias}.${this.#elem.value}`;
|
|
297
298
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
/**
|
|
300
|
+
* An element of the value's own JSON type: `OPENJSON` reads a string and a number back as the same text,
|
|
301
|
+
* so the `type` it reports is what tells `'5'` from `5`. A number compares by value, cast on both sides:
|
|
302
|
+
* text against a numeric parameter converts implicitly, which throws on an element that is no number.
|
|
303
|
+
*/
|
|
304
|
+
jsonElemEquals(ctx, _slot, alias, value) {
|
|
305
|
+
const type = `${alias}.${this.#elem.type}`;
|
|
306
|
+
if (value === null) {
|
|
307
|
+
return `${type} = 0`;
|
|
308
|
+
}
|
|
309
|
+
const elem = this.jsonElemDoc(alias);
|
|
310
|
+
const param = this.jsonScalarParam(ctx, value);
|
|
311
|
+
const equal = typeof value === 'number' ? `${this.numericCast(elem)} = ${this.numericCast(param)}` : `${elem} = ${param}`;
|
|
312
|
+
return `${equal} AND ${type} = ${openJsonType(value)}`;
|
|
302
313
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
|
|
314
|
+
jsonLength(slot) {
|
|
315
|
+
return `(SELECT COUNT(*) FROM OPENJSON(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))}))`;
|
|
306
316
|
}
|
|
317
|
+
/** SQL Server orders JSON as the text `OPENJSON` reads, so a number sorts by its value first. */
|
|
318
|
+
jsonSortModes = ['numeric', 'text'];
|
|
307
319
|
/** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
|
|
308
320
|
jsonSet(ctx, expr, set, _field) {
|
|
309
321
|
for (const [key, value] of Object.entries(set)) {
|
|
@@ -323,25 +335,24 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
323
335
|
}
|
|
324
336
|
/**
|
|
325
337
|
* The surviving elements are re-aggregated into an array and written back whole - there is no
|
|
326
|
-
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
|
|
338
|
+
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string. Only an
|
|
339
|
+
* array is rewritten: `JSON_MODIFY` would create an absent key, and any other value stays as it is.
|
|
327
340
|
*/
|
|
328
341
|
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
329
|
-
const
|
|
330
|
-
const val =
|
|
342
|
+
const slot = { base: escapedCol, path: key };
|
|
343
|
+
const val = this.jsonElemDoc(JSON_PULL_ALIAS);
|
|
331
344
|
// `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
|
|
332
345
|
// re-encoded from its reported `type` before the array is put back together - concatenated raw,
|
|
333
346
|
// the result is text the engine then refuses to parse as JSON.
|
|
334
|
-
const encoded = `CASE ${
|
|
347
|
+
const encoded = `CASE ${JSON_PULL_ALIAS}.${this.#elem.type}` +
|
|
335
348
|
` WHEN 0 THEN 'null'` +
|
|
336
349
|
` WHEN 1 THEN '"' + STRING_ESCAPE(${val}, 'json') + '"'` +
|
|
337
350
|
` ELSE ${val} END`;
|
|
338
351
|
// `IS NULL OR` because a JSON null element reads back as SQL NULL, and `<>` against one is
|
|
339
352
|
// unknown rather than true - which silently dropped every null from the array it rebuilt.
|
|
340
|
-
const kept = `SELECT '[' + STRING_AGG(${encoded}, ',') + ']' FROM
|
|
353
|
+
const kept = `SELECT '[' + STRING_AGG(${encoded}, ',') + ']' FROM ${this.jsonElemFrom(slot, JSON_PULL_ALIAS)}` +
|
|
341
354
|
` WHERE ${val} IS NULL OR ${val} <> ${this.jsonScalarParam(ctx, value)}`;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const path = jsonPath(key);
|
|
345
|
-
return `CASE WHEN JSON_QUERY(${escapedCol}, ${path}) IS NULL THEN ${expr} ELSE JSON_MODIFY(${expr}, ${path}, JSON_QUERY(COALESCE((${kept}), '[]'))) END`;
|
|
355
|
+
const pulled = `JSON_MODIFY(${expr}, ${jsonPath(key)}, JSON_QUERY(COALESCE((${kept}), '[]')))`;
|
|
356
|
+
return `CASE WHEN ${this.jsonIsArray(slot)} THEN ${pulled} ELSE ${expr} END`;
|
|
346
357
|
}
|
|
347
358
|
}
|