uql-orm 0.19.0 → 0.20.1
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/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/bunSql/bunSqlCockroachDialect.d.ts +1 -1
- package/dist/bunSql/bunSqlCockroachDialect.js +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +130 -58
- package/dist/dialect/abstractSqlDialect.d.ts.map +1 -1
- package/dist/dialect/abstractSqlDialect.js +226 -178
- package/dist/dialect/abstractSqlDialect.js.map +1 -1
- package/dist/dialect/jsonArrayElemMatchUtils.d.ts +5 -6
- package/dist/dialect/jsonArrayElemMatchUtils.d.ts.map +1 -1
- package/dist/dialect/jsonArrayElemMatchUtils.js +9 -19
- package/dist/dialect/jsonArrayElemMatchUtils.js.map +1 -1
- package/dist/dialect/jsonSql.d.ts +44 -0
- package/dist/dialect/jsonSql.d.ts.map +1 -0
- package/dist/dialect/jsonSql.js +57 -0
- package/dist/dialect/jsonSql.js.map +1 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +42 -2
- package/dist/dialect/mysqlLikeSqlDialect.d.ts.map +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +73 -22
- package/dist/dialect/mysqlLikeSqlDialect.js.map +1 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +28 -10
- package/dist/dialect/pgLikeSqlDialect.d.ts.map +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +56 -49
- package/dist/dialect/pgLikeSqlDialect.js.map +1 -1
- package/dist/dialect/queryContext.d.ts +13 -1
- package/dist/dialect/queryContext.d.ts.map +1 -1
- package/dist/dialect/queryContext.js +17 -2
- package/dist/dialect/queryContext.js.map +1 -1
- package/dist/dialect/vectorSqlDialect.d.ts +54 -0
- package/dist/dialect/vectorSqlDialect.d.ts.map +1 -0
- package/dist/dialect/vectorSqlDialect.js +70 -0
- package/dist/dialect/vectorSqlDialect.js.map +1 -0
- package/dist/entity/decorator/index-decorator.d.ts +6 -13
- package/dist/entity/decorator/index-decorator.d.ts.map +1 -1
- package/dist/entity/decorator/index-decorator.js.map +1 -1
- package/dist/http/handler.d.ts +5 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js.map +1 -1
- package/dist/maria/mariaDialect.d.ts +15 -4
- package/dist/maria/mariaDialect.d.ts.map +1 -1
- package/dist/maria/mariaDialect.js +25 -13
- package/dist/maria/mariaDialect.js.map +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts.map +1 -1
- package/dist/mongo/mongoDialect.d.ts +27 -6
- package/dist/mongo/mongoDialect.d.ts.map +1 -1
- package/dist/mongo/mongoDialect.js +107 -14
- package/dist/mongo/mongoDialect.js.map +1 -1
- package/dist/mongo/mongodbQuerier.d.ts.map +1 -1
- package/dist/mongo/mongodbQuerier.js +5 -4
- package/dist/mongo/mongodbQuerier.js.map +1 -1
- package/dist/mysql/mysqlDialect.d.ts +0 -1
- package/dist/mysql/mysqlDialect.d.ts.map +1 -1
- package/dist/mysql/mysqlDialect.js +0 -1
- package/dist/mysql/mysqlDialect.js.map +1 -1
- package/dist/postgres/postgresWireDriverCapabilities.d.ts +1 -1
- package/dist/postgres/postgresWireDriverCapabilities.js +1 -1
- package/dist/schema/schemaAST.d.ts.map +1 -1
- package/dist/schema/schemaAST.js +7 -19
- package/dist/schema/schemaAST.js.map +1 -1
- package/dist/schema/types.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.d.ts +28 -8
- package/dist/sqlite/sqliteDialect.d.ts.map +1 -1
- package/dist/sqlite/sqliteDialect.js +65 -80
- package/dist/sqlite/sqliteDialect.js.map +1 -1
- package/dist/type/dialect.d.ts +199 -0
- package/dist/type/dialect.d.ts.map +1 -0
- package/dist/type/dialect.js +2 -0
- package/dist/type/dialect.js.map +1 -0
- package/dist/type/entity.d.ts +64 -43
- package/dist/type/entity.d.ts.map +1 -1
- package/dist/type/entity.js.map +1 -1
- package/dist/type/index.d.ts +5 -0
- package/dist/type/index.d.ts.map +1 -1
- package/dist/type/index.js +5 -0
- package/dist/type/index.js.map +1 -1
- package/dist/type/querier.d.ts +3 -5
- package/dist/type/querier.d.ts.map +1 -1
- package/dist/type/querier.js.map +1 -1
- package/dist/type/query.d.ts +5 -711
- package/dist/type/query.d.ts.map +1 -1
- package/dist/type/query.js +1 -51
- package/dist/type/query.js.map +1 -1
- package/dist/type/queryAggregate.d.ts +191 -0
- package/dist/type/queryAggregate.d.ts.map +1 -0
- package/dist/type/queryAggregate.js +39 -0
- package/dist/type/queryAggregate.js.map +1 -0
- package/dist/type/queryRaw.d.ts +35 -0
- package/dist/type/queryRaw.d.ts.map +1 -0
- package/dist/type/queryRaw.js +11 -0
- package/dist/type/queryRaw.js.map +1 -0
- package/dist/type/queryWhere.d.ts +271 -0
- package/dist/type/queryWhere.d.ts.map +1 -0
- package/dist/type/queryWhere.js +2 -0
- package/dist/type/queryWhere.js.map +1 -0
- package/dist/type/universalQuerier.d.ts +2 -1
- package/dist/type/universalQuerier.d.ts.map +1 -1
- package/dist/type/utility.d.ts +8 -1
- package/dist/type/utility.d.ts.map +1 -1
- package/dist/type/vector.d.ts +55 -0
- package/dist/type/vector.d.ts.map +1 -0
- package/dist/type/vector.js +2 -0
- package/dist/type/vector.js.map +1 -0
- package/dist/util/ansiSqlLiteral.d.ts +2 -0
- package/dist/util/ansiSqlLiteral.d.ts.map +1 -1
- package/dist/util/ansiSqlLiteral.js +5 -1
- package/dist/util/ansiSqlLiteral.js.map +1 -1
- package/dist/util/dialect.util.d.ts +5 -5
- package/dist/util/dialect.util.d.ts.map +1 -1
- package/dist/util/dialect.util.js +15 -10
- package/dist/util/dialect.util.js.map +1 -1
- package/dist/util/object.util.d.ts +12 -1
- package/dist/util/object.util.d.ts.map +1 -1
- package/dist/util/object.util.js +18 -0
- package/dist/util/object.util.js.map +1 -1
- package/dist/util/relationQuery.util.js +1 -1
- package/dist/util/relationQuery.util.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { resolveVectorCast } from '../schema/canonicalType.js';
|
|
3
2
|
import { QueryRaw, RAW_ALIAS, RAW_VALUE, } from '../type/index.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/ansiSqlLiteral.js';
|
|
4
|
+
import { applyFilters, asSelectMap, buildQueryWhereAsMap, buildSortMap, escapeSqlId, fillOnFields, filterFieldKeys, flatObject, getFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, hasMultipleKeys, isJsonType, isJsonUpdateOp, isOperatorObject, isOperatorOnlyObject, isPopulatingRelations, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationAtKey, raw, someValue, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
|
+
import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
|
|
6
|
+
import { JSON_ELEM_ALIAS_PREFIX, jsonElemExists } from './jsonSql.js';
|
|
6
7
|
import { SqlQueryContext } from './queryContext.js';
|
|
7
|
-
|
|
8
|
+
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
9
|
+
export class AbstractSqlDialect extends VectorSqlDialect {
|
|
8
10
|
isolationLevelStrategy = 'inline';
|
|
9
11
|
alterColumnStrategy = 'single-statement';
|
|
10
12
|
alterColumnSyntax = 'ALTER COLUMN';
|
|
@@ -17,9 +19,6 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
17
19
|
* `insertMany` splits larger batches into multiple statements based on this limit.
|
|
18
20
|
*/
|
|
19
21
|
maxBindValues = 32766;
|
|
20
|
-
/** Vector index operator classes, keyed by distance metric. Partial: not every dialect supports every metric. */
|
|
21
|
-
vectorOpsClass = undefined;
|
|
22
|
-
vectorExtension = undefined;
|
|
23
22
|
getBeginTransactionStatements(isolationLevel) {
|
|
24
23
|
const level = isolationLevel?.toUpperCase();
|
|
25
24
|
const strategy = this.isolationLevelStrategy;
|
|
@@ -35,6 +34,19 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
35
34
|
createContext() {
|
|
36
35
|
return new SqlQueryContext(this);
|
|
37
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Builds SQL text in isolation via `build`, so the caller can embed it inline (e.g.
|
|
39
|
+
* `"col" = <text>`) instead of appending it at the end of `ctx`. The fragment binds any value
|
|
40
|
+
* straight into `ctx`'s own values array - shared by reference, not copied - so `addValue` numbers
|
|
41
|
+
* its placeholder correctly against the real query from the start; a fresh, empty array would
|
|
42
|
+
* instead number from `1` regardless of how many values `ctx` already has, misnumbering every
|
|
43
|
+
* bound value on `$n`-placeholder dialects once `ctx` isn't otherwise empty.
|
|
44
|
+
*/
|
|
45
|
+
buildFragment(ctx, build) {
|
|
46
|
+
const fragmentCtx = new SqlQueryContext(this, ctx.values);
|
|
47
|
+
build(fragmentCtx);
|
|
48
|
+
return fragmentCtx.sql;
|
|
49
|
+
}
|
|
38
50
|
addValue(values, value) {
|
|
39
51
|
values.push(this.normalizeValue(value));
|
|
40
52
|
return this.placeholder(values.length);
|
|
@@ -195,10 +207,11 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
195
207
|
ctx.append(`${joinAlias}.${this.escapeId(foreignColumnName)} = ${relPath}.${this.escapeId(localColumnName)}`);
|
|
196
208
|
refAppended = true;
|
|
197
209
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
// Unconditional, not gated by `relQuery.$where`: a joined relation's own filters (in
|
|
211
|
+
// particular `security: true` ones) must apply even to a bare `$populate: { rel: true }`
|
|
212
|
+
// with no explicit `$where`. `where()` -> `renderWhere()` no-ops cleanly (appends nothing)
|
|
213
|
+
// when there is truly nothing to add, so this is a pure superset of the old behavior.
|
|
214
|
+
this.where(ctx, relEntity, relQuery.$where ?? {}, { prefix: joinRelAlias, clause: 'AND' });
|
|
202
215
|
this.selectRelationJoins(ctx, relEntity, relQuery.$select, relQuery.$populate, { prefix: joinRelAlias });
|
|
203
216
|
});
|
|
204
217
|
}
|
|
@@ -389,6 +402,17 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
389
402
|
['$like', (v) => v],
|
|
390
403
|
['$ilike', (v) => v.toLowerCase()],
|
|
391
404
|
]);
|
|
405
|
+
/**
|
|
406
|
+
* The case-insensitive `LIKE_OP_MAP` keys - the value is lowercased, so the comparison must use
|
|
407
|
+
* `ilikeExpr` (Postgres's `ILIKE`) rather than `LIKE`. `$includes` is deliberately excluded even
|
|
408
|
+
* though it starts with the substring `$i`: it is case-sensitive, unlike `$iincludes`.
|
|
409
|
+
*/
|
|
410
|
+
static LIKE_CASE_INSENSITIVE_OPS = new Set([
|
|
411
|
+
'$istartsWith',
|
|
412
|
+
'$iendsWith',
|
|
413
|
+
'$iincludes',
|
|
414
|
+
'$ilike',
|
|
415
|
+
]);
|
|
392
416
|
/** Builds `prefix.column` from an already-resolved field. */
|
|
393
417
|
columnWithPrefix(key, field, prefix) {
|
|
394
418
|
const columnName = this.resolveColumnName(key, field);
|
|
@@ -402,11 +426,8 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
402
426
|
resolveOperandField(ctx, entity, key, opts) {
|
|
403
427
|
const col = getMeta(entity).fields[key];
|
|
404
428
|
if (col?.virtual) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return undefined;
|
|
408
|
-
}
|
|
409
|
-
return `(${col.virtual})`;
|
|
429
|
+
this.getComparisonKey(ctx, entity, key, opts);
|
|
430
|
+
return undefined;
|
|
410
431
|
}
|
|
411
432
|
return this.columnWithPrefix(key, col, opts.prefix);
|
|
412
433
|
}
|
|
@@ -467,7 +488,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
467
488
|
}
|
|
468
489
|
}
|
|
469
490
|
appendLikeOp(ctx, field, op, wrappedVal) {
|
|
470
|
-
const isIlike =
|
|
491
|
+
const isIlike = AbstractSqlDialect.LIKE_CASE_INSENSITIVE_OPS.has(op);
|
|
471
492
|
const ph = this.addValue(ctx.values, wrappedVal);
|
|
472
493
|
if (isIlike && field) {
|
|
473
494
|
ctx.append(this.ilikeExpr(field, ph));
|
|
@@ -501,15 +522,17 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
501
522
|
* Used by both `$elemMatch` and dot-notation paths.
|
|
502
523
|
* All dialect-specific behavior comes from overridable methods on `this`.
|
|
503
524
|
*/
|
|
504
|
-
buildJsonFieldCondition(ctx, fieldAccessor, jsonPath, op, value) {
|
|
525
|
+
buildJsonFieldCondition(ctx, fieldAccessor, jsonPath, op, value, asJson = false) {
|
|
505
526
|
const jsonField = fieldAccessor(jsonPath);
|
|
506
527
|
switch (op) {
|
|
507
528
|
case '$eq':
|
|
508
|
-
|
|
529
|
+
if (value === null)
|
|
530
|
+
return `${jsonField} IS NULL`;
|
|
531
|
+
return `${this.jsonComparand(jsonField, value)} = ${this.jsonOperand(ctx, value, asJson)}`;
|
|
509
532
|
case '$ne':
|
|
510
533
|
if (value === null)
|
|
511
534
|
return `${jsonField} IS NOT NULL`;
|
|
512
|
-
return this.neExpr(jsonField, this.
|
|
535
|
+
return this.neExpr(this.jsonComparand(jsonField, value), this.jsonOperand(ctx, value, asJson));
|
|
513
536
|
case '$gt':
|
|
514
537
|
return `${this.numericCast(jsonField)} > ${this.addValue(ctx.values, value)}`;
|
|
515
538
|
case '$gte':
|
|
@@ -538,7 +561,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
538
561
|
return `${jsonField} ${this.regexpOp} ${this.addValue(ctx.values, value)}`;
|
|
539
562
|
case '$in':
|
|
540
563
|
case '$nin':
|
|
541
|
-
return this.jsonInNin(ctx, jsonField, op, value);
|
|
564
|
+
return this.jsonInNin(ctx, jsonField, op, value, asJson);
|
|
542
565
|
case '$all':
|
|
543
566
|
return this.jsonAll(ctx, jsonField, value);
|
|
544
567
|
case '$size':
|
|
@@ -549,20 +572,122 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
549
572
|
throw TypeError(`unknown operator: ${op}`);
|
|
550
573
|
}
|
|
551
574
|
}
|
|
552
|
-
jsonInNin(ctx, jsonField, op, value) {
|
|
553
|
-
|
|
575
|
+
jsonInNin(ctx, jsonField, op, value, asJson) {
|
|
576
|
+
const values = Array.isArray(value) ? value : [];
|
|
577
|
+
const negate = op === '$nin';
|
|
578
|
+
if (!asJson) {
|
|
579
|
+
return `${this.jsonComparand(jsonField, values)}${this.formatIn(ctx, values, negate)}`;
|
|
580
|
+
}
|
|
581
|
+
// JSON values have no portable array literal, so the set expands into explicit comparisons.
|
|
582
|
+
const comparisons = values.map((val) => `${jsonField} ${negate ? '<>' : '='} ${this.jsonScalarParam(ctx, val)}`);
|
|
583
|
+
return `(${comparisons.join(negate ? ' AND ' : ' OR ')})`;
|
|
584
|
+
}
|
|
585
|
+
/** The bound operand of a JSON comparison: JSON-encoded when comparing against the JSON value. */
|
|
586
|
+
jsonOperand(ctx, value, asJson) {
|
|
587
|
+
return asJson ? this.jsonScalarParam(ctx, value) : this.addValue(ctx.values, value);
|
|
554
588
|
}
|
|
555
|
-
|
|
556
|
-
|
|
589
|
+
/**
|
|
590
|
+
* The left side of a comparison against a JSON scalar, cast when the operand is numeric - see
|
|
591
|
+
* {@link jsonCompareMode} for why each mode exists.
|
|
592
|
+
*/
|
|
593
|
+
jsonComparand(jsonField, value) {
|
|
594
|
+
return this.jsonCompareMode(value) === 'numeric' ? this.numericCast(jsonField) : jsonField;
|
|
557
595
|
}
|
|
558
|
-
|
|
559
|
-
|
|
596
|
+
/**
|
|
597
|
+
* How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
|
|
598
|
+
* of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
|
|
599
|
+
* compared in the representation every engine agrees on:
|
|
600
|
+
* - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
|
|
601
|
+
* would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
|
|
602
|
+
* - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
|
|
603
|
+
* portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
|
|
604
|
+
* - `text` - compare as extracted, which is also what the string operators need.
|
|
605
|
+
*
|
|
606
|
+
* Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
|
|
607
|
+
*/
|
|
608
|
+
jsonCompareMode(value) {
|
|
609
|
+
const operands = Array.isArray(value) ? value : [value];
|
|
610
|
+
if (operands.length === 0) {
|
|
611
|
+
return 'text';
|
|
612
|
+
}
|
|
613
|
+
if (operands.every((operand) => typeof operand === 'boolean')) {
|
|
614
|
+
return 'json';
|
|
615
|
+
}
|
|
616
|
+
return operands.every((operand) => typeof operand === 'number') ? 'numeric' : 'text';
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Whether the dialect's array containment ({@link jsonAll}) matches an object element that merely
|
|
620
|
+
* *includes* the given keys, as PostgreSQL's `@>` and MySQL's `JSON_CONTAINS` do. SQLite compares
|
|
621
|
+
* elements as whole JSON text, so it cannot express a partial match and always expands the
|
|
622
|
+
* per-field form below.
|
|
623
|
+
*/
|
|
624
|
+
jsonContainmentIsPartial = true;
|
|
625
|
+
/**
|
|
626
|
+
* Whether an exploded *scalar* element keeps its SQL type. SQLite's `json_each` yields JSON
|
|
627
|
+
* booleans as `0`/`1` integers and numbers as numbers, so such an element compares directly to a
|
|
628
|
+
* bound value; PostgreSQL and MySQL explode scalars to text, losing the type, so a non-string
|
|
629
|
+
* operand there has to compare as JSON (see {@link isJsonbOp}).
|
|
630
|
+
*/
|
|
631
|
+
jsonScalarElemKeepsType = false;
|
|
632
|
+
/**
|
|
633
|
+
* `$elemMatch`: at least one element of the JSON array satisfies `match`. Three shapes, decided
|
|
634
|
+
* here so every dialect only supplies {@link jsonElemFrom} / {@link jsonElemRef}:
|
|
635
|
+
* - keys are operators (`{ $startsWith: 'ad' }`) - scalar elements, conditions on the element;
|
|
636
|
+
* - a plain object with no nested operators - containment, which is the only form an index serves;
|
|
637
|
+
* - otherwise - per-field conditions over the exploded objects.
|
|
638
|
+
*/
|
|
639
|
+
jsonElemMatch(ctx, jsonField, match) {
|
|
640
|
+
// Conditions on the element itself. One `FROM` serves them all, so the element is read as JSON
|
|
641
|
+
// only when *every* operand needs it - the same all-operands rule the comparison classifier uses.
|
|
642
|
+
if (isOperatorOnlyObject(match)) {
|
|
643
|
+
const entries = Object.entries(match);
|
|
644
|
+
const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) => this.isJsonbOp(op, val));
|
|
645
|
+
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
646
|
+
const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, () => this.jsonElemRef(alias, undefined, asJson), '', op, val, asJson));
|
|
647
|
+
return jsonElemExists(this.jsonElemFrom(jsonField, [], alias, asJson), conditions);
|
|
648
|
+
}
|
|
649
|
+
if (isOperatorObject(match)) {
|
|
650
|
+
throw TypeError(`$elemMatch cannot mix operators with field names: ${Object.keys(match).join(', ')}`);
|
|
651
|
+
}
|
|
652
|
+
// A plain object with no nested operators is containment, which is also the only form an index
|
|
653
|
+
// can serve. SQLite compares elements exactly, so it always expands the per-field form below.
|
|
654
|
+
if (this.jsonContainmentIsPartial && !someValue(match, isOperatorObject)) {
|
|
655
|
+
return this.jsonAll(ctx, jsonField, [match]);
|
|
656
|
+
}
|
|
657
|
+
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
658
|
+
const conditions = buildElemMatchConditions(match, (field, op, opVal) => {
|
|
659
|
+
const asJson = this.isJsonbOp(op, opVal);
|
|
660
|
+
return this.buildJsonFieldCondition(ctx, (f) => this.jsonElemRef(alias, f, asJson), field, op, opVal, asJson);
|
|
661
|
+
});
|
|
662
|
+
return jsonElemExists(this.jsonElemFrom(jsonField, Object.keys(match), alias), conditions);
|
|
560
663
|
}
|
|
561
|
-
|
|
562
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
666
|
+
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
667
|
+
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
668
|
+
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
|
|
669
|
+
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
670
|
+
*
|
|
671
|
+
* Numbers stay on the text accessor with a numeric cast ({@link jsonComparand}), which keeps
|
|
672
|
+
* `1` equal to `1.0` - JSON equality would not.
|
|
673
|
+
*/
|
|
674
|
+
isJsonbOp(op, value) {
|
|
675
|
+
if (op === '$all' || op === '$size' || op === '$elemMatch') {
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
const comparesValue = op === '$eq' || op === '$ne' || op === '$in' || op === '$nin';
|
|
679
|
+
return comparesValue && this.jsonCompareMode(value) === 'json';
|
|
563
680
|
}
|
|
564
|
-
|
|
565
|
-
|
|
681
|
+
/**
|
|
682
|
+
* A JSON-encoded bound parameter, cast to the dialect's JSON type. Only the positional-placeholder
|
|
683
|
+
* dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
684
|
+
*/
|
|
685
|
+
jsonScalarParam(ctx, value) {
|
|
686
|
+
if (value instanceof QueryRaw) {
|
|
687
|
+
return this.addValue(ctx.values, value);
|
|
688
|
+
}
|
|
689
|
+
ctx.pushValue(JSON.stringify(value));
|
|
690
|
+
return this.jsonCast('?');
|
|
566
691
|
}
|
|
567
692
|
getComparisonKey(ctx, entity, key, { prefix } = {}) {
|
|
568
693
|
const meta = getMeta(entity);
|
|
@@ -629,59 +754,6 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
629
754
|
ctx.append(this.escapeId(name) + direction);
|
|
630
755
|
});
|
|
631
756
|
}
|
|
632
|
-
/**
|
|
633
|
-
* Resolve common parameters for a vector similarity ORDER BY expression.
|
|
634
|
-
* Shared by all dialect overrides of `appendVectorSort`.
|
|
635
|
-
*/
|
|
636
|
-
resolveVectorSortParams(meta, key, search) {
|
|
637
|
-
const field = meta.fields[key];
|
|
638
|
-
const colName = this.resolveColumnName(key, field);
|
|
639
|
-
const distance = search.$distance ?? field?.distance ?? 'cosine';
|
|
640
|
-
const vectorCast = resolveVectorCast(field);
|
|
641
|
-
return { colName, distance, field, vectorCast };
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Mapping of UQL vector distance metrics to native SQL functions.
|
|
645
|
-
* Override in dialects that use function-call syntax (e.g. SQLite, MariaDB).
|
|
646
|
-
* Dialects with operator-based syntax (e.g. Postgres) leave this empty and override `appendVectorSort` directly.
|
|
647
|
-
*/
|
|
648
|
-
vectorDistanceFns = new Map();
|
|
649
|
-
/**
|
|
650
|
-
* Append a vector similarity function call: `fn(col, ?)`.
|
|
651
|
-
* Used by dialects that express vector distance via SQL functions (SQLite, MariaDB).
|
|
652
|
-
*/
|
|
653
|
-
appendFunctionVectorSort(ctx, meta, key, search, dialectName) {
|
|
654
|
-
const { colName, distance, vectorCast } = this.resolveVectorSortParams(meta, key, search);
|
|
655
|
-
const fn = this.vectorDistanceFns.get(distance);
|
|
656
|
-
if (!fn) {
|
|
657
|
-
throw Error(`${dialectName} does not support vector distance metric: ${distance}`);
|
|
658
|
-
}
|
|
659
|
-
ctx.append(`${fn}(${this.escapeId(colName)}, `);
|
|
660
|
-
ctx.addValue(`[${search.$vector.join(',')}]`);
|
|
661
|
-
if (vectorCast && dialectName === 'PostgreSQL') {
|
|
662
|
-
ctx.append(`::${vectorCast}`);
|
|
663
|
-
}
|
|
664
|
-
ctx.append(')');
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* Append a vector distance projection.
|
|
668
|
-
*/
|
|
669
|
-
appendVectorProjection(ctx, meta, key, search) {
|
|
670
|
-
this.appendVectorSort(ctx, meta, key, search);
|
|
671
|
-
ctx.append(` AS ${this.escapeId(search.$project)}`);
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Append a vector similarity ORDER BY expression.
|
|
675
|
-
* Default: auto-delegates to `appendFunctionVectorSort` when `vectorDistanceFns` has entries.
|
|
676
|
-
* Override for operator-based syntax (e.g. PostgreSQL `<=>`, `<->` operators).
|
|
677
|
-
*/
|
|
678
|
-
appendVectorSort(ctx, meta, key, search) {
|
|
679
|
-
if (this.vectorDistanceFns.size > 0) {
|
|
680
|
-
this.appendFunctionVectorSort(ctx, meta, key, search, this.dialectName);
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
throw new TypeError('Vector similarity sort is not supported by this dialect. Use raw() for vector queries.');
|
|
684
|
-
}
|
|
685
757
|
pager(ctx, opts) {
|
|
686
758
|
if (opts.$limit) {
|
|
687
759
|
ctx.append(` LIMIT ${Number(opts.$limit)}`);
|
|
@@ -891,8 +963,8 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
891
963
|
const columnName = this.resolveColumnName(key, field);
|
|
892
964
|
const escapedCol = this.escapeId(columnName);
|
|
893
965
|
const value = filledPayload[key];
|
|
894
|
-
if (
|
|
895
|
-
this.formatJsonUpdate(ctx, escapedCol, value);
|
|
966
|
+
if (isJsonUpdateOp(value)) {
|
|
967
|
+
this.formatJsonUpdate(ctx, escapedCol, value, field);
|
|
896
968
|
}
|
|
897
969
|
else {
|
|
898
970
|
ctx.append(`${escapedCol} = `);
|
|
@@ -930,28 +1002,11 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
930
1002
|
if (callback && Object.hasOwn(sample, col)) {
|
|
931
1003
|
return `${this.escapeId(columnName)} = ${callback(this.escapeId(columnName))}`;
|
|
932
1004
|
}
|
|
933
|
-
const
|
|
934
|
-
this.
|
|
935
|
-
ctx.pushValue(...valCtx.values);
|
|
936
|
-
return `${this.escapeId(columnName)} = ${valCtx.sql}`;
|
|
1005
|
+
const text = this.buildFragment(ctx, (fragmentCtx) => this.formatPersistableValue(fragmentCtx, field, filledPayload[col]));
|
|
1006
|
+
return `${this.escapeId(columnName)} = ${text}`;
|
|
937
1007
|
})
|
|
938
1008
|
.join(', ');
|
|
939
1009
|
}
|
|
940
|
-
/**
|
|
941
|
-
* Shared ON CONFLICT ... DO UPDATE / DO NOTHING ... RETURNING logic for positional-placeholder
|
|
942
|
-
* dialects (SQLite). Uses a deferred context for update params so they follow INSERT params.
|
|
943
|
-
* PG uses its own implementation since `$N` numbered placeholders handle param ordering natively.
|
|
944
|
-
*/
|
|
945
|
-
onConflictUpsert(ctx, entity, conflictPaths, payload, insertFn) {
|
|
946
|
-
const meta = getMeta(entity);
|
|
947
|
-
const updateCtx = this.createContext();
|
|
948
|
-
const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, (name) => `EXCLUDED.${name}`);
|
|
949
|
-
const keysStr = this.getUpsertConflictPathsStr(meta, conflictPaths);
|
|
950
|
-
const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
|
|
951
|
-
insertFn(ctx, entity, payload);
|
|
952
|
-
ctx.append(` ON CONFLICT (${keysStr}) ${onConflict} ${this.returningId(entity)}`);
|
|
953
|
-
ctx.pushValue(...updateCtx.values);
|
|
954
|
-
}
|
|
955
1010
|
getUpsertConflictPathsStr(meta, conflictPaths) {
|
|
956
1011
|
return getKeys(conflictPaths)
|
|
957
1012
|
.map((key) => {
|
|
@@ -984,22 +1039,6 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
984
1039
|
escapeId(val, forbidQualified, addDot) {
|
|
985
1040
|
return escapeSqlId(val, this.escapeIdChar, forbidQualified, addDot);
|
|
986
1041
|
}
|
|
987
|
-
getPersistables(ctx, meta, payload, callbackKey) {
|
|
988
|
-
const payloads = fillOnFields(meta, payload, callbackKey);
|
|
989
|
-
return payloads.map((it) => this.getPersistable(ctx, meta, it, callbackKey));
|
|
990
|
-
}
|
|
991
|
-
getPersistable(ctx, meta, payload, callbackKey) {
|
|
992
|
-
const filledPayload = fillOnFields(meta, payload, callbackKey)[0];
|
|
993
|
-
const keys = filterFieldKeys(meta, filledPayload, callbackKey);
|
|
994
|
-
return keys.reduce((acc, key) => {
|
|
995
|
-
const field = meta.fields[key];
|
|
996
|
-
const valCtx = this.createContext();
|
|
997
|
-
this.formatPersistableValue(valCtx, field, filledPayload[key]);
|
|
998
|
-
ctx.pushValue(...valCtx.values);
|
|
999
|
-
acc[key] = valCtx.sql;
|
|
1000
|
-
return acc;
|
|
1001
|
-
}, {});
|
|
1002
|
-
}
|
|
1003
1042
|
formatPersistableValue(ctx, field, value) {
|
|
1004
1043
|
if (value instanceof QueryRaw) {
|
|
1005
1044
|
this.getRawValue(ctx, { value });
|
|
@@ -1016,50 +1055,52 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1016
1055
|
ctx.addValue(value);
|
|
1017
1056
|
}
|
|
1018
1057
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
* Base implementation uses MySQL-compatible syntax with *shallow* merge semantics
|
|
1024
|
-
* (RHS top-level keys replace LHS top-level keys, matching PostgreSQL's `jsonb || jsonb`).
|
|
1025
|
-
* Override in dialect subclasses when a dialect needs different JSON function semantics.
|
|
1058
|
+
* Reads `operand` as a JSON value. Passing the `?` placeholder yields the cast for a bound
|
|
1059
|
+
* parameter, and passing an expression re-reads text as JSON - the same SQL either way, which is
|
|
1060
|
+
* why this is one hook rather than a placeholder variant plus an expression variant.
|
|
1026
1061
|
*/
|
|
1027
|
-
|
|
1028
|
-
return
|
|
1062
|
+
jsonCast(operand) {
|
|
1063
|
+
return `CAST(${operand} AS JSON)`;
|
|
1029
1064
|
}
|
|
1030
|
-
|
|
1065
|
+
/**
|
|
1066
|
+
* Generate the full `"col" = <expression>` assignment for a JSON update operator payload.
|
|
1067
|
+
* Called from `update()` when a field value is a {@link JsonUpdateOp}.
|
|
1068
|
+
*
|
|
1069
|
+
* Each operator wraps the expression built so far, innermost-first in the order stated on
|
|
1070
|
+
* {@link JsonUpdateOp} (`$pull` -> `$set` -> `$push` -> `$unset`), so dialects only supply the
|
|
1071
|
+
* four SQL fragments below. Two invariants keep every dialect consistent and keep bound values in
|
|
1072
|
+
* step with their placeholders:
|
|
1073
|
+
* - `$pull` is innermost and its subquery reads `escapedCol`, so its value binds exactly once.
|
|
1074
|
+
* - Later fragments reference `expr` at most once, so a `$pull` subquery is never duplicated
|
|
1075
|
+
* (which would bind its value twice on positional-placeholder dialects). PostgreSQL's `$push`
|
|
1076
|
+
* is the one exception, and is safe there because its placeholders are numbered.
|
|
1077
|
+
*/
|
|
1078
|
+
formatJsonUpdate(ctx, escapedCol, value, field) {
|
|
1079
|
+
// Centralizes the one narrowing cast: the payload's keys are typed against the entity's JSON
|
|
1080
|
+
// payload, which the dialects do not need - they only build SQL from keys and values.
|
|
1081
|
+
const { $pull, $set, $push, $unset } = value;
|
|
1031
1082
|
let expr = escapedCol;
|
|
1032
|
-
if (hasKeys(
|
|
1033
|
-
|
|
1034
|
-
expr = `JSON_SET(COALESCE(${escapedCol}, '{}')`;
|
|
1035
|
-
for (const [key, v] of Object.entries(merge)) {
|
|
1036
|
-
expr += `, '$.${this.escapeJsonKey(key)}', ${this.getJsonCastExpr()}`;
|
|
1037
|
-
ctx.pushValue(JSON.stringify(v));
|
|
1038
|
-
}
|
|
1039
|
-
expr += ')';
|
|
1040
|
-
}
|
|
1041
|
-
if (hasKeys(value.$push)) {
|
|
1042
|
-
const push = value.$push;
|
|
1043
|
-
expr = `JSON_ARRAY_APPEND(${expr}`;
|
|
1044
|
-
for (const [key, v] of Object.entries(push)) {
|
|
1045
|
-
expr += `, '$.${this.escapeJsonKey(key)}', ${this.getJsonCastExpr()}`;
|
|
1046
|
-
ctx.pushValue(JSON.stringify(v));
|
|
1047
|
-
}
|
|
1048
|
-
expr += ')';
|
|
1083
|
+
if (hasKeys($pull)) {
|
|
1084
|
+
expr = this.jsonPull(ctx, expr, escapedCol, $pull);
|
|
1049
1085
|
}
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1086
|
+
if (hasKeys($set)) {
|
|
1087
|
+
expr = this.jsonSet(ctx, expr, $set, field);
|
|
1088
|
+
}
|
|
1089
|
+
if (hasKeys($push)) {
|
|
1090
|
+
expr = this.jsonPush(ctx, expr, $push);
|
|
1091
|
+
}
|
|
1092
|
+
if ($unset?.length) {
|
|
1093
|
+
expr = this.jsonUnset(ctx, expr, $unset);
|
|
1054
1094
|
}
|
|
1055
1095
|
ctx.append(`${escapedCol} = ${expr}`);
|
|
1056
1096
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1097
|
+
/**
|
|
1098
|
+
* Remove every element equal to the given value, per array key. Each key wraps the expression
|
|
1099
|
+
* built so far, so dialects only supply {@link jsonPullKey} - and because every key reads
|
|
1100
|
+
* `escapedCol` rather than the accumulated expression, values bind once, in key order.
|
|
1101
|
+
*/
|
|
1102
|
+
jsonPull(ctx, expr, escapedCol, pull) {
|
|
1103
|
+
return Object.entries(pull).reduce((acc, [key, value]) => this.jsonPullKey(ctx, acc, escapedCol, key, value), expr);
|
|
1063
1104
|
}
|
|
1064
1105
|
getRawValue(ctx, opts) {
|
|
1065
1106
|
const { value, prefix = '', escapedPrefix, autoPrefixAlias } = opts;
|
|
@@ -1105,7 +1146,6 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1105
1146
|
const colName = this.resolveColumnName(root, field);
|
|
1106
1147
|
const escapedCol = (prefix ? this.escapeId(prefix, true, true) : '') + this.escapeId(colName);
|
|
1107
1148
|
return {
|
|
1108
|
-
root,
|
|
1109
1149
|
jsonPath,
|
|
1110
1150
|
accessor: (asJsonb) => asJsonb ? this.getJsonPathJsonbExpr(escapedCol, jsonPath) : this.getJsonPathScalarExpr(escapedCol, jsonPath),
|
|
1111
1151
|
};
|
|
@@ -1124,7 +1164,8 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1124
1164
|
operators.forEach((op, index) => {
|
|
1125
1165
|
if (index > 0)
|
|
1126
1166
|
ctx.append(' AND ');
|
|
1127
|
-
const
|
|
1167
|
+
const asJson = this.isJsonbOp(op, value[op]);
|
|
1168
|
+
const sql = this.buildJsonFieldCondition(ctx, () => accessor(asJson), jsonPath, op, value[op], asJson);
|
|
1128
1169
|
if (sql) {
|
|
1129
1170
|
ctx.append(sql);
|
|
1130
1171
|
}
|
|
@@ -1143,7 +1184,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1143
1184
|
let expr = escapedColumn;
|
|
1144
1185
|
for (let i = 0; i < segments.length; i++) {
|
|
1145
1186
|
const op = i === segments.length - 1 ? '->>' : '->';
|
|
1146
|
-
expr = `(${expr}${op}'${
|
|
1187
|
+
expr = `(${expr}${op}'${escapeSingleQuotes(segments[i])}')`;
|
|
1147
1188
|
}
|
|
1148
1189
|
return expr;
|
|
1149
1190
|
}
|
|
@@ -1151,7 +1192,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1151
1192
|
const segments = jsonPath.split('.');
|
|
1152
1193
|
let expr = escapedColumn;
|
|
1153
1194
|
for (const segment of segments) {
|
|
1154
|
-
expr = `(${expr}->'${
|
|
1195
|
+
expr = `(${expr}->'${escapeSingleQuotes(segment)}')`;
|
|
1155
1196
|
}
|
|
1156
1197
|
return expr;
|
|
1157
1198
|
}
|
|
@@ -1170,15 +1211,25 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1170
1211
|
* Filter by relation using an EXISTS subquery.
|
|
1171
1212
|
* Supports all cardinalities: mm (via junction), 1m, m1, and 11.
|
|
1172
1213
|
*/
|
|
1173
|
-
|
|
1214
|
+
/**
|
|
1215
|
+
* The parent side of a relation-filtering subquery: its table name, escaped id reference, and a
|
|
1216
|
+
* guard against a relation with no configured references. Shared by
|
|
1217
|
+
* `compareRelation`/`compareRelationSize`. `rel.references` is asserted non-null at call sites
|
|
1218
|
+
* relying on this guard, since the check lives here instead of in the caller's own scope.
|
|
1219
|
+
*/
|
|
1220
|
+
resolveRelationParent(entity, key, rel, opts) {
|
|
1174
1221
|
const meta = getMeta(entity);
|
|
1175
1222
|
const parentTable = this.resolveTableName(entity, meta);
|
|
1176
|
-
const parentId = meta.id;
|
|
1177
|
-
const escapedParentId = (opts.prefix ? this.escapeId(opts.prefix, true, true) : this.escapeId(parentTable, false, true)) +
|
|
1178
|
-
this.escapeId(parentId);
|
|
1179
1223
|
if (!rel.references?.length) {
|
|
1180
1224
|
throw new TypeError(`Relation '${key}' on '${parentTable}' has no references defined`);
|
|
1181
1225
|
}
|
|
1226
|
+
const escapedParentId = (opts.prefix ? this.escapeId(opts.prefix, true, true) : this.escapeId(parentTable, false, true)) +
|
|
1227
|
+
this.escapeId(meta.id);
|
|
1228
|
+
return { parentTable, escapedParentId };
|
|
1229
|
+
}
|
|
1230
|
+
compareRelation(ctx, entity, key, val, rel, opts) {
|
|
1231
|
+
const { parentTable, escapedParentId } = this.resolveRelationParent(entity, key, rel, opts);
|
|
1232
|
+
const references = rel.references;
|
|
1182
1233
|
const relatedEntity = rel.entity();
|
|
1183
1234
|
const relatedMeta = getMeta(relatedEntity);
|
|
1184
1235
|
const relatedTable = this.resolveTableName(relatedEntity, relatedMeta);
|
|
@@ -1188,8 +1239,8 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1188
1239
|
const throughEntity = rel.through();
|
|
1189
1240
|
const throughMeta = getMeta(throughEntity);
|
|
1190
1241
|
const throughTable = this.resolveTableName(throughEntity, throughMeta);
|
|
1191
|
-
const localFk =
|
|
1192
|
-
const foreignFk =
|
|
1242
|
+
const localFk = references[0].local;
|
|
1243
|
+
const foreignFk = references[1].local;
|
|
1193
1244
|
const relatedId = relatedMeta.id;
|
|
1194
1245
|
ctx.append(this.escapeId(throughTable));
|
|
1195
1246
|
ctx.append(` WHERE ${this.escapeId(throughTable, false, true)}${this.escapeId(localFk)} = ${escapedParentId}`);
|
|
@@ -1206,11 +1257,11 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1206
1257
|
// 1m / m1 / 11: EXISTS (SELECT 1 FROM Related WHERE related.fk_or_pk = parent.pk_or_fk AND ...)
|
|
1207
1258
|
// Left side is always relatedTable.references[0].foreign
|
|
1208
1259
|
// Right side is the parent's PK (1m) or the parent's FK (m1/11)
|
|
1209
|
-
const joinLeft = `${this.escapeId(relatedTable, false, true)}${this.escapeId(
|
|
1260
|
+
const joinLeft = `${this.escapeId(relatedTable, false, true)}${this.escapeId(references[0].foreign)}`;
|
|
1210
1261
|
const joinRight = rel.cardinality === '1m'
|
|
1211
1262
|
? escapedParentId
|
|
1212
1263
|
: (opts.prefix ? this.escapeId(opts.prefix, true, true) : this.escapeId(parentTable, false, true)) +
|
|
1213
|
-
this.escapeId(
|
|
1264
|
+
this.escapeId(references[0].local);
|
|
1214
1265
|
ctx.append(this.escapeId(relatedTable));
|
|
1215
1266
|
ctx.append(` WHERE ${joinLeft} = ${joinRight}`);
|
|
1216
1267
|
this.where(ctx, relatedEntity, val, {
|
|
@@ -1226,14 +1277,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1226
1277
|
* Supports all cardinalities: mm (via junction), 1m.
|
|
1227
1278
|
*/
|
|
1228
1279
|
compareRelationSize(ctx, entity, key, sizeVal, rel, opts) {
|
|
1229
|
-
const
|
|
1230
|
-
const parentTable = this.resolveTableName(entity, meta);
|
|
1231
|
-
const parentId = meta.id;
|
|
1232
|
-
const escapedParentId = (opts.prefix ? this.escapeId(opts.prefix, true, true) : this.escapeId(parentTable, false, true)) +
|
|
1233
|
-
this.escapeId(parentId);
|
|
1234
|
-
if (!rel.references?.length) {
|
|
1235
|
-
throw new TypeError(`Relation '${key}' on '${parentTable}' has no references defined`);
|
|
1236
|
-
}
|
|
1280
|
+
const { escapedParentId } = this.resolveRelationParent(entity, key, rel, opts);
|
|
1237
1281
|
const appendSubquery = () => {
|
|
1238
1282
|
ctx.append('(SELECT COUNT(*) FROM ');
|
|
1239
1283
|
if (rel.cardinality === 'mm' && rel.through) {
|
|
@@ -1324,6 +1368,10 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1324
1368
|
throw TypeError(`unsupported $size comparison operator: ${op}`);
|
|
1325
1369
|
}
|
|
1326
1370
|
}
|
|
1371
|
+
/** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
|
|
1372
|
+
escape(value) {
|
|
1373
|
+
return escapeAnsiSqlLiteral(value);
|
|
1374
|
+
}
|
|
1327
1375
|
get regexpOp() {
|
|
1328
1376
|
return 'REGEXP';
|
|
1329
1377
|
}
|