uql-orm 0.18.0 → 0.20.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/CHANGELOG.md +77 -0
- package/README.md +3 -3
- package/dist/browser/querier/httpQuerier.d.ts +10 -10
- package/dist/browser/querier/httpQuerier.d.ts.map +1 -1
- package/dist/browser/querier/httpQuerier.js.map +1 -1
- package/dist/browser/type/clientQuerier.d.ts +9 -9
- package/dist/browser/type/clientQuerier.d.ts.map +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/bunSql/bunSqlCockroachDialect.d.ts +1 -1
- package/dist/bunSql/bunSqlCockroachDialect.js +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +113 -51
- package/dist/dialect/abstractSqlDialect.d.ts.map +1 -1
- package/dist/dialect/abstractSqlDialect.js +186 -123
- 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 +24 -0
- package/dist/dialect/jsonSql.d.ts.map +1 -0
- package/dist/dialect/jsonSql.js +37 -0
- package/dist/dialect/jsonSql.js.map +1 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +35 -3
- package/dist/dialect/mysqlLikeSqlDialect.d.ts.map +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +64 -18
- package/dist/dialect/mysqlLikeSqlDialect.js.map +1 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +24 -5
- package/dist/dialect/pgLikeSqlDialect.d.ts.map +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +50 -37
- package/dist/dialect/pgLikeSqlDialect.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 +73 -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/maria/mariaDialect.d.ts +15 -3
- package/dist/maria/mariaDialect.d.ts.map +1 -1
- package/dist/maria/mariaDialect.js +25 -12
- 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/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +2 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts.map +1 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +5 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js.map +1 -1
- package/dist/migrate/introspection/baseSqlIntrospector.js +1 -1
- package/dist/migrate/introspection/baseSqlIntrospector.js.map +1 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -3
- package/dist/migrate/introspection/mysqlIntrospector.d.ts.map +1 -1
- package/dist/migrate/introspection/mysqlIntrospector.js +0 -5
- package/dist/migrate/introspection/mysqlIntrospector.js.map +1 -1
- package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -3
- package/dist/migrate/introspection/postgresIntrospector.d.ts.map +1 -1
- package/dist/migrate/introspection/postgresIntrospector.js +0 -5
- package/dist/migrate/introspection/postgresIntrospector.js.map +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -3
- package/dist/migrate/introspection/sqliteIntrospector.d.ts.map +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.js +0 -5
- package/dist/migrate/introspection/sqliteIntrospector.js.map +1 -1
- package/dist/migrate/schemaGenerator.js +1 -1
- package/dist/migrate/schemaGenerator.js.map +1 -1
- package/dist/mongo/mongoDialect.d.ts +29 -8
- package/dist/mongo/mongoDialect.d.ts.map +1 -1
- package/dist/mongo/mongoDialect.js +101 -12
- package/dist/mongo/mongoDialect.js.map +1 -1
- package/dist/mongo/mongodbQuerier.d.ts +2 -2
- 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/postgres/postgresWireDriverCapabilities.d.ts +1 -1
- package/dist/postgres/postgresWireDriverCapabilities.js +1 -1
- package/dist/querier/abstractQuerier.d.ts +2 -2
- package/dist/querier/abstractQuerier.d.ts.map +1 -1
- package/dist/querier/abstractQuerier.js +6 -5
- package/dist/querier/abstractQuerier.js.map +1 -1
- package/dist/querier/abstractQuerierPool.d.ts +1 -1
- package/dist/querier/abstractQuerierPool.d.ts.map +1 -1
- package/dist/querier/abstractQuerierPool.js.map +1 -1
- package/dist/querier/abstractSqlQuerier.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts.map +1 -1
- package/dist/querier/abstractSqlQuerier.js.map +1 -1
- package/dist/querier/decorator/injectQuerier.d.ts +2 -2
- package/dist/querier/decorator/injectQuerier.d.ts.map +1 -1
- package/dist/querier/decorator/injectQuerier.js.map +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 +29 -8
- package/dist/sqlite/sqliteDialect.d.ts.map +1 -1
- package/dist/sqlite/sqliteDialect.js +59 -74
- package/dist/sqlite/sqliteDialect.js.map +1 -1
- package/dist/type/dialect.d.ts +192 -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 +97 -45
- 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 +4 -3
- package/dist/type/querier.d.ts.map +1 -1
- package/dist/type/querier.js +1 -1
- package/dist/type/querier.js.map +1 -1
- package/dist/type/query.d.ts +19 -668
- 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 +3 -2
- package/dist/type/universalQuerier.d.ts.map +1 -1
- package/dist/type/utility.d.ts +16 -2
- 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 +14 -2
- package/dist/util/dialect.util.d.ts.map +1 -1
- package/dist/util/dialect.util.js +23 -0
- 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 { 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 { 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,12 +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
|
-
get escapeIdChar() {
|
|
24
|
-
return this.quoteChar;
|
|
25
|
-
}
|
|
26
22
|
getBeginTransactionStatements(isolationLevel) {
|
|
27
23
|
const level = isolationLevel?.toUpperCase();
|
|
28
24
|
const strategy = this.isolationLevelStrategy;
|
|
@@ -78,7 +74,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
78
74
|
search(ctx, entity, q = {}, opts = {}) {
|
|
79
75
|
const meta = getMeta(entity);
|
|
80
76
|
const tableName = this.resolveTableName(entity, meta);
|
|
81
|
-
const prefix = this.resolveRelationAwarePrefix(tableName, meta, opts, q.$select, q.$populate);
|
|
77
|
+
const prefix = this.resolveRelationAwarePrefix(tableName, meta, opts, asSelectMap(q.$select), q.$populate);
|
|
82
78
|
if (opts.prefix !== prefix) {
|
|
83
79
|
opts = { ...opts, prefix };
|
|
84
80
|
}
|
|
@@ -93,11 +89,11 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
93
89
|
let selectArr;
|
|
94
90
|
if (select) {
|
|
95
91
|
if (Array.isArray(select)) {
|
|
96
|
-
//
|
|
92
|
+
// Raw SQL projections passed as QueryRaw[]
|
|
97
93
|
selectArr = select;
|
|
98
94
|
}
|
|
99
95
|
else {
|
|
100
|
-
selectArr = normalizeScalarFieldSelection(meta, select, exclude);
|
|
96
|
+
selectArr = normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
|
|
101
97
|
}
|
|
102
98
|
const id = meta.id;
|
|
103
99
|
if (id && opts.prefix && !selectArr.includes(id)) {
|
|
@@ -148,7 +144,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
148
144
|
select(ctx, entity, select, exclude, populate, opts = {}, distinct, sort) {
|
|
149
145
|
const meta = getMeta(entity);
|
|
150
146
|
const tableName = this.resolveTableName(entity, meta);
|
|
151
|
-
const mapSelect =
|
|
147
|
+
const mapSelect = asSelectMap(select);
|
|
152
148
|
const prefix = this.resolveRelationAwarePrefix(tableName, meta, opts, mapSelect, populate);
|
|
153
149
|
ctx.append(distinct ? 'SELECT DISTINCT ' : 'SELECT ');
|
|
154
150
|
this.selectFields(ctx, entity, select, { prefix }, exclude);
|
|
@@ -392,6 +388,17 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
392
388
|
['$like', (v) => v],
|
|
393
389
|
['$ilike', (v) => v.toLowerCase()],
|
|
394
390
|
]);
|
|
391
|
+
/**
|
|
392
|
+
* The case-insensitive `LIKE_OP_MAP` keys - the value is lowercased, so the comparison must use
|
|
393
|
+
* `ilikeExpr` (Postgres's `ILIKE`) rather than `LIKE`. `$includes` is deliberately excluded even
|
|
394
|
+
* though it starts with the substring `$i`: it is case-sensitive, unlike `$iincludes`.
|
|
395
|
+
*/
|
|
396
|
+
static LIKE_CASE_INSENSITIVE_OPS = new Set([
|
|
397
|
+
'$istartsWith',
|
|
398
|
+
'$iendsWith',
|
|
399
|
+
'$iincludes',
|
|
400
|
+
'$ilike',
|
|
401
|
+
]);
|
|
395
402
|
/** Builds `prefix.column` from an already-resolved field. */
|
|
396
403
|
columnWithPrefix(key, field, prefix) {
|
|
397
404
|
const columnName = this.resolveColumnName(key, field);
|
|
@@ -470,7 +477,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
470
477
|
}
|
|
471
478
|
}
|
|
472
479
|
appendLikeOp(ctx, field, op, wrappedVal) {
|
|
473
|
-
const isIlike =
|
|
480
|
+
const isIlike = AbstractSqlDialect.LIKE_CASE_INSENSITIVE_OPS.has(op);
|
|
474
481
|
const ph = this.addValue(ctx.values, wrappedVal);
|
|
475
482
|
if (isIlike && field) {
|
|
476
483
|
ctx.append(this.ilikeExpr(field, ph));
|
|
@@ -504,15 +511,17 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
504
511
|
* Used by both `$elemMatch` and dot-notation paths.
|
|
505
512
|
* All dialect-specific behavior comes from overridable methods on `this`.
|
|
506
513
|
*/
|
|
507
|
-
buildJsonFieldCondition(ctx, fieldAccessor, jsonPath, op, value) {
|
|
514
|
+
buildJsonFieldCondition(ctx, fieldAccessor, jsonPath, op, value, asJson = false) {
|
|
508
515
|
const jsonField = fieldAccessor(jsonPath);
|
|
509
516
|
switch (op) {
|
|
510
517
|
case '$eq':
|
|
511
|
-
|
|
518
|
+
if (value === null)
|
|
519
|
+
return `${jsonField} IS NULL`;
|
|
520
|
+
return `${this.jsonComparand(jsonField, value)} = ${this.jsonOperand(ctx, value, asJson)}`;
|
|
512
521
|
case '$ne':
|
|
513
522
|
if (value === null)
|
|
514
523
|
return `${jsonField} IS NOT NULL`;
|
|
515
|
-
return this.neExpr(jsonField, this.
|
|
524
|
+
return this.neExpr(this.jsonComparand(jsonField, value), this.jsonOperand(ctx, value, asJson));
|
|
516
525
|
case '$gt':
|
|
517
526
|
return `${this.numericCast(jsonField)} > ${this.addValue(ctx.values, value)}`;
|
|
518
527
|
case '$gte':
|
|
@@ -541,7 +550,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
541
550
|
return `${jsonField} ${this.regexpOp} ${this.addValue(ctx.values, value)}`;
|
|
542
551
|
case '$in':
|
|
543
552
|
case '$nin':
|
|
544
|
-
return this.jsonInNin(ctx, jsonField, op, value);
|
|
553
|
+
return this.jsonInNin(ctx, jsonField, op, value, asJson);
|
|
545
554
|
case '$all':
|
|
546
555
|
return this.jsonAll(ctx, jsonField, value);
|
|
547
556
|
case '$size':
|
|
@@ -552,20 +561,117 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
552
561
|
throw TypeError(`unknown operator: ${op}`);
|
|
553
562
|
}
|
|
554
563
|
}
|
|
555
|
-
jsonInNin(ctx, jsonField, op, value) {
|
|
556
|
-
|
|
564
|
+
jsonInNin(ctx, jsonField, op, value, asJson) {
|
|
565
|
+
const values = Array.isArray(value) ? value : [];
|
|
566
|
+
const negate = op === '$nin';
|
|
567
|
+
if (!asJson) {
|
|
568
|
+
return `${this.jsonComparand(jsonField, values)}${this.formatIn(ctx, values, negate)}`;
|
|
569
|
+
}
|
|
570
|
+
// JSON values have no portable array literal, so the set expands into explicit comparisons.
|
|
571
|
+
const comparisons = values.map((val) => `${jsonField} ${negate ? '<>' : '='} ${this.jsonScalarParam(ctx, val)}`);
|
|
572
|
+
return `(${comparisons.join(negate ? ' AND ' : ' OR ')})`;
|
|
573
|
+
}
|
|
574
|
+
/** The bound operand of a JSON comparison: JSON-encoded when comparing against the JSON value. */
|
|
575
|
+
jsonOperand(ctx, value, asJson) {
|
|
576
|
+
return asJson ? this.jsonScalarParam(ctx, value) : this.addValue(ctx.values, value);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* The left side of a comparison against a JSON scalar, cast when the operand is numeric - see
|
|
580
|
+
* {@link jsonCompareMode} for why each mode exists.
|
|
581
|
+
*/
|
|
582
|
+
jsonComparand(jsonField, value) {
|
|
583
|
+
return this.jsonCompareMode(value) === 'numeric' ? this.numericCast(jsonField) : jsonField;
|
|
557
584
|
}
|
|
558
|
-
|
|
559
|
-
|
|
585
|
+
/**
|
|
586
|
+
* How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
|
|
587
|
+
* of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
|
|
588
|
+
* compared in the representation every engine agrees on:
|
|
589
|
+
* - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
|
|
590
|
+
* would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
|
|
591
|
+
* - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
|
|
592
|
+
* portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
|
|
593
|
+
* - `text` - compare as extracted, which is also what the string operators need.
|
|
594
|
+
*
|
|
595
|
+
* Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
|
|
596
|
+
*/
|
|
597
|
+
jsonCompareMode(value) {
|
|
598
|
+
const operands = Array.isArray(value) ? value : [value];
|
|
599
|
+
if (operands.length === 0) {
|
|
600
|
+
return 'text';
|
|
601
|
+
}
|
|
602
|
+
if (operands.every((operand) => typeof operand === 'boolean')) {
|
|
603
|
+
return 'json';
|
|
604
|
+
}
|
|
605
|
+
return operands.every((operand) => typeof operand === 'number') ? 'numeric' : 'text';
|
|
560
606
|
}
|
|
561
|
-
|
|
562
|
-
|
|
607
|
+
/**
|
|
608
|
+
* Whether the dialect's array containment ({@link jsonAll}) matches an object element that merely
|
|
609
|
+
* *includes* the given keys, as PostgreSQL's `@>` and MySQL's `JSON_CONTAINS` do. SQLite compares
|
|
610
|
+
* elements as whole JSON text, so it cannot express a partial match and always expands the
|
|
611
|
+
* per-field form below.
|
|
612
|
+
*/
|
|
613
|
+
jsonContainmentIsPartial = true;
|
|
614
|
+
/**
|
|
615
|
+
* Whether an exploded *scalar* element keeps its SQL type. SQLite's `json_each` yields JSON
|
|
616
|
+
* booleans as `0`/`1` integers and numbers as numbers, so such an element compares directly to a
|
|
617
|
+
* bound value; PostgreSQL and MySQL explode scalars to text, losing the type, so a non-string
|
|
618
|
+
* operand there has to compare as JSON (see {@link isJsonbOp}).
|
|
619
|
+
*/
|
|
620
|
+
jsonScalarElemKeepsType = false;
|
|
621
|
+
/**
|
|
622
|
+
* `$elemMatch`: at least one element of the JSON array satisfies `match`. Three shapes, decided
|
|
623
|
+
* here so every dialect only supplies {@link jsonElemFrom} / {@link jsonElemRef}:
|
|
624
|
+
* - keys are operators (`{ $startsWith: 'ad' }`) - scalar elements, conditions on the element;
|
|
625
|
+
* - a plain object with no nested operators - containment, which is the only form an index serves;
|
|
626
|
+
* - otherwise - per-field conditions over the exploded objects.
|
|
627
|
+
*/
|
|
628
|
+
jsonElemMatch(ctx, jsonField, match) {
|
|
629
|
+
// Conditions on the element itself. One `FROM` serves them all, so the element is read as JSON
|
|
630
|
+
// only when *every* operand needs it - the same all-operands rule the comparison classifier uses.
|
|
631
|
+
if (isOperatorOnlyObject(match)) {
|
|
632
|
+
const entries = Object.entries(match);
|
|
633
|
+
const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) => this.isJsonbOp(op, val));
|
|
634
|
+
const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, () => this.jsonElemRef(undefined, asJson), '', op, val, asJson));
|
|
635
|
+
return jsonElemExists(this.jsonElemFrom(jsonField, [], asJson), conditions);
|
|
636
|
+
}
|
|
637
|
+
if (isOperatorObject(match)) {
|
|
638
|
+
throw TypeError(`$elemMatch cannot mix operators with field names: ${Object.keys(match).join(', ')}`);
|
|
639
|
+
}
|
|
640
|
+
// A plain object with no nested operators is containment, which is also the only form an index
|
|
641
|
+
// can serve. SQLite compares elements exactly, so it always expands the per-field form below.
|
|
642
|
+
if (this.jsonContainmentIsPartial && !someValue(match, isOperatorObject)) {
|
|
643
|
+
return this.jsonAll(ctx, jsonField, [match]);
|
|
644
|
+
}
|
|
645
|
+
const conditions = buildElemMatchConditions(match, (field, op, opVal) => {
|
|
646
|
+
const asJson = this.isJsonbOp(op, opVal);
|
|
647
|
+
return this.buildJsonFieldCondition(ctx, (f) => this.jsonElemRef(f, asJson), field, op, opVal, asJson);
|
|
648
|
+
});
|
|
649
|
+
return jsonElemExists(this.jsonElemFrom(jsonField, Object.keys(match)), conditions);
|
|
563
650
|
}
|
|
564
|
-
|
|
565
|
-
|
|
651
|
+
/**
|
|
652
|
+
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
653
|
+
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
654
|
+
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
655
|
+
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
|
|
656
|
+
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
657
|
+
*
|
|
658
|
+
* Numbers stay on the text accessor with a numeric cast ({@link jsonComparand}), which keeps
|
|
659
|
+
* `1` equal to `1.0` - JSON equality would not.
|
|
660
|
+
*/
|
|
661
|
+
isJsonbOp(op, value) {
|
|
662
|
+
if (op === '$all' || op === '$size' || op === '$elemMatch') {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
const comparesValue = op === '$eq' || op === '$ne' || op === '$in' || op === '$nin';
|
|
666
|
+
return comparesValue && this.jsonCompareMode(value) === 'json';
|
|
566
667
|
}
|
|
567
|
-
|
|
568
|
-
|
|
668
|
+
/** A JSON-encoded bound parameter, cast to the dialect's JSON type. */
|
|
669
|
+
jsonScalarParam(ctx, value) {
|
|
670
|
+
if (value instanceof QueryRaw) {
|
|
671
|
+
return this.addValue(ctx.values, value);
|
|
672
|
+
}
|
|
673
|
+
ctx.pushValue(JSON.stringify(value));
|
|
674
|
+
return this.getJsonCastExpr();
|
|
569
675
|
}
|
|
570
676
|
getComparisonKey(ctx, entity, key, { prefix } = {}) {
|
|
571
677
|
const meta = getMeta(entity);
|
|
@@ -632,59 +738,6 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
632
738
|
ctx.append(this.escapeId(name) + direction);
|
|
633
739
|
});
|
|
634
740
|
}
|
|
635
|
-
/**
|
|
636
|
-
* Resolve common parameters for a vector similarity ORDER BY expression.
|
|
637
|
-
* Shared by all dialect overrides of `appendVectorSort`.
|
|
638
|
-
*/
|
|
639
|
-
resolveVectorSortParams(meta, key, search) {
|
|
640
|
-
const field = meta.fields[key];
|
|
641
|
-
const colName = this.resolveColumnName(key, field);
|
|
642
|
-
const distance = search.$distance ?? field?.distance ?? 'cosine';
|
|
643
|
-
const vectorCast = resolveVectorCast(field);
|
|
644
|
-
return { colName, distance, field, vectorCast };
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Mapping of UQL vector distance metrics to native SQL functions.
|
|
648
|
-
* Override in dialects that use function-call syntax (e.g. SQLite, MariaDB).
|
|
649
|
-
* Dialects with operator-based syntax (e.g. Postgres) leave this empty and override `appendVectorSort` directly.
|
|
650
|
-
*/
|
|
651
|
-
vectorDistanceFns = new Map();
|
|
652
|
-
/**
|
|
653
|
-
* Append a vector similarity function call: `fn(col, ?)`.
|
|
654
|
-
* Used by dialects that express vector distance via SQL functions (SQLite, MariaDB).
|
|
655
|
-
*/
|
|
656
|
-
appendFunctionVectorSort(ctx, meta, key, search, dialectName) {
|
|
657
|
-
const { colName, distance, vectorCast } = this.resolveVectorSortParams(meta, key, search);
|
|
658
|
-
const fn = this.vectorDistanceFns.get(distance);
|
|
659
|
-
if (!fn) {
|
|
660
|
-
throw Error(`${dialectName} does not support vector distance metric: ${distance}`);
|
|
661
|
-
}
|
|
662
|
-
ctx.append(`${fn}(${this.escapeId(colName)}, `);
|
|
663
|
-
ctx.addValue(`[${search.$vector.join(',')}]`);
|
|
664
|
-
if (vectorCast && dialectName === 'PostgreSQL') {
|
|
665
|
-
ctx.append(`::${vectorCast}`);
|
|
666
|
-
}
|
|
667
|
-
ctx.append(')');
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Append a vector distance projection.
|
|
671
|
-
*/
|
|
672
|
-
appendVectorProjection(ctx, meta, key, search) {
|
|
673
|
-
this.appendVectorSort(ctx, meta, key, search);
|
|
674
|
-
ctx.append(` AS ${this.escapeId(search.$project)}`);
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Append a vector similarity ORDER BY expression.
|
|
678
|
-
* Default: auto-delegates to `appendFunctionVectorSort` when `vectorDistanceFns` has entries.
|
|
679
|
-
* Override for operator-based syntax (e.g. PostgreSQL `<=>`, `<->` operators).
|
|
680
|
-
*/
|
|
681
|
-
appendVectorSort(ctx, meta, key, search) {
|
|
682
|
-
if (this.vectorDistanceFns.size > 0) {
|
|
683
|
-
this.appendFunctionVectorSort(ctx, meta, key, search, this.dialectName);
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
throw new TypeError('Vector similarity sort is not supported by this dialect. Use raw() for vector queries.');
|
|
687
|
-
}
|
|
688
741
|
pager(ctx, opts) {
|
|
689
742
|
if (opts.$limit) {
|
|
690
743
|
ctx.append(` LIMIT ${Number(opts.$limit)}`);
|
|
@@ -894,8 +947,8 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
894
947
|
const columnName = this.resolveColumnName(key, field);
|
|
895
948
|
const escapedCol = this.escapeId(columnName);
|
|
896
949
|
const value = filledPayload[key];
|
|
897
|
-
if (
|
|
898
|
-
this.formatJsonUpdate(ctx, escapedCol, value);
|
|
950
|
+
if (isJsonUpdateOp(value)) {
|
|
951
|
+
this.formatJsonUpdate(ctx, escapedCol, value, field);
|
|
899
952
|
}
|
|
900
953
|
else {
|
|
901
954
|
ctx.append(`${escapedCol} = `);
|
|
@@ -1019,50 +1072,59 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1019
1072
|
ctx.addValue(value);
|
|
1020
1073
|
}
|
|
1021
1074
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1075
|
+
* Reads `operand` as a JSON value. Passing the `?` placeholder yields the cast for a bound
|
|
1076
|
+
* parameter, and passing an expression re-reads text as JSON - the same SQL either way, which is
|
|
1077
|
+
* why this is one hook rather than a placeholder variant plus an expression variant.
|
|
1078
|
+
*/
|
|
1079
|
+
jsonCast(operand) {
|
|
1080
|
+
return `CAST(${operand} AS JSON)`;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* The JSON cast applied to a bound placeholder. Only the positional-placeholder dialects use it -
|
|
1084
|
+
* PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
1029
1085
|
*/
|
|
1030
1086
|
getJsonCastExpr() {
|
|
1031
|
-
return '
|
|
1087
|
+
return this.jsonCast('?');
|
|
1032
1088
|
}
|
|
1033
|
-
|
|
1089
|
+
/**
|
|
1090
|
+
* Generate the full `"col" = <expression>` assignment for a JSON update operator payload.
|
|
1091
|
+
* Called from `update()` when a field value is a {@link JsonUpdateOp}.
|
|
1092
|
+
*
|
|
1093
|
+
* Each operator wraps the expression built so far, innermost-first in the order stated on
|
|
1094
|
+
* {@link JsonUpdateOp} (`$pull` -> `$set` -> `$push` -> `$unset`), so dialects only supply the
|
|
1095
|
+
* four SQL fragments below. Two invariants keep every dialect consistent and keep bound values in
|
|
1096
|
+
* step with their placeholders:
|
|
1097
|
+
* - `$pull` is innermost and its subquery reads `escapedCol`, so its value binds exactly once.
|
|
1098
|
+
* - Later fragments reference `expr` at most once, so a `$pull` subquery is never duplicated
|
|
1099
|
+
* (which would bind its value twice on positional-placeholder dialects). PostgreSQL's `$push`
|
|
1100
|
+
* is the one exception, and is safe there because its placeholders are numbered.
|
|
1101
|
+
*/
|
|
1102
|
+
formatJsonUpdate(ctx, escapedCol, value, field) {
|
|
1103
|
+
// Centralizes the one narrowing cast: the payload's keys are typed against the entity's JSON
|
|
1104
|
+
// payload, which the dialects do not need - they only build SQL from keys and values.
|
|
1105
|
+
const { $pull, $set, $push, $unset } = value;
|
|
1034
1106
|
let expr = escapedCol;
|
|
1035
|
-
if (hasKeys(
|
|
1036
|
-
|
|
1037
|
-
expr = `JSON_SET(COALESCE(${escapedCol}, '{}')`;
|
|
1038
|
-
for (const [key, v] of Object.entries(merge)) {
|
|
1039
|
-
expr += `, '$.${this.escapeJsonKey(key)}', ${this.getJsonCastExpr()}`;
|
|
1040
|
-
ctx.pushValue(JSON.stringify(v));
|
|
1041
|
-
}
|
|
1042
|
-
expr += ')';
|
|
1043
|
-
}
|
|
1044
|
-
if (hasKeys(value.$push)) {
|
|
1045
|
-
const push = value.$push;
|
|
1046
|
-
expr = `JSON_ARRAY_APPEND(${expr}`;
|
|
1047
|
-
for (const [key, v] of Object.entries(push)) {
|
|
1048
|
-
expr += `, '$.${this.escapeJsonKey(key)}', ${this.getJsonCastExpr()}`;
|
|
1049
|
-
ctx.pushValue(JSON.stringify(v));
|
|
1050
|
-
}
|
|
1051
|
-
expr += ')';
|
|
1107
|
+
if (hasKeys($pull)) {
|
|
1108
|
+
expr = this.jsonPull(ctx, expr, escapedCol, $pull);
|
|
1052
1109
|
}
|
|
1053
|
-
if (
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1110
|
+
if (hasKeys($set)) {
|
|
1111
|
+
expr = this.jsonSet(ctx, expr, $set, field);
|
|
1112
|
+
}
|
|
1113
|
+
if (hasKeys($push)) {
|
|
1114
|
+
expr = this.jsonPush(ctx, expr, $push);
|
|
1115
|
+
}
|
|
1116
|
+
if ($unset?.length) {
|
|
1117
|
+
expr = this.jsonUnset(ctx, expr, $unset);
|
|
1057
1118
|
}
|
|
1058
1119
|
ctx.append(`${escapedCol} = ${expr}`);
|
|
1059
1120
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* Remove every element equal to the given value, per array key. Each key wraps the expression
|
|
1123
|
+
* built so far, so dialects only supply {@link jsonPullKey} - and because every key reads
|
|
1124
|
+
* `escapedCol` rather than the accumulated expression, values bind once, in key order.
|
|
1125
|
+
*/
|
|
1126
|
+
jsonPull(ctx, expr, escapedCol, pull) {
|
|
1127
|
+
return Object.entries(pull).reduce((acc, [key, value]) => this.jsonPullKey(ctx, acc, escapedCol, key, value), expr);
|
|
1066
1128
|
}
|
|
1067
1129
|
getRawValue(ctx, opts) {
|
|
1068
1130
|
const { value, prefix = '', escapedPrefix, autoPrefixAlias } = opts;
|
|
@@ -1127,7 +1189,8 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1127
1189
|
operators.forEach((op, index) => {
|
|
1128
1190
|
if (index > 0)
|
|
1129
1191
|
ctx.append(' AND ');
|
|
1130
|
-
const
|
|
1192
|
+
const asJson = this.isJsonbOp(op, value[op]);
|
|
1193
|
+
const sql = this.buildJsonFieldCondition(ctx, () => accessor(asJson), jsonPath, op, value[op], asJson);
|
|
1131
1194
|
if (sql) {
|
|
1132
1195
|
ctx.append(sql);
|
|
1133
1196
|
}
|
|
@@ -1146,7 +1209,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1146
1209
|
let expr = escapedColumn;
|
|
1147
1210
|
for (let i = 0; i < segments.length; i++) {
|
|
1148
1211
|
const op = i === segments.length - 1 ? '->>' : '->';
|
|
1149
|
-
expr = `(${expr}${op}'${
|
|
1212
|
+
expr = `(${expr}${op}'${escapeSingleQuotes(segments[i])}')`;
|
|
1150
1213
|
}
|
|
1151
1214
|
return expr;
|
|
1152
1215
|
}
|
|
@@ -1154,7 +1217,7 @@ export class AbstractSqlDialect extends AbstractDialect {
|
|
|
1154
1217
|
const segments = jsonPath.split('.');
|
|
1155
1218
|
let expr = escapedColumn;
|
|
1156
1219
|
for (const segment of segments) {
|
|
1157
|
-
expr = `(${expr}->'${
|
|
1220
|
+
expr = `(${expr}->'${escapeSingleQuotes(segment)}')`;
|
|
1158
1221
|
}
|
|
1159
1222
|
return expr;
|
|
1160
1223
|
}
|