uql-orm 0.50.0 → 0.52.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 +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +2 -2
- package/dist/bunSql/bunSql.util.d.ts +33 -10
- package/dist/bunSql/bunSql.util.js +57 -42
- package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
- package/dist/bunSql/bunSqlQuerier.js +17 -8
- package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
- package/dist/bunSql/bunSqlQuerierPool.js +38 -25
- package/dist/bunSql/index.d.ts +1 -3
- package/dist/bunSql/index.js +0 -3
- package/dist/dialect/abstractSqlDialect.d.ts +61 -7
- package/dist/dialect/abstractSqlDialect.js +88 -27
- package/dist/dialect/aliases.d.ts +2 -0
- package/dist/dialect/aliases.js +2 -0
- package/dist/dialect/jsonSql.d.ts +2 -2
- package/dist/dialect/jsonSql.js +2 -2
- package/dist/dialect/mergeSqlDialect.d.ts +45 -0
- package/dist/dialect/mergeSqlDialect.js +89 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +3 -3
- package/dist/dialect/pgLikeSqlDialect.js +15 -12
- package/dist/migrate/builder/expressions.js +5 -0
- package/dist/migrate/introspection/index.d.ts +2 -0
- package/dist/migrate/introspection/index.js +2 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
- package/dist/migrate/introspection/postgresIntrospector.js +3 -3
- package/dist/migrate/introspection/registry.d.ts +3 -0
- package/dist/migrate/introspection/registry.js +28 -0
- package/dist/migrate/migrator.js +2 -21
- package/dist/mongo/mongoDialect.js +4 -1
- package/dist/mongo/mongodbQuerier.d.ts +2 -2
- package/dist/mongo/mongodbQuerier.js +8 -6
- package/dist/mssql/index.d.ts +3 -0
- package/dist/mssql/index.js +3 -0
- package/dist/mssql/mssqlDialect.d.ts +144 -0
- package/dist/mssql/mssqlDialect.js +328 -0
- package/dist/mssql/mssqlQuerier.d.ts +23 -0
- package/dist/mssql/mssqlQuerier.js +137 -0
- package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
- package/dist/mssql/mssqlQuerierPool.js +32 -0
- package/dist/mssql/mssqlWireTypes.d.ts +23 -0
- package/dist/mssql/mssqlWireTypes.js +44 -0
- package/dist/pglite/pgliteQuerier.d.ts +4 -2
- package/dist/pglite/pgliteQuerier.js +7 -2
- package/dist/postgres/pgCursorStream.d.ts +20 -0
- package/dist/postgres/pgCursorStream.js +49 -0
- package/dist/postgres/pgDialect.d.ts +1 -1
- package/dist/postgres/pgDialect.js +1 -1
- package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
- package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
- package/dist/querier/abstractQuerier.d.ts +3 -7
- package/dist/querier/abstractQuerier.js +22 -2
- package/dist/querier/abstractQuerierPool.d.ts +3 -3
- package/dist/schema/canonicalType.js +96 -113
- package/dist/sqlite/sqliteDialect.d.ts +7 -7
- package/dist/sqlite/sqliteDialect.js +21 -18
- package/dist/type/dialect.d.ts +31 -4
- package/dist/type/migratorDialect.d.ts +1 -1
- package/dist/type/migratorDialect.js +1 -0
- package/dist/type/query.d.ts +29 -6
- package/dist/type/universalQuerier.d.ts +3 -3
- package/package.json +13 -3
- package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
- package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
- package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
- package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
- package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
- package/dist/bunSql/bunSqliteDialect.js +0 -6
package/dist/bunSql/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export * from './bunSqlCockroachDialect.js';
|
|
3
|
-
export * from './bunSqlPostgresDialect.js';
|
|
1
|
+
export type { BunSqlConn, BunSqlDialectName, BunSqlResult } from './bunSql.util.js';
|
|
4
2
|
export * from './bunSqlQuerier.js';
|
|
5
3
|
export * from './bunSqlQuerierPool.js';
|
package/dist/bunSql/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryFilter, type QueryGroupMap, type QueryGroupOp, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorNear, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
1
|
+
import { type EntityData, type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryFilter, type QueryGroupMap, type QueryGroupOp, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorNear, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
2
2
|
import { type ParentPartition } from '../util/index.js';
|
|
3
3
|
import type { HydrateKind } from './hydrateColumn.js';
|
|
4
4
|
import { type JsonAccessMode } from './jsonSql.js';
|
|
@@ -6,6 +6,15 @@ import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
|
|
|
6
6
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
7
7
|
/** How a column's values are bound: see {@link AbstractSqlDialect.persistKind}. */
|
|
8
8
|
type PersistKind = 'plain' | 'json' | 'vector';
|
|
9
|
+
/** What {@link AbstractSqlDialect.insertShape} resolves once for a write, indexed in step. */
|
|
10
|
+
type InsertShape<E> = {
|
|
11
|
+
readonly meta: EntityMeta<E>;
|
|
12
|
+
readonly payloads: EntityData<E>[];
|
|
13
|
+
readonly keys: FieldKey<E>[];
|
|
14
|
+
readonly fields: (FieldOptions | undefined)[];
|
|
15
|
+
readonly columns: string[];
|
|
16
|
+
readonly kinds: PersistKind[];
|
|
17
|
+
};
|
|
9
18
|
/** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
|
|
10
19
|
type HydratableField = readonly [string, HydrateKind];
|
|
11
20
|
export type { HydrateKind };
|
|
@@ -111,6 +120,11 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
111
120
|
protected returningIdExpression<E>(meta: EntityMeta<E>): string;
|
|
112
121
|
search<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, joins?: QueryJoins): void;
|
|
113
122
|
selectFields<E>(ctx: QueryContext, entity: Type<E>, select: QuerySelectValue<E> | undefined, opts?: QuerySelectOptions, exclude?: QueryExclude<E>): void;
|
|
123
|
+
/**
|
|
124
|
+
* What follows `SELECT` before the projection. Empty everywhere but SQL Server, whose `FETCH` will
|
|
125
|
+
* not take a zero and which spells "no rows" as `TOP (0)` instead.
|
|
126
|
+
*/
|
|
127
|
+
protected selectModifier<E>(_q: Query<E>): string;
|
|
114
128
|
/**
|
|
115
129
|
* The expression a scalar field is read through, the plain column by default. MariaDB reads a
|
|
116
130
|
* vector column back with `VEC_ToText`, since selecting it raw yields its binary form.
|
|
@@ -118,7 +132,7 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
118
132
|
protected selectFieldExpr(escapedColumn: string, _field: FieldOptions): string;
|
|
119
133
|
/**
|
|
120
134
|
* The `$text` full-text predicate, which every engine spells differently: `MATCH ... AGAINST`
|
|
121
|
-
* (MySQL family), `
|
|
135
|
+
* (MySQL family), `TO_TSVECTOR @@ WEBSEARCH_TO_TSQUERY` (Postgres-wire), an FTS5 `MATCH` against
|
|
122
136
|
* the table itself (SQLite). No portable form exists, so a dialect without one says so here rather
|
|
123
137
|
* than inheriting another engine's syntax.
|
|
124
138
|
*/
|
|
@@ -253,7 +267,7 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
253
267
|
*/
|
|
254
268
|
protected readonly jsonContainmentIsPartial: boolean;
|
|
255
269
|
/**
|
|
256
|
-
* Whether an exploded *scalar* element keeps its SQL type. SQLite's `
|
|
270
|
+
* Whether an exploded *scalar* element keeps its SQL type. SQLite's `JSON_EACH` yields JSON
|
|
257
271
|
* booleans as `0`/`1` integers and numbers as numbers, so such an element compares directly to a
|
|
258
272
|
* bound value; PostgreSQL and MySQL explode scalars to text, losing the type, so a non-string
|
|
259
273
|
* operand there has to compare as JSON (see {@link isJsonbOp}).
|
|
@@ -280,7 +294,9 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
280
294
|
protected jsonScalarParam(ctx: QueryContext, value: unknown): string;
|
|
281
295
|
/** {@link resolveOperandField}, appended. */
|
|
282
296
|
getComparisonKey<E>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, opts?: QueryOptions): void;
|
|
283
|
-
|
|
297
|
+
/** Appends the `ORDER BY`, reporting whether there was one - which {@link pager} needs on the
|
|
298
|
+
* engines that refuse to page an unordered statement. */
|
|
299
|
+
sort<E>(ctx: QueryContext, entity: Type<E>, sort: QuerySortMap<E> | undefined, opts?: QuerySortOptions): boolean;
|
|
284
300
|
/**
|
|
285
301
|
* Walks `$sort` against the metadata of the entity each level addresses, rather than flattening it
|
|
286
302
|
* to dotted strings and reading every key off the root: only that way does a related column resolve
|
|
@@ -293,7 +309,11 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
293
309
|
* `$agg` alias - is an output alias, which is never table-qualified and needs no resolving.
|
|
294
310
|
*/
|
|
295
311
|
private sortColumn;
|
|
296
|
-
|
|
312
|
+
/**
|
|
313
|
+
* `LIMIT`/`OFFSET`. `sorted` says whether an `ORDER BY` was emitted just before, which
|
|
314
|
+
* {@link MergeSqlDialect} needs: SQL Server refuses to page a statement that has none.
|
|
315
|
+
*/
|
|
316
|
+
pager(ctx: QueryContext, opts: QueryPager, _sorted?: boolean): void;
|
|
297
317
|
/** Whether this engine has row locks at all. The SQLite family locks the database instead. */
|
|
298
318
|
readonly supportsRowLocks: boolean;
|
|
299
319
|
/**
|
|
@@ -306,6 +326,12 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
306
326
|
readonly supportsLockOf: boolean;
|
|
307
327
|
/** Validated before the querier checks for a transaction, so the clearer error wins. */
|
|
308
328
|
assertLockSupported<E>(entity: Type<E>, q: Query<E>, joins?: QueryJoins): void;
|
|
329
|
+
/**
|
|
330
|
+
* The lock as a hint on the table itself, for the engine that has no trailing `FOR UPDATE`. Empty
|
|
331
|
+
* everywhere else, which is where {@link appendLock} does the work instead - the two are the same
|
|
332
|
+
* lock spelled at opposite ends of the statement, so exactly one of them ever emits.
|
|
333
|
+
*/
|
|
334
|
+
protected lockHint<E>(_q: Query<E>): string;
|
|
309
335
|
/**
|
|
310
336
|
* The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
|
|
311
337
|
* joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
|
|
@@ -354,11 +380,32 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
354
380
|
protected readonly totalOverExpr = "COUNT(*) OVER ()";
|
|
355
381
|
find<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
|
|
356
382
|
insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
|
|
383
|
+
/**
|
|
384
|
+
* Where the clause reporting an insert's generated ids goes. `suffix` is `RETURNING ...` at the end
|
|
385
|
+
* of the statement, which every engine here but one spells that way; SQL Server's `OUTPUT` has no
|
|
386
|
+
* trailing form and sits between the column list and `VALUES`.
|
|
387
|
+
*
|
|
388
|
+
* A knob rather than a pair of hooks: one concept decides where the string {@link returningId}
|
|
389
|
+
* already built ends up, so the two ends cannot disagree.
|
|
390
|
+
*/
|
|
391
|
+
readonly returningPosition: 'suffix' | 'after-target';
|
|
357
392
|
/**
|
|
358
393
|
* `INSERT INTO ... VALUES (...)` and nothing more. The upsert builders extend this rather than
|
|
359
394
|
* {@link insert}: their own clause has to come before the `RETURNING`, not after it.
|
|
360
395
|
*/
|
|
361
|
-
protected appendInsertValues<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[]
|
|
396
|
+
protected appendInsertValues<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[],
|
|
397
|
+
/** Spliced between the column list and `VALUES`; see {@link returningPosition}. */
|
|
398
|
+
afterTarget?: string): void;
|
|
399
|
+
/**
|
|
400
|
+
* The columns an insert writes and the records it writes them from, resolved once.
|
|
401
|
+
*
|
|
402
|
+
* Split out of {@link appendInsertValues} because a `MERGE` needs the same rows as a `VALUES` row
|
|
403
|
+
* source rather than as an `INSERT`, and both have to apply `onInsert` defaults and the
|
|
404
|
+
* JSON/vector binding rules identically.
|
|
405
|
+
*/
|
|
406
|
+
protected insertShape<E>(entity: Type<E>, payload: E | E[]): InsertShape<E>;
|
|
407
|
+
/** `(a, b), (c, d)` - the row constructor an INSERT and a MERGE source both write. */
|
|
408
|
+
protected appendValueRows<E>(ctx: QueryContext, { payloads, keys, fields, kinds }: InsertShape<E>): void;
|
|
362
409
|
/**
|
|
363
410
|
* Emit the value for a column a payload record does not provide (the column list is the union
|
|
364
411
|
* across all records). `DEFAULT` delegates to the database default; SQLite overrides this since
|
|
@@ -562,7 +609,7 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
562
609
|
private buildExprComparison;
|
|
563
610
|
/**
|
|
564
611
|
* Build a complete `$size` comparison expression.
|
|
565
|
-
* @param sizeExprFn - function that appends the size expression to ctx (e.g. `
|
|
612
|
+
* @param sizeExprFn - function that appends the size expression to ctx (e.g. `JSONB_ARRAY_LENGTH("col")`)
|
|
566
613
|
*/
|
|
567
614
|
protected buildSizeComparison(ctx: QueryContext, sizeExprFn: () => void, sizeVal: number | QuerySizeComparisonOps): void;
|
|
568
615
|
/**
|
|
@@ -587,6 +634,13 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
587
634
|
/** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
|
|
588
635
|
escape(value: unknown): string;
|
|
589
636
|
protected get regexpOp(): string;
|
|
637
|
+
/**
|
|
638
|
+
* The `$regex` predicate. An infix operator on the MySQL family (`REGEXP`) and the Postgres one
|
|
639
|
+
* (`~`), but a function on Oracle and SQL Server 2025 (`REGEXP_LIKE(col, ?)`) - which is why this
|
|
640
|
+
* is a method rather than the operator token alone. An engine with no regex at all overrides it to
|
|
641
|
+
* throw, the way {@link appendTextSearch} already does.
|
|
642
|
+
*/
|
|
643
|
+
protected regexCondition(operand: string, placeholder: string): string;
|
|
590
644
|
protected get likeFn(): string;
|
|
591
645
|
/**
|
|
592
646
|
* Not-equal operator token for non-null comparisons.
|
|
@@ -168,8 +168,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
168
168
|
opts = { ...opts, prefix };
|
|
169
169
|
}
|
|
170
170
|
this.where(ctx, entity, q.$where, opts);
|
|
171
|
-
this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
|
|
172
|
-
this.pager(ctx, q);
|
|
171
|
+
const sorted = this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
|
|
172
|
+
this.pager(ctx, q, sorted);
|
|
173
173
|
}
|
|
174
174
|
selectFields(ctx, entity, select, opts = {}, exclude) {
|
|
175
175
|
const meta = getMeta(entity);
|
|
@@ -227,6 +227,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* What follows `SELECT` before the projection. Empty everywhere but SQL Server, whose `FETCH` will
|
|
232
|
+
* not take a zero and which spells "no rows" as `TOP (0)` instead.
|
|
233
|
+
*/
|
|
234
|
+
selectModifier(_q) {
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
230
237
|
/**
|
|
231
238
|
* The expression a scalar field is read through, the plain column by default. MariaDB reads a
|
|
232
239
|
* vector column back with `VEC_ToText`, since selecting it raw yields its binary form.
|
|
@@ -236,7 +243,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
236
243
|
}
|
|
237
244
|
/**
|
|
238
245
|
* The `$text` full-text predicate, which every engine spells differently: `MATCH ... AGAINST`
|
|
239
|
-
* (MySQL family), `
|
|
246
|
+
* (MySQL family), `TO_TSVECTOR @@ WEBSEARCH_TO_TSQUERY` (Postgres-wire), an FTS5 `MATCH` against
|
|
240
247
|
* the table itself (SQLite). No portable form exists, so a dialect without one says so here rather
|
|
241
248
|
* than inheriting another engine's syntax.
|
|
242
249
|
*/
|
|
@@ -248,6 +255,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
248
255
|
const { alias, ref } = this.tableRef(meta);
|
|
249
256
|
const prefix = this.resolveRelationAwarePrefix(alias, meta, opts, q.$populate, joins);
|
|
250
257
|
ctx.append(q.$distinct ? 'SELECT DISTINCT ' : 'SELECT ');
|
|
258
|
+
ctx.append(this.selectModifier(q));
|
|
251
259
|
this.selectFields(ctx, entity, q.$select, { prefix }, q.$exclude);
|
|
252
260
|
// Add related fields BEFORE FROM clause
|
|
253
261
|
this.selectRelationFields(ctx, joins);
|
|
@@ -261,7 +269,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
261
269
|
if (totalAlias) {
|
|
262
270
|
ctx.append(`, ${this.totalOverExpr} ${this.escapeId(totalAlias, true)}`);
|
|
263
271
|
}
|
|
264
|
-
ctx.append(` FROM ${ref}`);
|
|
272
|
+
ctx.append(` FROM ${ref}${this.lockHint(q)}`);
|
|
265
273
|
// Add JOINs AFTER FROM clause
|
|
266
274
|
this.selectRelationJoins(ctx, meta, alias, joins);
|
|
267
275
|
}
|
|
@@ -582,7 +590,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
582
590
|
case '$ne':
|
|
583
591
|
return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand, this.addValue(ctx.values, val));
|
|
584
592
|
case '$regex':
|
|
585
|
-
return
|
|
593
|
+
return this.regexCondition(operand, this.addValue(ctx.values, val));
|
|
586
594
|
case '$in':
|
|
587
595
|
case '$nin': {
|
|
588
596
|
if (!Array.isArray(val)) {
|
|
@@ -643,7 +651,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
643
651
|
return `${jsonField} IS NOT NULL`;
|
|
644
652
|
return this.neExpr(comparand(value), this.jsonOperand(ctx, value, asJson));
|
|
645
653
|
case '$regex':
|
|
646
|
-
return
|
|
654
|
+
return this.regexCondition(jsonField, this.addValue(ctx.values, value));
|
|
647
655
|
case '$in':
|
|
648
656
|
case '$nin':
|
|
649
657
|
return this.jsonInNin(ctx, jsonField, comparand, op, value, asJson);
|
|
@@ -679,7 +687,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
679
687
|
*/
|
|
680
688
|
jsonContainmentIsPartial = true;
|
|
681
689
|
/**
|
|
682
|
-
* Whether an exploded *scalar* element keeps its SQL type. SQLite's `
|
|
690
|
+
* Whether an exploded *scalar* element keeps its SQL type. SQLite's `JSON_EACH` yields JSON
|
|
683
691
|
* booleans as `0`/`1` integers and numbers as numbers, so such an element compares directly to a
|
|
684
692
|
* bound value; PostgreSQL and MySQL explode scalars to text, losing the type, so a non-string
|
|
685
693
|
* operand there has to compare as JSON (see {@link isJsonbOp}).
|
|
@@ -737,15 +745,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
737
745
|
return this.addValue(ctx.values, value);
|
|
738
746
|
}
|
|
739
747
|
ctx.pushValue(JSON.stringify(value));
|
|
740
|
-
|
|
748
|
+
// The placeholder for the value just pushed, so a named or numbered one is spelled correctly.
|
|
749
|
+
return this.jsonCast(this.placeholder(ctx.values.length));
|
|
741
750
|
}
|
|
742
751
|
/** {@link resolveOperandField}, appended. */
|
|
743
752
|
getComparisonKey(ctx, entity, key, opts = {}) {
|
|
744
753
|
ctx.append(this.resolveOperandField(ctx, entity, key, opts));
|
|
745
754
|
}
|
|
755
|
+
/** Appends the `ORDER BY`, reporting whether there was one - which {@link pager} needs on the
|
|
756
|
+
* engines that refuse to page an unordered statement. */
|
|
746
757
|
sort(ctx, entity, sort, opts = {}) {
|
|
747
758
|
if (!hasKeys(sort)) {
|
|
748
|
-
return;
|
|
759
|
+
return false;
|
|
749
760
|
}
|
|
750
761
|
// Collected before anything is appended so an unorderable key is reported instead of half a
|
|
751
762
|
// clause, and because a vector distance is the primary ordering wherever it appears in the map.
|
|
@@ -756,6 +767,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
756
767
|
if (terms.length) {
|
|
757
768
|
ctx.append(` ORDER BY ${terms.join(', ')}`);
|
|
758
769
|
}
|
|
770
|
+
return terms.length > 0;
|
|
759
771
|
}
|
|
760
772
|
/**
|
|
761
773
|
* Walks `$sort` against the metadata of the entity each level addresses, rather than flattening it
|
|
@@ -815,7 +827,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
815
827
|
const json = this.resolveJsonDotPath(meta, key, prefix);
|
|
816
828
|
return json ? this.jsonPathExpr(json.column, json.jsonPath, 'text') : this.escapeId(key);
|
|
817
829
|
}
|
|
818
|
-
|
|
830
|
+
/**
|
|
831
|
+
* `LIMIT`/`OFFSET`. `sorted` says whether an `ORDER BY` was emitted just before, which
|
|
832
|
+
* {@link MergeSqlDialect} needs: SQL Server refuses to page a statement that has none.
|
|
833
|
+
*/
|
|
834
|
+
pager(ctx, opts, _sorted = false) {
|
|
819
835
|
// `!== undefined`, not truthiness: `$limit: 0` asks for no rows, where "unset" means every row.
|
|
820
836
|
if (opts.$limit !== undefined) {
|
|
821
837
|
ctx.append(` LIMIT ${assertNonNegativeInteger(opts.$limit, '$limit')}`);
|
|
@@ -847,6 +863,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
847
863
|
throw new TypeError(`${this.dialectName} cannot narrow a row lock to one table, so $lock cannot be combined with a joined relation`);
|
|
848
864
|
}
|
|
849
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* The lock as a hint on the table itself, for the engine that has no trailing `FOR UPDATE`. Empty
|
|
868
|
+
* everywhere else, which is where {@link appendLock} does the work instead - the two are the same
|
|
869
|
+
* lock spelled at opposite ends of the statement, so exactly one of them ever emits.
|
|
870
|
+
*/
|
|
871
|
+
lockHint(_q) {
|
|
872
|
+
return '';
|
|
873
|
+
}
|
|
850
874
|
/**
|
|
851
875
|
* The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
|
|
852
876
|
* joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
|
|
@@ -946,8 +970,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
946
970
|
if (q.$having) {
|
|
947
971
|
this.having(ctx, q.$having, emittedColumns);
|
|
948
972
|
}
|
|
949
|
-
this.aggregateSort(ctx, q.$sort, emittedColumns);
|
|
950
|
-
this.pager(ctx, q);
|
|
973
|
+
const sorted = this.aggregateSort(ctx, q.$sort, emittedColumns);
|
|
974
|
+
this.pager(ctx, q, sorted);
|
|
951
975
|
}
|
|
952
976
|
/**
|
|
953
977
|
* ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
|
|
@@ -956,13 +980,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
956
980
|
*/
|
|
957
981
|
aggregateSort(ctx, sort, emittedColumns) {
|
|
958
982
|
if (!hasKeys(sort))
|
|
959
|
-
return;
|
|
983
|
+
return false;
|
|
960
984
|
ctx.append(' ORDER BY ');
|
|
961
985
|
Object.entries(sort).forEach(([key, dir], index) => {
|
|
962
986
|
if (index > 0)
|
|
963
987
|
ctx.append(', ');
|
|
964
988
|
ctx.append(this.aggregateRef(emittedColumns, key, '$sort') + this.resolveSortDirection(dir));
|
|
965
989
|
});
|
|
990
|
+
return true;
|
|
966
991
|
}
|
|
967
992
|
/** The SQL referencing one of an aggregate's emitted columns, rejecting any other name. */
|
|
968
993
|
aggregateRef(emittedColumns, key, clause) {
|
|
@@ -1021,22 +1046,48 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1021
1046
|
this.appendLock(ctx, entity, q, joins);
|
|
1022
1047
|
}
|
|
1023
1048
|
insert(ctx, entity, payload, opts) {
|
|
1024
|
-
this.appendInsertValues(ctx, entity, payload);
|
|
1025
1049
|
// Every engine whose ids come back from the statement itself wants the same clause, so it is
|
|
1026
|
-
//
|
|
1050
|
+
// built once here instead of in an identical `insert` override per dialect. `returningId` is
|
|
1027
1051
|
// empty on a composite key, which has no id to ask for.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1052
|
+
const returning = this.insertIdSource === 'returning' ? this.returningId(getMeta(entity)) : '';
|
|
1053
|
+
if (returning && this.returningPosition === 'after-target') {
|
|
1054
|
+
this.appendInsertValues(ctx, entity, payload, returning);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
this.appendInsertValues(ctx, entity, payload);
|
|
1058
|
+
if (returning) {
|
|
1059
|
+
ctx.append(` ${returning}`);
|
|
1033
1060
|
}
|
|
1034
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Where the clause reporting an insert's generated ids goes. `suffix` is `RETURNING ...` at the end
|
|
1064
|
+
* of the statement, which every engine here but one spells that way; SQL Server's `OUTPUT` has no
|
|
1065
|
+
* trailing form and sits between the column list and `VALUES`.
|
|
1066
|
+
*
|
|
1067
|
+
* A knob rather than a pair of hooks: one concept decides where the string {@link returningId}
|
|
1068
|
+
* already built ends up, so the two ends cannot disagree.
|
|
1069
|
+
*/
|
|
1070
|
+
returningPosition = 'suffix';
|
|
1035
1071
|
/**
|
|
1036
1072
|
* `INSERT INTO ... VALUES (...)` and nothing more. The upsert builders extend this rather than
|
|
1037
1073
|
* {@link insert}: their own clause has to come before the `RETURNING`, not after it.
|
|
1038
1074
|
*/
|
|
1039
|
-
appendInsertValues(ctx, entity, payload
|
|
1075
|
+
appendInsertValues(ctx, entity, payload,
|
|
1076
|
+
/** Spliced between the column list and `VALUES`; see {@link returningPosition}. */
|
|
1077
|
+
afterTarget = '') {
|
|
1078
|
+
const shape = this.insertShape(entity, payload);
|
|
1079
|
+
const tableName = this.escapedTableName(getMeta(entity));
|
|
1080
|
+
ctx.append(`INSERT INTO ${tableName} (${shape.columns.join(', ')})${afterTarget ? ` ${afterTarget}` : ''} VALUES `);
|
|
1081
|
+
this.appendValueRows(ctx, shape);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* The columns an insert writes and the records it writes them from, resolved once.
|
|
1085
|
+
*
|
|
1086
|
+
* Split out of {@link appendInsertValues} because a `MERGE` needs the same rows as a `VALUES` row
|
|
1087
|
+
* source rather than as an `INSERT`, and both have to apply `onInsert` defaults and the
|
|
1088
|
+
* JSON/vector binding rules identically.
|
|
1089
|
+
*/
|
|
1090
|
+
insertShape(entity, payload) {
|
|
1040
1091
|
const meta = getMeta(entity);
|
|
1041
1092
|
const payloads = fillOnFields(meta, payload, 'onInsert');
|
|
1042
1093
|
const keys = getInsertFieldKeys(meta, payloads);
|
|
@@ -1053,12 +1104,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1053
1104
|
columns[i] = this.escapedColumnName(meta, key);
|
|
1054
1105
|
kinds[i] = this.persistKind(field);
|
|
1055
1106
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1107
|
+
return { meta, payloads, keys, fields, columns, kinds };
|
|
1108
|
+
}
|
|
1109
|
+
/** `(a, b), (c, d)` - the row constructor an INSERT and a MERGE source both write. */
|
|
1110
|
+
appendValueRows(ctx, { payloads, keys, fields, kinds }) {
|
|
1111
|
+
const width = keys.length;
|
|
1058
1112
|
for (let r = 0; r < payloads.length; r++) {
|
|
1059
|
-
|
|
1060
|
-
ctx.append('), (');
|
|
1061
|
-
}
|
|
1113
|
+
ctx.append(r > 0 ? '), (' : '(');
|
|
1062
1114
|
const record = payloads[r];
|
|
1063
1115
|
for (let i = 0; i < width; i++) {
|
|
1064
1116
|
if (i > 0) {
|
|
@@ -1597,7 +1649,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1597
1649
|
}
|
|
1598
1650
|
/**
|
|
1599
1651
|
* Build a complete `$size` comparison expression.
|
|
1600
|
-
* @param sizeExprFn - function that appends the size expression to ctx (e.g. `
|
|
1652
|
+
* @param sizeExprFn - function that appends the size expression to ctx (e.g. `JSONB_ARRAY_LENGTH("col")`)
|
|
1601
1653
|
*/
|
|
1602
1654
|
buildSizeComparison(ctx, sizeExprFn, sizeVal) {
|
|
1603
1655
|
if (typeof sizeVal === 'number') {
|
|
@@ -1677,6 +1729,15 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1677
1729
|
get regexpOp() {
|
|
1678
1730
|
return 'REGEXP';
|
|
1679
1731
|
}
|
|
1732
|
+
/**
|
|
1733
|
+
* The `$regex` predicate. An infix operator on the MySQL family (`REGEXP`) and the Postgres one
|
|
1734
|
+
* (`~`), but a function on Oracle and SQL Server 2025 (`REGEXP_LIKE(col, ?)`) - which is why this
|
|
1735
|
+
* is a method rather than the operator token alone. An engine with no regex at all overrides it to
|
|
1736
|
+
* throw, the way {@link appendTextSearch} already does.
|
|
1737
|
+
*/
|
|
1738
|
+
regexCondition(operand, placeholder) {
|
|
1739
|
+
return `${operand} ${this.regexpOp} ${placeholder}`;
|
|
1740
|
+
}
|
|
1680
1741
|
get likeFn() {
|
|
1681
1742
|
return 'LIKE';
|
|
1682
1743
|
}
|
|
@@ -32,6 +32,8 @@ export declare const REL_NESTED_KEY = "_uql_target";
|
|
|
32
32
|
* syntax and keeps `VALUES(col)`.
|
|
33
33
|
*/
|
|
34
34
|
export declare const UPSERT_NEW_ROW_ALIAS = "_uql_new";
|
|
35
|
+
/** The row source a `MERGE` upsert reads its incoming values from, on SQL Server and Oracle. */
|
|
36
|
+
export declare const UPSERT_SOURCE_ALIAS = "_uql_src";
|
|
35
37
|
/**
|
|
36
38
|
* Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
|
|
37
39
|
* `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -32,6 +32,8 @@ export const REL_NESTED_KEY = '_uql_target';
|
|
|
32
32
|
* syntax and keeps `VALUES(col)`.
|
|
33
33
|
*/
|
|
34
34
|
export const UPSERT_NEW_ROW_ALIAS = '_uql_new';
|
|
35
|
+
/** The row source a `MERGE` upsert reads its incoming values from, on SQL Server and Oracle. */
|
|
36
|
+
export const UPSERT_SOURCE_ALIAS = '_uql_src';
|
|
35
37
|
/**
|
|
36
38
|
* Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
|
|
37
39
|
* `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
|
|
@@ -7,7 +7,7 @@ import type { FieldOptions, FieldType } from '../type/index.js';
|
|
|
7
7
|
export declare function jsonPath(path: string, suffix?: string): string;
|
|
8
8
|
/**
|
|
9
9
|
* `FN(target, path, value, ...)` - the multi-pair JSON assignment shape shared by MySQL's
|
|
10
|
-
* `JSON_SET` and SQLite's `
|
|
10
|
+
* `JSON_SET` and SQLite's `JSON_SET`/`JSON_INSERT`. `pathSuffix` appends an accessor per key
|
|
11
11
|
* (SQLite's `[#]` append). Values bind in key order through `bindValue`, the caller's
|
|
12
12
|
* `jsonScalarParam` bound to its `QueryContext`.
|
|
13
13
|
*/
|
|
@@ -54,7 +54,7 @@ export declare function jsonTypeMode(type: FieldType): JsonAccessMode;
|
|
|
54
54
|
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
55
55
|
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
56
56
|
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
57
|
-
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `
|
|
57
|
+
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `JSON_EXTRACT` yields
|
|
58
58
|
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
59
59
|
*
|
|
60
60
|
* Numbers stay on the text accessor with a numeric cast, which keeps `1` equal to `1.0` - JSON
|
package/dist/dialect/jsonSql.js
CHANGED
|
@@ -11,7 +11,7 @@ export function jsonPath(path, suffix = '') {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* `FN(target, path, value, ...)` - the multi-pair JSON assignment shape shared by MySQL's
|
|
14
|
-
* `JSON_SET` and SQLite's `
|
|
14
|
+
* `JSON_SET` and SQLite's `JSON_SET`/`JSON_INSERT`. `pathSuffix` appends an accessor per key
|
|
15
15
|
* (SQLite's `[#]` append). Values bind in key order through `bindValue`, the caller's
|
|
16
16
|
* `jsonScalarParam` bound to its `QueryContext`.
|
|
17
17
|
*/
|
|
@@ -77,7 +77,7 @@ export function jsonTypeMode(type) {
|
|
|
77
77
|
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
78
78
|
* do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
|
|
79
79
|
* a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
|
|
80
|
-
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `
|
|
80
|
+
* MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `JSON_EXTRACT` yields
|
|
81
81
|
* `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
|
|
82
82
|
*
|
|
83
83
|
* Numbers stay on the text accessor with a numeric cast, which keeps `1` equal to `1.0` - JSON
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { QueryConflictPaths, QueryContext, QueryPager, Type } from '../type/index.js';
|
|
2
|
+
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared SQL between SQL Server and Oracle: the two engines that spell paging and upsert the way the
|
|
5
|
+
* standard does, where the Postgres and MySQL families each predate it.
|
|
6
|
+
*
|
|
7
|
+
* A family base rather than a pair of knobs, the way {@link PgLikeSqlDialect} and
|
|
8
|
+
* {@link MysqlLikeSqlDialect} already are - `pager` and `upsert` are both plain overrides, so nothing
|
|
9
|
+
* in the core has to learn that a second spelling exists.
|
|
10
|
+
*/
|
|
11
|
+
export declare abstract class MergeSqlDialect extends AbstractSqlDialect {
|
|
12
|
+
readonly escapeIdChar = "\"";
|
|
13
|
+
/**
|
|
14
|
+
* `OFFSET ... ROWS FETCH NEXT ... ROWS ONLY`, and an `ORDER BY` where the statement has none.
|
|
15
|
+
*
|
|
16
|
+
* SQL Server refuses to page an unordered statement. A constant `ORDER BY` costs one clause the
|
|
17
|
+
* optimizer discards and keeps `$limit` and `$skip` meaning the same thing here as everywhere
|
|
18
|
+
* else; the alternative, `TOP (n)` in the select list, needs a second hook and still leaves a
|
|
19
|
+
* `$skip` with no `$sort` unanswerable.
|
|
20
|
+
*/
|
|
21
|
+
pager(ctx: QueryContext, opts: QueryPager & {
|
|
22
|
+
$distinct?: boolean;
|
|
23
|
+
}, sorted?: boolean): void;
|
|
24
|
+
/**
|
|
25
|
+
* `MERGE`, which both engines take in place of the `ON CONFLICT`/`ON DUPLICATE KEY` the other
|
|
26
|
+
* families have. The rows go in as a `VALUES` row source rather than an `INSERT`, built by
|
|
27
|
+
* {@link AbstractSqlDialect.insertShape} so both shapes apply `onInsert` defaults identically.
|
|
28
|
+
*
|
|
29
|
+
* Every value binds before the assignments are rendered, so a `?`-placeholder engine needs none of
|
|
30
|
+
* the scratch-context reordering `ON CONFLICT` does - the source is read positionally, in order.
|
|
31
|
+
*/
|
|
32
|
+
upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
|
|
33
|
+
/** `<target>.<col> = <source>.<col>` for every conflict key, which is what makes a row "the same". */
|
|
34
|
+
private mergeOn;
|
|
35
|
+
/**
|
|
36
|
+
* A lock hint on the merge target. `MERGE` takes an update key lock but releases it before the
|
|
37
|
+
* insert, so two concurrent upserts of the same key race into a duplicate-key error; `HOLDLOCK`
|
|
38
|
+
* holds it across both. Empty on Oracle, which does not have the hint and does not need it.
|
|
39
|
+
*/
|
|
40
|
+
protected readonly mergeTargetHint: string;
|
|
41
|
+
/** How the merge reports the row it wrote. Oracle has no such clause on a `MERGE` and omits it. */
|
|
42
|
+
protected mergeReturning(expression: string): string;
|
|
43
|
+
/** `MERGE` must be terminated on SQL Server; nothing else here cares. */
|
|
44
|
+
protected readonly statementTerminator: string;
|
|
45
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { assertNonNegativeInteger, getKeys } from '../util/index.js';
|
|
3
|
+
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
4
|
+
import { UPSERT_SOURCE_ALIAS } from './aliases.js';
|
|
5
|
+
/**
|
|
6
|
+
* Shared SQL between SQL Server and Oracle: the two engines that spell paging and upsert the way the
|
|
7
|
+
* standard does, where the Postgres and MySQL families each predate it.
|
|
8
|
+
*
|
|
9
|
+
* A family base rather than a pair of knobs, the way {@link PgLikeSqlDialect} and
|
|
10
|
+
* {@link MysqlLikeSqlDialect} already are - `pager` and `upsert` are both plain overrides, so nothing
|
|
11
|
+
* in the core has to learn that a second spelling exists.
|
|
12
|
+
*/
|
|
13
|
+
export class MergeSqlDialect extends AbstractSqlDialect {
|
|
14
|
+
escapeIdChar = '"';
|
|
15
|
+
/**
|
|
16
|
+
* `OFFSET ... ROWS FETCH NEXT ... ROWS ONLY`, and an `ORDER BY` where the statement has none.
|
|
17
|
+
*
|
|
18
|
+
* SQL Server refuses to page an unordered statement. A constant `ORDER BY` costs one clause the
|
|
19
|
+
* optimizer discards and keeps `$limit` and `$skip` meaning the same thing here as everywhere
|
|
20
|
+
* else; the alternative, `TOP (n)` in the select list, needs a second hook and still leaves a
|
|
21
|
+
* `$skip` with no `$sort` unanswerable.
|
|
22
|
+
*/
|
|
23
|
+
pager(ctx, opts, sorted = false) {
|
|
24
|
+
if (opts.$limit === undefined && opts.$skip === undefined) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!sorted) {
|
|
28
|
+
// A `SELECT DISTINCT` may only order by something it projects, so the constant cannot be used
|
|
29
|
+
// there; the first projected column is the one term always available.
|
|
30
|
+
ctx.append(` ORDER BY ${opts.$distinct ? '1' : '(SELECT NULL)'}`);
|
|
31
|
+
}
|
|
32
|
+
ctx.append(` OFFSET ${assertNonNegativeInteger(opts.$skip ?? 0, '$skip')} ROWS`);
|
|
33
|
+
if (opts.$limit !== undefined) {
|
|
34
|
+
ctx.append(` FETCH NEXT ${assertNonNegativeInteger(opts.$limit, '$limit')} ROWS ONLY`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `MERGE`, which both engines take in place of the `ON CONFLICT`/`ON DUPLICATE KEY` the other
|
|
39
|
+
* families have. The rows go in as a `VALUES` row source rather than an `INSERT`, built by
|
|
40
|
+
* {@link AbstractSqlDialect.insertShape} so both shapes apply `onInsert` defaults identically.
|
|
41
|
+
*
|
|
42
|
+
* Every value binds before the assignments are rendered, so a `?`-placeholder engine needs none of
|
|
43
|
+
* the scratch-context reordering `ON CONFLICT` does - the source is read positionally, in order.
|
|
44
|
+
*/
|
|
45
|
+
upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
|
|
46
|
+
const meta = getMeta(entity);
|
|
47
|
+
const table = this.escapedTableName(meta);
|
|
48
|
+
const source = this.escapeId(UPSERT_SOURCE_ALIAS, true);
|
|
49
|
+
// Before the row source, which is what fills the payload's `onInsert` fields: a column that
|
|
50
|
+
// exists only there - the generated key, `createdAt` - must not join the update set, or a row
|
|
51
|
+
// that already existed has both rewritten.
|
|
52
|
+
const update = this.getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, (col) => `${source}.${col}`);
|
|
53
|
+
const shape = this.insertShape(entity, payload);
|
|
54
|
+
const columns = shape.columns.join(', ');
|
|
55
|
+
ctx.append(`MERGE INTO ${table}${this.mergeTargetHint} USING (VALUES `);
|
|
56
|
+
this.appendValueRows(ctx, shape);
|
|
57
|
+
ctx.append(`) AS ${source} (${columns}) ON ${this.mergeOn(meta, conflictPaths, table, source)}`);
|
|
58
|
+
if (update) {
|
|
59
|
+
ctx.append(` WHEN MATCHED THEN UPDATE SET ${update}`);
|
|
60
|
+
}
|
|
61
|
+
ctx.append(` WHEN NOT MATCHED THEN INSERT (${columns}) VALUES (${shape.columns.map((col) => `${source}.${col}`).join(', ')})`);
|
|
62
|
+
const returning = [this.returningIdExpression(meta), extraReturning].filter(Boolean).join(', ');
|
|
63
|
+
if (returning) {
|
|
64
|
+
ctx.append(` ${this.mergeReturning(returning)}`);
|
|
65
|
+
}
|
|
66
|
+
ctx.append(this.statementTerminator);
|
|
67
|
+
}
|
|
68
|
+
/** `<target>.<col> = <source>.<col>` for every conflict key, which is what makes a row "the same". */
|
|
69
|
+
mergeOn(meta, conflictPaths, table, source) {
|
|
70
|
+
return getKeys(conflictPaths)
|
|
71
|
+
.map((key) => {
|
|
72
|
+
const column = this.escapeId(this.resolveColumnName(key, meta.fields[key]));
|
|
73
|
+
return `${table}.${column} = ${source}.${column}`;
|
|
74
|
+
})
|
|
75
|
+
.join(' AND ');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A lock hint on the merge target. `MERGE` takes an update key lock but releases it before the
|
|
79
|
+
* insert, so two concurrent upserts of the same key race into a duplicate-key error; `HOLDLOCK`
|
|
80
|
+
* holds it across both. Empty on Oracle, which does not have the hint and does not need it.
|
|
81
|
+
*/
|
|
82
|
+
mergeTargetHint = '';
|
|
83
|
+
/** How the merge reports the row it wrote. Oracle has no such clause on a `MERGE` and omits it. */
|
|
84
|
+
mergeReturning(expression) {
|
|
85
|
+
return `RETURNING ${expression}`;
|
|
86
|
+
}
|
|
87
|
+
/** `MERGE` must be terminated on SQL Server; nothing else here cares. */
|
|
88
|
+
statementTerminator = '';
|
|
89
|
+
}
|
|
@@ -37,7 +37,10 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
37
37
|
vectorIndexRequiresNotNull: false,
|
|
38
38
|
vectorSupportsLength: false,
|
|
39
39
|
supportsTimestamptz: false,
|
|
40
|
-
|
|
40
|
+
stringSizing: 'varchar',
|
|
41
|
+
supportsUnsigned: true,
|
|
42
|
+
multipleCascadePaths: true,
|
|
43
|
+
serverSideCursors: false,
|
|
41
44
|
};
|
|
42
45
|
/**
|
|
43
46
|
* `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
|
|
@@ -3,7 +3,7 @@ import { type ParentPartition } from '../util/relationQuery.util.js';
|
|
|
3
3
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
4
4
|
/**
|
|
5
5
|
* Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
|
|
6
|
-
* CockroachDB (wire- and SQL-compatible for everything below, including `
|
|
6
|
+
* CockroachDB (wire- and SQL-compatible for everything below, including `TO_TSVECTOR`/`TO_TSQUERY`
|
|
7
7
|
* and pgvector's `<=>`/`<->`/`<#>` distance operators, which CockroachDB implements natively).
|
|
8
8
|
* `xmax`-based upsert `created` detection is Postgres-only (CockroachDB has no `xmax`/`ctid`) and
|
|
9
9
|
* stays in {@link PostgresDialect}, along with the `vectorExtension`/`vectorIndexStyle` values that
|
|
@@ -64,8 +64,8 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
64
64
|
normalizeValue(value: unknown): unknown;
|
|
65
65
|
placeholder(index: number): string;
|
|
66
66
|
/**
|
|
67
|
-
* `
|
|
68
|
-
* (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `
|
|
67
|
+
* `TO_TSVECTOR(...) @@ WEBSEARCH_TO_TSQUERY(...)`. `WEBSEARCH_TO_TSQUERY` takes free-form user input
|
|
68
|
+
* (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`, which
|
|
69
69
|
* rejects anything unparseable - including a plain two-word search.
|
|
70
70
|
*/
|
|
71
71
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|