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
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm: SQL expressions in update data.
|
|
4
|
+
*
|
|
5
|
+
* `expr.greatest(col('viewCount'), 100)` and friends are branded node objects,
|
|
6
|
+
* and `expr.excluded('events')` names the incoming row of an upsert. Both are
|
|
7
|
+
* written into the SQL TEXT, so both carry a symbol brand: `JSON.parse` cannot
|
|
8
|
+
* produce one, so request data can never become an expression (the same
|
|
9
|
+
* reasoning as `col()` and `UNSAFE`). `Symbol.for` so the ESM and CJS copies of
|
|
10
|
+
* the package recognize each other's nodes. A plain look-alike object
|
|
11
|
+
* (`{ kind: 'greatest', args: [...] }`) is a VALUE, and on a non-JSON column
|
|
12
|
+
* the write path refuses it.
|
|
13
|
+
*
|
|
14
|
+
* The public types live in `query/types.ts` (`SqlExpr`, `ExcludedRef`). This
|
|
15
|
+
* module holds the node constructors, their validation, and
|
|
16
|
+
* {@link compileSetExpr}: the ONE traversal that produces an expression's SQL,
|
|
17
|
+
* its bound parameters and its cache fingerprint together, so the SET build,
|
|
18
|
+
* the cache-hit param collector and the fingerprint cannot disagree (the
|
|
19
|
+
* failure class behind the where-clause cache bugs).
|
|
20
|
+
*
|
|
21
|
+
* Where expressions are accepted: `update`, `updateMany`, `upsert`'s `update`,
|
|
22
|
+
* `createMany`'s `onConflict.update`, and nested update data (which runs
|
|
23
|
+
* through `update()`). Not in `create` / `createMany` data, where the write
|
|
24
|
+
* bind seam refuses a node.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.expr = exports.EXCLUDED_REF = exports.SQL_EXPR = void 0;
|
|
28
|
+
exports.isSqlExpr = isSqlExpr;
|
|
29
|
+
exports.isExcludedRef = isExcludedRef;
|
|
30
|
+
exports.isPlainExprShape = isPlainExprShape;
|
|
31
|
+
exports.plainExprShapeMessage = plainExprShapeMessage;
|
|
32
|
+
exports.exprNotAllowedMessage = exprNotAllowedMessage;
|
|
33
|
+
exports.assertExprTarget = assertExprTarget;
|
|
34
|
+
exports.compileSetExpr = compileSetExpr;
|
|
35
|
+
exports.resolveExcludedFromCreate = resolveExcludedFromCreate;
|
|
36
|
+
const errors_js_1 = require("../errors.js");
|
|
37
|
+
const column_ref_js_1 = require("./column-ref.js");
|
|
38
|
+
const filters_js_1 = require("./filters.js");
|
|
39
|
+
const normalize_args_js_1 = require("./normalize-args.js");
|
|
40
|
+
const utils_js_1 = require("./utils.js");
|
|
41
|
+
const where_js_1 = require("./where.js");
|
|
42
|
+
const where_compile_js_1 = require("./where-compile.js");
|
|
43
|
+
/** The brand every {@link SqlExpr} node carries. */
|
|
44
|
+
exports.SQL_EXPR = Symbol.for('turbine-orm.sqlExpr');
|
|
45
|
+
/** The brand an {@link ExcludedRef} carries (the incoming row of an upsert). */
|
|
46
|
+
exports.EXCLUDED_REF = Symbol.for('turbine-orm.excludedRef');
|
|
47
|
+
const EXPR_KINDS = new Set([
|
|
48
|
+
'add',
|
|
49
|
+
'sub',
|
|
50
|
+
'mul',
|
|
51
|
+
'div',
|
|
52
|
+
'coalesce',
|
|
53
|
+
'greatest',
|
|
54
|
+
'least',
|
|
55
|
+
'case',
|
|
56
|
+
'now',
|
|
57
|
+
'excluded',
|
|
58
|
+
]);
|
|
59
|
+
/** Operand count bounds for coalesce / greatest / least. */
|
|
60
|
+
const MIN_VARIADIC = 2;
|
|
61
|
+
const MAX_VARIADIC = 8;
|
|
62
|
+
/** Arm count bounds for case. */
|
|
63
|
+
const MAX_CASE_ARMS = 16;
|
|
64
|
+
/** Nesting bound, so a hostile tree cannot exhaust the stack. */
|
|
65
|
+
const MAX_EXPR_DEPTH = 32;
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Constructors
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
function makeNode(kind, args, extra) {
|
|
70
|
+
const node = { kind, args: Object.freeze([...args]), ...extra };
|
|
71
|
+
// Non-enumerable, like `col()`'s brand: the node logs and compares as
|
|
72
|
+
// `{ kind, args }`, and no `Object.keys` walker ever sees the symbol.
|
|
73
|
+
Object.defineProperty(node, exports.SQL_EXPR, { value: true, enumerable: false });
|
|
74
|
+
return Object.freeze(node);
|
|
75
|
+
}
|
|
76
|
+
function describeOperand(value) {
|
|
77
|
+
if (Array.isArray(value))
|
|
78
|
+
return 'an array';
|
|
79
|
+
if (typeof value === 'object' && value !== null) {
|
|
80
|
+
if (isPlainExprShape(value))
|
|
81
|
+
return 'a plain { kind, args } object (not built by expr)';
|
|
82
|
+
return (0, filters_js_1.isPlainRecord)(value) ? 'a plain object' : `a ${value.constructor?.name ?? 'object'} instance`;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === 'number')
|
|
85
|
+
return String(value);
|
|
86
|
+
return typeof value;
|
|
87
|
+
}
|
|
88
|
+
/** Refuse an operand no engine can bind as one value. Validated at construction, so the error names the call. */
|
|
89
|
+
function assertOperand(kind, position, value) {
|
|
90
|
+
const at = `expr.${kind}(): operand ${position + 1}`;
|
|
91
|
+
if (value === undefined) {
|
|
92
|
+
throw new errors_js_1.ValidationError(`${at} is undefined. Pass null for SQL NULL.`);
|
|
93
|
+
}
|
|
94
|
+
if (value === null || (0, column_ref_js_1.isColumnRef)(value) || isSqlExpr(value))
|
|
95
|
+
return;
|
|
96
|
+
switch (typeof value) {
|
|
97
|
+
case 'string':
|
|
98
|
+
case 'boolean':
|
|
99
|
+
case 'bigint':
|
|
100
|
+
return;
|
|
101
|
+
case 'number':
|
|
102
|
+
if (Number.isFinite(value))
|
|
103
|
+
return;
|
|
104
|
+
throw new errors_js_1.ValidationError(`${at} is ${value}; a bound number must be finite.`);
|
|
105
|
+
}
|
|
106
|
+
if (value instanceof Date) {
|
|
107
|
+
if (!Number.isNaN(value.getTime()))
|
|
108
|
+
return;
|
|
109
|
+
throw new errors_js_1.ValidationError(`${at} is an Invalid Date.`);
|
|
110
|
+
}
|
|
111
|
+
if (value instanceof Uint8Array)
|
|
112
|
+
return;
|
|
113
|
+
if ((0, column_ref_js_1.isPlainColumnRefShape)(value)) {
|
|
114
|
+
const name = JSON.stringify(value.col);
|
|
115
|
+
throw new errors_js_1.ValidationError(`${at} is a plain { col: ${name} } object, not a column reference. Build one with col(${name}) ` +
|
|
116
|
+
"(import { col } from 'turbine-orm').");
|
|
117
|
+
}
|
|
118
|
+
throw new errors_js_1.ValidationError(`${at} is ${describeOperand(value)}. An operand is a scalar (string, number, bigint, boolean, Date, ` +
|
|
119
|
+
'Buffer or null), col(field), or a node built by expr; a plain object is never an expression.');
|
|
120
|
+
}
|
|
121
|
+
/** Arithmetic takes numbers only: a string, boolean or Date is refused at construction. */
|
|
122
|
+
function assertNumericOperand(kind, position, value) {
|
|
123
|
+
assertOperand(kind, position, value);
|
|
124
|
+
const t = typeof value;
|
|
125
|
+
if (t === 'string' || t === 'boolean' || value instanceof Date || value instanceof Uint8Array) {
|
|
126
|
+
throw new errors_js_1.ValidationError(`expr.${kind}(): operand ${position + 1} is a ${value instanceof Date ? 'Date' : t}. Arithmetic takes ` +
|
|
127
|
+
'numbers, bigints, col() of a numeric column, or numeric expressions. Date arithmetic is not supported.');
|
|
128
|
+
}
|
|
129
|
+
if (isSqlExpr(value) && value.kind === 'now') {
|
|
130
|
+
throw new errors_js_1.ValidationError(`expr.${kind}(): operand ${position + 1} is expr.now(). Date arithmetic is not supported; ` +
|
|
131
|
+
'compute the timestamp in JavaScript and pass it as a value.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function arithmetic(kind, a, b) {
|
|
135
|
+
assertNumericOperand(kind, 0, a);
|
|
136
|
+
assertNumericOperand(kind, 1, b);
|
|
137
|
+
return makeNode(kind, [a, b]);
|
|
138
|
+
}
|
|
139
|
+
function variadic(kind, operands) {
|
|
140
|
+
if (operands.length < MIN_VARIADIC || operands.length > MAX_VARIADIC) {
|
|
141
|
+
throw new errors_js_1.ValidationError(`expr.${kind}() takes ${MIN_VARIADIC} to ${MAX_VARIADIC} operands, got ${operands.length}.`);
|
|
142
|
+
}
|
|
143
|
+
operands.forEach((v, i) => {
|
|
144
|
+
assertOperand(kind, i, v);
|
|
145
|
+
});
|
|
146
|
+
return makeNode(kind, operands);
|
|
147
|
+
}
|
|
148
|
+
function caseNode(arms, otherwise) {
|
|
149
|
+
if (!Array.isArray(arms) || arms.length === 0 || arms.length > MAX_CASE_ARMS) {
|
|
150
|
+
throw new errors_js_1.ValidationError(`expr.case() takes an array of 1 to ${MAX_CASE_ARMS} arms ({ when, then }), got ` +
|
|
151
|
+
`${Array.isArray(arms) ? `${arms.length} arms` : describeOperand(arms)}.`);
|
|
152
|
+
}
|
|
153
|
+
const frozen = arms.map((arm, i) => {
|
|
154
|
+
if (!(0, filters_js_1.isPlainRecord)(arm)) {
|
|
155
|
+
throw new errors_js_1.ValidationError(`expr.case(): arm ${i + 1} is ${describeOperand(arm)}; write { when, then }.`);
|
|
156
|
+
}
|
|
157
|
+
const extra = Object.keys(arm).filter((k) => k !== 'when' && k !== 'then');
|
|
158
|
+
if (extra.length > 0) {
|
|
159
|
+
throw new errors_js_1.ValidationError(`expr.case(): arm ${i + 1} has unknown key "${extra[0]}"; an arm is { when, then }.`);
|
|
160
|
+
}
|
|
161
|
+
if (!(0, filters_js_1.isPlainRecord)(arm.when)) {
|
|
162
|
+
throw new errors_js_1.ValidationError(`expr.case(): arm ${i + 1}'s when is ${describeOperand(arm.when)}; write a predicate object like \`where\`.`);
|
|
163
|
+
}
|
|
164
|
+
if (!('then' in arm)) {
|
|
165
|
+
throw new errors_js_1.ValidationError(`expr.case(): arm ${i + 1} has no then. Pass null for SQL NULL.`);
|
|
166
|
+
}
|
|
167
|
+
assertOperand('case', i, arm.then);
|
|
168
|
+
// biome-ignore lint/suspicious/noThenProperty: SQL's CASE WHEN ... THEN; a value operand, never a function, so never thenable
|
|
169
|
+
return Object.freeze({ when: arm.when, then: arm.then });
|
|
170
|
+
});
|
|
171
|
+
if (otherwise !== undefined)
|
|
172
|
+
assertOperand('case', arms.length, otherwise);
|
|
173
|
+
return makeNode('case', frozen, otherwise === undefined ? undefined : { else: otherwise });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Builders for SQL expressions in update data. Each returns a frozen, branded
|
|
177
|
+
* node; the SQL is generated per engine when the update compiles.
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { col, expr } from 'turbine-orm';
|
|
181
|
+
* await db.posts.update({
|
|
182
|
+
* where: { id },
|
|
183
|
+
* data: {
|
|
184
|
+
* viewCount: expr.greatest(col('viewCount'), 100),
|
|
185
|
+
* score: expr.add(col('score'), expr.mul(col('bonus'), 2)),
|
|
186
|
+
* editedAt: expr.now(),
|
|
187
|
+
* },
|
|
188
|
+
* });
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
exports.expr = Object.freeze({
|
|
192
|
+
/** `a + b`. Numbers, bigints, numeric columns and numeric expressions. */
|
|
193
|
+
add(a, b) {
|
|
194
|
+
return arithmetic('add', a, b);
|
|
195
|
+
},
|
|
196
|
+
/** `a - b`. */
|
|
197
|
+
sub(a, b) {
|
|
198
|
+
return arithmetic('sub', a, b);
|
|
199
|
+
},
|
|
200
|
+
/** `a * b`. */
|
|
201
|
+
mul(a, b) {
|
|
202
|
+
return arithmetic('mul', a, b);
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* `a / b`. When both operands are integers (an integer column, an integer
|
|
206
|
+
* number or bigint, or an integer expression) the division truncates toward
|
|
207
|
+
* zero on every engine, as the `divide` update operator does on an integer
|
|
208
|
+
* column. With a non-integer operand it is a decimal division.
|
|
209
|
+
*/
|
|
210
|
+
div(a, b) {
|
|
211
|
+
return arithmetic('div', a, b);
|
|
212
|
+
},
|
|
213
|
+
/** The first non-NULL operand (2 to 8). */
|
|
214
|
+
coalesce(a, b, ...rest) {
|
|
215
|
+
return variadic('coalesce', [a, b, ...rest]);
|
|
216
|
+
},
|
|
217
|
+
/**
|
|
218
|
+
* The largest operand (2 to 8), IGNORING NULLs on every engine (PostgreSQL's
|
|
219
|
+
* meaning; MySQL and SQLite are rewritten to match). NULL only when every
|
|
220
|
+
* operand is NULL.
|
|
221
|
+
*/
|
|
222
|
+
greatest(a, b, ...rest) {
|
|
223
|
+
return variadic('greatest', [a, b, ...rest]);
|
|
224
|
+
},
|
|
225
|
+
/** The smallest operand (2 to 8), ignoring NULLs on every engine. */
|
|
226
|
+
least(a, b, ...rest) {
|
|
227
|
+
return variadic('least', [a, b, ...rest]);
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
* `CASE WHEN ... THEN ... ELSE ... END`. Each arm's `when` is a predicate on
|
|
231
|
+
* the current row's columns written like `where` (no relation filters); the
|
|
232
|
+
* first matching arm wins. Without `otherwise` an unmatched row gets NULL.
|
|
233
|
+
*/
|
|
234
|
+
case(arms, otherwise) {
|
|
235
|
+
return caseNode(arms, otherwise);
|
|
236
|
+
},
|
|
237
|
+
/**
|
|
238
|
+
* The database server's current time, stored in the zone convention Turbine
|
|
239
|
+
* reads with: a zone-less `timestamp` / `date` gets the UTC wall clock (unless
|
|
240
|
+
* the client sets `utcTimestamps: false`). On PostgreSQL it is the
|
|
241
|
+
* TRANSACTION's start time; on MySQL, the statement's. Temporal columns only.
|
|
242
|
+
*/
|
|
243
|
+
now() {
|
|
244
|
+
return makeNode('now', []);
|
|
245
|
+
},
|
|
246
|
+
/**
|
|
247
|
+
* The incoming row's value for `field`: inside `upsert`'s `update` (the value
|
|
248
|
+
* `create` supplies) and `createMany`'s `onConflict.update` (each row's own
|
|
249
|
+
* value). Refused anywhere else.
|
|
250
|
+
*/
|
|
251
|
+
excluded(field) {
|
|
252
|
+
if (typeof field !== 'string' || field.length === 0) {
|
|
253
|
+
throw new errors_js_1.ValidationError('expr.excluded() takes the name of a field on the table, as a non-empty string.');
|
|
254
|
+
}
|
|
255
|
+
const node = { kind: 'excluded', args: Object.freeze([]), field };
|
|
256
|
+
Object.defineProperty(node, exports.SQL_EXPR, { value: true, enumerable: false });
|
|
257
|
+
Object.defineProperty(node, exports.EXCLUDED_REF, { value: true, enumerable: false });
|
|
258
|
+
return Object.freeze(node);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
/** True only for a node built by {@link expr} (any loaded copy of the package). */
|
|
262
|
+
function isSqlExpr(value) {
|
|
263
|
+
return (typeof value === 'object' &&
|
|
264
|
+
value !== null &&
|
|
265
|
+
value[exports.SQL_EXPR] === true &&
|
|
266
|
+
typeof value.kind === 'string');
|
|
267
|
+
}
|
|
268
|
+
/** True only for `expr.excluded(...)`. */
|
|
269
|
+
function isExcludedRef(value) {
|
|
270
|
+
return isSqlExpr(value) && value[exports.EXCLUDED_REF] === true;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* True for an UNBRANDED object shaped like a node (`{ kind: 'greatest', args }`),
|
|
274
|
+
* which is what parsed request data produces. The write path refuses it on a
|
|
275
|
+
* non-JSON column rather than storing it as JSON text.
|
|
276
|
+
*/
|
|
277
|
+
function isPlainExprShape(value) {
|
|
278
|
+
if (!(0, filters_js_1.isPlainRecord)(value) || isSqlExpr(value))
|
|
279
|
+
return false;
|
|
280
|
+
const kind = value.kind;
|
|
281
|
+
return (typeof kind === 'string' && EXPR_KINDS.has(kind) && (Array.isArray(value.args) || typeof value.field === 'string'));
|
|
282
|
+
}
|
|
283
|
+
/** The refusal for {@link isPlainExprShape}. */
|
|
284
|
+
function plainExprShapeMessage(table, key, kind) {
|
|
285
|
+
return (`A plain { kind: ${JSON.stringify(kind)}, ... } object written to "${table}.${key}" is not an expression. ` +
|
|
286
|
+
`Build one with expr.${EXPR_KINDS.has(kind) ? kind : 'add'}(...) (import { expr } from 'turbine-orm'): ` +
|
|
287
|
+
'expressions carry a symbol brand so that parsed request data can never become SQL. ' +
|
|
288
|
+
'To store JSON text in this column, pass `JSON.stringify(value)` or `jsonValue(value)`.');
|
|
289
|
+
}
|
|
290
|
+
/** The refusal for a node at a position that takes values only (create data, `{ set }`). */
|
|
291
|
+
function exprNotAllowedMessage(table, key, value) {
|
|
292
|
+
const what = (0, column_ref_js_1.isColumnRef)(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
|
|
293
|
+
return (`${what} cannot be written to "${table}.${key}" here: expressions and column references are accepted as the ` +
|
|
294
|
+
"direct value of a field in update, updateMany, upsert's update and createMany's onConflict.update, " +
|
|
295
|
+
'not in create / createMany data and not inside an operator such as { set }.');
|
|
296
|
+
}
|
|
297
|
+
const NUMERIC_TYPES = new Set([
|
|
298
|
+
'int2',
|
|
299
|
+
'int4',
|
|
300
|
+
'int8',
|
|
301
|
+
'smallint',
|
|
302
|
+
'integer',
|
|
303
|
+
'int',
|
|
304
|
+
'bigint',
|
|
305
|
+
'tinyint',
|
|
306
|
+
'mediumint',
|
|
307
|
+
'serial',
|
|
308
|
+
'bigserial',
|
|
309
|
+
'smallserial',
|
|
310
|
+
'numeric',
|
|
311
|
+
'decimal',
|
|
312
|
+
'dec',
|
|
313
|
+
'fixed',
|
|
314
|
+
'float4',
|
|
315
|
+
'float8',
|
|
316
|
+
'real',
|
|
317
|
+
'double precision',
|
|
318
|
+
'double',
|
|
319
|
+
'float',
|
|
320
|
+
'money',
|
|
321
|
+
'smallmoney',
|
|
322
|
+
'number',
|
|
323
|
+
]);
|
|
324
|
+
/** The integer subset of {@link NUMERIC_TYPES}. */
|
|
325
|
+
const INTEGER_TYPES = new Set([
|
|
326
|
+
'int2',
|
|
327
|
+
'int4',
|
|
328
|
+
'int8',
|
|
329
|
+
'smallint',
|
|
330
|
+
'integer',
|
|
331
|
+
'int',
|
|
332
|
+
'bigint',
|
|
333
|
+
'tinyint',
|
|
334
|
+
'mediumint',
|
|
335
|
+
'serial',
|
|
336
|
+
'bigserial',
|
|
337
|
+
'smallserial',
|
|
338
|
+
]);
|
|
339
|
+
const TEMPORAL_TYPES = {
|
|
340
|
+
timestamptz: 'tz',
|
|
341
|
+
'timestamp with time zone': 'tz',
|
|
342
|
+
datetimeoffset: 'tz',
|
|
343
|
+
timestamp: 'timestamp',
|
|
344
|
+
'timestamp without time zone': 'timestamp',
|
|
345
|
+
datetime: 'timestamp',
|
|
346
|
+
datetime2: 'timestamp',
|
|
347
|
+
smalldatetime: 'timestamp',
|
|
348
|
+
date: 'date',
|
|
349
|
+
time: 'time',
|
|
350
|
+
timetz: 'time',
|
|
351
|
+
'time with time zone': 'time',
|
|
352
|
+
'time without time zone': 'time',
|
|
353
|
+
};
|
|
354
|
+
/** A column type token, lowercased, without precision or `unsigned`. */
|
|
355
|
+
function normalizeType(dbType) {
|
|
356
|
+
return dbType
|
|
357
|
+
.toLowerCase()
|
|
358
|
+
.replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, '')
|
|
359
|
+
.replace(/\bunsigned\b|\bzerofill\b/g, '')
|
|
360
|
+
.replace(/\s+/g, ' ')
|
|
361
|
+
.trim();
|
|
362
|
+
}
|
|
363
|
+
function columnClass(dbType) {
|
|
364
|
+
if (dbType === undefined)
|
|
365
|
+
return 'unknown';
|
|
366
|
+
const t = normalizeType(dbType);
|
|
367
|
+
if (t === 'json' || t === 'jsonb')
|
|
368
|
+
return 'json';
|
|
369
|
+
if (t.startsWith('_') || t.endsWith('[]'))
|
|
370
|
+
return 'array';
|
|
371
|
+
if (NUMERIC_TYPES.has(t))
|
|
372
|
+
return 'numeric';
|
|
373
|
+
if (Object.hasOwn(TEMPORAL_TYPES, t))
|
|
374
|
+
return 'temporal';
|
|
375
|
+
return 'other';
|
|
376
|
+
}
|
|
377
|
+
/** The {@link NumClass} of a column of this type. */
|
|
378
|
+
function numClassOfType(dbType) {
|
|
379
|
+
if (dbType === undefined)
|
|
380
|
+
return 'unknown';
|
|
381
|
+
const t = normalizeType(dbType);
|
|
382
|
+
if (INTEGER_TYPES.has(t))
|
|
383
|
+
return 'int';
|
|
384
|
+
return NUMERIC_TYPES.has(t) ? 'dec' : 'other';
|
|
385
|
+
}
|
|
386
|
+
/** The {@link NumClass} of a bound value. */
|
|
387
|
+
function numClassOfValue(value) {
|
|
388
|
+
if (value === null)
|
|
389
|
+
return 'null';
|
|
390
|
+
if (typeof value === 'bigint')
|
|
391
|
+
return 'int';
|
|
392
|
+
if (typeof value === 'number')
|
|
393
|
+
return Number.isInteger(value) ? 'int' : 'dec';
|
|
394
|
+
return 'other';
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The class of a result computed from these operands: anything non-numeric
|
|
398
|
+
* wins, then a non-integer, then an untyped column; NULL is neutral.
|
|
399
|
+
*/
|
|
400
|
+
function combineClasses(classes) {
|
|
401
|
+
let out = 'null';
|
|
402
|
+
for (const c of classes) {
|
|
403
|
+
if (c === 'other')
|
|
404
|
+
return 'other';
|
|
405
|
+
if (c === 'dec' || (c === 'unknown' && out !== 'dec') || (c === 'int' && out === 'null'))
|
|
406
|
+
out = c;
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
function temporalKind(dbType) {
|
|
411
|
+
if (dbType === undefined)
|
|
412
|
+
return undefined;
|
|
413
|
+
const t = normalizeType(dbType);
|
|
414
|
+
return Object.hasOwn(TEMPORAL_TYPES, t) ? TEMPORAL_TYPES[t] : undefined;
|
|
415
|
+
}
|
|
416
|
+
function columnType(qi, column) {
|
|
417
|
+
return qi.columnPgTypeMap.get(column);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Refuse an expression assigned to a column it cannot target in this release
|
|
421
|
+
* (json/jsonb and array columns). Shared by the walker and the single-row
|
|
422
|
+
* upsert's `excluded` resolution, so both paths refuse the same columns.
|
|
423
|
+
*/
|
|
424
|
+
function assertExprTarget(qi, key, value, operation) {
|
|
425
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
426
|
+
if (column === undefined) {
|
|
427
|
+
// The SET builder's own unknown-column error names the key; reach it.
|
|
428
|
+
return qi.toColumn(key);
|
|
429
|
+
}
|
|
430
|
+
const cls = columnClass(columnType(qi, column));
|
|
431
|
+
if (cls === 'json' || cls === 'array') {
|
|
432
|
+
// A bare copy of the incoming row's same-typed column (the onConflict
|
|
433
|
+
// field list, `update: ['payload']`) applies no operator, so it is a plain
|
|
434
|
+
// assignment on any column type.
|
|
435
|
+
if (isBareStructuredCopy(qi, column, value))
|
|
436
|
+
return column;
|
|
437
|
+
const what = (0, column_ref_js_1.isColumnRef)(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
|
|
438
|
+
throw new errors_js_1.ValidationError(`${what} in ${operation} data cannot target "${qi.table}.${key}": it is ${cls === 'json' ? 'a json/jsonb' : 'an array'} ` +
|
|
439
|
+
'column, and expressions on json and array columns are not supported. Write a plain value instead.');
|
|
440
|
+
}
|
|
441
|
+
return column;
|
|
442
|
+
}
|
|
443
|
+
/** `expr.excluded(f)` assigned whole to a column of exactly f's declared type. */
|
|
444
|
+
function isBareStructuredCopy(qi, column, value) {
|
|
445
|
+
if (!isExcludedRef(value))
|
|
446
|
+
return false;
|
|
447
|
+
const source = (0, utils_js_1.resolveColumnName)(qi.tableMeta, value.field);
|
|
448
|
+
const type = columnType(qi, column);
|
|
449
|
+
return source !== undefined && type !== undefined && columnType(qi, source) === type;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Compile one expression (or `col()` reference) assigned to `env.targetKey`.
|
|
453
|
+
* The single traversal: SQL, params (through `env.bind`, in emission order)
|
|
454
|
+
* and fingerprint come out of one walk.
|
|
455
|
+
*
|
|
456
|
+
* What an expression stores is the same on every engine, by one rule:
|
|
457
|
+
* arithmetic is integer arithmetic when both operands are integers (integer
|
|
458
|
+
* columns, integer numbers, bigints, integer expressions), and integer
|
|
459
|
+
* division truncates toward zero, as `{ divide }` does on an integer column.
|
|
460
|
+
* Any other numeric operand makes it decimal. A non-integer result is refused
|
|
461
|
+
* on an integer column, because the engines disagree on converting one (round
|
|
462
|
+
* half away from zero, round half to even, truncate, or store it unconverted).
|
|
463
|
+
* The class is decided from the operands' types and the bound values' kinds,
|
|
464
|
+
* both part of the fingerprint, so the SQL text never depends on anything the
|
|
465
|
+
* cache key does not capture.
|
|
466
|
+
*/
|
|
467
|
+
function compileSetExpr(env, value) {
|
|
468
|
+
const { qi } = env;
|
|
469
|
+
assertEngine(qi);
|
|
470
|
+
const column = assertExprTarget(qi, env.targetKey, value, env.operation);
|
|
471
|
+
const state = { env, targetType: columnType(qi, column), reads: new Set() };
|
|
472
|
+
const part = walkOperand(state, value, false, 0);
|
|
473
|
+
if (part.cls === 'dec' && numClassOfType(state.targetType) === 'int') {
|
|
474
|
+
const what = (0, column_ref_js_1.isColumnRef)(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
|
|
475
|
+
throw new errors_js_1.ValidationError(`${what} in ${env.operation} data on "${qi.table}.${env.targetKey}" computes a non-integer value (an operand ` +
|
|
476
|
+
`is a non-integer number or a non-integer column), and "${env.targetKey}" is an integer column ` +
|
|
477
|
+
`(${state.targetType}). Engines convert such a value differently, so it is refused. Use integer operands ` +
|
|
478
|
+
"(expr.div truncates: expr.div(expr.mul(col('n'), 3), 2) is n * 1.5 truncated), or compute the value in " +
|
|
479
|
+
'JavaScript.');
|
|
480
|
+
}
|
|
481
|
+
return { sql: part.sql, fingerprint: part.fp, reads: state.reads };
|
|
482
|
+
}
|
|
483
|
+
function assertEngine(qi) {
|
|
484
|
+
if (qi.dialect.capabilities.updateExpressions)
|
|
485
|
+
return;
|
|
486
|
+
throw new errors_js_1.UnsupportedFeatureError('update expressions (expr / col() in update data)', qi.dialect.name, 'Compute the value in JavaScript and write it as a plain value.');
|
|
487
|
+
}
|
|
488
|
+
function walkOperand(state, value, inArithmetic, depth) {
|
|
489
|
+
if (depth > MAX_EXPR_DEPTH) {
|
|
490
|
+
throw new errors_js_1.ValidationError(`An update expression is nested more than ${MAX_EXPR_DEPTH} levels deep.`);
|
|
491
|
+
}
|
|
492
|
+
if ((0, column_ref_js_1.isColumnRef)(value)) {
|
|
493
|
+
const column = resolveOperandColumn(state, value.col, 'col', inArithmetic);
|
|
494
|
+
state.reads.add(column);
|
|
495
|
+
return { sql: state.env.column(column), fp: `c:${column}`, cls: numClassOfType(columnType(state.env.qi, column)) };
|
|
496
|
+
}
|
|
497
|
+
if (isSqlExpr(value))
|
|
498
|
+
return walkNode(state, value, inArithmetic, depth);
|
|
499
|
+
// A value leaf. Its shape was validated when the node was built; the bind
|
|
500
|
+
// seam coerces it for the TARGET column, exactly as a plain value would be.
|
|
501
|
+
// The constructors check their own operands, so this catches the one shape
|
|
502
|
+
// they cannot see: a non-number reaching arithmetic through coalesce,
|
|
503
|
+
// greatest, least or a case arm (`expr.add(expr.coalesce('x', 1), 2)`).
|
|
504
|
+
if (inArithmetic &&
|
|
505
|
+
(typeof value === 'string' || typeof value === 'boolean' || value instanceof Date || value instanceof Uint8Array)) {
|
|
506
|
+
throw new errors_js_1.ValidationError(`An arithmetic operand in ${state.env.operation} data on "${state.env.qi.table}.${state.env.targetKey}" is a ` +
|
|
507
|
+
`${value instanceof Date ? 'Date' : value instanceof Uint8Array ? 'Uint8Array' : typeof value}. ` +
|
|
508
|
+
'Arithmetic takes numbers, bigints, col() of a numeric column, or numeric expressions.');
|
|
509
|
+
}
|
|
510
|
+
// The class is part of the fingerprint: it decides PostgreSQL's cast on an
|
|
511
|
+
// arithmetic operand and MySQL's DIV, so two calls differing only in it must
|
|
512
|
+
// not share a cached statement.
|
|
513
|
+
const cls = numClassOfValue(value);
|
|
514
|
+
const arithmetic = inArithmetic ? (cls === 'dec' ? 'dec' : 'int') : undefined;
|
|
515
|
+
return { sql: state.env.bind(value, arithmetic), fp: `?${cls}`, cls };
|
|
516
|
+
}
|
|
517
|
+
/** Resolve a field an operand names, refusing columns no expression may read. */
|
|
518
|
+
function resolveOperandColumn(state, field, how, inArithmetic) {
|
|
519
|
+
const { qi } = state.env;
|
|
520
|
+
const call = how === 'col' ? `col(${JSON.stringify(field)})` : `expr.excluded(${JSON.stringify(field)})`;
|
|
521
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, field);
|
|
522
|
+
if (column === undefined) {
|
|
523
|
+
throw new errors_js_1.ValidationError(`${call} in ${state.env.operation} data on "${qi.table}" names no column. ` +
|
|
524
|
+
`Known fields: ${Object.keys(qi.tableMeta.columnMap).join(', ') || '(none)'}.`);
|
|
525
|
+
}
|
|
526
|
+
const cls = columnClass(columnType(qi, column));
|
|
527
|
+
if (cls === 'json' || cls === 'array') {
|
|
528
|
+
throw new errors_js_1.ValidationError(`${call} on "${qi.table}" reads ${cls === 'json' ? 'a json/jsonb' : 'an array'} column; expressions over json and ` +
|
|
529
|
+
'array columns are not supported.');
|
|
530
|
+
}
|
|
531
|
+
if (inArithmetic && cls !== 'numeric' && cls !== 'unknown') {
|
|
532
|
+
throw new errors_js_1.ValidationError(`${call} on "${qi.table}" is used in arithmetic, but "${field}" is not a numeric column ` +
|
|
533
|
+
`(${columnType(qi, column)}). Arithmetic takes numeric columns only.`);
|
|
534
|
+
}
|
|
535
|
+
return column;
|
|
536
|
+
}
|
|
537
|
+
const ARITHMETIC_SQL = { add: '+', sub: '-', mul: '*', div: '/' };
|
|
538
|
+
function walkNode(state, node, inArithmetic, depth) {
|
|
539
|
+
const { env } = state;
|
|
540
|
+
const { qi } = env;
|
|
541
|
+
switch (node.kind) {
|
|
542
|
+
case 'add':
|
|
543
|
+
case 'sub':
|
|
544
|
+
case 'mul':
|
|
545
|
+
case 'div': {
|
|
546
|
+
const cls = columnClass(state.targetType);
|
|
547
|
+
if (!inArithmetic && cls !== 'numeric' && cls !== 'unknown') {
|
|
548
|
+
throw new errors_js_1.ValidationError(`expr.${node.kind}() in ${env.operation} data on "${qi.table}.${env.targetKey}": arithmetic needs a numeric ` +
|
|
549
|
+
`column, and this one is ${state.targetType}.`);
|
|
550
|
+
}
|
|
551
|
+
const [a, b] = node.args.map((arg) => walkOperand(state, arg, true, depth + 1));
|
|
552
|
+
const result = combineClasses([a.cls, b.cls]);
|
|
553
|
+
// Integer division is the engine's truncating operator: DIV on MySQL,
|
|
554
|
+
// whose `/` is decimal division even between integers (truncating
|
|
555
|
+
// toward zero, like PostgreSQL's, SQLite's and SQL Server's `/`).
|
|
556
|
+
const op = node.kind === 'div' && result === 'int' ? qi.dialect.capabilities.integerDivision : ARITHMETIC_SQL[node.kind];
|
|
557
|
+
return { sql: `(${a.sql} ${op} ${b.sql})`, fp: `${node.kind}(${a.fp},${b.fp})`, cls: result };
|
|
558
|
+
}
|
|
559
|
+
case 'coalesce': {
|
|
560
|
+
if (node.args.length > MIN_VARIADIC)
|
|
561
|
+
qi.markVariableArity();
|
|
562
|
+
const parts = node.args.map((arg) => walkOperand(state, arg, inArithmetic, depth + 1));
|
|
563
|
+
return {
|
|
564
|
+
sql: `COALESCE(${parts.map((p) => p.sql).join(', ')})`,
|
|
565
|
+
fp: `coalesce(${parts.map((p) => p.fp).join(',')})`,
|
|
566
|
+
cls: combineClasses(parts.map((p) => p.cls)),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
case 'greatest':
|
|
570
|
+
case 'least': {
|
|
571
|
+
if (node.args.length > MIN_VARIADIC)
|
|
572
|
+
qi.markVariableArity();
|
|
573
|
+
const parts = node.args.map((arg) => walkOperand(state, arg, inArithmetic, depth + 1));
|
|
574
|
+
return {
|
|
575
|
+
sql: renderExtreme(qi, node.kind, parts.map((p) => p.sql)),
|
|
576
|
+
fp: `${node.kind}(${parts.map((p) => p.fp).join(',')})`,
|
|
577
|
+
cls: combineClasses(parts.map((p) => p.cls)),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
case 'case':
|
|
581
|
+
return walkCase(state, node, inArithmetic, depth);
|
|
582
|
+
case 'now': {
|
|
583
|
+
if (inArithmetic) {
|
|
584
|
+
throw new errors_js_1.ValidationError('expr.now() cannot be an arithmetic operand: date arithmetic is not supported.');
|
|
585
|
+
}
|
|
586
|
+
const rendered = renderNow(state);
|
|
587
|
+
return { sql: rendered, fp: `now:${rendered}`, cls: 'other' };
|
|
588
|
+
}
|
|
589
|
+
case 'excluded': {
|
|
590
|
+
const field = node.field;
|
|
591
|
+
if (!env.excluded) {
|
|
592
|
+
throw new errors_js_1.ValidationError(`expr.excluded(${JSON.stringify(field)}) names the incoming row of an upsert (upsert's update, or ` +
|
|
593
|
+
`createMany's onConflict.update); it has no meaning in ${env.operation} data.`);
|
|
594
|
+
}
|
|
595
|
+
// Depth 0 is the whole assigned value: a bare copy, which assertExprTarget
|
|
596
|
+
// has already allowed on a same-typed json or array column.
|
|
597
|
+
const column = depth === 0 && isBareStructuredCopy(qi, assertExprTarget(qi, env.targetKey, node, env.operation), node)
|
|
598
|
+
? (0, utils_js_1.resolveColumnName)(qi.tableMeta, field)
|
|
599
|
+
: resolveOperandColumn(state, field, 'excluded', inArithmetic);
|
|
600
|
+
if (env.insertColumns && !env.insertColumns.has(column)) {
|
|
601
|
+
throw new errors_js_1.ValidationError(`expr.excluded(${JSON.stringify(field)}) on "${qi.table}" names a field the incoming rows do not supply. ` +
|
|
602
|
+
'Add it to every row of data, or read the existing value with col() instead.');
|
|
603
|
+
}
|
|
604
|
+
return { sql: env.excluded(column), fp: `x:${column}`, cls: numClassOfType(columnType(qi, column)) };
|
|
605
|
+
}
|
|
606
|
+
default:
|
|
607
|
+
throw new errors_js_1.ValidationError(`Unknown expression kind "${String(node.kind)}".`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function walkCase(state, node, inArithmetic, depth) {
|
|
611
|
+
const { env } = state;
|
|
612
|
+
const { qi } = env;
|
|
613
|
+
// The arm count is the caller's array length, written into the SQL text, so
|
|
614
|
+
// the statement is not a fixed shape (see BuilderCtx.markVariableArity).
|
|
615
|
+
qi.markVariableArity();
|
|
616
|
+
const sqlParts = [];
|
|
617
|
+
const fpParts = [];
|
|
618
|
+
const classes = [];
|
|
619
|
+
node.args.forEach((arm, i) => {
|
|
620
|
+
const when = caseWhen(state, arm.when, i);
|
|
621
|
+
const predicate = env.predicate(when);
|
|
622
|
+
const then = walkOperand(state, arm.then, inArithmetic, depth + 1);
|
|
623
|
+
sqlParts.push(`WHEN ${predicate} THEN ${then.sql}`);
|
|
624
|
+
classes.push(then.cls);
|
|
625
|
+
fpParts.push(`w:${(0, where_js_1.fingerprintWhere)(qi, when)}=>${then.fp}`);
|
|
626
|
+
});
|
|
627
|
+
const hasElse = node.else !== undefined;
|
|
628
|
+
const otherwise = hasElse ? walkOperand(state, node.else, inArithmetic, depth + 1) : undefined;
|
|
629
|
+
if (otherwise)
|
|
630
|
+
classes.push(otherwise.cls);
|
|
631
|
+
return {
|
|
632
|
+
sql: `CASE ${sqlParts.join(' ')}${otherwise ? ` ELSE ${otherwise.sql}` : ''} END`,
|
|
633
|
+
fp: `case(${fpParts.join(';')}|e:${otherwise ? otherwise.fp : '-'})`,
|
|
634
|
+
cls: combineClasses(classes),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Prune and validate one arm's `when`: `undefined` keys drop out (the C1 rule
|
|
639
|
+
* for every `where` position), an arm left with no predicate is refused, and
|
|
640
|
+
* relation filters are refused. The columns it reads are recorded.
|
|
641
|
+
*/
|
|
642
|
+
function caseWhen(state, when, index) {
|
|
643
|
+
const { qi } = state.env;
|
|
644
|
+
const pruned = (0, normalize_args_js_1.pruneUndefinedArgs)(qi.schema, qi.table, { where: when }).where;
|
|
645
|
+
if (!(0, filters_js_1.isPlainRecord)(pruned) || Object.keys(pruned).length === 0) {
|
|
646
|
+
throw new errors_js_1.ValidationError(`expr.case(): arm ${index + 1}'s when on "${qi.table}" has no predicate${pruned === when ? '' : ' (every key was undefined)'}. Every arm needs a condition; use the second argument of expr.case() for the fallback value.`);
|
|
647
|
+
}
|
|
648
|
+
collectPredicateReads(state, pruned, 0);
|
|
649
|
+
return pruned;
|
|
650
|
+
}
|
|
651
|
+
function collectPredicateReads(state, where, depth) {
|
|
652
|
+
const { qi } = state.env;
|
|
653
|
+
if (depth > MAX_EXPR_DEPTH) {
|
|
654
|
+
throw new errors_js_1.ValidationError(`An expr.case() predicate is nested more than ${MAX_EXPR_DEPTH} levels deep.`);
|
|
655
|
+
}
|
|
656
|
+
for (const event of (0, where_compile_js_1.walkWhere)(qi.whereHost, where)) {
|
|
657
|
+
switch (event.kind) {
|
|
658
|
+
case 'or':
|
|
659
|
+
case 'and':
|
|
660
|
+
for (const c of event.conditions)
|
|
661
|
+
collectPredicateReads(state, c, depth + 1);
|
|
662
|
+
break;
|
|
663
|
+
case 'not':
|
|
664
|
+
collectPredicateReads(state, event.condition, depth + 1);
|
|
665
|
+
break;
|
|
666
|
+
case 'relation':
|
|
667
|
+
throw new errors_js_1.ValidationError(`expr.case() on "${qi.table}": "${event.key}" is a relation filter. A case arm's when takes predicates ` +
|
|
668
|
+
"on the row's own columns only.");
|
|
669
|
+
case 'scalar': {
|
|
670
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, event.key);
|
|
671
|
+
if (column !== undefined)
|
|
672
|
+
state.reads.add(column);
|
|
673
|
+
collectRefReads(state, event.value, 0);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** Record the columns `col()` references inside an operator object read. */
|
|
680
|
+
function collectRefReads(state, value, depth) {
|
|
681
|
+
if (depth > 4)
|
|
682
|
+
return;
|
|
683
|
+
if ((0, column_ref_js_1.isColumnRef)(value)) {
|
|
684
|
+
const column = (0, utils_js_1.resolveColumnName)(state.env.qi.tableMeta, value.col);
|
|
685
|
+
if (column !== undefined)
|
|
686
|
+
state.reads.add(column);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if ((0, filters_js_1.isPlainRecord)(value))
|
|
690
|
+
for (const v of Object.values(value))
|
|
691
|
+
collectRefReads(state, v, depth + 1);
|
|
692
|
+
}
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
// Per-engine rendering
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
/**
|
|
697
|
+
* GREATEST / LEAST that ignore NULLs on every engine. PostgreSQL's ignore NULL
|
|
698
|
+
* arguments natively. MySQL's and SQLite's return NULL when ANY argument is
|
|
699
|
+
* NULL, so each position i becomes COALESCE(arg_i, <the others>): when any
|
|
700
|
+
* argument is non-NULL every position is non-NULL and each non-NULL argument
|
|
701
|
+
* still appears as itself in its own position, so the extreme is the extreme of
|
|
702
|
+
* the non-NULL arguments; when all are NULL the result is NULL. SQL Server
|
|
703
|
+
* before 2022 has no GREATEST, and MAX over a VALUES list ignores NULLs.
|
|
704
|
+
*
|
|
705
|
+
* Each argument was compiled once, so a bound value repeated across positions
|
|
706
|
+
* reuses ONE placeholder (every engine here binds by number or by name).
|
|
707
|
+
*/
|
|
708
|
+
function renderExtreme(qi, kind, args) {
|
|
709
|
+
switch (qi.dialect.name) {
|
|
710
|
+
case 'postgresql':
|
|
711
|
+
return `${kind === 'greatest' ? 'GREATEST' : 'LEAST'}(${args.join(', ')})`;
|
|
712
|
+
case 'mssql':
|
|
713
|
+
return (`(SELECT ${kind === 'greatest' ? 'MAX' : 'MIN'}(turbine_v) FROM ` +
|
|
714
|
+
`(VALUES ${args.map((a) => `(${a})`).join(', ')}) AS turbine_ext(turbine_v))`);
|
|
715
|
+
case 'mysql':
|
|
716
|
+
case 'sqlite': {
|
|
717
|
+
const fn = qi.dialect.name === 'sqlite' ? (kind === 'greatest' ? 'max' : 'min') : kind.toUpperCase();
|
|
718
|
+
const positions = args.map((a, i) => `COALESCE(${[a, ...args.filter((_, j) => j !== i)].join(', ')})`);
|
|
719
|
+
return `${fn}(${positions.join(', ')})`;
|
|
720
|
+
}
|
|
721
|
+
default:
|
|
722
|
+
throw unspelledExpression(qi, `expr.${kind}()`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* The server's current time for the target column, in the convention the read
|
|
727
|
+
* path uses. Zone-less `timestamp` / `date` columns take the UTC wall clock
|
|
728
|
+
* (PostgreSQL with `utcTimestamps: false` takes the session's local time, which
|
|
729
|
+
* is what that setting reads back), `timestamptz` takes the instant, and on
|
|
730
|
+
* SQLite, where Turbine binds a Date as ISO-8601 UTC text, the same text (the
|
|
731
|
+
* `YYYY-MM-DD` UTC day on a date column).
|
|
732
|
+
*/
|
|
733
|
+
function renderNow(state) {
|
|
734
|
+
const { qi, targetKey, operation } = state.env;
|
|
735
|
+
const kind = temporalKind(state.targetType);
|
|
736
|
+
if (kind === undefined || kind === 'time') {
|
|
737
|
+
throw new errors_js_1.ValidationError(`expr.now() in ${operation} data cannot target "${qi.table}.${targetKey}"` +
|
|
738
|
+
(state.targetType === undefined ? ': the column has no known type. ' : ` (${state.targetType}): `) +
|
|
739
|
+
'it fits timestamp, timestamptz, datetime and date columns only.');
|
|
740
|
+
}
|
|
741
|
+
const utc = qi.utcTimestamps !== false;
|
|
742
|
+
switch (qi.dialect.name) {
|
|
743
|
+
case 'postgresql':
|
|
744
|
+
if (kind === 'tz')
|
|
745
|
+
return 'now()';
|
|
746
|
+
if (kind === 'date')
|
|
747
|
+
return utc ? "(now() AT TIME ZONE 'UTC')::date" : 'CURRENT_DATE';
|
|
748
|
+
return utc ? "(now() AT TIME ZONE 'UTC')" : 'LOCALTIMESTAMP';
|
|
749
|
+
case 'mysql':
|
|
750
|
+
return kind === 'date' ? 'UTC_DATE()' : 'UTC_TIMESTAMP(3)';
|
|
751
|
+
case 'sqlite':
|
|
752
|
+
// The text a bound Date takes on SQLite: the UTC day on a date column.
|
|
753
|
+
return kind === 'date' ? "strftime('%Y-%m-%d', 'now')" : "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
754
|
+
case 'mssql':
|
|
755
|
+
if (kind === 'tz')
|
|
756
|
+
return 'SYSDATETIMEOFFSET()';
|
|
757
|
+
if (kind === 'date')
|
|
758
|
+
return 'CAST(SYSUTCDATETIME() AS date)';
|
|
759
|
+
return 'SYSUTCDATETIME()';
|
|
760
|
+
default:
|
|
761
|
+
throw unspelledExpression(qi, 'expr.now()');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* GREATEST / LEAST and now() are still spelled per engine NAME (until they
|
|
766
|
+
* become dialect hooks), so a dialect under any other name is refused rather
|
|
767
|
+
* than handed one engine's spelling as a fallback. That includes a dialect
|
|
768
|
+
* spread from a shipped engine dialect under a new name: its capabilities say
|
|
769
|
+
* it takes update expressions, but nothing here knows how it spells these
|
|
770
|
+
* three. Every other expression is rendered from capabilities.
|
|
771
|
+
*/
|
|
772
|
+
function unspelledExpression(qi, what) {
|
|
773
|
+
return new errors_js_1.UnsupportedFeatureError(`${what} in update data`, qi.dialect.name, `${what} is spelled per engine, and only postgresql, mysql, sqlite and mssql have a spelling. ` +
|
|
774
|
+
'Compute the value in JavaScript and write it as a plain value.');
|
|
775
|
+
}
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// Single-row upsert: `excluded` is `create`'s value
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
/**
|
|
780
|
+
* Rewrite every `expr.excluded(f)` in a single-row upsert's `update` to the
|
|
781
|
+
* value `create` supplies for `f`. The incoming row of a one-row upsert IS
|
|
782
|
+
* `create`, so the statement path, the lookup-first path (a plain update) and
|
|
783
|
+
* a nested upsert store the same row. A top-level reference becomes
|
|
784
|
+
* `{ set: value }`, which writes the value exactly as a plain value would be
|
|
785
|
+
* written, on every column type (json and array columns take `set` too). A
|
|
786
|
+
* reference inside an expression becomes a value operand, checked as the
|
|
787
|
+
* node's constructor checks one; the expression is then compiled by the
|
|
788
|
+
* update, with the column-type rules every expression gets. Needs only the
|
|
789
|
+
* table's name and metadata, so the nested-write engine shares it. Returns
|
|
790
|
+
* `update` by reference when it holds no reference.
|
|
791
|
+
*/
|
|
792
|
+
function resolveExcludedFromCreate(target, update, create) {
|
|
793
|
+
if (!update || !Object.values(update).some(holdsExcluded))
|
|
794
|
+
return update;
|
|
795
|
+
const createByColumn = new Map();
|
|
796
|
+
for (const [key, value] of Object.entries(create ?? {})) {
|
|
797
|
+
if (value === undefined)
|
|
798
|
+
continue;
|
|
799
|
+
const column = (0, utils_js_1.resolveColumnName)(target.tableMeta, key);
|
|
800
|
+
if (column !== undefined)
|
|
801
|
+
createByColumn.set(column, value);
|
|
802
|
+
}
|
|
803
|
+
const incomingValue = (ref) => {
|
|
804
|
+
const column = (0, utils_js_1.resolveColumnName)(target.tableMeta, ref.field);
|
|
805
|
+
if (column === undefined || !createByColumn.has(column)) {
|
|
806
|
+
throw new errors_js_1.ValidationError(`expr.excluded(${JSON.stringify(ref.field)}) in upsert's update on "${target.table}" names a field ` +
|
|
807
|
+
'`create` does not supply. It reads the incoming row, which is `create`.');
|
|
808
|
+
}
|
|
809
|
+
return createByColumn.get(column);
|
|
810
|
+
};
|
|
811
|
+
const out = {};
|
|
812
|
+
for (const [key, value] of Object.entries(update)) {
|
|
813
|
+
if (!holdsExcluded(value)) {
|
|
814
|
+
out[key] = value;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
out[key] = isExcludedRef(value)
|
|
818
|
+
? { set: incomingValue(value) }
|
|
819
|
+
: replaceExcluded(value, incomingValue, 0);
|
|
820
|
+
}
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
function holdsExcluded(value, depth = 0) {
|
|
824
|
+
if (depth > MAX_EXPR_DEPTH || !isSqlExpr(value))
|
|
825
|
+
return false;
|
|
826
|
+
if (isExcludedRef(value))
|
|
827
|
+
return true;
|
|
828
|
+
if (value.kind === 'case') {
|
|
829
|
+
const node = value;
|
|
830
|
+
return node.args.some((arm) => holdsExcluded(arm.then, depth + 1)) || holdsExcluded(node.else, depth + 1);
|
|
831
|
+
}
|
|
832
|
+
return value.args.some((arg) => holdsExcluded(arg, depth + 1));
|
|
833
|
+
}
|
|
834
|
+
function replaceExcluded(node, incomingValue, depth) {
|
|
835
|
+
if (depth > MAX_EXPR_DEPTH) {
|
|
836
|
+
throw new errors_js_1.ValidationError(`An update expression is nested more than ${MAX_EXPR_DEPTH} levels deep.`);
|
|
837
|
+
}
|
|
838
|
+
// The value `create` supplies takes the reference's place, so it must pass
|
|
839
|
+
// the check the node's constructor applies to an operand in that position.
|
|
840
|
+
const arithmeticNode = node.kind === 'add' || node.kind === 'sub' || node.kind === 'mul' || node.kind === 'div';
|
|
841
|
+
const swap = (operand, position) => {
|
|
842
|
+
if (isExcludedRef(operand)) {
|
|
843
|
+
const value = incomingValue(operand);
|
|
844
|
+
if (arithmeticNode)
|
|
845
|
+
assertNumericOperand(node.kind, position, value);
|
|
846
|
+
else
|
|
847
|
+
assertOperand(node.kind, position, value);
|
|
848
|
+
return value;
|
|
849
|
+
}
|
|
850
|
+
return isSqlExpr(operand) ? replaceExcluded(operand, incomingValue, depth + 1) : operand;
|
|
851
|
+
};
|
|
852
|
+
if (node.kind === 'case') {
|
|
853
|
+
const c = node;
|
|
854
|
+
// biome-ignore lint/suspicious/noThenProperty: SQL's CASE WHEN ... THEN; a value operand, never a function
|
|
855
|
+
const arms = c.args.map((arm, i) => Object.freeze({ when: arm.when, then: swap(arm.then, i) }));
|
|
856
|
+
return makeNode('case', arms, c.else === undefined ? undefined : { else: swap(c.else, arms.length) });
|
|
857
|
+
}
|
|
858
|
+
return makeNode(node.kind, node.args.map(swap));
|
|
859
|
+
}
|