turbine-orm 0.81.0 → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/query/writes.js
CHANGED
|
@@ -10,17 +10,37 @@
|
|
|
10
10
|
* stay class-resident, reached through the ctx. See builder.ts for the thin
|
|
11
11
|
* delegating methods and the async execute wrappers.
|
|
12
12
|
*/
|
|
13
|
+
import { maxBindParameters, } from '../dialect.js';
|
|
13
14
|
import { NotFoundError, OptimisticLockError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
|
-
import { camelToSnake
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { camelToSnake } from '../schema.js';
|
|
16
|
+
import { isColumnRef } from './column-ref.js';
|
|
17
|
+
import { assertMutationWhereIdentifiesOneRow, expandCompoundUniqueWhere, otherCollidingUniqueKey, uniqueKeyNames, upsertCreateCollidingKey, upsertWhereMatchesCreate, } from './compound-unique.js';
|
|
18
|
+
import { compileSetExpr, expr as exprBuilders, exprNotAllowedMessage, isPlainExprShape, isSqlExpr, plainExprShapeMessage, resolveExcludedFromCreate, } from './expr.js';
|
|
19
|
+
import { isPlainRecord, isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
|
|
20
|
+
import { isJsonValue, serializeJsonValue } from './json-value.js';
|
|
21
|
+
import { pruneUndefinedArgs } from './normalize-args.js';
|
|
17
22
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
|
|
18
23
|
import { canonicalWriteEntries, coerceTemporalValue, resolveColumnName } from './utils.js';
|
|
19
24
|
import * as whereMod from './where.js';
|
|
20
25
|
/**
|
|
21
|
-
* Normalize one `data` value before it is bound as a write param.
|
|
26
|
+
* Normalize one `data` value before it is bound as a write param. This is THE
|
|
27
|
+
* write bind seam: create, createMany, update, updateMany, both halves of
|
|
28
|
+
* upsert, the cache-hit param collectors and (through those public methods)
|
|
29
|
+
* the nested-write engine all bind through it.
|
|
22
30
|
*
|
|
23
|
-
*
|
|
31
|
+
* JSON columns (json / jsonb):
|
|
32
|
+
*
|
|
33
|
+
* - A JS array is serialized with `JSON.stringify`. The drivers' default
|
|
34
|
+
* array serialization is a Postgres ARRAY literal, so `[]` was stored as the
|
|
35
|
+
* object `{}`, `["a"]` failed with 22P02, and `createMany`'s UNNEST
|
|
36
|
+
* transpose flattened `[1, 2, 3]` into three rows.
|
|
37
|
+
* - A {@link jsonValue} wrapper is serialized with `JSON.stringify`, which is
|
|
38
|
+
* how a JSON string or a JSON `null` is written.
|
|
39
|
+
* - A JS string is RAW JSON TEXT and passes through untouched, so
|
|
40
|
+
* `JSON.stringify(value)` callers keep storing the parsed value. A plain
|
|
41
|
+
* object passes through too: every driver already serializes it as JSON.
|
|
42
|
+
*
|
|
43
|
+
* Temporal columns, a JS `Date`:
|
|
24
44
|
*
|
|
25
45
|
* 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
|
|
26
46
|
* as a full ISO timestamp with the process offset and Postgres answers
|
|
@@ -30,45 +50,246 @@ import * as whereMod from './where.js';
|
|
|
30
50
|
* non-UTC process the stored value is wrong and (because the read path
|
|
31
51
|
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
32
52
|
* the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
|
|
33
|
-
* in query/utils.ts.
|
|
53
|
+
* in query/utils.ts. `timestamptz` is deliberately untouched: it stores a
|
|
54
|
+
* real instant and the driver's local-offset string is already correct.
|
|
55
|
+
*
|
|
56
|
+
* A plain object or an array bound to a known scalar column (text, int, uuid,
|
|
57
|
+
* ...) is refused with E003, and so is a `Map` or `Set` anywhere; see
|
|
58
|
+
* {@link assertScalarBindable} and {@link assertNotCollection}.
|
|
34
59
|
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
60
|
+
* Expressions (`expr.*`, `col()`) never bind as a value: the update paths
|
|
61
|
+
* compile them before reaching here, so one that arrives is in a position that
|
|
62
|
+
* takes values only (create data, `{ set }`) and is refused. An unbranded
|
|
63
|
+
* object shaped like one is refused on a non-JSON column on every engine.
|
|
37
64
|
*
|
|
38
|
-
* Every other value
|
|
39
|
-
*
|
|
40
|
-
* column
|
|
65
|
+
* Every other value is returned by identity, so a primitive costs one check
|
|
66
|
+
* and no lookup. The exception is a string on an engine that defines the
|
|
67
|
+
* dialect's `checkTextWrite` (SQLite), which is checked against its column's
|
|
68
|
+
* type first; see {@link checkTextWrite}.
|
|
41
69
|
*/
|
|
42
70
|
export function coerceWriteValue(qi, key, value) {
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
if (typeof value === 'string') {
|
|
72
|
+
if (qi.dialect.checkTextWrite !== undefined)
|
|
73
|
+
checkTextWrite(qi, key, value);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
if (value === null || typeof value !== 'object')
|
|
45
77
|
return value;
|
|
78
|
+
if (isSqlExpr(value) || isColumnRef(value)) {
|
|
79
|
+
throw new ValidationError(exprNotAllowedMessage(qi.table, key, value));
|
|
80
|
+
}
|
|
81
|
+
assertNotCollection(qi, key, value);
|
|
46
82
|
// THE key-resolution rule, shared with `toColumn` (the SQL side of this very
|
|
47
|
-
// statement) so the two can never disagree about which column a key names
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// cache-HIT param-collect path, where an unknown key must not turn into a
|
|
52
|
-
// different error than the build path already raises.
|
|
83
|
+
// statement) so the two can never disagree about which column a key names.
|
|
84
|
+
// Non-throwing, because this also runs on the cache-HIT param-collect path,
|
|
85
|
+
// where an unknown key must not turn into a different error than the build
|
|
86
|
+
// path already raises.
|
|
53
87
|
const column = resolveColumnName(qi.tableMeta, key);
|
|
54
|
-
|
|
88
|
+
const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
|
|
89
|
+
if (isJsonValue(value))
|
|
90
|
+
return jsonText(qi, key, value.value);
|
|
91
|
+
if (dbType !== undefined && whereMod.isJsonColumnType(qi, dbType)) {
|
|
92
|
+
return Array.isArray(value) ? jsonText(qi, key, value) : value;
|
|
93
|
+
}
|
|
94
|
+
if (isPlainExprShape(value)) {
|
|
95
|
+
throw new ValidationError(plainExprShapeMessage(qi.table, key, String(value.kind)));
|
|
96
|
+
}
|
|
97
|
+
assertScalarBindable(qi, key, dbType, value);
|
|
98
|
+
if (!(value instanceof Date) && !Array.isArray(value))
|
|
55
99
|
return value;
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
100
|
+
// A column missing from the type map resolves to undefined → no coercion.
|
|
101
|
+
// The gate is where.ts's, so a written Date and a compared Date take the
|
|
102
|
+
// same form: a `date` column the UTC date part on every SQL engine, a
|
|
103
|
+
// zone-less `timestamp` the UTC literal on PostgreSQL only.
|
|
104
|
+
return coerceTemporalValue(dbType, value, whereMod.utcDateTimeRewrite(qi, dbType));
|
|
60
105
|
}
|
|
61
106
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
* Ask the engine whether it stores `value` in the column `key` names as that
|
|
108
|
+
* column's type (the dialect's `checkTextWrite`); E003 when it would not.
|
|
109
|
+
* An unknown key is left to the build path's own error, as in
|
|
110
|
+
* {@link coerceWriteValue}.
|
|
111
|
+
*/
|
|
112
|
+
function checkTextWrite(qi, key, value) {
|
|
113
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
114
|
+
const columnType = column ? qi.columnPgTypeMap.get(column) : undefined;
|
|
115
|
+
if (!column || columnType === undefined)
|
|
116
|
+
return;
|
|
117
|
+
const refusal = qi.dialect.checkTextWrite?.(qi.table, column, columnType, value);
|
|
118
|
+
if (refusal !== undefined)
|
|
119
|
+
throw new ValidationError(refusal);
|
|
120
|
+
}
|
|
121
|
+
/** `JSON.stringify(value)` for a JSON column, or E003 when it has no JSON form. */
|
|
122
|
+
function jsonText(qi, key, value) {
|
|
123
|
+
assertNotCollection(qi, key, value);
|
|
124
|
+
const text = serializeJsonValue(value);
|
|
125
|
+
if (text === undefined) {
|
|
126
|
+
throw new ValidationError(`Cannot write "${qi.table}.${key}": the value has no JSON form (${describeValue(value)}). ` +
|
|
127
|
+
'Convert it first, e.g. a bigint to a string or a number.');
|
|
128
|
+
}
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
function describeValue(value) {
|
|
132
|
+
return typeof value === 'bigint' ? 'it contains a bigint' : `got ${typeof value}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* PostgreSQL column types on which a plain object or an array can only be a
|
|
136
|
+
* mistake. The driver serializes an object as JSON text and an array as an
|
|
137
|
+
* ARRAY literal, so on a text column `{ first, last }` was stored as
|
|
138
|
+
* `{"first":"A","last":"B"}` and `['a']` as `{"a"}`, silently. A type outside
|
|
139
|
+
* this list (enums, domains, extension and user types) is never refused here,
|
|
140
|
+
* because a domain over jsonb or an extension type may legitimately take one.
|
|
69
141
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
142
|
+
const SCALAR_PG_TYPES = new Set([
|
|
143
|
+
'text',
|
|
144
|
+
'varchar',
|
|
145
|
+
'character varying',
|
|
146
|
+
'char',
|
|
147
|
+
'character',
|
|
148
|
+
'bpchar',
|
|
149
|
+
'citext',
|
|
150
|
+
'name',
|
|
151
|
+
'uuid',
|
|
152
|
+
'int2',
|
|
153
|
+
'int4',
|
|
154
|
+
'int8',
|
|
155
|
+
'smallint',
|
|
156
|
+
'integer',
|
|
157
|
+
'bigint',
|
|
158
|
+
'numeric',
|
|
159
|
+
'decimal',
|
|
160
|
+
'float4',
|
|
161
|
+
'float8',
|
|
162
|
+
'real',
|
|
163
|
+
'double precision',
|
|
164
|
+
'money',
|
|
165
|
+
'bool',
|
|
166
|
+
'boolean',
|
|
167
|
+
'date',
|
|
168
|
+
'time',
|
|
169
|
+
'timetz',
|
|
170
|
+
'timestamp',
|
|
171
|
+
'timestamptz',
|
|
172
|
+
'time without time zone',
|
|
173
|
+
'time with time zone',
|
|
174
|
+
'timestamp without time zone',
|
|
175
|
+
'timestamp with time zone',
|
|
176
|
+
'interval',
|
|
177
|
+
'bytea',
|
|
178
|
+
'inet',
|
|
179
|
+
'cidr',
|
|
180
|
+
'macaddr',
|
|
181
|
+
'xml',
|
|
182
|
+
]);
|
|
183
|
+
/**
|
|
184
|
+
* Column types, in every engine's spelling, that can only hold a scalar: a
|
|
185
|
+
* plain object or an array written to one is a mistake on every engine (the
|
|
186
|
+
* MySQL and SQLite shims stored it as JSON text, in an INTEGER column too).
|
|
187
|
+
*/
|
|
188
|
+
const NON_TEXT_SCALAR_TYPES = new Set([
|
|
189
|
+
'int',
|
|
190
|
+
'integer',
|
|
191
|
+
'tinyint',
|
|
192
|
+
'smallint',
|
|
193
|
+
'mediumint',
|
|
194
|
+
'bigint',
|
|
195
|
+
'int2',
|
|
196
|
+
'int4',
|
|
197
|
+
'int8',
|
|
198
|
+
'numeric',
|
|
199
|
+
'decimal',
|
|
200
|
+
'dec',
|
|
201
|
+
'real',
|
|
202
|
+
'float',
|
|
203
|
+
'double',
|
|
204
|
+
'double precision',
|
|
205
|
+
'money',
|
|
206
|
+
'smallmoney',
|
|
207
|
+
'bit',
|
|
208
|
+
'bool',
|
|
209
|
+
'boolean',
|
|
210
|
+
'date',
|
|
211
|
+
'time',
|
|
212
|
+
'datetime',
|
|
213
|
+
'datetime2',
|
|
214
|
+
'smalldatetime',
|
|
215
|
+
'datetimeoffset',
|
|
216
|
+
'timestamp',
|
|
217
|
+
'timestamptz',
|
|
218
|
+
'year',
|
|
219
|
+
'uuid',
|
|
220
|
+
'uniqueidentifier',
|
|
221
|
+
'blob',
|
|
222
|
+
'tinyblob',
|
|
223
|
+
'mediumblob',
|
|
224
|
+
'longblob',
|
|
225
|
+
'binary',
|
|
226
|
+
'varbinary',
|
|
227
|
+
'image',
|
|
228
|
+
]);
|
|
229
|
+
/**
|
|
230
|
+
* MySQL's text types. MySQL has a JSON column type, so a text column is not
|
|
231
|
+
* where an object goes, as on PostgreSQL. SQLite and SQL Server have no JSON
|
|
232
|
+
* type: Turbine's own DDL stores json in TEXT / NVARCHAR(MAX), so an object
|
|
233
|
+
* written to a text column there is serialized as JSON on purpose.
|
|
234
|
+
*/
|
|
235
|
+
const MYSQL_TEXT_TYPES = new Set([
|
|
236
|
+
'char',
|
|
237
|
+
'varchar',
|
|
238
|
+
'tinytext',
|
|
239
|
+
'text',
|
|
240
|
+
'mediumtext',
|
|
241
|
+
'longtext',
|
|
242
|
+
'enum',
|
|
243
|
+
'set',
|
|
244
|
+
]);
|
|
245
|
+
/** A column type token, lowercased, without a length, precision or sign modifier. */
|
|
246
|
+
function baseColumnType(dbType) {
|
|
247
|
+
return dbType
|
|
248
|
+
.toLowerCase()
|
|
249
|
+
.replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, '')
|
|
250
|
+
.replace(/\b(unsigned|signed|zerofill)\b/g, '')
|
|
251
|
+
.replace(/\s+/g, ' ')
|
|
252
|
+
.trim();
|
|
253
|
+
}
|
|
254
|
+
/** Whether this engine's column of `dbType` refuses a plain object or an array. */
|
|
255
|
+
function refusesStructuredValue(qi, dbType) {
|
|
256
|
+
const columns = qi.dialect.capabilities.structuredValueColumns;
|
|
257
|
+
if (columns === 'pg-scalars')
|
|
258
|
+
return SCALAR_PG_TYPES.has(dbType.toLowerCase());
|
|
259
|
+
const t = baseColumnType(dbType);
|
|
260
|
+
return NON_TEXT_SCALAR_TYPES.has(t) || (columns === 'scalars-and-text' && MYSQL_TEXT_TYPES.has(t));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Refuse a `Map` or `Set` as a written value, on every column. Neither has a
|
|
264
|
+
* stored form: JSON.stringify writes `{}` for both, so the entries were lost
|
|
265
|
+
* without an error, on a json column too.
|
|
266
|
+
*/
|
|
267
|
+
function assertNotCollection(qi, key, value) {
|
|
268
|
+
if (!(value instanceof Map) && !(value instanceof Set))
|
|
269
|
+
return;
|
|
270
|
+
const what = value instanceof Map ? 'a Map' : 'a Set';
|
|
271
|
+
throw new ValidationError(`Cannot write ${what} to "${qi.table}.${key}": it has no stored form, and JSON would write it as {} and lose ` +
|
|
272
|
+
`its entries. Convert it first (${value instanceof Map ? 'Object.fromEntries(map)' : '[...set]'}).`);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Refuse a plain object, or an array, bound to a known scalar column. Dates,
|
|
276
|
+
* Buffers, class instances (a Decimal wrapper, an interval) and every
|
|
277
|
+
* primitive are untouched, and so is every array column (`_text`) since its
|
|
278
|
+
* type is not a scalar one. Which text columns refuse one is per engine; see
|
|
279
|
+
* {@link MYSQL_TEXT_TYPES}.
|
|
280
|
+
*/
|
|
281
|
+
function assertScalarBindable(qi, key, dbType, value) {
|
|
282
|
+
if (dbType === undefined)
|
|
283
|
+
return;
|
|
284
|
+
const isArray = Array.isArray(value);
|
|
285
|
+
if (!isArray && !isUnmatchedPlainObject(value))
|
|
286
|
+
return;
|
|
287
|
+
if (!refusesStructuredValue(qi, dbType))
|
|
288
|
+
return;
|
|
289
|
+
throw new ValidationError(`Cannot write ${isArray ? 'an array' : 'an object'} to "${qi.table}.${key}" (column type ${dbType}). ` +
|
|
290
|
+
'Only json/jsonb columns take objects, and only array columns take arrays. ' +
|
|
291
|
+
'To store JSON text in this column, pass `JSON.stringify(value)` or `jsonValue(value)`.' +
|
|
292
|
+
(isArray ? '' : ' Atomic operators (`{ set: v }`, `{ increment: n }`, ...) apply in update data only.'));
|
|
72
293
|
}
|
|
73
294
|
/**
|
|
74
295
|
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
@@ -76,11 +297,157 @@ function utcDateTimeWrites(qi) {
|
|
|
76
297
|
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
77
298
|
* builder as reads, so no user value is interpolated.
|
|
78
299
|
*/
|
|
79
|
-
export function buildReselectByWhere(qi, whereObj, projection) {
|
|
300
|
+
export function buildReselectByWhere(qi, whereObj, projection, lockRows = false) {
|
|
80
301
|
const params = [];
|
|
81
302
|
const clause = whereMod.buildWhereClause(qi, whereObj, params);
|
|
82
303
|
const where = clause ? ` WHERE ${clause}` : '';
|
|
83
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}${lockRows ? forUpdate(qi) : ''}`,
|
|
306
|
+
params,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Row-lock suffix for a read inside a reselect write's transaction, so a
|
|
311
|
+
* concurrent write of the same row waits instead of interleaving. Appended
|
|
312
|
+
* where the engine's reselect locks rows (MySQL); elsewhere nothing is.
|
|
313
|
+
*/
|
|
314
|
+
function forUpdate(qi) {
|
|
315
|
+
return qi.dialect.capabilities.reselectLocksRows ? ' FOR UPDATE' : '';
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether a write that sets `columns` may move a row out of `whereObj`, so the
|
|
319
|
+
* row must be re-read by its key rather than by `where`: a soft delete under a
|
|
320
|
+
* `deletedAt: null` global filter, `NOT: { status: 'done' }` with `status:
|
|
321
|
+
* 'done'`, a key change.
|
|
322
|
+
*
|
|
323
|
+
* Read off the compiled predicate, where every column it reads is quoted at
|
|
324
|
+
* any depth (`NOT`, `OR`, a relation filter's correlation, a column reference,
|
|
325
|
+
* the global filter), so no where shape is missed. A column of another table
|
|
326
|
+
* with the same name answers true, which costs one locking read and is never
|
|
327
|
+
* wrong.
|
|
328
|
+
*/
|
|
329
|
+
function writeMayMoveRow(qi, whereObj, columns) {
|
|
330
|
+
if (columns.size === 0)
|
|
331
|
+
return false;
|
|
332
|
+
const clause = whereMod.buildWhereClause(qi, whereObj, []);
|
|
333
|
+
if (!clause)
|
|
334
|
+
return false;
|
|
335
|
+
for (const column of columns)
|
|
336
|
+
if (clause.includes(qi.q(column)))
|
|
337
|
+
return true;
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The columns that identify a row after a write: the primary key, else the
|
|
342
|
+
* first unique key, else none.
|
|
343
|
+
*/
|
|
344
|
+
function identityColumns(qi) {
|
|
345
|
+
return qi.tableMeta.primaryKey.length > 0 ? qi.tableMeta.primaryKey : (uniqueKeyNames(qi.tableMeta)[0] ?? []);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Lock the row(s) `whereObj` matches and return the first one's identity key as
|
|
349
|
+
* a `where`, with every key column `data` assigns replaced by its new value, so
|
|
350
|
+
* the row can be re-read after the write moves it. `null` when no row matched.
|
|
351
|
+
*
|
|
352
|
+
* Refused (E017) before anything is written: a table with no key to re-read by,
|
|
353
|
+
* a key column assigned by something whose result is not known here (an
|
|
354
|
+
* expression, `divide`, a non-integer operand), and, unless the call is a
|
|
355
|
+
* full-table write, a `where` that matched more than one row, which only an
|
|
356
|
+
* engine comparing a key loosely produces.
|
|
357
|
+
*/
|
|
358
|
+
async function lockMovedRowKey(qi, exec, whereObj, data, operation, manyRows) {
|
|
359
|
+
const identity = identityColumns(qi);
|
|
360
|
+
if (identity.length === 0) {
|
|
361
|
+
throw new UnsupportedFeatureError(`${operation} on "${qi.table}" that changes a column its \`where\` reads`, qi.dialect.name, 'The written row is read back by a key, and the table has no primary key or unique key. Give the table a ' +
|
|
362
|
+
'primary key, or leave the columns `where` reads out of data.');
|
|
363
|
+
}
|
|
364
|
+
const assigned = new Map();
|
|
365
|
+
for (const [key, value] of writeEntries(qi, data)) {
|
|
366
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
367
|
+
if (column !== undefined && identity.includes(column))
|
|
368
|
+
assigned.set(column, [key, value]);
|
|
369
|
+
}
|
|
370
|
+
const params = [];
|
|
371
|
+
const clause = whereMod.buildWhereClause(qi, whereObj, params);
|
|
372
|
+
const sql = `SELECT ${identity.map((c) => qi.q(c)).join(', ')} FROM ${qi.q(qi.table)}${clause ? ` WHERE ${clause}` : ''}${forUpdate(qi)}`;
|
|
373
|
+
const rows = (await exec(sql, params)).rows;
|
|
374
|
+
const row = rows[0];
|
|
375
|
+
if (!row)
|
|
376
|
+
return null;
|
|
377
|
+
if (!manyRows)
|
|
378
|
+
assertOneRowByKey(qi, operation, identity, rows.length);
|
|
379
|
+
const key = {};
|
|
380
|
+
for (const column of identity) {
|
|
381
|
+
const entry = assigned.get(column);
|
|
382
|
+
const next = entry === undefined ? row[column] : movedKeyValue(qi, operation, entry[0], row[column], entry[1]);
|
|
383
|
+
key[fieldOf(qi, column)] = reselectKeyValue(qi, column, next);
|
|
384
|
+
}
|
|
385
|
+
return key;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The value a key column holds after `assigned` is written over `current`: a
|
|
389
|
+
* plain value or `{ set }` as given, and `increment` / `decrement` / `multiply`
|
|
390
|
+
* by an integer computed exactly on an integer key. Anything else is refused
|
|
391
|
+
* (E017) because its result, and so the row, cannot be known before the write.
|
|
392
|
+
*/
|
|
393
|
+
function movedKeyValue(qi, operation, key, current, assigned) {
|
|
394
|
+
const op = setOperatorOf(qi, key, assigned);
|
|
395
|
+
if (op === null)
|
|
396
|
+
return assigned;
|
|
397
|
+
if (op === 'set')
|
|
398
|
+
return assigned.set;
|
|
399
|
+
const operand = op === 'expr' ? undefined : assigned[op];
|
|
400
|
+
const base = integerOf(current);
|
|
401
|
+
if (base !== undefined && typeof operand === 'number' && Number.isSafeInteger(operand) && op !== 'divide') {
|
|
402
|
+
const n = BigInt(operand);
|
|
403
|
+
const next = op === 'increment' ? base + n : op === 'decrement' ? base - n : base * n;
|
|
404
|
+
return typeof current === 'number' &&
|
|
405
|
+
next >= BigInt(Number.MIN_SAFE_INTEGER) &&
|
|
406
|
+
next <= BigInt(Number.MAX_SAFE_INTEGER)
|
|
407
|
+
? Number(next)
|
|
408
|
+
: typeof current === 'bigint'
|
|
409
|
+
? next
|
|
410
|
+
: next.toString();
|
|
411
|
+
}
|
|
412
|
+
throw new UnsupportedFeatureError(`${operation} on "${qi.table}" that changes the key "${key}" with ${op === 'expr' ? 'an expression' : `\`${op}\``}`, qi.dialect.name, 'The written row is read back by its key, and the new key is not known before the write. Read the row, then ' +
|
|
413
|
+
`write the new key with \`{ set: value }\`${op === 'expr' ? '' : ', or use an integer `increment`, `decrement` or `multiply`'}.`);
|
|
414
|
+
}
|
|
415
|
+
/** The columns the top level of `where` names, for a message. */
|
|
416
|
+
function whereColumns(qi, where) {
|
|
417
|
+
return Object.keys(where).flatMap((key) => {
|
|
418
|
+
const column = where[key] === undefined ? undefined : resolveColumnName(qi.tableMeta, key);
|
|
419
|
+
return column === undefined ? [] : [column];
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/** An integer key value as a bigint, or `undefined` for anything else. */
|
|
423
|
+
function integerOf(value) {
|
|
424
|
+
if (typeof value === 'bigint')
|
|
425
|
+
return value;
|
|
426
|
+
if (typeof value === 'number')
|
|
427
|
+
return Number.isSafeInteger(value) ? BigInt(value) : undefined;
|
|
428
|
+
if (typeof value === 'string' && /^-?[0-9]+$/.test(value))
|
|
429
|
+
return BigInt(value);
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* `where` with each top-level key value bound as a reselect binds keys (see
|
|
434
|
+
* {@link whereMod.TEXT_COMPARED_TYPES}), or `where` itself when nothing changes.
|
|
435
|
+
*/
|
|
436
|
+
function textSafeKeyWhere(qi, where) {
|
|
437
|
+
let out;
|
|
438
|
+
for (const [key, value] of Object.entries(where)) {
|
|
439
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
440
|
+
if (column === undefined)
|
|
441
|
+
continue;
|
|
442
|
+
const plain = isPlainRecord(value) && Object.keys(value).length === 1 && 'equals' in value;
|
|
443
|
+
const raw = plain ? value.equals : value;
|
|
444
|
+
const bound = reselectKeyValue(qi, column, raw);
|
|
445
|
+
if (bound === raw)
|
|
446
|
+
continue;
|
|
447
|
+
out ??= { ...where };
|
|
448
|
+
out[key] = plain ? { equals: bound } : bound;
|
|
449
|
+
}
|
|
450
|
+
return out ?? where;
|
|
84
451
|
}
|
|
85
452
|
/**
|
|
86
453
|
* Build the all-defaults INSERT for a `data` that names no column, via the
|
|
@@ -150,31 +517,181 @@ export function buildCreate(qi, args, projection) {
|
|
|
150
517
|
};
|
|
151
518
|
}
|
|
152
519
|
/**
|
|
153
|
-
* Build the `'reselect'` plan for {@link buildCreate}: run the INSERT,
|
|
154
|
-
* `SELECT
|
|
155
|
-
*
|
|
156
|
-
*
|
|
520
|
+
* Build the `'reselect'` plan for {@link buildCreate} (MySQL): run the INSERT,
|
|
521
|
+
* then `SELECT ... WHERE <key> = ?` by a key whose value is known exactly
|
|
522
|
+
* (see {@link createReselectKey}). Returns `undefined` unless the dialect's
|
|
523
|
+
* result strategy is `'reselect'`, so the RETURNING engines pay nothing.
|
|
157
524
|
*/
|
|
158
525
|
export function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
|
|
159
526
|
if (qi.dialect.resultStrategy !== 'reselect')
|
|
160
527
|
return undefined;
|
|
528
|
+
const key = createReselectKey(qi, data);
|
|
161
529
|
return async (exec) => {
|
|
162
530
|
const writeResult = await exec(insertSql, insertParams);
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
|
|
173
|
-
}
|
|
174
|
-
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
175
|
-
return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
531
|
+
const params = key.values.map((value) => value === INSERT_ID
|
|
532
|
+
? generatedKey(qi, writeResult, key)
|
|
533
|
+
: value === ZERO_INSERT_ID
|
|
534
|
+
? zeroKey(qi, writeResult)
|
|
535
|
+
: value);
|
|
536
|
+
const where = key.columns.map((c, i) => `${qi.q(c)} = ${qi.p(i + 1)}`).join(' AND ');
|
|
537
|
+
const result = await exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${where}`, params);
|
|
538
|
+
assertOneRowByKey(qi, 'create', key.columns, result.rows.length);
|
|
539
|
+
return result;
|
|
176
540
|
};
|
|
177
541
|
}
|
|
542
|
+
/** Stands for the driver's generated insert id in a {@link ReselectKey}. */
|
|
543
|
+
const INSERT_ID = Symbol('turbine.insertId');
|
|
544
|
+
/**
|
|
545
|
+
* Stands for a 0 written to an AUTO_INCREMENT key. MySQL reads 0 there as
|
|
546
|
+
* "assign the next value" (unless the session's sql_mode has
|
|
547
|
+
* NO_AUTO_VALUE_ON_ZERO, which stores 0), so the row is re-read by the value
|
|
548
|
+
* the server reports it stored. Re-reading by 0 found nothing, and the create
|
|
549
|
+
* that had just inserted a row failed as "not found".
|
|
550
|
+
*/
|
|
551
|
+
const ZERO_INSERT_ID = Symbol('turbine.zeroInsertId');
|
|
552
|
+
/**
|
|
553
|
+
* The key that finds the row a `create` inserted, decided before the INSERT is
|
|
554
|
+
* sent. Without RETURNING, MySQL reports only the AUTO_INCREMENT value it
|
|
555
|
+
* assigned (`insertId`, 0 when there is none), so a key the server fills any
|
|
556
|
+
* other way (`DEFAULT (UUID())`) cannot be read back: re-reading by 0 matched
|
|
557
|
+
* every CHAR key MySQL converts to the number 0, other tenants' rows included.
|
|
558
|
+
*
|
|
559
|
+
* In order: the primary key, when `data` supplies every member or the one it
|
|
560
|
+
* omits is server-numbered ({@link isServerNumberedKey}); else the first unique
|
|
561
|
+
* key `data` supplies in full. Anything else is refused (E017) before the
|
|
562
|
+
* INSERT, naming the fix. A NULL never identifies a row, so it counts as absent.
|
|
563
|
+
*/
|
|
564
|
+
function createReselectKey(qi, data) {
|
|
565
|
+
const supplied = new Map();
|
|
566
|
+
for (const [key, value] of writeEntries(qi, data)) {
|
|
567
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
568
|
+
// Same rewrite the INSERT's own params got, then the text-key rule.
|
|
569
|
+
if (column !== undefined && value !== null) {
|
|
570
|
+
supplied.set(column, reselectKeyValue(qi, column, coerceWriteValue(qi, key, value)));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const pk = qi.tableMeta.primaryKey;
|
|
574
|
+
const missing = pk.filter((c) => !supplied.has(c));
|
|
575
|
+
if (pk.length > 0 && (missing.length === 0 || (missing.length === 1 && isServerNumberedKey(qi, missing[0])))) {
|
|
576
|
+
return {
|
|
577
|
+
columns: pk,
|
|
578
|
+
values: pk.map((c) => {
|
|
579
|
+
if (!supplied.has(c))
|
|
580
|
+
return INSERT_ID;
|
|
581
|
+
const value = supplied.get(c);
|
|
582
|
+
return isZeroKeyValue(value) && isServerNumberedKey(qi, c) ? ZERO_INSERT_ID : value;
|
|
583
|
+
}),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
for (const key of uniqueKeyNames(qi.tableMeta)) {
|
|
587
|
+
if (key.every((c) => supplied.has(c)))
|
|
588
|
+
return { columns: key, values: key.map((c) => supplied.get(c)) };
|
|
589
|
+
}
|
|
590
|
+
const fields = (missing.length > 0 ? missing : pk).map((c) => `"${fieldOf(qi, c)}"`).join(', ');
|
|
591
|
+
throw new UnsupportedFeatureError(pk.length > 0
|
|
592
|
+
? `create on "${qi.table}" without ${fields} in data`
|
|
593
|
+
: `create on "${qi.table}", which has no primary key,`, qi.dialect.name, 'The inserted row is read back by a key, and the engine reports a generated value only for an ' +
|
|
594
|
+
'AUTO_INCREMENT column. ' +
|
|
595
|
+
(pk.length > 0
|
|
596
|
+
? `Pass ${fields} in data (for a UUID key, generate it client-side, e.g. crypto.randomUUID()), or make the key AUTO_INCREMENT.`
|
|
597
|
+
: 'Supply every column of a unique key in data, or give the table a primary key.'));
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Whether the server numbers this key column itself, so the driver's insert id
|
|
601
|
+
* is its new value. `isGenerated` decides when the metadata records it;
|
|
602
|
+
* metadata that does not (hand-written, or an introspector that leaves it
|
|
603
|
+
* unset) counts an integer column with a default.
|
|
604
|
+
*/
|
|
605
|
+
function isServerNumberedKey(qi, column) {
|
|
606
|
+
const col = qi.tableMeta.columns.find((c) => c.name === column);
|
|
607
|
+
if (!col)
|
|
608
|
+
return false;
|
|
609
|
+
if (col.isGenerated !== undefined)
|
|
610
|
+
return col.isGenerated;
|
|
611
|
+
return col.hasDefault && isIntegerColumnType(col.dialectType ?? col.pgType);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Whether a column's database type is an integer type, in the PostgreSQL,
|
|
615
|
+
* MySQL and code-first spellings. A MySQL display width or sign modifier
|
|
616
|
+
* (`int(11) unsigned`) is ignored, except that `tinyint(1)` is MySQL's
|
|
617
|
+
* BOOLEAN and is not counted.
|
|
618
|
+
*/
|
|
619
|
+
function isIntegerColumnType(dbType) {
|
|
620
|
+
if (!dbType)
|
|
621
|
+
return false;
|
|
622
|
+
const t = dbType.trim().toLowerCase();
|
|
623
|
+
if (t === 'tinyint(1)')
|
|
624
|
+
return false;
|
|
625
|
+
const base = t
|
|
626
|
+
.replace(/\s+(unsigned|signed|zerofill)\b/g, '')
|
|
627
|
+
.replace(/\s*\(\s*\d+\s*\)$/, '')
|
|
628
|
+
.trim();
|
|
629
|
+
return INTEGER_COLUMN_TYPES.has(base);
|
|
630
|
+
}
|
|
631
|
+
const INTEGER_COLUMN_TYPES = new Set([
|
|
632
|
+
'tinyint',
|
|
633
|
+
'smallint',
|
|
634
|
+
'mediumint',
|
|
635
|
+
'int',
|
|
636
|
+
'integer',
|
|
637
|
+
'bigint',
|
|
638
|
+
'int2',
|
|
639
|
+
'int4',
|
|
640
|
+
'int8',
|
|
641
|
+
'serial',
|
|
642
|
+
'bigserial',
|
|
643
|
+
'smallserial',
|
|
644
|
+
]);
|
|
645
|
+
function isZeroKeyValue(value) {
|
|
646
|
+
return value === 0 || value === 0n || (typeof value === 'string' && /^0+$/.test(value));
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* The stored value of a key written as 0 (see {@link ZERO_INSERT_ID}): the
|
|
650
|
+
* insert id the engine reports for the AUTO_INCREMENT column, which is the
|
|
651
|
+
* assigned value, or 0 when the server stored the 0 as written.
|
|
652
|
+
*/
|
|
653
|
+
function zeroKey(qi, result) {
|
|
654
|
+
const id = qi.mutationInsertId(result);
|
|
655
|
+
const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
|
|
656
|
+
(typeof id === 'bigint' && id > 0n) ||
|
|
657
|
+
(typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
|
|
658
|
+
return positive ? id : 0;
|
|
659
|
+
}
|
|
660
|
+
/** The insert id of `result` as a key value, or E017 when the engine reported none. */
|
|
661
|
+
function generatedKey(qi, result, key) {
|
|
662
|
+
const id = qi.mutationInsertId(result);
|
|
663
|
+
const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
|
|
664
|
+
(typeof id === 'bigint' && id > 0n) ||
|
|
665
|
+
(typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
|
|
666
|
+
if (positive)
|
|
667
|
+
return id;
|
|
668
|
+
const column = key.columns[key.values.indexOf(INSERT_ID)];
|
|
669
|
+
throw new UnsupportedFeatureError(`create on "${qi.table}" without "${fieldOf(qi, column)}" in data`, qi.dialect.name, `The engine reported no generated value for "${fieldOf(qi, column)}", so the inserted row cannot be read back ` +
|
|
670
|
+
`and the insert is rolled back. Pass "${fieldOf(qi, column)}" in data, or make it AUTO_INCREMENT.`);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* A key value as a reselect binds it: a number or bigint for a text-class
|
|
674
|
+
* column ({@link whereMod.TEXT_COMPARED_TYPES}) is sent as the text the INSERT
|
|
675
|
+
* stored, since MySQL compares `'0zz' = 0` as true. Converted on every engine,
|
|
676
|
+
* not only MySQL, because one caller (the identity read) is not MySQL-only.
|
|
677
|
+
*/
|
|
678
|
+
function reselectKeyValue(qi, column, value) {
|
|
679
|
+
if (typeof value !== 'number' && typeof value !== 'bigint')
|
|
680
|
+
return value;
|
|
681
|
+
const type = qi.columnPgTypeMap.get(column);
|
|
682
|
+
return type !== undefined && whereMod.TEXT_COMPARED_TYPES.has(type.toLowerCase()) ? String(value) : value;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* A re-read by a unique key finds one row. A second one means the engine
|
|
686
|
+
* matched the key loosely, so which row is the written one is unknown: refused
|
|
687
|
+
* (E017), which rolls the write back.
|
|
688
|
+
*/
|
|
689
|
+
function assertOneRowByKey(qi, operation, columns, rows) {
|
|
690
|
+
if (rows <= 1)
|
|
691
|
+
return;
|
|
692
|
+
throw new UnsupportedFeatureError(`${operation} on "${qi.table}"`, qi.dialect.name, `Re-reading the written row by (${columns.map((c) => fieldOf(qi, c)).join(', ')}) matched ${rows} rows, so the ` +
|
|
693
|
+
'written one cannot be told apart and the write is rolled back. Pass each key value with its column type.');
|
|
694
|
+
}
|
|
178
695
|
/**
|
|
179
696
|
* The `[key, value]` pairs a write's `data` object actually names, in the
|
|
180
697
|
* table's own column order.
|
|
@@ -262,14 +779,81 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
|
262
779
|
function quoteList(fields) {
|
|
263
780
|
return fields.map((f) => `"${f}"`).join(', ');
|
|
264
781
|
}
|
|
265
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Refuse a statement that carries more bind parameters than the engine takes.
|
|
784
|
+
* Only the row-major `VALUES` form can get there (PostgreSQL's UNNEST form
|
|
785
|
+
* binds one array per column), and past the limit the failure is a raw driver
|
|
786
|
+
* or protocol error. Executing in chunks would change what one `createMany`
|
|
787
|
+
* is (one statement, one RETURNING set), so the caller chooses the chunking.
|
|
788
|
+
*/
|
|
789
|
+
function assertWithinBindLimit(qi, paramCount, rowCount, columnCount) {
|
|
790
|
+
const limit = maxBindParameters(qi.dialect);
|
|
791
|
+
if (limit === undefined || paramCount <= limit)
|
|
792
|
+
return;
|
|
793
|
+
const perChunk = Math.max(1, Math.floor(limit / Math.max(1, columnCount)));
|
|
794
|
+
throw new ValidationError(`createMany on "${qi.table}": ${rowCount} rows of ${columnCount} columns need ${paramCount} bind parameters, ` +
|
|
795
|
+
`over the ${qi.dialect.name} limit of ${limit} per statement. ` +
|
|
796
|
+
`Split the rows into chunks of at most ${perChunk} and call createMany once per chunk ` +
|
|
797
|
+
'(inside one `$transaction` if they must succeed or fail together).');
|
|
798
|
+
}
|
|
799
|
+
export function buildCreateMany(qi, args, overBindLimit = 'refuse', projection) {
|
|
800
|
+
const prepared = prepareCreateMany(qi, args, projection);
|
|
801
|
+
if ('deferred' in prepared)
|
|
802
|
+
return prepared.deferred;
|
|
803
|
+
return emitCreateMany(qi, prepared, prepared.rowValues, overBindLimit, true);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* `createMany()` as the statements it runs: one when the rows fit one
|
|
807
|
+
* statement, else consecutive chunks that each do. The whole call is validated
|
|
808
|
+
* once, before any chunk is built, so a refusal (a duplicate conflict key in
|
|
809
|
+
* the last chunk included) sends nothing, and the chunks are sized before any
|
|
810
|
+
* statement is built, so no engine's own per-statement check refuses one.
|
|
811
|
+
*
|
|
812
|
+
* Built eagerly and synchronously: the chunks read state the call set on the
|
|
813
|
+
* builder (`skipGlobalFilters`), which a concurrent call could change across
|
|
814
|
+
* an await.
|
|
815
|
+
*/
|
|
816
|
+
export function planCreateMany(qi, args, projection) {
|
|
817
|
+
const prepared = prepareCreateMany(qi, args, projection);
|
|
818
|
+
if ('deferred' in prepared)
|
|
819
|
+
return { chunks: [prepared.deferred], rowsPerChunk: args.data.length };
|
|
820
|
+
const rows = prepared.rowValues;
|
|
821
|
+
const size = rowsPerStatement(qi, prepared);
|
|
822
|
+
if (rows.length <= size) {
|
|
823
|
+
return { chunks: [emitCreateMany(qi, prepared, rows, 'allow', true)], rowsPerChunk: size };
|
|
824
|
+
}
|
|
825
|
+
if (prepared.conflict && !prepared.identity) {
|
|
826
|
+
throw new ValidationError(`createMany onConflict on "${qi.table}": ${rows.length} rows take more than one statement here (at most ` +
|
|
827
|
+
`${size} per statement), and the rows each statement writes cannot be identified (onConflict.update assigns ` +
|
|
828
|
+
"the key, or the only key is a PII column), so two rows equal under the key's comparison rules could merge " +
|
|
829
|
+
`across statements unnoticed. Pass at most ${size} rows per call.`);
|
|
830
|
+
}
|
|
831
|
+
const chunks = [];
|
|
832
|
+
for (let i = 0; i < rows.length; i += size) {
|
|
833
|
+
chunks.push(emitCreateMany(qi, prepared, rows.slice(i, i + size), 'refuse', i === 0));
|
|
834
|
+
}
|
|
835
|
+
return { chunks, rowsPerChunk: size };
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Everything `createMany` checks about the call, over every row, or the whole
|
|
839
|
+
* answer when no row-values statement is needed (no rows, or rows of pure
|
|
840
|
+
* defaults).
|
|
841
|
+
*/
|
|
842
|
+
function prepareCreateMany(qi, args, projection) {
|
|
266
843
|
const qt = qi.q(qi.table);
|
|
844
|
+
// Validated on every call (UNSAFE only); it acts on onConflict's update.
|
|
845
|
+
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
846
|
+
// The data-independent half of onConflict is checked before the empty-data
|
|
847
|
+
// shortcut, so a call is never valid with no rows and invalid with some.
|
|
848
|
+
const conflict = args.onConflict === undefined ? null : resolveConflictPlan(qi, args);
|
|
267
849
|
if (args.data.length === 0) {
|
|
268
850
|
return {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
851
|
+
deferred: {
|
|
852
|
+
sql: `SELECT * FROM ${qt} WHERE false`,
|
|
853
|
+
params: [],
|
|
854
|
+
transform: () => [],
|
|
855
|
+
tag: `${qi.table}.createMany`,
|
|
856
|
+
},
|
|
273
857
|
};
|
|
274
858
|
}
|
|
275
859
|
assertWritable(qi, 'createMany');
|
|
@@ -278,22 +862,35 @@ export function buildCreateMany(qi, args) {
|
|
|
278
862
|
}
|
|
279
863
|
const keys = definedKeys(qi, args.data[0]);
|
|
280
864
|
assertUniformCreateManyRows(qi, args.data, keys);
|
|
865
|
+
const columns = keys.map((k) => qi.toColumn(k));
|
|
866
|
+
if (conflict)
|
|
867
|
+
assertConflictTargetSupplied(qi, conflict, columns);
|
|
281
868
|
// No column named by the first row: every row is pure defaults (the bulk
|
|
282
869
|
// counterpart of `create({ data: {} })`, see buildDefaultValuesInsert). The
|
|
283
870
|
// uniformity check above has already established that EVERY row is empty.
|
|
284
871
|
if (keys.length === 0) {
|
|
285
872
|
return {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
873
|
+
deferred: {
|
|
874
|
+
sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates, projection),
|
|
875
|
+
params: [],
|
|
876
|
+
transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
|
|
877
|
+
tag: `${qi.table}.createMany`,
|
|
878
|
+
},
|
|
290
879
|
};
|
|
291
880
|
}
|
|
292
|
-
const columns = keys.map((k) => qi.toColumn(k));
|
|
293
881
|
const rowValues = args.data.map((row) => {
|
|
294
882
|
const record = row;
|
|
295
883
|
return keys.map((key) => coerceWriteValue(qi, key, record[key]));
|
|
296
884
|
});
|
|
885
|
+
let identity = null;
|
|
886
|
+
if (conflict) {
|
|
887
|
+
// Over every row, so a chunked call refuses before its first chunk is sent.
|
|
888
|
+
assertNoDuplicateKeys(qi, conflict, columns, rowValues);
|
|
889
|
+
if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key') {
|
|
890
|
+
assertMysqlBulkUpsert(qi, conflict, columns, rowValues);
|
|
891
|
+
}
|
|
892
|
+
identity = conflictIdentity(qi, conflict, projection);
|
|
893
|
+
}
|
|
297
894
|
// Use actual Postgres types for array casts in the default PostgreSQL dialect.
|
|
298
895
|
// Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
|
|
299
896
|
// type the UNNEST output as text, which Postgres refuses to coerce to the
|
|
@@ -314,21 +911,450 @@ export function buildCreateMany(qi, args) {
|
|
|
314
911
|
// directly. Detected from the column's own declared type rather than from
|
|
315
912
|
// the shape of the first row's value, so a row whose array column happens to
|
|
316
913
|
// be null or absent still takes the correct form.
|
|
914
|
+
//
|
|
915
|
+
// json/jsonb columns stay on UNNEST. coerceWriteValue has already turned
|
|
916
|
+
// every JS array bound to one into JSON TEXT, so the transpose has no
|
|
917
|
+
// nested array to flatten (a raw `[1, 2, 3]` there used to become three
|
|
918
|
+
// rows), and every other cell is a string, object or null that the driver
|
|
919
|
+
// serializes as one array element.
|
|
317
920
|
const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
921
|
+
return {
|
|
922
|
+
conflict,
|
|
923
|
+
identity,
|
|
924
|
+
columns,
|
|
925
|
+
insert: {
|
|
926
|
+
table: qt,
|
|
927
|
+
columns: quotedColumns,
|
|
928
|
+
columnArrayTypes: typeCasts,
|
|
929
|
+
skipDuplicates: args.skipDuplicates,
|
|
930
|
+
returning: writeReturningColumns(qi, identity?.selection ?? projection),
|
|
931
|
+
requireRowValues: hasArrayColumn,
|
|
932
|
+
},
|
|
321
933
|
rowValues,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
934
|
+
projection,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* One `createMany` statement over `rowValues` (all of a prepared call's rows,
|
|
939
|
+
* or a chunk of them). `first` marks the call's first statement, where an
|
|
940
|
+
* onConflict call starts counting the rows it wrote.
|
|
941
|
+
*/
|
|
942
|
+
function emitCreateMany(qi, prepared, rowValues, overBindLimit, first) {
|
|
943
|
+
const { conflict, columns, projection } = prepared;
|
|
944
|
+
const insert = { ...prepared.insert, rowValues };
|
|
945
|
+
if (conflict) {
|
|
946
|
+
return buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, prepared.identity, first);
|
|
947
|
+
}
|
|
948
|
+
const built = qi.dialect.buildBulkInsertStatement(insert);
|
|
949
|
+
if (overBindLimit === 'refuse')
|
|
950
|
+
assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
|
|
951
|
+
return {
|
|
952
|
+
sql: built.sql,
|
|
953
|
+
params: built.params,
|
|
954
|
+
transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
|
|
955
|
+
tag: `${qi.table}.createMany`,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Rows one `createMany` statement may carry: the most that keep its bind
|
|
960
|
+
* parameters within the engine's limit and, on SQL Server, its rows within
|
|
961
|
+
* the 1000 a plain `INSERT ... VALUES` accepts. The parameter cost is measured
|
|
962
|
+
* from one-row and two-row builds, which separates the per-row parameters from
|
|
963
|
+
* the ones bound once per statement (an onConflict update's values); the
|
|
964
|
+
* PostgreSQL UNNEST form binds one array per column, so its rows never run out.
|
|
965
|
+
*/
|
|
966
|
+
function rowsPerStatement(qi, prepared) {
|
|
967
|
+
// An onConflict statement (SQL Server's MERGE) reads its rows as a derived
|
|
968
|
+
// table, which the plain INSERT ... VALUES row cap does not bind.
|
|
969
|
+
const cap = prepared.conflict ? Infinity : qi.dialect.capabilities.maxInsertRows;
|
|
970
|
+
const limit = maxBindParameters(qi.dialect);
|
|
971
|
+
const rows = prepared.rowValues;
|
|
972
|
+
if (limit === undefined || rows.length < 2)
|
|
973
|
+
return cap;
|
|
974
|
+
const one = emitCreateMany(qi, prepared, rows.slice(0, 1), 'allow', true).params.length;
|
|
975
|
+
const perRow = emitCreateMany(qi, prepared, rows.slice(0, 2), 'allow', true).params.length - one;
|
|
976
|
+
if (perRow <= 0)
|
|
977
|
+
return cap;
|
|
978
|
+
const fixed = Math.max(0, one - perRow);
|
|
979
|
+
return Math.max(1, Math.min(cap, Math.floor((limit - fixed) / perRow)));
|
|
980
|
+
}
|
|
981
|
+
const ON_CONFLICT_KEYS = new Set(['target', 'update', 'where', 'ignore']);
|
|
982
|
+
/**
|
|
983
|
+
* Validate the data-independent half of `onConflict`: the engine supports it,
|
|
984
|
+
* its shape, its target names exactly one unique key, and its update and
|
|
985
|
+
* where are usable. Every refusal is E003 except an engine gap (E017).
|
|
986
|
+
*/
|
|
987
|
+
function resolveConflictPlan(qi, args) {
|
|
988
|
+
const table = qi.table;
|
|
989
|
+
if (!qi.dialect.supportsBulkUpsert || typeof qi.dialect.buildBulkUpsertStatement !== 'function') {
|
|
990
|
+
throw new UnsupportedFeatureError('createMany onConflict', qi.dialect.name, 'This engine has no multi-row upsert statement. Call upsert() per row inside $transaction.');
|
|
991
|
+
}
|
|
992
|
+
if (args.skipDuplicates) {
|
|
993
|
+
throw new ValidationError(`createMany on "${table}": onConflict and skipDuplicates cannot be combined. skipDuplicates skips a ` +
|
|
994
|
+
'conflict on ANY unique key, onConflict names one; to skip conflicts on that key, use ' +
|
|
995
|
+
'onConflict: { target, ignore: true }.');
|
|
996
|
+
}
|
|
997
|
+
const oc = args.onConflict;
|
|
998
|
+
if (!isPlainRecord(oc)) {
|
|
999
|
+
throw new ValidationError(`createMany on "${table}": onConflict must be { target, update } or { target, ignore: true }.`);
|
|
1000
|
+
}
|
|
1001
|
+
const unknownKey = Object.keys(oc).find((k) => !ON_CONFLICT_KEYS.has(k) && oc[k] !== undefined);
|
|
1002
|
+
if (unknownKey !== undefined) {
|
|
1003
|
+
throw new ValidationError(`createMany on "${table}": onConflict has no option "${unknownKey}". It takes target, update, where and ignore.`);
|
|
1004
|
+
}
|
|
1005
|
+
const target = resolveConflictTarget(qi, oc.target);
|
|
1006
|
+
if (oc.ignore !== undefined && oc.ignore !== true) {
|
|
1007
|
+
throw new ValidationError(`createMany on "${table}": onConflict.ignore takes \`true\` only.`);
|
|
1008
|
+
}
|
|
1009
|
+
if (oc.ignore === true) {
|
|
1010
|
+
if (oc.update !== undefined || oc.where !== undefined) {
|
|
1011
|
+
throw new ValidationError(`createMany on "${table}": onConflict takes either ignore: true or update (with an optional where), not both.`);
|
|
1012
|
+
}
|
|
1013
|
+
return { target, update: null, where: null };
|
|
1014
|
+
}
|
|
1015
|
+
return { target, update: resolveConflictUpdate(qi, oc.update), where: resolveConflictWhere(qi, oc.where) };
|
|
1016
|
+
}
|
|
1017
|
+
function resolveConflictTarget(qi, raw) {
|
|
1018
|
+
const table = qi.table;
|
|
1019
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((f) => typeof f !== 'string')) {
|
|
1020
|
+
throw new ValidationError(`createMany on "${table}": onConflict.target must be a non-empty array of field names (the primary key or ` +
|
|
1021
|
+
`one unique key), e.g. target: ['email']. Valid targets: ${describeConflictTargets(qi)}.`);
|
|
1022
|
+
}
|
|
1023
|
+
const columns = [];
|
|
1024
|
+
for (const field of raw) {
|
|
1025
|
+
const column = resolveColumnName(qi.tableMeta, field);
|
|
1026
|
+
if (column === undefined) {
|
|
1027
|
+
throw new ValidationError(`createMany on "${table}": onConflict.target names "${field}", which is not a column. ` +
|
|
1028
|
+
`Valid targets: ${describeConflictTargets(qi)}.`);
|
|
1029
|
+
}
|
|
1030
|
+
if (columns.includes(column)) {
|
|
1031
|
+
throw new ValidationError(`createMany on "${table}": onConflict.target names "${field}" twice.`);
|
|
1032
|
+
}
|
|
1033
|
+
columns.push(column);
|
|
1034
|
+
}
|
|
1035
|
+
const wanted = new Set(columns);
|
|
1036
|
+
const sameSet = (set) => set.length === wanted.size && set.every((c) => wanted.has(c));
|
|
1037
|
+
if (uniqueKeyNames(qi.tableMeta).some(sameSet))
|
|
1038
|
+
return columns;
|
|
1039
|
+
const partial = qi.tableMeta.indexes.some((idx) => idx.unique && idx.partial && sameSet(idx.columns));
|
|
1040
|
+
throw new ValidationError(`createMany on "${table}": onConflict.target (${columns.map((c) => fieldOf(qi, c)).join(', ')}) ` +
|
|
1041
|
+
(partial
|
|
1042
|
+
? 'is a PARTIAL unique index, which a conflict clause can only use by restating its predicate. '
|
|
1043
|
+
: 'is not the primary key or a unique key of the table, as the schema metadata knows it. ') +
|
|
1044
|
+
`Valid targets: ${describeConflictTargets(qi)}.`);
|
|
1045
|
+
}
|
|
1046
|
+
function fieldOf(qi, column) {
|
|
1047
|
+
return qi.tableMeta.reverseColumnMap[column] ?? column;
|
|
1048
|
+
}
|
|
1049
|
+
function describeConflictTargets(qi) {
|
|
1050
|
+
const keys = uniqueKeyNames(qi.tableMeta);
|
|
1051
|
+
if (keys.length === 0)
|
|
1052
|
+
return '(none: the table has no primary key or unique key in its metadata)';
|
|
1053
|
+
return keys.map((cols) => `[${cols.map((c) => `'${fieldOf(qi, c)}'`).join(', ')}]`).join(', ');
|
|
1054
|
+
}
|
|
1055
|
+
/** The conflict-UPDATE's data: the field-list shorthand becomes `{ f: expr.excluded(f) }`. */
|
|
1056
|
+
function resolveConflictUpdate(qi, raw) {
|
|
1057
|
+
const empty = new ValidationError(`createMany on "${qi.table}": onConflict.update is empty; use onConflict: { target, ignore: true } to skip ` +
|
|
1058
|
+
'conflicting rows.');
|
|
1059
|
+
if (Array.isArray(raw)) {
|
|
1060
|
+
if (raw.length === 0)
|
|
1061
|
+
throw empty;
|
|
1062
|
+
const update = {};
|
|
1063
|
+
for (const field of raw) {
|
|
1064
|
+
if (typeof field !== 'string') {
|
|
1065
|
+
throw new ValidationError(`createMany on "${qi.table}": onConflict.update as a list takes field names, got ${typeof field}.`);
|
|
1066
|
+
}
|
|
1067
|
+
if (Object.hasOwn(update, field)) {
|
|
1068
|
+
throw new ValidationError(`createMany on "${qi.table}": onConflict.update names "${field}" twice.`);
|
|
1069
|
+
}
|
|
1070
|
+
update[field] = exprBuilders.excluded(field);
|
|
1071
|
+
}
|
|
1072
|
+
return update;
|
|
1073
|
+
}
|
|
1074
|
+
if (!isPlainRecord(raw)) {
|
|
1075
|
+
throw new ValidationError(`createMany on "${qi.table}": onConflict.update takes a list of field names or a { field: value } object.`);
|
|
1076
|
+
}
|
|
1077
|
+
if (!Object.values(raw).some((v) => v !== undefined))
|
|
1078
|
+
throw empty;
|
|
1079
|
+
assertNoGeneratedColumns(qi, raw, 'createMany');
|
|
1080
|
+
return raw;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* `onConflict.where`, with `undefined` keys pruned as in every `where`. One
|
|
1084
|
+
* whose every key was `undefined` is refused: dropping it would update the
|
|
1085
|
+
* rows it was written to protect.
|
|
1086
|
+
*/
|
|
1087
|
+
function resolveConflictWhere(qi, raw) {
|
|
1088
|
+
if (raw === undefined)
|
|
1089
|
+
return null;
|
|
1090
|
+
if (!isPlainRecord(raw)) {
|
|
1091
|
+
throw new ValidationError(`createMany on "${qi.table}": onConflict.where must be a where object.`);
|
|
1092
|
+
}
|
|
1093
|
+
const pruned = pruneUndefinedArgs(qi.schema, qi.table, { where: raw }).where;
|
|
1094
|
+
if (Object.keys(pruned).length === 0) {
|
|
1095
|
+
if (Object.keys(raw).length > 0) {
|
|
1096
|
+
throw new ValidationError(`createMany on "${qi.table}": every key in onConflict.where was undefined, so it would restrict nothing. ` +
|
|
1097
|
+
'Check the values you meant to filter on, or leave where out to update every conflicting row.');
|
|
1098
|
+
}
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
return pruned;
|
|
1102
|
+
}
|
|
1103
|
+
/** Every row must supply every target column: a row without its key cannot conflict on it. */
|
|
1104
|
+
function assertConflictTargetSupplied(qi, conflict, columns) {
|
|
1105
|
+
const missing = conflict.target.filter((c) => !columns.includes(c));
|
|
1106
|
+
if (missing.length === 0)
|
|
1107
|
+
return;
|
|
1108
|
+
throw new ValidationError(`createMany onConflict on "${qi.table}": the target field ${missing.map((c) => `"${fieldOf(qi, c)}"`).join(', ')} ` +
|
|
1109
|
+
'is missing from the rows. Every row must supply every target field; a key the database generates ' +
|
|
1110
|
+
'(an identity or serial id it has not assigned yet) cannot be a conflict target.');
|
|
1111
|
+
}
|
|
1112
|
+
/** A byte-equality token for one coerced key value. */
|
|
1113
|
+
function keyToken(value) {
|
|
1114
|
+
let s;
|
|
1115
|
+
if (value instanceof Date)
|
|
1116
|
+
s = value.toISOString();
|
|
1117
|
+
else if (value instanceof Uint8Array)
|
|
1118
|
+
s = Array.from(value, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
1119
|
+
else
|
|
1120
|
+
s = String(value);
|
|
1121
|
+
return `${s.length}:${s}`;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Refuse two rows of one call that share a conflict key, before any SQL. The
|
|
1125
|
+
* engines disagree on it (PostgreSQL raises 21000, SQL Server's MERGE 8672,
|
|
1126
|
+
* SQLite and MySQL apply the rows in sequence), and chunking at the bind
|
|
1127
|
+
* limit would turn the error into "the second chunk updates the first chunk's
|
|
1128
|
+
* row". Byte-equal on the coerced values, so a case-insensitive collation can
|
|
1129
|
+
* still let two rows through. A row with NULL in the key never conflicts.
|
|
1130
|
+
*/
|
|
1131
|
+
function assertNoDuplicateKeys(qi, conflict, columns, rowValues) {
|
|
1132
|
+
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1133
|
+
const seen = new Map();
|
|
1134
|
+
for (let i = 0; i < rowValues.length; i++) {
|
|
1135
|
+
const row = rowValues[i];
|
|
1136
|
+
const parts = at.map((j) => row[j]);
|
|
1137
|
+
if (parts.some((v) => v === null || v === undefined))
|
|
1138
|
+
continue;
|
|
1139
|
+
const key = parts.map(keyToken).join('|');
|
|
1140
|
+
const first = seen.get(key);
|
|
1141
|
+
if (first !== undefined) {
|
|
1142
|
+
throw new ValidationError(`createMany onConflict on "${qi.table}": rows ${first} and ${i} share the conflict key ` +
|
|
1143
|
+
`(${conflict.target.map((c) => fieldOf(qi, c)).join(', ')}). One statement cannot insert or update one ` +
|
|
1144
|
+
'key twice; merge those rows first.');
|
|
1145
|
+
}
|
|
1146
|
+
seen.set(key, i);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* The identity for an onConflict call: the conflict target (the key that
|
|
1151
|
+
* collides) or the primary key, preferring one the caller's projection
|
|
1152
|
+
* returns. `null` when the written rows cannot be told
|
|
1153
|
+
* apart: each candidate is assigned by `onConflict.update` (so a row's key can
|
|
1154
|
+
* change between statements) or is PII, which a write never returns.
|
|
1155
|
+
*/
|
|
1156
|
+
function conflictIdentity(qi, conflict, projection) {
|
|
1157
|
+
const assigned = conflict.update ? namedColumns(qi.tableMeta, conflict.update) : new Set();
|
|
1158
|
+
const pii = piiColumns(qi, qi.tableMeta);
|
|
1159
|
+
const usable = [conflict.target, qi.tableMeta.primaryKey].filter((key) => key.length > 0 && key.every((c) => !assigned.has(c) && !pii.has(c)));
|
|
1160
|
+
// One the caller's projection already returns, so the statement is unchanged.
|
|
1161
|
+
const columns = usable.find((key) => !projection || key.every((c) => projection.columns.includes(c))) ?? usable[0];
|
|
1162
|
+
if (!columns)
|
|
1163
|
+
return null;
|
|
1164
|
+
const missing = projection ? columns.filter((c) => !projection.columns.includes(c)) : [];
|
|
1165
|
+
if (missing.length === 0)
|
|
1166
|
+
return { columns, selection: projection, added: [], seen: new Set() };
|
|
1167
|
+
const wanted = new Set([...(projection?.columns ?? []), ...missing]);
|
|
1168
|
+
return {
|
|
1169
|
+
columns,
|
|
1170
|
+
selection: { columns: qi.tableMeta.allColumns.filter((c) => wanted.has(c)) },
|
|
1171
|
+
added: missing.map((c) => fieldOf(qi, c)),
|
|
1172
|
+
seen: new Set(),
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
/** Refuse a table row that one onConflict call wrote twice (see {@link ConflictIdentity}). */
|
|
1176
|
+
function assertEachRowWrittenOnce(qi, conflict, identity, rows, first) {
|
|
1177
|
+
if (first)
|
|
1178
|
+
identity.seen.clear();
|
|
1179
|
+
for (const row of rows) {
|
|
1180
|
+
const values = identity.columns.map((c) => row[c]);
|
|
1181
|
+
// Every engine returns the identity columns (see conflictIdentity); a row
|
|
1182
|
+
// without them cannot be compared, so it is not counted.
|
|
1183
|
+
if (values.some((v) => v === undefined))
|
|
1184
|
+
continue;
|
|
1185
|
+
const token = values.map(keyToken).join('|');
|
|
1186
|
+
if (identity.seen.has(token))
|
|
1187
|
+
throw conflictKeySharedError(qi, conflict.target);
|
|
1188
|
+
identity.seen.add(token);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* The E003 for two rows of one `createMany({ onConflict })` that share a key
|
|
1193
|
+
* only under the column's comparison rules (a case-insensitive collation, a
|
|
1194
|
+
* `citext` column), which the byte-equality check above cannot see.
|
|
1195
|
+
*/
|
|
1196
|
+
function conflictKeySharedError(qi, target, cause) {
|
|
1197
|
+
return new ValidationError(`createMany onConflict on "${qi.table}": two rows in this call share the conflict key ` +
|
|
1198
|
+
`(${target.map((c) => fieldOf(qi, c)).join(', ')}) under the column's comparison rules (a case-insensitive ` +
|
|
1199
|
+
'collation, for example), so the call would insert or update one row twice. Merge those rows first.', cause === undefined ? undefined : { cause });
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* `err` from running a `createMany({ onConflict })`, with the engine's
|
|
1203
|
+
* "one statement touched one row twice" errors (PostgreSQL 21000, SQL Server
|
|
1204
|
+
* 8672) turned into the E003 that names the cause. Anything else is returned
|
|
1205
|
+
* as it was.
|
|
1206
|
+
*/
|
|
1207
|
+
export function explainConflictKeyCollision(qi, args, err) {
|
|
1208
|
+
if (args.onConflict === undefined || !touchedRowTwice(err))
|
|
1209
|
+
return err;
|
|
1210
|
+
const raw = args.onConflict.target;
|
|
1211
|
+
const target = Array.isArray(raw)
|
|
1212
|
+
? raw.map((f) => (typeof f === 'string' ? (resolveColumnName(qi.tableMeta, f) ?? f) : String(f)))
|
|
1213
|
+
: [];
|
|
1214
|
+
return conflictKeySharedError(qi, target, err);
|
|
1215
|
+
}
|
|
1216
|
+
/** PostgreSQL 21000 or SQL Server 8672 on `err` or a cause under it (Turbine wraps or scrubs driver errors). */
|
|
1217
|
+
function touchedRowTwice(err) {
|
|
1218
|
+
let e = err;
|
|
1219
|
+
for (let depth = 0; depth < 4 && e && typeof e === 'object'; depth++) {
|
|
1220
|
+
if (e.code === '21000' || e.sqlstate === '21000' || e.number === 8672)
|
|
1221
|
+
return true;
|
|
1222
|
+
e = e.cause;
|
|
1223
|
+
}
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* MySQL's ON DUPLICATE KEY UPDATE has no conflict target: it fires on
|
|
1228
|
+
* WHATEVER unique key a row violates, and with several matching keys it
|
|
1229
|
+
* updates one row. So the target must be the only key a row can violate, and
|
|
1230
|
+
* the shapes that need a predicate slot or a per-row outcome are refused.
|
|
1231
|
+
*/
|
|
1232
|
+
function assertMysqlBulkUpsert(qi, conflict, columns, rowValues) {
|
|
1233
|
+
const fix = (why, hint) => new UnsupportedFeatureError(`createMany onConflict on "${qi.table}" ${why}`, qi.dialect.name, hint);
|
|
1234
|
+
if (conflict.update === null) {
|
|
1235
|
+
throw fix('with ignore: true', 'MySQL cannot tell the inserted rows from the skipped ones afterwards. Use onConflict: { target, update }.');
|
|
1236
|
+
}
|
|
1237
|
+
if (conflict.where !== null) {
|
|
1238
|
+
throw fix('with a where', 'ON DUPLICATE KEY UPDATE has no predicate slot.');
|
|
1239
|
+
}
|
|
1240
|
+
const gf = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
1241
|
+
if (gf && whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), gf, [])) {
|
|
1242
|
+
throw fix('on a table with a global filter', "ON DUPLICATE KEY UPDATE cannot carry the filter, so another tenant's row could be updated. Call upsert() " +
|
|
1243
|
+
'per row, or pass skipGlobalFilters: UNSAFE.');
|
|
1244
|
+
}
|
|
1245
|
+
// The single-row upsert's rule (otherCollidingUniqueKey), on column presence
|
|
1246
|
+
// so the verdict does not depend on the rows' values.
|
|
1247
|
+
const present = new Set(columns);
|
|
1248
|
+
const other = otherCollidingUniqueKey(qi.tableMeta, [conflict.target], (c) => (present.has(c) ? 'value' : 'absent'));
|
|
1249
|
+
if (other) {
|
|
1250
|
+
throw fix(`with target (${conflict.target.map((c) => fieldOf(qi, c)).join(', ')})`, `"${qi.table}" has another unique key (${other.map((c) => fieldOf(qi, c)).join(', ')}) that a row in data can ` +
|
|
1251
|
+
"collide on, and MySQL's ON DUPLICATE KEY UPDATE cannot be limited to one key, so it could update a row the " +
|
|
1252
|
+
'target does not name. Call upsert() per row (it looks each row up first), or leave that key out of data.');
|
|
1253
|
+
}
|
|
1254
|
+
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1255
|
+
if (rowValues.some((row) => at.some((j) => row[j] === null || row[j] === undefined))) {
|
|
1256
|
+
throw fix('with a NULL in a target field', 'MySQL returns the written rows by re-reading them by key, and a NULL key reads back nothing. Supply every ' +
|
|
1257
|
+
'target value.');
|
|
1258
|
+
}
|
|
1259
|
+
const numbered = at.filter((j) => isServerNumberedKey(qi, columns[j]));
|
|
1260
|
+
if (numbered.length > 0 && rowValues.some((row) => numbered.some((j) => isZeroKeyValue(row[j])))) {
|
|
1261
|
+
const field = fieldOf(qi, columns[numbered[0]]);
|
|
1262
|
+
throw fix(`with 0 in the AUTO_INCREMENT target field "${field}"`, 'MySQL assigns a new value for 0, so the row is inserted instead of conflicting, and it cannot be re-read ' +
|
|
1263
|
+
`by its key. Pass the row's real "${field}", or create() the rows that need a new one.`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/** The SELECT that re-reads a MySQL multi-row upsert's rows by their conflict keys. */
|
|
1267
|
+
function buildKeyReselect(qi, conflict, columns, rowValues, projection) {
|
|
1268
|
+
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1269
|
+
const params = [];
|
|
1270
|
+
const tuples = rowValues.map((row) => {
|
|
1271
|
+
const refs = at.map((j, k) => {
|
|
1272
|
+
params.push(reselectKeyValue(qi, conflict.target[k], row[j]));
|
|
1273
|
+
return qi.p(params.length);
|
|
1274
|
+
});
|
|
1275
|
+
return refs.length === 1 ? refs[0] : `(${refs.join(', ')})`;
|
|
326
1276
|
});
|
|
1277
|
+
const keyCols = conflict.target.map((c) => qi.q(c));
|
|
1278
|
+
const lhs = keyCols.length === 1 ? keyCols[0] : `(${keyCols.join(', ')})`;
|
|
1279
|
+
return {
|
|
1280
|
+
sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${lhs} IN (${tuples.join(', ')})`,
|
|
1281
|
+
params,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, identity, first) {
|
|
1285
|
+
// The duplicate-key and MySQL checks ran over every row in prepareCreateMany.
|
|
1286
|
+
const rowValues = insert.rowValues;
|
|
1287
|
+
// `updatedAt` columns are stamped on the conflict-UPDATE as update() stamps
|
|
1288
|
+
// them, unless the update names the column.
|
|
1289
|
+
const updateData = conflict.update ? applyUpdatedAtColumns(qi, conflict.update) : null;
|
|
1290
|
+
const insertColumns = new Set(columns);
|
|
1291
|
+
const gf = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
1292
|
+
const built = qi.dialect.buildBulkUpsertStatement({
|
|
1293
|
+
...insert,
|
|
1294
|
+
skipDuplicates: false,
|
|
1295
|
+
conflictColumns: conflict.target.map((c) => qi.q(c)),
|
|
1296
|
+
update: updateData
|
|
1297
|
+
? (params, refs) => {
|
|
1298
|
+
const setClauses = buildSetClauses(qi, writeEntries(qi, updateData), params, {
|
|
1299
|
+
operation: 'createMany',
|
|
1300
|
+
target: refs.target,
|
|
1301
|
+
incoming: refs.incoming,
|
|
1302
|
+
insertColumns,
|
|
1303
|
+
});
|
|
1304
|
+
// The caller's predicate and the global filter both read the EXISTING
|
|
1305
|
+
// row, so a row they exclude is neither updated nor returned (and,
|
|
1306
|
+
// its key being taken, not inserted either). Inserts are not filtered,
|
|
1307
|
+
// as create() is not.
|
|
1308
|
+
const predicates = [];
|
|
1309
|
+
for (const where of [conflict.where, gf]) {
|
|
1310
|
+
if (!where)
|
|
1311
|
+
continue;
|
|
1312
|
+
const sql = whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, refs.target, where, params);
|
|
1313
|
+
if (sql)
|
|
1314
|
+
predicates.push(sql);
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
setClauses,
|
|
1318
|
+
where: predicates.length <= 1 ? (predicates[0] ?? null) : predicates.map((p) => `(${p})`).join(' AND '),
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
: null,
|
|
1322
|
+
});
|
|
1323
|
+
if (overBindLimit === 'refuse') {
|
|
1324
|
+
assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
|
|
1325
|
+
}
|
|
327
1326
|
return {
|
|
328
1327
|
sql: built.sql,
|
|
329
1328
|
params: built.params,
|
|
330
|
-
|
|
1329
|
+
// The rows written: inserted or updated. A row `ignore` skipped, or one the
|
|
1330
|
+
// predicate or a global filter excluded, is not in the result, and the
|
|
1331
|
+
// order is the engine's, not the input's.
|
|
1332
|
+
transform: (result) => {
|
|
1333
|
+
if (identity)
|
|
1334
|
+
assertEachRowWrittenOnce(qi, conflict, identity, result.rows, first);
|
|
1335
|
+
const rows = result.rows.map((row) => parseWriteRow(qi, row, projection));
|
|
1336
|
+
if (identity && identity.added.length > 0) {
|
|
1337
|
+
for (const row of rows)
|
|
1338
|
+
for (const field of identity.added)
|
|
1339
|
+
delete row[field];
|
|
1340
|
+
}
|
|
1341
|
+
return rows;
|
|
1342
|
+
},
|
|
331
1343
|
tag: `${qi.table}.createMany`,
|
|
1344
|
+
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
1345
|
+
? async (exec) => {
|
|
1346
|
+
await exec(built.sql, built.params);
|
|
1347
|
+
const sel = buildKeyReselect(qi, conflict, columns, rowValues, identity?.selection ?? projection);
|
|
1348
|
+
const result = await exec(sel.sql, sel.params);
|
|
1349
|
+
// Every row was inserted or updated and byte-equal keys were
|
|
1350
|
+
// refused up front, so fewer rows back means two rows' keys are
|
|
1351
|
+
// equal under the column's collation and MySQL applied the second
|
|
1352
|
+
// as an update of the first. Refused, which rolls the call back.
|
|
1353
|
+
if (result.rows.length < rowValues.length)
|
|
1354
|
+
throw conflictKeySharedError(qi, conflict.target);
|
|
1355
|
+
return result;
|
|
1356
|
+
}
|
|
1357
|
+
: undefined,
|
|
332
1358
|
};
|
|
333
1359
|
}
|
|
334
1360
|
export function buildUpdate(qi, args, projection) {
|
|
@@ -394,8 +1420,7 @@ export function buildUpdate(qi, args, projection) {
|
|
|
394
1420
|
: `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
|
|
395
1421
|
const params = [];
|
|
396
1422
|
const buildSql = (freshParams) => {
|
|
397
|
-
const
|
|
398
|
-
const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
|
|
1423
|
+
const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams);
|
|
399
1424
|
if (lock) {
|
|
400
1425
|
const versionCol = qi.toSqlColumn(lock.field);
|
|
401
1426
|
setClauses.push(`${versionCol} = ${versionCol} + 1`);
|
|
@@ -464,6 +1489,18 @@ export function buildUpdate(qi, args, projection) {
|
|
|
464
1489
|
// Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
|
|
465
1490
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
466
1491
|
? async (exec) => {
|
|
1492
|
+
// Runs in one transaction (executeMutation), savepointed inside a
|
|
1493
|
+
// caller's. When the write may move the row out of `where` (it sets
|
|
1494
|
+
// a column `where` reads, the version column included), the row is
|
|
1495
|
+
// locked and re-read by its key, since `where` no longer finds it.
|
|
1496
|
+
const written = namedColumns(qi.tableMeta, dataObj);
|
|
1497
|
+
if (lock)
|
|
1498
|
+
written.add(qi.toColumn(lock.field));
|
|
1499
|
+
const key = writeMayMoveRow(qi, whereObj, written)
|
|
1500
|
+
? await lockMovedRowKey(qi, exec, whereObj, dataObj, 'update', allowFullTableScan)
|
|
1501
|
+
: undefined;
|
|
1502
|
+
if (key === null)
|
|
1503
|
+
return { rows: [], rowCount: 0 };
|
|
467
1504
|
const writeResult = await exec(sql, params, preparedName);
|
|
468
1505
|
// Optimistic-lock conflict: the version-checked UPDATE matched no
|
|
469
1506
|
// row. The re-fetch below uses `where` WITHOUT the version
|
|
@@ -477,14 +1514,21 @@ export function buildUpdate(qi, args, projection) {
|
|
|
477
1514
|
expectedVersion: lock.expected,
|
|
478
1515
|
});
|
|
479
1516
|
}
|
|
480
|
-
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
481
|
-
|
|
1517
|
+
const sel = buildReselectByWhere(qi, key ?? whereObj, projection);
|
|
1518
|
+
const result = await exec(sel.sql, sel.params);
|
|
1519
|
+
// `update` writes one row; more than one here means the engine
|
|
1520
|
+
// matched the where loosely, and the write is rolled back.
|
|
1521
|
+
if (key || !allowFullTableScan) {
|
|
1522
|
+
assertOneRowByKey(qi, 'update', key ? identityColumns(qi) : whereColumns(qi, userWhere), result.rows.length);
|
|
1523
|
+
}
|
|
1524
|
+
return result;
|
|
482
1525
|
}
|
|
483
1526
|
: undefined,
|
|
484
1527
|
};
|
|
485
1528
|
}
|
|
486
1529
|
export function buildDelete(qi, args, projection) {
|
|
487
1530
|
assertWritable(qi, 'delete');
|
|
1531
|
+
assertNoOptimisticLock(qi, args, 'delete');
|
|
488
1532
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
489
1533
|
// Prisma compound-unique selector → the column conjunction (before the guard).
|
|
490
1534
|
const userWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
|
|
@@ -533,21 +1577,72 @@ export function buildDelete(qi, args, projection) {
|
|
|
533
1577
|
// by the same where, then run the DELETE, returning the captured row.
|
|
534
1578
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
535
1579
|
? async (exec) => {
|
|
536
|
-
|
|
1580
|
+
// In one transaction (executeMutation): the row is locked while it
|
|
1581
|
+
// is read, and returned only if THIS delete removed it, so of two
|
|
1582
|
+
// concurrent deletes of one row exactly one reports it.
|
|
1583
|
+
const sel = buildReselectByWhere(qi, whereObj, projection, true);
|
|
537
1584
|
const pre = await exec(sel.sql, sel.params);
|
|
538
|
-
|
|
539
|
-
|
|
1585
|
+
// `delete` removes one row; refused before the DELETE when the
|
|
1586
|
+
// engine matched the where loosely.
|
|
1587
|
+
if (!allowFullTableScan)
|
|
1588
|
+
assertOneRowByKey(qi, 'delete', whereColumns(qi, userWhere), pre.rows.length);
|
|
1589
|
+
const deleted = await exec(entry.sql, params, entry.name);
|
|
1590
|
+
return (deleted.rowCount ?? 0) === 0 ? { rows: [], rowCount: 0 } : pre;
|
|
540
1591
|
}
|
|
541
1592
|
: undefined,
|
|
542
1593
|
};
|
|
543
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Refuse `limit` / `take` / `offset` / `skip` on `updateMany` / `deleteMany`.
|
|
1597
|
+
* No engine applies them to a bulk write here, so `deleteMany({ where, limit:
|
|
1598
|
+
* 1000 })` used to touch every matching row. Shared with the PowDB engine.
|
|
1599
|
+
*/
|
|
1600
|
+
export function assertNoBulkWritePaging(table, operation, args) {
|
|
1601
|
+
const a = args;
|
|
1602
|
+
const key = ['limit', 'take', 'offset', 'skip'].find((k) => a[k] !== undefined);
|
|
1603
|
+
if (key === undefined)
|
|
1604
|
+
return;
|
|
1605
|
+
throw new ValidationError(`\`${key}\` is not supported on ${operation} for "${table}": it would apply to every row the \`where\` ` +
|
|
1606
|
+
`matches, not ${key === 'limit' || key === 'take' ? 'only the first ones' : 'skip any'}. To ${operation === 'deleteMany' ? 'delete' : 'update'} a bounded batch, ` +
|
|
1607
|
+
'select the ids first (`findMany({ where, select: { id: true }, orderBy, limit })`), then pass ' +
|
|
1608
|
+
'`where: { id: { in: ids } }`.');
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Refuse `optimisticLock` on a write that does not implement it. It is an
|
|
1612
|
+
* `update` option; on a sibling method it used to be ignored, so a stale
|
|
1613
|
+
* write went through with no error, which is the one outcome the option
|
|
1614
|
+
* exists to prevent. The message names the equivalent for each method.
|
|
1615
|
+
*/
|
|
1616
|
+
export function assertNoOptimisticLock(qi, args, operation) {
|
|
1617
|
+
const lock = args.optimisticLock;
|
|
1618
|
+
if (lock === undefined)
|
|
1619
|
+
return;
|
|
1620
|
+
const field = typeof lock?.field === 'string' ? lock.field : 'version';
|
|
1621
|
+
throw new ValidationError(`\`optimisticLock\` is an \`update\` option and ${operation} on "${qi.table}" does not apply it. ` +
|
|
1622
|
+
OPTIMISTIC_LOCK_ALTERNATIVE[operation](field));
|
|
1623
|
+
}
|
|
1624
|
+
const OPTIMISTIC_LOCK_ALTERNATIVE = {
|
|
1625
|
+
updateMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`) and bump it in \`data\` ` +
|
|
1626
|
+
`(\`${field}: { increment: 1 }\`); the returned count is the number of rows that were still at that version.`,
|
|
1627
|
+
upsert: () => 'Inside one `$transaction`, read the row by `where`, then call `update({ where, data, optimisticLock })` ' +
|
|
1628
|
+
'when it exists or `create` when it does not.',
|
|
1629
|
+
delete: (field) => `Put the expected version in \`where\` beside the key (\`where: { id, ${field}: expected }\`): a row at ` +
|
|
1630
|
+
'another version is not deleted, and the call throws NotFoundError.',
|
|
1631
|
+
deleteMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`); the returned count is the number of rows ` +
|
|
1632
|
+
'that were still at that version.',
|
|
1633
|
+
};
|
|
544
1634
|
export function upsertLookupReason(qi, args) {
|
|
1635
|
+
assertNoOptimisticLock(qi, args, 'upsert');
|
|
545
1636
|
const where = (args.where ?? {});
|
|
546
1637
|
const create = (args.create ?? {});
|
|
547
1638
|
if (!upsertWhereMatchesCreate(qi.tableMeta, where, create))
|
|
548
1639
|
return 'where-differs';
|
|
549
1640
|
if (writeEntries(qi, (args.update ?? {})).length === 0)
|
|
550
1641
|
return 'empty-update';
|
|
1642
|
+
if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key' &&
|
|
1643
|
+
upsertCreateCollidingKey(qi.tableMeta, where, create)) {
|
|
1644
|
+
return 'other-unique-key';
|
|
1645
|
+
}
|
|
551
1646
|
if (!qi.dialect.supportsUpsertUpdateWhere) {
|
|
552
1647
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
553
1648
|
const filter = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
@@ -558,8 +1653,13 @@ export function upsertLookupReason(qi, args) {
|
|
|
558
1653
|
return null;
|
|
559
1654
|
}
|
|
560
1655
|
/** The refusal `buildUpsert` raises for each reason, naming the way forward. */
|
|
561
|
-
function upsertStatementRefusal(qi, reason, where) {
|
|
1656
|
+
function upsertStatementRefusal(qi, reason, where, create) {
|
|
562
1657
|
const call = 'Call `upsert()` unbatched: it looks the row up by `where` first.';
|
|
1658
|
+
if (reason === 'other-unique-key') {
|
|
1659
|
+
const key = upsertCreateCollidingKey(qi.tableMeta, where, create) ?? [];
|
|
1660
|
+
return new UnsupportedFeatureError(`a batched upsert on "${qi.table}" whose \`create\` also sets another unique key${key.length > 0 ? ` (${key.map((c) => fieldOf(qi, c)).join(', ')})` : ''},`, qi.dialect.name, 'ON DUPLICATE KEY UPDATE updates whichever row a unique key collides on first, which may not be the row `where` names. ' +
|
|
1661
|
+
call);
|
|
1662
|
+
}
|
|
563
1663
|
if (reason === 'global-filter') {
|
|
564
1664
|
return new UnsupportedFeatureError(`a batched upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, `Its upsert statement cannot carry the filter. ${call} Or pass \`skipGlobalFilters: UNSAFE\`.`);
|
|
565
1665
|
}
|
|
@@ -572,6 +1672,7 @@ function upsertStatementRefusal(qi, reason, where) {
|
|
|
572
1672
|
}
|
|
573
1673
|
export function buildUpsert(qi, args, projection) {
|
|
574
1674
|
assertWritable(qi, 'upsert');
|
|
1675
|
+
assertNoOptimisticLock(qi, args, 'upsert');
|
|
575
1676
|
assertNoGeneratedColumns(qi, args.create, 'upsert');
|
|
576
1677
|
assertNoGeneratedColumns(qi, args.update, 'upsert');
|
|
577
1678
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
@@ -597,8 +1698,9 @@ export function buildUpsert(qi, args, projection) {
|
|
|
597
1698
|
// `upsert()` never reaches here with a reason (it looks the row up instead),
|
|
598
1699
|
// so this refuses only a batched upsert, which has no round trip to spare.
|
|
599
1700
|
const lookupReason = upsertLookupReason(qi, args);
|
|
600
|
-
if (lookupReason)
|
|
601
|
-
throw upsertStatementRefusal(qi, lookupReason, upsertWhere);
|
|
1701
|
+
if (lookupReason) {
|
|
1702
|
+
throw upsertStatementRefusal(qi, lookupReason, upsertWhere, args.create);
|
|
1703
|
+
}
|
|
602
1704
|
// Build the INSERT part from create data
|
|
603
1705
|
const createEntries = writeEntries(qi, args.create);
|
|
604
1706
|
const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
|
|
@@ -609,18 +1711,30 @@ export function buildUpsert(qi, args, projection) {
|
|
|
609
1711
|
const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
|
|
610
1712
|
const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
|
|
611
1713
|
// The conflict-UPDATE SET goes through the SAME operator-aware compiler as
|
|
612
|
-
// `update()` (
|
|
1714
|
+
// `update()` (buildSetClauses), so `set` / `increment` / `decrement` /
|
|
613
1715
|
// `multiply` / `divide` mean here what they mean there and a misspelled
|
|
614
1716
|
// operator gets the same E003. This branch used to bind each value directly,
|
|
615
1717
|
// which stored an operator OBJECT as the JSON text `{"set":"x"}` in a text
|
|
616
|
-
// column with no error at all.
|
|
1718
|
+
// column with no error at all. buildSetClauses pushes onto `params` as it
|
|
617
1719
|
// goes, so the numbering continues after the create params exactly as the
|
|
618
1720
|
// hand-rolled `paramIdx` did. No SQL cache here (the statement is rebuilt per
|
|
619
1721
|
// call), so the fingerprint / collect mirrors update() keeps are not needed.
|
|
620
|
-
|
|
1722
|
+
// `updatedAt` columns are stamped on the conflict-UPDATE exactly as the
|
|
1723
|
+
// lookup path's `update()` stamps them, so the column does not depend on
|
|
1724
|
+
// which path an upsert takes. (An empty `update` never reaches here: it is
|
|
1725
|
+
// a lookup that leaves the found row untouched, stamp included.)
|
|
1726
|
+
//
|
|
1727
|
+
// `expr.excluded(f)` is `create`'s value for `f` (the incoming row of a
|
|
1728
|
+
// one-row upsert IS `create`), resolved before compiling so this path and the
|
|
1729
|
+
// lookup path's plain `update()` write the same row.
|
|
1730
|
+
const update = resolveExcludedFromCreate(qi, args.update, args.create);
|
|
1731
|
+
const stampedUpdate = applyUpdatedAtColumns(qi, update);
|
|
1732
|
+
const updateEntries = writeEntries(qi, stampedUpdate);
|
|
621
1733
|
const params = [...createParams];
|
|
622
|
-
const
|
|
623
|
-
|
|
1734
|
+
const setClauses = buildSetClauses(qi, updateEntries, params, {
|
|
1735
|
+
operation: 'upsert',
|
|
1736
|
+
target: upsertTargetRef(qi),
|
|
1737
|
+
});
|
|
624
1738
|
// Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
|
|
625
1739
|
// upsert never resurrects a soft-deleted row or writes across tenants. Only
|
|
626
1740
|
// on engines whose upsert can carry a predicate (Postgres, SQLite); the gf
|
|
@@ -689,38 +1803,46 @@ export function buildUpsert(qi, args, projection) {
|
|
|
689
1803
|
// Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
|
|
690
1804
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
691
1805
|
? async (exec) => {
|
|
1806
|
+
const lookup = (whereMod.mergeGlobalFilter(qi, textSafeKeyWhere(qi, upsertWhere)) ?? {});
|
|
1807
|
+
// An update that sets a column `where` reads moves the row out of
|
|
1808
|
+
// `where`'s reach: lock it and re-read it by its key. An insert
|
|
1809
|
+
// matches `where` (this path requires `where` pinned to `create`),
|
|
1810
|
+
// so a row that is not there yet is re-read by `where`.
|
|
1811
|
+
const key = writeMayMoveRow(qi, lookup, namedColumns(qi.tableMeta, stampedUpdate))
|
|
1812
|
+
? await lockMovedRowKey(qi, exec, lookup, stampedUpdate, 'upsert', false)
|
|
1813
|
+
: undefined;
|
|
692
1814
|
await exec(sql, params);
|
|
693
|
-
const sel = buildReselectByWhere(qi,
|
|
694
|
-
|
|
1815
|
+
const sel = buildReselectByWhere(qi, key ?? lookup, projection);
|
|
1816
|
+
const result = await exec(sel.sql, sel.params);
|
|
1817
|
+
assertOneRowByKey(qi, 'upsert', key ? identityColumns(qi) : whereColumns(qi, upsertWhere), result.rows.length);
|
|
1818
|
+
return result;
|
|
695
1819
|
}
|
|
696
1820
|
: undefined,
|
|
697
1821
|
};
|
|
698
1822
|
}
|
|
699
1823
|
/**
|
|
700
|
-
* The
|
|
1824
|
+
* The rendered reference to an upsert's TARGET row, which every column the
|
|
1825
|
+
* conflict-UPDATE reads goes through (an arithmetic operator's column, an
|
|
1826
|
+
* expression's `col()`, a case predicate).
|
|
701
1827
|
*
|
|
702
1828
|
* `INSERT ... ON CONFLICT DO UPDATE SET n = n + 1` is 42702 on PostgreSQL
|
|
703
1829
|
* because `n` could be the target row's or `excluded`'s, so the reference is
|
|
704
1830
|
* qualified with the table name on every engine whose upsert statement names
|
|
705
1831
|
* the table directly (PostgreSQL, SQLite, MySQL all accept `"t"."n"` there).
|
|
706
|
-
* SQL Server
|
|
707
|
-
*
|
|
708
|
-
* and the alias belongs to that dialect's own `buildUpsertStatement` rather
|
|
709
|
-
* than to this module. An arithmetic operator in an mssql upsert may therefore
|
|
710
|
-
* still be refused by the server as an ambiguous column; that shape has never
|
|
711
|
-
* worked (it previously bound the operator OBJECT as the value), and the sound
|
|
712
|
-
* fix is for the dialect to publish its target alias, which is a change to
|
|
713
|
-
* `dialect.ts`. It fails loudly either way, so no wrong answer is possible.
|
|
1832
|
+
* SQL Server's `MERGE` aliases its target, so the table name does not resolve
|
|
1833
|
+
* there and the dialect publishes the alias ({@link Dialect.upsertTargetAlias}).
|
|
714
1834
|
* `set` and literal values reference no column and are unaffected everywhere.
|
|
715
1835
|
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
1836
|
+
* Read from a property only SQL Server sets, never inferred from a hook's
|
|
1837
|
+
* presence: every engine dialect spreads `postgresDialect`.
|
|
718
1838
|
*/
|
|
719
|
-
function
|
|
720
|
-
return qi.dialect.
|
|
1839
|
+
function upsertTargetRef(qi) {
|
|
1840
|
+
return qi.dialect.upsertTargetAlias ?? qi.q(qi.table);
|
|
721
1841
|
}
|
|
722
1842
|
export function buildUpdateMany(qi, args) {
|
|
723
1843
|
assertWritable(qi, 'updateMany');
|
|
1844
|
+
assertNoBulkWritePaging(qi.table, 'updateMany', args);
|
|
1845
|
+
assertNoOptimisticLock(qi, args, 'updateMany');
|
|
724
1846
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
725
1847
|
const dataObj = applyUpdatedAtColumns(qi, args.data);
|
|
726
1848
|
assertNoGeneratedColumns(qi, dataObj, 'updateMany');
|
|
@@ -743,8 +1865,7 @@ export function buildUpdateMany(qi, args) {
|
|
|
743
1865
|
const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
744
1866
|
const params = [];
|
|
745
1867
|
const buildSql = (freshParams) => {
|
|
746
|
-
const
|
|
747
|
-
const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
|
|
1868
|
+
const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams, { operation: 'updateMany' });
|
|
748
1869
|
const whereClause = whereMod.buildWhereClause(qi, whereObj, freshParams);
|
|
749
1870
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
750
1871
|
return `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}`;
|
|
@@ -763,6 +1884,8 @@ export function buildUpdateMany(qi, args) {
|
|
|
763
1884
|
}
|
|
764
1885
|
export function buildDeleteMany(qi, args) {
|
|
765
1886
|
assertWritable(qi, 'deleteMany');
|
|
1887
|
+
assertNoBulkWritePaging(qi.table, 'deleteMany', args);
|
|
1888
|
+
assertNoOptimisticLock(qi, args, 'deleteMany');
|
|
766
1889
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
767
1890
|
whereMod.assertMutationHasPredicate(qi, 'deleteMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'));
|
|
768
1891
|
const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
|
|
@@ -788,8 +1911,8 @@ export function buildDeleteMany(qi, args) {
|
|
|
788
1911
|
/**
|
|
789
1912
|
* The snake_case names of a table's PII-tagged (`defineSchema` `pii: true`)
|
|
790
1913
|
* columns. PII columns are excluded from default projections (findMany /
|
|
791
|
-
* findUnique / relation subqueries / batched loads) unless
|
|
792
|
-
*
|
|
1914
|
+
* findUnique / relation subqueries / batched loads) unless a read passes
|
|
1915
|
+
* `includePii: UNSAFE`; naming one in `select` without it is E003. Returns an
|
|
793
1916
|
* empty set for any table with no PII column, so untagged schemas keep their
|
|
794
1917
|
* byte-identical SQL.
|
|
795
1918
|
*/
|
|
@@ -822,8 +1945,8 @@ export function piiColumns(_qi, meta) {
|
|
|
822
1945
|
* The camelCase field names of a table's PII-tagged columns: the read-side
|
|
823
1946
|
* counterpart of {@link piiColumns} applied to already-parsed entities.
|
|
824
1947
|
* Used to strip PII from a write's RETURNING/reselect row (writes accept no
|
|
825
|
-
* `includePii
|
|
826
|
-
*
|
|
1948
|
+
* `includePii`, and a PII column named in a write's `select` is E003, so their
|
|
1949
|
+
* returned row never carries one; you may still write PII fields freely).
|
|
827
1950
|
*/
|
|
828
1951
|
export function piiFields(_qi, meta) {
|
|
829
1952
|
const out = [];
|
|
@@ -857,8 +1980,9 @@ export function writeProjectionCacheSegment(projection) {
|
|
|
857
1980
|
* column so the PII values never leave the database on a write. A PII-tagged
|
|
858
1981
|
* PRIMARY KEY column is kept in the projection regardless (the returned row
|
|
859
1982
|
* must stay addressable): tag sensitive data, not keys, a PII PK is
|
|
860
|
-
* documented out of scope for stripping. Writes accept no `
|
|
861
|
-
*
|
|
1983
|
+
* documented out of scope for stripping. Writes accept no `includePii` and
|
|
1984
|
+
* their `select` cannot name a PII column, so this is the whole write-return
|
|
1985
|
+
* policy at the SQL level;
|
|
862
1986
|
* {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
|
|
863
1987
|
* SQL already excludes the columns). Derived purely from static per-table
|
|
864
1988
|
* schema metadata, so the write SQL cache needs no extra key segment.
|
|
@@ -945,8 +2069,8 @@ export function writeReselectSelection(qi, projection) {
|
|
|
945
2069
|
*/
|
|
946
2070
|
export function parseWriteRow(qi, row, projection) {
|
|
947
2071
|
const parsed = qi.parseRow(row, qi.table);
|
|
948
|
-
// A
|
|
949
|
-
// so
|
|
2072
|
+
// A write's projection never names a PII column (E003, see WriteProjection),
|
|
2073
|
+
// so for PII this strip spares nothing; it stays as defense in depth.
|
|
950
2074
|
const pii = piiColumns(qi, qi.tableMeta);
|
|
951
2075
|
if (pii.size === 0)
|
|
952
2076
|
return parsed;
|
|
@@ -987,85 +2111,260 @@ export function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
987
2111
|
}
|
|
988
2112
|
}
|
|
989
2113
|
}
|
|
2114
|
+
/** The SQL operator each arithmetic update operator compiles to. */
|
|
2115
|
+
const ARITHMETIC_SQL = { increment: '+', decrement: '-', multiply: '*', divide: '/' };
|
|
990
2116
|
/**
|
|
991
|
-
*
|
|
2117
|
+
* The SQL operator for one arithmetic update operator on one column.
|
|
992
2118
|
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
*
|
|
2119
|
+
* `divide` on an INTEGER column is the engine's truncating integer division
|
|
2120
|
+
* (`integerDivision`): `DIV` on MySQL, whose `/` is decimal division, so
|
|
2121
|
+
* `75 / 2` is 37.5 and the assignment back to an INT rounds it to 38, where
|
|
2122
|
+
* PostgreSQL's integer division truncates to 37. `DIV` truncates toward zero,
|
|
2123
|
+
* as PostgreSQL does. Decided by the COLUMN TYPE alone, never by
|
|
2124
|
+
* the bound divisor: the SQL text must not depend on a value, or the template
|
|
2125
|
+
* cache would serve one shape to the other.
|
|
2126
|
+
*/
|
|
2127
|
+
function arithmeticSql(qi, op, column) {
|
|
2128
|
+
if (op === 'divide' && isIntegerColumnType(qi.columnPgTypeMap.get(column))) {
|
|
2129
|
+
return qi.dialect.capabilities.integerDivision;
|
|
2130
|
+
}
|
|
2131
|
+
return ARITHMETIC_SQL[op];
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* The atomic operator a SET value names, or `null` when it is a plain value.
|
|
2135
|
+
* THE one rule, read by the SET build, its fingerprint and its cache-hit param
|
|
2136
|
+
* collector, so the three cannot disagree about whether a value is an
|
|
2137
|
+
* operator.
|
|
2138
|
+
*
|
|
2139
|
+
* A branded expression or `col()` reference is checked FIRST and answers
|
|
2140
|
+
* `'expr'`: it is a frozen plain object too, and must never reach the
|
|
2141
|
+
* operator test below. An operator is a plain object with EXACTLY one key from
|
|
2142
|
+
* {@link UPDATE_OPERATOR_KEYS}. On a json/jsonb column only `set` is an
|
|
2143
|
+
* operator: an object there IS the JSON value, so `{ increment: 1 }` is the
|
|
2144
|
+
* document `{"increment":1}`, and `{ set: doc }` is the way to write a
|
|
2145
|
+
* document that is itself shaped like `{ set: ... }`.
|
|
2146
|
+
*/
|
|
2147
|
+
export function setOperatorOf(qi, key, value) {
|
|
2148
|
+
if (isSqlExpr(value) || isColumnRef(value))
|
|
2149
|
+
return 'expr';
|
|
2150
|
+
if (!isUnmatchedPlainObject(value))
|
|
2151
|
+
return null;
|
|
2152
|
+
const keys = Object.keys(value);
|
|
2153
|
+
if (keys.length !== 1 || !UPDATE_OPERATOR_KEYS.has(keys[0]))
|
|
2154
|
+
return null;
|
|
2155
|
+
const op = keys[0];
|
|
2156
|
+
if (op !== 'set' && isJsonWriteColumn(qi, key))
|
|
2157
|
+
return null;
|
|
2158
|
+
return op;
|
|
2159
|
+
}
|
|
2160
|
+
/** Whether `key` names a json/jsonb column (non-throwing; unknown keys are not). */
|
|
2161
|
+
function isJsonWriteColumn(qi, key) {
|
|
2162
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
2163
|
+
const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
|
|
2164
|
+
return dbType !== undefined && whereMod.isJsonColumnType(qi, dbType);
|
|
2165
|
+
}
|
|
2166
|
+
const UPDATE_SET = { operation: 'update' };
|
|
2167
|
+
const NO_READS = new Set();
|
|
2168
|
+
/**
|
|
2169
|
+
* Compile a SET list: one clause per entry, in the order given (the canonical
|
|
2170
|
+
* column order of {@link writeEntries}), pushing params as it goes so later
|
|
2171
|
+
* clauses (WHERE, version check) continue the numbering.
|
|
2172
|
+
*
|
|
2173
|
+
* On MySQL the clause STRINGS are then reordered (see
|
|
2174
|
+
* {@link orderForLeftToRightSet}). Params are not: MySQL binds by name
|
|
2175
|
+
* (`:pN`), so a clause keeps its placeholders wherever it lands, and the
|
|
2176
|
+
* cache-hit collector ({@link collectSetParams}) keeps pushing in canonical
|
|
2177
|
+
* order.
|
|
2178
|
+
*/
|
|
2179
|
+
function buildSetClauses(qi, entries, params, ctx = UPDATE_SET) {
|
|
2180
|
+
const compiled = entries.map(([k, v]) => compileSetEntry(qi, k, v, params, ctx));
|
|
2181
|
+
return qi.dialect.capabilities.setClauseOrder === 'left-to-right'
|
|
2182
|
+
? orderForLeftToRightSet(qi, compiled)
|
|
2183
|
+
: compiled.map((c) => c.sql);
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Build a single SET clause entry.
|
|
2187
|
+
*
|
|
2188
|
+
* Supports plain values, atomic operator objects ({ set, increment,
|
|
2189
|
+
* decrement, multiply, divide }) and expressions (`expr.*`, `col()`), as
|
|
2190
|
+
* decided by {@link setOperatorOf}.
|
|
1000
2191
|
*
|
|
1001
2192
|
* Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
|
|
1002
2193
|
* pushes any required params onto the shared params array so that WHERE
|
|
1003
2194
|
* clause numbering continues correctly afterward.
|
|
1004
2195
|
*
|
|
1005
|
-
* `
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
2196
|
+
* `ctx.target` qualifies the column REFERENCES the right-hand side reads
|
|
2197
|
+
* (`"view_count" = "users"."view_count" + $n`): absent for a plain `UPDATE`,
|
|
2198
|
+
* the table name in an upsert, because inside `ON CONFLICT ... DO UPDATE SET`
|
|
2199
|
+
* the target table and `excluded` are BOTH in scope and a bare reference is
|
|
2200
|
+
* 42702 on PostgreSQL. The assignment target on the left stays bare on every
|
|
2201
|
+
* engine (it can only ever be the target table's column), and `set` / literal
|
|
2202
|
+
* values reference no column at all, so their SQL is byte-identical with or
|
|
2203
|
+
* without a qualifier.
|
|
1013
2204
|
*/
|
|
1014
|
-
|
|
2205
|
+
function compileSetEntry(qi, key, value, params, ctx) {
|
|
1015
2206
|
const col = qi.toSqlColumn(key);
|
|
1016
|
-
const
|
|
2207
|
+
const column = qi.toColumn(key);
|
|
2208
|
+
const op = setOperatorOf(qi, key, value);
|
|
2209
|
+
if (op === 'expr') {
|
|
2210
|
+
const compiled = compileSetExpr(exprEnv(qi, key, params, ctx), value);
|
|
2211
|
+
return { sql: `${col} = ${compiled.sql}`, column, reads: compiled.reads };
|
|
2212
|
+
}
|
|
1017
2213
|
// Enum columns get an explicit `::"EnumName"` cast on their value bind
|
|
1018
2214
|
// (see enumTypeForColumn); `''` everywhere else. Value-invariant, so the
|
|
1019
2215
|
// SQL cache and collectSetParams are unaffected.
|
|
1020
|
-
const cast = whereMod.enumCastSuffix(qi,
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
|
|
1031
|
-
const op = keys[0];
|
|
1032
|
-
const opValue = v[op];
|
|
1033
|
-
if (op === 'set') {
|
|
1034
|
-
params.push(coerceWriteValue(qi, key, opValue));
|
|
1035
|
-
return `${col} = ${qi.p(params.length)}${cast}`;
|
|
1036
|
-
}
|
|
1037
|
-
// Arithmetic operators: must be finite numbers
|
|
1038
|
-
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
1039
|
-
throw new ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
|
|
1040
|
-
}
|
|
1041
|
-
if (op === 'increment') {
|
|
1042
|
-
params.push(opValue);
|
|
1043
|
-
return `${col} = ${ref} + ${qi.p(params.length)}`;
|
|
1044
|
-
}
|
|
1045
|
-
if (op === 'decrement') {
|
|
1046
|
-
params.push(opValue);
|
|
1047
|
-
return `${col} = ${ref} - ${qi.p(params.length)}`;
|
|
1048
|
-
}
|
|
1049
|
-
if (op === 'multiply') {
|
|
1050
|
-
params.push(opValue);
|
|
1051
|
-
return `${col} = ${ref} * ${qi.p(params.length)}`;
|
|
1052
|
-
}
|
|
1053
|
-
if (op === 'divide') {
|
|
1054
|
-
params.push(opValue);
|
|
1055
|
-
return `${col} = ${ref} / ${qi.p(params.length)}`;
|
|
1056
|
-
}
|
|
2216
|
+
const cast = whereMod.enumCastSuffix(qi, column);
|
|
2217
|
+
if (op === 'set') {
|
|
2218
|
+
params.push(coerceWriteValue(qi, key, value.set));
|
|
2219
|
+
return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
|
|
2220
|
+
}
|
|
2221
|
+
if (op !== null) {
|
|
2222
|
+
const opValue = value[op];
|
|
2223
|
+
// Arithmetic operators: must be finite numbers
|
|
2224
|
+
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
2225
|
+
throw new ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
|
|
1057
2226
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
//
|
|
1061
|
-
|
|
2227
|
+
params.push(opValue);
|
|
2228
|
+
const ref = ctx.target ? `${ctx.target}.${col}` : col;
|
|
2229
|
+
// Reads only its own column, which no ordering needs to respect.
|
|
2230
|
+
return { sql: `${col} = ${ref} ${arithmeticSql(qi, op, column)} ${qi.p(params.length)}`, column, reads: NO_READS };
|
|
1062
2231
|
}
|
|
2232
|
+
// A misspelled operator gets its own E003 before the bind seam's general
|
|
2233
|
+
// shape check.
|
|
2234
|
+
assertBindableSetValue(qi, key, value);
|
|
1063
2235
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
1064
2236
|
params.push(coerceWriteValue(qi, key, value));
|
|
1065
|
-
return `${col} = ${qi.p(params.length)}${cast}
|
|
2237
|
+
return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* The environment one expression compiles in: how a value leaf binds, how a
|
|
2241
|
+
* column of the target row and of the incoming row render, and how a case
|
|
2242
|
+
* predicate compiles.
|
|
2243
|
+
*/
|
|
2244
|
+
function exprEnv(qi, key, params, ctx) {
|
|
2245
|
+
const cast = exprValueCast(qi, key);
|
|
2246
|
+
const target = ctx.target;
|
|
2247
|
+
const incoming = ctx.incoming;
|
|
2248
|
+
return {
|
|
2249
|
+
qi,
|
|
2250
|
+
targetKey: key,
|
|
2251
|
+
operation: ctx.operation,
|
|
2252
|
+
bind: (value, arithmetic) => {
|
|
2253
|
+
params.push(coerceWriteValue(qi, key, value));
|
|
2254
|
+
return `${qi.p(params.length)}${arithmetic ? arithmeticValueCast(qi, key, cast, arithmetic) : cast}`;
|
|
2255
|
+
},
|
|
2256
|
+
column: (column) => (target ? `${target}.${qi.q(column)}` : qi.q(column)),
|
|
2257
|
+
excluded: incoming ? (column) => `${incoming}.${qi.q(column)}` : undefined,
|
|
2258
|
+
insertColumns: ctx.insertColumns,
|
|
2259
|
+
predicate: (when) => {
|
|
2260
|
+
const sql = target
|
|
2261
|
+
? whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, target, when, params)
|
|
2262
|
+
: whereMod.buildWhereClause(qi, when, params);
|
|
2263
|
+
if (sql === null) {
|
|
2264
|
+
throw new ValidationError(`expr.case() on "${qi.table}": an arm's when compiles to no predicate.`);
|
|
2265
|
+
}
|
|
2266
|
+
return sql;
|
|
2267
|
+
},
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* A type token that is safe to write after `::` as is: a plain name, a
|
|
2272
|
+
* multi-word built-in (`double precision`), optionally with a precision.
|
|
2273
|
+
*/
|
|
2274
|
+
const SAFE_CAST_TYPE = /^[A-Za-z_][A-Za-z0-9_]*( [A-Za-z_][A-Za-z0-9_]*)*(\(\d+( ?, ?\d+)?\))?$/;
|
|
2275
|
+
/**
|
|
2276
|
+
* The cast every bound value inside an expression carries on PostgreSQL: the
|
|
2277
|
+
* TARGET column's type. node-postgres sends parameters untyped, so in an
|
|
2278
|
+
* expression whose operands are all bound values PostgreSQL infers `text` and
|
|
2279
|
+
* fails (`CASE WHEN ... THEN $1 ELSE $2 END` into an int column is 42804, and
|
|
2280
|
+
* `$1 + $2` is 42725). Enums use the quoted enum cast; a type the metadata
|
|
2281
|
+
* does not name plainly, or one from another schema, gets none. Other engines
|
|
2282
|
+
* bind typed values and get none.
|
|
2283
|
+
*/
|
|
2284
|
+
function exprValueCast(qi, key) {
|
|
2285
|
+
if (!qi.dialect.capabilities.paramTypeCasts)
|
|
2286
|
+
return '';
|
|
2287
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
2288
|
+
if (column === undefined)
|
|
2289
|
+
return '';
|
|
2290
|
+
const enumCast = whereMod.enumCastSuffix(qi, column);
|
|
2291
|
+
if (enumCast)
|
|
2292
|
+
return enumCast;
|
|
2293
|
+
if (qi.crossSchemaTypeColumns.has(column))
|
|
2294
|
+
return '';
|
|
2295
|
+
const type = qi.columnPgTypeMap.get(column);
|
|
2296
|
+
if (type === undefined || type.startsWith('_') || !SAFE_CAST_TYPE.test(type))
|
|
2297
|
+
return '';
|
|
2298
|
+
return `::${type}`;
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* The cast on a bound arithmetic operand on PostgreSQL: the operand's own
|
|
2302
|
+
* class, so `n / 2` stays integer division into a float column and `n * 1.5`
|
|
2303
|
+
* is not forced through an integer cast. An integer operand keeps the target
|
|
2304
|
+
* column's cast when that column is an integer one, `int8` otherwise; a
|
|
2305
|
+
* non-integer one is `numeric`. Other engines bind typed values and get none.
|
|
2306
|
+
*/
|
|
2307
|
+
function arithmeticValueCast(qi, key, targetCast, cls) {
|
|
2308
|
+
if (!qi.dialect.capabilities.paramTypeCasts)
|
|
2309
|
+
return '';
|
|
2310
|
+
if (cls === 'dec')
|
|
2311
|
+
return '::numeric';
|
|
2312
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
2313
|
+
return column !== undefined && targetCast !== '' && isIntegerColumnType(qi.columnPgTypeMap.get(column))
|
|
2314
|
+
? targetCast
|
|
2315
|
+
: '::int8';
|
|
1066
2316
|
}
|
|
1067
2317
|
/**
|
|
1068
|
-
*
|
|
2318
|
+
* MySQL evaluates a single-table UPDATE's SET list LEFT TO RIGHT, and a later
|
|
2319
|
+
* assignment sees the NEW value of an earlier column; PostgreSQL, SQLite and
|
|
2320
|
+
* SQL Server read old values. So `{ a: col('b'), b: col('a') }` swaps there
|
|
2321
|
+
* and duplicates on MySQL. `ON DUPLICATE KEY UPDATE` behaves the same way.
|
|
2322
|
+
*
|
|
2323
|
+
* Order the list so every assignment that READS a column comes before the
|
|
2324
|
+
* assignment that WRITES it: a stable topological sort that keeps the
|
|
2325
|
+
* canonical order wherever the reads allow, so the order is a pure function of
|
|
2326
|
+
* the data's shape and the SQL-cache key (which already captures that shape)
|
|
2327
|
+
* stays sufficient. A cycle (a swap) has no such order and is refused.
|
|
2328
|
+
*/
|
|
2329
|
+
function orderForLeftToRightSet(qi, entries) {
|
|
2330
|
+
if (entries.every((e) => e.reads.size === 0))
|
|
2331
|
+
return entries.map((e) => e.sql);
|
|
2332
|
+
const writer = new Map();
|
|
2333
|
+
entries.forEach((e, i) => {
|
|
2334
|
+
writer.set(e.column, i);
|
|
2335
|
+
});
|
|
2336
|
+
const before = entries.map(() => []);
|
|
2337
|
+
const pending = entries.map(() => 0);
|
|
2338
|
+
entries.forEach((e, i) => {
|
|
2339
|
+
for (const column of e.reads) {
|
|
2340
|
+
const j = writer.get(column);
|
|
2341
|
+
if (j === undefined || j === i)
|
|
2342
|
+
continue;
|
|
2343
|
+
before[i].push(j);
|
|
2344
|
+
pending[j]++;
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
const ready = pending.flatMap((n, i) => (n === 0 ? [i] : []));
|
|
2348
|
+
const order = [];
|
|
2349
|
+
while (ready.length > 0) {
|
|
2350
|
+
ready.sort((a, b) => a - b);
|
|
2351
|
+
const i = ready.shift();
|
|
2352
|
+
order.push(i);
|
|
2353
|
+
for (const j of before[i])
|
|
2354
|
+
if (--pending[j] === 0)
|
|
2355
|
+
ready.push(j);
|
|
2356
|
+
}
|
|
2357
|
+
if (order.length < entries.length) {
|
|
2358
|
+
const stuck = entries
|
|
2359
|
+
.filter((_, i) => pending[i] > 0)
|
|
2360
|
+
.map((e) => qi.tableMeta.reverseColumnMap[e.column] ?? e.column);
|
|
2361
|
+
throw new UnsupportedFeatureError(`An update on "${qi.table}" that reads ${stuck.map((f) => `"${f}"`).join(' and ')} while assigning them`, qi.dialect.name, "MySQL evaluates SET left to right, so a later assignment sees the earlier one's new value and this " +
|
|
2362
|
+
'cannot be expressed. Split it into two updates.');
|
|
2363
|
+
}
|
|
2364
|
+
return order.map((i) => entries[i].sql);
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Reject a `data` value that reached {@link buildSetClauses}'s plain-value
|
|
1069
2368
|
* fallthrough as a plain object with exactly ONE key that is not a known
|
|
1070
2369
|
* atomic operator. On a scalar column that shape can only be a misspelled
|
|
1071
2370
|
* operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
|
|
@@ -1096,52 +2395,47 @@ export function assertBindableSetValue(qi, key, value) {
|
|
|
1096
2395
|
}
|
|
1097
2396
|
/**
|
|
1098
2397
|
* Fingerprint SET clauses for update/updateMany.
|
|
1099
|
-
* Captures key names + operator types (set/increment/etc) but not values.
|
|
2398
|
+
* Captures key names + operator types (set/increment/etc) but not values. An
|
|
2399
|
+
* expression contributes `k:expr(<shape>)` from the same walk that compiles
|
|
2400
|
+
* it, so a statement without one keeps its key byte-identical.
|
|
1100
2401
|
*/
|
|
1101
2402
|
export function fingerprintSet(qi, data) {
|
|
1102
2403
|
const entries = writeEntries(qi, data);
|
|
1103
2404
|
const parts = [];
|
|
1104
2405
|
for (const [k, v] of entries) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const keys = Object.keys(v);
|
|
1111
|
-
if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
|
|
1112
|
-
parts.push(`${k}:${keys[0]}`);
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
2406
|
+
const op = setOperatorOf(qi, k, v);
|
|
2407
|
+
if (op === 'expr') {
|
|
2408
|
+
const env = exprEnv(qi, k, [], UPDATE_SET);
|
|
2409
|
+
parts.push(`${k}:expr(${compileSetExpr(env, v).fingerprint})`);
|
|
2410
|
+
continue;
|
|
1115
2411
|
}
|
|
1116
|
-
parts.push(`${k}
|
|
2412
|
+
parts.push(`${k}:${op ?? 'eq'}`);
|
|
1117
2413
|
}
|
|
1118
2414
|
return parts.join(',');
|
|
1119
2415
|
}
|
|
1120
2416
|
/**
|
|
1121
|
-
* Collect SET params for update/updateMany. Mirrors
|
|
2417
|
+
* Collect SET params for update/updateMany. Mirrors buildSetClauses param order.
|
|
1122
2418
|
*/
|
|
1123
2419
|
export function collectSetParams(qi, data, params) {
|
|
1124
2420
|
const entries = writeEntries(qi, data);
|
|
1125
2421
|
for (const [k, v] of entries) {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
// Mirror the build path's misspelled-operator guard: the SET fingerprint
|
|
1141
|
-
// is `key:eq` for both a plain value and a bogus operator object, so a
|
|
1142
|
-
// cache HIT would otherwise bind the object and skip the check.
|
|
1143
|
-
assertBindableSetValue(qi, k, v);
|
|
2422
|
+
const op = setOperatorOf(qi, k, v);
|
|
2423
|
+
if (op === 'expr') {
|
|
2424
|
+
// The build's own walk: it binds the same leaves in the same order.
|
|
2425
|
+
compileSetExpr(exprEnv(qi, k, params, UPDATE_SET), v);
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
if (op !== null) {
|
|
2429
|
+
// `set` is the only operator that binds a user value of arbitrary type;
|
|
2430
|
+
// the arithmetic operators are finite numbers, which coerceWriteValue
|
|
2431
|
+
// returns untouched. Mirrors buildSetClauses.
|
|
2432
|
+
params.push(coerceWriteValue(qi, k, v[op]));
|
|
2433
|
+
continue;
|
|
1144
2434
|
}
|
|
2435
|
+
// Mirror the build path's misspelled-operator guard: the SET fingerprint
|
|
2436
|
+
// is `key:eq` for both a plain value and a bogus operator object, so a
|
|
2437
|
+
// cache HIT would otherwise bind the object and skip the check.
|
|
2438
|
+
assertBindableSetValue(qi, k, v);
|
|
1145
2439
|
params.push(coerceWriteValue(qi, k, v));
|
|
1146
2440
|
}
|
|
1147
2441
|
}
|