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/cjs/query/writes.js
CHANGED
|
@@ -50,8 +50,12 @@ exports.buildReselectByWhere = buildReselectByWhere;
|
|
|
50
50
|
exports.buildCreate = buildCreate;
|
|
51
51
|
exports.makeCreateReselect = makeCreateReselect;
|
|
52
52
|
exports.buildCreateMany = buildCreateMany;
|
|
53
|
+
exports.planCreateMany = planCreateMany;
|
|
54
|
+
exports.explainConflictKeyCollision = explainConflictKeyCollision;
|
|
53
55
|
exports.buildUpdate = buildUpdate;
|
|
54
56
|
exports.buildDelete = buildDelete;
|
|
57
|
+
exports.assertNoBulkWritePaging = assertNoBulkWritePaging;
|
|
58
|
+
exports.assertNoOptimisticLock = assertNoOptimisticLock;
|
|
55
59
|
exports.upsertLookupReason = upsertLookupReason;
|
|
56
60
|
exports.buildUpsert = buildUpsert;
|
|
57
61
|
exports.buildUpdateMany = buildUpdateMany;
|
|
@@ -65,21 +69,41 @@ exports.writeReselectSelection = writeReselectSelection;
|
|
|
65
69
|
exports.parseWriteRow = parseWriteRow;
|
|
66
70
|
exports.assertWritable = assertWritable;
|
|
67
71
|
exports.assertNoGeneratedColumns = assertNoGeneratedColumns;
|
|
68
|
-
exports.
|
|
72
|
+
exports.setOperatorOf = setOperatorOf;
|
|
69
73
|
exports.assertBindableSetValue = assertBindableSetValue;
|
|
70
74
|
exports.fingerprintSet = fingerprintSet;
|
|
71
75
|
exports.collectSetParams = collectSetParams;
|
|
76
|
+
const dialect_js_1 = require("../dialect.js");
|
|
72
77
|
const errors_js_1 = require("../errors.js");
|
|
73
78
|
const schema_js_1 = require("../schema.js");
|
|
79
|
+
const column_ref_js_1 = require("./column-ref.js");
|
|
74
80
|
const compound_unique_js_1 = require("./compound-unique.js");
|
|
81
|
+
const expr_js_1 = require("./expr.js");
|
|
75
82
|
const filters_js_1 = require("./filters.js");
|
|
83
|
+
const json_value_js_1 = require("./json-value.js");
|
|
84
|
+
const normalize_args_js_1 = require("./normalize-args.js");
|
|
76
85
|
const types_js_1 = require("./types.js");
|
|
77
86
|
const utils_js_1 = require("./utils.js");
|
|
78
87
|
const whereMod = __importStar(require("./where.js"));
|
|
79
88
|
/**
|
|
80
|
-
* Normalize one `data` value before it is bound as a write param.
|
|
89
|
+
* Normalize one `data` value before it is bound as a write param. This is THE
|
|
90
|
+
* write bind seam: create, createMany, update, updateMany, both halves of
|
|
91
|
+
* upsert, the cache-hit param collectors and (through those public methods)
|
|
92
|
+
* the nested-write engine all bind through it.
|
|
81
93
|
*
|
|
82
|
-
*
|
|
94
|
+
* JSON columns (json / jsonb):
|
|
95
|
+
*
|
|
96
|
+
* - A JS array is serialized with `JSON.stringify`. The drivers' default
|
|
97
|
+
* array serialization is a Postgres ARRAY literal, so `[]` was stored as the
|
|
98
|
+
* object `{}`, `["a"]` failed with 22P02, and `createMany`'s UNNEST
|
|
99
|
+
* transpose flattened `[1, 2, 3]` into three rows.
|
|
100
|
+
* - A {@link jsonValue} wrapper is serialized with `JSON.stringify`, which is
|
|
101
|
+
* how a JSON string or a JSON `null` is written.
|
|
102
|
+
* - A JS string is RAW JSON TEXT and passes through untouched, so
|
|
103
|
+
* `JSON.stringify(value)` callers keep storing the parsed value. A plain
|
|
104
|
+
* object passes through too: every driver already serializes it as JSON.
|
|
105
|
+
*
|
|
106
|
+
* Temporal columns, a JS `Date`:
|
|
83
107
|
*
|
|
84
108
|
* 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
|
|
85
109
|
* as a full ISO timestamp with the process offset and Postgres answers
|
|
@@ -89,45 +113,246 @@ const whereMod = __importStar(require("./where.js"));
|
|
|
89
113
|
* non-UTC process the stored value is wrong and (because the read path
|
|
90
114
|
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
91
115
|
* the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
|
|
92
|
-
* in query/utils.ts.
|
|
116
|
+
* in query/utils.ts. `timestamptz` is deliberately untouched: it stores a
|
|
117
|
+
* real instant and the driver's local-offset string is already correct.
|
|
118
|
+
*
|
|
119
|
+
* A plain object or an array bound to a known scalar column (text, int, uuid,
|
|
120
|
+
* ...) is refused with E003, and so is a `Map` or `Set` anywhere; see
|
|
121
|
+
* {@link assertScalarBindable} and {@link assertNotCollection}.
|
|
93
122
|
*
|
|
94
|
-
* `
|
|
95
|
-
*
|
|
123
|
+
* Expressions (`expr.*`, `col()`) never bind as a value: the update paths
|
|
124
|
+
* compile them before reaching here, so one that arrives is in a position that
|
|
125
|
+
* takes values only (create data, `{ set }`) and is refused. An unbranded
|
|
126
|
+
* object shaped like one is refused on a non-JSON column on every engine.
|
|
96
127
|
*
|
|
97
|
-
* Every other value
|
|
98
|
-
*
|
|
99
|
-
* column
|
|
128
|
+
* Every other value is returned by identity, so a primitive costs one check
|
|
129
|
+
* and no lookup. The exception is a string on an engine that defines the
|
|
130
|
+
* dialect's `checkTextWrite` (SQLite), which is checked against its column's
|
|
131
|
+
* type first; see {@link checkTextWrite}.
|
|
100
132
|
*/
|
|
101
133
|
function coerceWriteValue(qi, key, value) {
|
|
102
|
-
|
|
103
|
-
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
if (qi.dialect.checkTextWrite !== undefined)
|
|
136
|
+
checkTextWrite(qi, key, value);
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
if (value === null || typeof value !== 'object')
|
|
104
140
|
return value;
|
|
141
|
+
if ((0, expr_js_1.isSqlExpr)(value) || (0, column_ref_js_1.isColumnRef)(value)) {
|
|
142
|
+
throw new errors_js_1.ValidationError((0, expr_js_1.exprNotAllowedMessage)(qi.table, key, value));
|
|
143
|
+
}
|
|
144
|
+
assertNotCollection(qi, key, value);
|
|
105
145
|
// THE key-resolution rule, shared with `toColumn` (the SQL side of this very
|
|
106
|
-
// statement) so the two can never disagree about which column a key names
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// cache-HIT param-collect path, where an unknown key must not turn into a
|
|
111
|
-
// different error than the build path already raises.
|
|
146
|
+
// statement) so the two can never disagree about which column a key names.
|
|
147
|
+
// Non-throwing, because this also runs on the cache-HIT param-collect path,
|
|
148
|
+
// where an unknown key must not turn into a different error than the build
|
|
149
|
+
// path already raises.
|
|
112
150
|
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
113
|
-
|
|
151
|
+
const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
|
|
152
|
+
if ((0, json_value_js_1.isJsonValue)(value))
|
|
153
|
+
return jsonText(qi, key, value.value);
|
|
154
|
+
if (dbType !== undefined && whereMod.isJsonColumnType(qi, dbType)) {
|
|
155
|
+
return Array.isArray(value) ? jsonText(qi, key, value) : value;
|
|
156
|
+
}
|
|
157
|
+
if ((0, expr_js_1.isPlainExprShape)(value)) {
|
|
158
|
+
throw new errors_js_1.ValidationError((0, expr_js_1.plainExprShapeMessage)(qi.table, key, String(value.kind)));
|
|
159
|
+
}
|
|
160
|
+
assertScalarBindable(qi, key, dbType, value);
|
|
161
|
+
if (!(value instanceof Date) && !Array.isArray(value))
|
|
114
162
|
return value;
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
163
|
+
// A column missing from the type map resolves to undefined → no coercion.
|
|
164
|
+
// The gate is where.ts's, so a written Date and a compared Date take the
|
|
165
|
+
// same form: a `date` column the UTC date part on every SQL engine, a
|
|
166
|
+
// zone-less `timestamp` the UTC literal on PostgreSQL only.
|
|
167
|
+
return (0, utils_js_1.coerceTemporalValue)(dbType, value, whereMod.utcDateTimeRewrite(qi, dbType));
|
|
119
168
|
}
|
|
120
169
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
170
|
+
* Ask the engine whether it stores `value` in the column `key` names as that
|
|
171
|
+
* column's type (the dialect's `checkTextWrite`); E003 when it would not.
|
|
172
|
+
* An unknown key is left to the build path's own error, as in
|
|
173
|
+
* {@link coerceWriteValue}.
|
|
174
|
+
*/
|
|
175
|
+
function checkTextWrite(qi, key, value) {
|
|
176
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
177
|
+
const columnType = column ? qi.columnPgTypeMap.get(column) : undefined;
|
|
178
|
+
if (!column || columnType === undefined)
|
|
179
|
+
return;
|
|
180
|
+
const refusal = qi.dialect.checkTextWrite?.(qi.table, column, columnType, value);
|
|
181
|
+
if (refusal !== undefined)
|
|
182
|
+
throw new errors_js_1.ValidationError(refusal);
|
|
183
|
+
}
|
|
184
|
+
/** `JSON.stringify(value)` for a JSON column, or E003 when it has no JSON form. */
|
|
185
|
+
function jsonText(qi, key, value) {
|
|
186
|
+
assertNotCollection(qi, key, value);
|
|
187
|
+
const text = (0, json_value_js_1.serializeJsonValue)(value);
|
|
188
|
+
if (text === undefined) {
|
|
189
|
+
throw new errors_js_1.ValidationError(`Cannot write "${qi.table}.${key}": the value has no JSON form (${describeValue(value)}). ` +
|
|
190
|
+
'Convert it first, e.g. a bigint to a string or a number.');
|
|
191
|
+
}
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
function describeValue(value) {
|
|
195
|
+
return typeof value === 'bigint' ? 'it contains a bigint' : `got ${typeof value}`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* PostgreSQL column types on which a plain object or an array can only be a
|
|
199
|
+
* mistake. The driver serializes an object as JSON text and an array as an
|
|
200
|
+
* ARRAY literal, so on a text column `{ first, last }` was stored as
|
|
201
|
+
* `{"first":"A","last":"B"}` and `['a']` as `{"a"}`, silently. A type outside
|
|
202
|
+
* this list (enums, domains, extension and user types) is never refused here,
|
|
203
|
+
* because a domain over jsonb or an extension type may legitimately take one.
|
|
128
204
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
205
|
+
const SCALAR_PG_TYPES = new Set([
|
|
206
|
+
'text',
|
|
207
|
+
'varchar',
|
|
208
|
+
'character varying',
|
|
209
|
+
'char',
|
|
210
|
+
'character',
|
|
211
|
+
'bpchar',
|
|
212
|
+
'citext',
|
|
213
|
+
'name',
|
|
214
|
+
'uuid',
|
|
215
|
+
'int2',
|
|
216
|
+
'int4',
|
|
217
|
+
'int8',
|
|
218
|
+
'smallint',
|
|
219
|
+
'integer',
|
|
220
|
+
'bigint',
|
|
221
|
+
'numeric',
|
|
222
|
+
'decimal',
|
|
223
|
+
'float4',
|
|
224
|
+
'float8',
|
|
225
|
+
'real',
|
|
226
|
+
'double precision',
|
|
227
|
+
'money',
|
|
228
|
+
'bool',
|
|
229
|
+
'boolean',
|
|
230
|
+
'date',
|
|
231
|
+
'time',
|
|
232
|
+
'timetz',
|
|
233
|
+
'timestamp',
|
|
234
|
+
'timestamptz',
|
|
235
|
+
'time without time zone',
|
|
236
|
+
'time with time zone',
|
|
237
|
+
'timestamp without time zone',
|
|
238
|
+
'timestamp with time zone',
|
|
239
|
+
'interval',
|
|
240
|
+
'bytea',
|
|
241
|
+
'inet',
|
|
242
|
+
'cidr',
|
|
243
|
+
'macaddr',
|
|
244
|
+
'xml',
|
|
245
|
+
]);
|
|
246
|
+
/**
|
|
247
|
+
* Column types, in every engine's spelling, that can only hold a scalar: a
|
|
248
|
+
* plain object or an array written to one is a mistake on every engine (the
|
|
249
|
+
* MySQL and SQLite shims stored it as JSON text, in an INTEGER column too).
|
|
250
|
+
*/
|
|
251
|
+
const NON_TEXT_SCALAR_TYPES = new Set([
|
|
252
|
+
'int',
|
|
253
|
+
'integer',
|
|
254
|
+
'tinyint',
|
|
255
|
+
'smallint',
|
|
256
|
+
'mediumint',
|
|
257
|
+
'bigint',
|
|
258
|
+
'int2',
|
|
259
|
+
'int4',
|
|
260
|
+
'int8',
|
|
261
|
+
'numeric',
|
|
262
|
+
'decimal',
|
|
263
|
+
'dec',
|
|
264
|
+
'real',
|
|
265
|
+
'float',
|
|
266
|
+
'double',
|
|
267
|
+
'double precision',
|
|
268
|
+
'money',
|
|
269
|
+
'smallmoney',
|
|
270
|
+
'bit',
|
|
271
|
+
'bool',
|
|
272
|
+
'boolean',
|
|
273
|
+
'date',
|
|
274
|
+
'time',
|
|
275
|
+
'datetime',
|
|
276
|
+
'datetime2',
|
|
277
|
+
'smalldatetime',
|
|
278
|
+
'datetimeoffset',
|
|
279
|
+
'timestamp',
|
|
280
|
+
'timestamptz',
|
|
281
|
+
'year',
|
|
282
|
+
'uuid',
|
|
283
|
+
'uniqueidentifier',
|
|
284
|
+
'blob',
|
|
285
|
+
'tinyblob',
|
|
286
|
+
'mediumblob',
|
|
287
|
+
'longblob',
|
|
288
|
+
'binary',
|
|
289
|
+
'varbinary',
|
|
290
|
+
'image',
|
|
291
|
+
]);
|
|
292
|
+
/**
|
|
293
|
+
* MySQL's text types. MySQL has a JSON column type, so a text column is not
|
|
294
|
+
* where an object goes, as on PostgreSQL. SQLite and SQL Server have no JSON
|
|
295
|
+
* type: Turbine's own DDL stores json in TEXT / NVARCHAR(MAX), so an object
|
|
296
|
+
* written to a text column there is serialized as JSON on purpose.
|
|
297
|
+
*/
|
|
298
|
+
const MYSQL_TEXT_TYPES = new Set([
|
|
299
|
+
'char',
|
|
300
|
+
'varchar',
|
|
301
|
+
'tinytext',
|
|
302
|
+
'text',
|
|
303
|
+
'mediumtext',
|
|
304
|
+
'longtext',
|
|
305
|
+
'enum',
|
|
306
|
+
'set',
|
|
307
|
+
]);
|
|
308
|
+
/** A column type token, lowercased, without a length, precision or sign modifier. */
|
|
309
|
+
function baseColumnType(dbType) {
|
|
310
|
+
return dbType
|
|
311
|
+
.toLowerCase()
|
|
312
|
+
.replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, '')
|
|
313
|
+
.replace(/\b(unsigned|signed|zerofill)\b/g, '')
|
|
314
|
+
.replace(/\s+/g, ' ')
|
|
315
|
+
.trim();
|
|
316
|
+
}
|
|
317
|
+
/** Whether this engine's column of `dbType` refuses a plain object or an array. */
|
|
318
|
+
function refusesStructuredValue(qi, dbType) {
|
|
319
|
+
const columns = qi.dialect.capabilities.structuredValueColumns;
|
|
320
|
+
if (columns === 'pg-scalars')
|
|
321
|
+
return SCALAR_PG_TYPES.has(dbType.toLowerCase());
|
|
322
|
+
const t = baseColumnType(dbType);
|
|
323
|
+
return NON_TEXT_SCALAR_TYPES.has(t) || (columns === 'scalars-and-text' && MYSQL_TEXT_TYPES.has(t));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Refuse a `Map` or `Set` as a written value, on every column. Neither has a
|
|
327
|
+
* stored form: JSON.stringify writes `{}` for both, so the entries were lost
|
|
328
|
+
* without an error, on a json column too.
|
|
329
|
+
*/
|
|
330
|
+
function assertNotCollection(qi, key, value) {
|
|
331
|
+
if (!(value instanceof Map) && !(value instanceof Set))
|
|
332
|
+
return;
|
|
333
|
+
const what = value instanceof Map ? 'a Map' : 'a Set';
|
|
334
|
+
throw new errors_js_1.ValidationError(`Cannot write ${what} to "${qi.table}.${key}": it has no stored form, and JSON would write it as {} and lose ` +
|
|
335
|
+
`its entries. Convert it first (${value instanceof Map ? 'Object.fromEntries(map)' : '[...set]'}).`);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Refuse a plain object, or an array, bound to a known scalar column. Dates,
|
|
339
|
+
* Buffers, class instances (a Decimal wrapper, an interval) and every
|
|
340
|
+
* primitive are untouched, and so is every array column (`_text`) since its
|
|
341
|
+
* type is not a scalar one. Which text columns refuse one is per engine; see
|
|
342
|
+
* {@link MYSQL_TEXT_TYPES}.
|
|
343
|
+
*/
|
|
344
|
+
function assertScalarBindable(qi, key, dbType, value) {
|
|
345
|
+
if (dbType === undefined)
|
|
346
|
+
return;
|
|
347
|
+
const isArray = Array.isArray(value);
|
|
348
|
+
if (!isArray && !(0, filters_js_1.isUnmatchedPlainObject)(value))
|
|
349
|
+
return;
|
|
350
|
+
if (!refusesStructuredValue(qi, dbType))
|
|
351
|
+
return;
|
|
352
|
+
throw new errors_js_1.ValidationError(`Cannot write ${isArray ? 'an array' : 'an object'} to "${qi.table}.${key}" (column type ${dbType}). ` +
|
|
353
|
+
'Only json/jsonb columns take objects, and only array columns take arrays. ' +
|
|
354
|
+
'To store JSON text in this column, pass `JSON.stringify(value)` or `jsonValue(value)`.' +
|
|
355
|
+
(isArray ? '' : ' Atomic operators (`{ set: v }`, `{ increment: n }`, ...) apply in update data only.'));
|
|
131
356
|
}
|
|
132
357
|
/**
|
|
133
358
|
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
@@ -135,11 +360,157 @@ function utcDateTimeWrites(qi) {
|
|
|
135
360
|
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
136
361
|
* builder as reads, so no user value is interpolated.
|
|
137
362
|
*/
|
|
138
|
-
function buildReselectByWhere(qi, whereObj, projection) {
|
|
363
|
+
function buildReselectByWhere(qi, whereObj, projection, lockRows = false) {
|
|
139
364
|
const params = [];
|
|
140
365
|
const clause = whereMod.buildWhereClause(qi, whereObj, params);
|
|
141
366
|
const where = clause ? ` WHERE ${clause}` : '';
|
|
142
|
-
return {
|
|
367
|
+
return {
|
|
368
|
+
sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}${lockRows ? forUpdate(qi) : ''}`,
|
|
369
|
+
params,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Row-lock suffix for a read inside a reselect write's transaction, so a
|
|
374
|
+
* concurrent write of the same row waits instead of interleaving. Appended
|
|
375
|
+
* where the engine's reselect locks rows (MySQL); elsewhere nothing is.
|
|
376
|
+
*/
|
|
377
|
+
function forUpdate(qi) {
|
|
378
|
+
return qi.dialect.capabilities.reselectLocksRows ? ' FOR UPDATE' : '';
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Whether a write that sets `columns` may move a row out of `whereObj`, so the
|
|
382
|
+
* row must be re-read by its key rather than by `where`: a soft delete under a
|
|
383
|
+
* `deletedAt: null` global filter, `NOT: { status: 'done' }` with `status:
|
|
384
|
+
* 'done'`, a key change.
|
|
385
|
+
*
|
|
386
|
+
* Read off the compiled predicate, where every column it reads is quoted at
|
|
387
|
+
* any depth (`NOT`, `OR`, a relation filter's correlation, a column reference,
|
|
388
|
+
* the global filter), so no where shape is missed. A column of another table
|
|
389
|
+
* with the same name answers true, which costs one locking read and is never
|
|
390
|
+
* wrong.
|
|
391
|
+
*/
|
|
392
|
+
function writeMayMoveRow(qi, whereObj, columns) {
|
|
393
|
+
if (columns.size === 0)
|
|
394
|
+
return false;
|
|
395
|
+
const clause = whereMod.buildWhereClause(qi, whereObj, []);
|
|
396
|
+
if (!clause)
|
|
397
|
+
return false;
|
|
398
|
+
for (const column of columns)
|
|
399
|
+
if (clause.includes(qi.q(column)))
|
|
400
|
+
return true;
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The columns that identify a row after a write: the primary key, else the
|
|
405
|
+
* first unique key, else none.
|
|
406
|
+
*/
|
|
407
|
+
function identityColumns(qi) {
|
|
408
|
+
return qi.tableMeta.primaryKey.length > 0 ? qi.tableMeta.primaryKey : ((0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta)[0] ?? []);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Lock the row(s) `whereObj` matches and return the first one's identity key as
|
|
412
|
+
* a `where`, with every key column `data` assigns replaced by its new value, so
|
|
413
|
+
* the row can be re-read after the write moves it. `null` when no row matched.
|
|
414
|
+
*
|
|
415
|
+
* Refused (E017) before anything is written: a table with no key to re-read by,
|
|
416
|
+
* a key column assigned by something whose result is not known here (an
|
|
417
|
+
* expression, `divide`, a non-integer operand), and, unless the call is a
|
|
418
|
+
* full-table write, a `where` that matched more than one row, which only an
|
|
419
|
+
* engine comparing a key loosely produces.
|
|
420
|
+
*/
|
|
421
|
+
async function lockMovedRowKey(qi, exec, whereObj, data, operation, manyRows) {
|
|
422
|
+
const identity = identityColumns(qi);
|
|
423
|
+
if (identity.length === 0) {
|
|
424
|
+
throw new errors_js_1.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 ' +
|
|
425
|
+
'primary key, or leave the columns `where` reads out of data.');
|
|
426
|
+
}
|
|
427
|
+
const assigned = new Map();
|
|
428
|
+
for (const [key, value] of writeEntries(qi, data)) {
|
|
429
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
430
|
+
if (column !== undefined && identity.includes(column))
|
|
431
|
+
assigned.set(column, [key, value]);
|
|
432
|
+
}
|
|
433
|
+
const params = [];
|
|
434
|
+
const clause = whereMod.buildWhereClause(qi, whereObj, params);
|
|
435
|
+
const sql = `SELECT ${identity.map((c) => qi.q(c)).join(', ')} FROM ${qi.q(qi.table)}${clause ? ` WHERE ${clause}` : ''}${forUpdate(qi)}`;
|
|
436
|
+
const rows = (await exec(sql, params)).rows;
|
|
437
|
+
const row = rows[0];
|
|
438
|
+
if (!row)
|
|
439
|
+
return null;
|
|
440
|
+
if (!manyRows)
|
|
441
|
+
assertOneRowByKey(qi, operation, identity, rows.length);
|
|
442
|
+
const key = {};
|
|
443
|
+
for (const column of identity) {
|
|
444
|
+
const entry = assigned.get(column);
|
|
445
|
+
const next = entry === undefined ? row[column] : movedKeyValue(qi, operation, entry[0], row[column], entry[1]);
|
|
446
|
+
key[fieldOf(qi, column)] = reselectKeyValue(qi, column, next);
|
|
447
|
+
}
|
|
448
|
+
return key;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* The value a key column holds after `assigned` is written over `current`: a
|
|
452
|
+
* plain value or `{ set }` as given, and `increment` / `decrement` / `multiply`
|
|
453
|
+
* by an integer computed exactly on an integer key. Anything else is refused
|
|
454
|
+
* (E017) because its result, and so the row, cannot be known before the write.
|
|
455
|
+
*/
|
|
456
|
+
function movedKeyValue(qi, operation, key, current, assigned) {
|
|
457
|
+
const op = setOperatorOf(qi, key, assigned);
|
|
458
|
+
if (op === null)
|
|
459
|
+
return assigned;
|
|
460
|
+
if (op === 'set')
|
|
461
|
+
return assigned.set;
|
|
462
|
+
const operand = op === 'expr' ? undefined : assigned[op];
|
|
463
|
+
const base = integerOf(current);
|
|
464
|
+
if (base !== undefined && typeof operand === 'number' && Number.isSafeInteger(operand) && op !== 'divide') {
|
|
465
|
+
const n = BigInt(operand);
|
|
466
|
+
const next = op === 'increment' ? base + n : op === 'decrement' ? base - n : base * n;
|
|
467
|
+
return typeof current === 'number' &&
|
|
468
|
+
next >= BigInt(Number.MIN_SAFE_INTEGER) &&
|
|
469
|
+
next <= BigInt(Number.MAX_SAFE_INTEGER)
|
|
470
|
+
? Number(next)
|
|
471
|
+
: typeof current === 'bigint'
|
|
472
|
+
? next
|
|
473
|
+
: next.toString();
|
|
474
|
+
}
|
|
475
|
+
throw new errors_js_1.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 ' +
|
|
476
|
+
`write the new key with \`{ set: value }\`${op === 'expr' ? '' : ', or use an integer `increment`, `decrement` or `multiply`'}.`);
|
|
477
|
+
}
|
|
478
|
+
/** The columns the top level of `where` names, for a message. */
|
|
479
|
+
function whereColumns(qi, where) {
|
|
480
|
+
return Object.keys(where).flatMap((key) => {
|
|
481
|
+
const column = where[key] === undefined ? undefined : (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
482
|
+
return column === undefined ? [] : [column];
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/** An integer key value as a bigint, or `undefined` for anything else. */
|
|
486
|
+
function integerOf(value) {
|
|
487
|
+
if (typeof value === 'bigint')
|
|
488
|
+
return value;
|
|
489
|
+
if (typeof value === 'number')
|
|
490
|
+
return Number.isSafeInteger(value) ? BigInt(value) : undefined;
|
|
491
|
+
if (typeof value === 'string' && /^-?[0-9]+$/.test(value))
|
|
492
|
+
return BigInt(value);
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* `where` with each top-level key value bound as a reselect binds keys (see
|
|
497
|
+
* {@link whereMod.TEXT_COMPARED_TYPES}), or `where` itself when nothing changes.
|
|
498
|
+
*/
|
|
499
|
+
function textSafeKeyWhere(qi, where) {
|
|
500
|
+
let out;
|
|
501
|
+
for (const [key, value] of Object.entries(where)) {
|
|
502
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
503
|
+
if (column === undefined)
|
|
504
|
+
continue;
|
|
505
|
+
const plain = (0, filters_js_1.isPlainRecord)(value) && Object.keys(value).length === 1 && 'equals' in value;
|
|
506
|
+
const raw = plain ? value.equals : value;
|
|
507
|
+
const bound = reselectKeyValue(qi, column, raw);
|
|
508
|
+
if (bound === raw)
|
|
509
|
+
continue;
|
|
510
|
+
out ??= { ...where };
|
|
511
|
+
out[key] = plain ? { equals: bound } : bound;
|
|
512
|
+
}
|
|
513
|
+
return out ?? where;
|
|
143
514
|
}
|
|
144
515
|
/**
|
|
145
516
|
* Build the all-defaults INSERT for a `data` that names no column, via the
|
|
@@ -209,31 +580,181 @@ function buildCreate(qi, args, projection) {
|
|
|
209
580
|
};
|
|
210
581
|
}
|
|
211
582
|
/**
|
|
212
|
-
* Build the `'reselect'` plan for {@link buildCreate}: run the INSERT,
|
|
213
|
-
* `SELECT
|
|
214
|
-
*
|
|
215
|
-
*
|
|
583
|
+
* Build the `'reselect'` plan for {@link buildCreate} (MySQL): run the INSERT,
|
|
584
|
+
* then `SELECT ... WHERE <key> = ?` by a key whose value is known exactly
|
|
585
|
+
* (see {@link createReselectKey}). Returns `undefined` unless the dialect's
|
|
586
|
+
* result strategy is `'reselect'`, so the RETURNING engines pay nothing.
|
|
216
587
|
*/
|
|
217
588
|
function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
|
|
218
589
|
if (qi.dialect.resultStrategy !== 'reselect')
|
|
219
590
|
return undefined;
|
|
591
|
+
const key = createReselectKey(qi, data);
|
|
220
592
|
return async (exec) => {
|
|
221
593
|
const writeResult = await exec(insertSql, insertParams);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
|
|
232
|
-
}
|
|
233
|
-
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
234
|
-
return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
594
|
+
const params = key.values.map((value) => value === INSERT_ID
|
|
595
|
+
? generatedKey(qi, writeResult, key)
|
|
596
|
+
: value === ZERO_INSERT_ID
|
|
597
|
+
? zeroKey(qi, writeResult)
|
|
598
|
+
: value);
|
|
599
|
+
const where = key.columns.map((c, i) => `${qi.q(c)} = ${qi.p(i + 1)}`).join(' AND ');
|
|
600
|
+
const result = await exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${where}`, params);
|
|
601
|
+
assertOneRowByKey(qi, 'create', key.columns, result.rows.length);
|
|
602
|
+
return result;
|
|
235
603
|
};
|
|
236
604
|
}
|
|
605
|
+
/** Stands for the driver's generated insert id in a {@link ReselectKey}. */
|
|
606
|
+
const INSERT_ID = Symbol('turbine.insertId');
|
|
607
|
+
/**
|
|
608
|
+
* Stands for a 0 written to an AUTO_INCREMENT key. MySQL reads 0 there as
|
|
609
|
+
* "assign the next value" (unless the session's sql_mode has
|
|
610
|
+
* NO_AUTO_VALUE_ON_ZERO, which stores 0), so the row is re-read by the value
|
|
611
|
+
* the server reports it stored. Re-reading by 0 found nothing, and the create
|
|
612
|
+
* that had just inserted a row failed as "not found".
|
|
613
|
+
*/
|
|
614
|
+
const ZERO_INSERT_ID = Symbol('turbine.zeroInsertId');
|
|
615
|
+
/**
|
|
616
|
+
* The key that finds the row a `create` inserted, decided before the INSERT is
|
|
617
|
+
* sent. Without RETURNING, MySQL reports only the AUTO_INCREMENT value it
|
|
618
|
+
* assigned (`insertId`, 0 when there is none), so a key the server fills any
|
|
619
|
+
* other way (`DEFAULT (UUID())`) cannot be read back: re-reading by 0 matched
|
|
620
|
+
* every CHAR key MySQL converts to the number 0, other tenants' rows included.
|
|
621
|
+
*
|
|
622
|
+
* In order: the primary key, when `data` supplies every member or the one it
|
|
623
|
+
* omits is server-numbered ({@link isServerNumberedKey}); else the first unique
|
|
624
|
+
* key `data` supplies in full. Anything else is refused (E017) before the
|
|
625
|
+
* INSERT, naming the fix. A NULL never identifies a row, so it counts as absent.
|
|
626
|
+
*/
|
|
627
|
+
function createReselectKey(qi, data) {
|
|
628
|
+
const supplied = new Map();
|
|
629
|
+
for (const [key, value] of writeEntries(qi, data)) {
|
|
630
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
631
|
+
// Same rewrite the INSERT's own params got, then the text-key rule.
|
|
632
|
+
if (column !== undefined && value !== null) {
|
|
633
|
+
supplied.set(column, reselectKeyValue(qi, column, coerceWriteValue(qi, key, value)));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const pk = qi.tableMeta.primaryKey;
|
|
637
|
+
const missing = pk.filter((c) => !supplied.has(c));
|
|
638
|
+
if (pk.length > 0 && (missing.length === 0 || (missing.length === 1 && isServerNumberedKey(qi, missing[0])))) {
|
|
639
|
+
return {
|
|
640
|
+
columns: pk,
|
|
641
|
+
values: pk.map((c) => {
|
|
642
|
+
if (!supplied.has(c))
|
|
643
|
+
return INSERT_ID;
|
|
644
|
+
const value = supplied.get(c);
|
|
645
|
+
return isZeroKeyValue(value) && isServerNumberedKey(qi, c) ? ZERO_INSERT_ID : value;
|
|
646
|
+
}),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
for (const key of (0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta)) {
|
|
650
|
+
if (key.every((c) => supplied.has(c)))
|
|
651
|
+
return { columns: key, values: key.map((c) => supplied.get(c)) };
|
|
652
|
+
}
|
|
653
|
+
const fields = (missing.length > 0 ? missing : pk).map((c) => `"${fieldOf(qi, c)}"`).join(', ');
|
|
654
|
+
throw new errors_js_1.UnsupportedFeatureError(pk.length > 0
|
|
655
|
+
? `create on "${qi.table}" without ${fields} in data`
|
|
656
|
+
: `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 ' +
|
|
657
|
+
'AUTO_INCREMENT column. ' +
|
|
658
|
+
(pk.length > 0
|
|
659
|
+
? `Pass ${fields} in data (for a UUID key, generate it client-side, e.g. crypto.randomUUID()), or make the key AUTO_INCREMENT.`
|
|
660
|
+
: 'Supply every column of a unique key in data, or give the table a primary key.'));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Whether the server numbers this key column itself, so the driver's insert id
|
|
664
|
+
* is its new value. `isGenerated` decides when the metadata records it;
|
|
665
|
+
* metadata that does not (hand-written, or an introspector that leaves it
|
|
666
|
+
* unset) counts an integer column with a default.
|
|
667
|
+
*/
|
|
668
|
+
function isServerNumberedKey(qi, column) {
|
|
669
|
+
const col = qi.tableMeta.columns.find((c) => c.name === column);
|
|
670
|
+
if (!col)
|
|
671
|
+
return false;
|
|
672
|
+
if (col.isGenerated !== undefined)
|
|
673
|
+
return col.isGenerated;
|
|
674
|
+
return col.hasDefault && isIntegerColumnType(col.dialectType ?? col.pgType);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Whether a column's database type is an integer type, in the PostgreSQL,
|
|
678
|
+
* MySQL and code-first spellings. A MySQL display width or sign modifier
|
|
679
|
+
* (`int(11) unsigned`) is ignored, except that `tinyint(1)` is MySQL's
|
|
680
|
+
* BOOLEAN and is not counted.
|
|
681
|
+
*/
|
|
682
|
+
function isIntegerColumnType(dbType) {
|
|
683
|
+
if (!dbType)
|
|
684
|
+
return false;
|
|
685
|
+
const t = dbType.trim().toLowerCase();
|
|
686
|
+
if (t === 'tinyint(1)')
|
|
687
|
+
return false;
|
|
688
|
+
const base = t
|
|
689
|
+
.replace(/\s+(unsigned|signed|zerofill)\b/g, '')
|
|
690
|
+
.replace(/\s*\(\s*\d+\s*\)$/, '')
|
|
691
|
+
.trim();
|
|
692
|
+
return INTEGER_COLUMN_TYPES.has(base);
|
|
693
|
+
}
|
|
694
|
+
const INTEGER_COLUMN_TYPES = new Set([
|
|
695
|
+
'tinyint',
|
|
696
|
+
'smallint',
|
|
697
|
+
'mediumint',
|
|
698
|
+
'int',
|
|
699
|
+
'integer',
|
|
700
|
+
'bigint',
|
|
701
|
+
'int2',
|
|
702
|
+
'int4',
|
|
703
|
+
'int8',
|
|
704
|
+
'serial',
|
|
705
|
+
'bigserial',
|
|
706
|
+
'smallserial',
|
|
707
|
+
]);
|
|
708
|
+
function isZeroKeyValue(value) {
|
|
709
|
+
return value === 0 || value === 0n || (typeof value === 'string' && /^0+$/.test(value));
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* The stored value of a key written as 0 (see {@link ZERO_INSERT_ID}): the
|
|
713
|
+
* insert id the engine reports for the AUTO_INCREMENT column, which is the
|
|
714
|
+
* assigned value, or 0 when the server stored the 0 as written.
|
|
715
|
+
*/
|
|
716
|
+
function zeroKey(qi, result) {
|
|
717
|
+
const id = qi.mutationInsertId(result);
|
|
718
|
+
const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
|
|
719
|
+
(typeof id === 'bigint' && id > 0n) ||
|
|
720
|
+
(typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
|
|
721
|
+
return positive ? id : 0;
|
|
722
|
+
}
|
|
723
|
+
/** The insert id of `result` as a key value, or E017 when the engine reported none. */
|
|
724
|
+
function generatedKey(qi, result, key) {
|
|
725
|
+
const id = qi.mutationInsertId(result);
|
|
726
|
+
const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
|
|
727
|
+
(typeof id === 'bigint' && id > 0n) ||
|
|
728
|
+
(typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
|
|
729
|
+
if (positive)
|
|
730
|
+
return id;
|
|
731
|
+
const column = key.columns[key.values.indexOf(INSERT_ID)];
|
|
732
|
+
throw new errors_js_1.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 ` +
|
|
733
|
+
`and the insert is rolled back. Pass "${fieldOf(qi, column)}" in data, or make it AUTO_INCREMENT.`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* A key value as a reselect binds it: a number or bigint for a text-class
|
|
737
|
+
* column ({@link whereMod.TEXT_COMPARED_TYPES}) is sent as the text the INSERT
|
|
738
|
+
* stored, since MySQL compares `'0zz' = 0` as true. Converted on every engine,
|
|
739
|
+
* not only MySQL, because one caller (the identity read) is not MySQL-only.
|
|
740
|
+
*/
|
|
741
|
+
function reselectKeyValue(qi, column, value) {
|
|
742
|
+
if (typeof value !== 'number' && typeof value !== 'bigint')
|
|
743
|
+
return value;
|
|
744
|
+
const type = qi.columnPgTypeMap.get(column);
|
|
745
|
+
return type !== undefined && whereMod.TEXT_COMPARED_TYPES.has(type.toLowerCase()) ? String(value) : value;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* A re-read by a unique key finds one row. A second one means the engine
|
|
749
|
+
* matched the key loosely, so which row is the written one is unknown: refused
|
|
750
|
+
* (E017), which rolls the write back.
|
|
751
|
+
*/
|
|
752
|
+
function assertOneRowByKey(qi, operation, columns, rows) {
|
|
753
|
+
if (rows <= 1)
|
|
754
|
+
return;
|
|
755
|
+
throw new errors_js_1.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 ` +
|
|
756
|
+
'written one cannot be told apart and the write is rolled back. Pass each key value with its column type.');
|
|
757
|
+
}
|
|
237
758
|
/**
|
|
238
759
|
* The `[key, value]` pairs a write's `data` object actually names, in the
|
|
239
760
|
* table's own column order.
|
|
@@ -321,14 +842,81 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
|
321
842
|
function quoteList(fields) {
|
|
322
843
|
return fields.map((f) => `"${f}"`).join(', ');
|
|
323
844
|
}
|
|
324
|
-
|
|
845
|
+
/**
|
|
846
|
+
* Refuse a statement that carries more bind parameters than the engine takes.
|
|
847
|
+
* Only the row-major `VALUES` form can get there (PostgreSQL's UNNEST form
|
|
848
|
+
* binds one array per column), and past the limit the failure is a raw driver
|
|
849
|
+
* or protocol error. Executing in chunks would change what one `createMany`
|
|
850
|
+
* is (one statement, one RETURNING set), so the caller chooses the chunking.
|
|
851
|
+
*/
|
|
852
|
+
function assertWithinBindLimit(qi, paramCount, rowCount, columnCount) {
|
|
853
|
+
const limit = (0, dialect_js_1.maxBindParameters)(qi.dialect);
|
|
854
|
+
if (limit === undefined || paramCount <= limit)
|
|
855
|
+
return;
|
|
856
|
+
const perChunk = Math.max(1, Math.floor(limit / Math.max(1, columnCount)));
|
|
857
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": ${rowCount} rows of ${columnCount} columns need ${paramCount} bind parameters, ` +
|
|
858
|
+
`over the ${qi.dialect.name} limit of ${limit} per statement. ` +
|
|
859
|
+
`Split the rows into chunks of at most ${perChunk} and call createMany once per chunk ` +
|
|
860
|
+
'(inside one `$transaction` if they must succeed or fail together).');
|
|
861
|
+
}
|
|
862
|
+
function buildCreateMany(qi, args, overBindLimit = 'refuse', projection) {
|
|
863
|
+
const prepared = prepareCreateMany(qi, args, projection);
|
|
864
|
+
if ('deferred' in prepared)
|
|
865
|
+
return prepared.deferred;
|
|
866
|
+
return emitCreateMany(qi, prepared, prepared.rowValues, overBindLimit, true);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* `createMany()` as the statements it runs: one when the rows fit one
|
|
870
|
+
* statement, else consecutive chunks that each do. The whole call is validated
|
|
871
|
+
* once, before any chunk is built, so a refusal (a duplicate conflict key in
|
|
872
|
+
* the last chunk included) sends nothing, and the chunks are sized before any
|
|
873
|
+
* statement is built, so no engine's own per-statement check refuses one.
|
|
874
|
+
*
|
|
875
|
+
* Built eagerly and synchronously: the chunks read state the call set on the
|
|
876
|
+
* builder (`skipGlobalFilters`), which a concurrent call could change across
|
|
877
|
+
* an await.
|
|
878
|
+
*/
|
|
879
|
+
function planCreateMany(qi, args, projection) {
|
|
880
|
+
const prepared = prepareCreateMany(qi, args, projection);
|
|
881
|
+
if ('deferred' in prepared)
|
|
882
|
+
return { chunks: [prepared.deferred], rowsPerChunk: args.data.length };
|
|
883
|
+
const rows = prepared.rowValues;
|
|
884
|
+
const size = rowsPerStatement(qi, prepared);
|
|
885
|
+
if (rows.length <= size) {
|
|
886
|
+
return { chunks: [emitCreateMany(qi, prepared, rows, 'allow', true)], rowsPerChunk: size };
|
|
887
|
+
}
|
|
888
|
+
if (prepared.conflict && !prepared.identity) {
|
|
889
|
+
throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": ${rows.length} rows take more than one statement here (at most ` +
|
|
890
|
+
`${size} per statement), and the rows each statement writes cannot be identified (onConflict.update assigns ` +
|
|
891
|
+
"the key, or the only key is a PII column), so two rows equal under the key's comparison rules could merge " +
|
|
892
|
+
`across statements unnoticed. Pass at most ${size} rows per call.`);
|
|
893
|
+
}
|
|
894
|
+
const chunks = [];
|
|
895
|
+
for (let i = 0; i < rows.length; i += size) {
|
|
896
|
+
chunks.push(emitCreateMany(qi, prepared, rows.slice(i, i + size), 'refuse', i === 0));
|
|
897
|
+
}
|
|
898
|
+
return { chunks, rowsPerChunk: size };
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Everything `createMany` checks about the call, over every row, or the whole
|
|
902
|
+
* answer when no row-values statement is needed (no rows, or rows of pure
|
|
903
|
+
* defaults).
|
|
904
|
+
*/
|
|
905
|
+
function prepareCreateMany(qi, args, projection) {
|
|
325
906
|
const qt = qi.q(qi.table);
|
|
907
|
+
// Validated on every call (UNSAFE only); it acts on onConflict's update.
|
|
908
|
+
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
909
|
+
// The data-independent half of onConflict is checked before the empty-data
|
|
910
|
+
// shortcut, so a call is never valid with no rows and invalid with some.
|
|
911
|
+
const conflict = args.onConflict === undefined ? null : resolveConflictPlan(qi, args);
|
|
326
912
|
if (args.data.length === 0) {
|
|
327
913
|
return {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
914
|
+
deferred: {
|
|
915
|
+
sql: `SELECT * FROM ${qt} WHERE false`,
|
|
916
|
+
params: [],
|
|
917
|
+
transform: () => [],
|
|
918
|
+
tag: `${qi.table}.createMany`,
|
|
919
|
+
},
|
|
332
920
|
};
|
|
333
921
|
}
|
|
334
922
|
assertWritable(qi, 'createMany');
|
|
@@ -337,22 +925,35 @@ function buildCreateMany(qi, args) {
|
|
|
337
925
|
}
|
|
338
926
|
const keys = definedKeys(qi, args.data[0]);
|
|
339
927
|
assertUniformCreateManyRows(qi, args.data, keys);
|
|
928
|
+
const columns = keys.map((k) => qi.toColumn(k));
|
|
929
|
+
if (conflict)
|
|
930
|
+
assertConflictTargetSupplied(qi, conflict, columns);
|
|
340
931
|
// No column named by the first row: every row is pure defaults (the bulk
|
|
341
932
|
// counterpart of `create({ data: {} })`, see buildDefaultValuesInsert). The
|
|
342
933
|
// uniformity check above has already established that EVERY row is empty.
|
|
343
934
|
if (keys.length === 0) {
|
|
344
935
|
return {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
936
|
+
deferred: {
|
|
937
|
+
sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates, projection),
|
|
938
|
+
params: [],
|
|
939
|
+
transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
|
|
940
|
+
tag: `${qi.table}.createMany`,
|
|
941
|
+
},
|
|
349
942
|
};
|
|
350
943
|
}
|
|
351
|
-
const columns = keys.map((k) => qi.toColumn(k));
|
|
352
944
|
const rowValues = args.data.map((row) => {
|
|
353
945
|
const record = row;
|
|
354
946
|
return keys.map((key) => coerceWriteValue(qi, key, record[key]));
|
|
355
947
|
});
|
|
948
|
+
let identity = null;
|
|
949
|
+
if (conflict) {
|
|
950
|
+
// Over every row, so a chunked call refuses before its first chunk is sent.
|
|
951
|
+
assertNoDuplicateKeys(qi, conflict, columns, rowValues);
|
|
952
|
+
if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key') {
|
|
953
|
+
assertMysqlBulkUpsert(qi, conflict, columns, rowValues);
|
|
954
|
+
}
|
|
955
|
+
identity = conflictIdentity(qi, conflict, projection);
|
|
956
|
+
}
|
|
356
957
|
// Use actual Postgres types for array casts in the default PostgreSQL dialect.
|
|
357
958
|
// Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
|
|
358
959
|
// type the UNNEST output as text, which Postgres refuses to coerce to the
|
|
@@ -373,21 +974,450 @@ function buildCreateMany(qi, args) {
|
|
|
373
974
|
// directly. Detected from the column's own declared type rather than from
|
|
374
975
|
// the shape of the first row's value, so a row whose array column happens to
|
|
375
976
|
// be null or absent still takes the correct form.
|
|
977
|
+
//
|
|
978
|
+
// json/jsonb columns stay on UNNEST. coerceWriteValue has already turned
|
|
979
|
+
// every JS array bound to one into JSON TEXT, so the transpose has no
|
|
980
|
+
// nested array to flatten (a raw `[1, 2, 3]` there used to become three
|
|
981
|
+
// rows), and every other cell is a string, object or null that the driver
|
|
982
|
+
// serializes as one array element.
|
|
376
983
|
const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
984
|
+
return {
|
|
985
|
+
conflict,
|
|
986
|
+
identity,
|
|
987
|
+
columns,
|
|
988
|
+
insert: {
|
|
989
|
+
table: qt,
|
|
990
|
+
columns: quotedColumns,
|
|
991
|
+
columnArrayTypes: typeCasts,
|
|
992
|
+
skipDuplicates: args.skipDuplicates,
|
|
993
|
+
returning: writeReturningColumns(qi, identity?.selection ?? projection),
|
|
994
|
+
requireRowValues: hasArrayColumn,
|
|
995
|
+
},
|
|
380
996
|
rowValues,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
997
|
+
projection,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* One `createMany` statement over `rowValues` (all of a prepared call's rows,
|
|
1002
|
+
* or a chunk of them). `first` marks the call's first statement, where an
|
|
1003
|
+
* onConflict call starts counting the rows it wrote.
|
|
1004
|
+
*/
|
|
1005
|
+
function emitCreateMany(qi, prepared, rowValues, overBindLimit, first) {
|
|
1006
|
+
const { conflict, columns, projection } = prepared;
|
|
1007
|
+
const insert = { ...prepared.insert, rowValues };
|
|
1008
|
+
if (conflict) {
|
|
1009
|
+
return buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, prepared.identity, first);
|
|
1010
|
+
}
|
|
1011
|
+
const built = qi.dialect.buildBulkInsertStatement(insert);
|
|
1012
|
+
if (overBindLimit === 'refuse')
|
|
1013
|
+
assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
|
|
1014
|
+
return {
|
|
1015
|
+
sql: built.sql,
|
|
1016
|
+
params: built.params,
|
|
1017
|
+
transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
|
|
1018
|
+
tag: `${qi.table}.createMany`,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Rows one `createMany` statement may carry: the most that keep its bind
|
|
1023
|
+
* parameters within the engine's limit and, on SQL Server, its rows within
|
|
1024
|
+
* the 1000 a plain `INSERT ... VALUES` accepts. The parameter cost is measured
|
|
1025
|
+
* from one-row and two-row builds, which separates the per-row parameters from
|
|
1026
|
+
* the ones bound once per statement (an onConflict update's values); the
|
|
1027
|
+
* PostgreSQL UNNEST form binds one array per column, so its rows never run out.
|
|
1028
|
+
*/
|
|
1029
|
+
function rowsPerStatement(qi, prepared) {
|
|
1030
|
+
// An onConflict statement (SQL Server's MERGE) reads its rows as a derived
|
|
1031
|
+
// table, which the plain INSERT ... VALUES row cap does not bind.
|
|
1032
|
+
const cap = prepared.conflict ? Infinity : qi.dialect.capabilities.maxInsertRows;
|
|
1033
|
+
const limit = (0, dialect_js_1.maxBindParameters)(qi.dialect);
|
|
1034
|
+
const rows = prepared.rowValues;
|
|
1035
|
+
if (limit === undefined || rows.length < 2)
|
|
1036
|
+
return cap;
|
|
1037
|
+
const one = emitCreateMany(qi, prepared, rows.slice(0, 1), 'allow', true).params.length;
|
|
1038
|
+
const perRow = emitCreateMany(qi, prepared, rows.slice(0, 2), 'allow', true).params.length - one;
|
|
1039
|
+
if (perRow <= 0)
|
|
1040
|
+
return cap;
|
|
1041
|
+
const fixed = Math.max(0, one - perRow);
|
|
1042
|
+
return Math.max(1, Math.min(cap, Math.floor((limit - fixed) / perRow)));
|
|
1043
|
+
}
|
|
1044
|
+
const ON_CONFLICT_KEYS = new Set(['target', 'update', 'where', 'ignore']);
|
|
1045
|
+
/**
|
|
1046
|
+
* Validate the data-independent half of `onConflict`: the engine supports it,
|
|
1047
|
+
* its shape, its target names exactly one unique key, and its update and
|
|
1048
|
+
* where are usable. Every refusal is E003 except an engine gap (E017).
|
|
1049
|
+
*/
|
|
1050
|
+
function resolveConflictPlan(qi, args) {
|
|
1051
|
+
const table = qi.table;
|
|
1052
|
+
if (!qi.dialect.supportsBulkUpsert || typeof qi.dialect.buildBulkUpsertStatement !== 'function') {
|
|
1053
|
+
throw new errors_js_1.UnsupportedFeatureError('createMany onConflict', qi.dialect.name, 'This engine has no multi-row upsert statement. Call upsert() per row inside $transaction.');
|
|
1054
|
+
}
|
|
1055
|
+
if (args.skipDuplicates) {
|
|
1056
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict and skipDuplicates cannot be combined. skipDuplicates skips a ` +
|
|
1057
|
+
'conflict on ANY unique key, onConflict names one; to skip conflicts on that key, use ' +
|
|
1058
|
+
'onConflict: { target, ignore: true }.');
|
|
1059
|
+
}
|
|
1060
|
+
const oc = args.onConflict;
|
|
1061
|
+
if (!(0, filters_js_1.isPlainRecord)(oc)) {
|
|
1062
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict must be { target, update } or { target, ignore: true }.`);
|
|
1063
|
+
}
|
|
1064
|
+
const unknownKey = Object.keys(oc).find((k) => !ON_CONFLICT_KEYS.has(k) && oc[k] !== undefined);
|
|
1065
|
+
if (unknownKey !== undefined) {
|
|
1066
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict has no option "${unknownKey}". It takes target, update, where and ignore.`);
|
|
1067
|
+
}
|
|
1068
|
+
const target = resolveConflictTarget(qi, oc.target);
|
|
1069
|
+
if (oc.ignore !== undefined && oc.ignore !== true) {
|
|
1070
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.ignore takes \`true\` only.`);
|
|
1071
|
+
}
|
|
1072
|
+
if (oc.ignore === true) {
|
|
1073
|
+
if (oc.update !== undefined || oc.where !== undefined) {
|
|
1074
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict takes either ignore: true or update (with an optional where), not both.`);
|
|
1075
|
+
}
|
|
1076
|
+
return { target, update: null, where: null };
|
|
1077
|
+
}
|
|
1078
|
+
return { target, update: resolveConflictUpdate(qi, oc.update), where: resolveConflictWhere(qi, oc.where) };
|
|
1079
|
+
}
|
|
1080
|
+
function resolveConflictTarget(qi, raw) {
|
|
1081
|
+
const table = qi.table;
|
|
1082
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((f) => typeof f !== 'string')) {
|
|
1083
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target must be a non-empty array of field names (the primary key or ` +
|
|
1084
|
+
`one unique key), e.g. target: ['email']. Valid targets: ${describeConflictTargets(qi)}.`);
|
|
1085
|
+
}
|
|
1086
|
+
const columns = [];
|
|
1087
|
+
for (const field of raw) {
|
|
1088
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, field);
|
|
1089
|
+
if (column === undefined) {
|
|
1090
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target names "${field}", which is not a column. ` +
|
|
1091
|
+
`Valid targets: ${describeConflictTargets(qi)}.`);
|
|
1092
|
+
}
|
|
1093
|
+
if (columns.includes(column)) {
|
|
1094
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target names "${field}" twice.`);
|
|
1095
|
+
}
|
|
1096
|
+
columns.push(column);
|
|
1097
|
+
}
|
|
1098
|
+
const wanted = new Set(columns);
|
|
1099
|
+
const sameSet = (set) => set.length === wanted.size && set.every((c) => wanted.has(c));
|
|
1100
|
+
if ((0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta).some(sameSet))
|
|
1101
|
+
return columns;
|
|
1102
|
+
const partial = qi.tableMeta.indexes.some((idx) => idx.unique && idx.partial && sameSet(idx.columns));
|
|
1103
|
+
throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target (${columns.map((c) => fieldOf(qi, c)).join(', ')}) ` +
|
|
1104
|
+
(partial
|
|
1105
|
+
? 'is a PARTIAL unique index, which a conflict clause can only use by restating its predicate. '
|
|
1106
|
+
: 'is not the primary key or a unique key of the table, as the schema metadata knows it. ') +
|
|
1107
|
+
`Valid targets: ${describeConflictTargets(qi)}.`);
|
|
1108
|
+
}
|
|
1109
|
+
function fieldOf(qi, column) {
|
|
1110
|
+
return qi.tableMeta.reverseColumnMap[column] ?? column;
|
|
1111
|
+
}
|
|
1112
|
+
function describeConflictTargets(qi) {
|
|
1113
|
+
const keys = (0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta);
|
|
1114
|
+
if (keys.length === 0)
|
|
1115
|
+
return '(none: the table has no primary key or unique key in its metadata)';
|
|
1116
|
+
return keys.map((cols) => `[${cols.map((c) => `'${fieldOf(qi, c)}'`).join(', ')}]`).join(', ');
|
|
1117
|
+
}
|
|
1118
|
+
/** The conflict-UPDATE's data: the field-list shorthand becomes `{ f: expr.excluded(f) }`. */
|
|
1119
|
+
function resolveConflictUpdate(qi, raw) {
|
|
1120
|
+
const empty = new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update is empty; use onConflict: { target, ignore: true } to skip ` +
|
|
1121
|
+
'conflicting rows.');
|
|
1122
|
+
if (Array.isArray(raw)) {
|
|
1123
|
+
if (raw.length === 0)
|
|
1124
|
+
throw empty;
|
|
1125
|
+
const update = {};
|
|
1126
|
+
for (const field of raw) {
|
|
1127
|
+
if (typeof field !== 'string') {
|
|
1128
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update as a list takes field names, got ${typeof field}.`);
|
|
1129
|
+
}
|
|
1130
|
+
if (Object.hasOwn(update, field)) {
|
|
1131
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update names "${field}" twice.`);
|
|
1132
|
+
}
|
|
1133
|
+
update[field] = expr_js_1.expr.excluded(field);
|
|
1134
|
+
}
|
|
1135
|
+
return update;
|
|
1136
|
+
}
|
|
1137
|
+
if (!(0, filters_js_1.isPlainRecord)(raw)) {
|
|
1138
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update takes a list of field names or a { field: value } object.`);
|
|
1139
|
+
}
|
|
1140
|
+
if (!Object.values(raw).some((v) => v !== undefined))
|
|
1141
|
+
throw empty;
|
|
1142
|
+
assertNoGeneratedColumns(qi, raw, 'createMany');
|
|
1143
|
+
return raw;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* `onConflict.where`, with `undefined` keys pruned as in every `where`. One
|
|
1147
|
+
* whose every key was `undefined` is refused: dropping it would update the
|
|
1148
|
+
* rows it was written to protect.
|
|
1149
|
+
*/
|
|
1150
|
+
function resolveConflictWhere(qi, raw) {
|
|
1151
|
+
if (raw === undefined)
|
|
1152
|
+
return null;
|
|
1153
|
+
if (!(0, filters_js_1.isPlainRecord)(raw)) {
|
|
1154
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.where must be a where object.`);
|
|
1155
|
+
}
|
|
1156
|
+
const pruned = (0, normalize_args_js_1.pruneUndefinedArgs)(qi.schema, qi.table, { where: raw }).where;
|
|
1157
|
+
if (Object.keys(pruned).length === 0) {
|
|
1158
|
+
if (Object.keys(raw).length > 0) {
|
|
1159
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": every key in onConflict.where was undefined, so it would restrict nothing. ` +
|
|
1160
|
+
'Check the values you meant to filter on, or leave where out to update every conflicting row.');
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
return pruned;
|
|
1165
|
+
}
|
|
1166
|
+
/** Every row must supply every target column: a row without its key cannot conflict on it. */
|
|
1167
|
+
function assertConflictTargetSupplied(qi, conflict, columns) {
|
|
1168
|
+
const missing = conflict.target.filter((c) => !columns.includes(c));
|
|
1169
|
+
if (missing.length === 0)
|
|
1170
|
+
return;
|
|
1171
|
+
throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": the target field ${missing.map((c) => `"${fieldOf(qi, c)}"`).join(', ')} ` +
|
|
1172
|
+
'is missing from the rows. Every row must supply every target field; a key the database generates ' +
|
|
1173
|
+
'(an identity or serial id it has not assigned yet) cannot be a conflict target.');
|
|
1174
|
+
}
|
|
1175
|
+
/** A byte-equality token for one coerced key value. */
|
|
1176
|
+
function keyToken(value) {
|
|
1177
|
+
let s;
|
|
1178
|
+
if (value instanceof Date)
|
|
1179
|
+
s = value.toISOString();
|
|
1180
|
+
else if (value instanceof Uint8Array)
|
|
1181
|
+
s = Array.from(value, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
1182
|
+
else
|
|
1183
|
+
s = String(value);
|
|
1184
|
+
return `${s.length}:${s}`;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Refuse two rows of one call that share a conflict key, before any SQL. The
|
|
1188
|
+
* engines disagree on it (PostgreSQL raises 21000, SQL Server's MERGE 8672,
|
|
1189
|
+
* SQLite and MySQL apply the rows in sequence), and chunking at the bind
|
|
1190
|
+
* limit would turn the error into "the second chunk updates the first chunk's
|
|
1191
|
+
* row". Byte-equal on the coerced values, so a case-insensitive collation can
|
|
1192
|
+
* still let two rows through. A row with NULL in the key never conflicts.
|
|
1193
|
+
*/
|
|
1194
|
+
function assertNoDuplicateKeys(qi, conflict, columns, rowValues) {
|
|
1195
|
+
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1196
|
+
const seen = new Map();
|
|
1197
|
+
for (let i = 0; i < rowValues.length; i++) {
|
|
1198
|
+
const row = rowValues[i];
|
|
1199
|
+
const parts = at.map((j) => row[j]);
|
|
1200
|
+
if (parts.some((v) => v === null || v === undefined))
|
|
1201
|
+
continue;
|
|
1202
|
+
const key = parts.map(keyToken).join('|');
|
|
1203
|
+
const first = seen.get(key);
|
|
1204
|
+
if (first !== undefined) {
|
|
1205
|
+
throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": rows ${first} and ${i} share the conflict key ` +
|
|
1206
|
+
`(${conflict.target.map((c) => fieldOf(qi, c)).join(', ')}). One statement cannot insert or update one ` +
|
|
1207
|
+
'key twice; merge those rows first.');
|
|
1208
|
+
}
|
|
1209
|
+
seen.set(key, i);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* The identity for an onConflict call: the conflict target (the key that
|
|
1214
|
+
* collides) or the primary key, preferring one the caller's projection
|
|
1215
|
+
* returns. `null` when the written rows cannot be told
|
|
1216
|
+
* apart: each candidate is assigned by `onConflict.update` (so a row's key can
|
|
1217
|
+
* change between statements) or is PII, which a write never returns.
|
|
1218
|
+
*/
|
|
1219
|
+
function conflictIdentity(qi, conflict, projection) {
|
|
1220
|
+
const assigned = conflict.update ? namedColumns(qi.tableMeta, conflict.update) : new Set();
|
|
1221
|
+
const pii = piiColumns(qi, qi.tableMeta);
|
|
1222
|
+
const usable = [conflict.target, qi.tableMeta.primaryKey].filter((key) => key.length > 0 && key.every((c) => !assigned.has(c) && !pii.has(c)));
|
|
1223
|
+
// One the caller's projection already returns, so the statement is unchanged.
|
|
1224
|
+
const columns = usable.find((key) => !projection || key.every((c) => projection.columns.includes(c))) ?? usable[0];
|
|
1225
|
+
if (!columns)
|
|
1226
|
+
return null;
|
|
1227
|
+
const missing = projection ? columns.filter((c) => !projection.columns.includes(c)) : [];
|
|
1228
|
+
if (missing.length === 0)
|
|
1229
|
+
return { columns, selection: projection, added: [], seen: new Set() };
|
|
1230
|
+
const wanted = new Set([...(projection?.columns ?? []), ...missing]);
|
|
1231
|
+
return {
|
|
1232
|
+
columns,
|
|
1233
|
+
selection: { columns: qi.tableMeta.allColumns.filter((c) => wanted.has(c)) },
|
|
1234
|
+
added: missing.map((c) => fieldOf(qi, c)),
|
|
1235
|
+
seen: new Set(),
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
/** Refuse a table row that one onConflict call wrote twice (see {@link ConflictIdentity}). */
|
|
1239
|
+
function assertEachRowWrittenOnce(qi, conflict, identity, rows, first) {
|
|
1240
|
+
if (first)
|
|
1241
|
+
identity.seen.clear();
|
|
1242
|
+
for (const row of rows) {
|
|
1243
|
+
const values = identity.columns.map((c) => row[c]);
|
|
1244
|
+
// Every engine returns the identity columns (see conflictIdentity); a row
|
|
1245
|
+
// without them cannot be compared, so it is not counted.
|
|
1246
|
+
if (values.some((v) => v === undefined))
|
|
1247
|
+
continue;
|
|
1248
|
+
const token = values.map(keyToken).join('|');
|
|
1249
|
+
if (identity.seen.has(token))
|
|
1250
|
+
throw conflictKeySharedError(qi, conflict.target);
|
|
1251
|
+
identity.seen.add(token);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* The E003 for two rows of one `createMany({ onConflict })` that share a key
|
|
1256
|
+
* only under the column's comparison rules (a case-insensitive collation, a
|
|
1257
|
+
* `citext` column), which the byte-equality check above cannot see.
|
|
1258
|
+
*/
|
|
1259
|
+
function conflictKeySharedError(qi, target, cause) {
|
|
1260
|
+
return new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": two rows in this call share the conflict key ` +
|
|
1261
|
+
`(${target.map((c) => fieldOf(qi, c)).join(', ')}) under the column's comparison rules (a case-insensitive ` +
|
|
1262
|
+
'collation, for example), so the call would insert or update one row twice. Merge those rows first.', cause === undefined ? undefined : { cause });
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* `err` from running a `createMany({ onConflict })`, with the engine's
|
|
1266
|
+
* "one statement touched one row twice" errors (PostgreSQL 21000, SQL Server
|
|
1267
|
+
* 8672) turned into the E003 that names the cause. Anything else is returned
|
|
1268
|
+
* as it was.
|
|
1269
|
+
*/
|
|
1270
|
+
function explainConflictKeyCollision(qi, args, err) {
|
|
1271
|
+
if (args.onConflict === undefined || !touchedRowTwice(err))
|
|
1272
|
+
return err;
|
|
1273
|
+
const raw = args.onConflict.target;
|
|
1274
|
+
const target = Array.isArray(raw)
|
|
1275
|
+
? raw.map((f) => (typeof f === 'string' ? ((0, utils_js_1.resolveColumnName)(qi.tableMeta, f) ?? f) : String(f)))
|
|
1276
|
+
: [];
|
|
1277
|
+
return conflictKeySharedError(qi, target, err);
|
|
1278
|
+
}
|
|
1279
|
+
/** PostgreSQL 21000 or SQL Server 8672 on `err` or a cause under it (Turbine wraps or scrubs driver errors). */
|
|
1280
|
+
function touchedRowTwice(err) {
|
|
1281
|
+
let e = err;
|
|
1282
|
+
for (let depth = 0; depth < 4 && e && typeof e === 'object'; depth++) {
|
|
1283
|
+
if (e.code === '21000' || e.sqlstate === '21000' || e.number === 8672)
|
|
1284
|
+
return true;
|
|
1285
|
+
e = e.cause;
|
|
1286
|
+
}
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* MySQL's ON DUPLICATE KEY UPDATE has no conflict target: it fires on
|
|
1291
|
+
* WHATEVER unique key a row violates, and with several matching keys it
|
|
1292
|
+
* updates one row. So the target must be the only key a row can violate, and
|
|
1293
|
+
* the shapes that need a predicate slot or a per-row outcome are refused.
|
|
1294
|
+
*/
|
|
1295
|
+
function assertMysqlBulkUpsert(qi, conflict, columns, rowValues) {
|
|
1296
|
+
const fix = (why, hint) => new errors_js_1.UnsupportedFeatureError(`createMany onConflict on "${qi.table}" ${why}`, qi.dialect.name, hint);
|
|
1297
|
+
if (conflict.update === null) {
|
|
1298
|
+
throw fix('with ignore: true', 'MySQL cannot tell the inserted rows from the skipped ones afterwards. Use onConflict: { target, update }.');
|
|
1299
|
+
}
|
|
1300
|
+
if (conflict.where !== null) {
|
|
1301
|
+
throw fix('with a where', 'ON DUPLICATE KEY UPDATE has no predicate slot.');
|
|
1302
|
+
}
|
|
1303
|
+
const gf = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
1304
|
+
if (gf && whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), gf, [])) {
|
|
1305
|
+
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() " +
|
|
1306
|
+
'per row, or pass skipGlobalFilters: UNSAFE.');
|
|
1307
|
+
}
|
|
1308
|
+
// The single-row upsert's rule (otherCollidingUniqueKey), on column presence
|
|
1309
|
+
// so the verdict does not depend on the rows' values.
|
|
1310
|
+
const present = new Set(columns);
|
|
1311
|
+
const other = (0, compound_unique_js_1.otherCollidingUniqueKey)(qi.tableMeta, [conflict.target], (c) => (present.has(c) ? 'value' : 'absent'));
|
|
1312
|
+
if (other) {
|
|
1313
|
+
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 ` +
|
|
1314
|
+
"collide on, and MySQL's ON DUPLICATE KEY UPDATE cannot be limited to one key, so it could update a row the " +
|
|
1315
|
+
'target does not name. Call upsert() per row (it looks each row up first), or leave that key out of data.');
|
|
1316
|
+
}
|
|
1317
|
+
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1318
|
+
if (rowValues.some((row) => at.some((j) => row[j] === null || row[j] === undefined))) {
|
|
1319
|
+
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 ' +
|
|
1320
|
+
'target value.');
|
|
1321
|
+
}
|
|
1322
|
+
const numbered = at.filter((j) => isServerNumberedKey(qi, columns[j]));
|
|
1323
|
+
if (numbered.length > 0 && rowValues.some((row) => numbered.some((j) => isZeroKeyValue(row[j])))) {
|
|
1324
|
+
const field = fieldOf(qi, columns[numbered[0]]);
|
|
1325
|
+
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 ' +
|
|
1326
|
+
`by its key. Pass the row's real "${field}", or create() the rows that need a new one.`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/** The SELECT that re-reads a MySQL multi-row upsert's rows by their conflict keys. */
|
|
1330
|
+
function buildKeyReselect(qi, conflict, columns, rowValues, projection) {
|
|
1331
|
+
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1332
|
+
const params = [];
|
|
1333
|
+
const tuples = rowValues.map((row) => {
|
|
1334
|
+
const refs = at.map((j, k) => {
|
|
1335
|
+
params.push(reselectKeyValue(qi, conflict.target[k], row[j]));
|
|
1336
|
+
return qi.p(params.length);
|
|
1337
|
+
});
|
|
1338
|
+
return refs.length === 1 ? refs[0] : `(${refs.join(', ')})`;
|
|
385
1339
|
});
|
|
1340
|
+
const keyCols = conflict.target.map((c) => qi.q(c));
|
|
1341
|
+
const lhs = keyCols.length === 1 ? keyCols[0] : `(${keyCols.join(', ')})`;
|
|
1342
|
+
return {
|
|
1343
|
+
sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${lhs} IN (${tuples.join(', ')})`,
|
|
1344
|
+
params,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, identity, first) {
|
|
1348
|
+
// The duplicate-key and MySQL checks ran over every row in prepareCreateMany.
|
|
1349
|
+
const rowValues = insert.rowValues;
|
|
1350
|
+
// `updatedAt` columns are stamped on the conflict-UPDATE as update() stamps
|
|
1351
|
+
// them, unless the update names the column.
|
|
1352
|
+
const updateData = conflict.update ? applyUpdatedAtColumns(qi, conflict.update) : null;
|
|
1353
|
+
const insertColumns = new Set(columns);
|
|
1354
|
+
const gf = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
1355
|
+
const built = qi.dialect.buildBulkUpsertStatement({
|
|
1356
|
+
...insert,
|
|
1357
|
+
skipDuplicates: false,
|
|
1358
|
+
conflictColumns: conflict.target.map((c) => qi.q(c)),
|
|
1359
|
+
update: updateData
|
|
1360
|
+
? (params, refs) => {
|
|
1361
|
+
const setClauses = buildSetClauses(qi, writeEntries(qi, updateData), params, {
|
|
1362
|
+
operation: 'createMany',
|
|
1363
|
+
target: refs.target,
|
|
1364
|
+
incoming: refs.incoming,
|
|
1365
|
+
insertColumns,
|
|
1366
|
+
});
|
|
1367
|
+
// The caller's predicate and the global filter both read the EXISTING
|
|
1368
|
+
// row, so a row they exclude is neither updated nor returned (and,
|
|
1369
|
+
// its key being taken, not inserted either). Inserts are not filtered,
|
|
1370
|
+
// as create() is not.
|
|
1371
|
+
const predicates = [];
|
|
1372
|
+
for (const where of [conflict.where, gf]) {
|
|
1373
|
+
if (!where)
|
|
1374
|
+
continue;
|
|
1375
|
+
const sql = whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, refs.target, where, params);
|
|
1376
|
+
if (sql)
|
|
1377
|
+
predicates.push(sql);
|
|
1378
|
+
}
|
|
1379
|
+
return {
|
|
1380
|
+
setClauses,
|
|
1381
|
+
where: predicates.length <= 1 ? (predicates[0] ?? null) : predicates.map((p) => `(${p})`).join(' AND '),
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
: null,
|
|
1385
|
+
});
|
|
1386
|
+
if (overBindLimit === 'refuse') {
|
|
1387
|
+
assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
|
|
1388
|
+
}
|
|
386
1389
|
return {
|
|
387
1390
|
sql: built.sql,
|
|
388
1391
|
params: built.params,
|
|
389
|
-
|
|
1392
|
+
// The rows written: inserted or updated. A row `ignore` skipped, or one the
|
|
1393
|
+
// predicate or a global filter excluded, is not in the result, and the
|
|
1394
|
+
// order is the engine's, not the input's.
|
|
1395
|
+
transform: (result) => {
|
|
1396
|
+
if (identity)
|
|
1397
|
+
assertEachRowWrittenOnce(qi, conflict, identity, result.rows, first);
|
|
1398
|
+
const rows = result.rows.map((row) => parseWriteRow(qi, row, projection));
|
|
1399
|
+
if (identity && identity.added.length > 0) {
|
|
1400
|
+
for (const row of rows)
|
|
1401
|
+
for (const field of identity.added)
|
|
1402
|
+
delete row[field];
|
|
1403
|
+
}
|
|
1404
|
+
return rows;
|
|
1405
|
+
},
|
|
390
1406
|
tag: `${qi.table}.createMany`,
|
|
1407
|
+
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
1408
|
+
? async (exec) => {
|
|
1409
|
+
await exec(built.sql, built.params);
|
|
1410
|
+
const sel = buildKeyReselect(qi, conflict, columns, rowValues, identity?.selection ?? projection);
|
|
1411
|
+
const result = await exec(sel.sql, sel.params);
|
|
1412
|
+
// Every row was inserted or updated and byte-equal keys were
|
|
1413
|
+
// refused up front, so fewer rows back means two rows' keys are
|
|
1414
|
+
// equal under the column's collation and MySQL applied the second
|
|
1415
|
+
// as an update of the first. Refused, which rolls the call back.
|
|
1416
|
+
if (result.rows.length < rowValues.length)
|
|
1417
|
+
throw conflictKeySharedError(qi, conflict.target);
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
1420
|
+
: undefined,
|
|
391
1421
|
};
|
|
392
1422
|
}
|
|
393
1423
|
function buildUpdate(qi, args, projection) {
|
|
@@ -453,8 +1483,7 @@ function buildUpdate(qi, args, projection) {
|
|
|
453
1483
|
: `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
|
|
454
1484
|
const params = [];
|
|
455
1485
|
const buildSql = (freshParams) => {
|
|
456
|
-
const
|
|
457
|
-
const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
|
|
1486
|
+
const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams);
|
|
458
1487
|
if (lock) {
|
|
459
1488
|
const versionCol = qi.toSqlColumn(lock.field);
|
|
460
1489
|
setClauses.push(`${versionCol} = ${versionCol} + 1`);
|
|
@@ -523,6 +1552,18 @@ function buildUpdate(qi, args, projection) {
|
|
|
523
1552
|
// Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
|
|
524
1553
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
525
1554
|
? async (exec) => {
|
|
1555
|
+
// Runs in one transaction (executeMutation), savepointed inside a
|
|
1556
|
+
// caller's. When the write may move the row out of `where` (it sets
|
|
1557
|
+
// a column `where` reads, the version column included), the row is
|
|
1558
|
+
// locked and re-read by its key, since `where` no longer finds it.
|
|
1559
|
+
const written = namedColumns(qi.tableMeta, dataObj);
|
|
1560
|
+
if (lock)
|
|
1561
|
+
written.add(qi.toColumn(lock.field));
|
|
1562
|
+
const key = writeMayMoveRow(qi, whereObj, written)
|
|
1563
|
+
? await lockMovedRowKey(qi, exec, whereObj, dataObj, 'update', allowFullTableScan)
|
|
1564
|
+
: undefined;
|
|
1565
|
+
if (key === null)
|
|
1566
|
+
return { rows: [], rowCount: 0 };
|
|
526
1567
|
const writeResult = await exec(sql, params, preparedName);
|
|
527
1568
|
// Optimistic-lock conflict: the version-checked UPDATE matched no
|
|
528
1569
|
// row. The re-fetch below uses `where` WITHOUT the version
|
|
@@ -536,14 +1577,21 @@ function buildUpdate(qi, args, projection) {
|
|
|
536
1577
|
expectedVersion: lock.expected,
|
|
537
1578
|
});
|
|
538
1579
|
}
|
|
539
|
-
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
540
|
-
|
|
1580
|
+
const sel = buildReselectByWhere(qi, key ?? whereObj, projection);
|
|
1581
|
+
const result = await exec(sel.sql, sel.params);
|
|
1582
|
+
// `update` writes one row; more than one here means the engine
|
|
1583
|
+
// matched the where loosely, and the write is rolled back.
|
|
1584
|
+
if (key || !allowFullTableScan) {
|
|
1585
|
+
assertOneRowByKey(qi, 'update', key ? identityColumns(qi) : whereColumns(qi, userWhere), result.rows.length);
|
|
1586
|
+
}
|
|
1587
|
+
return result;
|
|
541
1588
|
}
|
|
542
1589
|
: undefined,
|
|
543
1590
|
};
|
|
544
1591
|
}
|
|
545
1592
|
function buildDelete(qi, args, projection) {
|
|
546
1593
|
assertWritable(qi, 'delete');
|
|
1594
|
+
assertNoOptimisticLock(qi, args, 'delete');
|
|
547
1595
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
548
1596
|
// Prisma compound-unique selector → the column conjunction (before the guard).
|
|
549
1597
|
const userWhere = (0, compound_unique_js_1.expandCompoundUniqueWhere)(qi.tableMeta, args.where);
|
|
@@ -592,21 +1640,72 @@ function buildDelete(qi, args, projection) {
|
|
|
592
1640
|
// by the same where, then run the DELETE, returning the captured row.
|
|
593
1641
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
594
1642
|
? async (exec) => {
|
|
595
|
-
|
|
1643
|
+
// In one transaction (executeMutation): the row is locked while it
|
|
1644
|
+
// is read, and returned only if THIS delete removed it, so of two
|
|
1645
|
+
// concurrent deletes of one row exactly one reports it.
|
|
1646
|
+
const sel = buildReselectByWhere(qi, whereObj, projection, true);
|
|
596
1647
|
const pre = await exec(sel.sql, sel.params);
|
|
597
|
-
|
|
598
|
-
|
|
1648
|
+
// `delete` removes one row; refused before the DELETE when the
|
|
1649
|
+
// engine matched the where loosely.
|
|
1650
|
+
if (!allowFullTableScan)
|
|
1651
|
+
assertOneRowByKey(qi, 'delete', whereColumns(qi, userWhere), pre.rows.length);
|
|
1652
|
+
const deleted = await exec(entry.sql, params, entry.name);
|
|
1653
|
+
return (deleted.rowCount ?? 0) === 0 ? { rows: [], rowCount: 0 } : pre;
|
|
599
1654
|
}
|
|
600
1655
|
: undefined,
|
|
601
1656
|
};
|
|
602
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Refuse `limit` / `take` / `offset` / `skip` on `updateMany` / `deleteMany`.
|
|
1660
|
+
* No engine applies them to a bulk write here, so `deleteMany({ where, limit:
|
|
1661
|
+
* 1000 })` used to touch every matching row. Shared with the PowDB engine.
|
|
1662
|
+
*/
|
|
1663
|
+
function assertNoBulkWritePaging(table, operation, args) {
|
|
1664
|
+
const a = args;
|
|
1665
|
+
const key = ['limit', 'take', 'offset', 'skip'].find((k) => a[k] !== undefined);
|
|
1666
|
+
if (key === undefined)
|
|
1667
|
+
return;
|
|
1668
|
+
throw new errors_js_1.ValidationError(`\`${key}\` is not supported on ${operation} for "${table}": it would apply to every row the \`where\` ` +
|
|
1669
|
+
`matches, not ${key === 'limit' || key === 'take' ? 'only the first ones' : 'skip any'}. To ${operation === 'deleteMany' ? 'delete' : 'update'} a bounded batch, ` +
|
|
1670
|
+
'select the ids first (`findMany({ where, select: { id: true }, orderBy, limit })`), then pass ' +
|
|
1671
|
+
'`where: { id: { in: ids } }`.');
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Refuse `optimisticLock` on a write that does not implement it. It is an
|
|
1675
|
+
* `update` option; on a sibling method it used to be ignored, so a stale
|
|
1676
|
+
* write went through with no error, which is the one outcome the option
|
|
1677
|
+
* exists to prevent. The message names the equivalent for each method.
|
|
1678
|
+
*/
|
|
1679
|
+
function assertNoOptimisticLock(qi, args, operation) {
|
|
1680
|
+
const lock = args.optimisticLock;
|
|
1681
|
+
if (lock === undefined)
|
|
1682
|
+
return;
|
|
1683
|
+
const field = typeof lock?.field === 'string' ? lock.field : 'version';
|
|
1684
|
+
throw new errors_js_1.ValidationError(`\`optimisticLock\` is an \`update\` option and ${operation} on "${qi.table}" does not apply it. ` +
|
|
1685
|
+
OPTIMISTIC_LOCK_ALTERNATIVE[operation](field));
|
|
1686
|
+
}
|
|
1687
|
+
const OPTIMISTIC_LOCK_ALTERNATIVE = {
|
|
1688
|
+
updateMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`) and bump it in \`data\` ` +
|
|
1689
|
+
`(\`${field}: { increment: 1 }\`); the returned count is the number of rows that were still at that version.`,
|
|
1690
|
+
upsert: () => 'Inside one `$transaction`, read the row by `where`, then call `update({ where, data, optimisticLock })` ' +
|
|
1691
|
+
'when it exists or `create` when it does not.',
|
|
1692
|
+
delete: (field) => `Put the expected version in \`where\` beside the key (\`where: { id, ${field}: expected }\`): a row at ` +
|
|
1693
|
+
'another version is not deleted, and the call throws NotFoundError.',
|
|
1694
|
+
deleteMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`); the returned count is the number of rows ` +
|
|
1695
|
+
'that were still at that version.',
|
|
1696
|
+
};
|
|
603
1697
|
function upsertLookupReason(qi, args) {
|
|
1698
|
+
assertNoOptimisticLock(qi, args, 'upsert');
|
|
604
1699
|
const where = (args.where ?? {});
|
|
605
1700
|
const create = (args.create ?? {});
|
|
606
1701
|
if (!(0, compound_unique_js_1.upsertWhereMatchesCreate)(qi.tableMeta, where, create))
|
|
607
1702
|
return 'where-differs';
|
|
608
1703
|
if (writeEntries(qi, (args.update ?? {})).length === 0)
|
|
609
1704
|
return 'empty-update';
|
|
1705
|
+
if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key' &&
|
|
1706
|
+
(0, compound_unique_js_1.upsertCreateCollidingKey)(qi.tableMeta, where, create)) {
|
|
1707
|
+
return 'other-unique-key';
|
|
1708
|
+
}
|
|
610
1709
|
if (!qi.dialect.supportsUpsertUpdateWhere) {
|
|
611
1710
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
612
1711
|
const filter = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
@@ -617,8 +1716,13 @@ function upsertLookupReason(qi, args) {
|
|
|
617
1716
|
return null;
|
|
618
1717
|
}
|
|
619
1718
|
/** The refusal `buildUpsert` raises for each reason, naming the way forward. */
|
|
620
|
-
function upsertStatementRefusal(qi, reason, where) {
|
|
1719
|
+
function upsertStatementRefusal(qi, reason, where, create) {
|
|
621
1720
|
const call = 'Call `upsert()` unbatched: it looks the row up by `where` first.';
|
|
1721
|
+
if (reason === 'other-unique-key') {
|
|
1722
|
+
const key = (0, compound_unique_js_1.upsertCreateCollidingKey)(qi.tableMeta, where, create) ?? [];
|
|
1723
|
+
return new errors_js_1.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. ' +
|
|
1724
|
+
call);
|
|
1725
|
+
}
|
|
622
1726
|
if (reason === 'global-filter') {
|
|
623
1727
|
return new errors_js_1.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\`.`);
|
|
624
1728
|
}
|
|
@@ -631,6 +1735,7 @@ function upsertStatementRefusal(qi, reason, where) {
|
|
|
631
1735
|
}
|
|
632
1736
|
function buildUpsert(qi, args, projection) {
|
|
633
1737
|
assertWritable(qi, 'upsert');
|
|
1738
|
+
assertNoOptimisticLock(qi, args, 'upsert');
|
|
634
1739
|
assertNoGeneratedColumns(qi, args.create, 'upsert');
|
|
635
1740
|
assertNoGeneratedColumns(qi, args.update, 'upsert');
|
|
636
1741
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
@@ -656,8 +1761,9 @@ function buildUpsert(qi, args, projection) {
|
|
|
656
1761
|
// `upsert()` never reaches here with a reason (it looks the row up instead),
|
|
657
1762
|
// so this refuses only a batched upsert, which has no round trip to spare.
|
|
658
1763
|
const lookupReason = upsertLookupReason(qi, args);
|
|
659
|
-
if (lookupReason)
|
|
660
|
-
throw upsertStatementRefusal(qi, lookupReason, upsertWhere);
|
|
1764
|
+
if (lookupReason) {
|
|
1765
|
+
throw upsertStatementRefusal(qi, lookupReason, upsertWhere, args.create);
|
|
1766
|
+
}
|
|
661
1767
|
// Build the INSERT part from create data
|
|
662
1768
|
const createEntries = writeEntries(qi, args.create);
|
|
663
1769
|
const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
|
|
@@ -668,18 +1774,30 @@ function buildUpsert(qi, args, projection) {
|
|
|
668
1774
|
const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
|
|
669
1775
|
const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
|
|
670
1776
|
// The conflict-UPDATE SET goes through the SAME operator-aware compiler as
|
|
671
|
-
// `update()` (
|
|
1777
|
+
// `update()` (buildSetClauses), so `set` / `increment` / `decrement` /
|
|
672
1778
|
// `multiply` / `divide` mean here what they mean there and a misspelled
|
|
673
1779
|
// operator gets the same E003. This branch used to bind each value directly,
|
|
674
1780
|
// which stored an operator OBJECT as the JSON text `{"set":"x"}` in a text
|
|
675
|
-
// column with no error at all.
|
|
1781
|
+
// column with no error at all. buildSetClauses pushes onto `params` as it
|
|
676
1782
|
// goes, so the numbering continues after the create params exactly as the
|
|
677
1783
|
// hand-rolled `paramIdx` did. No SQL cache here (the statement is rebuilt per
|
|
678
1784
|
// call), so the fingerprint / collect mirrors update() keeps are not needed.
|
|
679
|
-
|
|
1785
|
+
// `updatedAt` columns are stamped on the conflict-UPDATE exactly as the
|
|
1786
|
+
// lookup path's `update()` stamps them, so the column does not depend on
|
|
1787
|
+
// which path an upsert takes. (An empty `update` never reaches here: it is
|
|
1788
|
+
// a lookup that leaves the found row untouched, stamp included.)
|
|
1789
|
+
//
|
|
1790
|
+
// `expr.excluded(f)` is `create`'s value for `f` (the incoming row of a
|
|
1791
|
+
// one-row upsert IS `create`), resolved before compiling so this path and the
|
|
1792
|
+
// lookup path's plain `update()` write the same row.
|
|
1793
|
+
const update = (0, expr_js_1.resolveExcludedFromCreate)(qi, args.update, args.create);
|
|
1794
|
+
const stampedUpdate = applyUpdatedAtColumns(qi, update);
|
|
1795
|
+
const updateEntries = writeEntries(qi, stampedUpdate);
|
|
680
1796
|
const params = [...createParams];
|
|
681
|
-
const
|
|
682
|
-
|
|
1797
|
+
const setClauses = buildSetClauses(qi, updateEntries, params, {
|
|
1798
|
+
operation: 'upsert',
|
|
1799
|
+
target: upsertTargetRef(qi),
|
|
1800
|
+
});
|
|
683
1801
|
// Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
|
|
684
1802
|
// upsert never resurrects a soft-deleted row or writes across tenants. Only
|
|
685
1803
|
// on engines whose upsert can carry a predicate (Postgres, SQLite); the gf
|
|
@@ -748,38 +1866,46 @@ function buildUpsert(qi, args, projection) {
|
|
|
748
1866
|
// Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
|
|
749
1867
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
750
1868
|
? async (exec) => {
|
|
1869
|
+
const lookup = (whereMod.mergeGlobalFilter(qi, textSafeKeyWhere(qi, upsertWhere)) ?? {});
|
|
1870
|
+
// An update that sets a column `where` reads moves the row out of
|
|
1871
|
+
// `where`'s reach: lock it and re-read it by its key. An insert
|
|
1872
|
+
// matches `where` (this path requires `where` pinned to `create`),
|
|
1873
|
+
// so a row that is not there yet is re-read by `where`.
|
|
1874
|
+
const key = writeMayMoveRow(qi, lookup, namedColumns(qi.tableMeta, stampedUpdate))
|
|
1875
|
+
? await lockMovedRowKey(qi, exec, lookup, stampedUpdate, 'upsert', false)
|
|
1876
|
+
: undefined;
|
|
751
1877
|
await exec(sql, params);
|
|
752
|
-
const sel = buildReselectByWhere(qi,
|
|
753
|
-
|
|
1878
|
+
const sel = buildReselectByWhere(qi, key ?? lookup, projection);
|
|
1879
|
+
const result = await exec(sel.sql, sel.params);
|
|
1880
|
+
assertOneRowByKey(qi, 'upsert', key ? identityColumns(qi) : whereColumns(qi, upsertWhere), result.rows.length);
|
|
1881
|
+
return result;
|
|
754
1882
|
}
|
|
755
1883
|
: undefined,
|
|
756
1884
|
};
|
|
757
1885
|
}
|
|
758
1886
|
/**
|
|
759
|
-
* The
|
|
1887
|
+
* The rendered reference to an upsert's TARGET row, which every column the
|
|
1888
|
+
* conflict-UPDATE reads goes through (an arithmetic operator's column, an
|
|
1889
|
+
* expression's `col()`, a case predicate).
|
|
760
1890
|
*
|
|
761
1891
|
* `INSERT ... ON CONFLICT DO UPDATE SET n = n + 1` is 42702 on PostgreSQL
|
|
762
1892
|
* because `n` could be the target row's or `excluded`'s, so the reference is
|
|
763
1893
|
* qualified with the table name on every engine whose upsert statement names
|
|
764
1894
|
* the table directly (PostgreSQL, SQLite, MySQL all accept `"t"."n"` there).
|
|
765
|
-
* SQL Server
|
|
766
|
-
*
|
|
767
|
-
* and the alias belongs to that dialect's own `buildUpsertStatement` rather
|
|
768
|
-
* than to this module. An arithmetic operator in an mssql upsert may therefore
|
|
769
|
-
* still be refused by the server as an ambiguous column; that shape has never
|
|
770
|
-
* worked (it previously bound the operator OBJECT as the value), and the sound
|
|
771
|
-
* fix is for the dialect to publish its target alias, which is a change to
|
|
772
|
-
* `dialect.ts`. It fails loudly either way, so no wrong answer is possible.
|
|
1895
|
+
* SQL Server's `MERGE` aliases its target, so the table name does not resolve
|
|
1896
|
+
* there and the dialect publishes the alias ({@link Dialect.upsertTargetAlias}).
|
|
773
1897
|
* `set` and literal values reference no column and are unaffected everywhere.
|
|
774
1898
|
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
1899
|
+
* Read from a property only SQL Server sets, never inferred from a hook's
|
|
1900
|
+
* presence: every engine dialect spreads `postgresDialect`.
|
|
777
1901
|
*/
|
|
778
|
-
function
|
|
779
|
-
return qi.dialect.
|
|
1902
|
+
function upsertTargetRef(qi) {
|
|
1903
|
+
return qi.dialect.upsertTargetAlias ?? qi.q(qi.table);
|
|
780
1904
|
}
|
|
781
1905
|
function buildUpdateMany(qi, args) {
|
|
782
1906
|
assertWritable(qi, 'updateMany');
|
|
1907
|
+
assertNoBulkWritePaging(qi.table, 'updateMany', args);
|
|
1908
|
+
assertNoOptimisticLock(qi, args, 'updateMany');
|
|
783
1909
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
784
1910
|
const dataObj = applyUpdatedAtColumns(qi, args.data);
|
|
785
1911
|
assertNoGeneratedColumns(qi, dataObj, 'updateMany');
|
|
@@ -802,8 +1928,7 @@ function buildUpdateMany(qi, args) {
|
|
|
802
1928
|
const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
803
1929
|
const params = [];
|
|
804
1930
|
const buildSql = (freshParams) => {
|
|
805
|
-
const
|
|
806
|
-
const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
|
|
1931
|
+
const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams, { operation: 'updateMany' });
|
|
807
1932
|
const whereClause = whereMod.buildWhereClause(qi, whereObj, freshParams);
|
|
808
1933
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
809
1934
|
return `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}`;
|
|
@@ -822,6 +1947,8 @@ function buildUpdateMany(qi, args) {
|
|
|
822
1947
|
}
|
|
823
1948
|
function buildDeleteMany(qi, args) {
|
|
824
1949
|
assertWritable(qi, 'deleteMany');
|
|
1950
|
+
assertNoBulkWritePaging(qi.table, 'deleteMany', args);
|
|
1951
|
+
assertNoOptimisticLock(qi, args, 'deleteMany');
|
|
825
1952
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
826
1953
|
whereMod.assertMutationHasPredicate(qi, 'deleteMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'));
|
|
827
1954
|
const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
|
|
@@ -847,8 +1974,8 @@ function buildDeleteMany(qi, args) {
|
|
|
847
1974
|
/**
|
|
848
1975
|
* The snake_case names of a table's PII-tagged (`defineSchema` `pii: true`)
|
|
849
1976
|
* columns. PII columns are excluded from default projections (findMany /
|
|
850
|
-
* findUnique / relation subqueries / batched loads) unless
|
|
851
|
-
*
|
|
1977
|
+
* findUnique / relation subqueries / batched loads) unless a read passes
|
|
1978
|
+
* `includePii: UNSAFE`; naming one in `select` without it is E003. Returns an
|
|
852
1979
|
* empty set for any table with no PII column, so untagged schemas keep their
|
|
853
1980
|
* byte-identical SQL.
|
|
854
1981
|
*/
|
|
@@ -881,8 +2008,8 @@ function piiColumns(_qi, meta) {
|
|
|
881
2008
|
* The camelCase field names of a table's PII-tagged columns: the read-side
|
|
882
2009
|
* counterpart of {@link piiColumns} applied to already-parsed entities.
|
|
883
2010
|
* Used to strip PII from a write's RETURNING/reselect row (writes accept no
|
|
884
|
-
* `includePii
|
|
885
|
-
*
|
|
2011
|
+
* `includePii`, and a PII column named in a write's `select` is E003, so their
|
|
2012
|
+
* returned row never carries one; you may still write PII fields freely).
|
|
886
2013
|
*/
|
|
887
2014
|
function piiFields(_qi, meta) {
|
|
888
2015
|
const out = [];
|
|
@@ -916,8 +2043,9 @@ function writeProjectionCacheSegment(projection) {
|
|
|
916
2043
|
* column so the PII values never leave the database on a write. A PII-tagged
|
|
917
2044
|
* PRIMARY KEY column is kept in the projection regardless (the returned row
|
|
918
2045
|
* must stay addressable): tag sensitive data, not keys, a PII PK is
|
|
919
|
-
* documented out of scope for stripping. Writes accept no `
|
|
920
|
-
*
|
|
2046
|
+
* documented out of scope for stripping. Writes accept no `includePii` and
|
|
2047
|
+
* their `select` cannot name a PII column, so this is the whole write-return
|
|
2048
|
+
* policy at the SQL level;
|
|
921
2049
|
* {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
|
|
922
2050
|
* SQL already excludes the columns). Derived purely from static per-table
|
|
923
2051
|
* schema metadata, so the write SQL cache needs no extra key segment.
|
|
@@ -1004,8 +2132,8 @@ function writeReselectSelection(qi, projection) {
|
|
|
1004
2132
|
*/
|
|
1005
2133
|
function parseWriteRow(qi, row, projection) {
|
|
1006
2134
|
const parsed = qi.parseRow(row, qi.table);
|
|
1007
|
-
// A
|
|
1008
|
-
// so
|
|
2135
|
+
// A write's projection never names a PII column (E003, see WriteProjection),
|
|
2136
|
+
// so for PII this strip spares nothing; it stays as defense in depth.
|
|
1009
2137
|
const pii = piiColumns(qi, qi.tableMeta);
|
|
1010
2138
|
if (pii.size === 0)
|
|
1011
2139
|
return parsed;
|
|
@@ -1046,85 +2174,260 @@ function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
1046
2174
|
}
|
|
1047
2175
|
}
|
|
1048
2176
|
}
|
|
2177
|
+
/** The SQL operator each arithmetic update operator compiles to. */
|
|
2178
|
+
const ARITHMETIC_SQL = { increment: '+', decrement: '-', multiply: '*', divide: '/' };
|
|
2179
|
+
/**
|
|
2180
|
+
* The SQL operator for one arithmetic update operator on one column.
|
|
2181
|
+
*
|
|
2182
|
+
* `divide` on an INTEGER column is the engine's truncating integer division
|
|
2183
|
+
* (`integerDivision`): `DIV` on MySQL, whose `/` is decimal division, so
|
|
2184
|
+
* `75 / 2` is 37.5 and the assignment back to an INT rounds it to 38, where
|
|
2185
|
+
* PostgreSQL's integer division truncates to 37. `DIV` truncates toward zero,
|
|
2186
|
+
* as PostgreSQL does. Decided by the COLUMN TYPE alone, never by
|
|
2187
|
+
* the bound divisor: the SQL text must not depend on a value, or the template
|
|
2188
|
+
* cache would serve one shape to the other.
|
|
2189
|
+
*/
|
|
2190
|
+
function arithmeticSql(qi, op, column) {
|
|
2191
|
+
if (op === 'divide' && isIntegerColumnType(qi.columnPgTypeMap.get(column))) {
|
|
2192
|
+
return qi.dialect.capabilities.integerDivision;
|
|
2193
|
+
}
|
|
2194
|
+
return ARITHMETIC_SQL[op];
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* The atomic operator a SET value names, or `null` when it is a plain value.
|
|
2198
|
+
* THE one rule, read by the SET build, its fingerprint and its cache-hit param
|
|
2199
|
+
* collector, so the three cannot disagree about whether a value is an
|
|
2200
|
+
* operator.
|
|
2201
|
+
*
|
|
2202
|
+
* A branded expression or `col()` reference is checked FIRST and answers
|
|
2203
|
+
* `'expr'`: it is a frozen plain object too, and must never reach the
|
|
2204
|
+
* operator test below. An operator is a plain object with EXACTLY one key from
|
|
2205
|
+
* {@link UPDATE_OPERATOR_KEYS}. On a json/jsonb column only `set` is an
|
|
2206
|
+
* operator: an object there IS the JSON value, so `{ increment: 1 }` is the
|
|
2207
|
+
* document `{"increment":1}`, and `{ set: doc }` is the way to write a
|
|
2208
|
+
* document that is itself shaped like `{ set: ... }`.
|
|
2209
|
+
*/
|
|
2210
|
+
function setOperatorOf(qi, key, value) {
|
|
2211
|
+
if ((0, expr_js_1.isSqlExpr)(value) || (0, column_ref_js_1.isColumnRef)(value))
|
|
2212
|
+
return 'expr';
|
|
2213
|
+
if (!(0, filters_js_1.isUnmatchedPlainObject)(value))
|
|
2214
|
+
return null;
|
|
2215
|
+
const keys = Object.keys(value);
|
|
2216
|
+
if (keys.length !== 1 || !filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0]))
|
|
2217
|
+
return null;
|
|
2218
|
+
const op = keys[0];
|
|
2219
|
+
if (op !== 'set' && isJsonWriteColumn(qi, key))
|
|
2220
|
+
return null;
|
|
2221
|
+
return op;
|
|
2222
|
+
}
|
|
2223
|
+
/** Whether `key` names a json/jsonb column (non-throwing; unknown keys are not). */
|
|
2224
|
+
function isJsonWriteColumn(qi, key) {
|
|
2225
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
2226
|
+
const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
|
|
2227
|
+
return dbType !== undefined && whereMod.isJsonColumnType(qi, dbType);
|
|
2228
|
+
}
|
|
2229
|
+
const UPDATE_SET = { operation: 'update' };
|
|
2230
|
+
const NO_READS = new Set();
|
|
1049
2231
|
/**
|
|
1050
|
-
*
|
|
2232
|
+
* Compile a SET list: one clause per entry, in the order given (the canonical
|
|
2233
|
+
* column order of {@link writeEntries}), pushing params as it goes so later
|
|
2234
|
+
* clauses (WHERE, version check) continue the numbering.
|
|
1051
2235
|
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
|
|
1058
|
-
|
|
2236
|
+
* On MySQL the clause STRINGS are then reordered (see
|
|
2237
|
+
* {@link orderForLeftToRightSet}). Params are not: MySQL binds by name
|
|
2238
|
+
* (`:pN`), so a clause keeps its placeholders wherever it lands, and the
|
|
2239
|
+
* cache-hit collector ({@link collectSetParams}) keeps pushing in canonical
|
|
2240
|
+
* order.
|
|
2241
|
+
*/
|
|
2242
|
+
function buildSetClauses(qi, entries, params, ctx = UPDATE_SET) {
|
|
2243
|
+
const compiled = entries.map(([k, v]) => compileSetEntry(qi, k, v, params, ctx));
|
|
2244
|
+
return qi.dialect.capabilities.setClauseOrder === 'left-to-right'
|
|
2245
|
+
? orderForLeftToRightSet(qi, compiled)
|
|
2246
|
+
: compiled.map((c) => c.sql);
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Build a single SET clause entry.
|
|
2250
|
+
*
|
|
2251
|
+
* Supports plain values, atomic operator objects ({ set, increment,
|
|
2252
|
+
* decrement, multiply, divide }) and expressions (`expr.*`, `col()`), as
|
|
2253
|
+
* decided by {@link setOperatorOf}.
|
|
1059
2254
|
*
|
|
1060
2255
|
* Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
|
|
1061
2256
|
* pushes any required params onto the shared params array so that WHERE
|
|
1062
2257
|
* clause numbering continues correctly afterward.
|
|
1063
2258
|
*
|
|
1064
|
-
* `
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
2259
|
+
* `ctx.target` qualifies the column REFERENCES the right-hand side reads
|
|
2260
|
+
* (`"view_count" = "users"."view_count" + $n`): absent for a plain `UPDATE`,
|
|
2261
|
+
* the table name in an upsert, because inside `ON CONFLICT ... DO UPDATE SET`
|
|
2262
|
+
* the target table and `excluded` are BOTH in scope and a bare reference is
|
|
2263
|
+
* 42702 on PostgreSQL. The assignment target on the left stays bare on every
|
|
2264
|
+
* engine (it can only ever be the target table's column), and `set` / literal
|
|
2265
|
+
* values reference no column at all, so their SQL is byte-identical with or
|
|
2266
|
+
* without a qualifier.
|
|
1072
2267
|
*/
|
|
1073
|
-
function
|
|
2268
|
+
function compileSetEntry(qi, key, value, params, ctx) {
|
|
1074
2269
|
const col = qi.toSqlColumn(key);
|
|
1075
|
-
const
|
|
2270
|
+
const column = qi.toColumn(key);
|
|
2271
|
+
const op = setOperatorOf(qi, key, value);
|
|
2272
|
+
if (op === 'expr') {
|
|
2273
|
+
const compiled = (0, expr_js_1.compileSetExpr)(exprEnv(qi, key, params, ctx), value);
|
|
2274
|
+
return { sql: `${col} = ${compiled.sql}`, column, reads: compiled.reads };
|
|
2275
|
+
}
|
|
1076
2276
|
// Enum columns get an explicit `::"EnumName"` cast on their value bind
|
|
1077
2277
|
// (see enumTypeForColumn); `''` everywhere else. Value-invariant, so the
|
|
1078
2278
|
// SQL cache and collectSetParams are unaffected.
|
|
1079
|
-
const cast = whereMod.enumCastSuffix(qi,
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
|
|
1090
|
-
const op = keys[0];
|
|
1091
|
-
const opValue = v[op];
|
|
1092
|
-
if (op === 'set') {
|
|
1093
|
-
params.push(coerceWriteValue(qi, key, opValue));
|
|
1094
|
-
return `${col} = ${qi.p(params.length)}${cast}`;
|
|
1095
|
-
}
|
|
1096
|
-
// Arithmetic operators: must be finite numbers
|
|
1097
|
-
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
1098
|
-
throw new errors_js_1.ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
|
|
1099
|
-
}
|
|
1100
|
-
if (op === 'increment') {
|
|
1101
|
-
params.push(opValue);
|
|
1102
|
-
return `${col} = ${ref} + ${qi.p(params.length)}`;
|
|
1103
|
-
}
|
|
1104
|
-
if (op === 'decrement') {
|
|
1105
|
-
params.push(opValue);
|
|
1106
|
-
return `${col} = ${ref} - ${qi.p(params.length)}`;
|
|
1107
|
-
}
|
|
1108
|
-
if (op === 'multiply') {
|
|
1109
|
-
params.push(opValue);
|
|
1110
|
-
return `${col} = ${ref} * ${qi.p(params.length)}`;
|
|
1111
|
-
}
|
|
1112
|
-
if (op === 'divide') {
|
|
1113
|
-
params.push(opValue);
|
|
1114
|
-
return `${col} = ${ref} / ${qi.p(params.length)}`;
|
|
1115
|
-
}
|
|
2279
|
+
const cast = whereMod.enumCastSuffix(qi, column);
|
|
2280
|
+
if (op === 'set') {
|
|
2281
|
+
params.push(coerceWriteValue(qi, key, value.set));
|
|
2282
|
+
return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
|
|
2283
|
+
}
|
|
2284
|
+
if (op !== null) {
|
|
2285
|
+
const opValue = value[op];
|
|
2286
|
+
// Arithmetic operators: must be finite numbers
|
|
2287
|
+
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
2288
|
+
throw new errors_js_1.ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
|
|
1116
2289
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
//
|
|
1120
|
-
|
|
2290
|
+
params.push(opValue);
|
|
2291
|
+
const ref = ctx.target ? `${ctx.target}.${col}` : col;
|
|
2292
|
+
// Reads only its own column, which no ordering needs to respect.
|
|
2293
|
+
return { sql: `${col} = ${ref} ${arithmeticSql(qi, op, column)} ${qi.p(params.length)}`, column, reads: NO_READS };
|
|
1121
2294
|
}
|
|
2295
|
+
// A misspelled operator gets its own E003 before the bind seam's general
|
|
2296
|
+
// shape check.
|
|
2297
|
+
assertBindableSetValue(qi, key, value);
|
|
1122
2298
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
1123
2299
|
params.push(coerceWriteValue(qi, key, value));
|
|
1124
|
-
return `${col} = ${qi.p(params.length)}${cast}
|
|
2300
|
+
return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* The environment one expression compiles in: how a value leaf binds, how a
|
|
2304
|
+
* column of the target row and of the incoming row render, and how a case
|
|
2305
|
+
* predicate compiles.
|
|
2306
|
+
*/
|
|
2307
|
+
function exprEnv(qi, key, params, ctx) {
|
|
2308
|
+
const cast = exprValueCast(qi, key);
|
|
2309
|
+
const target = ctx.target;
|
|
2310
|
+
const incoming = ctx.incoming;
|
|
2311
|
+
return {
|
|
2312
|
+
qi,
|
|
2313
|
+
targetKey: key,
|
|
2314
|
+
operation: ctx.operation,
|
|
2315
|
+
bind: (value, arithmetic) => {
|
|
2316
|
+
params.push(coerceWriteValue(qi, key, value));
|
|
2317
|
+
return `${qi.p(params.length)}${arithmetic ? arithmeticValueCast(qi, key, cast, arithmetic) : cast}`;
|
|
2318
|
+
},
|
|
2319
|
+
column: (column) => (target ? `${target}.${qi.q(column)}` : qi.q(column)),
|
|
2320
|
+
excluded: incoming ? (column) => `${incoming}.${qi.q(column)}` : undefined,
|
|
2321
|
+
insertColumns: ctx.insertColumns,
|
|
2322
|
+
predicate: (when) => {
|
|
2323
|
+
const sql = target
|
|
2324
|
+
? whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, target, when, params)
|
|
2325
|
+
: whereMod.buildWhereClause(qi, when, params);
|
|
2326
|
+
if (sql === null) {
|
|
2327
|
+
throw new errors_js_1.ValidationError(`expr.case() on "${qi.table}": an arm's when compiles to no predicate.`);
|
|
2328
|
+
}
|
|
2329
|
+
return sql;
|
|
2330
|
+
},
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* A type token that is safe to write after `::` as is: a plain name, a
|
|
2335
|
+
* multi-word built-in (`double precision`), optionally with a precision.
|
|
2336
|
+
*/
|
|
2337
|
+
const SAFE_CAST_TYPE = /^[A-Za-z_][A-Za-z0-9_]*( [A-Za-z_][A-Za-z0-9_]*)*(\(\d+( ?, ?\d+)?\))?$/;
|
|
2338
|
+
/**
|
|
2339
|
+
* The cast every bound value inside an expression carries on PostgreSQL: the
|
|
2340
|
+
* TARGET column's type. node-postgres sends parameters untyped, so in an
|
|
2341
|
+
* expression whose operands are all bound values PostgreSQL infers `text` and
|
|
2342
|
+
* fails (`CASE WHEN ... THEN $1 ELSE $2 END` into an int column is 42804, and
|
|
2343
|
+
* `$1 + $2` is 42725). Enums use the quoted enum cast; a type the metadata
|
|
2344
|
+
* does not name plainly, or one from another schema, gets none. Other engines
|
|
2345
|
+
* bind typed values and get none.
|
|
2346
|
+
*/
|
|
2347
|
+
function exprValueCast(qi, key) {
|
|
2348
|
+
if (!qi.dialect.capabilities.paramTypeCasts)
|
|
2349
|
+
return '';
|
|
2350
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
2351
|
+
if (column === undefined)
|
|
2352
|
+
return '';
|
|
2353
|
+
const enumCast = whereMod.enumCastSuffix(qi, column);
|
|
2354
|
+
if (enumCast)
|
|
2355
|
+
return enumCast;
|
|
2356
|
+
if (qi.crossSchemaTypeColumns.has(column))
|
|
2357
|
+
return '';
|
|
2358
|
+
const type = qi.columnPgTypeMap.get(column);
|
|
2359
|
+
if (type === undefined || type.startsWith('_') || !SAFE_CAST_TYPE.test(type))
|
|
2360
|
+
return '';
|
|
2361
|
+
return `::${type}`;
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* The cast on a bound arithmetic operand on PostgreSQL: the operand's own
|
|
2365
|
+
* class, so `n / 2` stays integer division into a float column and `n * 1.5`
|
|
2366
|
+
* is not forced through an integer cast. An integer operand keeps the target
|
|
2367
|
+
* column's cast when that column is an integer one, `int8` otherwise; a
|
|
2368
|
+
* non-integer one is `numeric`. Other engines bind typed values and get none.
|
|
2369
|
+
*/
|
|
2370
|
+
function arithmeticValueCast(qi, key, targetCast, cls) {
|
|
2371
|
+
if (!qi.dialect.capabilities.paramTypeCasts)
|
|
2372
|
+
return '';
|
|
2373
|
+
if (cls === 'dec')
|
|
2374
|
+
return '::numeric';
|
|
2375
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
2376
|
+
return column !== undefined && targetCast !== '' && isIntegerColumnType(qi.columnPgTypeMap.get(column))
|
|
2377
|
+
? targetCast
|
|
2378
|
+
: '::int8';
|
|
1125
2379
|
}
|
|
1126
2380
|
/**
|
|
1127
|
-
*
|
|
2381
|
+
* MySQL evaluates a single-table UPDATE's SET list LEFT TO RIGHT, and a later
|
|
2382
|
+
* assignment sees the NEW value of an earlier column; PostgreSQL, SQLite and
|
|
2383
|
+
* SQL Server read old values. So `{ a: col('b'), b: col('a') }` swaps there
|
|
2384
|
+
* and duplicates on MySQL. `ON DUPLICATE KEY UPDATE` behaves the same way.
|
|
2385
|
+
*
|
|
2386
|
+
* Order the list so every assignment that READS a column comes before the
|
|
2387
|
+
* assignment that WRITES it: a stable topological sort that keeps the
|
|
2388
|
+
* canonical order wherever the reads allow, so the order is a pure function of
|
|
2389
|
+
* the data's shape and the SQL-cache key (which already captures that shape)
|
|
2390
|
+
* stays sufficient. A cycle (a swap) has no such order and is refused.
|
|
2391
|
+
*/
|
|
2392
|
+
function orderForLeftToRightSet(qi, entries) {
|
|
2393
|
+
if (entries.every((e) => e.reads.size === 0))
|
|
2394
|
+
return entries.map((e) => e.sql);
|
|
2395
|
+
const writer = new Map();
|
|
2396
|
+
entries.forEach((e, i) => {
|
|
2397
|
+
writer.set(e.column, i);
|
|
2398
|
+
});
|
|
2399
|
+
const before = entries.map(() => []);
|
|
2400
|
+
const pending = entries.map(() => 0);
|
|
2401
|
+
entries.forEach((e, i) => {
|
|
2402
|
+
for (const column of e.reads) {
|
|
2403
|
+
const j = writer.get(column);
|
|
2404
|
+
if (j === undefined || j === i)
|
|
2405
|
+
continue;
|
|
2406
|
+
before[i].push(j);
|
|
2407
|
+
pending[j]++;
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
const ready = pending.flatMap((n, i) => (n === 0 ? [i] : []));
|
|
2411
|
+
const order = [];
|
|
2412
|
+
while (ready.length > 0) {
|
|
2413
|
+
ready.sort((a, b) => a - b);
|
|
2414
|
+
const i = ready.shift();
|
|
2415
|
+
order.push(i);
|
|
2416
|
+
for (const j of before[i])
|
|
2417
|
+
if (--pending[j] === 0)
|
|
2418
|
+
ready.push(j);
|
|
2419
|
+
}
|
|
2420
|
+
if (order.length < entries.length) {
|
|
2421
|
+
const stuck = entries
|
|
2422
|
+
.filter((_, i) => pending[i] > 0)
|
|
2423
|
+
.map((e) => qi.tableMeta.reverseColumnMap[e.column] ?? e.column);
|
|
2424
|
+
throw new errors_js_1.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 " +
|
|
2425
|
+
'cannot be expressed. Split it into two updates.');
|
|
2426
|
+
}
|
|
2427
|
+
return order.map((i) => entries[i].sql);
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Reject a `data` value that reached {@link buildSetClauses}'s plain-value
|
|
1128
2431
|
* fallthrough as a plain object with exactly ONE key that is not a known
|
|
1129
2432
|
* atomic operator. On a scalar column that shape can only be a misspelled
|
|
1130
2433
|
* operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
|
|
@@ -1155,52 +2458,47 @@ function assertBindableSetValue(qi, key, value) {
|
|
|
1155
2458
|
}
|
|
1156
2459
|
/**
|
|
1157
2460
|
* Fingerprint SET clauses for update/updateMany.
|
|
1158
|
-
* Captures key names + operator types (set/increment/etc) but not values.
|
|
2461
|
+
* Captures key names + operator types (set/increment/etc) but not values. An
|
|
2462
|
+
* expression contributes `k:expr(<shape>)` from the same walk that compiles
|
|
2463
|
+
* it, so a statement without one keeps its key byte-identical.
|
|
1159
2464
|
*/
|
|
1160
2465
|
function fingerprintSet(qi, data) {
|
|
1161
2466
|
const entries = writeEntries(qi, data);
|
|
1162
2467
|
const parts = [];
|
|
1163
2468
|
for (const [k, v] of entries) {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const keys = Object.keys(v);
|
|
1170
|
-
if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
|
|
1171
|
-
parts.push(`${k}:${keys[0]}`);
|
|
1172
|
-
continue;
|
|
1173
|
-
}
|
|
2469
|
+
const op = setOperatorOf(qi, k, v);
|
|
2470
|
+
if (op === 'expr') {
|
|
2471
|
+
const env = exprEnv(qi, k, [], UPDATE_SET);
|
|
2472
|
+
parts.push(`${k}:expr(${(0, expr_js_1.compileSetExpr)(env, v).fingerprint})`);
|
|
2473
|
+
continue;
|
|
1174
2474
|
}
|
|
1175
|
-
parts.push(`${k}
|
|
2475
|
+
parts.push(`${k}:${op ?? 'eq'}`);
|
|
1176
2476
|
}
|
|
1177
2477
|
return parts.join(',');
|
|
1178
2478
|
}
|
|
1179
2479
|
/**
|
|
1180
|
-
* Collect SET params for update/updateMany. Mirrors
|
|
2480
|
+
* Collect SET params for update/updateMany. Mirrors buildSetClauses param order.
|
|
1181
2481
|
*/
|
|
1182
2482
|
function collectSetParams(qi, data, params) {
|
|
1183
2483
|
const entries = writeEntries(qi, data);
|
|
1184
2484
|
for (const [k, v] of entries) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
continue;
|
|
1198
|
-
}
|
|
1199
|
-
// Mirror the build path's misspelled-operator guard: the SET fingerprint
|
|
1200
|
-
// is `key:eq` for both a plain value and a bogus operator object, so a
|
|
1201
|
-
// cache HIT would otherwise bind the object and skip the check.
|
|
1202
|
-
assertBindableSetValue(qi, k, v);
|
|
2485
|
+
const op = setOperatorOf(qi, k, v);
|
|
2486
|
+
if (op === 'expr') {
|
|
2487
|
+
// The build's own walk: it binds the same leaves in the same order.
|
|
2488
|
+
(0, expr_js_1.compileSetExpr)(exprEnv(qi, k, params, UPDATE_SET), v);
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
if (op !== null) {
|
|
2492
|
+
// `set` is the only operator that binds a user value of arbitrary type;
|
|
2493
|
+
// the arithmetic operators are finite numbers, which coerceWriteValue
|
|
2494
|
+
// returns untouched. Mirrors buildSetClauses.
|
|
2495
|
+
params.push(coerceWriteValue(qi, k, v[op]));
|
|
2496
|
+
continue;
|
|
1203
2497
|
}
|
|
2498
|
+
// Mirror the build path's misspelled-operator guard: the SET fingerprint
|
|
2499
|
+
// is `key:eq` for both a plain value and a bogus operator object, so a
|
|
2500
|
+
// cache HIT would otherwise bind the object and skip the check.
|
|
2501
|
+
assertBindableSetValue(qi, k, v);
|
|
1204
2502
|
params.push(coerceWriteValue(qi, k, v));
|
|
1205
2503
|
}
|
|
1206
2504
|
}
|