@lobomfz/db 0.3.9 → 0.4.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 CHANGED
@@ -5,7 +5,7 @@ SQLite database with Arktype schemas and typed Kysely client for Bun.
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- bun add @lobomfz/db arktype kysely
8
+ bun add @lobomfz/db
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -65,12 +65,12 @@ type("string").configure({ unique: true }); // UNIQUE
65
65
  type("number.integer").configure({ references: "users.id", onDelete: "cascade" }); // FK
66
66
  ```
67
67
 
68
- JSON columns are validated against the schema on write by default. To also validate on read, or to disable write validation:
68
+ JSON columns are validated against the schema on write by default. To also validate on read:
69
69
 
70
70
  ```typescript
71
71
  new Database({
72
72
  // ...
73
- validation: { onRead: true }, // default: { onRead: false, onWrite: true }
73
+ validation: { onRead: true }, // default: { onRead: false }
74
74
  });
75
75
  ```
76
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobomfz/db",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "Bun SQLite database with Arktype schemas and typed Kysely client",
5
5
  "keywords": [
6
6
  "arktype",
package/src/database.ts CHANGED
@@ -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 { ResultHydrationPlugin, type ColumnCoercion, type ColumnsMap } from "./plugin.js";
12
+ import { ResultHydrationPlugin } from "./plugin.js";
13
13
  import type {
14
14
  DatabaseOptions,
15
15
  IndexDefinition,
@@ -67,14 +67,19 @@ const defaultPragmas: DatabasePragmas = {
67
67
  export class Database<T extends SchemaRecord> {
68
68
  private sqlite: BunDatabase;
69
69
 
70
- private columns: ColumnsMap = new Map();
71
- private tableColumns = new Map<string, Set<string>>();
72
-
73
70
  readonly infer: TablesFromSchemas<T> = undefined as any;
74
71
 
75
72
  readonly kysely: Kysely<TablesFromSchemas<T>>;
76
73
 
77
74
  constructor(private options: DatabaseOptions<T>) {
75
+ const tableSchemas = new Map<string, Type>(Object.entries(options.schema.tables));
76
+ const writeSchemas = new Map<string, Type>(
77
+ Object.entries(options.schema.tables).map(([table, schema]) => [
78
+ table,
79
+ this.createWriteSchema(schema),
80
+ ]),
81
+ );
82
+
78
83
  this.sqlite = new BunDatabase(options.path);
79
84
 
80
85
  this.applyPragmas();
@@ -83,14 +88,13 @@ export class Database<T extends SchemaRecord> {
83
88
 
84
89
  const validation = {
85
90
  onRead: options.validation?.onRead ?? false,
86
- onWrite: options.validation?.onWrite ?? true,
87
91
  };
88
92
 
89
93
  this.kysely = new Kysely<TablesFromSchemas<T>>({
90
94
  dialect: new BunSqliteDialect({ database: this.sqlite }),
91
95
  plugins: [
92
- new WriteValidationPlugin(this.columns, validation),
93
- new ResultHydrationPlugin(this.columns, this.tableColumns, validation),
96
+ new WriteValidationPlugin(writeSchemas),
97
+ new ResultHydrationPlugin(tableSchemas, validation),
94
98
  ],
95
99
  });
96
100
  }
@@ -293,30 +297,16 @@ export class Database<T extends SchemaRecord> {
293
297
  return structureProps.map((p) => this.normalizeProp(p, schema));
294
298
  }
295
299
 
296
- private registerColumns(tableName: string, props: Prop[]) {
297
- this.tableColumns.set(tableName, new Set(props.map((p) => p.key)));
298
-
299
- const colMap = new Map<string, ColumnCoercion>();
300
-
301
- for (const prop of props) {
302
- if (prop.isBoolean) {
303
- colMap.set(prop.key, "boolean");
304
- continue;
305
- }
306
-
307
- if (prop.isDate) {
308
- colMap.set(prop.key, "date");
309
- continue;
310
- }
300
+ private createWriteSchema(schema: Type) {
301
+ const autoIncrementColumns = this.parseSchemaProps(schema)
302
+ .filter((prop) => prop.generated === "autoincrement")
303
+ .map((prop) => prop.key);
311
304
 
312
- if (prop.isJson && prop.jsonSchema) {
313
- colMap.set(prop.key, { type: "json", schema: prop.jsonSchema });
314
- }
305
+ if (autoIncrementColumns.length === 0) {
306
+ return schema;
315
307
  }
316
308
 
317
- if (colMap.size > 0) {
318
- this.columns.set(tableName, colMap);
319
- }
309
+ return (schema as any).omit(...autoIncrementColumns) as Type;
320
310
  }
321
311
 
322
312
  private generateCreateTableSQL(tableName: string, props: Prop[]) {
@@ -343,8 +333,6 @@ export class Database<T extends SchemaRecord> {
343
333
  for (const [name, schema] of Object.entries(this.options.schema.tables)) {
344
334
  const props = this.parseSchemaProps(schema);
345
335
 
346
- this.registerColumns(name, props);
347
-
348
336
  const columns = props.map((prop) => {
349
337
  const isNotNull = this.columnConstraint(prop) === "NOT NULL";
350
338
  const defaultClause = this.defaultClause(prop);
package/src/generated.ts CHANGED
@@ -7,7 +7,7 @@ const generatedTypes = {
7
7
  now: () =>
8
8
  type("Date")
9
9
  .configure({ _generated: "now" })
10
- .default(() => new Date(0)),
10
+ .default(() => new Date()),
11
11
  };
12
12
 
13
13
  export function generated<P extends GeneratedPreset>(
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ export {
12
12
  } from "kysely";
13
13
  export { type, type Type } from "arktype";
14
14
  export { configure } from "arktype/config";
15
- export { JsonValidationError } from "./validation-error.js";
15
+ export { ValidationError } from "./validation-error.js";
16
16
  export type { DbFieldMeta } from "./env.js";
17
17
  export type {
18
18
  DatabaseOptions,
package/src/plugin.ts CHANGED
@@ -18,10 +18,10 @@ import {
18
18
  } from "kysely";
19
19
 
20
20
  import { JsonParseError } from "./errors.js";
21
- import { JsonValidationError } from "./validation-error.js";
21
+ import type { StructureProp } from "./types.js";
22
+ import { ValidationError } from "./validation-error.js";
22
23
 
23
- export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
24
- export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
24
+ type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
25
25
 
26
26
  type ResolvedCoercion = { table: string; coercion: ColumnCoercion };
27
27
 
@@ -79,13 +79,70 @@ const jsonObjectFromFragments = [
79
79
  const typePreservingAggregateFunctions = new Set(["max", "min"]);
80
80
 
81
81
  export class ResultHydrationPlugin implements KyselyPlugin {
82
+ private columns = new Map<string, Map<string, ColumnCoercion>>();
83
+ private tableColumns = new Map<string, Set<string>>();
82
84
  private queryPlans = new WeakMap<QueryId, QueryPlan>();
83
85
 
84
86
  constructor(
85
- private columns: ColumnsMap,
86
- private tableColumns: Map<string, Set<string>>,
87
+ schemas: Map<string, Type>,
87
88
  private validation: { onRead: boolean },
88
- ) {}
89
+ ) {
90
+ this.registerSchemas(schemas);
91
+ }
92
+
93
+ private registerSchemas(schemas: Map<string, Type>) {
94
+ for (const [table, schema] of schemas) {
95
+ const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
96
+
97
+ if (!structureProps) {
98
+ continue;
99
+ }
100
+
101
+ this.tableColumns.set(
102
+ table,
103
+ new Set(structureProps.map((prop) => prop.key)),
104
+ );
105
+
106
+ const columns = new Map<string, ColumnCoercion>();
107
+
108
+ for (const prop of structureProps) {
109
+ const coercion = this.getColumnCoercion(prop, schema);
110
+
111
+ if (!coercion) {
112
+ continue;
113
+ }
114
+
115
+ columns.set(prop.key, coercion);
116
+ }
117
+
118
+ if (columns.size > 0) {
119
+ this.columns.set(table, columns);
120
+ }
121
+ }
122
+ }
123
+
124
+ private getColumnCoercion(prop: StructureProp, parentSchema: Type) {
125
+ const concrete = prop.value.branches.filter(
126
+ (branch) => branch.unit !== null && branch.domain !== "undefined",
127
+ );
128
+
129
+ if (prop.value.proto === Date || concrete.some((branch) => branch.proto === Date)) {
130
+ return "date" satisfies ColumnCoercion;
131
+ }
132
+
133
+ if (concrete.length > 0 && concrete.every((branch) => branch.domain === "boolean")) {
134
+ return "boolean" satisfies ColumnCoercion;
135
+ }
136
+
137
+ if (concrete.some((branch) => !!branch.structure)) {
138
+ return {
139
+ type: "json",
140
+ schema: (parentSchema as any).get(prop.key) as Type,
141
+ } satisfies ColumnCoercion;
142
+ }
143
+
144
+ return null;
145
+ }
89
146
 
90
147
  transformQuery: KyselyPlugin["transformQuery"] = (args) => {
91
148
  this.queryPlans.set(args.queryId, {
@@ -141,7 +198,7 @@ export class ResultHydrationPlugin implements KyselyPlugin {
141
198
  const result = schema(value);
142
199
 
143
200
  if (result instanceof type.errors) {
144
- throw new JsonValidationError(table, col, result.summary);
201
+ throw new ValidationError(table, result.summary, col);
145
202
  }
146
203
  }
147
204
 
package/src/types.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  import type { Generated } from "kysely";
2
2
  import type { Type } from "arktype";
3
3
 
4
+ export type ArkBranch = {
5
+ domain?: string;
6
+ proto?: unknown;
7
+ unit?: unknown;
8
+ structure?: unknown;
9
+ };
10
+
11
+ export type StructureProp = {
12
+ key: string;
13
+ required: boolean;
14
+ inner: { default?: unknown };
15
+ value: Type & {
16
+ branches: ArkBranch[];
17
+ proto?: unknown;
18
+ };
19
+ };
20
+
4
21
  type ExtractInput<T> = T extends { inferIn: infer I } ? I : never;
5
22
  type ExtractOutput<T> = T extends { infer: infer O } ? O : never;
6
23
 
@@ -60,7 +77,6 @@ export type DatabaseSchema<T extends SchemaRecord> = {
60
77
 
61
78
  export type JsonValidation = {
62
79
  onRead?: boolean;
63
- onWrite?: boolean;
64
80
  };
65
81
 
66
82
  export type DatabaseOptions<T extends SchemaRecord> = {
@@ -1,11 +1,15 @@
1
- export class JsonValidationError extends Error {
1
+ export class ValidationError extends Error {
2
2
  constructor(
3
3
  readonly table: string,
4
- readonly column: string,
5
4
  readonly summary: string,
5
+ readonly column: string | null = null,
6
6
  ) {
7
- super(`JSON validation failed for ${table}.${column}: ${summary}`);
7
+ super(
8
+ column
9
+ ? `Validation failed for ${table}.${column}: ${summary}`
10
+ : `Validation failed for ${table}: ${summary}`,
11
+ );
8
12
 
9
- this.name = "JsonValidationError";
13
+ this.name = "ValidationError";
10
14
  }
11
15
  }
@@ -1,35 +1,84 @@
1
1
  import { type, type Type } from "arktype";
2
2
  import {
3
3
  type KyselyPlugin,
4
+ type InsertQueryNode as KyselyInsertQueryNode,
5
+ type UpdateQueryNode as KyselyUpdateQueryNode,
6
+ type OnConflictNode as KyselyOnConflictNode,
7
+ type ColumnUpdateNode as KyselyColumnUpdateNode,
8
+ type ValuesItemNode,
9
+ type OperationNode,
4
10
  type RootOperationNode,
5
11
  ColumnNode,
12
+ ColumnUpdateNode,
6
13
  DefaultInsertValueNode,
14
+ InsertQueryNode,
15
+ OnConflictNode,
16
+ PrimitiveValueListNode,
17
+ ReferenceNode,
7
18
  TableNode,
19
+ ValueListNode,
8
20
  ValueNode,
9
21
  ValuesNode,
10
22
  } from "kysely";
11
23
 
12
- import type { ColumnsMap } from "./plugin.js";
13
- import { JsonValidationError } from "./validation-error.js";
24
+ import type { StructureProp } from "./types.js";
25
+ import { ValidationError } from "./validation-error.js";
26
+
27
+ type TableWriteSchema = {
28
+ schema: Type;
29
+ columns: string[];
30
+ columnSet: Set<string>;
31
+ optionalNonNullColumns: Set<string>;
32
+ insertNullColumns: Set<string>;
33
+ };
34
+
35
+ type InsertRow = {
36
+ values: Record<string, unknown>;
37
+ passthrough: Map<string, OperationNode>;
38
+ };
14
39
 
15
40
  export class WriteValidationPlugin implements KyselyPlugin {
16
- constructor(
17
- private columns: ColumnsMap,
18
- private validation: { onWrite: boolean },
19
- ) {}
41
+ private schemas = new Map<string, TableWriteSchema>();
20
42
 
21
- transformQuery: KyselyPlugin["transformQuery"] = (args) => {
22
- if (this.validation.onWrite) {
23
- this.validateWriteNode(args.node);
24
- }
43
+ constructor(schemas: Map<string, Type>) {
44
+ this.registerSchemas(schemas);
45
+ }
25
46
 
26
- return args.node;
47
+ transformQuery: KyselyPlugin["transformQuery"] = (args) => {
48
+ return this.transformWriteNode(args.node);
27
49
  };
28
50
 
29
51
  transformResult: KyselyPlugin["transformResult"] = async (args) => {
30
52
  return args.result;
31
53
  };
32
54
 
55
+ private registerSchemas(schemas: Map<string, Type>) {
56
+ for (const [table, schema] of schemas) {
57
+ const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
58
+ const columns = structureProps?.map((prop) => prop.key) ?? [];
59
+
60
+ this.schemas.set(table, {
61
+ schema,
62
+ columns,
63
+ columnSet: new Set(columns),
64
+ optionalNonNullColumns: new Set(
65
+ structureProps
66
+ ?.filter((prop) => !prop.required && !this.acceptsNull(prop.value))
67
+ .map((prop) => prop.key) ?? [],
68
+ ),
69
+ insertNullColumns: new Set(
70
+ structureProps
71
+ ?.filter((prop) => this.acceptsNull(prop.value) && prop.inner.default === undefined)
72
+ .map((prop) => prop.key) ?? [],
73
+ ),
74
+ });
75
+ }
76
+ }
77
+
78
+ private acceptsNull(field: StructureProp["value"]) {
79
+ return field.branches.some((branch) => branch.unit === null || branch.domain === "null");
80
+ }
81
+
33
82
  private getTableFromNode(node: RootOperationNode) {
34
83
  switch (node.kind) {
35
84
  case "InsertQueryNode":
@@ -48,90 +97,316 @@ export class WriteValidationPlugin implements KyselyPlugin {
48
97
  }
49
98
  }
50
99
 
51
- private validateJsonValue(table: string, col: string, value: unknown, schema: Type) {
52
- if (value === null || value === undefined) {
53
- return;
100
+ private firstErrorColumn(result: { [index: number]: { path: ArrayLike<unknown> } | undefined }) {
101
+ const column = result[0]?.path[0];
102
+
103
+ if (typeof column === "string") {
104
+ return column;
54
105
  }
55
106
 
107
+ return null;
108
+ }
109
+
110
+ private morph(table: string, schema: Type, value: Record<string, unknown>) {
56
111
  const result = schema(value);
57
112
 
58
113
  if (result instanceof type.errors) {
59
- throw new JsonValidationError(table, col, result.summary);
114
+ throw new ValidationError(table, result.summary, this.firstErrorColumn(result));
60
115
  }
116
+
117
+ return result as Record<string, unknown>;
118
+ }
119
+
120
+ private pickSchema(schema: Type, columns: Iterable<string>) {
121
+ return (schema as any).pick(...columns) as Type;
61
122
  }
62
123
 
63
- private validateWriteNode(node: RootOperationNode) {
64
- if (node.kind !== "InsertQueryNode" && node.kind !== "UpdateQueryNode") {
65
- return;
124
+ private stripNullOptionalFields(tableSchema: TableWriteSchema, value: Record<string, unknown>) {
125
+ const stripped = { ...value };
126
+
127
+ for (const column of tableSchema.optionalNonNullColumns) {
128
+ if (stripped[column] === null) {
129
+ delete stripped[column];
130
+ }
66
131
  }
67
132
 
133
+ return stripped;
134
+ }
135
+
136
+ private prepareInsertValue(tableSchema: TableWriteSchema, value: Record<string, unknown>) {
137
+ const prepared = this.stripNullOptionalFields(tableSchema, value);
138
+
139
+ for (const column of tableSchema.insertNullColumns) {
140
+ if (!Object.prototype.hasOwnProperty.call(prepared, column)) {
141
+ prepared[column] = null;
142
+ }
143
+ }
144
+
145
+ return prepared;
146
+ }
147
+
148
+ private transformWriteNode(node: RootOperationNode) {
68
149
  const table = this.getTableFromNode(node);
69
150
 
70
151
  if (!table) {
71
- return;
152
+ return node;
153
+ }
154
+
155
+ if (node.kind === "InsertQueryNode") {
156
+ return this.transformInsert(node, table);
72
157
  }
73
158
 
74
- const cols = this.columns.get(table);
159
+ if (node.kind === "UpdateQueryNode") {
160
+ return this.transformUpdate(node, table);
161
+ }
162
+
163
+ return node;
164
+ }
165
+
166
+ private getInsertRow(columns: string[], valueList: ValuesItemNode) {
167
+ const row: InsertRow = {
168
+ values: {},
169
+ passthrough: new Map(),
170
+ };
171
+
172
+ if (PrimitiveValueListNode.is(valueList)) {
173
+ for (let i = 0; i < columns.length; i++) {
174
+ row.values[columns[i]!] = valueList.values[i];
175
+ }
75
176
 
76
- if (!cols) {
77
- return;
177
+ return row;
78
178
  }
79
179
 
80
- for (const [col, value] of this.writeValues(node)) {
81
- const coercion = cols.get(col);
180
+ if (!ValueListNode.is(valueList)) {
181
+ return null;
182
+ }
82
183
 
83
- if (!coercion || typeof coercion === "string") {
184
+ for (let i = 0; i < columns.length; i++) {
185
+ const column = columns[i]!;
186
+ const value = valueList.values[i];
187
+
188
+ if (!value || DefaultInsertValueNode.is(value)) {
84
189
  continue;
85
190
  }
86
191
 
87
- this.validateJsonValue(table, col, value, coercion.schema);
192
+ if (ValueNode.is(value)) {
193
+ row.values[column] = value.value;
194
+ continue;
195
+ }
196
+
197
+ row.passthrough.set(column, value);
88
198
  }
199
+
200
+ return row;
89
201
  }
90
202
 
91
- private *writeValues(node: RootOperationNode) {
92
- if (node.kind === "InsertQueryNode") {
93
- const columns = node.columns?.map((column) => column.column.name);
203
+ private morphInsertRow(table: string, tableSchema: TableWriteSchema, row: InsertRow) {
204
+ const schemaLiteralValues = Object.fromEntries(
205
+ Object.entries(row.values).filter(([column]) => tableSchema.columnSet.has(column)),
206
+ );
207
+ const nonSchemaLiteralValues = Object.fromEntries(
208
+ Object.entries(row.values).filter(([column]) => !tableSchema.columnSet.has(column)),
209
+ );
210
+ const schemaLiteralColumns = Object.keys(schemaLiteralValues);
94
211
 
95
- if (!columns || !node.values || !ValuesNode.is(node.values)) {
96
- return;
212
+ if (schemaLiteralColumns.length === 0) {
213
+ return row;
214
+ }
215
+
216
+ const morphedSchemaValues = this.morph(
217
+ table,
218
+ this.pickSchema(tableSchema.schema, schemaLiteralColumns),
219
+ this.prepareInsertValue(tableSchema, schemaLiteralValues),
220
+ );
221
+
222
+ return {
223
+ values: { ...nonSchemaLiteralValues, ...morphedSchemaValues },
224
+ passthrough: row.passthrough,
225
+ };
226
+ }
227
+
228
+ private getInsertColumns(
229
+ tableSchema: TableWriteSchema,
230
+ originalColumns: string[],
231
+ rows: InsertRow[],
232
+ ) {
233
+ const columns = [...originalColumns];
234
+
235
+ for (const column of tableSchema.columns) {
236
+ if (columns.includes(column)) {
237
+ continue;
97
238
  }
98
239
 
99
- for (const valueList of node.values.values) {
100
- for (let i = 0; i < columns.length; i++) {
101
- const col = columns[i]!;
240
+ if (rows.some((row) => Object.prototype.hasOwnProperty.call(row.values, column))) {
241
+ columns.push(column);
242
+ }
243
+ }
244
+
245
+ for (const row of rows) {
246
+ for (const column of Object.keys(row.values)) {
247
+ if (!columns.includes(column)) {
248
+ columns.push(column);
249
+ }
250
+ }
251
+ }
102
252
 
103
- if (valueList.kind === "PrimitiveValueListNode") {
104
- yield [col, valueList.values[i]] as [string, unknown];
105
- continue;
106
- }
253
+ return columns;
254
+ }
107
255
 
108
- const raw = valueList.values[i];
256
+ private createInsertValueList(columns: string[], row: InsertRow) {
257
+ return ValueListNode.create(
258
+ columns.map((column) => {
259
+ const passthrough = row.passthrough.get(column);
109
260
 
110
- if (!raw || DefaultInsertValueNode.is(raw)) {
111
- continue;
112
- }
261
+ if (passthrough) {
262
+ return passthrough;
263
+ }
113
264
 
114
- yield [col, ValueNode.is(raw) ? raw.value : raw] as [string, unknown];
265
+ if (Object.prototype.hasOwnProperty.call(row.values, column)) {
266
+ return ValueNode.create(row.values[column]);
115
267
  }
268
+
269
+ return DefaultInsertValueNode.create();
270
+ }),
271
+ );
272
+ }
273
+
274
+ private transformInsert(node: KyselyInsertQueryNode, table: string) {
275
+ const onConflict = node.onConflict
276
+ ? this.transformOnConflict(table, node.onConflict)
277
+ : undefined;
278
+ const tableSchema = this.schemas.get(table);
279
+
280
+ if (!tableSchema) {
281
+ if (onConflict) {
282
+ return InsertQueryNode.cloneWith(node, { onConflict });
283
+ }
284
+
285
+ return node;
286
+ }
287
+
288
+ const columns = node.columns?.map((column) => column.column.name);
289
+
290
+ if (!columns || !node.values || !ValuesNode.is(node.values)) {
291
+ if (onConflict) {
292
+ return InsertQueryNode.cloneWith(node, { onConflict });
116
293
  }
117
294
 
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];
295
+ return node;
296
+ }
297
+
298
+ const rows: InsertRow[] = [];
299
+
300
+ for (const valueList of node.values.values) {
301
+ const row = this.getInsertRow(columns, valueList);
302
+
303
+ if (!row) {
304
+ if (onConflict) {
305
+ return InsertQueryNode.cloneWith(node, { onConflict });
121
306
  }
307
+
308
+ return node;
309
+ }
310
+
311
+ rows.push(this.morphInsertRow(table, tableSchema, row));
312
+ }
313
+
314
+ const insertColumns = this.getInsertColumns(tableSchema, columns, rows);
315
+
316
+ return InsertQueryNode.cloneWith(node, {
317
+ columns: insertColumns.map((column) => ColumnNode.create(column)),
318
+ values: ValuesNode.create(rows.map((row) => this.createInsertValueList(insertColumns, row))),
319
+ onConflict,
320
+ });
321
+ }
322
+
323
+ private transformUpdates(table: string, updates: readonly KyselyColumnUpdateNode[]) {
324
+ const tableSchema = this.schemas.get(table);
325
+
326
+ if (!tableSchema) {
327
+ return updates;
328
+ }
329
+
330
+ const literalColumns = new Set<string>();
331
+ const literalValues: Record<string, unknown> = {};
332
+ const nullOptionalColumns = new Set<string>();
333
+
334
+ for (const update of updates) {
335
+ if (!ColumnNode.is(update.column) || !ValueNode.is(update.value)) {
336
+ continue;
337
+ }
338
+
339
+ const column = update.column.column.name;
340
+
341
+ if (!tableSchema.columnSet.has(column)) {
342
+ continue;
343
+ }
344
+
345
+ if (update.value.value === null && tableSchema.optionalNonNullColumns.has(column)) {
346
+ nullOptionalColumns.add(column);
347
+ continue;
122
348
  }
123
349
 
124
- return;
350
+ literalColumns.add(column);
351
+ literalValues[column] = update.value.value;
125
352
  }
126
353
 
127
- if (node.kind !== "UpdateQueryNode" || !node.updates) {
128
- return;
354
+ if (literalColumns.size === 0 && nullOptionalColumns.size === 0) {
355
+ return updates;
129
356
  }
130
357
 
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];
358
+ const morphed =
359
+ literalColumns.size === 0
360
+ ? {}
361
+ : this.morph(
362
+ table,
363
+ this.pickSchema(tableSchema.schema, literalColumns),
364
+ this.stripNullOptionalFields(tableSchema, literalValues),
365
+ );
366
+
367
+ return updates.flatMap((update) => {
368
+ if (!ColumnNode.is(update.column) || !ValueNode.is(update.value)) {
369
+ return [update];
134
370
  }
371
+
372
+ const column = update.column.column.name;
373
+
374
+ if (!literalColumns.has(column) || !tableSchema.columnSet.has(column)) {
375
+ if (nullOptionalColumns.has(column)) {
376
+ return [ColumnUpdateNode.create(update.column, ValueNode.create(null))];
377
+ }
378
+
379
+ return [update];
380
+ }
381
+
382
+ if (!Object.prototype.hasOwnProperty.call(morphed, column)) {
383
+ return [
384
+ ColumnUpdateNode.create(update.column, ReferenceNode.create(ColumnNode.create(column))),
385
+ ];
386
+ }
387
+
388
+ return [ColumnUpdateNode.create(update.column, ValueNode.create(morphed[column]))];
389
+ });
390
+ }
391
+
392
+ private transformOnConflict(table: string, node: KyselyOnConflictNode) {
393
+ if (!node.updates) {
394
+ return node;
395
+ }
396
+
397
+ return OnConflictNode.cloneWith(node, {
398
+ updates: this.transformUpdates(table, node.updates),
399
+ });
400
+ }
401
+
402
+ private transformUpdate(node: KyselyUpdateQueryNode, table: string) {
403
+ if (!node.updates) {
404
+ return node;
135
405
  }
406
+
407
+ return Object.freeze({
408
+ ...node,
409
+ updates: this.transformUpdates(table, node.updates),
410
+ });
136
411
  }
137
412
  }