@lobomfz/db 0.3.8 → 0.3.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobomfz/db",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Bun SQLite database with Arktype schemas and typed Kysely client",
5
5
  "keywords": [
6
6
  "arktype",
package/src/database.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Database as BunDatabase } from "bun:sqlite";
2
2
 
3
3
  import type { Type } from "arktype";
4
- import { Kysely, ParseJSONResultsPlugin } from "kysely";
4
+ import { Kysely } from "kysely";
5
5
 
6
6
  import { BunSqliteDialect } from "./dialect/dialect.js";
7
7
  import type { DbFieldMeta } from "./env.js";
@@ -9,7 +9,7 @@ import type { GeneratedPreset } from "./generated.js";
9
9
  import { Differ, type DesiredTable } from "./migration/diff.js";
10
10
  import { Executor } from "./migration/execute.js";
11
11
  import { Introspector } from "./migration/introspect.js";
12
- import { DeserializePlugin, type ColumnCoercion, type ColumnsMap } from "./plugin.js";
12
+ import { ResultHydrationPlugin, type ColumnCoercion, type ColumnsMap } from "./plugin.js";
13
13
  import type {
14
14
  DatabaseOptions,
15
15
  IndexDefinition,
@@ -17,6 +17,7 @@ import type {
17
17
  TablesFromSchemas,
18
18
  DatabasePragmas,
19
19
  } from "./types.js";
20
+ import { WriteValidationPlugin } from "./write-validation-plugin.js";
20
21
 
21
22
  type ArkBranch = {
22
23
  domain?: string;
@@ -88,8 +89,8 @@ export class Database<T extends SchemaRecord> {
88
89
  this.kysely = new Kysely<TablesFromSchemas<T>>({
89
90
  dialect: new BunSqliteDialect({ database: this.sqlite }),
90
91
  plugins: [
91
- new DeserializePlugin(this.columns, this.tableColumns, validation),
92
- new ParseJSONResultsPlugin(),
92
+ new WriteValidationPlugin(this.columns, validation),
93
+ new ResultHydrationPlugin(this.columns, this.tableColumns, validation),
93
94
  ],
94
95
  });
95
96
  }
package/src/plugin.ts CHANGED
@@ -3,25 +3,21 @@ import type { Type } from "arktype";
3
3
  import {
4
4
  type KyselyPlugin,
5
5
  type OperationNode,
6
+ type QueryId,
6
7
  type RootOperationNode,
7
8
  type UnknownRow,
8
- type QueryId,
9
9
  AggregateFunctionNode,
10
- TableNode,
11
10
  AliasNode,
12
- ValuesNode,
13
- ValueNode,
11
+ CastNode,
14
12
  ColumnNode,
15
- DefaultInsertValueNode,
16
13
  IdentifierNode,
17
- ReferenceNode,
18
14
  ParensNode,
19
- CastNode,
15
+ ReferenceNode,
20
16
  SelectQueryNode,
17
+ TableNode,
21
18
  } from "kysely";
22
19
 
23
20
  import { JsonParseError } from "./errors.js";
24
- import type { JsonValidation } from "./types.js";
25
21
  import { JsonValidationError } from "./validation-error.js";
26
22
 
27
23
  export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
@@ -29,23 +25,73 @@ export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
29
25
 
30
26
  type ResolvedCoercion = { table: string; coercion: ColumnCoercion };
31
27
 
28
+ type CoercionPlan = {
29
+ kind: "coercion";
30
+ table: string;
31
+ column: string;
32
+ coercion: ColumnCoercion;
33
+ };
34
+
35
+ type ObjectPlan = {
36
+ kind: "object";
37
+ table: string;
38
+ column: string;
39
+ fields: Map<string, ValuePlan>;
40
+ };
41
+
42
+ type ArrayPlan = {
43
+ kind: "array";
44
+ table: string;
45
+ column: string;
46
+ fields: Map<string, ValuePlan>;
47
+ };
48
+
49
+ type ValuePlan = CoercionPlan | ObjectPlan | ArrayPlan;
50
+
51
+ type QueryPlan = {
52
+ table: string | null;
53
+ selectionPlans: Map<string, ValuePlan>;
54
+ };
55
+
56
+ type JsonHelper = {
57
+ kind: "object" | "array";
58
+ query: SelectQueryNode;
59
+ };
60
+
61
+ type RawOperationNode = OperationNode & {
62
+ kind: "RawNode";
63
+ sqlFragments: readonly string[];
64
+ parameters: readonly OperationNode[];
65
+ };
66
+
67
+ const jsonArrayFromFragments = [
68
+ "(select coalesce(json_group_array(json_object(",
69
+ ")), '[]') from ",
70
+ " as agg)",
71
+ ] as const;
72
+
73
+ const jsonObjectFromFragments = [
74
+ "(select json_object(",
75
+ ") from ",
76
+ " as obj)",
77
+ ] as const;
78
+
32
79
  const typePreservingAggregateFunctions = new Set(["max", "min"]);
33
80
 
34
- export class DeserializePlugin implements KyselyPlugin {
35
- private queryNodes = new WeakMap<QueryId, RootOperationNode>();
81
+ export class ResultHydrationPlugin implements KyselyPlugin {
82
+ private queryPlans = new WeakMap<QueryId, QueryPlan>();
36
83
 
37
84
  constructor(
38
85
  private columns: ColumnsMap,
39
86
  private tableColumns: Map<string, Set<string>>,
40
- private validation: Required<JsonValidation>,
87
+ private validation: { onRead: boolean },
41
88
  ) {}
42
89
 
43
90
  transformQuery: KyselyPlugin["transformQuery"] = (args) => {
44
- this.queryNodes.set(args.queryId, args.node);
45
-
46
- if (this.validation.onWrite) {
47
- this.validateWriteNode(args.node);
48
- }
91
+ this.queryPlans.set(args.queryId, {
92
+ table: this.getTableFromNode(args.node),
93
+ selectionPlans: this.getSelectionPlans(args.node),
94
+ });
49
95
 
50
96
  return args.node;
51
97
  };
@@ -99,127 +145,115 @@ export class DeserializePlugin implements KyselyPlugin {
99
145
  }
100
146
  }
101
147
 
102
- private validateWriteNode(node: RootOperationNode) {
103
- if (node.kind !== "InsertQueryNode" && node.kind !== "UpdateQueryNode") {
104
- return;
105
- }
106
-
107
- const table = this.getTableFromNode(node);
108
-
109
- if (!table) {
110
- return;
148
+ private parseJson(table: string, column: string, value: string) {
149
+ try {
150
+ return JSON.parse(value);
151
+ } catch (e) {
152
+ throw new JsonParseError(table, column, value, e);
111
153
  }
154
+ }
112
155
 
113
- const cols = this.columns.get(table);
114
-
115
- if (!cols) {
116
- return;
156
+ private hydrateCoercion(plan: CoercionPlan, value: unknown) {
157
+ if (value === null || value === undefined) {
158
+ return value;
117
159
  }
118
160
 
119
- for (const [col, value] of this.writeValues(node)) {
120
- const coercion = cols.get(col);
121
-
122
- if (!coercion || typeof coercion === "string") {
123
- continue;
161
+ if (plan.coercion === "boolean") {
162
+ if (typeof value === "number") {
163
+ return value === 1;
124
164
  }
125
165
 
126
- this.validateJsonValue(table, col, value, coercion.schema);
166
+ return value;
127
167
  }
128
- }
129
168
 
130
- private *writeValues(node: RootOperationNode) {
131
- if (node.kind === "InsertQueryNode") {
132
- const columns = node.columns?.map((c) => c.column.name);
133
-
134
- if (!columns || !node.values || !ValuesNode.is(node.values)) {
135
- return;
169
+ if (plan.coercion === "date") {
170
+ if (typeof value === "number") {
171
+ return new Date(value * 1000);
136
172
  }
137
173
 
138
- for (const valueList of node.values.values) {
139
- for (let i = 0; i < columns.length; i++) {
140
- const col = columns[i]!;
141
-
142
- if (valueList.kind === "PrimitiveValueListNode") {
143
- yield [col, valueList.values[i]] as [string, unknown];
144
- continue;
145
- }
146
-
147
- const raw = valueList.values[i];
174
+ return value;
175
+ }
148
176
 
149
- if (!raw || DefaultInsertValueNode.is(raw)) {
150
- continue;
151
- }
177
+ const parsed =
178
+ typeof value === "string" ? this.parseJson(plan.table, plan.column, value) : value;
152
179
 
153
- yield [col, ValueNode.is(raw) ? raw.value : raw] as [string, unknown];
154
- }
155
- }
180
+ if (this.validation.onRead) {
181
+ this.validateJsonValue(plan.table, plan.column, parsed, plan.coercion.schema);
182
+ }
156
183
 
157
- for (const update of node.onConflict?.updates ?? []) {
158
- if (ColumnNode.is(update.column) && ValueNode.is(update.value)) {
159
- yield [update.column.column.name, update.value.value] as [string, unknown];
160
- }
161
- }
184
+ return parsed;
185
+ }
162
186
 
163
- return;
187
+ private parseStructuredValue(table: string, column: string, value: unknown) {
188
+ if (value === null || value === undefined) {
189
+ return value;
164
190
  }
165
191
 
166
- if (node.kind !== "UpdateQueryNode" || !node.updates) {
167
- return;
192
+ if (typeof value === "string") {
193
+ return this.parseJson(table, column, value);
168
194
  }
169
195
 
170
- for (const update of node.updates) {
171
- if (ColumnNode.is(update.column) && ValueNode.is(update.value)) {
172
- yield [update.column.column.name, update.value.value] as [string, unknown];
173
- }
174
- }
196
+ return value;
175
197
  }
176
198
 
177
- private coerceSingle(table: string, row: UnknownRow, col: string, coercion: ColumnCoercion) {
178
- if (coercion === "boolean") {
179
- if (typeof row[col] === "number") {
180
- row[col] = row[col] === 1;
181
- }
199
+ private isPlainObject(value: unknown): value is Record<string, unknown> {
200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
201
+ }
182
202
 
183
- return;
203
+ private hydrateObject(plan: ObjectPlan, value: unknown) {
204
+ const parsed = this.parseStructuredValue(plan.table, plan.column, value);
205
+
206
+ if (!this.isPlainObject(parsed)) {
207
+ return parsed;
184
208
  }
185
209
 
186
- if (coercion === "date") {
187
- if (typeof row[col] === "number") {
188
- row[col] = new Date(row[col] * 1000);
210
+ for (const [field, fieldPlan] of plan.fields) {
211
+ if (!(field in parsed)) {
212
+ continue;
189
213
  }
190
214
 
191
- return;
215
+ parsed[field] = this.hydrateValue(fieldPlan, parsed[field]);
192
216
  }
193
217
 
194
- if (typeof row[col] !== "string") {
195
- return;
218
+ return parsed;
219
+ }
220
+
221
+ private hydrateArray(plan: ArrayPlan, value: unknown) {
222
+ const parsed = this.parseStructuredValue(plan.table, plan.column, value);
223
+
224
+ if (!Array.isArray(parsed)) {
225
+ return parsed;
196
226
  }
197
227
 
198
- const value = row[col];
228
+ for (let i = 0; i < parsed.length; i++) {
229
+ const item = parsed[i];
199
230
 
200
- let parsed: unknown;
231
+ if (!this.isPlainObject(item)) {
232
+ continue;
233
+ }
201
234
 
202
- try {
203
- parsed = JSON.parse(value);
204
- } catch (e) {
205
- throw new JsonParseError(table, col, value, e);
206
- }
235
+ for (const [field, fieldPlan] of plan.fields) {
236
+ if (!(field in item)) {
237
+ continue;
238
+ }
207
239
 
208
- if (this.validation.onRead) {
209
- this.validateJsonValue(table, col, parsed, coercion.schema);
240
+ item[field] = this.hydrateValue(fieldPlan, item[field]);
241
+ }
210
242
  }
211
243
 
212
- row[col] = parsed;
244
+ return parsed;
213
245
  }
214
246
 
215
- private coerceRow(table: string, row: UnknownRow, cols: Map<string, ColumnCoercion>) {
216
- for (const [col, coercion] of cols) {
217
- if (!(col in row)) {
218
- continue;
219
- }
247
+ private hydrateValue(plan: ValuePlan, value: unknown): unknown {
248
+ if (plan.kind === "coercion") {
249
+ return this.hydrateCoercion(plan, value);
250
+ }
220
251
 
221
- this.coerceSingle(table, row, col, coercion);
252
+ if (plan.kind === "object") {
253
+ return this.hydrateObject(plan, value);
222
254
  }
255
+
256
+ return this.hydrateArray(plan, value);
223
257
  }
224
258
 
225
259
  private getIdentifierName(node: OperationNode | undefined) {
@@ -313,20 +347,115 @@ export class DeserializePlugin implements KyselyPlugin {
313
347
  return match;
314
348
  }
315
349
 
316
- private resolveSelectionCoercion(
350
+ private isRawNode(node: OperationNode): node is RawOperationNode {
351
+ return node.kind === "RawNode";
352
+ }
353
+
354
+ private matchesFragments(
355
+ fragments: readonly string[],
356
+ expected: readonly [string, string, string],
357
+ ) {
358
+ return (
359
+ fragments.length === expected.length &&
360
+ fragments.every((fragment, index) => fragment === expected[index])
361
+ );
362
+ }
363
+
364
+ private getJsonHelper(node: RawOperationNode): JsonHelper | null {
365
+ const query = node.parameters[1];
366
+
367
+ if (!query || !SelectQueryNode.is(query)) {
368
+ return null;
369
+ }
370
+
371
+ if (this.matchesFragments(node.sqlFragments, jsonObjectFromFragments)) {
372
+ return { kind: "object", query };
373
+ }
374
+
375
+ if (this.matchesFragments(node.sqlFragments, jsonArrayFromFragments)) {
376
+ return { kind: "array", query };
377
+ }
378
+
379
+ return null;
380
+ }
381
+
382
+ private getStructuredFieldPlans(node: SelectQueryNode) {
383
+ const result = new Map<string, ValuePlan>();
384
+
385
+ if (!node.selections) {
386
+ return result;
387
+ }
388
+
389
+ const scope = this.getTableScope(node);
390
+
391
+ for (const selectionNode of node.selections) {
392
+ const output = this.getSelectionOutputName(selectionNode.selection);
393
+
394
+ if (!output) {
395
+ continue;
396
+ }
397
+
398
+ const plan = this.resolveSelectionPlan(selectionNode.selection, scope, output);
399
+
400
+ if (plan) {
401
+ result.set(output, plan);
402
+ }
403
+ }
404
+
405
+ return result;
406
+ }
407
+
408
+ private resolveJsonHelperPlan(node: RawOperationNode, output: string | null): ValuePlan | null {
409
+ if (!output) {
410
+ return null;
411
+ }
412
+
413
+ const helper = this.getJsonHelper(node);
414
+
415
+ if (!helper) {
416
+ return null;
417
+ }
418
+
419
+ const table = this.getTableFromNode(helper.query) ?? output;
420
+ const fields = this.getStructuredFieldPlans(helper.query);
421
+
422
+ if (helper.kind === "object") {
423
+ return { kind: "object", table, column: output, fields };
424
+ }
425
+
426
+ return { kind: "array", table, column: output, fields };
427
+ }
428
+
429
+ private resolveSelectionPlan(
317
430
  node: OperationNode,
318
431
  scope: Map<string, string>,
319
- ): ResolvedCoercion | null {
432
+ output: string | null,
433
+ ): ValuePlan | null {
320
434
  if (AliasNode.is(node)) {
321
- return this.resolveSelectionCoercion(node.node, scope);
435
+ return this.resolveSelectionPlan(node.node, scope, output ?? this.getIdentifierName(node.alias));
322
436
  }
323
437
 
324
438
  if (ReferenceNode.is(node) || ColumnNode.is(node)) {
325
- return this.resolveReferenceCoercion(node, scope);
439
+ if (!output) {
440
+ return null;
441
+ }
442
+
443
+ const resolved = this.resolveReferenceCoercion(node, scope);
444
+
445
+ if (!resolved) {
446
+ return null;
447
+ }
448
+
449
+ return {
450
+ kind: "coercion",
451
+ table: resolved.table,
452
+ column: output,
453
+ coercion: resolved.coercion,
454
+ };
326
455
  }
327
456
 
328
457
  if (SelectQueryNode.is(node)) {
329
- return this.resolveScalarSubqueryCoercion(node);
458
+ return this.resolveScalarSubqueryPlan(node, output);
330
459
  }
331
460
 
332
461
  if (AggregateFunctionNode.is(node)) {
@@ -337,26 +466,30 @@ export class DeserializePlugin implements KyselyPlugin {
337
466
  return null;
338
467
  }
339
468
 
340
- return this.resolveSelectionCoercion(node.aggregated[0]!, scope);
469
+ return this.resolveSelectionPlan(node.aggregated[0]!, scope, output);
341
470
  }
342
471
 
343
472
  if (ParensNode.is(node)) {
344
- return this.resolveSelectionCoercion(node.node, scope);
473
+ return this.resolveSelectionPlan(node.node, scope, output);
345
474
  }
346
475
 
347
476
  if (CastNode.is(node)) {
348
477
  return null;
349
478
  }
350
479
 
480
+ if (this.isRawNode(node)) {
481
+ return this.resolveJsonHelperPlan(node, output);
482
+ }
483
+
351
484
  return null;
352
485
  }
353
486
 
354
- private resolveScalarSubqueryCoercion(node: SelectQueryNode) {
487
+ private resolveScalarSubqueryPlan(node: SelectQueryNode, output: string | null) {
355
488
  if (!node.selections || node.selections.length !== 1) {
356
489
  return null;
357
490
  }
358
491
 
359
- return this.resolveSelectionCoercion(node.selections[0]!.selection, this.getTableScope(node));
492
+ return this.resolveSelectionPlan(node.selections[0]!.selection, this.getTableScope(node), output);
360
493
  }
361
494
 
362
495
  private getSelectionOutputName(node: OperationNode) {
@@ -375,8 +508,8 @@ export class DeserializePlugin implements KyselyPlugin {
375
508
  return null;
376
509
  }
377
510
 
378
- private getSelectCoercions(node: RootOperationNode) {
379
- const result = new Map<string, ResolvedCoercion>();
511
+ private getSelectionPlans(node: RootOperationNode) {
512
+ const result = new Map<string, ValuePlan>();
380
513
 
381
514
  if (node.kind !== "SelectQueryNode" || !node.selections) {
382
515
  return result;
@@ -391,65 +524,86 @@ export class DeserializePlugin implements KyselyPlugin {
391
524
  continue;
392
525
  }
393
526
 
394
- const resolved = this.resolveSelectionCoercion(selectionNode.selection, scope);
527
+ const plan = this.resolveSelectionPlan(selectionNode.selection, scope, output);
395
528
 
396
- if (resolved) {
397
- result.set(output, resolved);
529
+ if (plan) {
530
+ result.set(output, plan);
398
531
  }
399
532
  }
400
533
 
401
534
  return result;
402
535
  }
403
536
 
404
- transformResult: KyselyPlugin["transformResult"] = async (args) => {
405
- const node = this.queryNodes.get(args.queryId);
537
+ private coerceMainRow(table: string, row: UnknownRow, cols: Map<string, ColumnCoercion>) {
538
+ for (const [column, coercion] of cols) {
539
+ if (!(column in row)) {
540
+ continue;
541
+ }
406
542
 
407
- if (!node) {
408
- return args.result;
543
+ row[column] = this.hydrateCoercion({
544
+ kind: "coercion",
545
+ table,
546
+ column,
547
+ coercion,
548
+ }, row[column]);
409
549
  }
550
+ }
410
551
 
411
- const table = this.getTableFromNode(node);
552
+ transformResult: KyselyPlugin["transformResult"] = async (args) => {
553
+ const plan = this.queryPlans.get(args.queryId);
412
554
 
413
- if (!table) {
555
+ if (!plan) {
414
556
  return args.result;
415
557
  }
416
558
 
417
- const mainCols = this.columns.get(table);
418
- const mainTableColumns = this.tableColumns.get(table);
419
- const selectCoercions = this.getSelectCoercions(node);
559
+ const mainCols = plan.table ? this.columns.get(plan.table) : null;
560
+ const mainTableColumns = plan.table ? this.tableColumns.get(plan.table) : null;
420
561
 
421
562
  for (const row of args.result.rows) {
422
- if (mainCols) {
423
- this.coerceRow(table, row, mainCols);
563
+ if (plan.table && mainCols) {
564
+ this.coerceMainRow(plan.table, row, mainCols);
424
565
  }
425
566
 
426
- for (const col of Object.keys(row)) {
427
- const resolved = selectCoercions.get(col);
428
-
429
- if (resolved) {
430
- this.coerceSingle(resolved.table, row, col, resolved.coercion);
567
+ for (const [column, selectionPlan] of plan.selectionPlans) {
568
+ if (!(column in row)) {
431
569
  continue;
432
570
  }
433
571
 
434
- if (mainTableColumns?.has(col)) {
572
+ row[column] = this.hydrateValue(selectionPlan, row[column]);
573
+ }
574
+
575
+ if (!plan.table) {
576
+ continue;
577
+ }
578
+
579
+ for (const column of Object.keys(row)) {
580
+ if (plan.selectionPlans.has(column) || mainTableColumns?.has(column)) {
435
581
  continue;
436
582
  }
437
583
 
438
584
  for (const [otherTable, otherCols] of this.columns) {
439
- if (otherTable === table) {
585
+ if (otherTable === plan.table) {
440
586
  continue;
441
587
  }
442
588
 
443
- const coercion = otherCols.get(col);
589
+ const coercion = otherCols.get(column);
444
590
 
445
- if (coercion) {
446
- this.coerceSingle(otherTable, row, col, coercion);
447
- break;
591
+ if (!coercion) {
592
+ continue;
448
593
  }
594
+
595
+ row[column] = this.hydrateCoercion({
596
+ kind: "coercion",
597
+ table: otherTable,
598
+ column,
599
+ coercion,
600
+ }, row[column]);
601
+
602
+ break;
449
603
  }
450
604
  }
451
605
  }
452
606
 
453
- return { ...args.result };
607
+ return args.result;
454
608
  };
455
609
  }
@@ -0,0 +1,137 @@
1
+ import { type, type Type } from "arktype";
2
+ import {
3
+ type KyselyPlugin,
4
+ type RootOperationNode,
5
+ ColumnNode,
6
+ DefaultInsertValueNode,
7
+ TableNode,
8
+ ValueNode,
9
+ ValuesNode,
10
+ } from "kysely";
11
+
12
+ import type { ColumnsMap } from "./plugin.js";
13
+ import { JsonValidationError } from "./validation-error.js";
14
+
15
+ export class WriteValidationPlugin implements KyselyPlugin {
16
+ constructor(
17
+ private columns: ColumnsMap,
18
+ private validation: { onWrite: boolean },
19
+ ) {}
20
+
21
+ transformQuery: KyselyPlugin["transformQuery"] = (args) => {
22
+ if (this.validation.onWrite) {
23
+ this.validateWriteNode(args.node);
24
+ }
25
+
26
+ return args.node;
27
+ };
28
+
29
+ transformResult: KyselyPlugin["transformResult"] = async (args) => {
30
+ return args.result;
31
+ };
32
+
33
+ private getTableFromNode(node: RootOperationNode) {
34
+ switch (node.kind) {
35
+ case "InsertQueryNode":
36
+ return node.into?.table.identifier.name ?? null;
37
+
38
+ case "UpdateQueryNode": {
39
+ if (node.table && TableNode.is(node.table)) {
40
+ return node.table.table.identifier.name;
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ default:
47
+ return null;
48
+ }
49
+ }
50
+
51
+ private validateJsonValue(table: string, col: string, value: unknown, schema: Type) {
52
+ if (value === null || value === undefined) {
53
+ return;
54
+ }
55
+
56
+ const result = schema(value);
57
+
58
+ if (result instanceof type.errors) {
59
+ throw new JsonValidationError(table, col, result.summary);
60
+ }
61
+ }
62
+
63
+ private validateWriteNode(node: RootOperationNode) {
64
+ if (node.kind !== "InsertQueryNode" && node.kind !== "UpdateQueryNode") {
65
+ return;
66
+ }
67
+
68
+ const table = this.getTableFromNode(node);
69
+
70
+ if (!table) {
71
+ return;
72
+ }
73
+
74
+ const cols = this.columns.get(table);
75
+
76
+ if (!cols) {
77
+ return;
78
+ }
79
+
80
+ for (const [col, value] of this.writeValues(node)) {
81
+ const coercion = cols.get(col);
82
+
83
+ if (!coercion || typeof coercion === "string") {
84
+ continue;
85
+ }
86
+
87
+ this.validateJsonValue(table, col, value, coercion.schema);
88
+ }
89
+ }
90
+
91
+ private *writeValues(node: RootOperationNode) {
92
+ if (node.kind === "InsertQueryNode") {
93
+ const columns = node.columns?.map((column) => column.column.name);
94
+
95
+ if (!columns || !node.values || !ValuesNode.is(node.values)) {
96
+ return;
97
+ }
98
+
99
+ for (const valueList of node.values.values) {
100
+ for (let i = 0; i < columns.length; i++) {
101
+ const col = columns[i]!;
102
+
103
+ if (valueList.kind === "PrimitiveValueListNode") {
104
+ yield [col, valueList.values[i]] as [string, unknown];
105
+ continue;
106
+ }
107
+
108
+ const raw = valueList.values[i];
109
+
110
+ if (!raw || DefaultInsertValueNode.is(raw)) {
111
+ continue;
112
+ }
113
+
114
+ yield [col, ValueNode.is(raw) ? raw.value : raw] as [string, unknown];
115
+ }
116
+ }
117
+
118
+ for (const update of node.onConflict?.updates ?? []) {
119
+ if (ColumnNode.is(update.column) && ValueNode.is(update.value)) {
120
+ yield [update.column.column.name, update.value.value] as [string, unknown];
121
+ }
122
+ }
123
+
124
+ return;
125
+ }
126
+
127
+ if (node.kind !== "UpdateQueryNode" || !node.updates) {
128
+ return;
129
+ }
130
+
131
+ for (const update of node.updates) {
132
+ if (ColumnNode.is(update.column) && ValueNode.is(update.value)) {
133
+ yield [update.column.column.name, update.value.value] as [string, unknown];
134
+ }
135
+ }
136
+ }
137
+ }