@vibeorm/sql 4.1.0 → 4.3.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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Key-list membership (`SqlExpr` `keyList`, the public `where: { IN }`):
3
+ * the postgres element type each member's array binds as.
4
+ *
5
+ * Postgres compares `"col" = $n` with the parameter typed as the column's
6
+ * type WITHOUT its modifier (measured on postgres 18.6: `timestamptz(3) =
7
+ * '…00.1234'` matches nothing, `numeric(10,2) = '1.234'` matches nothing,
8
+ * `varchar(3) = 'abcd'` matches nothing). The unnest form must compare the
9
+ * same way, so every member binds as `$n::<base type>[]`: a `timestamptz(3)[]`
10
+ * cast would round `.1234` onto the stored `.123` and match a row `=` does not.
11
+ * The base type also keeps the column's own operator and index: `bpchar` for
12
+ * `char(n)` (pad-insensitive like `=`), `citext` (case-insensitive like `=`;
13
+ * a `text[]` member would compare case-sensitively), the native enum's own
14
+ * type, and text collations resolve to the column's collation, as for `=`.
15
+ */
16
+ import type { KeyListMember } from "./ast.ts";
17
+ import type { SqlDialect } from "./dialect.ts";
18
+ /**
19
+ * The postgres element type a key-list member's array binds as, or
20
+ * `undefined` when the member has none (Json, or a native type this list does
21
+ * not know). Never throws: a caller deciding whether a key list CAN carry a
22
+ * member (the automatic OR rewrite) asks here; {@link postgresKeyListElementType}
23
+ * is the throwing form the renderer uses.
24
+ */
25
+ export declare function postgresKeyListElementTypeOf(params: {
26
+ dialect: SqlDialect;
27
+ member: KeyListMember;
28
+ }): string | undefined;
29
+ /**
30
+ * The postgres element type a key-list member's array binds as
31
+ * (`$n::<type>[]`), derived from the column's declared type facts.
32
+ *
33
+ * @throws VibeError `VIBE_UNSUPPORTED_CAPABILITY` for a member whose type has
34
+ * no equality a key can use (Json) or a native type this list does not know.
35
+ */
36
+ export declare function postgresKeyListElementType(params: {
37
+ dialect: SqlDialect;
38
+ member: KeyListMember;
39
+ }): string;
40
+ /**
41
+ * Whether a key list must bind its rows as a row-value list of UNTYPED
42
+ * parameters, `(a, b) IN (($1, $2), …)`, instead of typed arrays on the
43
+ * `unnest` strategy: some member is a `DateTime` without a declared native
44
+ * type. Its element type would be `timestamptz`, but such a field may sit on
45
+ * a zone-less `timestamp(3)` column (a Prisma-created database;
46
+ * docs/dialect-notes.md "DateTime without time zone on postgres"), where
47
+ * `timestamp = timestamptz` converts the column through the session TimeZone
48
+ * and matches other rows than `"col" = $n` (an untyped parameter the server
49
+ * types from the column). The row-value list keeps that typing; PostgreSQL
50
+ * plans it like the OR of conjunctions, so its planning grows with the list.
51
+ * Shared by the renderer and the runtime's render-cache walk.
52
+ */
53
+ export declare function keyListBindsUntypedRows(params: {
54
+ members: readonly KeyListMember[];
55
+ }): boolean;
56
+ /**
57
+ * The cast after a bulk-insert column's array placeholder (`$n::<cast>[]`),
58
+ * or `undefined` when the column has none and the insert must keep VALUES.
59
+ * Never throws: the query builder asks here while deciding admission.
60
+ *
61
+ * Every type the key list knows binds its BASE type, exactly as above — the
62
+ * INSERT's assignment then applies the column's modifier (length, precision,
63
+ * rounding) the way it does for a VALUES parameter. Json is the one addition:
64
+ * it binds through a text slot, `text::jsonb` (`text::json` for `@db.Json`),
65
+ * because PostgreSQL has no assignment cast from text to jsonb and bun:sql's
66
+ * named statements would JSON-encode pre-encoded text bound to a jsonb slot
67
+ * (the `JsonParamCell` rule, per element).
68
+ */
69
+ export declare function postgresBulkInsertArrayCastOf(params: {
70
+ dialect: SqlDialect;
71
+ member: KeyListMember;
72
+ }): string | undefined;
73
+ /**
74
+ * The throwing form of {@link postgresBulkInsertArrayCastOf}, for the renderer.
75
+ *
76
+ * @throws VibeError `VIBE_UNSUPPORTED_CAPABILITY` for a native type the
77
+ * bulk-insert form does not know.
78
+ */
79
+ export declare function postgresBulkInsertArrayCast(params: {
80
+ dialect: SqlDialect;
81
+ member: KeyListMember;
82
+ }): string;
83
+ //# sourceMappingURL=key-list.d.ts.map
package/dist/like.d.ts CHANGED
@@ -41,4 +41,21 @@ export declare function strictMatchPattern(params: {
41
41
  op: "contains" | "startsWith" | "endsWith";
42
42
  value: string;
43
43
  }): string;
