uql-orm 0.24.6 → 0.25.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/README.md +9 -8
- package/dist/cockroachdb/crdbQuerierPool.d.ts +1 -1
- package/dist/cockroachdb/crdbQuerierPool.js +5 -4
- package/dist/dialect/abstractSqlDialect.d.ts +78 -27
- package/dist/dialect/abstractSqlDialect.js +194 -130
- package/dist/dialect/hydrateColumn.d.ts +16 -0
- package/dist/dialect/hydrateColumn.js +66 -0
- package/dist/dialect/jsonSql.d.ts +24 -0
- package/dist/dialect/jsonSql.js +39 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +10 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +3 -7
- package/dist/dialect/pgLikeSqlDialect.js +2 -14
- package/dist/dialect/vectorCast.d.ts +15 -0
- package/dist/dialect/vectorCast.js +58 -0
- package/dist/entity/metadata/definition.d.ts +0 -1
- package/dist/entity/metadata/definition.js +1 -1
- package/dist/maria/mariaDialect.d.ts +3 -2
- package/dist/maria/mariaDialect.js +3 -18
- package/dist/maria/mariadbQuerierPool.js +6 -1
- package/dist/migrate/builder/migrationBuilder.d.ts +12 -16
- package/dist/migrate/builder/migrationBuilder.js +24 -59
- package/dist/migrate/builder/tableBuilder.js +0 -12
- package/dist/migrate/cli.d.ts +0 -1
- package/dist/migrate/cli.js +1 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +0 -3
- package/dist/migrate/drift/driftDetector.js +17 -15
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +9 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +18 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +8 -2
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +10 -9
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +0 -3
- package/dist/migrate/introspection/mysqlIntrospector.js +0 -9
- package/dist/migrate/introspection/postgresIntrospector.d.ts +0 -3
- package/dist/migrate/introspection/postgresIntrospector.js +0 -12
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +1 -9
- package/dist/migrate/migrator.d.ts +17 -0
- package/dist/migrate/migrator.js +51 -50
- package/dist/migrate/schemaGenerator.d.ts +19 -8
- package/dist/migrate/schemaGenerator.js +48 -17
- package/dist/neon/neonQuerierPool.d.ts +1 -1
- package/dist/neon/neonQuerierPool.js +6 -4
- package/dist/postgres/abstractPgQuerierPool.d.ts +6 -0
- package/dist/postgres/abstractPgQuerierPool.js +3 -0
- package/dist/postgres/pgNumericTypes.d.ts +41 -0
- package/dist/postgres/pgNumericTypes.js +35 -0
- package/dist/postgres/pgQuerierPool.d.ts +1 -1
- package/dist/postgres/pgQuerierPool.js +5 -4
- package/dist/querier/abstractSqlQuerier.d.ts +9 -2
- package/dist/querier/abstractSqlQuerier.js +34 -25
- package/dist/schema/canonicalType.js +2 -12
- package/dist/schema/schemaAST.js +0 -24
- package/dist/schema/schemaASTBuilder.d.ts +1 -1
- package/dist/schema/schemaASTBuilder.js +4 -5
- package/dist/sqlite/nodeSqliteQuerierPool.js +1 -0
- package/dist/sqlite/sqliteQuerierPool.d.ts +5 -0
- package/dist/sqlite/sqliteQuerierPool.js +7 -0
- package/dist/turso/tursoLocalQuerierPool.js +1 -0
- package/dist/type/entity.d.ts +16 -2
- package/dist/type/migration.d.ts +30 -9
- package/dist/type/queryAggregate.d.ts +3 -0
- package/dist/util/field.util.d.ts +4 -0
- package/dist/util/field.util.js +12 -0
- package/dist/util/sqlLiteral.js +18 -15
- package/package.json +6 -6
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
2
|
import { QueryRaw, RAW_ALIAS, RAW_VALUE, } from '../type/index.js';
|
|
3
|
-
import { asSelectMap, buildQueryWhereAsMap, buildSortMap, escapeSqlId, fillOnFields, filterFieldKeys, flatObject, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, hasMultipleKeys, isJsonType, isJsonUpdateOp, isOperatorObject, isOperatorOnlyObject, isPopulatingRelations, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationAtKey, parseRelationSize, raw, someValue, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
|
+
import { asSelectMap, buildQueryWhereAsMap, buildSortMap, escapeSqlId, fillOnFields, filterFieldKeys, flatObject, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, hasMultipleKeys, isBooleanType, isJsonType, isJsonUpdateOp, isNumericType, isOperatorObject, isOperatorOnlyObject, isPopulatingRelations, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationAtKey, parseRelationSize, raw, someValue, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
4
|
import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
5
5
|
import { IndexSqlDialect } from './indexSqlDialect.js';
|
|
6
6
|
import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
|
|
7
|
-
import { JSON_ELEM_ALIAS_PREFIX, jsonElemExists } from './jsonSql.js';
|
|
7
|
+
import { isJsonbOp, JSON_ELEM_ALIAS_PREFIX, jsonCompareMode, jsonElemExists } from './jsonSql.js';
|
|
8
8
|
import { SqlQueryContext } from './queryContext.js';
|
|
9
|
-
import { isVectorFieldType } from './vectorCast.js';
|
|
9
|
+
import { isVectorFieldType, resolveVectorCast } from './vectorCast.js';
|
|
10
10
|
export class AbstractSqlDialect extends IndexSqlDialect {
|
|
11
11
|
isolationLevelStrategy = 'inline';
|
|
12
12
|
alterColumnStrategy = 'single-statement';
|
|
@@ -433,44 +433,15 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
433
433
|
}
|
|
434
434
|
compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
|
|
435
435
|
const field = this.resolveOperandField(ctx, entity, key, opts);
|
|
436
|
-
|
|
437
|
-
if (simpleOp) {
|
|
438
|
-
this.appendFieldSql(ctx, field, `${simpleOp}${this.addValue(ctx.values, val)}`);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const likeWrap = AbstractSqlDialect.LIKE_OP_MAP.get(op);
|
|
442
|
-
if (likeWrap) {
|
|
443
|
-
this.appendLikeOp(ctx, field, op, likeWrap(val));
|
|
436
|
+
if (this.appendOperatorCondition(ctx, field, op, val)) {
|
|
444
437
|
return;
|
|
445
438
|
}
|
|
446
439
|
switch (op) {
|
|
447
|
-
case '$eq':
|
|
448
|
-
case '$ne':
|
|
449
|
-
this.appendEqNe(ctx, field, op, val);
|
|
450
|
-
break;
|
|
451
|
-
case '$regex':
|
|
452
|
-
this.appendFieldSql(ctx, field, ` ${this.regexpOp} ${this.addValue(ctx.values, val)}`);
|
|
453
|
-
break;
|
|
454
440
|
case '$not':
|
|
455
441
|
ctx.append('NOT (');
|
|
456
442
|
this.compare(ctx, entity, key, val, opts);
|
|
457
443
|
ctx.append(')');
|
|
458
444
|
break;
|
|
459
|
-
case '$in':
|
|
460
|
-
case '$nin':
|
|
461
|
-
this.appendInNin(ctx, field, op, val);
|
|
462
|
-
break;
|
|
463
|
-
case '$between': {
|
|
464
|
-
const [min, max] = val;
|
|
465
|
-
this.appendFieldSql(ctx, field, ` BETWEEN ${this.addValue(ctx.values, min)} AND ${this.addValue(ctx.values, max)}`);
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
case '$isNull':
|
|
469
|
-
this.appendFieldSql(ctx, field, val ? ' IS NULL' : ' IS NOT NULL');
|
|
470
|
-
break;
|
|
471
|
-
case '$isNotNull':
|
|
472
|
-
this.appendFieldSql(ctx, field, val ? ' IS NOT NULL' : ' IS NULL');
|
|
473
|
-
break;
|
|
474
445
|
case '$all':
|
|
475
446
|
ctx.append(this.jsonAll(ctx, field ?? '', val));
|
|
476
447
|
break;
|
|
@@ -484,6 +455,57 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
484
455
|
throw TypeError(`unknown operator: ${op}`);
|
|
485
456
|
}
|
|
486
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Render `<operand> <op> <value>` for every operator that needs nothing but its left-hand SQL, and
|
|
460
|
+
* report whether `op` was one of them.
|
|
461
|
+
*
|
|
462
|
+
* One implementation for three callers that each had their own: a WHERE column, a HAVING aggregate
|
|
463
|
+
* expression, and a `$size` count (which passes no operand, since its expression is already in the
|
|
464
|
+
* context). They previously disagreed - HAVING carried a second comparison-operator map and threw
|
|
465
|
+
* `unsupported HAVING operator` on the `$like` that `QueryHavingMap` accepts, and neither of the
|
|
466
|
+
* other two turned `$eq: null` into `IS NULL` the way the WHERE path does.
|
|
467
|
+
*
|
|
468
|
+
* The operators kept out are the ones that need more than an operand: `$not` recurses through the
|
|
469
|
+
* entity, and `$all`/`$size`/`$elemMatch` address a JSON document.
|
|
470
|
+
*/
|
|
471
|
+
appendOperatorCondition(ctx, operand, op, val) {
|
|
472
|
+
const simpleOp = AbstractSqlDialect.COMPARE_OP_MAP.get(op);
|
|
473
|
+
if (simpleOp) {
|
|
474
|
+
this.appendFieldSql(ctx, operand, `${simpleOp}${this.addValue(ctx.values, val)}`);
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
const likeWrap = AbstractSqlDialect.LIKE_OP_MAP.get(op);
|
|
478
|
+
if (likeWrap) {
|
|
479
|
+
this.appendLikeOp(ctx, operand, op, likeWrap(val));
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
switch (op) {
|
|
483
|
+
case '$eq':
|
|
484
|
+
case '$ne':
|
|
485
|
+
this.appendEqNe(ctx, operand, op, val);
|
|
486
|
+
return true;
|
|
487
|
+
case '$regex':
|
|
488
|
+
this.appendFieldSql(ctx, operand, ` ${this.regexpOp} ${this.addValue(ctx.values, val)}`);
|
|
489
|
+
return true;
|
|
490
|
+
case '$in':
|
|
491
|
+
case '$nin':
|
|
492
|
+
this.appendInNin(ctx, operand, op, val);
|
|
493
|
+
return true;
|
|
494
|
+
case '$between': {
|
|
495
|
+
const [min, max] = val;
|
|
496
|
+
this.appendFieldSql(ctx, operand, ` BETWEEN ${this.addValue(ctx.values, min)} AND ${this.addValue(ctx.values, max)}`);
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
case '$isNull':
|
|
500
|
+
this.appendFieldSql(ctx, operand, val ? ' IS NULL' : ' IS NOT NULL');
|
|
501
|
+
return true;
|
|
502
|
+
case '$isNotNull':
|
|
503
|
+
this.appendFieldSql(ctx, operand, val ? ' IS NOT NULL' : ' IS NULL');
|
|
504
|
+
return true;
|
|
505
|
+
default:
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
487
509
|
appendLikeOp(ctx, field, op, wrappedVal) {
|
|
488
510
|
const isIlike = AbstractSqlDialect.LIKE_CASE_INSENSITIVE_OPS.has(op);
|
|
489
511
|
const ph = this.addValue(ctx.values, wrappedVal);
|
|
@@ -512,7 +534,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
512
534
|
}
|
|
513
535
|
}
|
|
514
536
|
appendInNin(ctx, field, op, val) {
|
|
515
|
-
|
|
537
|
+
if (!Array.isArray(val)) {
|
|
538
|
+
// Not covered by the types: `/http` casts client JSON straight to `Query`, so this arrives untyped.
|
|
539
|
+
throw TypeError(`${op} expects an array, got ${val === null ? 'null' : typeof val}`);
|
|
540
|
+
}
|
|
541
|
+
this.appendFieldSql(ctx, field, this.formatIn(ctx, val, op === '$nin'));
|
|
516
542
|
}
|
|
517
543
|
/**
|
|
518
544
|
* Build a comparison condition for a JSON field.
|
|
@@ -588,29 +614,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
588
614
|
* {@link jsonCompareMode} for why each mode exists.
|
|
589
615
|
*/
|
|
590
616
|
jsonComparand(jsonField, value) {
|
|
591
|
-
return
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
|
|
595
|
-
* of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
|
|
596
|
-
* compared in the representation every engine agrees on:
|
|
597
|
-
* - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
|
|
598
|
-
* would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
|
|
599
|
-
* - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
|
|
600
|
-
* portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
|
|
601
|
-
* - `text` - compare as extracted, which is also what the string operators need.
|
|
602
|
-
*
|
|
603
|
-
* Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
|
|
604
|
-
*/
|
|
605
|
-
jsonCompareMode(value) {
|
|
606
|
-
const operands = Array.isArray(value) ? value : [value];
|
|
607
|
-
if (operands.length === 0) {
|
|
608
|
-
return 'text';
|
|
609
|
-
}
|
|
610
|
-
if (operands.every((operand) => typeof operand === 'boolean')) {
|
|
611
|
-
return 'json';
|
|
612
|
-
}
|
|
613
|
-
return operands.every((operand) => typeof operand === 'number') ? 'numeric' : 'text';
|
|
617
|
+
return jsonCompareMode(value) === 'numeric' ? this.numericCast(jsonField) : jsonField;
|
|
614
618
|
}
|
|
615
619
|
/**
|
|
616
620
|
* Whether the dialect's array containment ({@link jsonAll}) matches an object element that merely
|
|
@@ -638,7 +642,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
638
642
|
// only when *every* operand needs it - the same all-operands rule the comparison classifier uses.
|
|
639
643
|
if (isOperatorOnlyObject(match)) {
|
|
640
644
|
const entries = Object.entries(match);
|
|
641
|
-
const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) =>
|
|
645
|
+
const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) => isJsonbOp(op, val));
|
|
642
646
|
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
643
647
|
const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, () => this.jsonElemRef(alias, undefined, asJson), '', op, val, asJson));
|
|
644
648
|
return jsonElemExists(this.jsonElemFrom(jsonField, [], alias, asJson), conditions);
|
|
@@ -653,28 +657,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
653
657
|
}
|
|
654
658
|
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
655
659
|
const conditions = buildElemMatchConditions(match, (field, op, opVal) => {
|
|
656
|
-
const asJson =
|
|
660
|
+
const asJson = isJsonbOp(op, opVal);
|
|
657
661
|
return this.buildJsonFieldCondition(ctx, (f) => this.jsonElemRef(alias, f, asJson), field, op, opVal, asJson);
|
|
658
662
|
});
|
|
659
663
|
return jsonElemExists(this.jsonElemFrom(jsonField, Object.keys(match), alias), conditions);
|
|
660
664
|
}
|
|
661
|
-
/**
|
|
662
|
-
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
663
|
-
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
664
|
-
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
665
|
-
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
|
|
666
|
-
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
667
|
-
*
|
|
668
|
-
* Numbers stay on the text accessor with a numeric cast ({@link jsonComparand}), which keeps
|
|
669
|
-
* `1` equal to `1.0` - JSON equality would not.
|
|
670
|
-
*/
|
|
671
|
-
isJsonbOp(op, value) {
|
|
672
|
-
if (op === '$all' || op === '$size' || op === '$elemMatch') {
|
|
673
|
-
return true;
|
|
674
|
-
}
|
|
675
|
-
const comparesValue = op === '$eq' || op === '$ne' || op === '$in' || op === '$nin';
|
|
676
|
-
return comparesValue && this.jsonCompareMode(value) === 'json';
|
|
677
|
-
}
|
|
678
665
|
/**
|
|
679
666
|
* A JSON-encoded bound parameter, cast to the dialect's JSON type. Only the positional-placeholder
|
|
680
667
|
* dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
@@ -856,51 +843,17 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
856
843
|
return direction;
|
|
857
844
|
}
|
|
858
845
|
/** Scalar comparison operators shared by `HAVING` conditions and `$size` comparisons. */
|
|
859
|
-
static comparisonOpMap = new Map([
|
|
860
|
-
['$eq', '='],
|
|
861
|
-
['$ne', '<>'],
|
|
862
|
-
['$gt', '>'],
|
|
863
|
-
['$gte', '>='],
|
|
864
|
-
['$lt', '<'],
|
|
865
|
-
['$lte', '<='],
|
|
866
|
-
]);
|
|
867
846
|
havingCondition(ctx, expr, condition) {
|
|
868
847
|
if (typeof condition !== 'object' || condition === null) {
|
|
869
|
-
|
|
870
|
-
ctx.addValue(condition);
|
|
848
|
+
this.appendOperatorCondition(ctx, expr, '$eq', condition);
|
|
871
849
|
return;
|
|
872
850
|
}
|
|
873
851
|
const ops = condition;
|
|
874
|
-
|
|
875
|
-
keys.forEach((op, i) => {
|
|
852
|
+
getKeys(ops).forEach((op, i) => {
|
|
876
853
|
if (i > 0)
|
|
877
854
|
ctx.append(' AND ');
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const [min, max] = val;
|
|
881
|
-
ctx.append(`${expr} BETWEEN `);
|
|
882
|
-
ctx.addValue(min);
|
|
883
|
-
ctx.append(' AND ');
|
|
884
|
-
ctx.addValue(max);
|
|
885
|
-
}
|
|
886
|
-
else if (op === '$in' || op === '$nin') {
|
|
887
|
-
ctx.append(`${expr}${this.formatIn(ctx, Array.isArray(val) ? val : [], op === '$nin')}`);
|
|
888
|
-
}
|
|
889
|
-
else if (op === '$isNull') {
|
|
890
|
-
ctx.append(`${expr}${val ? ' IS NULL' : ' IS NOT NULL'}`);
|
|
891
|
-
}
|
|
892
|
-
else if (op === '$isNotNull') {
|
|
893
|
-
ctx.append(`${expr}${val ? ' IS NOT NULL' : ' IS NULL'}`);
|
|
894
|
-
}
|
|
895
|
-
else if (op === '$ne') {
|
|
896
|
-
ctx.append(this.neExpr(expr, this.addValue(ctx.values, val)));
|
|
897
|
-
}
|
|
898
|
-
else {
|
|
899
|
-
const sqlOp = AbstractSqlDialect.comparisonOpMap.get(op);
|
|
900
|
-
if (!sqlOp)
|
|
901
|
-
throw TypeError(`unsupported HAVING operator: ${op}`);
|
|
902
|
-
ctx.append(`${expr} ${sqlOp} `);
|
|
903
|
-
ctx.addValue(val);
|
|
855
|
+
if (!this.appendOperatorCondition(ctx, expr, op, ops[op])) {
|
|
856
|
+
throw TypeError(`unsupported HAVING operator: ${op}`);
|
|
904
857
|
}
|
|
905
858
|
});
|
|
906
859
|
}
|
|
@@ -1008,14 +961,25 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1008
961
|
*/
|
|
1009
962
|
upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
|
|
1010
963
|
const meta = getMeta(entity);
|
|
1011
|
-
const updateCtx = this.createContext();
|
|
964
|
+
const updateCtx = this.upsertUpdateBindsInPlace ? ctx : this.createContext();
|
|
1012
965
|
const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, this.upsertExcluded);
|
|
1013
966
|
const keys = this.getUpsertConflictPathsStr(meta, conflictPaths);
|
|
1014
967
|
const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
|
|
1015
968
|
this.appendInsertValues(ctx, entity, payload);
|
|
1016
969
|
ctx.append(` ON CONFLICT (${keys}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
|
|
1017
|
-
|
|
970
|
+
if (updateCtx !== ctx) {
|
|
971
|
+
ctx.pushValue(...updateCtx.values);
|
|
972
|
+
}
|
|
1018
973
|
}
|
|
974
|
+
/**
|
|
975
|
+
* Whether the upsert's update assignments can bind straight into the statement's own context.
|
|
976
|
+
*
|
|
977
|
+
* They cannot on a `?`-placeholder dialect: the assignments are built before the insert but read
|
|
978
|
+
* after it, so their values have to be pushed afterwards to land in the right positional order.
|
|
979
|
+
* A `$n` placeholder carries its own index, so there is nothing to reorder - but it also cannot use
|
|
980
|
+
* the scratch context, whose numbering would restart at `$1` and collide with the insert's.
|
|
981
|
+
*/
|
|
982
|
+
upsertUpdateBindsInPlace = false;
|
|
1019
983
|
/** How an `ON CONFLICT` assignment reads the row that was being inserted. */
|
|
1020
984
|
upsertExcluded = (columnName) => `EXCLUDED.${columnName}`;
|
|
1021
985
|
getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, callback) {
|
|
@@ -1089,6 +1053,95 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1089
1053
|
}
|
|
1090
1054
|
return isVectorFieldType(type) ? 'vector' : 'plain';
|
|
1091
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Which of an entity's columns need decoding on READ, and how: the inverse of {@link persistKind},
|
|
1058
|
+
* cached per entity for the same reason it classifies per column. A 1000-row read of a 10-field
|
|
1059
|
+
* entity otherwise asks `isJsonType` (which lowercases a string on every call) 10,000 times to get
|
|
1060
|
+
* the same ten answers. Most entities land here for their numeric columns alone, where the per-row
|
|
1061
|
+
* cost is one `typeof` against a value the driver usually decoded already.
|
|
1062
|
+
*
|
|
1063
|
+
* Dialect-aware exactly like {@link supportedVectorType}, because it has to be: a `sparsevec` field
|
|
1064
|
+
* is written as a plain dense vector everywhere but Postgres, so reading it back by the field's own
|
|
1065
|
+
* declared cast would look for a sparse literal that was never stored.
|
|
1066
|
+
*
|
|
1067
|
+
* A type lands here rather than at the driver when the wire type alone cannot decide it, and only
|
|
1068
|
+
* the declaration can: `Boolean` is 0/1 in a SQLite INTEGER and a MySQL `TINYINT(1)`, both
|
|
1069
|
+
* indistinguishable from a genuine small integer; a decimal is text from pg *and* mysql2, and only
|
|
1070
|
+
* the field says it was meant as a number; and `type: BigInt` shares BIGINT with `type: Number`, so
|
|
1071
|
+
* the wire decode has to be undone for it. All are no-ops where the driver already decoded.
|
|
1072
|
+
*
|
|
1073
|
+
* Classified through the same `isNumericType`/`isBooleanType`/`isJsonType` the rest of the library
|
|
1074
|
+
* uses, not against the constructors: `type` accepts a string logical type for every one of these
|
|
1075
|
+
* (`@Field({ type: 'decimal' })`), and matching `=== Number` alone left those reading back as text.
|
|
1076
|
+
*/
|
|
1077
|
+
hydratableFields(entity) {
|
|
1078
|
+
const cached = this.hydratable.get(entity);
|
|
1079
|
+
if (cached) {
|
|
1080
|
+
return cached;
|
|
1081
|
+
}
|
|
1082
|
+
const decoded = [];
|
|
1083
|
+
for (const [key, field] of Object.entries(getMeta(entity).fields)) {
|
|
1084
|
+
const kind = this.hydrateKind(field);
|
|
1085
|
+
if (kind) {
|
|
1086
|
+
decoded.push([key, kind]);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
this.hydratable.set(entity, decoded);
|
|
1090
|
+
return decoded;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* The same classification for an aggregate row. Not cached, because these columns are a shape of the
|
|
1094
|
+
* query rather than of the entity, and it is computed once per call either way.
|
|
1095
|
+
*
|
|
1096
|
+
* Mirrors `QueryAggregateFnResult`, which is the contract callers already compile against:
|
|
1097
|
+
* `$count`/`$sum`/`$avg` are a number whatever they aggregate, while `$min`/`$max` and every
|
|
1098
|
+
* `$group` column keep the aggregated field's own type, so they decode as that field would. Without
|
|
1099
|
+
* it a `$sum` over a BIGINT column came back as `'500'` from a result type that says `number`, since
|
|
1100
|
+
* Postgres widens that sum to NUMERIC and no driver can know it was meant as a JS number.
|
|
1101
|
+
*/
|
|
1102
|
+
hydratableAggregates(entity, q) {
|
|
1103
|
+
const { fields } = getMeta(entity);
|
|
1104
|
+
const decoded = [];
|
|
1105
|
+
for (const entry of parseGroupMap(q.$group, q.$agg)) {
|
|
1106
|
+
if (entry.kind === 'fn' && entry.op !== '$min' && entry.op !== '$max') {
|
|
1107
|
+
decoded.push([entry.alias, 'number']);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
// `$min`/`$max` read the field they aggregate; a `$group` column is that field. Only `$count`
|
|
1111
|
+
// takes `'*'`, and it went down the numeric path above, so there is always a field to look up.
|
|
1112
|
+
const key = entry.kind === 'fn' ? entry.fieldRef : entry.alias;
|
|
1113
|
+
const kind = this.hydrateKind(fields[key]);
|
|
1114
|
+
if (kind) {
|
|
1115
|
+
decoded.push([entry.alias, kind]);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return decoded;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode.
|
|
1122
|
+
*
|
|
1123
|
+
* Ordered for correctness, not for speed - this runs once per entity, cached, never per row. The
|
|
1124
|
+
* one order that is load-bearing is `BigInt` before {@link isNumericType}, which answers true for
|
|
1125
|
+
* `BigInt` as well as `Number`: swap them and every `type: BigInt` property silently decodes to a
|
|
1126
|
+
* JS number again.
|
|
1127
|
+
*/
|
|
1128
|
+
hydrateKind(field) {
|
|
1129
|
+
const type = field?.type;
|
|
1130
|
+
if (isJsonType(type)) {
|
|
1131
|
+
return 'json';
|
|
1132
|
+
}
|
|
1133
|
+
if (isVectorFieldType(type)) {
|
|
1134
|
+
return this.supportedVectorType(resolveVectorCast(field));
|
|
1135
|
+
}
|
|
1136
|
+
if (isBooleanType(type)) {
|
|
1137
|
+
return 'boolean';
|
|
1138
|
+
}
|
|
1139
|
+
if (type === BigInt) {
|
|
1140
|
+
return 'bigint';
|
|
1141
|
+
}
|
|
1142
|
+
return isNumericType(type) ? 'number' : undefined;
|
|
1143
|
+
}
|
|
1144
|
+
hydratable = new WeakMap();
|
|
1092
1145
|
/** The one type dispatch for a persisted value, over a column kind decided by the caller. */
|
|
1093
1146
|
writePersistableValue(ctx, kind, field, value) {
|
|
1094
1147
|
if (value instanceof QueryRaw) {
|
|
@@ -1218,7 +1271,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1218
1271
|
operators.forEach((op, index) => {
|
|
1219
1272
|
if (index > 0)
|
|
1220
1273
|
ctx.append(' AND ');
|
|
1221
|
-
const asJson =
|
|
1274
|
+
const asJson = isJsonbOp(op, value[op]);
|
|
1222
1275
|
const sql = this.buildJsonFieldCondition(ctx, () => accessor(asJson), jsonPath, op, value[op], asJson);
|
|
1223
1276
|
if (sql) {
|
|
1224
1277
|
ctx.append(sql);
|
|
@@ -1366,24 +1419,35 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1366
1419
|
ctx.append(')');
|
|
1367
1420
|
}
|
|
1368
1421
|
}
|
|
1422
|
+
/** The runtime half of {@link QuerySizeComparisonOps}: what a count can sensibly be compared with. */
|
|
1423
|
+
static SIZE_COMPARE_OPS = new Set([
|
|
1424
|
+
'$eq',
|
|
1425
|
+
'$ne',
|
|
1426
|
+
'$gt',
|
|
1427
|
+
'$gte',
|
|
1428
|
+
'$lt',
|
|
1429
|
+
'$lte',
|
|
1430
|
+
'$between',
|
|
1431
|
+
]);
|
|
1369
1432
|
/**
|
|
1370
|
-
* Append a single size comparison operator and value
|
|
1433
|
+
* Append a single size comparison operator and value. No operand: the count expression is already
|
|
1434
|
+
* in the context, so this contributes only the ` <op> <value>` tail.
|
|
1435
|
+
*
|
|
1436
|
+
* Gated on {@link SIZE_COMPARE_OPS} rather than on whatever the shared renderer accepts, because
|
|
1437
|
+
* that renderer also knows `$like`, `$regex` and `$in`, none of which mean anything against a
|
|
1438
|
+
* count. `$size: { $like: 5 }` has to stay the error it always was.
|
|
1371
1439
|
*/
|
|
1372
1440
|
appendSizeOp(ctx, op, val) {
|
|
1373
|
-
if (op
|
|
1374
|
-
const [min, max] = val;
|
|
1375
|
-
ctx.append(' BETWEEN ');
|
|
1376
|
-
ctx.addValue(min);
|
|
1377
|
-
ctx.append(' AND ');
|
|
1378
|
-
ctx.addValue(max);
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
const sqlOp = AbstractSqlDialect.comparisonOpMap.get(op);
|
|
1382
|
-
if (!sqlOp) {
|
|
1441
|
+
if (!AbstractSqlDialect.SIZE_COMPARE_OPS.has(op)) {
|
|
1383
1442
|
throw TypeError(`unsupported $size comparison operator: ${op}`);
|
|
1384
1443
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1444
|
+
// A COUNT is never NULL, so equality stays plain here instead of taking the shared renderer's
|
|
1445
|
+
// null-safe `$ne` (`IS DISTINCT FROM` on Postgres, `IS NOT` on SQLite). Same rows, shorter SQL.
|
|
1446
|
+
if (op === '$eq' || op === '$ne') {
|
|
1447
|
+
this.appendFieldSql(ctx, undefined, ` ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx.values, val)}`);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
this.appendOperatorCondition(ctx, undefined, op, val);
|
|
1387
1451
|
}
|
|
1388
1452
|
/** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
|
|
1389
1453
|
escape(value) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type VectorCast } from './vectorCast.js';
|
|
2
|
+
/**
|
|
3
|
+
* How a stored column is decoded on read: the inverse of `AbstractSqlDialect.persistKind`. `json`
|
|
4
|
+
* parses; a {@link VectorCast} says which literal; `boolean` undoes an engine with no boolean type,
|
|
5
|
+
* `number` and `bigint` a driver that hands a wide integer or a decimal back as text.
|
|
6
|
+
*/
|
|
7
|
+
export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | VectorCast;
|
|
8
|
+
/**
|
|
9
|
+
* Decode one non-null cell. Kept beside {@link HydrateKind} rather than inlined into the querier's
|
|
10
|
+
* row walk, so classifying a column and decoding it stay one subject in one file.
|
|
11
|
+
*
|
|
12
|
+
* Every branch is a no-op on a value the driver already decoded, because which types arrive as text
|
|
13
|
+
* varies per driver and the entity is the only thing that says what they were meant to be. A value
|
|
14
|
+
* that does not match its column's format is returned untouched rather than replaced by a guess.
|
|
15
|
+
*/
|
|
16
|
+
export declare function decodeColumn(value: unknown, kind: HydrateKind): unknown;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseVectorLiteral } from './vectorCast.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decode one non-null cell. Kept beside {@link HydrateKind} rather than inlined into the querier's
|
|
4
|
+
* row walk, so classifying a column and decoding it stay one subject in one file.
|
|
5
|
+
*
|
|
6
|
+
* Every branch is a no-op on a value the driver already decoded, because which types arrive as text
|
|
7
|
+
* varies per driver and the entity is the only thing that says what they were meant to be. A value
|
|
8
|
+
* that does not match its column's format is returned untouched rather than replaced by a guess.
|
|
9
|
+
*/
|
|
10
|
+
export function decodeColumn(value, kind) {
|
|
11
|
+
if (kind === 'boolean') {
|
|
12
|
+
// 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
|
|
13
|
+
return typeof value === 'boolean' ? value : Boolean(value);
|
|
14
|
+
}
|
|
15
|
+
const text = asText(value);
|
|
16
|
+
if (kind === 'bigint') {
|
|
17
|
+
if (typeof value === 'bigint') {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// A number, not just text: `type: BigInt` is BIGINT, which the pg pools decode at the wire.
|
|
22
|
+
return BigInt(text ?? value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Not an integer after all (a fractional column declared `bigint`); keep what the driver gave.
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Everything below decodes text; anything else the driver already returned correctly.
|
|
30
|
+
if (text === undefined) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (kind === 'number') {
|
|
34
|
+
const decoded = Number(text);
|
|
35
|
+
return Number.isNaN(decoded) ? value : decoded;
|
|
36
|
+
}
|
|
37
|
+
if (kind === 'json') {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Keep the original value when the driver returns non-JSON text.
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return parseVectorLiteral(text, kind) ?? value;
|
|
47
|
+
}
|
|
48
|
+
/** Lazy so a consumer that never reads an encoded column never constructs one. */
|
|
49
|
+
let decoder;
|
|
50
|
+
/**
|
|
51
|
+
* The text a driver returned, or `undefined` when it returned something already decoded.
|
|
52
|
+
*
|
|
53
|
+
* Bytes count as text: `bun:sql` hands a MySQL DECIMAL, and any `SUM` over one, back as a `Buffer`,
|
|
54
|
+
* so a string-only check left those as raw bytes. `TextDecoder` rather than `Buffer.toString`, because
|
|
55
|
+
* this module is reachable from the browser entry and may not name a Node builtin.
|
|
56
|
+
*/
|
|
57
|
+
function asText(value) {
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if (value instanceof Uint8Array) {
|
|
62
|
+
decoder ??= new TextDecoder();
|
|
63
|
+
return decoder.decode(value);
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
@@ -41,3 +41,27 @@ export declare function jsonSetTarget(expr: string, field: FieldOptions | undefi
|
|
|
41
41
|
export declare function jsonRemoveCall(fn: string, expr: string, keys: readonly string[]): string;
|
|
42
42
|
/** `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. */
|
|
43
43
|
export declare function jsonElemExists(from: string, conditions: readonly string[]): string;
|
|
44
|
+
/**
|
|
45
|
+
* How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
|
|
46
|
+
* of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
|
|
47
|
+
* compared in the representation every engine agrees on:
|
|
48
|
+
* - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
|
|
49
|
+
* would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
|
|
50
|
+
* - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
|
|
51
|
+
* portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
|
|
52
|
+
* - `text` - compare as extracted, which is also what the string operators need.
|
|
53
|
+
*
|
|
54
|
+
* Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
|
|
55
|
+
*/
|
|
56
|
+
export declare function jsonCompareMode(value: unknown): 'json' | 'numeric' | 'text';
|
|
57
|
+
/**
|
|
58
|
+
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
59
|
+
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
60
|
+
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
61
|
+
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
|
|
62
|
+
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
63
|
+
*
|
|
64
|
+
* Numbers stay on the text accessor with a numeric cast, which keeps `1` equal to `1.0` - JSON
|
|
65
|
+
* equality would not.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isJsonbOp(op: string, value?: unknown): boolean;
|
package/dist/dialect/jsonSql.js
CHANGED
|
@@ -54,3 +54,42 @@ export function jsonElemExists(from, conditions) {
|
|
|
54
54
|
const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
55
55
|
return `EXISTS (SELECT 1 FROM ${from}${where})`;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
|
|
59
|
+
* of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
|
|
60
|
+
* compared in the representation every engine agrees on:
|
|
61
|
+
* - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
|
|
62
|
+
* would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
|
|
63
|
+
* - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
|
|
64
|
+
* portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
|
|
65
|
+
* - `text` - compare as extracted, which is also what the string operators need.
|
|
66
|
+
*
|
|
67
|
+
* Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
|
|
68
|
+
*/
|
|
69
|
+
export function jsonCompareMode(value) {
|
|
70
|
+
const operands = Array.isArray(value) ? value : [value];
|
|
71
|
+
if (operands.length === 0) {
|
|
72
|
+
return 'text';
|
|
73
|
+
}
|
|
74
|
+
if (operands.every((operand) => typeof operand === 'boolean')) {
|
|
75
|
+
return 'json';
|
|
76
|
+
}
|
|
77
|
+
return operands.every((operand) => typeof operand === 'number') ? 'numeric' : 'text';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
81
|
+
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
82
|
+
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
83
|
+
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
|
|
84
|
+
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
85
|
+
*
|
|
86
|
+
* Numbers stay on the text accessor with a numeric cast, which keeps `1` equal to `1.0` - JSON
|
|
87
|
+
* equality would not.
|
|
88
|
+
*/
|
|
89
|
+
export function isJsonbOp(op, value) {
|
|
90
|
+
if (op === '$all' || op === '$size' || op === '$elemMatch') {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const comparesValue = op === '$eq' || op === '$ne' || op === '$in' || op === '$nin';
|
|
94
|
+
return comparesValue && jsonCompareMode(value) === 'json';
|
|
95
|
+
}
|
|
@@ -36,6 +36,11 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
36
36
|
* payload) has to land after the insert's for a `?`-placeholder driver.
|
|
37
37
|
*/
|
|
38
38
|
upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
|
|
39
|
+
/**
|
|
40
|
+
* Appended to both branches above. Empty on MySQL, which has no `INSERT ... RETURNING`; MariaDB
|
|
41
|
+
* 10.5+ has it, and used to restate this whole method just to add it.
|
|
42
|
+
*/
|
|
43
|
+
protected upsertReturning<E>(_entity: Type<E>): string;
|
|
39
44
|
readonly maxBindValues: number;
|
|
40
45
|
escape(value: unknown): string;
|
|
41
46
|
/**
|
|
@@ -58,17 +58,26 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
58
58
|
const meta = getMeta(entity);
|
|
59
59
|
const updateCtx = this.createContext();
|
|
60
60
|
const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, (name) => `VALUES(${name})`);
|
|
61
|
+
const returning = this.upsertReturning(entity);
|
|
61
62
|
if (update) {
|
|
62
63
|
this.appendInsertValues(ctx, entity, payload);
|
|
63
|
-
ctx.append(` ON DUPLICATE KEY UPDATE ${update}`);
|
|
64
|
+
ctx.append(` ON DUPLICATE KEY UPDATE ${update}${returning}`);
|
|
64
65
|
ctx.pushValue(...updateCtx.values);
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
const insertCtx = this.createContext();
|
|
68
69
|
this.appendInsertValues(insertCtx, entity, payload);
|
|
69
70
|
ctx.append(insertCtx.sql.replace(/^INSERT/, 'INSERT IGNORE'));
|
|
71
|
+
ctx.append(returning);
|
|
70
72
|
ctx.pushValue(...insertCtx.values);
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Appended to both branches above. Empty on MySQL, which has no `INSERT ... RETURNING`; MariaDB
|
|
76
|
+
* 10.5+ has it, and used to restate this whole method just to add it.
|
|
77
|
+
*/
|
|
78
|
+
upsertReturning(_entity) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
72
81
|
maxBindValues = 65535;
|
|
73
82
|
escape(value) {
|
|
74
83
|
return escapeMysqlSqlLiteral(value);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DialectFeatures, type EntityMeta, type FieldOptions, type IndexColumnSchema, type IndexFeature, type IndexSchema, type JsonColumnType, type
|
|
1
|
+
import { type DialectFeatures, type EntityMeta, type FieldOptions, type IndexColumnSchema, type IndexFeature, type IndexSchema, type JsonColumnType, type QueryContext, type QuerySizeComparisonOps, type QueryTextSearchOptions, type QueryVectorSearch, type Type, type VectorDistance } from '../type/index.js';
|
|
2
2
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
3
3
|
/**
|
|
4
4
|
* Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
|
|
@@ -19,6 +19,8 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
19
19
|
readonly commitTransactionCommand = "COMMIT";
|
|
20
20
|
readonly rollbackTransactionCommand = "ROLLBACK";
|
|
21
21
|
readonly alterColumnStrategy = "separate-clauses";
|
|
22
|
+
/** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
|
|
23
|
+
protected readonly upsertUpdateBindsInPlace = true;
|
|
22
24
|
readonly insertIdSource = "returning";
|
|
23
25
|
readonly maxBindValues: number;
|
|
24
26
|
/**
|
|
@@ -46,12 +48,6 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
46
48
|
protected indexInclude(index: IndexSchema): string;
|
|
47
49
|
protected indexTuning(index: IndexSchema): string;
|
|
48
50
|
placeholder(index: number): string;
|
|
49
|
-
/**
|
|
50
|
-
* The same statement as the base, binding the assignments into the main context instead of a second
|
|
51
|
-
* one: `$N` placeholders carry their own index, so the values need not be in statement order, and
|
|
52
|
-
* computing them first is what keeps `appendInsertValues`' `onInsert` fields out of the update set.
|
|
53
|
-
*/
|
|
54
|
-
upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
|
|
55
51
|
/**
|
|
56
52
|
* `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes free-form user input
|
|
57
53
|
* (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `to_tsquery`, which
|