uql-orm 0.24.5 → 0.24.7
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 -28
- package/dist/dialect/abstractSqlDialect.js +196 -147
- 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/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 +8 -0
- package/dist/migrate/migrator.js +19 -29
- package/dist/migrate/schemaGenerator.js +0 -12
- package/dist/mongo/mongoDialect.d.ts +7 -0
- package/dist/mongo/mongoDialect.js +37 -1
- package/dist/mongo/mongodbQuerier.js +8 -3
- 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/abstractQuerier.d.ts +0 -3
- package/dist/querier/abstractQuerier.js +10 -9
- package/dist/querier/abstractSqlQuerier.d.ts +9 -2
- package/dist/querier/abstractSqlQuerier.js +31 -24
- package/dist/schema/canonicalType.js +2 -12
- package/dist/schema/schemaAST.js +0 -24
- package/dist/schema/schemaASTBuilder.js +0 -3
- package/dist/type/query.d.ts +2 -0
- 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 +5 -5
|
@@ -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';
|
|
@@ -100,23 +100,13 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
100
100
|
const meta = getMeta(entity);
|
|
101
101
|
const prefix = opts.prefix ? opts.prefix + '.' : '';
|
|
102
102
|
const escapedPrefix = this.escapeId(opts.prefix, true, true);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
selectArr = normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
|
|
111
|
-
}
|
|
112
|
-
const id = meta.id;
|
|
113
|
-
if (id && opts.prefix && !selectArr.includes(id)) {
|
|
114
|
-
selectArr = [id, ...selectArr];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
selectArr = normalizeScalarFieldSelection(meta, undefined, exclude);
|
|
119
|
-
}
|
|
103
|
+
const scalars = Array.isArray(select)
|
|
104
|
+
? select // raw SQL projections passed as QueryRaw[]
|
|
105
|
+
: normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
|
|
106
|
+
// A prefix means relations are in play: rows arrive keyed by the id and `fillToManyRelations`
|
|
107
|
+
// groups children by it, so it outlives any subtraction - `$exclude` or falsy `$select` alike.
|
|
108
|
+
const id = meta.id;
|
|
109
|
+
const selectArr = id && opts.prefix && !scalars.includes(id) ? [id, ...scalars] : scalars;
|
|
120
110
|
if (!selectArr.length) {
|
|
121
111
|
ctx.append(escapedPrefix + '*');
|
|
122
112
|
return;
|
|
@@ -387,7 +377,6 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
387
377
|
ctx.append(')');
|
|
388
378
|
}
|
|
389
379
|
}
|
|
390
|
-
/** Simple comparison operators: `getComparisonKey → op → addValue`. */
|
|
391
380
|
/** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
|
|
392
381
|
escapedColumns = new WeakMap();
|
|
393
382
|
static NEGATE_OP_MAP = new Map([
|
|
@@ -444,44 +433,15 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
444
433
|
}
|
|
445
434
|
compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
|
|
446
435
|
const field = this.resolveOperandField(ctx, entity, key, opts);
|
|
447
|
-
|
|
448
|
-
if (simpleOp) {
|
|
449
|
-
this.appendFieldSql(ctx, field, `${simpleOp}${this.addValue(ctx.values, val)}`);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
const likeWrap = AbstractSqlDialect.LIKE_OP_MAP.get(op);
|
|
453
|
-
if (likeWrap) {
|
|
454
|
-
this.appendLikeOp(ctx, field, op, likeWrap(val));
|
|
436
|
+
if (this.appendOperatorCondition(ctx, field, op, val)) {
|
|
455
437
|
return;
|
|
456
438
|
}
|
|
457
439
|
switch (op) {
|
|
458
|
-
case '$eq':
|
|
459
|
-
case '$ne':
|
|
460
|
-
this.appendEqNe(ctx, field, op, val);
|
|
461
|
-
break;
|
|
462
|
-
case '$regex':
|
|
463
|
-
this.appendFieldSql(ctx, field, ` ${this.regexpOp} ${this.addValue(ctx.values, val)}`);
|
|
464
|
-
break;
|
|
465
440
|
case '$not':
|
|
466
441
|
ctx.append('NOT (');
|
|
467
442
|
this.compare(ctx, entity, key, val, opts);
|
|
468
443
|
ctx.append(')');
|
|
469
444
|
break;
|
|
470
|
-
case '$in':
|
|
471
|
-
case '$nin':
|
|
472
|
-
this.appendInNin(ctx, field, op, val);
|
|
473
|
-
break;
|
|
474
|
-
case '$between': {
|
|
475
|
-
const [min, max] = val;
|
|
476
|
-
this.appendFieldSql(ctx, field, ` BETWEEN ${this.addValue(ctx.values, min)} AND ${this.addValue(ctx.values, max)}`);
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
case '$isNull':
|
|
480
|
-
this.appendFieldSql(ctx, field, val ? ' IS NULL' : ' IS NOT NULL');
|
|
481
|
-
break;
|
|
482
|
-
case '$isNotNull':
|
|
483
|
-
this.appendFieldSql(ctx, field, val ? ' IS NOT NULL' : ' IS NULL');
|
|
484
|
-
break;
|
|
485
445
|
case '$all':
|
|
486
446
|
ctx.append(this.jsonAll(ctx, field ?? '', val));
|
|
487
447
|
break;
|
|
@@ -495,6 +455,57 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
495
455
|
throw TypeError(`unknown operator: ${op}`);
|
|
496
456
|
}
|
|
497
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
|
+
}
|
|
498
509
|
appendLikeOp(ctx, field, op, wrappedVal) {
|
|
499
510
|
const isIlike = AbstractSqlDialect.LIKE_CASE_INSENSITIVE_OPS.has(op);
|
|
500
511
|
const ph = this.addValue(ctx.values, wrappedVal);
|
|
@@ -599,29 +610,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
599
610
|
* {@link jsonCompareMode} for why each mode exists.
|
|
600
611
|
*/
|
|
601
612
|
jsonComparand(jsonField, value) {
|
|
602
|
-
return
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
|
|
606
|
-
* of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
|
|
607
|
-
* compared in the representation every engine agrees on:
|
|
608
|
-
* - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
|
|
609
|
-
* would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
|
|
610
|
-
* - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
|
|
611
|
-
* portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
|
|
612
|
-
* - `text` - compare as extracted, which is also what the string operators need.
|
|
613
|
-
*
|
|
614
|
-
* Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
|
|
615
|
-
*/
|
|
616
|
-
jsonCompareMode(value) {
|
|
617
|
-
const operands = Array.isArray(value) ? value : [value];
|
|
618
|
-
if (operands.length === 0) {
|
|
619
|
-
return 'text';
|
|
620
|
-
}
|
|
621
|
-
if (operands.every((operand) => typeof operand === 'boolean')) {
|
|
622
|
-
return 'json';
|
|
623
|
-
}
|
|
624
|
-
return operands.every((operand) => typeof operand === 'number') ? 'numeric' : 'text';
|
|
613
|
+
return jsonCompareMode(value) === 'numeric' ? this.numericCast(jsonField) : jsonField;
|
|
625
614
|
}
|
|
626
615
|
/**
|
|
627
616
|
* Whether the dialect's array containment ({@link jsonAll}) matches an object element that merely
|
|
@@ -649,7 +638,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
649
638
|
// only when *every* operand needs it - the same all-operands rule the comparison classifier uses.
|
|
650
639
|
if (isOperatorOnlyObject(match)) {
|
|
651
640
|
const entries = Object.entries(match);
|
|
652
|
-
const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) =>
|
|
641
|
+
const asJson = !this.jsonScalarElemKeepsType && entries.every(([op, val]) => isJsonbOp(op, val));
|
|
653
642
|
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
654
643
|
const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, () => this.jsonElemRef(alias, undefined, asJson), '', op, val, asJson));
|
|
655
644
|
return jsonElemExists(this.jsonElemFrom(jsonField, [], alias, asJson), conditions);
|
|
@@ -664,28 +653,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
664
653
|
}
|
|
665
654
|
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
666
655
|
const conditions = buildElemMatchConditions(match, (field, op, opVal) => {
|
|
667
|
-
const asJson =
|
|
656
|
+
const asJson = isJsonbOp(op, opVal);
|
|
668
657
|
return this.buildJsonFieldCondition(ctx, (f) => this.jsonElemRef(alias, f, asJson), field, op, opVal, asJson);
|
|
669
658
|
});
|
|
670
659
|
return jsonElemExists(this.jsonElemFrom(jsonField, Object.keys(match), alias), conditions);
|
|
671
660
|
}
|
|
672
|
-
/**
|
|
673
|
-
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
674
|
-
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
675
|
-
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
676
|
-
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
|
|
677
|
-
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
678
|
-
*
|
|
679
|
-
* Numbers stay on the text accessor with a numeric cast ({@link jsonComparand}), which keeps
|
|
680
|
-
* `1` equal to `1.0` - JSON equality would not.
|
|
681
|
-
*/
|
|
682
|
-
isJsonbOp(op, value) {
|
|
683
|
-
if (op === '$all' || op === '$size' || op === '$elemMatch') {
|
|
684
|
-
return true;
|
|
685
|
-
}
|
|
686
|
-
const comparesValue = op === '$eq' || op === '$ne' || op === '$in' || op === '$nin';
|
|
687
|
-
return comparesValue && this.jsonCompareMode(value) === 'json';
|
|
688
|
-
}
|
|
689
661
|
/**
|
|
690
662
|
* A JSON-encoded bound parameter, cast to the dialect's JSON type. Only the positional-placeholder
|
|
691
663
|
* dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
@@ -867,51 +839,17 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
867
839
|
return direction;
|
|
868
840
|
}
|
|
869
841
|
/** Scalar comparison operators shared by `HAVING` conditions and `$size` comparisons. */
|
|
870
|
-
static comparisonOpMap = new Map([
|
|
871
|
-
['$eq', '='],
|
|
872
|
-
['$ne', '<>'],
|
|
873
|
-
['$gt', '>'],
|
|
874
|
-
['$gte', '>='],
|
|
875
|
-
['$lt', '<'],
|
|
876
|
-
['$lte', '<='],
|
|
877
|
-
]);
|
|
878
842
|
havingCondition(ctx, expr, condition) {
|
|
879
843
|
if (typeof condition !== 'object' || condition === null) {
|
|
880
|
-
|
|
881
|
-
ctx.addValue(condition);
|
|
844
|
+
this.appendOperatorCondition(ctx, expr, '$eq', condition);
|
|
882
845
|
return;
|
|
883
846
|
}
|
|
884
847
|
const ops = condition;
|
|
885
|
-
|
|
886
|
-
keys.forEach((op, i) => {
|
|
848
|
+
getKeys(ops).forEach((op, i) => {
|
|
887
849
|
if (i > 0)
|
|
888
850
|
ctx.append(' AND ');
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const [min, max] = val;
|
|
892
|
-
ctx.append(`${expr} BETWEEN `);
|
|
893
|
-
ctx.addValue(min);
|
|
894
|
-
ctx.append(' AND ');
|
|
895
|
-
ctx.addValue(max);
|
|
896
|
-
}
|
|
897
|
-
else if (op === '$in' || op === '$nin') {
|
|
898
|
-
ctx.append(`${expr}${this.formatIn(ctx, Array.isArray(val) ? val : [], op === '$nin')}`);
|
|
899
|
-
}
|
|
900
|
-
else if (op === '$isNull') {
|
|
901
|
-
ctx.append(`${expr}${val ? ' IS NULL' : ' IS NOT NULL'}`);
|
|
902
|
-
}
|
|
903
|
-
else if (op === '$isNotNull') {
|
|
904
|
-
ctx.append(`${expr}${val ? ' IS NOT NULL' : ' IS NULL'}`);
|
|
905
|
-
}
|
|
906
|
-
else if (op === '$ne') {
|
|
907
|
-
ctx.append(this.neExpr(expr, this.addValue(ctx.values, val)));
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
910
|
-
const sqlOp = AbstractSqlDialect.comparisonOpMap.get(op);
|
|
911
|
-
if (!sqlOp)
|
|
912
|
-
throw TypeError(`unsupported HAVING operator: ${op}`);
|
|
913
|
-
ctx.append(`${expr} ${sqlOp} `);
|
|
914
|
-
ctx.addValue(val);
|
|
851
|
+
if (!this.appendOperatorCondition(ctx, expr, op, ops[op])) {
|
|
852
|
+
throw TypeError(`unsupported HAVING operator: ${op}`);
|
|
915
853
|
}
|
|
916
854
|
});
|
|
917
855
|
}
|
|
@@ -1019,14 +957,25 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1019
957
|
*/
|
|
1020
958
|
upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
|
|
1021
959
|
const meta = getMeta(entity);
|
|
1022
|
-
const updateCtx = this.createContext();
|
|
960
|
+
const updateCtx = this.upsertUpdateBindsInPlace ? ctx : this.createContext();
|
|
1023
961
|
const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, this.upsertExcluded);
|
|
1024
962
|
const keys = this.getUpsertConflictPathsStr(meta, conflictPaths);
|
|
1025
963
|
const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
|
|
1026
964
|
this.appendInsertValues(ctx, entity, payload);
|
|
1027
965
|
ctx.append(` ON CONFLICT (${keys}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
|
|
1028
|
-
|
|
966
|
+
if (updateCtx !== ctx) {
|
|
967
|
+
ctx.pushValue(...updateCtx.values);
|
|
968
|
+
}
|
|
1029
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Whether the upsert's update assignments can bind straight into the statement's own context.
|
|
972
|
+
*
|
|
973
|
+
* They cannot on a `?`-placeholder dialect: the assignments are built before the insert but read
|
|
974
|
+
* after it, so their values have to be pushed afterwards to land in the right positional order.
|
|
975
|
+
* A `$n` placeholder carries its own index, so there is nothing to reorder - but it also cannot use
|
|
976
|
+
* the scratch context, whose numbering would restart at `$1` and collide with the insert's.
|
|
977
|
+
*/
|
|
978
|
+
upsertUpdateBindsInPlace = false;
|
|
1030
979
|
/** How an `ON CONFLICT` assignment reads the row that was being inserted. */
|
|
1031
980
|
upsertExcluded = (columnName) => `EXCLUDED.${columnName}`;
|
|
1032
981
|
getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, callback) {
|
|
@@ -1100,6 +1049,95 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1100
1049
|
}
|
|
1101
1050
|
return isVectorFieldType(type) ? 'vector' : 'plain';
|
|
1102
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Which of an entity's columns need decoding on READ, and how: the inverse of {@link persistKind},
|
|
1054
|
+
* cached per entity for the same reason it classifies per column. A 1000-row read of a 10-field
|
|
1055
|
+
* entity otherwise asks `isJsonType` (which lowercases a string on every call) 10,000 times to get
|
|
1056
|
+
* the same ten answers. Most entities land here for their numeric columns alone, where the per-row
|
|
1057
|
+
* cost is one `typeof` against a value the driver usually decoded already.
|
|
1058
|
+
*
|
|
1059
|
+
* Dialect-aware exactly like {@link supportedVectorType}, because it has to be: a `sparsevec` field
|
|
1060
|
+
* is written as a plain dense vector everywhere but Postgres, so reading it back by the field's own
|
|
1061
|
+
* declared cast would look for a sparse literal that was never stored.
|
|
1062
|
+
*
|
|
1063
|
+
* A type lands here rather than at the driver when the wire type alone cannot decide it, and only
|
|
1064
|
+
* the declaration can: `Boolean` is 0/1 in a SQLite INTEGER and a MySQL `TINYINT(1)`, both
|
|
1065
|
+
* indistinguishable from a genuine small integer; a decimal is text from pg *and* mysql2, and only
|
|
1066
|
+
* the field says it was meant as a number; and `type: BigInt` shares BIGINT with `type: Number`, so
|
|
1067
|
+
* the wire decode has to be undone for it. All are no-ops where the driver already decoded.
|
|
1068
|
+
*
|
|
1069
|
+
* Classified through the same `isNumericType`/`isBooleanType`/`isJsonType` the rest of the library
|
|
1070
|
+
* uses, not against the constructors: `type` accepts a string logical type for every one of these
|
|
1071
|
+
* (`@Field({ type: 'decimal' })`), and matching `=== Number` alone left those reading back as text.
|
|
1072
|
+
*/
|
|
1073
|
+
hydratableFields(entity) {
|
|
1074
|
+
const cached = this.hydratable.get(entity);
|
|
1075
|
+
if (cached) {
|
|
1076
|
+
return cached;
|
|
1077
|
+
}
|
|
1078
|
+
const decoded = [];
|
|
1079
|
+
for (const [key, field] of Object.entries(getMeta(entity).fields)) {
|
|
1080
|
+
const kind = this.hydrateKind(field);
|
|
1081
|
+
if (kind) {
|
|
1082
|
+
decoded.push([key, kind]);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
this.hydratable.set(entity, decoded);
|
|
1086
|
+
return decoded;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* The same classification for an aggregate row. Not cached, because these columns are a shape of the
|
|
1090
|
+
* query rather than of the entity, and it is computed once per call either way.
|
|
1091
|
+
*
|
|
1092
|
+
* Mirrors `QueryAggregateFnResult`, which is the contract callers already compile against:
|
|
1093
|
+
* `$count`/`$sum`/`$avg` are a number whatever they aggregate, while `$min`/`$max` and every
|
|
1094
|
+
* `$group` column keep the aggregated field's own type, so they decode as that field would. Without
|
|
1095
|
+
* it a `$sum` over a BIGINT column came back as `'500'` from a result type that says `number`, since
|
|
1096
|
+
* Postgres widens that sum to NUMERIC and no driver can know it was meant as a JS number.
|
|
1097
|
+
*/
|
|
1098
|
+
hydratableAggregates(entity, q) {
|
|
1099
|
+
const { fields } = getMeta(entity);
|
|
1100
|
+
const decoded = [];
|
|
1101
|
+
for (const entry of parseGroupMap(q.$group, q.$agg)) {
|
|
1102
|
+
if (entry.kind === 'fn' && entry.op !== '$min' && entry.op !== '$max') {
|
|
1103
|
+
decoded.push([entry.alias, 'number']);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
// `$min`/`$max` read the field they aggregate; a `$group` column is that field. Only `$count`
|
|
1107
|
+
// takes `'*'`, and it went down the numeric path above, so there is always a field to look up.
|
|
1108
|
+
const key = entry.kind === 'fn' ? entry.fieldRef : entry.alias;
|
|
1109
|
+
const kind = this.hydrateKind(fields[key]);
|
|
1110
|
+
if (kind) {
|
|
1111
|
+
decoded.push([entry.alias, kind]);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return decoded;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode.
|
|
1118
|
+
*
|
|
1119
|
+
* Ordered for correctness, not for speed - this runs once per entity, cached, never per row. The
|
|
1120
|
+
* one order that is load-bearing is `BigInt` before {@link isNumericType}, which answers true for
|
|
1121
|
+
* `BigInt` as well as `Number`: swap them and every `type: BigInt` property silently decodes to a
|
|
1122
|
+
* JS number again.
|
|
1123
|
+
*/
|
|
1124
|
+
hydrateKind(field) {
|
|
1125
|
+
const type = field?.type;
|
|
1126
|
+
if (isJsonType(type)) {
|
|
1127
|
+
return 'json';
|
|
1128
|
+
}
|
|
1129
|
+
if (isVectorFieldType(type)) {
|
|
1130
|
+
return this.supportedVectorType(resolveVectorCast(field));
|
|
1131
|
+
}
|
|
1132
|
+
if (isBooleanType(type)) {
|
|
1133
|
+
return 'boolean';
|
|
1134
|
+
}
|
|
1135
|
+
if (type === BigInt) {
|
|
1136
|
+
return 'bigint';
|
|
1137
|
+
}
|
|
1138
|
+
return isNumericType(type) ? 'number' : undefined;
|
|
1139
|
+
}
|
|
1140
|
+
hydratable = new WeakMap();
|
|
1103
1141
|
/** The one type dispatch for a persisted value, over a column kind decided by the caller. */
|
|
1104
1142
|
writePersistableValue(ctx, kind, field, value) {
|
|
1105
1143
|
if (value instanceof QueryRaw) {
|
|
@@ -1229,7 +1267,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1229
1267
|
operators.forEach((op, index) => {
|
|
1230
1268
|
if (index > 0)
|
|
1231
1269
|
ctx.append(' AND ');
|
|
1232
|
-
const asJson =
|
|
1270
|
+
const asJson = isJsonbOp(op, value[op]);
|
|
1233
1271
|
const sql = this.buildJsonFieldCondition(ctx, () => accessor(asJson), jsonPath, op, value[op], asJson);
|
|
1234
1272
|
if (sql) {
|
|
1235
1273
|
ctx.append(sql);
|
|
@@ -1377,24 +1415,35 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1377
1415
|
ctx.append(')');
|
|
1378
1416
|
}
|
|
1379
1417
|
}
|
|
1418
|
+
/** The runtime half of {@link QuerySizeComparisonOps}: what a count can sensibly be compared with. */
|
|
1419
|
+
static SIZE_COMPARE_OPS = new Set([
|
|
1420
|
+
'$eq',
|
|
1421
|
+
'$ne',
|
|
1422
|
+
'$gt',
|
|
1423
|
+
'$gte',
|
|
1424
|
+
'$lt',
|
|
1425
|
+
'$lte',
|
|
1426
|
+
'$between',
|
|
1427
|
+
]);
|
|
1380
1428
|
/**
|
|
1381
|
-
* Append a single size comparison operator and value
|
|
1429
|
+
* Append a single size comparison operator and value. No operand: the count expression is already
|
|
1430
|
+
* in the context, so this contributes only the ` <op> <value>` tail.
|
|
1431
|
+
*
|
|
1432
|
+
* Gated on {@link SIZE_COMPARE_OPS} rather than on whatever the shared renderer accepts, because
|
|
1433
|
+
* that renderer also knows `$like`, `$regex` and `$in`, none of which mean anything against a
|
|
1434
|
+
* count. `$size: { $like: 5 }` has to stay the error it always was.
|
|
1382
1435
|
*/
|
|
1383
1436
|
appendSizeOp(ctx, op, val) {
|
|
1384
|
-
if (op
|
|
1385
|
-
const [min, max] = val;
|
|
1386
|
-
ctx.append(' BETWEEN ');
|
|
1387
|
-
ctx.addValue(min);
|
|
1388
|
-
ctx.append(' AND ');
|
|
1389
|
-
ctx.addValue(max);
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
const sqlOp = AbstractSqlDialect.comparisonOpMap.get(op);
|
|
1393
|
-
if (!sqlOp) {
|
|
1437
|
+
if (!AbstractSqlDialect.SIZE_COMPARE_OPS.has(op)) {
|
|
1394
1438
|
throw TypeError(`unsupported $size comparison operator: ${op}`);
|
|
1395
1439
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1440
|
+
// A COUNT is never NULL, so equality stays plain here instead of taking the shared renderer's
|
|
1441
|
+
// null-safe `$ne` (`IS DISTINCT FROM` on Postgres, `IS NOT` on SQLite). Same rows, shorter SQL.
|
|
1442
|
+
if (op === '$eq' || op === '$ne') {
|
|
1443
|
+
this.appendFieldSql(ctx, undefined, ` ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx.values, val)}`);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
this.appendOperatorCondition(ctx, undefined, op, val);
|
|
1398
1447
|
}
|
|
1399
1448
|
/** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
|
|
1400
1449
|
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);
|