44
+ /**
45
+ * Does a `strictMatch` render the collation `LIKE` conjunct BEFORE its
46
+ * byte-exact test? Only the `binary` strategy's (mysql) non-negated
47
+ * `startsWith`: `(col LIKE ? AND col LIKE CAST(? AS BINARY))`, the same
48
+ * pattern bound twice. A byte-exact prefix match is always a collation match
49
+ * (a collation never separates identical characters, and the escaped prefix
50
+ * plus `%` has no other wildcard), so the AND keeps exactly the binary test's
51
+ * rows — while the plain conjunct lets a btree index on the column bound the
52
+ * scan (measured on mysql 26.7: 5.92 → 0.24 ms, 100 002 → 3 rows read). The
53
+ * ONE decision point for the renderer AND the runtime's render-cache mirror,
54
+ * which must bind the pattern twice exactly when the renderer does.
55
+ */
56
+ export declare function strictMatchCollationPrefilter(params: {
57
+ strategy: "native" | "glob" | "binary";
58
+ op: "equals" | "contains" | "startsWith" | "endsWith";
59
+ negate: boolean;
60
+ }): boolean;
44
61
  //# sourceMappingURL=like.d.ts.map
@@ -0,0 +1,9 @@
1
+ import type { CheckIR } from "@vibeorm/schema";
2
+ import type { SqlDialect } from "./dialect.ts";
3
+ /** The single owned SQLite calendar/domain CHECK; native DATE dialects need no marker. */
4
+ export declare function renderLocalDateCheck({ dialect, table, column }: {
5
+ readonly dialect: SqlDialect;
6
+ readonly table: string;
7
+ readonly column: string;
8
+ }): CheckIR | undefined;
9
+ //# sourceMappingURL=local-date.d.ts.map
@@ -7,6 +7,12 @@ export declare function toPgArrayLiteral(params: {
7
7
  * not a literal (defensive — the value then passes through untouched).
8
8
  * Unquoted `NULL` is SQL NULL; postgres quotes any element that could be
9
9
  * mistaken for it.
10
+ *
11
+ * RD-07: elements are SLICED out of the literal (an unquoted element up to the
12
+ * next comma, a quoted one as the runs between its escapes) instead of built
13
+ * one character at a time; the output, malformed input included, is the same
14
+ * as the former char-by-char parser's (differential test
15
+ * `tests/pg-array-literal-sliced.test.ts`).
10
16
  */
11
17
  export declare function parsePgArrayLiteral(params: {
12
18
  literal: string;
@@ -6,6 +6,9 @@
6
6
  * `WHERE`. Constitution rule 4 — every SQL-text decision lives in a dialect
7
7
  * here, never in migrate, the DSL or a query builder.
8
8
  *
9
+ * Shared scalar rendering also serves expression-index keys. The caller owns
10
+ * expression-index DDL and its additional database-object qualification.
11
+ *
9
12
  * What it guarantees:
10
13
  *
11
14
  * - **Logical field names resolve to physical columns.** `@map("qty_total")`
@@ -14,9 +17,8 @@
14
17
  * - **Identifiers are quoted through `dialect.quoteIdent`, literals through
15
18
  * `dialect.quoteStringLiteral`.** No value the author writes can escape its
16
19
  * literal and become SQL structure.
17
- * - **Operand scalars must match the field's.** Compared through
18
- * `fieldTypeIdentity`, the same answer foreign-key pairing uses, so a check
19
- * and a relation can never disagree about what a column IS.
20
+ * - **Operand scalars must match the expression result type.** Local dates keep
21
+ * their distinct logical identity, shared with foreign-key pairing.
20
22
  * - **A dialect that cannot express something refuses here**, at the earliest
21
23
  * knowable moment (generate time), with a `VibeError` carrying a stable
22
24
  * `meta.rule` — never silently different SQL.
@@ -26,13 +28,13 @@
26
28
  * an implicit `IS NOT NULL` guard. An author who wants all-or-none writes it
27
29
  * with `isNull`/`isNotNull`.
28
30
  */
29
- import type { EnumIR, ModelIR, PredicateIR } from "@vibeorm/schema";
31
+ import type { EnumIR, ModelIR, PredicateBooleanUsage, PredicateExpression, PredicateIR, PredicateResultType } from "@vibeorm/schema";
30
32
  import type { SqlDialect } from "./dialect.ts";
31
33
  /**
32
- * Where a predicate is being rendered. The two sites have different database
33
- * limits, so the renderer has to know which one it is serving.
34
+ * Where a Boolean predicate is being rendered. Each host has different database
35
+ * limits; scalar index-expression usage is handled by renderSchemaExpression.
34
36
  */
35
- export type PredicateUsage = "check" | "indexPredicate" | "exclusionPredicate";
37
+ export type PredicateUsage = PredicateBooleanUsage;
36
38
  /**
37
39
  * The part of a model a predicate reads: its name (for error messages) and its
38
40
  * fields (for physical column names and scalar types). Narrow on purpose — a
@@ -50,4 +52,16 @@ export declare function renderPredicate(params: {
50
52
  /** Human label naming the declaration, e.g. `CHECK "order_total_nonnegative"`. */
51
53
  site: string;
52
54
  }): string;
55
+ /** Render a scalar schema expression. Expression-index DDL and host qualification belong to the caller. */
56
+ export declare function renderSchemaExpression({ expression, model, dialect, enums, usage, site }: {
57
+ expression: PredicateExpression;
58
+ model: PredicateModel;
59
+ dialect: SqlDialect;
60
+ enums?: readonly EnumIR[];
61
+ usage: "indexExpression";
62
+ site: string;
63
+ }): {
64
+ readonly text: string;
65
+ readonly resultType: PredicateResultType;
66
+ };
53
67
  //# sourceMappingURL=predicate.d.ts.map
@@ -13,6 +13,11 @@ registerSqlHandleCheck({ check: (value) => expressionStates.has(value) || source
13
13
  function columnContract(definition) {
14
14
  return JSON.stringify({ scalar: definition.scalar, nullable: definition.nullable === true, list: definition.list === true, enumType: definition.enumType, enumValues: definition.enumValues, nativeType: definition.nativeType, transport: definition.transport });
15
15
  }
16
+ function assertWritableColumn({ table, column }) {
17
+ if (table.columns[column]?.generated === true) {
18
+ throw new VibeError({ code: "VIBE_VALIDATION", message: `Generated SQL column "${column}" is read-only`, meta: { reason: "generated-column-write", table: table.name, field: column, path: [column] } });
19
+ }
20
+ }
16
21
  function invalid(message) {
17
22
  throw new VibeError({ code: "VIBE_VALIDATION", message });
18
23
  }
@@ -64,7 +69,8 @@ function checkScope(params) {
64
69
  import { assertSqlNamespace } from "@vibeorm/schema";
65
70
 
66
71
  // packages/sql/src/query/expressions.ts
67
- import { assertSqlParamBuilding, decode, float32Codec, isSqlBindingCollector, isSqlFragment, isSqlParam, jsonListParameter, nativeEnumArrayParameter, parsePgArrayLiteral, recordSqlBinding, renderSqlFragment, resultScalarContract, snapshotSqlValue, sqlParamContract, timeOfDayCodec, toPgArrayLiteral } from "@vibeorm/sql";
72
+ import { isLocalDateField } from "@vibeorm/schema";
73
+ import { assertSqlParamBuilding, decode, float32Codec, isSqlBindingCollector, isSqlFragment, isSqlParam, jsonListParameter, localDateCodec, nativeEnumArrayParameter, parsePgArrayLiteral, recordSqlBinding, renderSqlFragment, resultScalarContract, snapshotSqlValue, sqlParamContract, timeOfDayCodec, toPgArrayLiteral } from "@vibeorm/sql";
68
74
  var nullableFloat = decode.nullable({ decoder: decode.float });
69
75
  var countDefinition = Object.freeze({ name: "", scalar: "BigInt", decoder: decode.bigInt });
70
76
  var floatDefinition = Object.freeze({ name: "", scalar: "Float", nullable: true, decoder: nullableFloat });
@@ -74,6 +80,8 @@ function nullableDecoder(decoder) {
74
80
  if (lifted === undefined) {
75
81
  lifted = Object.freeze({ decode: (params) => params.value === null ? null : decoder.decode(params) });
76
82
  nullableDecoders.set(decoder, lifted);
83
+ if (localDateValueDecoders.has(decoder))
84
+ localDateValueDecoders.add(lifted);
77
85
  }
78
86
  return lifted;
79
87
  }
@@ -87,11 +95,19 @@ function inputDecoder(definition) {
87
95
  invalid("A decode-only projection requires a separate inputDecoder for parameters and writes");
88
96
  return decoder;
89
97
  }
98
+ function isLocalDateColumn({ definition }) {
99
+ return definition?.scalar === "String" && isLocalDateField({ field: { type: "String", nativeType: definition.nativeType } });
100
+ }
90
101
  function nativeColumnDefinition(definition) {
102
+ const localDate = isLocalDateColumn({ definition });
103
+ if (localDate && definition.list === true)
104
+ invalid("Local-date list columns are unsupported");
105
+ if (localDate && localDateDecoders.has(definition.decoder))
106
+ return definition;
91
107
  const contract = resultScalarContract({ decoder: definition.decoder });
92
- if (contract === undefined || contract.scalar !== definition.scalar)
108
+ if (!localDate && (contract === undefined || contract.scalar !== definition.scalar))
93
109
  return definition;
94
- const codec = definition.scalar === "DateTime" ? timeOfDayCodec({ nativeType: definition.nativeType }) : definition.scalar === "Float" && /^(real|float4)$/i.test(definition.nativeType ?? "") ? float32Codec : undefined;
110
+ const codec = localDate ? localDateCodec : definition.scalar === "DateTime" ? timeOfDayCodec({ nativeType: definition.nativeType }) : definition.scalar === "Float" && /^(real|float4)$/i.test(definition.nativeType ?? "") ? float32Codec : undefined;
95
111
  if (codec === undefined)
96
112
  return definition;
97
113
  const original = definition.decoder;
@@ -101,7 +117,7 @@ function nativeColumnDefinition(definition) {
101
117
  decoders = new Map;
102
118
  nativeDecoders.set(original, decoders);
103
119
  }
104
- const key = `${definition.nativeType}|${list}`;
120
+ const key = localDate ? "local-date" : `${definition.nativeType}|${list}`;
105
121
  let decoder = decoders.get(key);
106
122
  if (decoder === undefined) {
107
123
  decoder = Object.freeze({
@@ -112,20 +128,32 @@ function nativeColumnDefinition(definition) {
112
128
  const value = raw === null ? null : list && Array.isArray(elements) ? elements.map(convert) : convert(raw);
113
129
  return original.decode({ ...params, value });
114
130
  },
115
- encode: (params) => {
116
- const encoded = original.encode(params);
117
- const convert = (value) => value === null ? null : codec.encode(value);
118
- return list && Array.isArray(encoded) ? encoded.map(convert) : convert(encoded);
119
- }
131
+ ..."encode" in original && typeof original.encode === "function" ? {
132
+ encode: (params) => {
133
+ const encoded = original.encode(params);
134
+ const convert = (value) => value === null ? null : codec.encode(value);
135
+ return list && Array.isArray(encoded) ? encoded.map(convert) : convert(encoded);
136
+ }
137
+ } : {}
120
138
  });
121
139
  decoders.set(key, decoder);
140
+ if (localDate) {
141
+ localDateDecoders.add(decoder);
142
+ if (contract?.scalar === "String" && !contract.list)
143
+ localDateValueDecoders.add(decoder);
144
+ }
122
145
  }
123
146
  return { ...definition, decoder };
124
147
  }
125
148
  var nativeDecoders = new WeakMap;
149
+ var localDateDecoders = new WeakSet;
150
+ var localDateValueDecoders = new WeakSet;
151
+ function hasLocalDateValueInput({ definition }) {
152
+ return isLocalDateColumn({ definition }) && localDateValueDecoders.has(definition.decoder) && (definition.inputDecoder === undefined || localDateValueDecoders.has(definition.inputDecoder));
153
+ }
126
154
  function transportProjection(params) {
127
155
  const { context, expression, definition } = params;
128
- if (definition.transport === "text" || definition.scalar === "Json" || definition.scalar === "BigInt" || definition.list === true && definition.scalar === "Float") {
156
+ if (context.dialect.name !== "sqlite" && isLocalDateColumn({ definition }) || definition.transport === "text" || definition.scalar === "Json" || definition.scalar === "BigInt" || definition.list === true && definition.scalar === "Float") {
129
157
  return `CAST(${expression} AS ${context.dialect.name === "mysql" ? "CHAR" : "TEXT"})`;
130
158
  }
131
159
  return expression;
@@ -141,7 +169,7 @@ function columnInputContract(definition) {
141
169
  enumValues: definition.enumValues,
142
170
  enumType: definition.enumType,
143
171
  jsonStorage: definition.jsonStorage,
144
- ...definition.inputDecoder !== undefined && definition.inputDecoder !== definition.decoder ? { columnIdentity: encoder } : {}
172
+ ...definition.inputDecoder !== undefined && definition.inputDecoder !== definition.decoder && !hasLocalDateValueInput({ definition }) ? { columnIdentity: encoder } : {}
145
173
  };
146
174
  }
147
175
  function physicalInput(contract) {
@@ -190,6 +218,8 @@ function encodeColumnValue(params) {
190
218
  invalid("A non-null SQL column cannot receive SQL NULL from its encoder");
191
219
  return null;
192
220
  }
221
+ if (isLocalDateColumn({ definition }))
222
+ return localDateCodec.encode(encoded);
193
223
  if (definition.scalar === "BigInt" && definition.list !== true && dialect.name === "sqlite") {
194
224
  const integer = decode.bigInt.decode({ value: encoded, dialect: "sqlite" });
195
225
  if (integer < -9223372036854775808n || integer > 9223372036854775807n)
@@ -239,7 +269,7 @@ function bindColumnValue(params) {
239
269
  }
240
270
  function predicateDefinition(expression) {
241
271
  const state = expressionState(expression);
242
- if (state.definition?.inputDecoder !== undefined && (!("encode" in state.definition.decoder) || typeof state.definition.decoder.encode !== "function"))
272
+ if (state.definition?.inputDecoder !== undefined && (!("encode" in state.definition.decoder) || typeof state.definition.decoder.encode !== "function") && !hasLocalDateValueInput({ definition: state.definition }))
243
273
  unsupported("Input tokens cannot infer predicates over read-transformed columns");
244
274
  if (state.definition !== undefined)
245
275
  return state.definition;
@@ -297,7 +327,7 @@ function literal(params) {
297
327
  function comparedLiteral(params) {
298
328
  const state = expressionState(params.expression);
299
329
  const definition = state.definition;
300
- if (definition?.inputDecoder !== undefined && (!("encode" in definition.decoder) || typeof definition.decoder.encode !== "function")) {
330
+ if (definition?.inputDecoder !== undefined && (!("encode" in definition.decoder) || typeof definition.decoder.encode !== "function") && !hasLocalDateValueInput({ definition })) {
301
331
  unsupported("Implicit literal predicates are unavailable for read-transformed columns; use an explicit trusted SQL input contract");
302
332
  }
303
333
  const value = snapshotSqlValue(params.value);
@@ -335,6 +365,9 @@ function comparison(params) {
335
365
  const right = typeof params.right === "object" && params.right !== null && expressionStates.has(params.right) ? params.right : comparedLiteral({ expression: params.left, value: params.right });
336
366
  const leftState = expressionState(params.left);
337
367
  const rightState = expressionState(right);
368
+ if (leftState.parameter === undefined && rightState.parameter === undefined && leftState.definition !== undefined && rightState.definition !== undefined && isLocalDateColumn({ definition: leftState.definition }) !== isLocalDateColumn({ definition: rightState.definition })) {
369
+ invalid("Local-date comparisons require matching local-date columns, not plain text or another scalar");
370
+ }
338
371
  const unsafeDecimal = (state) => state.definition?.scalar === "Decimal";
339
372
  return makeExpression(childrenState({
340
373
  children: [params.left, right],
@@ -342,7 +375,7 @@ function comparison(params) {
342
375
  render: (context) => {
343
376
  if (context.dialect.name === "sqlite" && (unsafeDecimal(leftState) || unsafeDecimal(rightState)))
344
377
  unsupported("SQLite Decimal comparisons are not exact; use an explicit raw numeric contract");
345
- const left = leftState.render(context);
378
+ const left = leftState.parameter !== undefined && isLocalDateColumn({ definition: rightState.definition }) ? bindColumnValue({ context, definition: rightState.definition, encoder: leftState.parameter.decoder, value: leftState.parameter.value }) : leftState.render(context);
346
379
  const operand = leftState.parameter === undefined && leftState.definition !== undefined && rightState.parameter !== undefined ? bindColumnValue({ context, definition: leftState.definition, encoder: rightState.parameter.decoder, value: rightState.parameter.value }) : rightState.render(context);
347
380
  return `(${left} ${params.operator} ${operand})`;
348
381
  }
@@ -363,7 +396,7 @@ function aggregate(params) {
363
396
  invalid("Nested aggregate expressions are unsupported");
364
397
  if (definition === undefined || definition.list === true || !["String", "Int", "BigInt", "Float", "DateTime"].includes(definition.scalar))
365
398
  unsupported("MIN/MAX require a qualified ordered scalar column; use an explicit raw decoder otherwise");
366
- if (definition.scalar === "String" && definition.nativeType !== undefined && !/^(text|varchar|char|bpchar|tinytext|mediumtext|longtext)(?:\s*\(.*\))?$/i.test(definition.nativeType))
399
+ if (definition.scalar === "String" && !isLocalDateColumn({ definition }) && definition.nativeType !== undefined && !/^(text|varchar|char|bpchar|tinytext|mediumtext|longtext)(?:\s*\(.*\))?$/i.test(definition.nativeType))
367
400
  unsupported("MIN/MAX do not support this native String type; UUID and other non-text types need explicit SQL");
368
401
  return makeExpression({
369
402
  ...state,
@@ -375,7 +408,7 @@ function aggregate(params) {
375
408
  ungrouped: new Set,
376
409
  aggregate: true,
377
410
  render: (context) => `${params.fn}(${state.render(context)})`,
378
- projection: definition.scalar === "BigInt" ? (context) => `CAST(${params.fn}(${state.render(context)}) AS ${context.dialect.name === "mysql" ? "CHAR" : "TEXT"})` : undefined
411
+ projection: definition.scalar === "BigInt" || isLocalDateColumn({ definition }) ? (context) => transportProjection({ context, expression: `${params.fn}(${state.render(context)})`, definition }) : undefined
379
412
  });
380
413
  }
381
414
  function floatAggregate(params) {
@@ -488,6 +521,8 @@ function makeTable(params) {
488
521
  identifier(key);
489
522
  identifier(definition.name);
490
523
  assertDecoder(definition.decoder);
524
+ if (definition.generated !== undefined && definition.generated !== true)
525
+ invalid("SQL generated-column metadata must be true when present");
491
526
  if (definition.inputDecoder !== undefined && typeof definition.inputDecoder.encode !== "function")
492
527
  invalid("SQL input decoders must have an encoder");
493
528
  if (definition.enumType !== undefined) {
@@ -501,9 +536,11 @@ function makeTable(params) {
501
536
  const physical = Object.values(columns).map((column) => column.name);
502
537
  if (new Set(physical).size !== physical.length)
503
538
  invalid("Physical column names must be unique within a source");
504
- const required = [...params.required ?? Object.entries(columns).filter(([, definition]) => definition.nullable !== true && definition.dbDefault !== true).map(([name]) => name)];
539
+ const required = [...params.required ?? Object.entries(columns).filter(([, definition]) => definition.nullable !== true && definition.dbDefault !== true && definition.generated !== true).map(([name]) => name)];
505
540
  if (required.some((key) => !Object.hasOwn(columns, key)))
506
541
  invalid("Required insert columns must belong to the table");
542
+ for (const column of required)
543
+ assertWritableColumn({ table: { name: params.name, columns, required }, column });
507
544
  const source = Object.freeze(Object.create(null));
508
545
  sourceStates.set(source, Object.freeze({ name: params.name, namespace: params.namespace, columns: Object.freeze(columns), required: Object.freeze(required), readOnly: params.readOnly === true }));
509
546
  return source;
@@ -874,9 +911,11 @@ function cells(params) {
874
911
  invalid("SQL write rows must be field objects");
875
912
  const table = sourceState(params.binding.source);
876
913
  const row = params.value;
877
- for (const name of Object.keys(row))
914
+ for (const name of Object.keys(row)) {
878
915
  if (!Object.hasOwn(table.columns, name))
879
916
  invalid(`Unknown SQL write field "${name}"`);
917
+ assertWritableColumn({ table, column: name });
918
+ }
880
919
  return Object.freeze(Object.keys(table.columns).filter((name) => Object.hasOwn(row, name)).map((name) => {
881
920
  const value = row[name];
882
921
  if (isSqlParam3(value)) {
@@ -1009,6 +1048,7 @@ function input(params) {
1009
1048
  invalid("qb.input requires a writable physical table");
1010
1049
  if (!Object.hasOwn(table.columns, params.column))
1011
1050
  invalid("SQL input column does not belong to its table");
1051
+ assertWritableColumn({ table, column: params.column });
1012
1052
  const definition = table.columns[params.column];
1013
1053
  let descriptor = columnInputs.get(definition);
1014
1054
  if (descriptor === undefined) {
@@ -1077,4 +1117,4 @@ export {
1077
1117
  sum
1078
1118
  };
1079
1119
 
1080
- //# debugId=B60163A02807B75F64756E2164756E21
1120
+ //# debugId=3FADE857D3991B3164756E2164756E21