uql-orm 0.67.1 → 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/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
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
|
-
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonUpdateOp, isOperatorMap,
|
|
5
|
-
import { escapeAnsiSqlLiteral
|
|
6
|
-
import { COUNT_ALIAS, COUNTED_ROWS_ALIAS, JSON_ELEM_ALIAS, relationSortColumn } from './aliases.js';
|
|
7
|
-
import {
|
|
8
|
-
import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
|
|
4
|
+
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, raw, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
|
+
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
|
+
import { COUNT_ALIAS, COUNTED_ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn } from './aliases.js';
|
|
7
|
+
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
9
8
|
import { SqlQueryContext } from './queryContext.js';
|
|
10
9
|
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
11
10
|
import { resolveVectorCast } from './vectorCast.js';
|
|
12
11
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
13
12
|
/** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
|
|
14
13
|
export function relationTermKey({ sql, key }) {
|
|
15
|
-
|
|
16
|
-
throw new TypeError(`a raw $select in a populated relation needs an alias, the key its value lands under: ${sql}`);
|
|
17
|
-
}
|
|
18
|
-
return key;
|
|
14
|
+
return orRefuse(key, `a raw $select in a populated relation needs an alias, the key its value lands under: ${sql}`);
|
|
19
15
|
}
|
|
20
16
|
/**
|
|
21
17
|
* What a projection reads: its raw expressions, or its fields past any `$exclude`, and every field where
|
|
@@ -27,6 +23,13 @@ function projectedKeys(meta, select, exclude, json) {
|
|
|
27
23
|
: normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
|
|
28
24
|
return selected.length || !json ? selected : normalizeScalarFieldSelection(meta);
|
|
29
25
|
}
|
|
26
|
+
/** `value`, where there is one; a `TypeError` saying `refusal` where there is none. */
|
|
27
|
+
function orRefuse(value, refusal) {
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
throw TypeError(refusal);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
30
33
|
/** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
|
|
31
34
|
function inOperands(op, value) {
|
|
32
35
|
if (!Array.isArray(value)) {
|
|
@@ -206,8 +209,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
206
209
|
}
|
|
207
210
|
/**
|
|
208
211
|
* The expression a scalar field is read through in the statement's own rows, the plain column by
|
|
209
|
-
* default. MariaDB reads a vector column back
|
|
210
|
-
*
|
|
212
|
+
* default. MariaDB reads a vector column back as hex, since selecting it raw yields its binary form.
|
|
213
|
+
* A related row's column crosses JSON through {@link carriedFields} instead.
|
|
211
214
|
*/
|
|
212
215
|
selectFieldExpr(escapedColumn, _field) {
|
|
213
216
|
return escapedColumn;
|
|
@@ -413,7 +416,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
413
416
|
// Detect JSONB dot-notation: 'column.path' where column is a registered JSON/JSONB field
|
|
414
417
|
const jsonDot = this.resolveJsonDotPath(meta, key, opts.prefix);
|
|
415
418
|
if (jsonDot) {
|
|
416
|
-
this.
|
|
419
|
+
ctx.append(this.jsonConditions(ctx, this.jsonPathTarget(jsonDot), val));
|
|
417
420
|
return;
|
|
418
421
|
}
|
|
419
422
|
if (key.includes('.')) {
|
|
@@ -430,19 +433,12 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
430
433
|
return;
|
|
431
434
|
}
|
|
432
435
|
const value = this.normalizeWhereValue(val);
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
ctx.append(' AND ');
|
|
440
|
-
}
|
|
441
|
-
this.compareFieldOperator(ctx, entity, key, op, value[op], opts);
|
|
442
|
-
});
|
|
443
|
-
if (operators.length > 1) {
|
|
444
|
-
ctx.append(')');
|
|
445
|
-
}
|
|
436
|
+
const parts = getKeys(value).map((op) => this.fieldCondition(ctx, entity, key, op, value[op], opts));
|
|
437
|
+
ctx.append(AbstractSqlDialect.conjunction(parts));
|
|
438
|
+
}
|
|
439
|
+
/** Conditions joined by `AND`, parenthesized where there is more than one. */
|
|
440
|
+
static conjunction(parts) {
|
|
441
|
+
return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
|
|
446
442
|
}
|
|
447
443
|
compareLogicalOperator(ctx, entity, key, val, opts) {
|
|
448
444
|
const { join, negate } = AbstractSqlDialect.GROUP_OPS[key];
|
|
@@ -477,14 +473,16 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
477
473
|
/** What a `$near` says about the search itself; everything else in it is a bound. */
|
|
478
474
|
static VECTOR_QUERY_KEYS = new Set(VECTOR_QUERY_KEYS);
|
|
479
475
|
/**
|
|
480
|
-
* The
|
|
481
|
-
*
|
|
482
|
-
*
|
|
476
|
+
* The ordered comparisons, `QueryOrderedOp` at runtime, derived from the map above rather than spelled
|
|
477
|
+
* again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
|
|
478
|
+
* for, and the operators that read a JSON path as a number.
|
|
483
479
|
*/
|
|
484
|
-
static
|
|
480
|
+
static ORDERED_OPS = new Set([
|
|
485
481
|
...AbstractSqlDialect.COMPARE_OP_MAP.keys(),
|
|
486
482
|
'$between',
|
|
487
483
|
]);
|
|
484
|
+
/** The operators an equality compares by value, which a JSON path reads the way that value compares. */
|
|
485
|
+
static EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
|
|
488
486
|
/**
|
|
489
487
|
* Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
|
|
490
488
|
* Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
|
|
@@ -546,42 +544,31 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
546
544
|
const inlined = field && isInlinedExpression(field) ? field.computed : undefined;
|
|
547
545
|
return inlined ? this.rawFragment(ctx, inlined, prefix, entity) : undefined;
|
|
548
546
|
}
|
|
549
|
-
/**
|
|
547
|
+
/** {@link fieldCondition}, appended. */
|
|
550
548
|
compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
549
|
+
ctx.append(this.fieldCondition(ctx, entity, key, op, val, opts));
|
|
550
|
+
}
|
|
551
|
+
/** One operator of a field's condition. Both come from the query as data, so neither is trusted. */
|
|
552
|
+
fieldCondition(ctx, entity, key, op, val, opts) {
|
|
553
|
+
if (op === '$not') {
|
|
554
|
+
return `NOT (${this.buildFragment(ctx, (fragmentCtx) => this.compare(fragmentCtx, entity, key, val, opts))})`;
|
|
554
555
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
ctx.append('NOT (');
|
|
558
|
-
this.compare(ctx, entity, key, val, opts);
|
|
559
|
-
ctx.append(')');
|
|
560
|
-
break;
|
|
561
|
-
case '$all':
|
|
562
|
-
ctx.append(this.jsonAll(ctx, field, val));
|
|
563
|
-
break;
|
|
564
|
-
case '$size':
|
|
565
|
-
ctx.append(this.jsonSize(ctx, field, val));
|
|
566
|
-
break;
|
|
567
|
-
case '$elemMatch':
|
|
568
|
-
ctx.append(this.jsonElemMatch(ctx, field, val));
|
|
569
|
-
break;
|
|
570
|
-
case '$near':
|
|
571
|
-
this.compareVectorNear(ctx, getMeta(entity), key, val);
|
|
572
|
-
break;
|
|
573
|
-
default:
|
|
574
|
-
throw TypeError(`unknown operator: ${op}`);
|
|
556
|
+
if (op === '$near') {
|
|
557
|
+
return this.vectorNearCondition(ctx, getMeta(entity), key, val);
|
|
575
558
|
}
|
|
559
|
+
const field = this.resolveOperandField(ctx, entity, key, opts);
|
|
560
|
+
const condition = this.operatorCondition(ctx, field, op, val) ?? this.jsonArrayCondition(ctx, { base: field, path: '' }, op, val);
|
|
561
|
+
return orRefuse(condition, `unknown operator: ${op}`);
|
|
576
562
|
}
|
|
577
563
|
/**
|
|
578
564
|
* `<operand> <op> <value>` for every operator that needs only its left-hand SQL, shared by a column, a
|
|
579
|
-
* `HAVING` expression and a
|
|
565
|
+
* JSON path, a `HAVING` expression, a count and a distance; `undefined` for the rest. `bind` renders
|
|
566
|
+
* each compared value, a plain placeholder unless a JSON path reads it otherwise.
|
|
580
567
|
*/
|
|
581
|
-
operatorCondition(ctx, operand, op, val) {
|
|
568
|
+
operatorCondition(ctx, operand, op, val, bind = (value) => this.addValue(ctx, value)) {
|
|
582
569
|
const compareOp = AbstractSqlDialect.COMPARE_OP_MAP.get(op);
|
|
583
570
|
if (compareOp) {
|
|
584
|
-
return `${operand}${compareOp}${
|
|
571
|
+
return `${operand}${compareOp}${bind(val)}`;
|
|
585
572
|
}
|
|
586
573
|
const like = this.likeCondition(ctx, operand, op, val);
|
|
587
574
|
if (like) {
|
|
@@ -589,17 +576,17 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
589
576
|
}
|
|
590
577
|
switch (op) {
|
|
591
578
|
case '$eq':
|
|
592
|
-
return val === null ? `${operand} IS NULL` : `${operand} = ${
|
|
579
|
+
return val === null ? `${operand} IS NULL` : `${operand} = ${bind(val)}`;
|
|
593
580
|
case '$ne':
|
|
594
|
-
return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand,
|
|
581
|
+
return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand, bind(val));
|
|
595
582
|
case '$regex':
|
|
596
583
|
return this.regexCondition(operand, this.addValue(ctx, val));
|
|
597
584
|
case '$in':
|
|
598
585
|
case '$nin':
|
|
599
|
-
return this.formatIn(ctx, operand, inOperands(op, val), op === '$nin');
|
|
586
|
+
return this.formatIn(ctx, operand, inOperands(op, val), op === '$nin', bind);
|
|
600
587
|
case '$between': {
|
|
601
588
|
const [min, max] = val;
|
|
602
|
-
return `${operand} BETWEEN ${
|
|
589
|
+
return `${operand} BETWEEN ${bind(min)} AND ${bind(max)}`;
|
|
603
590
|
}
|
|
604
591
|
case '$isNull':
|
|
605
592
|
return operand + (val ? ' IS NULL' : ' IS NOT NULL');
|
|
@@ -609,114 +596,147 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
609
596
|
return undefined;
|
|
610
597
|
}
|
|
611
598
|
}
|
|
612
|
-
/**
|
|
613
|
-
|
|
614
|
-
const condition = this.operatorCondition(ctx, operand, op, val);
|
|
615
|
-
if (condition === undefined) {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
ctx.append(condition);
|
|
619
|
-
return true;
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Build a comparison condition for a JSON field.
|
|
623
|
-
* Used by both `$elemMatch` and dot-notation paths.
|
|
624
|
-
* All dialect-specific behavior comes from overridable methods on `this`.
|
|
625
|
-
*/
|
|
626
|
-
buildJsonFieldCondition(ctx, fieldAccessor, jsonPath, op, value, asJson = false) {
|
|
627
|
-
const jsonField = fieldAccessor(jsonPath, asJson ? 'json' : 'text');
|
|
628
|
-
// The left side of a comparison reads the path the way its operand is compared: a numeric one
|
|
629
|
-
// cast to a number, a boolean as the JSON value.
|
|
630
|
-
const comparand = (val) => fieldAccessor(jsonPath, jsonCompareMode(val));
|
|
631
|
-
// The `$like` family reads a JSON path exactly as it reads a column, case folding included.
|
|
632
|
-
const like = this.likeCondition(ctx, jsonField, op, value);
|
|
633
|
-
if (like) {
|
|
634
|
-
return like;
|
|
635
|
-
}
|
|
636
|
-
// The ordered comparisons read the path as a number whatever the operand is, and spell their
|
|
637
|
-
// operator out of the same table a comparison against a column does.
|
|
638
|
-
const compareOp = AbstractSqlDialect.COMPARE_OP_MAP.get(op);
|
|
639
|
-
if (compareOp) {
|
|
640
|
-
return `${fieldAccessor(jsonPath, 'numeric')}${compareOp}${this.addValue(ctx, value)}`;
|
|
641
|
-
}
|
|
599
|
+
/** `$all`, `$size` and `$elemMatch`, which read the JSON array at `slot`; `undefined` for the rest. */
|
|
600
|
+
jsonArrayCondition(ctx, slot, op, val) {
|
|
642
601
|
switch (op) {
|
|
643
|
-
case '$eq':
|
|
644
|
-
if (value === null)
|
|
645
|
-
return `${jsonField} IS NULL`;
|
|
646
|
-
return `${comparand(value)} = ${this.jsonOperand(ctx, value, asJson)}`;
|
|
647
|
-
case '$ne':
|
|
648
|
-
if (value === null)
|
|
649
|
-
return `${jsonField} IS NOT NULL`;
|
|
650
|
-
return this.neExpr(comparand(value), this.jsonOperand(ctx, value, asJson));
|
|
651
|
-
case '$regex':
|
|
652
|
-
return this.regexCondition(jsonField, this.addValue(ctx, value));
|
|
653
|
-
case '$in':
|
|
654
|
-
case '$nin':
|
|
655
|
-
return this.jsonInNin(ctx, jsonField, comparand, op, value, asJson);
|
|
656
602
|
case '$all':
|
|
657
|
-
return this.jsonAll(ctx,
|
|
603
|
+
return this.jsonAll(ctx, slot, val);
|
|
658
604
|
case '$size':
|
|
659
|
-
return this.
|
|
605
|
+
return this.sizeCondition(ctx, (fragmentCtx) => fragmentCtx.append(this.jsonLength(slot)), val);
|
|
660
606
|
case '$elemMatch':
|
|
661
|
-
return this.jsonElemMatch(ctx,
|
|
607
|
+
return this.jsonElemMatch(ctx, slot, val);
|
|
662
608
|
default:
|
|
663
|
-
|
|
609
|
+
return undefined;
|
|
664
610
|
}
|
|
665
611
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
return `(${comparisons.join(negate ? ' AND ' : ' OR ')})`;
|
|
612
|
+
/** A path of a JSON document, read the way each operator reads it. */
|
|
613
|
+
jsonPathTarget(slot) {
|
|
614
|
+
return { slot, read: (mode) => this.jsonPathExpr(slot.base, slot.path, mode) };
|
|
615
|
+
}
|
|
616
|
+
/** Every operator `target` is compared with, `AND`-joined. */
|
|
617
|
+
jsonConditions(ctx, target, val) {
|
|
618
|
+
const value = this.normalizeWhereValue(val);
|
|
619
|
+
return AbstractSqlDialect.conjunction(getKeys(value).map((op) => this.jsonCondition(ctx, target, op, value[op])));
|
|
675
620
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
621
|
+
jsonCondition(ctx, target, op, value) {
|
|
622
|
+
if (op === '$not') {
|
|
623
|
+
return `NOT (${this.jsonConditions(ctx, target, value)})`;
|
|
624
|
+
}
|
|
625
|
+
const array = this.jsonArrayCondition(ctx, target.slot, op, value);
|
|
626
|
+
if (array !== undefined) {
|
|
627
|
+
return array;
|
|
628
|
+
}
|
|
629
|
+
const mode = AbstractSqlDialect.jsonOperatorMode(op, value);
|
|
630
|
+
const operand = target.read(mode);
|
|
631
|
+
// Only a boolean compares as a JSON value, so the set holds two at most, and MySQL documents `IN()`
|
|
632
|
+
// as unsupported on JSON values: the comparisons are spelled out.
|
|
633
|
+
if (mode === 'json' && (op === '$in' || op === '$nin')) {
|
|
634
|
+
const negate = op === '$nin';
|
|
635
|
+
const comparisons = inOperands(op, value).map((val) => `${operand} ${negate ? '<>' : '='} ${this.jsonScalarParam(ctx, val)}`);
|
|
636
|
+
return `(${comparisons.join(negate ? ' AND ' : ' OR ')})`;
|
|
637
|
+
}
|
|
638
|
+
const condition = this.operatorCondition(ctx, operand, op, value, (val) => this.jsonOperand(ctx, val, mode));
|
|
639
|
+
return orRefuse(condition, `unknown operator: ${op}`);
|
|
679
640
|
}
|
|
680
641
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
642
|
+
* How `op` reads a JSON value: an ordered comparison as a number, an equality as its operand compares,
|
|
643
|
+
* and a pattern or a null check as text.
|
|
683
644
|
*/
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (isOperatorOnlyObject(match)) {
|
|
688
|
-
const entries = Object.entries(match);
|
|
689
|
-
const asJson = !this.features.typedJsonElements && entries.every(([op, val]) => isJsonbOp(op, val));
|
|
690
|
-
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
691
|
-
const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, this.elemAccessor(alias, asJson), '', op, val, asJson));
|
|
692
|
-
return jsonElemExists(this.jsonElemFrom(jsonField, [], alias, asJson), conditions);
|
|
693
|
-
}
|
|
694
|
-
if (isOperatorObject(match)) {
|
|
695
|
-
throw TypeError(`$elemMatch cannot mix operators with field names: ${Object.keys(match).join(', ')}`);
|
|
645
|
+
static jsonOperatorMode(op, value) {
|
|
646
|
+
if (AbstractSqlDialect.ORDERED_OPS.has(op)) {
|
|
647
|
+
return 'numeric';
|
|
696
648
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
649
|
+
return AbstractSqlDialect.EQUALITY_OPS.has(op) ? jsonCompareMode(value) : 'text';
|
|
650
|
+
}
|
|
651
|
+
/** A bound operand of a JSON comparison, read the way `mode` reads the value it is compared with. */
|
|
652
|
+
jsonOperand(ctx, value, mode) {
|
|
653
|
+
if (mode === 'json') {
|
|
654
|
+
return this.jsonScalarParam(ctx, value);
|
|
701
655
|
}
|
|
656
|
+
const placeholder = this.addValue(ctx, value);
|
|
657
|
+
return mode === 'numeric' ? this.numericCast(placeholder) : placeholder;
|
|
658
|
+
}
|
|
659
|
+
/** The JSON value at `slot`, as an array operator reads it. */
|
|
660
|
+
jsonValue(slot) {
|
|
661
|
+
return slot.path ? this.jsonPathExpr(slot.base, slot.path, 'json') : slot.base;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* `$all`: the JSON array at `slot` has an element holding each value, as Postgres's `@>` and MySQL's
|
|
665
|
+
* `JSON_CONTAINS` read one: a scalar equal, an array each of its elements, an object each of its keys.
|
|
666
|
+
* Plain JSON goes to {@link jsonContains}; an operator anywhere in it is matched element by element.
|
|
667
|
+
*/
|
|
668
|
+
jsonAll(ctx, slot, values) {
|
|
669
|
+
return holdsOperator(values) ? this.jsonElemsHold(ctx, slot, values) : this.jsonContains(ctx, slot, values);
|
|
670
|
+
}
|
|
671
|
+
/** `$all` over plain JSON, which an engine with containment of its own spells natively, for an index to serve. */
|
|
672
|
+
jsonContains(ctx, slot, values) {
|
|
673
|
+
return this.jsonElemsHold(ctx, slot, values);
|
|
674
|
+
}
|
|
675
|
+
/** One `EXISTS` per value, over the elements of the array at `slot`: a scalar equal, anything else held. */
|
|
676
|
+
jsonElemsHold(ctx, slot, values) {
|
|
702
677
|
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
678
|
+
const from = this.jsonElemFrom(slot, alias);
|
|
679
|
+
const element = {
|
|
680
|
+
slot: { base: this.jsonElemDoc(alias), path: '' },
|
|
681
|
+
read: (mode) => this.jsonElemValue(slot, alias, mode),
|
|
682
|
+
};
|
|
683
|
+
const conditions = values.map((value) => {
|
|
684
|
+
const holds = isOperatorMap(value) || Array.isArray(value)
|
|
685
|
+
? this.jsonHolds(ctx, element, value)
|
|
686
|
+
: [this.jsonElemEquals(ctx, slot, alias, value)];
|
|
687
|
+
return jsonElemExists(from, holds, this.jsonElemHint);
|
|
706
688
|
});
|
|
707
|
-
return
|
|
689
|
+
return AbstractSqlDialect.conjunction(conditions);
|
|
708
690
|
}
|
|
709
691
|
/**
|
|
710
|
-
*
|
|
711
|
-
* of
|
|
712
|
-
* which is what the operator-only form matches on.
|
|
692
|
+
* What the JSON `target` reads satisfies to hold `value`: an array each of its elements, an object each
|
|
693
|
+
* of its keys as a path, and an operator map or a scalar compared as a path is.
|
|
713
694
|
*/
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
695
|
+
jsonHolds(ctx, target, value) {
|
|
696
|
+
if (Array.isArray(value)) {
|
|
697
|
+
return [this.jsonAll(ctx, target.slot, value)];
|
|
698
|
+
}
|
|
699
|
+
if (!isJsonObject(value)) {
|
|
700
|
+
return [this.jsonConditions(ctx, target, value)];
|
|
701
|
+
}
|
|
702
|
+
const { base, path } = target.slot;
|
|
703
|
+
return Object.entries(value).flatMap(([key, item]) => this.jsonHolds(ctx, this.jsonPathTarget({ base, path: path ? `${path}.${key}` : key }), item));
|
|
704
|
+
}
|
|
705
|
+
/** An element of the array at `slot` equal to `value`, both read as JSON. */
|
|
706
|
+
jsonElemEquals(ctx, slot, alias, value) {
|
|
707
|
+
return `${this.jsonElemValue(slot, alias, 'json')} = ${this.jsonScalarParam(ctx, value)}`;
|
|
708
|
+
}
|
|
709
|
+
/** The JSON array at `slot` contains at least one of `values`, each as `$all` reads it. */
|
|
710
|
+
jsonAny(ctx, slot, values) {
|
|
711
|
+
return `(${values.map((value) => this.jsonAll(ctx, slot, [value])).join(' OR ')})`;
|
|
712
|
+
}
|
|
713
|
+
/** An element of the array at `slot` itself, read the way `mode` reads a path: its document, or its root. */
|
|
714
|
+
jsonElemValue(_slot, alias, mode) {
|
|
715
|
+
const doc = this.jsonElemDoc(alias);
|
|
716
|
+
return mode === 'json' ? doc : this.jsonPathExpr(doc, '', mode);
|
|
719
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* `$elemMatch`: an element holds `match`, as `$all` reads one value, its operators testing the element
|
|
720
|
+
* itself. An element equal to one value, or to one of several, is containment whatever the operator
|
|
721
|
+
* says, which compares by JSON type and is what an array index serves.
|
|
722
|
+
*/
|
|
723
|
+
jsonElemMatch(ctx, slot, match) {
|
|
724
|
+
const keys = Object.keys(match);
|
|
725
|
+
if (keys.some(isOperatorKey) && !keys.every(isOperatorKey)) {
|
|
726
|
+
throw TypeError(`$elemMatch cannot mix operators with field names: ${keys.join(', ')}`);
|
|
727
|
+
}
|
|
728
|
+
const single = keys.length === 1;
|
|
729
|
+
const { $eq: equal, $in: within } = match;
|
|
730
|
+
if (single && isJsonScalar(equal)) {
|
|
731
|
+
return this.jsonAll(ctx, slot, [equal]);
|
|
732
|
+
}
|
|
733
|
+
if (single && Array.isArray(within) && within.length > 0 && within.every(isJsonScalar)) {
|
|
734
|
+
return this.jsonAny(ctx, slot, within);
|
|
735
|
+
}
|
|
736
|
+
return this.jsonAll(ctx, slot, [match]);
|
|
737
|
+
}
|
|
738
|
+
/** The optimizer hint a `$elemMatch` subquery opens with, where the engine plans one wrong without it. */
|
|
739
|
+
jsonElemHint = '';
|
|
720
740
|
/**
|
|
721
741
|
* A JSON-encoded bound parameter, cast to the dialect's JSON type. Only the positional-placeholder
|
|
722
742
|
* dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
@@ -799,35 +819,45 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
799
819
|
? { key: keyPath, expr: this.escapeId(value.$project), direction: '', output: true }
|
|
800
820
|
: {
|
|
801
821
|
key: keyPath,
|
|
802
|
-
expr: this.buildFragment(ctx, (fragmentCtx) => this.
|
|
822
|
+
expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value)),
|
|
803
823
|
direction: '',
|
|
804
824
|
output: false,
|
|
805
825
|
});
|
|
806
826
|
continue;
|
|
807
827
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
828
|
+
const direction = this.resolveSortDirection(value);
|
|
829
|
+
// A JSON path can sort by more than one reading, each carried under a name of its own.
|
|
830
|
+
this.sortColumns(ctx, meta, key, prefix).forEach((column, index) => {
|
|
831
|
+
columns.push({ key: index ? `${keyPath}:${index}` : keyPath, ...column, direction });
|
|
812
832
|
});
|
|
813
833
|
}
|
|
814
834
|
}
|
|
815
835
|
/**
|
|
816
|
-
* The `ORDER BY`
|
|
817
|
-
* `$select` alias - is an output alias, which is never
|
|
836
|
+
* The `ORDER BY` operands for one key: a JSON path's in each of {@link jsonSortModes}. A key that is
|
|
837
|
+
* not a field of `meta` - a `raw()` projection, a `$select` alias - is an output alias, which is never
|
|
838
|
+
* table-qualified and needs no resolving.
|
|
818
839
|
*/
|
|
819
|
-
|
|
840
|
+
sortColumns(ctx, meta, key, prefix) {
|
|
820
841
|
const field = meta.fields[key];
|
|
821
842
|
if (field) {
|
|
822
843
|
const expr = this.inlinedOperand(ctx, field, prefix ?? this.resolveTableAlias(meta), meta.entity) ??
|
|
823
844
|
this.columnWithPrefix(key, field, prefix);
|
|
824
|
-
return { expr, output: false };
|
|
845
|
+
return [{ expr, output: false }];
|
|
825
846
|
}
|
|
826
847
|
const json = this.resolveJsonDotPath(meta, key, prefix);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
848
|
+
if (!json) {
|
|
849
|
+
return [{ expr: this.escapeId(key), output: true }];
|
|
850
|
+
}
|
|
851
|
+
return this.jsonSortModes.map((mode) => ({
|
|
852
|
+
expr: this.jsonPathExpr(json.base, json.path, mode),
|
|
853
|
+
output: false,
|
|
854
|
+
}));
|
|
830
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* How a JSON path is sorted by: as the JSON value, which the engine orders by type and a number by its
|
|
858
|
+
* value. An engine that orders JSON as text reads a number first, then the text.
|
|
859
|
+
*/
|
|
860
|
+
jsonSortModes = ['json'];
|
|
831
861
|
/**
|
|
832
862
|
* `LIMIT`/`OFFSET`. `sorted` says whether an `ORDER BY` was emitted just before, which
|
|
833
863
|
* {@link MergeSqlDialect} needs: SQL Server refuses to page a statement that has none.
|
|
@@ -1007,21 +1037,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1007
1037
|
]);
|
|
1008
1038
|
resolveSortDirection(sort) {
|
|
1009
1039
|
const direction = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
|
|
1010
|
-
|
|
1011
|
-
throw TypeError(`unknown sort direction: ${sort}`);
|
|
1012
|
-
}
|
|
1013
|
-
return direction;
|
|
1040
|
+
return orRefuse(direction, `unknown sort direction: ${sort}`);
|
|
1014
1041
|
}
|
|
1015
|
-
/**
|
|
1042
|
+
/** Every operator of one `HAVING` condition, `AND`-joined. */
|
|
1016
1043
|
havingCondition(ctx, expr, condition) {
|
|
1017
1044
|
const ops = this.normalizeWhereValue(condition);
|
|
1018
|
-
getKeys(ops).
|
|
1019
|
-
|
|
1020
|
-
ctx.append(' AND ');
|
|
1021
|
-
if (!this.appendOperatorCondition(ctx, expr, op, ops[op])) {
|
|
1022
|
-
throw TypeError(`unsupported HAVING operator: ${op}`);
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1045
|
+
const parts = getKeys(ops).map((op) => orRefuse(this.operatorCondition(ctx, expr, op, ops[op]), `unsupported HAVING operator: ${op}`));
|
|
1046
|
+
ctx.append(parts.join(' AND '));
|
|
1025
1047
|
}
|
|
1026
1048
|
/**
|
|
1027
1049
|
* How many rows the filter matched, on every row of the page. A window function runs before
|
|
@@ -1386,6 +1408,27 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1386
1408
|
jsonPull(ctx, expr, escapedCol, pull) {
|
|
1387
1409
|
return Object.entries(pull).reduce((acc, [key, value]) => this.jsonPullKey(ctx, acc, escapedCol, key, value), expr);
|
|
1388
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Wrap `expr` so the array at `key` no longer contains `value`: the array rebuilt from the elements that
|
|
1413
|
+
* differ from it, and any other value put back as it is. `JSON_REPLACE` leaves an absent key, and a NULL
|
|
1414
|
+
* column, untouched.
|
|
1415
|
+
*/
|
|
1416
|
+
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
1417
|
+
const slot = { base: escapedCol, path: key };
|
|
1418
|
+
const elem = this.jsonElemValue(slot, JSON_PULL_ALIAS, 'json');
|
|
1419
|
+
const differs = this.jsonDiffers(elem, this.jsonScalarParam(ctx, value));
|
|
1420
|
+
const kept = `SELECT ${this.jsonArrayOf(elem)} FROM ${this.jsonElemFrom(slot, JSON_PULL_ALIAS)} WHERE ${differs}`;
|
|
1421
|
+
const pulled = `CASE WHEN ${this.jsonIsArray(slot)} THEN (${kept}) ELSE ${this.jsonValue(slot)} END`;
|
|
1422
|
+
return `JSON_REPLACE(${expr}, ${jsonPath(key)}, ${pulled})`;
|
|
1423
|
+
}
|
|
1424
|
+
/** The elements a `$pull` keeps, back in one array, and an empty one where it keeps none. */
|
|
1425
|
+
jsonArrayOf(elem) {
|
|
1426
|
+
return `COALESCE(JSON_ARRAYAGG(${elem}), JSON_ARRAY())`;
|
|
1427
|
+
}
|
|
1428
|
+
/** Whether an element is not the pulled value, both read as JSON. */
|
|
1429
|
+
jsonDiffers(elem, operand) {
|
|
1430
|
+
return `${elem} <> ${operand}`;
|
|
1431
|
+
}
|
|
1389
1432
|
compileDdl(sql, entity) {
|
|
1390
1433
|
const ctx = this.createContext({ inlineValues: true });
|
|
1391
1434
|
if (sql instanceof QueryRaw) {
|
|
@@ -1412,12 +1455,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1412
1455
|
escapedPrefix: escapedPrefix ?? this.escapeId(prefix, true, true),
|
|
1413
1456
|
});
|
|
1414
1457
|
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Resolves a dot-notation key to its JSON field metadata.
|
|
1417
|
-
* Shared by `where()` and `sort()` to detect 'column.path' keys where 'column' is a JSON/JSONB field.
|
|
1418
|
-
*
|
|
1419
|
-
* @returns resolved metadata or `undefined` if the key is not a JSON dot-notation path
|
|
1420
|
-
*/
|
|
1458
|
+
/** A `column.path` key of `meta`'s JSON field as the path it names, shared by `where` and `sort`; else `undefined`. */
|
|
1421
1459
|
resolveJsonDotPath(meta, key, prefix) {
|
|
1422
1460
|
const dotIndex = key.indexOf('.');
|
|
1423
1461
|
if (dotIndex <= 0) {
|
|
@@ -1430,66 +1468,17 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1430
1468
|
}
|
|
1431
1469
|
const colName = this.resolveColumnName(root, field);
|
|
1432
1470
|
const prefixed = (prefix ? this.escapeId(prefix, true, true) : '') + this.escapeId(colName);
|
|
1433
|
-
return {
|
|
1471
|
+
return { base: prefixed, path: key.slice(dotIndex + 1) };
|
|
1434
1472
|
}
|
|
1435
1473
|
/**
|
|
1436
1474
|
* One JSON path, read the way `mode` asks for: the one place the three readings are chosen between,
|
|
1437
1475
|
* so `$where`, `$sort` and every operator reach a path the same way. Public because a JSON index
|
|
1438
1476
|
* is matched back by its own text, so the migrator's `CREATE INDEX` has to spell it from here too.
|
|
1439
1477
|
*/
|
|
1440
|
-
jsonPathExpr(escapedColumn,
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
const scalar = this.getJsonPathScalarExpr(escapedColumn, jsonPath);
|
|
1445
|
-
return mode === 'numeric' ? this.numericCast(scalar) : scalar;
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Compare a JSONB dot-notation path, e.g. `'settings.isArchived': { $ne: true }`.
|
|
1449
|
-
* Receives a pre-resolved `resolveJsonDotPath` result to avoid redundant computation.
|
|
1450
|
-
*/
|
|
1451
|
-
compareJsonPath(ctx, resolved, val) {
|
|
1452
|
-
const { jsonPath, column } = resolved;
|
|
1453
|
-
const accessor = (path, mode) => this.jsonPathExpr(column, path, mode);
|
|
1454
|
-
const value = this.normalizeWhereValue(val);
|
|
1455
|
-
const operators = getKeys(value);
|
|
1456
|
-
if (operators.length > 1) {
|
|
1457
|
-
ctx.append('(');
|
|
1458
|
-
}
|
|
1459
|
-
operators.forEach((op, index) => {
|
|
1460
|
-
if (index > 0)
|
|
1461
|
-
ctx.append(' AND ');
|
|
1462
|
-
const asJson = isJsonbOp(op, value[op]);
|
|
1463
|
-
const sql = this.buildJsonFieldCondition(ctx, accessor, jsonPath, op, value[op], asJson);
|
|
1464
|
-
if (sql) {
|
|
1465
|
-
ctx.append(sql);
|
|
1466
|
-
}
|
|
1467
|
-
});
|
|
1468
|
-
if (operators.length > 1) {
|
|
1469
|
-
ctx.append(')');
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
/**
|
|
1473
|
-
* Returns SQL that extracts a scalar value from a JSON path.
|
|
1474
|
-
* Dialects can override this to customize path access syntax while preserving
|
|
1475
|
-
* the shared comparison/operator pipeline.
|
|
1476
|
-
*/
|
|
1477
|
-
getJsonPathScalarExpr(escapedColumn, jsonPath) {
|
|
1478
|
-
const segments = jsonPath.split('.');
|
|
1479
|
-
let expr = escapedColumn;
|
|
1480
|
-
for (let i = 0; i < segments.length; i++) {
|
|
1481
|
-
const op = i === segments.length - 1 ? '->>' : '->';
|
|
1482
|
-
expr = `(${expr}${op}'${escapeSingleQuotes(segments[i])}')`;
|
|
1483
|
-
}
|
|
1484
|
-
return expr;
|
|
1485
|
-
}
|
|
1486
|
-
getJsonPathJsonbExpr(escapedColumn, jsonPath) {
|
|
1487
|
-
const segments = jsonPath.split('.');
|
|
1488
|
-
let expr = escapedColumn;
|
|
1489
|
-
for (const segment of segments) {
|
|
1490
|
-
expr = `(${expr}->'${escapeSingleQuotes(segment)}')`;
|
|
1491
|
-
}
|
|
1492
|
-
return expr;
|
|
1478
|
+
jsonPathExpr(escapedColumn, path, mode) {
|
|
1479
|
+
return mode === 'numeric'
|
|
1480
|
+
? this.numericCast(this.jsonPathReading(escapedColumn, path, 'text'))
|
|
1481
|
+
: this.jsonPathReading(escapedColumn, path, mode);
|
|
1493
1482
|
}
|
|
1494
1483
|
/**
|
|
1495
1484
|
* Normalizes a raw WHERE value into an operator map.
|
|
@@ -1685,57 +1674,47 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1685
1674
|
}
|
|
1686
1675
|
/** Filter by relation size: the same subquery, counting instead of testing for existence. */
|
|
1687
1676
|
compareRelationSize(ctx, entity, relKey, sizeVal, rel, opts) {
|
|
1688
|
-
|
|
1677
|
+
const count = (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, getMeta(entity), relKey, rel, opts, 'COUNT(*)', {});
|
|
1678
|
+
ctx.append(this.sizeCondition(ctx, count, sizeVal));
|
|
1689
1679
|
}
|
|
1690
1680
|
/**
|
|
1691
|
-
* `<expr> <op> <value>` for each
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
* it twice. Shared by `$size`, which counts, and `$near`, which measures a distance.
|
|
1681
|
+
* `<expr> <op> <value>` for each bound, `AND`-joined. `expr` is spelled once per bound because it is an
|
|
1682
|
+
* expression, not a column: a `WHERE` has no output alias to refer back to. Shared by `$size`, which
|
|
1683
|
+
* counts, and `$near`, which measures a distance.
|
|
1695
1684
|
*/
|
|
1696
|
-
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
entries.forEach(([op, val], index) => {
|
|
1702
|
-
if (index > 0) {
|
|
1703
|
-
ctx.append(' AND ');
|
|
1704
|
-
}
|
|
1705
|
-
exprFn();
|
|
1706
|
-
appendOp(op, val);
|
|
1707
|
-
});
|
|
1708
|
-
if (entries.length > 1) {
|
|
1709
|
-
ctx.append(')');
|
|
1710
|
-
}
|
|
1685
|
+
boundConditions(ctx, expr, bounds, condition, refusal) {
|
|
1686
|
+
const parts = Object.entries(bounds)
|
|
1687
|
+
.filter(([, val]) => val !== undefined)
|
|
1688
|
+
.map(([op, val]) => orRefuse(condition(this.buildFragment(ctx, expr), op, val), `${refusal}: ${op}`));
|
|
1689
|
+
return AbstractSqlDialect.conjunction(parts);
|
|
1711
1690
|
}
|
|
1712
1691
|
/**
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1692
|
+
* A count compared with `size`, a number or its bounds. A count is never NULL, so its equality stays
|
|
1693
|
+
* plain rather than the null-safe `$ne` (`IS DISTINCT FROM`, `IS NOT`): same rows, shorter SQL.
|
|
1715
1694
|
*/
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1695
|
+
sizeCondition(ctx, count, size) {
|
|
1696
|
+
const bounds = typeof size === 'number' ? { $eq: size } : size;
|
|
1697
|
+
return this.boundConditions(ctx, count, bounds, (operand, op, val) => {
|
|
1698
|
+
if (op === '$eq' || op === '$ne') {
|
|
1699
|
+
return `${operand} ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx, val)}`;
|
|
1700
|
+
}
|
|
1701
|
+
return AbstractSqlDialect.ORDERED_OPS.has(op) ? this.operatorCondition(ctx, operand, op, val) : undefined;
|
|
1702
|
+
}, 'unsupported $size comparison operator');
|
|
1724
1703
|
}
|
|
1725
1704
|
/** `<distance> <op> ?`, the `$where` half of a vector search, its bounds checked here since `/http` input is untyped. */
|
|
1726
|
-
|
|
1705
|
+
vectorNearCondition(ctx, meta, key, near) {
|
|
1727
1706
|
const bounds = {};
|
|
1728
1707
|
for (const [op, val] of Object.entries(near)) {
|
|
1729
1708
|
if (AbstractSqlDialect.VECTOR_QUERY_KEYS.has(op) || val === undefined) {
|
|
1730
1709
|
continue;
|
|
1731
1710
|
}
|
|
1732
|
-
if (!AbstractSqlDialect.
|
|
1711
|
+
if (!AbstractSqlDialect.ORDERED_OPS.has(op)) {
|
|
1733
1712
|
throw TypeError(`unsupported $near bound: ${op}`);
|
|
1734
1713
|
}
|
|
1735
1714
|
bounds[op] = val;
|
|
1736
1715
|
}
|
|
1737
1716
|
if (!hasKeys(bounds)) {
|
|
1738
|
-
const boundOps = [...AbstractSqlDialect.
|
|
1717
|
+
const boundOps = [...AbstractSqlDialect.ORDERED_OPS].join(', ');
|
|
1739
1718
|
throw TypeError(`$near on '${key}' needs a bound (${boundOps}); without one it filters nothing`);
|
|
1740
1719
|
}
|
|
1741
1720
|
// Required by the type, so this only fires for a query that never met it: `/http` casts client
|
|
@@ -1746,30 +1725,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1746
1725
|
throw TypeError(`$near on '${key}' needs its own $vector`);
|
|
1747
1726
|
}
|
|
1748
1727
|
const search = { $vector, $distance };
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
/** The runtime half of {@link QuerySizeComparisonOps}: what a count can sensibly be compared with. */
|
|
1752
|
-
static SIZE_COMPARE_OPS = new Set([
|
|
1753
|
-
'$eq',
|
|
1754
|
-
'$ne',
|
|
1755
|
-
'$gt',
|
|
1756
|
-
'$gte',
|
|
1757
|
-
'$lt',
|
|
1758
|
-
'$lte',
|
|
1759
|
-
'$between',
|
|
1760
|
-
]);
|
|
1761
|
-
/** ` <op> <value>` after a count already written, refusing any operator a count cannot be compared with. */
|
|
1762
|
-
appendSizeOp(ctx, op, val) {
|
|
1763
|
-
if (!AbstractSqlDialect.SIZE_COMPARE_OPS.has(op)) {
|
|
1764
|
-
throw TypeError(`unsupported $size comparison operator: ${op}`);
|
|
1765
|
-
}
|
|
1766
|
-
// A COUNT is never NULL, so equality stays plain here instead of taking the shared renderer's
|
|
1767
|
-
// null-safe `$ne` (`IS DISTINCT FROM` on Postgres, `IS NOT` on SQLite). Same rows, shorter SQL.
|
|
1768
|
-
if (op === '$eq' || op === '$ne') {
|
|
1769
|
-
ctx.append(` ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx, val)}`);
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
this.appendOperatorCondition(ctx, '', op, val);
|
|
1728
|
+
const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, search);
|
|
1729
|
+
return this.boundConditions(ctx, distance, bounds, (operand, op, val) => this.operatorCondition(ctx, operand, op, val), 'unsupported $near bound');
|
|
1773
1730
|
}
|
|
1774
1731
|
/** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
|
|
1775
1732
|
escape(value) {
|
|
@@ -1800,12 +1757,12 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1800
1757
|
neExpr(field, ph) {
|
|
1801
1758
|
return `${field} ${this.neOp} ${ph}`;
|
|
1802
1759
|
}
|
|
1803
|
-
/** `operand IN (...)`
|
|
1804
|
-
formatIn(
|
|
1760
|
+
/** `operand IN (...)` of each value as `bind` renders it, or the constant an empty set reduces to: no value is in it. */
|
|
1761
|
+
formatIn(_ctx, operand, values, negate, bind) {
|
|
1805
1762
|
if (!values.length) {
|
|
1806
1763
|
return negate ? '1 = 1' : '1 = 0';
|
|
1807
1764
|
}
|
|
1808
|
-
const phs = values.map(
|
|
1765
|
+
const phs = values.map(bind).join(', ');
|
|
1809
1766
|
return `${operand} ${negate ? 'NOT IN' : 'IN'} (${phs})`;
|
|
1810
1767
|
}
|
|
1811
1768
|
toString() {
|