uql-orm 0.79.0 → 0.80.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.map +2 -2
- package/dist/cockroachdb/cockroachDialect.js +5 -1
- package/dist/dialect/abstractDialect.d.ts +1 -31
- package/dist/dialect/abstractDialect.js +3 -27
- package/dist/dialect/abstractSqlDialect.d.ts +25 -56
- package/dist/dialect/abstractSqlDialect.js +78 -145
- package/dist/dialect/aliases.d.ts +5 -0
- package/dist/dialect/aliases.js +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
- package/dist/dialect/operators.d.ts +66 -0
- package/dist/dialect/operators.js +129 -0
- package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
- package/dist/dialect/pgLikeSqlDialect.js +16 -3
- package/dist/entity/decorator/entity.d.ts +6 -1
- package/dist/entity/decorator/entity.js +12 -1
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +6 -1
- package/dist/entity/metadata/definition.js +19 -0
- package/dist/migrate/codegen/entityTypes.js +1 -2
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +2 -0
- package/dist/migrate/ddl/mssqlTableDdl.js +5 -0
- package/dist/migrate/ddl/tableDdl.d.ts +2 -0
- package/dist/migrate/ddl/tableDdl.js +4 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +4 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +10 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +13 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +21 -0
- package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
- package/dist/migrate/introspection/mongoIntrospector.js +4 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +8 -0
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mysqlIntrospector.js +9 -0
- package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/postgresIntrospector.js +10 -0
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +3 -0
- package/dist/migrate/migrator.d.ts +28 -1
- package/dist/migrate/migrator.js +88 -9
- package/dist/migrate/schemaGenerator.d.ts +12 -1
- package/dist/migrate/schemaGenerator.js +47 -5
- package/dist/migrate/storage/databaseStorage.d.ts +4 -0
- package/dist/migrate/storage/databaseStorage.js +14 -8
- package/dist/migrate/triggerSql.d.ts +24 -0
- package/dist/migrate/triggerSql.js +229 -0
- package/dist/mongo/mongoDialect.d.ts +0 -21
- package/dist/mongo/mongoDialect.js +105 -100
- package/dist/mongo/mongodbQuerier.js +17 -1
- package/dist/mssql/mssqlDialect.d.ts +18 -7
- package/dist/mssql/mssqlDialect.js +77 -33
- package/dist/mssql/mssqlQuerier.js +2 -2
- package/dist/schema/canonicalType.d.ts +6 -1
- package/dist/schema/canonicalType.js +14 -0
- package/dist/schema/schemaASTBuilder.d.ts +2 -8
- package/dist/schema/schemaASTBuilder.js +6 -20
- package/dist/sqlite/sqliteDialect.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.js +12 -3
- package/dist/type/dialect.d.ts +69 -9
- package/dist/type/entity.d.ts +96 -3
- package/dist/type/migration.d.ts +17 -0
- package/dist/type/query.d.ts +13 -4
- package/dist/type/queryWhere.d.ts +4 -2
- package/dist/util/field.util.d.ts +9 -1
- package/dist/util/field.util.js +14 -2
- package/dist/util/fieldOption.util.d.ts +2 -2
- package/dist/util/fieldOption.util.js +2 -2
- package/dist/util/raw.d.ts +9 -1
- package/dist/util/raw.js +36 -11
- package/dist/util/sql.util.d.ts +12 -0
- package/dist/util/sql.util.js +24 -3
- package/dist/util/uqlError.d.ts +2 -0
- package/dist/util/uqlError.js +4 -0
- package/package.json +4 -4
- package/skills/uql-orm/SKILL.md +11 -6
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS,
|
|
2
|
+
import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, } from '../type/index.js';
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
4
|
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
|
-
import { UqlUsageError } from '../util/uqlError.js';
|
|
6
|
+
import { kindOf, UqlUsageError } from '../util/uqlError.js';
|
|
7
7
|
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
|
|
8
8
|
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
9
|
+
import { betweenBounds, COMPARE_OPS, EQUALITY_OPS, GROUP_OPS, groupClauses, inOperands, isGroupOp, isOrderedOp, isVectorQuery, LIKE_OPS, ORDERED_OPS, VECTOR_QUERY_KEY_SET, whereOperators, } from './operators.js';
|
|
9
10
|
import { SqlQueryContext } from './queryContext.js';
|
|
10
11
|
import { groupPathField, NO_JOINS, aggregateColumnField, resolveGroupJoins, resolveQueryJoins, relationSortTerms, resolveSortableJoin, } from './queryJoins.js';
|
|
11
12
|
import { resolveVectorCast } from './vectorCast.js';
|
|
@@ -33,14 +34,43 @@ function orRefuse(value, refusal) {
|
|
|
33
34
|
}
|
|
34
35
|
return value;
|
|
35
36
|
}
|
|
36
|
-
/**
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
/** Conditions joined by `AND`, parenthesized where there is more than one. */
|
|
38
|
+
function conjunction(parts) {
|
|
39
|
+
return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How `op` reads a JSON value: an ordered comparison as a number, an equality as its operand compares,
|
|
43
|
+
* and a pattern or a null check as text.
|
|
44
|
+
*/
|
|
45
|
+
function jsonOperatorMode(op, value) {
|
|
46
|
+
if (ORDERED_OPS.has(op)) {
|
|
47
|
+
return 'numeric';
|
|
40
48
|
}
|
|
41
|
-
return value;
|
|
49
|
+
return EQUALITY_OPS.has(op) ? jsonCompareMode(value) : 'text';
|
|
42
50
|
}
|
|
51
|
+
/** `$group` aggregate operator to SQL function name, over ops `resolveAggregateOp` has already allowlisted. */
|
|
52
|
+
const AGGREGATE_FN = {
|
|
53
|
+
$count: 'COUNT',
|
|
54
|
+
$sum: 'SUM',
|
|
55
|
+
$avg: 'AVG',
|
|
56
|
+
$min: 'MIN',
|
|
57
|
+
$max: 'MAX',
|
|
58
|
+
};
|
|
59
|
+
const SORT_DIRECTION_MAP = new Map([
|
|
60
|
+
[1, {}],
|
|
61
|
+
['asc', {}],
|
|
62
|
+
['desc', { direction: ' DESC' }],
|
|
63
|
+
[-1, { direction: ' DESC' }],
|
|
64
|
+
['ascNullsFirst', { nulls: 'first' }],
|
|
65
|
+
['ascNullsLast', { nulls: 'last' }],
|
|
66
|
+
['descNullsFirst', { direction: ' DESC', nulls: 'first' }],
|
|
67
|
+
['descNullsLast', { direction: ' DESC', nulls: 'last' }],
|
|
68
|
+
]);
|
|
43
69
|
export class AbstractSqlDialect extends VectorSqlDialect {
|
|
70
|
+
/** Itself, unless the engine is a fork running another's SQL, which is the only case that overrides. */
|
|
71
|
+
get dialectFamily() {
|
|
72
|
+
return this.dialectName;
|
|
73
|
+
}
|
|
44
74
|
/**
|
|
45
75
|
* How this engine declares a namespace, so a generated migration creates the schemas its tables
|
|
46
76
|
* need before creating them. Only reached where {@link DialectFeatures.schemas} is on. MySQL and
|
|
@@ -142,10 +172,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
142
172
|
const expression = this.returningIdExpression(meta);
|
|
143
173
|
return expression ? `RETURNING ${expression}` : '';
|
|
144
174
|
}
|
|
175
|
+
/** What the returned row is read off, for an engine that names it: SQL Server's `INSERTED.`. */
|
|
176
|
+
returnedRowPrefix = '';
|
|
145
177
|
/** `<id column> AS id` on its own, for a statement composing a `RETURNING` list of several items. */
|
|
146
178
|
returningIdExpression(meta) {
|
|
147
179
|
const [idKey] = meta.ids;
|
|
148
|
-
return meta.ids.length === 1
|
|
180
|
+
return meta.ids.length === 1
|
|
181
|
+
? `${this.returnedRowPrefix}${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}`
|
|
182
|
+
: '';
|
|
149
183
|
}
|
|
150
184
|
search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS, order) {
|
|
151
185
|
const meta = getMeta(entity);
|
|
@@ -258,7 +292,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
258
292
|
}
|
|
259
293
|
/** The columns a `$text` over `keys` reads, qualified by `prefix` where the statement joins, as any column is. */
|
|
260
294
|
textColumns(meta, keys, prefix) {
|
|
261
|
-
return keys.map((key) => this.columnWithPrefix(key, meta.fields[key], prefix));
|
|
295
|
+
return keys.map((key) => this.columnWithPrefix(key, meta.fields[key], { prefix }));
|
|
262
296
|
}
|
|
263
297
|
/** Ranks by the root `$text` of `where`, which is looked up only once a `$sort` asks for it. */
|
|
264
298
|
textRanker(meta, where, prefix) {
|
|
@@ -447,14 +481,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
447
481
|
}
|
|
448
482
|
this.getComparisonKey(ctx, entity, key, opts);
|
|
449
483
|
ctx.append(' = ');
|
|
450
|
-
this.getRawValue(ctx, { value: val
|
|
484
|
+
this.getRawValue(ctx, { ...opts, value: val });
|
|
451
485
|
return;
|
|
452
486
|
}
|
|
453
487
|
if (key === '$text') {
|
|
454
488
|
this.appendTextSearch(ctx, meta, val, opts.prefix);
|
|
455
489
|
return;
|
|
456
490
|
}
|
|
457
|
-
if (
|
|
491
|
+
if (isGroupOp(key)) {
|
|
458
492
|
this.compareLogicalOperator(ctx, entity, key, val, opts);
|
|
459
493
|
return;
|
|
460
494
|
}
|
|
@@ -477,27 +511,23 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
477
511
|
this.compareRelation(ctx, entity, key, val, rel, opts);
|
|
478
512
|
return;
|
|
479
513
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
ctx.append(AbstractSqlDialect.conjunction(parts));
|
|
483
|
-
}
|
|
484
|
-
/** Conditions joined by `AND`, parenthesized where there is more than one. */
|
|
485
|
-
static conjunction(parts) {
|
|
486
|
-
return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
|
|
514
|
+
const parts = whereOperators(val, 'unknown operator').map(([op, value]) => this.fieldCondition(ctx, entity, key, op, value, opts));
|
|
515
|
+
ctx.append(conjunction(parts));
|
|
487
516
|
}
|
|
488
517
|
compareLogicalOperator(ctx, entity, key, val, opts) {
|
|
489
|
-
const { join, negate } =
|
|
490
|
-
const items =
|
|
518
|
+
const { join, negate } = GROUP_OPS[key];
|
|
519
|
+
const items = groupClauses(key, val);
|
|
491
520
|
// With more than one item each is an operand of the operator joining them, so a compound item
|
|
492
521
|
// parenthesizes itself and precedence never applies; a lone item is this group verbatim, so it
|
|
493
522
|
// inherits the group's own position. A negation always makes its subject an operand.
|
|
494
523
|
const childOperand = items.length > 1 || negate || opts.operand;
|
|
495
524
|
const parts = this.renderOperands(ctx, items, (fragmentCtx, entry) => {
|
|
525
|
+
// The same scope as the group, so every render option carries over: a trigger's `NEW.` included.
|
|
496
526
|
if (entry instanceof QueryRaw) {
|
|
497
|
-
this.getRawValue(fragmentCtx, { value: entry
|
|
527
|
+
this.getRawValue(fragmentCtx, { ...opts, value: entry });
|
|
498
528
|
}
|
|
499
529
|
else {
|
|
500
|
-
this.renderWhere(fragmentCtx, entity, entry, {
|
|
530
|
+
this.renderWhere(fragmentCtx, entity, entry, { ...opts, operand: childOperand, clause: false });
|
|
501
531
|
}
|
|
502
532
|
});
|
|
503
533
|
if (!parts.length) {
|
|
@@ -509,40 +539,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
509
539
|
}
|
|
510
540
|
/** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
|
|
511
541
|
escapedColumns = new WeakMap();
|
|
512
|
-
static COMPARE_OP_MAP = new Map([
|
|
513
|
-
['$gt', ' > '],
|
|
514
|
-
['$gte', ' >= '],
|
|
515
|
-
['$lt', ' < '],
|
|
516
|
-
['$lte', ' <= '],
|
|
517
|
-
]);
|
|
518
|
-
/** What a `$near` says about the search itself; everything else in it is a bound. */
|
|
519
|
-
static VECTOR_QUERY_KEYS = new Set(VECTOR_QUERY_KEYS);
|
|
520
|
-
/**
|
|
521
|
-
* The ordered comparisons, `QueryOrderedOp` at runtime, derived from the map above rather than spelled
|
|
522
|
-
* again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
|
|
523
|
-
* for, and the operators that read a JSON path as a number.
|
|
524
|
-
*/
|
|
525
|
-
static ORDERED_OPS = new Set([
|
|
526
|
-
...AbstractSqlDialect.COMPARE_OP_MAP.keys(),
|
|
527
|
-
'$between',
|
|
528
|
-
]);
|
|
529
|
-
/** The operators an equality compares by value, which a JSON path reads the way that value compares. */
|
|
530
|
-
static EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
|
|
531
|
-
/**
|
|
532
|
-
* Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
|
|
533
|
-
* Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
|
|
534
|
-
* two can never drift apart - and neither one decides case folding, which is
|
|
535
|
-
* {@link caseInsensitiveMatch}'s single call.
|
|
536
|
-
*/
|
|
537
|
-
static LIKE_OPS = new Map([
|
|
538
|
-
['$like', '$ilike', (v) => v],
|
|
539
|
-
['$startsWith', '$istartsWith', (v) => `${v}%`],
|
|
540
|
-
['$endsWith', '$iendsWith', (v) => `%${v}`],
|
|
541
|
-
['$includes', '$iincludes', (v) => `%${v}%`],
|
|
542
|
-
].flatMap(([sensitive, insensitive, pattern]) => [
|
|
543
|
-
[sensitive, { pattern, insensitive: false }],
|
|
544
|
-
[insensitive, { pattern, insensitive: true }],
|
|
545
|
-
]));
|
|
546
542
|
/**
|
|
547
543
|
* How the engine matches case-insensitively: `ilike` has the operator, `native` ignores case already
|
|
548
544
|
* (SQLite, where folding in JS would break non-ASCII), and `fold` lowers both sides.
|
|
@@ -554,7 +550,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
554
550
|
* compared against.
|
|
555
551
|
*/
|
|
556
552
|
likeCondition(ctx, operand, op, val) {
|
|
557
|
-
const like =
|
|
553
|
+
const like = LIKE_OPS.get(op);
|
|
558
554
|
if (!like) {
|
|
559
555
|
return undefined;
|
|
560
556
|
}
|
|
@@ -565,8 +561,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
565
561
|
return `${fold ? `LOWER(${operand})` : operand} ${matchOp} ${ph}`;
|
|
566
562
|
}
|
|
567
563
|
/** Builds `prefix.column` from an already-resolved field, through the same memo writes use. */
|
|
568
|
-
columnWithPrefix(key, field,
|
|
569
|
-
return this.escapeId(prefix, true, true) + this.escapedColumnOf(key, field);
|
|
564
|
+
columnWithPrefix(key, field, opts) {
|
|
565
|
+
return (opts.escapedPrefix ?? this.escapeId(opts.prefix, true, true)) + this.escapedColumnOf(key, field);
|
|
570
566
|
}
|
|
571
567
|
/**
|
|
572
568
|
* The SQL a field comparison reads its left-hand side from. An inlined field builds its expression
|
|
@@ -577,7 +573,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
577
573
|
const meta = getMeta(entity);
|
|
578
574
|
const field = meta.fields[key];
|
|
579
575
|
return (this.inlinedOperand(ctx, field, opts.prefix ?? this.resolveTableAlias(meta), entity) ??
|
|
580
|
-
this.columnWithPrefix(key, field, opts
|
|
576
|
+
this.columnWithPrefix(key, field, opts));
|
|
581
577
|
}
|
|
582
578
|
/**
|
|
583
579
|
* The expression an inlined computed field stands for, or nothing when the field is a real column.
|
|
@@ -608,10 +604,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
608
604
|
/**
|
|
609
605
|
* `<operand> <op> <value>` for every operator that needs only its left-hand SQL, shared by a column, a
|
|
610
606
|
* JSON path, a `HAVING` expression, a count and a distance; `undefined` for the rest. `bind` renders
|
|
611
|
-
* each compared value, a plain placeholder unless a JSON path reads it otherwise.
|
|
607
|
+
* each compared value, a plain placeholder unless a JSON path reads it otherwise. NULL compares as the
|
|
608
|
+
* engine compares it: `<>`, `NOT IN` and `NOT` are unknown on a NULL, which SQL drops.
|
|
612
609
|
*/
|
|
613
610
|
operatorCondition(ctx, operand, op, val, bind = (value) => this.addValue(ctx, value)) {
|
|
614
|
-
const compareOp =
|
|
611
|
+
const compareOp = COMPARE_OPS.get(op);
|
|
615
612
|
if (compareOp) {
|
|
616
613
|
return `${operand}${compareOp}${bind(val)}`;
|
|
617
614
|
}
|
|
@@ -623,14 +620,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
623
620
|
case '$eq':
|
|
624
621
|
return val === null ? `${operand} IS NULL` : `${operand} = ${bind(val)}`;
|
|
625
622
|
case '$ne':
|
|
626
|
-
return val === null ? `${operand} IS NOT NULL` :
|
|
623
|
+
return val === null ? `${operand} IS NOT NULL` : `${operand} <> ${bind(val)}`;
|
|
627
624
|
case '$regex':
|
|
628
625
|
return this.regexCondition(operand, this.addValue(ctx, val));
|
|
629
626
|
case '$in':
|
|
630
627
|
case '$nin':
|
|
631
628
|
return this.formatIn(ctx, operand, inOperands(op, val), op === '$nin', bind);
|
|
632
629
|
case '$between': {
|
|
633
|
-
const [min, max] = val;
|
|
630
|
+
const [min, max] = betweenBounds(val);
|
|
634
631
|
return `${operand} BETWEEN ${bind(min)} AND ${bind(max)}`;
|
|
635
632
|
}
|
|
636
633
|
case '$isNull':
|
|
@@ -660,8 +657,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
660
657
|
}
|
|
661
658
|
/** Every operator `target` is compared with, `AND`-joined. */
|
|
662
659
|
jsonConditions(ctx, target, val) {
|
|
663
|
-
|
|
664
|
-
return AbstractSqlDialect.conjunction(getKeys(value).map((op) => this.jsonCondition(ctx, target, op, value[op])));
|
|
660
|
+
return conjunction(whereOperators(val, 'unknown operator').map(([op, value]) => this.jsonCondition(ctx, target, op, value)));
|
|
665
661
|
}
|
|
666
662
|
jsonCondition(ctx, target, op, value) {
|
|
667
663
|
if (op === '$not') {
|
|
@@ -671,7 +667,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
671
667
|
if (array !== undefined) {
|
|
672
668
|
return array;
|
|
673
669
|
}
|
|
674
|
-
const mode =
|
|
670
|
+
const mode = jsonOperatorMode(op, value);
|
|
675
671
|
const operand = target.read(mode);
|
|
676
672
|
// Only a boolean compares as a JSON value, so the set holds two at most, and MySQL documents `IN()`
|
|
677
673
|
// as unsupported on JSON values: the comparisons are spelled out.
|
|
@@ -683,16 +679,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
683
679
|
const condition = this.operatorCondition(ctx, operand, op, value, (val) => this.jsonOperand(ctx, val, mode));
|
|
684
680
|
return orRefuse(condition, `unknown operator: ${op}`);
|
|
685
681
|
}
|
|
686
|
-
/**
|
|
687
|
-
* How `op` reads a JSON value: an ordered comparison as a number, an equality as its operand compares,
|
|
688
|
-
* and a pattern or a null check as text.
|
|
689
|
-
*/
|
|
690
|
-
static jsonOperatorMode(op, value) {
|
|
691
|
-
if (AbstractSqlDialect.ORDERED_OPS.has(op)) {
|
|
692
|
-
return 'numeric';
|
|
693
|
-
}
|
|
694
|
-
return AbstractSqlDialect.EQUALITY_OPS.has(op) ? jsonCompareMode(value) : 'text';
|
|
695
|
-
}
|
|
696
682
|
/** A bound operand of a JSON comparison, read the way `mode` reads the value it is compared with. */
|
|
697
683
|
jsonOperand(ctx, value, mode) {
|
|
698
684
|
if (mode === 'json') {
|
|
@@ -731,7 +717,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
731
717
|
: [this.jsonElemEquals(ctx, slot, alias, value)];
|
|
732
718
|
return jsonElemExists(from, holds, this.jsonElemHint);
|
|
733
719
|
});
|
|
734
|
-
return
|
|
720
|
+
return conjunction(conditions);
|
|
735
721
|
}
|
|
736
722
|
/**
|
|
737
723
|
* What the JSON `target` reads satisfies to hold `value`: an array each of its elements, an object each
|
|
@@ -897,7 +883,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
897
883
|
const field = meta.fields[key];
|
|
898
884
|
if (field) {
|
|
899
885
|
const expr = this.inlinedOperand(ctx, field, prefix ?? this.resolveTableAlias(meta), meta.entity) ??
|
|
900
|
-
this.columnWithPrefix(key, field, prefix);
|
|
886
|
+
this.columnWithPrefix(key, field, { prefix });
|
|
901
887
|
return [{ expr, output: false }];
|
|
902
888
|
}
|
|
903
889
|
const json = this.resolveJsonDotPath(meta, key, prefix);
|
|
@@ -1007,14 +993,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1007
993
|
estimatedCount(_ctx, _entity) {
|
|
1008
994
|
throw new UqlUsageError(`${this.dialectName} does not support estimatedCount`);
|
|
1009
995
|
}
|
|
1010
|
-
/** `$group` aggregate operator to SQL function name, over ops `resolveAggregateOp` has already allowlisted. */
|
|
1011
|
-
static AGGREGATE_FN = {
|
|
1012
|
-
$count: 'COUNT',
|
|
1013
|
-
$sum: 'SUM',
|
|
1014
|
-
$avg: 'AVG',
|
|
1015
|
-
$min: 'MIN',
|
|
1016
|
-
$max: 'MAX',
|
|
1017
|
-
};
|
|
1018
996
|
aggregate(ctx, entity, q, opts = {}) {
|
|
1019
997
|
const meta = getMeta(entity);
|
|
1020
998
|
const entries = parseGroupMap(q.$group, q.$select);
|
|
@@ -1121,18 +1099,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1121
1099
|
this.havingCondition(ctx, this.aggregateRef(emittedColumns, alias, '$having'), condition);
|
|
1122
1100
|
});
|
|
1123
1101
|
}
|
|
1124
|
-
static SORT_DIRECTION_MAP = new Map([
|
|
1125
|
-
[1, {}],
|
|
1126
|
-
['asc', {}],
|
|
1127
|
-
['desc', { direction: ' DESC' }],
|
|
1128
|
-
[-1, { direction: ' DESC' }],
|
|
1129
|
-
['ascNullsFirst', { nulls: 'first' }],
|
|
1130
|
-
['ascNullsLast', { nulls: 'last' }],
|
|
1131
|
-
['descNullsFirst', { direction: ' DESC', nulls: 'first' }],
|
|
1132
|
-
['descNullsLast', { direction: ' DESC', nulls: 'last' }],
|
|
1133
|
-
]);
|
|
1134
1102
|
resolveSortDirection(sort) {
|
|
1135
|
-
const order =
|
|
1103
|
+
const order = SORT_DIRECTION_MAP.get(sort);
|
|
1136
1104
|
return orRefuse(order, `unknown sort direction: ${sort}`);
|
|
1137
1105
|
}
|
|
1138
1106
|
/**
|
|
@@ -1155,8 +1123,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1155
1123
|
}
|
|
1156
1124
|
/** Every operator of one `HAVING` condition, `AND`-joined. */
|
|
1157
1125
|
havingCondition(ctx, expr, condition) {
|
|
1158
|
-
const
|
|
1159
|
-
const parts =
|
|
1126
|
+
const refusal = 'unsupported HAVING operator';
|
|
1127
|
+
const parts = whereOperators(condition, refusal).map(([op, value]) => orRefuse(this.operatorCondition(ctx, expr, op, value), `${refusal}: ${op}`));
|
|
1160
1128
|
ctx.append(parts.join(' AND '));
|
|
1161
1129
|
}
|
|
1162
1130
|
/**
|
|
@@ -1620,17 +1588,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1620
1588
|
? this.numericCast(this.jsonPathReading(escapedColumn, path, 'text'))
|
|
1621
1589
|
: this.jsonPathReading(escapedColumn, path, mode);
|
|
1622
1590
|
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Normalizes a raw WHERE value into an operator map.
|
|
1625
|
-
* Arrays become `$in`, operator maps pass through, everything else becomes `$eq`.
|
|
1626
|
-
*/
|
|
1627
|
-
normalizeWhereValue(val) {
|
|
1628
|
-
if (Array.isArray(val))
|
|
1629
|
-
return { $in: val };
|
|
1630
|
-
if (isOperatorMap(val))
|
|
1631
|
-
return val;
|
|
1632
|
-
return { $eq: val };
|
|
1633
|
-
}
|
|
1634
1591
|
/**
|
|
1635
1592
|
* A field key's mapped column (`@Field({ name })`), escaped, memoized per dialect instance: field
|
|
1636
1593
|
* metadata is shared between dialects while this result is not, since `escapeIdChar` and the naming
|
|
@@ -1759,7 +1716,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1759
1716
|
}
|
|
1760
1717
|
/** One aggregate function call, the one spelling every statement that aggregates writes. */
|
|
1761
1718
|
aggregateFn(op, operand, distinct) {
|
|
1762
|
-
return `${
|
|
1719
|
+
return `${AGGREGATE_FN[op]}(${distinct ? 'DISTINCT ' : ''}${operand})`;
|
|
1763
1720
|
}
|
|
1764
1721
|
/**
|
|
1765
1722
|
* One equality per key of the parent, anded: a composite correlates on every column, and matching on
|
|
@@ -1901,45 +1858,31 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1901
1858
|
const parts = Object.entries(bounds)
|
|
1902
1859
|
.filter(([, val]) => val !== undefined)
|
|
1903
1860
|
.map(([op, val]) => orRefuse(condition(this.buildFragment(ctx, expr), op, val), `${refusal}: ${op}`));
|
|
1904
|
-
return
|
|
1861
|
+
return conjunction(parts);
|
|
1905
1862
|
}
|
|
1906
|
-
/**
|
|
1907
|
-
* A count compared with `size`, a number or its bounds. A count is never NULL, so its equality stays
|
|
1908
|
-
* plain rather than the null-safe `$ne` (`IS DISTINCT FROM`, `IS NOT`): same rows, shorter SQL.
|
|
1909
|
-
*/
|
|
1863
|
+
/** A count compared with `size`, a number or its bounds. */
|
|
1910
1864
|
sizeCondition(ctx, count, size) {
|
|
1911
1865
|
const bounds = typeof size === 'number' ? { $eq: size } : size;
|
|
1912
|
-
return this.boundConditions(ctx, count, bounds, (operand, op, val) =>
|
|
1913
|
-
if (op === '$eq' || op === '$ne') {
|
|
1914
|
-
return `${operand} ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx, val)}`;
|
|
1915
|
-
}
|
|
1916
|
-
return AbstractSqlDialect.ORDERED_OPS.has(op) ? this.operatorCondition(ctx, operand, op, val) : undefined;
|
|
1917
|
-
}, 'unsupported $size comparison operator');
|
|
1866
|
+
return this.boundConditions(ctx, count, bounds, (operand, op, val) => op === '$eq' || op === '$ne' || isOrderedOp(op) ? this.operatorCondition(ctx, operand, op, val) : undefined, 'unsupported $size comparison operator');
|
|
1918
1867
|
}
|
|
1919
1868
|
/** `<distance> <op> ?`, the `$where` half of a vector search, its bounds checked here since `/http` input is untyped. */
|
|
1920
1869
|
vectorNearCondition(ctx, meta, key, near, prefix) {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
if (AbstractSqlDialect.VECTOR_QUERY_KEYS.has(op) || val === undefined) {
|
|
1924
|
-
continue;
|
|
1925
|
-
}
|
|
1926
|
-
if (!AbstractSqlDialect.ORDERED_OPS.has(op)) {
|
|
1927
|
-
throw new UqlUsageError(`unsupported $near bound: ${op}`);
|
|
1928
|
-
}
|
|
1929
|
-
bounds[op] = val;
|
|
1870
|
+
if (!isOperatorMap(near)) {
|
|
1871
|
+
throw new UqlUsageError(`$near on '${key}' expects an object of its $vector and bounds, got ${kindOf(near)}`);
|
|
1930
1872
|
}
|
|
1873
|
+
const bounds = Object.fromEntries(Object.entries(near).filter(([op, val]) => !VECTOR_QUERY_KEY_SET.has(op) && val !== undefined));
|
|
1931
1874
|
if (!hasKeys(bounds)) {
|
|
1932
|
-
const boundOps = [...
|
|
1875
|
+
const boundOps = [...ORDERED_OPS].join(', ');
|
|
1933
1876
|
throw new UqlUsageError(`$near on '${key}' needs a bound (${boundOps}); without one it filters nothing`);
|
|
1934
1877
|
}
|
|
1935
1878
|
// Required by the type, so this only fires for a query that never met it: `/http` casts client
|
|
1936
1879
|
// JSON straight to `Query`. A `$near` never borrows the `$sort`'s vector, which is what keeps the
|
|
1937
1880
|
// predicate meaning the same thing in a `count`, or in an entity filter merged into a `$where`.
|
|
1938
|
-
if (!near
|
|
1881
|
+
if (!isVectorQuery(near)) {
|
|
1939
1882
|
throw new UqlUsageError(`$near on '${key}' needs its own $vector`);
|
|
1940
1883
|
}
|
|
1941
1884
|
const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, near, prefix);
|
|
1942
|
-
return this.boundConditions(ctx, distance, bounds, (operand, op, val) => this.operatorCondition(ctx, operand, op, val), 'unsupported $near bound');
|
|
1885
|
+
return this.boundConditions(ctx, distance, bounds, (operand, op, val) => (isOrderedOp(op) ? this.operatorCondition(ctx, operand, op, val) : undefined), 'unsupported $near bound');
|
|
1943
1886
|
}
|
|
1944
1887
|
/** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
|
|
1945
1888
|
escape(value) {
|
|
@@ -1960,16 +1903,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1960
1903
|
get likeFn() {
|
|
1961
1904
|
return 'LIKE';
|
|
1962
1905
|
}
|
|
1963
|
-
/**
|
|
1964
|
-
* Not-equal operator token for non-null comparisons.
|
|
1965
|
-
* Postgres uses `IS DISTINCT FROM`; MySQL/Maria uses custom `neExpr`.
|
|
1966
|
-
*/
|
|
1967
|
-
get neOp() {
|
|
1968
|
-
return '<>';
|
|
1969
|
-
}
|
|
1970
|
-
neExpr(field, ph) {
|
|
1971
|
-
return `${field} ${this.neOp} ${ph}`;
|
|
1972
|
-
}
|
|
1973
1906
|
/** `operand IN (...)` of each value as `bind` renders it, or the constant an empty set reduces to: no value is in it. */
|
|
1974
1907
|
formatIn(_ctx, operand, values, negate, bind) {
|
|
1975
1908
|
if (!values.length) {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { RelationAggregateSpec } from '../type/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* What every schema object uql creates is named from, so a hand-written trigger or function is never
|
|
4
|
+
* offered for dropping: ownership is the name, since no engine records who created one.
|
|
5
|
+
*/
|
|
6
|
+
export declare const OWNED_PREFIX = "_uql";
|
|
2
7
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
3
8
|
export declare const TOTAL_ALIAS = "_uql_total";
|
|
4
9
|
/**
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Every identifier UQL invents, `_uql`-prefixed to stay off a user's own, collected in one place:
|
|
2
2
|
// the ends writing and reading one sit in different modules, and a drift between them fails silently.
|
|
3
|
+
/**
|
|
4
|
+
* What every schema object uql creates is named from, so a hand-written trigger or function is never
|
|
5
|
+
* offered for dropping: ownership is the name, since no engine records who created one.
|
|
6
|
+
*/
|
|
7
|
+
export const OWNED_PREFIX = '_uql';
|
|
3
8
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
4
9
|
export const TOTAL_ALIAS = '_uql_total';
|
|
5
10
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, QueryTextSearchOptions, RowLockFeatures, SqlDialectFeatures, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, QueryTextSearchOptions, RowLockFeatures, SqlDialectFeatures, SqlDialectName, Type } from '../type/index.js';
|
|
2
2
|
import { AbstractSqlDialect, type DerivedRelation, type RelationRows } from './abstractSqlDialect.js';
|
|
3
3
|
import { type JsonSlot } from './jsonSql.js';
|
|
4
4
|
/** What the MySQL-family engines have. */
|
|
@@ -7,6 +7,8 @@ export declare const MYSQL_ROW_LOCKS: RowLockFeatures;
|
|
|
7
7
|
export declare const MYSQL_FEATURES: SqlDialectFeatures;
|
|
8
8
|
/** What MySQL and MariaDB share, their JSON functions above all: `JSON_LENGTH`, `JSON_CONTAINS`, `JSON_TABLE`, `JSON_SET`. */
|
|
9
9
|
export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
10
|
+
/** Every member of this family runs the same SQL, so a body written once serves them all. */
|
|
11
|
+
get dialectFamily(): SqlDialectName;
|
|
10
12
|
readonly features: SqlDialectFeatures;
|
|
11
13
|
/**
|
|
12
14
|
* `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
|
|
@@ -89,7 +91,7 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
89
91
|
protected appendTextScore<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>, keys: readonly string[], prefix: string | undefined): void;
|
|
90
92
|
/** `DOUBLE`, never a bare `DECIMAL`, which is `DECIMAL(10,0)` and rounds `1.4` to `1`. */
|
|
91
93
|
protected numericCast(expr: string): string;
|
|
92
|
-
|
|
94
|
+
neExpr(field: string, ph: string): string;
|
|
93
95
|
/**
|
|
94
96
|
* Omitting the `COALESCE` on a NOT NULL column keeps MySQL's partial in-place JSON update
|
|
95
97
|
* applicable: it requires the target column as the direct `JSON_SET` input.
|
|
@@ -15,7 +15,6 @@ const MAX_LIMIT = BigInt.asUintN(64, -1n);
|
|
|
15
15
|
/** Declared apart so MariaDB, which has no `FOR UPDATE OF`, can restate that one part of it. */
|
|
16
16
|
export const MYSQL_ROW_LOCKS = { of: true, withWindow: true, placement: 'suffix' };
|
|
17
17
|
export const MYSQL_FEATURES = {
|
|
18
|
-
ifNotExists: true,
|
|
19
18
|
indexIfNotExists: false,
|
|
20
19
|
schemas: true,
|
|
21
20
|
dropTableCascade: false,
|
|
@@ -39,11 +38,25 @@ export const MYSQL_FEATURES = {
|
|
|
39
38
|
narrowVectorTypes: false,
|
|
40
39
|
vectorTuningNeedsTransaction: false,
|
|
41
40
|
serialDeclaresPrimaryKey: false,
|
|
41
|
+
triggers: {
|
|
42
|
+
preamble: '',
|
|
43
|
+
assignsRow: true,
|
|
44
|
+
body: 'inline',
|
|
45
|
+
guards: 'thenEndIf',
|
|
46
|
+
layout: 'timingFirst',
|
|
47
|
+
rows: 'row',
|
|
48
|
+
scope: 'schema',
|
|
49
|
+
before: true,
|
|
50
|
+
},
|
|
42
51
|
};
|
|
43
52
|
/** The one `JSON_TABLE` column an exploded array reads each element through, as a JSON document. */
|
|
44
53
|
const ELEM_COLUMN = 'v';
|
|
45
54
|
/** What MySQL and MariaDB share, their JSON functions above all: `JSON_LENGTH`, `JSON_CONTAINS`, `JSON_TABLE`, `JSON_SET`. */
|
|
46
55
|
export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
56
|
+
/** Every member of this family runs the same SQL, so a body written once serves them all. */
|
|
57
|
+
get dialectFamily() {
|
|
58
|
+
return 'mysql';
|
|
59
|
+
}
|
|
47
60
|
features = MYSQL_FEATURES;
|
|
48
61
|
/**
|
|
49
62
|
* `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type QueryGroupOp, type QueryOrderedOp, type QueryVectorQuery, type QueryWhereArray, type QueryWhereFieldOp } from '../type/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* How each grouping operator renders: the operator joining its clauses, and whether the group is
|
|
4
|
+
* negated (`$not` is `NOT (a AND b)`). Total over {@link QueryGroupOp}.
|
|
5
|
+
*/
|
|
6
|
+
export declare const GROUP_OPS: {
|
|
7
|
+
readonly $and: {
|
|
8
|
+
readonly join: '$and';
|
|
9
|
+
readonly negate: false;
|
|
10
|
+
};
|
|
11
|
+
readonly $or: {
|
|
12
|
+
readonly join: '$or';
|
|
13
|
+
readonly negate: false;
|
|
14
|
+
};
|
|
15
|
+
readonly $not: {
|
|
16
|
+
readonly join: '$and';
|
|
17
|
+
readonly negate: true;
|
|
18
|
+
};
|
|
19
|
+
readonly $nor: {
|
|
20
|
+
readonly join: '$or';
|
|
21
|
+
readonly negate: true;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** Whether a `$where` key groups clauses, narrowing it for the renderers that read {@link GROUP_OPS}. */
|
|
25
|
+
export declare function isGroupOp(key: string): key is QueryGroupOp;
|
|
26
|
+
/**
|
|
27
|
+
* A group operator's clauses, rejecting what the types do not cover: `/http` casts client JSON
|
|
28
|
+
* straight to `Query`, so a scalar can arrive where an array belongs. Shared so both backends
|
|
29
|
+
* refuse the same payload rather than one throwing and the other failing further in.
|
|
30
|
+
*/
|
|
31
|
+
export declare function groupClauses<E>(key: QueryGroupOp, val: QueryWhereArray<E> | undefined): QueryWhereArray<E>;
|
|
32
|
+
/**
|
|
33
|
+
* A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
|
|
34
|
+
* anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
|
|
35
|
+
*/
|
|
36
|
+
export declare function whereOperators(val: unknown, refusal: string): [QueryWhereFieldOp, unknown][];
|
|
37
|
+
/** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
|
|
38
|
+
export declare function inOperands(op: string, value: unknown): unknown[];
|
|
39
|
+
/** A `$between`'s two bounds, which `/http` hands over untyped too; one missing would bind `undefined`. */
|
|
40
|
+
export declare function betweenBounds(value: unknown): readonly [unknown, unknown];
|
|
41
|
+
/** Whether a `$near` names the vector it measures from, which its type requires and `/http` does not. */
|
|
42
|
+
export declare function isVectorQuery(near: Record<string, unknown>): near is Record<string, unknown> & QueryVectorQuery;
|
|
43
|
+
/** One entry of {@link LIKE_OPS}: how the pattern is built, and whether it ignores case. */
|
|
44
|
+
export type LikeOp = {
|
|
45
|
+
readonly pattern: (value: string) => string;
|
|
46
|
+
readonly insensitive: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare const COMPARE_OPS: ReadonlyMap<QueryWhereFieldOp, string>;
|
|
49
|
+
/**
|
|
50
|
+
* The ordered comparisons, `QueryOrderedOp` at runtime, derived from {@link COMPARE_OPS} rather than spelled
|
|
51
|
+
* again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
|
|
52
|
+
* for, and the operators that read a JSON path as a number.
|
|
53
|
+
*/
|
|
54
|
+
export declare const ORDERED_OPS: ReadonlySet<string>;
|
|
55
|
+
export declare function isOrderedOp(op: string): op is QueryOrderedOp;
|
|
56
|
+
/** The operators an equality compares by value, which a JSON path reads the way that value compares. */
|
|
57
|
+
export declare const EQUALITY_OPS: ReadonlySet<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
|
|
60
|
+
* Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
|
|
61
|
+
* two can never drift apart - and neither one decides case folding, which is
|
|
62
|
+
* `AbstractSqlDialect.caseInsensitiveMatch`'s single call.
|
|
63
|
+
*/
|
|
64
|
+
export declare const LIKE_OPS: ReadonlyMap<string, LikeOp>;
|
|
65
|
+
/** What a `$near` says about the search itself; everything else in it is a bound. */
|
|
66
|
+
export declare const VECTOR_QUERY_KEY_SET: ReadonlySet<string>;
|