turbine-orm 0.49.0 → 0.50.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 +122 -39
- package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
- package/dist/cjs/adapters/index.d.ts +110 -0
- package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
- package/dist/cjs/cli/config.d.ts +181 -0
- package/dist/cjs/cli/config.js +32 -6
- package/dist/cjs/cli/destructive.d.ts +38 -0
- package/dist/cjs/cli/index.d.ts +359 -0
- package/dist/cjs/cli/index.js +228 -56
- package/dist/cjs/cli/loader.d.ts +61 -0
- package/dist/cjs/cli/mcp.d.ts +42 -0
- package/dist/cjs/cli/migrate.d.ts +356 -0
- package/dist/cjs/cli/migrate.js +131 -40
- package/dist/cjs/cli/observe-ui.d.ts +1 -0
- package/dist/cjs/cli/observe-ui.js +14 -5
- package/dist/cjs/cli/observe.d.ts +25 -0
- package/dist/cjs/cli/observe.js +49 -12
- package/dist/cjs/cli/pii-tags.d.ts +53 -0
- package/dist/cjs/cli/prisma-report.d.ts +33 -0
- package/dist/cjs/cli/prisma-report.js +73 -0
- package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
- package/dist/cjs/cli/prisma-resolve.js +1 -0
- package/dist/cjs/cli/prisma-schema.d.ts +176 -0
- package/dist/cjs/cli/prisma-schema.js +82 -4
- package/dist/cjs/cli/rate-limit.d.ts +32 -0
- package/dist/cjs/cli/rate-limit.js +45 -0
- package/dist/cjs/cli/studio-demo.d.ts +43 -0
- package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
- package/dist/cjs/cli/studio.d.ts +207 -0
- package/dist/cjs/cli/studio.js +136 -71
- package/dist/cjs/cli/ui.d.ts +73 -0
- package/dist/cjs/cli/ui.js +51 -9
- package/dist/cjs/client.d.ts +837 -0
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/dialect.d.ts +516 -0
- package/dist/cjs/dialect.js +37 -12
- package/dist/cjs/errors.d.ts +370 -0
- package/dist/cjs/generate.d.ts +137 -0
- package/dist/cjs/generate.js +39 -6
- package/dist/cjs/index-advisor.d.ts +153 -0
- package/dist/cjs/index-stats.d.ts +384 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/introspect.d.ts +269 -0
- package/dist/cjs/mssql.d.ts +232 -0
- package/dist/cjs/mssql.js +6 -0
- package/dist/cjs/mysql.d.ts +173 -0
- package/dist/cjs/mysql.js +16 -0
- package/dist/cjs/nested-write.d.ts +96 -0
- package/dist/cjs/nested-write.js +414 -24
- package/dist/cjs/observe.d.ts +115 -0
- package/dist/cjs/optional-peer-import.d.cts +72 -0
- package/dist/cjs/pipeline-submittable.d.ts +93 -0
- package/dist/cjs/pipeline.d.ts +71 -0
- package/dist/cjs/powdb-introspect.d.ts +84 -0
- package/dist/cjs/powdb.d.ts +931 -0
- package/dist/cjs/powdb.js +106 -21
- package/dist/cjs/powql.d.ts +592 -0
- package/dist/cjs/powql.js +42 -6
- package/dist/cjs/prisma-compat.d.ts +283 -0
- package/dist/cjs/prisma-compat.js +167 -9
- package/dist/cjs/query/aggregates.d.ts +92 -0
- package/dist/cjs/query/aggregates.js +7 -3
- package/dist/cjs/query/batched-loader.d.ts +193 -0
- package/dist/cjs/query/builder.d.ts +849 -0
- package/dist/cjs/query/builder.js +571 -65
- package/dist/cjs/query/compound-unique.d.ts +51 -0
- package/dist/cjs/query/deferred.d.ts +223 -0
- package/dist/cjs/query/filters.d.ts +201 -0
- package/dist/cjs/query/index.d.ts +14 -0
- package/dist/cjs/query/index.js +6 -1
- package/dist/cjs/query/relations.d.ts +609 -0
- package/dist/cjs/query/relations.js +693 -46
- package/dist/cjs/query/types.d.ts +1300 -0
- package/dist/cjs/query/utils.d.ts +209 -0
- package/dist/cjs/query/utils.js +208 -1
- package/dist/cjs/query/warn-registry.d.ts +68 -0
- package/dist/cjs/query/warn-registry.js +9 -0
- package/dist/cjs/query/where-compile.d.ts +139 -0
- package/dist/cjs/query/where.d.ts +548 -0
- package/dist/cjs/query/where.js +58 -22
- package/dist/cjs/query/writes.d.ts +172 -0
- package/dist/cjs/query/writes.js +105 -12
- package/dist/cjs/realtime.d.ts +70 -0
- package/dist/cjs/schema-builder.d.ts +354 -0
- package/dist/cjs/schema-metadata.d.ts +83 -0
- package/dist/cjs/schema-sql.d.ts +217 -0
- package/dist/cjs/schema-sql.js +23 -5
- package/dist/cjs/schema.d.ts +356 -0
- package/dist/cjs/schema.js +125 -0
- package/dist/cjs/seed.d.ts +15 -0
- package/dist/cjs/serverless.d.ts +142 -0
- package/dist/cjs/sqlite.d.ts +143 -0
- package/dist/cjs/sqlite.js +4 -0
- package/dist/cjs/typed-sql.d.ts +102 -0
- package/dist/cli/config.d.ts +18 -4
- package/dist/cli/config.js +31 -6
- package/dist/cli/index.d.ts +123 -0
- package/dist/cli/index.js +223 -58
- package/dist/cli/migrate.d.ts +59 -10
- package/dist/cli/migrate.js +128 -41
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +14 -5
- package/dist/cli/observe.d.ts +7 -1
- package/dist/cli/observe.js +48 -12
- package/dist/cli/prisma-report.d.ts +14 -0
- package/dist/cli/prisma-report.js +72 -0
- package/dist/cli/prisma-resolve.d.ts +6 -0
- package/dist/cli/prisma-resolve.js +1 -0
- package/dist/cli/prisma-schema.d.ts +62 -2
- package/dist/cli/prisma-schema.js +81 -4
- package/dist/cli/rate-limit.d.ts +32 -0
- package/dist/cli/rate-limit.js +40 -0
- package/dist/cli/studio.d.ts +5 -5
- package/dist/cli/studio.js +135 -70
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +51 -9
- package/dist/client.d.ts +40 -0
- package/dist/client.js +3 -0
- package/dist/dialect.d.ts +17 -1
- package/dist/dialect.js +37 -12
- package/dist/generate.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +6 -0
- package/dist/mysql.js +16 -0
- package/dist/nested-write.d.ts +2 -0
- package/dist/nested-write.js +415 -25
- package/dist/powdb.d.ts +4 -2
- package/dist/powdb.js +106 -21
- package/dist/powql.d.ts +5 -0
- package/dist/powql.js +42 -6
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +166 -8
- package/dist/query/aggregates.js +7 -3
- package/dist/query/builder.d.ts +292 -21
- package/dist/query/builder.js +570 -64
- package/dist/query/deferred.d.ts +39 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +173 -5
- package/dist/query/relations.js +688 -47
- package/dist/query/types.d.ts +123 -39
- package/dist/query/utils.d.ts +116 -0
- package/dist/query/utils.js +198 -0
- package/dist/query/warn-registry.d.ts +9 -0
- package/dist/query/warn-registry.js +9 -0
- package/dist/query/where.d.ts +38 -1
- package/dist/query/where.js +58 -23
- package/dist/query/writes.d.ts +42 -1
- package/dist/query/writes.js +104 -13
- package/dist/schema-sql.d.ts +14 -0
- package/dist/schema-sql.js +23 -5
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +123 -0
- package/dist/sqlite.js +4 -0
- package/package.json +77 -28
package/dist/query/where.d.ts
CHANGED
|
@@ -35,6 +35,14 @@ export interface BuilderCtx {
|
|
|
35
35
|
readonly scopedHostCache: Map<string, WhereHost>;
|
|
36
36
|
readonly columnPgTypeMap: Map<string, string>;
|
|
37
37
|
readonly columnArrayTypeMap: Map<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* The client's `utcTimestamps` setting, when the owning QueryInterface
|
|
40
|
+
* supplies it. Optional so an older ctx literal (and every test that builds
|
|
41
|
+
* one by hand) keeps the default. `false` opts out of BOTH the UTC read
|
|
42
|
+
* parsing and the symmetric UTC bind rewriting for zone-less `date` /
|
|
43
|
+
* `timestamp` columns (see `coerceWriteValue` in writes.ts).
|
|
44
|
+
*/
|
|
45
|
+
readonly utcTimestamps?: boolean;
|
|
38
46
|
readonly crossSchemaTypeColumns: Set<string>;
|
|
39
47
|
/**
|
|
40
48
|
* The active query's `skipGlobalFilters` opt-out. A live getter/setter over
|
|
@@ -62,7 +70,13 @@ export interface BuilderCtx {
|
|
|
62
70
|
readonly camelDateFieldCache: Map<string, Set<string>>;
|
|
63
71
|
limitOneClause(): string;
|
|
64
72
|
buildPagination(limitPh: string | undefined, offsetPh: string | undefined, hasOrderBy: boolean): string;
|
|
65
|
-
paginationRef(value: unknown, params: unknown[]): string;
|
|
73
|
+
paginationRef(value: unknown, params: unknown[], arg?: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Coerce + validate a LIMIT/OFFSET argument (non-negative safe integer).
|
|
76
|
+
* The cache-hit param-collect paths call it directly, so a warmed template
|
|
77
|
+
* can never bind an unvalidated NaN (which Postgres reads as "no limit").
|
|
78
|
+
*/
|
|
79
|
+
paginationValue(value: unknown, arg?: string): number;
|
|
66
80
|
}
|
|
67
81
|
/**
|
|
68
82
|
* Column-reference resolution context threaded into
|
|
@@ -76,6 +90,13 @@ interface ColumnRefContext {
|
|
|
76
90
|
meta: TableMetadata;
|
|
77
91
|
table: string;
|
|
78
92
|
prefix: string;
|
|
93
|
+
/**
|
|
94
|
+
* The operator's own RAW (unquoted) column name. The `column` argument the
|
|
95
|
+
* operator builders receive is already quoted on the build side and raw on
|
|
96
|
+
* the collect side, so the temporal bind rewrite resolves the column's type
|
|
97
|
+
* from here instead — the one value both sides pass identically.
|
|
98
|
+
*/
|
|
99
|
+
rawColumn: string;
|
|
79
100
|
}
|
|
80
101
|
/**
|
|
81
102
|
* A table-scoped WHERE compilation context for a sub-where that is NOT the
|
|
@@ -310,6 +331,22 @@ export declare function buildSubWhereForRelation(qi: BuilderCtx, targetTable: st
|
|
|
310
331
|
* (relation targets, not just `qi.table`).
|
|
311
332
|
*/
|
|
312
333
|
export declare function pgTypeForColumn(_qi: BuilderCtx, meta: TableMetadata, column: string): string;
|
|
334
|
+
/**
|
|
335
|
+
* Rewrite a WHERE operand bound against `column` the same way the write path
|
|
336
|
+
* rewrites a `data` value ({@link coerceTemporalValue}): a JS `Date` on a
|
|
337
|
+
* `time` / `timetz` column becomes a time-of-day literal (Postgres otherwise
|
|
338
|
+
* answers `22007 invalid input syntax for type time` for the ISO timestamp the
|
|
339
|
+
* driver would send), and on a zone-less `date` / `timestamp` column it becomes
|
|
340
|
+
* the UTC-component literal, so a predicate matches the value a write of the
|
|
341
|
+
* same `Date` stored.
|
|
342
|
+
*
|
|
343
|
+
* This is a VALUE transform only — it never changes the emitted SQL — so the
|
|
344
|
+
* SQL-template cache is unaffected, and it is applied on the cache-hit
|
|
345
|
+
* param-collect path as well as the build path.
|
|
346
|
+
*
|
|
347
|
+
* `timestamptz` and every non-temporal column are returned by identity.
|
|
348
|
+
*/
|
|
349
|
+
export declare function coerceWhereOperand(qi: BuilderCtx, meta: TableMetadata, column: string, value: unknown): unknown;
|
|
313
350
|
/**
|
|
314
351
|
* The Postgres enum type name for a column, when the schema knows one.
|
|
315
352
|
*
|
package/dist/query/where.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake, normalizeKeyColumns } from '../schema.js';
|
|
15
15
|
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_RANGE_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
16
|
-
import { escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
|
|
16
|
+
import { coerceTemporalValue, escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
|
|
17
17
|
import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
|
|
18
18
|
/**
|
|
19
19
|
* Produce a value-invariant fingerprint of a where clause.
|
|
@@ -158,6 +158,7 @@ export function collectScalarParams(qi, key, value, params) {
|
|
|
158
158
|
meta: qi.tableMeta,
|
|
159
159
|
table: qi.table,
|
|
160
160
|
prefix: '',
|
|
161
|
+
rawColumn,
|
|
161
162
|
});
|
|
162
163
|
return;
|
|
163
164
|
default:
|
|
@@ -165,7 +166,7 @@ export function collectScalarParams(qi, key, value, params) {
|
|
|
165
166
|
// the build path, so a cache hit can never silently bind a
|
|
166
167
|
// misspelled-operator object.
|
|
167
168
|
assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
|
|
168
|
-
params.push(value);
|
|
169
|
+
params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
|
|
169
170
|
return;
|
|
170
171
|
}
|
|
171
172
|
}
|
|
@@ -228,24 +229,26 @@ export function collectOperatorParams(qi, column, op, params, refCtx) {
|
|
|
228
229
|
resolveColumnRef(qi, v, refCtx, op.mode);
|
|
229
230
|
return true;
|
|
230
231
|
};
|
|
232
|
+
// Mirrors buildOperatorClauses' temporal bind rewrite exactly.
|
|
233
|
+
const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
|
|
231
234
|
if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
|
|
232
235
|
assertBindableEqualsOperand(op.equals, `"${column}"`);
|
|
233
|
-
params.push(op.equals);
|
|
236
|
+
params.push(cv(op.equals));
|
|
234
237
|
}
|
|
235
238
|
if (op.gt !== undefined && !skipRef(op.gt))
|
|
236
|
-
params.push(op.gt);
|
|
239
|
+
params.push(cv(op.gt));
|
|
237
240
|
if (op.gte !== undefined && !skipRef(op.gte))
|
|
238
|
-
params.push(op.gte);
|
|
241
|
+
params.push(cv(op.gte));
|
|
239
242
|
if (op.lt !== undefined && !skipRef(op.lt))
|
|
240
|
-
params.push(op.lt);
|
|
243
|
+
params.push(cv(op.lt));
|
|
241
244
|
if (op.lte !== undefined && !skipRef(op.lte))
|
|
242
|
-
params.push(op.lte);
|
|
245
|
+
params.push(cv(op.lte));
|
|
243
246
|
if (op.not !== undefined && op.not !== null && !skipRef(op.not))
|
|
244
|
-
params.push(op.not);
|
|
247
|
+
params.push(cv(op.not));
|
|
245
248
|
if (op.in !== undefined)
|
|
246
|
-
params.push(qi.inParam(op.in));
|
|
249
|
+
params.push(qi.inParam(cv(op.in)));
|
|
247
250
|
if (op.notIn !== undefined)
|
|
248
|
-
params.push(qi.inParam(op.notIn));
|
|
251
|
+
params.push(qi.inParam(cv(op.notIn)));
|
|
249
252
|
if (op.contains !== undefined)
|
|
250
253
|
params.push(`%${escapeLike(op.contains)}%`);
|
|
251
254
|
if (op.startsWith !== undefined)
|
|
@@ -555,6 +558,7 @@ export function buildScalarClause(qi, key, value, params, andClauses) {
|
|
|
555
558
|
meta: qi.tableMeta,
|
|
556
559
|
table: qi.table,
|
|
557
560
|
prefix: '',
|
|
561
|
+
rawColumn,
|
|
558
562
|
}));
|
|
559
563
|
return;
|
|
560
564
|
default:
|
|
@@ -562,7 +566,7 @@ export function buildScalarClause(qi, key, value, params, andClauses) {
|
|
|
562
566
|
// is almost always a misspelled operator (`startWith` for `startsWith`);
|
|
563
567
|
// the guard also runs on the cache-hit param-collect path.
|
|
564
568
|
assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
|
|
565
|
-
params.push(value);
|
|
569
|
+
params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
|
|
566
570
|
andClauses.push(`${column} = ${qi.p(params.length)}`);
|
|
567
571
|
return;
|
|
568
572
|
}
|
|
@@ -704,11 +708,12 @@ export function buildScopedScalarClause(qi, scope, field, value, params, clauses
|
|
|
704
708
|
meta,
|
|
705
709
|
table: scope.table,
|
|
706
710
|
prefix: scope.qualifier,
|
|
711
|
+
rawColumn: col,
|
|
707
712
|
}));
|
|
708
713
|
return;
|
|
709
714
|
}
|
|
710
715
|
assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
|
|
711
|
-
params.push(value);
|
|
716
|
+
params.push(coerceWhereOperand(qi, meta, col, value));
|
|
712
717
|
clauses.push(`${qCol} = ${qi.p(params.length)}`);
|
|
713
718
|
}
|
|
714
719
|
/**
|
|
@@ -760,11 +765,11 @@ export function collectScopedScalarParams(qi, scope, field, value, params) {
|
|
|
760
765
|
}
|
|
761
766
|
}
|
|
762
767
|
if (isWhereOperator(value)) {
|
|
763
|
-
collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '' });
|
|
768
|
+
collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '', rawColumn: col });
|
|
764
769
|
return;
|
|
765
770
|
}
|
|
766
771
|
assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
|
|
767
|
-
params.push(value);
|
|
772
|
+
params.push(coerceWhereOperand(qi, meta, col, value));
|
|
768
773
|
}
|
|
769
774
|
/**
|
|
770
775
|
* Value-invariant fingerprint of a scoped sub-where. Same canonical
|
|
@@ -944,6 +949,30 @@ export function buildSubWhereForRelation(qi, targetTable, subWhere, params) {
|
|
|
944
949
|
export function pgTypeForColumn(_qi, meta, column) {
|
|
945
950
|
return meta.dialectTypes?.[column] ?? meta.pgTypes?.[column] ?? 'text';
|
|
946
951
|
}
|
|
952
|
+
/**
|
|
953
|
+
* Rewrite a WHERE operand bound against `column` the same way the write path
|
|
954
|
+
* rewrites a `data` value ({@link coerceTemporalValue}): a JS `Date` on a
|
|
955
|
+
* `time` / `timetz` column becomes a time-of-day literal (Postgres otherwise
|
|
956
|
+
* answers `22007 invalid input syntax for type time` for the ISO timestamp the
|
|
957
|
+
* driver would send), and on a zone-less `date` / `timestamp` column it becomes
|
|
958
|
+
* the UTC-component literal, so a predicate matches the value a write of the
|
|
959
|
+
* same `Date` stored.
|
|
960
|
+
*
|
|
961
|
+
* This is a VALUE transform only — it never changes the emitted SQL — so the
|
|
962
|
+
* SQL-template cache is unaffected, and it is applied on the cache-hit
|
|
963
|
+
* param-collect path as well as the build path.
|
|
964
|
+
*
|
|
965
|
+
* `timestamptz` and every non-temporal column are returned by identity.
|
|
966
|
+
*/
|
|
967
|
+
export function coerceWhereOperand(qi, meta, column, value) {
|
|
968
|
+
if (!(value instanceof Date) && !Array.isArray(value))
|
|
969
|
+
return value;
|
|
970
|
+
return coerceTemporalValue(pgTypeForColumn(qi, meta, column), value,
|
|
971
|
+
// Same PostgreSQL + `utcTimestamps` gate as the write path (see
|
|
972
|
+
// `utcDateTimeWrites` in writes.ts): only the read/write-symmetric engine
|
|
973
|
+
// gets the zone-less rewrite. Time-of-day always rewrites.
|
|
974
|
+
qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false);
|
|
975
|
+
}
|
|
947
976
|
/**
|
|
948
977
|
* The Postgres enum type name for a column, when the schema knows one.
|
|
949
978
|
*
|
|
@@ -1081,6 +1110,9 @@ export function columnRefSql(qi, ref, ctx, mode) {
|
|
|
1081
1110
|
*/
|
|
1082
1111
|
export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
1083
1112
|
const clauses = [];
|
|
1113
|
+
// Temporal bind rewrite, identical to `collectOperatorParams`. Value-only, so
|
|
1114
|
+
// the emitted SQL (and therefore the template cache) is untouched.
|
|
1115
|
+
const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
|
|
1084
1116
|
if (op.equals !== undefined) {
|
|
1085
1117
|
if (op.equals === null) {
|
|
1086
1118
|
clauses.push(`${column} IS NULL`);
|
|
@@ -1090,7 +1122,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1090
1122
|
}
|
|
1091
1123
|
else {
|
|
1092
1124
|
assertBindableEqualsOperand(op.equals, column);
|
|
1093
|
-
params.push(op.equals);
|
|
1125
|
+
params.push(cv(op.equals));
|
|
1094
1126
|
clauses.push(`${column} = ${qi.p(params.length)}`);
|
|
1095
1127
|
}
|
|
1096
1128
|
}
|
|
@@ -1099,7 +1131,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1099
1131
|
clauses.push(`${column} > ${columnRefSql(qi, op.gt, refCtx, op.mode)}`);
|
|
1100
1132
|
}
|
|
1101
1133
|
else {
|
|
1102
|
-
params.push(op.gt);
|
|
1134
|
+
params.push(cv(op.gt));
|
|
1103
1135
|
clauses.push(`${column} > ${qi.p(params.length)}`);
|
|
1104
1136
|
}
|
|
1105
1137
|
}
|
|
@@ -1108,7 +1140,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1108
1140
|
clauses.push(`${column} >= ${columnRefSql(qi, op.gte, refCtx, op.mode)}`);
|
|
1109
1141
|
}
|
|
1110
1142
|
else {
|
|
1111
|
-
params.push(op.gte);
|
|
1143
|
+
params.push(cv(op.gte));
|
|
1112
1144
|
clauses.push(`${column} >= ${qi.p(params.length)}`);
|
|
1113
1145
|
}
|
|
1114
1146
|
}
|
|
@@ -1117,7 +1149,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1117
1149
|
clauses.push(`${column} < ${columnRefSql(qi, op.lt, refCtx, op.mode)}`);
|
|
1118
1150
|
}
|
|
1119
1151
|
else {
|
|
1120
|
-
params.push(op.lt);
|
|
1152
|
+
params.push(cv(op.lt));
|
|
1121
1153
|
clauses.push(`${column} < ${qi.p(params.length)}`);
|
|
1122
1154
|
}
|
|
1123
1155
|
}
|
|
@@ -1126,7 +1158,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1126
1158
|
clauses.push(`${column} <= ${columnRefSql(qi, op.lte, refCtx, op.mode)}`);
|
|
1127
1159
|
}
|
|
1128
1160
|
else {
|
|
1129
|
-
params.push(op.lte);
|
|
1161
|
+
params.push(cv(op.lte));
|
|
1130
1162
|
clauses.push(`${column} <= ${qi.p(params.length)}`);
|
|
1131
1163
|
}
|
|
1132
1164
|
}
|
|
@@ -1138,16 +1170,16 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1138
1170
|
clauses.push(`${column} != ${columnRefSql(qi, op.not, refCtx, op.mode)}`);
|
|
1139
1171
|
}
|
|
1140
1172
|
else {
|
|
1141
|
-
params.push(op.not);
|
|
1173
|
+
params.push(cv(op.not));
|
|
1142
1174
|
clauses.push(`${column} != ${qi.p(params.length)}`);
|
|
1143
1175
|
}
|
|
1144
1176
|
}
|
|
1145
1177
|
if (op.in !== undefined) {
|
|
1146
|
-
params.push(qi.inParam(op.in));
|
|
1178
|
+
params.push(qi.inParam(cv(op.in)));
|
|
1147
1179
|
clauses.push(qi.inClause(column, qi.p(params.length), false));
|
|
1148
1180
|
}
|
|
1149
1181
|
if (op.notIn !== undefined) {
|
|
1150
|
-
params.push(qi.inParam(op.notIn));
|
|
1182
|
+
params.push(qi.inParam(cv(op.notIn)));
|
|
1151
1183
|
clauses.push(qi.inClause(column, qi.p(params.length), true));
|
|
1152
1184
|
}
|
|
1153
1185
|
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
@@ -1207,7 +1239,10 @@ export function vectorOperator(qi, field, rawColumn, metric) {
|
|
|
1207
1239
|
throw new ValidationError(`[turbine] Column "${field}" on table "${qi.table}" is not a vector column ` +
|
|
1208
1240
|
`(actual type: ${colType}); cannot apply a vector distance operation.`);
|
|
1209
1241
|
}
|
|
1210
|
-
|
|
1242
|
+
// ownLookup, not a bare index: an inherited Object.prototype member
|
|
1243
|
+
// ("constructor", "toString", …) would otherwise resolve to a truthy builtin
|
|
1244
|
+
// and be spliced into the ORDER BY / WHERE clause as its source text.
|
|
1245
|
+
const op = ownLookup(VECTOR_METRIC_OPERATORS, metric);
|
|
1211
1246
|
if (!op) {
|
|
1212
1247
|
throw new ValidationError(`[turbine] Unknown vector metric "${metric}" for column "${field}". ` +
|
|
1213
1248
|
`Valid metrics: ${Object.keys(VECTOR_METRIC_OPERATORS).join(', ')}.`);
|
package/dist/query/writes.d.ts
CHANGED
|
@@ -15,6 +15,29 @@ import type { TableMetadata } from '../schema.js';
|
|
|
15
15
|
import type { DeferredQuery } from './deferred.js';
|
|
16
16
|
import type { CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, UpdateArgs, UpdateManyArgs, UpsertArgs } from './types.js';
|
|
17
17
|
import type { BuilderCtx } from './where.js';
|
|
18
|
+
/**
|
|
19
|
+
* Normalize one `data` value before it is bound as a write param.
|
|
20
|
+
*
|
|
21
|
+
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
22
|
+
*
|
|
23
|
+
* 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
|
|
24
|
+
* as a full ISO timestamp with the process offset and Postgres answers
|
|
25
|
+
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
26
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
|
|
27
|
+
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
28
|
+
* non-UTC process the stored value is wrong and (because the read path
|
|
29
|
+
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
30
|
+
* the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
|
|
31
|
+
* in query/utils.ts.
|
|
32
|
+
*
|
|
33
|
+
* `timestamptz` is deliberately untouched: it stores a real instant and the
|
|
34
|
+
* driver's local-offset string is already correct for it.
|
|
35
|
+
*
|
|
36
|
+
* Every other value, including a string that is already a valid literal, is
|
|
37
|
+
* returned by identity, so this is a byte-for-byte no-op for every non-temporal
|
|
38
|
+
* column.
|
|
39
|
+
*/
|
|
40
|
+
export declare function coerceWriteValue(qi: BuilderCtx, key: string, value: unknown): unknown;
|
|
18
41
|
/**
|
|
19
42
|
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
20
43
|
* by a write's `where` clause. Used by the `'reselect'` result strategy to
|
|
@@ -120,6 +143,24 @@ export declare function assertNoGeneratedColumns(qi: BuilderCtx, data: Record<st
|
|
|
120
143
|
* clause numbering continues correctly afterward.
|
|
121
144
|
*/
|
|
122
145
|
export declare function buildSetClause(qi: BuilderCtx, key: string, value: unknown, params: unknown[]): string;
|
|
146
|
+
/**
|
|
147
|
+
* Reject a `data` value that reached {@link buildSetClause}'s plain-value
|
|
148
|
+
* fallthrough as a plain object with exactly ONE key that is not a known
|
|
149
|
+
* atomic operator. On a scalar column that shape can only be a misspelled
|
|
150
|
+
* operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
|
|
151
|
+
* JSON text `{"bogus":"x"}` into the column and silently corrupts it.
|
|
152
|
+
*
|
|
153
|
+
* Deliberately narrow. Skipped for:
|
|
154
|
+
* - json / jsonb columns, which legitimately take arbitrary objects;
|
|
155
|
+
* - class instances (Buffer for bytea, Decimal wrappers, …), arrays and Dates,
|
|
156
|
+
* which {@link isUnmatchedPlainObject} already excludes;
|
|
157
|
+
* - multi-key objects, which are never operator-shaped (a real operator object
|
|
158
|
+
* has exactly one key) and are far more likely a deliberate payload.
|
|
159
|
+
*
|
|
160
|
+
* Shared by the SQL-build path and the cache-hit param-collect path, so a
|
|
161
|
+
* warmed template can never skip the check.
|
|
162
|
+
*/
|
|
163
|
+
export declare function assertBindableSetValue(qi: BuilderCtx, key: string, value: unknown): void;
|
|
123
164
|
/**
|
|
124
165
|
* Fingerprint SET clauses for update/updateMany.
|
|
125
166
|
* Captures key names + operator types (set/increment/etc) but not values.
|
|
@@ -128,4 +169,4 @@ export declare function fingerprintSet(_qi: BuilderCtx, data: Record<string, unk
|
|
|
128
169
|
/**
|
|
129
170
|
* Collect SET params for update/updateMany. Mirrors buildSetClause param order.
|
|
130
171
|
*/
|
|
131
|
-
export declare function collectSetParams(
|
|
172
|
+
export declare function collectSetParams(qi: BuilderCtx, data: Record<string, unknown>, params: unknown[]): void;
|
package/dist/query/writes.js
CHANGED
|
@@ -13,8 +13,58 @@
|
|
|
13
13
|
import { NotFoundError, OptimisticLockError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
15
15
|
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
16
|
-
import { UPDATE_OPERATOR_KEYS } from './filters.js';
|
|
16
|
+
import { isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
|
|
17
|
+
import { coerceTemporalValue, ownLookup } from './utils.js';
|
|
17
18
|
import * as whereMod from './where.js';
|
|
19
|
+
/**
|
|
20
|
+
* Normalize one `data` value before it is bound as a write param.
|
|
21
|
+
*
|
|
22
|
+
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
23
|
+
*
|
|
24
|
+
* 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
|
|
25
|
+
* as a full ISO timestamp with the process offset and Postgres answers
|
|
26
|
+
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
27
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
|
|
28
|
+
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
29
|
+
* non-UTC process the stored value is wrong and (because the read path
|
|
30
|
+
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
31
|
+
* the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
|
|
32
|
+
* in query/utils.ts.
|
|
33
|
+
*
|
|
34
|
+
* `timestamptz` is deliberately untouched: it stores a real instant and the
|
|
35
|
+
* driver's local-offset string is already correct for it.
|
|
36
|
+
*
|
|
37
|
+
* Every other value, including a string that is already a valid literal, is
|
|
38
|
+
* returned by identity, so this is a byte-for-byte no-op for every non-temporal
|
|
39
|
+
* column.
|
|
40
|
+
*/
|
|
41
|
+
export function coerceWriteValue(qi, key, value) {
|
|
42
|
+
// Cheap shape check first: the common path costs one check and no lookup.
|
|
43
|
+
if (!(value instanceof Date) && !Array.isArray(value))
|
|
44
|
+
return value;
|
|
45
|
+
// Non-throwing column resolution: this runs on the cache-HIT param-collect
|
|
46
|
+
// path too, where an unknown key must not turn into a different error than
|
|
47
|
+
// the build path already raises.
|
|
48
|
+
const column = ownLookup(qi.tableMeta.columnMap, key);
|
|
49
|
+
if (!column)
|
|
50
|
+
return value;
|
|
51
|
+
// Metadata generated by an older Turbine still carries per-column types
|
|
52
|
+
// (`pgTypes` has always been part of TableMetadata), and a column missing
|
|
53
|
+
// from the map resolves to undefined → no coercion, today's behavior.
|
|
54
|
+
return coerceTemporalValue(qi.columnPgTypeMap.get(column), value, utcDateTimeWrites(qi));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether the zone-less `date` / `timestamp` UTC rewrite applies.
|
|
58
|
+
*
|
|
59
|
+
* PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
|
|
60
|
+
* `datetime2` are converted or bound by their own drivers, and MySQL in
|
|
61
|
+
* particular reads a zone-less literal in the SESSION time zone, so a UTC
|
|
62
|
+
* literal would be misread there. `utcTimestamps: false` opts out — it is the
|
|
63
|
+
* same switch that turns off the UTC READ parsing, so the two stay symmetric.
|
|
64
|
+
*/
|
|
65
|
+
function utcDateTimeWrites(qi) {
|
|
66
|
+
return qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false;
|
|
67
|
+
}
|
|
18
68
|
/**
|
|
19
69
|
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
20
70
|
* by a write's `where` clause. Used by the `'reselect'` result strategy to
|
|
@@ -32,7 +82,7 @@ export function buildCreate(qi, args) {
|
|
|
32
82
|
assertNoGeneratedColumns(qi, args.data, 'create');
|
|
33
83
|
const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
|
|
34
84
|
const columns = entries.map(([k]) => qi.toSqlColumn(k));
|
|
35
|
-
const params = entries.map(([, v]) => v);
|
|
85
|
+
const params = entries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
36
86
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
37
87
|
const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
38
88
|
const sql = qi.dialect.buildInsertStatement({
|
|
@@ -78,7 +128,9 @@ export function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
|
78
128
|
let idx = 1;
|
|
79
129
|
for (const pk of qi.tableMeta.primaryKey) {
|
|
80
130
|
const field = qi.tableMeta.reverseColumnMap[pk] ?? snakeToCamel(pk);
|
|
81
|
-
|
|
131
|
+
// Same rewrite the INSERT's own params got: a temporal PK re-fetched with
|
|
132
|
+
// a raw Date would not match the literal that was just written.
|
|
133
|
+
selParams.push(coerceWriteValue(qi, field, data[field] ?? data[pk] ?? insertId));
|
|
82
134
|
conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
|
|
83
135
|
}
|
|
84
136
|
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
@@ -103,7 +155,7 @@ export function buildCreateMany(qi, args) {
|
|
|
103
155
|
const columns = keys.map((k) => qi.toColumn(k));
|
|
104
156
|
const rowValues = args.data.map((row) => {
|
|
105
157
|
const record = row;
|
|
106
|
-
return keys.map((key) => record[key]);
|
|
158
|
+
return keys.map((key) => coerceWriteValue(qi, key, record[key]));
|
|
107
159
|
});
|
|
108
160
|
// Use actual Postgres types for array casts in the default PostgreSQL dialect.
|
|
109
161
|
// Enum columns cast to `"EnumName"[]` — the generic text[] fallback would
|
|
@@ -303,7 +355,7 @@ export function buildUpsert(qi, args) {
|
|
|
303
355
|
// Build the INSERT part from create data
|
|
304
356
|
const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
|
|
305
357
|
const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
|
|
306
|
-
const createParams = createEntries.map(([, v]) => v);
|
|
358
|
+
const createParams = createEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
307
359
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
308
360
|
const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
309
361
|
// The conflict target comes from `where` keys — must be unique/PK columns
|
|
@@ -317,7 +369,7 @@ export function buildUpsert(qi, args) {
|
|
|
317
369
|
paramIdx++;
|
|
318
370
|
return clause;
|
|
319
371
|
});
|
|
320
|
-
const updateParams = updateEntries.map(([, v]) => v);
|
|
372
|
+
const updateParams = updateEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
321
373
|
const params = [...createParams, ...updateParams];
|
|
322
374
|
// Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
|
|
323
375
|
// upsert never resurrects a soft-deleted row or writes across tenants. Only
|
|
@@ -557,7 +609,7 @@ export function buildSetClause(qi, key, value, params) {
|
|
|
557
609
|
const op = keys[0];
|
|
558
610
|
const opValue = v[op];
|
|
559
611
|
if (op === 'set') {
|
|
560
|
-
params.push(opValue);
|
|
612
|
+
params.push(coerceWriteValue(qi, key, opValue));
|
|
561
613
|
return `${col} = ${qi.p(params.length)}${cast}`;
|
|
562
614
|
}
|
|
563
615
|
// Arithmetic operators: must be finite numbers
|
|
@@ -582,12 +634,44 @@ export function buildSetClause(qi, key, value, params) {
|
|
|
582
634
|
}
|
|
583
635
|
}
|
|
584
636
|
// Fall through: multi-key objects or non-operator single-key objects
|
|
585
|
-
// are treated as plain values (e.g., JSONB column payloads)
|
|
637
|
+
// are treated as plain values (e.g., JSONB column payloads), unless the
|
|
638
|
+
// shape can only be a misspelled atomic operator (see below).
|
|
639
|
+
assertBindableSetValue(qi, key, value);
|
|
586
640
|
}
|
|
587
641
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
588
|
-
params.push(value);
|
|
642
|
+
params.push(coerceWriteValue(qi, key, value));
|
|
589
643
|
return `${col} = ${qi.p(params.length)}${cast}`;
|
|
590
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Reject a `data` value that reached {@link buildSetClause}'s plain-value
|
|
647
|
+
* fallthrough as a plain object with exactly ONE key that is not a known
|
|
648
|
+
* atomic operator. On a scalar column that shape can only be a misspelled
|
|
649
|
+
* operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
|
|
650
|
+
* JSON text `{"bogus":"x"}` into the column and silently corrupts it.
|
|
651
|
+
*
|
|
652
|
+
* Deliberately narrow. Skipped for:
|
|
653
|
+
* - json / jsonb columns, which legitimately take arbitrary objects;
|
|
654
|
+
* - class instances (Buffer for bytea, Decimal wrappers, …), arrays and Dates,
|
|
655
|
+
* which {@link isUnmatchedPlainObject} already excludes;
|
|
656
|
+
* - multi-key objects, which are never operator-shaped (a real operator object
|
|
657
|
+
* has exactly one key) and are far more likely a deliberate payload.
|
|
658
|
+
*
|
|
659
|
+
* Shared by the SQL-build path and the cache-hit param-collect path, so a
|
|
660
|
+
* warmed template can never skip the check.
|
|
661
|
+
*/
|
|
662
|
+
export function assertBindableSetValue(qi, key, value) {
|
|
663
|
+
if (!isUnmatchedPlainObject(value))
|
|
664
|
+
return;
|
|
665
|
+
const keys = Object.keys(value);
|
|
666
|
+
if (keys.length !== 1 || UPDATE_OPERATOR_KEYS.has(keys[0]))
|
|
667
|
+
return;
|
|
668
|
+
const column = qi.toColumn(key);
|
|
669
|
+
if (whereMod.isJsonColumnType(qi, whereMod.getColumnPgType(qi, column)))
|
|
670
|
+
return;
|
|
671
|
+
throw new ValidationError(`[turbine] Unknown update operator "${keys[0]}" on "${qi.table}.${key}". ` +
|
|
672
|
+
`Supported: ${[...UPDATE_OPERATOR_KEYS].join(', ')}. ` +
|
|
673
|
+
'A plain object is only written as a value on a json/jsonb column.');
|
|
674
|
+
}
|
|
591
675
|
/**
|
|
592
676
|
* Fingerprint SET clauses for update/updateMany.
|
|
593
677
|
* Captures key names + operator types (set/increment/etc) but not values.
|
|
@@ -614,9 +698,9 @@ export function fingerprintSet(_qi, data) {
|
|
|
614
698
|
/**
|
|
615
699
|
* Collect SET params for update/updateMany. Mirrors buildSetClause param order.
|
|
616
700
|
*/
|
|
617
|
-
export function collectSetParams(
|
|
701
|
+
export function collectSetParams(qi, data, params) {
|
|
618
702
|
const entries = Object.entries(data).filter(([, v]) => v !== undefined);
|
|
619
|
-
for (const [, v] of entries) {
|
|
703
|
+
for (const [k, v] of entries) {
|
|
620
704
|
if (v !== null &&
|
|
621
705
|
typeof v === 'object' &&
|
|
622
706
|
!Array.isArray(v) &&
|
|
@@ -625,10 +709,17 @@ export function collectSetParams(_qi, data, params) {
|
|
|
625
709
|
const obj = v;
|
|
626
710
|
const keys = Object.keys(obj);
|
|
627
711
|
if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
|
|
628
|
-
|
|
712
|
+
// `set` is the only operator that binds a user value of arbitrary type;
|
|
713
|
+
// the arithmetic operators are finite numbers, which coerceWriteValue
|
|
714
|
+
// returns untouched. Mirrors buildSetClause.
|
|
715
|
+
params.push(coerceWriteValue(qi, k, obj[keys[0]]));
|
|
629
716
|
continue;
|
|
630
717
|
}
|
|
718
|
+
// Mirror the build path's misspelled-operator guard: the SET fingerprint
|
|
719
|
+
// is `key:eq` for both a plain value and a bogus operator object, so a
|
|
720
|
+
// cache HIT would otherwise bind the object and skip the check.
|
|
721
|
+
assertBindableSetValue(qi, k, v);
|
|
631
722
|
}
|
|
632
|
-
params.push(v);
|
|
723
|
+
params.push(coerceWriteValue(qi, k, v));
|
|
633
724
|
}
|
|
634
725
|
}
|
package/dist/schema-sql.d.ts
CHANGED
|
@@ -126,7 +126,21 @@ export declare function diffEnumValues(enumName: string, schemaLabels: readonly
|
|
|
126
126
|
};
|
|
127
127
|
/** A check constraint as declared or read from the DB. */
|
|
128
128
|
export interface CheckSpec {
|
|
129
|
+
/** Constraint name. Always emitted through `dialect.quoteIdentifier`. */
|
|
129
130
|
name: string;
|
|
131
|
+
/**
|
|
132
|
+
* RAW SQL boolean expression (e.g. `price >= 0`), emitted VERBATIM into the
|
|
133
|
+
* `CHECK (...)` clause.
|
|
134
|
+
*
|
|
135
|
+
* This is a deliberate escape hatch: a CHECK body is an arbitrary SQL
|
|
136
|
+
* expression, so it cannot be quoted, parameterized, or validated by the DDL
|
|
137
|
+
* builder without reimplementing the server's expression parser. The contract
|
|
138
|
+
* is therefore on the caller: the expression MUST come from the project's own
|
|
139
|
+
* schema definition (a `defineSchema` literal or an introspected constraint),
|
|
140
|
+
* and must NEVER be built from user input, request data, or any other
|
|
141
|
+
* untrusted source. Anything interpolated into it runs with the privileges of
|
|
142
|
+
* the migration.
|
|
143
|
+
*/
|
|
130
144
|
expression: string;
|
|
131
145
|
}
|
|
132
146
|
/**
|
package/dist/schema-sql.js
CHANGED
|
@@ -57,14 +57,23 @@ function generateCreateEnumType(enumName, labels, dialect) {
|
|
|
57
57
|
/**
|
|
58
58
|
* Resolve the DDL type token for a column: an enum type name, a `vector(n)`
|
|
59
59
|
* literal, or the dialect's scalar type — with a trailing `[]` for arrays.
|
|
60
|
+
*
|
|
61
|
+
* `vectorDimensions` is the one number interpolated into the type token, so it
|
|
62
|
+
* is validated here as a positive integer within pgvector's limit rather than
|
|
63
|
+
* being trusted to be numeric.
|
|
60
64
|
*/
|
|
61
|
-
function resolveDdlType(config, dialect) {
|
|
65
|
+
function resolveDdlType(config, dialect, columnName) {
|
|
62
66
|
let base;
|
|
63
67
|
if (config.enumName) {
|
|
64
68
|
base = dialect.quoteIdentifier(config.enumName);
|
|
65
69
|
}
|
|
66
70
|
else if (config.vectorDimensions != null) {
|
|
67
|
-
|
|
71
|
+
const dims = config.vectorDimensions;
|
|
72
|
+
// pgvector caps a `vector` column at 16000 dimensions.
|
|
73
|
+
if (typeof dims !== 'number' || !Number.isInteger(dims) || dims < 1 || dims > 16000) {
|
|
74
|
+
throw new ValidationError(`[turbine] Column "${columnName}": vector dimensions must be an integer between 1 and 16000, got ${String(dims)}.`);
|
|
75
|
+
}
|
|
76
|
+
base = `vector(${dims})`;
|
|
68
77
|
}
|
|
69
78
|
else {
|
|
70
79
|
base = dialect.buildColumnType({ type: config.type, maxLength: config.maxLength });
|
|
@@ -223,6 +232,9 @@ function generateCreateTable(table, resolveRef, dialect = postgresDialect) {
|
|
|
223
232
|
columnDefs.push(dialect.buildPrimaryKeyConstraint(cols));
|
|
224
233
|
}
|
|
225
234
|
// Table-level CHECK constraints (named → CONSTRAINT "name" CHECK (...)).
|
|
235
|
+
// The constraint NAME is quoted; the EXPRESSION is raw SQL emitted verbatim
|
|
236
|
+
// by design (see the CheckSpec.expression contract) and must never be built
|
|
237
|
+
// from untrusted input.
|
|
226
238
|
for (const chk of table.checks ?? []) {
|
|
227
239
|
columnDefs.push(chk.name
|
|
228
240
|
? `CONSTRAINT ${dialect.quoteIdentifier(chk.name)} CHECK (${chk.expression})`
|
|
@@ -269,7 +281,7 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
|
|
|
269
281
|
// Resolve the DDL type (enum name / vector(n) / scalar, plus [] for arrays).
|
|
270
282
|
// Passed as a fully-formed `type` token with no maxLength so the dialect
|
|
271
283
|
// doesn't re-apply VARCHAR(n) on top of it.
|
|
272
|
-
const ddlType = resolveDdlType(config, dialect);
|
|
284
|
+
const ddlType = resolveDdlType(config, dialect, snakeName);
|
|
273
285
|
let def = dialect.buildColumnDefinition({
|
|
274
286
|
name: dialect.quoteIdentifier(snakeName),
|
|
275
287
|
type: ddlType,
|
|
@@ -288,7 +300,9 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
|
|
|
288
300
|
if (config.onUpdate)
|
|
289
301
|
def += ` ON UPDATE ${referentialActionToSql(config.onUpdate)}`;
|
|
290
302
|
}
|
|
291
|
-
// Column-level CHECK constraint
|
|
303
|
+
// Column-level CHECK constraint. The expression is RAW SQL authored in the
|
|
304
|
+
// schema and is emitted verbatim by design (a DDL builder cannot parse or
|
|
305
|
+
// parameterize a boolean expression); never build it from untrusted input.
|
|
292
306
|
if (config.check) {
|
|
293
307
|
def += ` CHECK (${config.check})`;
|
|
294
308
|
}
|
|
@@ -529,6 +543,8 @@ export function diffCheckConstraints(table, schemaChecks, dbChecks, dialect = po
|
|
|
529
543
|
const dbByName = new Map(dbChecks.map((c) => [c.name, c]));
|
|
530
544
|
const schemaByName = new Map(schemaChecks.map((c) => [c.name, c]));
|
|
531
545
|
const norm = (e) => e.replace(/\s+/g, ' ').trim();
|
|
546
|
+
// `c.expression` is raw SQL emitted verbatim by design (see CheckSpec):
|
|
547
|
+
// schema-authored only, never untrusted input.
|
|
532
548
|
const addStmt = (c) => `ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(c.name)} CHECK (${c.expression});`;
|
|
533
549
|
const dropStmt = (name) => `ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(name)};`;
|
|
534
550
|
for (const sc of schemaChecks) {
|
|
@@ -741,7 +757,7 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
741
757
|
if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
|
|
742
758
|
// resolveDdlType handles enum names, vector(n), arrays, and VARCHAR(n) —
|
|
743
759
|
// config.type alone would emit the internal ENUM/VECTOR sentinels here.
|
|
744
|
-
const sqlType = resolveDdlType(config, dialect);
|
|
760
|
+
const sqlType = resolveDdlType(config, dialect, snakeName);
|
|
745
761
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
746
762
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
747
763
|
const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
|
|
@@ -879,6 +895,8 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
879
895
|
const normExpr = (e) => e.replace(/\s+/g, ' ').trim();
|
|
880
896
|
for (const sc of namedSchemaChecks) {
|
|
881
897
|
const existing = dbCheckByName.get(sc.name);
|
|
898
|
+
// The constraint name is quoted; `sc.expression` is raw SQL emitted
|
|
899
|
+
// verbatim by design (see CheckSpec) and must be schema-authored.
|
|
882
900
|
const addSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(sc.name)} CHECK (${sc.expression});`;
|
|
883
901
|
const dropSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(sc.name)};`;
|
|
884
902
|
if (!existing) {
|