@lobomfz/db 0.3.6 → 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/README.md +13 -13
- package/package.json +4 -2
- package/src/database.ts +31 -24
- package/src/dialect/connection.ts +3 -1
- package/src/dialect/dialect.ts +3 -2
- package/src/dialect/driver.ts +5 -3
- package/src/env.ts +1 -0
- package/src/generated.ts +2 -5
- package/src/index.ts +17 -8
- package/src/migration/diff.ts +33 -7
- package/src/migration/execute.ts +2 -1
- package/src/migration/introspect.ts +4 -2
- package/src/plugin.ts +294 -128
- package/src/write-validation-plugin.ts +137 -0
package/README.md
CHANGED
|
@@ -80,20 +80,20 @@ Schema changes are applied automatically on startup. Every time `new Database(..
|
|
|
80
80
|
|
|
81
81
|
### What's supported
|
|
82
82
|
|
|
83
|
-
| Change
|
|
84
|
-
|
|
85
|
-
| New table
|
|
86
|
-
| Removed table
|
|
87
|
-
| New nullable column
|
|
83
|
+
| Change | Strategy |
|
|
84
|
+
| -------------------------------- | ------------------------ |
|
|
85
|
+
| New table | `CREATE TABLE` |
|
|
86
|
+
| Removed table | `DROP TABLE` |
|
|
87
|
+
| New nullable column | `ALTER TABLE ADD COLUMN` |
|
|
88
88
|
| New NOT NULL column with DEFAULT | `ALTER TABLE ADD COLUMN` |
|
|
89
|
-
| Removed column
|
|
90
|
-
| Type change
|
|
91
|
-
| Nullability change
|
|
92
|
-
| DEFAULT change
|
|
93
|
-
| UNIQUE added/removed
|
|
94
|
-
| FK added/removed/changed
|
|
95
|
-
| Index added
|
|
96
|
-
| Index removed
|
|
89
|
+
| Removed column | Table rebuild |
|
|
90
|
+
| Type change | Table rebuild |
|
|
91
|
+
| Nullability change | Table rebuild |
|
|
92
|
+
| DEFAULT change | Table rebuild |
|
|
93
|
+
| UNIQUE added/removed | Table rebuild |
|
|
94
|
+
| FK added/removed/changed | Table rebuild |
|
|
95
|
+
| Index added | `CREATE INDEX` |
|
|
96
|
+
| Index removed | `DROP INDEX` |
|
|
97
97
|
|
|
98
98
|
Table rebuilds follow SQLite's [recommended procedure](https://www.sqlite.org/lang_altertable.html#otheralter): create a new table with the target schema, copy data from the old table, drop the old table, rename the new one. Foreign keys are disabled during rebuilds and validated via `PRAGMA foreign_key_check` before committing.
|
|
99
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobomfz/db",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Bun SQLite database with Arktype schemas and typed Kysely client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arktype",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
".": "./src/index.ts"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"check": "tsgo && oxlint"
|
|
31
|
+
"check": "tsgo && oxlint && bun test.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {},
|
|
34
34
|
"devDependencies": {
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"oxlint": "^1.57.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
+
"@ark/schema": ">=0.56.0",
|
|
42
|
+
"@ark/util": ">=0.56.0",
|
|
41
43
|
"arktype": "^2.1.29",
|
|
42
44
|
"kysely": "^0.28.14"
|
|
43
45
|
}
|
package/src/database.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2
2
|
|
|
3
3
|
import type { Type } from "arktype";
|
|
4
|
-
import { Kysely
|
|
5
|
-
|
|
6
|
-
import { BunSqliteDialect } from "./dialect/dialect";
|
|
7
|
-
import type { DbFieldMeta } from "./env";
|
|
8
|
-
import type { GeneratedPreset } from "./generated";
|
|
9
|
-
import { Differ, type DesiredTable } from "./migration/diff";
|
|
10
|
-
import { Executor } from "./migration/execute";
|
|
11
|
-
import { Introspector } from "./migration/introspect";
|
|
12
|
-
import {
|
|
4
|
+
import { Kysely } from "kysely";
|
|
5
|
+
|
|
6
|
+
import { BunSqliteDialect } from "./dialect/dialect.js";
|
|
7
|
+
import type { DbFieldMeta } from "./env.js";
|
|
8
|
+
import type { GeneratedPreset } from "./generated.js";
|
|
9
|
+
import { Differ, type DesiredTable } from "./migration/diff.js";
|
|
10
|
+
import { Executor } from "./migration/execute.js";
|
|
11
|
+
import { Introspector } from "./migration/introspect.js";
|
|
12
|
+
import { ResultHydrationPlugin, type ColumnCoercion, type ColumnsMap } from "./plugin.js";
|
|
13
13
|
import type {
|
|
14
14
|
DatabaseOptions,
|
|
15
15
|
IndexDefinition,
|
|
16
16
|
SchemaRecord,
|
|
17
17
|
TablesFromSchemas,
|
|
18
18
|
DatabasePragmas,
|
|
19
|
-
} from "./types";
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
import { WriteValidationPlugin } from "./write-validation-plugin.js";
|
|
20
21
|
|
|
21
22
|
type ArkBranch = {
|
|
22
23
|
domain?: string;
|
|
@@ -24,6 +25,7 @@ type ArkBranch = {
|
|
|
24
25
|
unit?: unknown;
|
|
25
26
|
structure?: unknown;
|
|
26
27
|
inner?: { divisor?: unknown };
|
|
28
|
+
meta?: DbFieldMeta & { _generated?: GeneratedPreset };
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
type StructureProp = {
|
|
@@ -87,8 +89,8 @@ export class Database<T extends SchemaRecord> {
|
|
|
87
89
|
this.kysely = new Kysely<TablesFromSchemas<T>>({
|
|
88
90
|
dialect: new BunSqliteDialect({ database: this.sqlite }),
|
|
89
91
|
plugins: [
|
|
90
|
-
new
|
|
91
|
-
new
|
|
92
|
+
new WriteValidationPlugin(this.columns, validation),
|
|
93
|
+
new ResultHydrationPlugin(this.columns, this.tableColumns, validation),
|
|
92
94
|
],
|
|
93
95
|
});
|
|
94
96
|
}
|
|
@@ -114,46 +116,49 @@ export class Database<T extends SchemaRecord> {
|
|
|
114
116
|
private normalizeProp(structureProp: StructureProp, parentSchema: Type) {
|
|
115
117
|
const { key, value: v, inner } = structureProp;
|
|
116
118
|
const kind: Prop["kind"] = structureProp.required ? "required" : "optional";
|
|
117
|
-
const generated = v.meta._generated;
|
|
118
119
|
const defaultValue = inner.default;
|
|
119
120
|
|
|
120
|
-
const
|
|
121
|
-
const nullable =
|
|
121
|
+
const concrete = v.branches.filter((b) => b.unit !== null && b.domain !== "undefined");
|
|
122
|
+
const nullable = concrete.length < v.branches.length;
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
const branchMeta = v.branches.find((b) => b.meta && Object.keys(b.meta).length > 0)?.meta;
|
|
125
|
+
const meta = { ...branchMeta, ...v.meta };
|
|
126
|
+
const generated = meta._generated;
|
|
127
|
+
|
|
128
|
+
if (v.proto === Date || concrete.some((b) => b.proto === Date)) {
|
|
124
129
|
return { key, kind, nullable, isDate: true, generated, defaultValue };
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
if (v.proto === Uint8Array ||
|
|
132
|
+
if (v.proto === Uint8Array || concrete.some((b) => b.proto === Uint8Array)) {
|
|
128
133
|
return {
|
|
129
134
|
key,
|
|
130
135
|
kind,
|
|
131
136
|
nullable,
|
|
132
137
|
isBlob: true,
|
|
133
|
-
meta
|
|
138
|
+
meta,
|
|
134
139
|
generated,
|
|
135
140
|
defaultValue,
|
|
136
141
|
};
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
if (
|
|
144
|
+
if (concrete.length > 0 && concrete.every((b) => b.domain === "boolean")) {
|
|
140
145
|
return { key, kind, nullable, isBoolean: true, generated, defaultValue };
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
if (
|
|
148
|
+
if (concrete.some((b) => !!b.structure)) {
|
|
144
149
|
return {
|
|
145
150
|
key,
|
|
146
151
|
kind,
|
|
147
152
|
nullable,
|
|
148
153
|
isJson: true,
|
|
149
154
|
jsonSchema: (parentSchema as any).get(key) as Type,
|
|
150
|
-
meta
|
|
155
|
+
meta,
|
|
151
156
|
generated,
|
|
152
157
|
defaultValue,
|
|
153
158
|
};
|
|
154
159
|
}
|
|
155
160
|
|
|
156
|
-
const branch =
|
|
161
|
+
const branch = concrete[0];
|
|
157
162
|
|
|
158
163
|
return {
|
|
159
164
|
key,
|
|
@@ -161,7 +166,7 @@ export class Database<T extends SchemaRecord> {
|
|
|
161
166
|
nullable,
|
|
162
167
|
domain: branch?.domain,
|
|
163
168
|
isInteger: !!branch?.inner?.divisor,
|
|
164
|
-
meta
|
|
169
|
+
meta,
|
|
165
170
|
generated,
|
|
166
171
|
defaultValue,
|
|
167
172
|
};
|
|
@@ -224,7 +229,9 @@ export class Database<T extends SchemaRecord> {
|
|
|
224
229
|
return `DEFAULT ${prop.defaultValue}`;
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
throw new Error(
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Unsupported default value type: ${typeof prop.defaultValue} ${JSON.stringify(prop)}`,
|
|
234
|
+
);
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
private columnDef(prop: Prop) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
2
3
|
import type { CompiledQuery, DatabaseConnection, QueryResult } from "kysely";
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
import { serializeParam } from "./serialize.js";
|
|
4
6
|
|
|
5
7
|
export class BunSqliteConnection implements DatabaseConnection {
|
|
6
8
|
readonly #db: Database;
|
package/src/dialect/dialect.ts
CHANGED
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
type Kysely,
|
|
10
10
|
type QueryCompiler,
|
|
11
11
|
} from "kysely";
|
|
12
|
-
|
|
13
|
-
import {
|
|
12
|
+
|
|
13
|
+
import type { BunSqliteDialectConfig } from "./config.js";
|
|
14
|
+
import { BunSqliteDriver } from "./driver.js";
|
|
14
15
|
|
|
15
16
|
export class BunSqliteDialect implements Dialect {
|
|
16
17
|
readonly #config: BunSqliteDialectConfig;
|
package/src/dialect/driver.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
2
3
|
import { CompiledQuery, type DatabaseConnection, type Driver } from "kysely";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
|
|
5
|
+
import type { BunSqliteDialectConfig } from "./config.js";
|
|
6
|
+
import { BunSqliteConnection } from "./connection.js";
|
|
7
|
+
import { ConnectionMutex } from "./mutex.js";
|
|
6
8
|
|
|
7
9
|
export class BunSqliteDriver implements Driver {
|
|
8
10
|
readonly #config: BunSqliteDialectConfig;
|
package/src/env.ts
CHANGED
package/src/generated.ts
CHANGED
|
@@ -3,13 +3,10 @@ import { type } from "arktype";
|
|
|
3
3
|
export type GeneratedPreset = "autoincrement" | "now";
|
|
4
4
|
|
|
5
5
|
const generatedTypes = {
|
|
6
|
-
autoincrement: () =>
|
|
7
|
-
type("number.integer")
|
|
8
|
-
.configure({ _generated: "autoincrement" } as any)
|
|
9
|
-
.default(0),
|
|
6
|
+
autoincrement: () => type("number.integer").configure({ _generated: "autoincrement" }).default(0),
|
|
10
7
|
now: () =>
|
|
11
8
|
type("Date")
|
|
12
|
-
.configure({ _generated: "now" }
|
|
9
|
+
.configure({ _generated: "now" })
|
|
13
10
|
.default(() => new Date(0)),
|
|
14
11
|
};
|
|
15
12
|
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
export { Database } from "./database";
|
|
2
|
-
export { generated, type GeneratedPreset } from "./generated";
|
|
3
|
-
export { JsonParseError } from "./errors";
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export { Database } from "./database.js";
|
|
2
|
+
export { generated, type GeneratedPreset } from "./generated.js";
|
|
3
|
+
export { JsonParseError } from "./errors.js";
|
|
4
|
+
export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
|
5
|
+
export {
|
|
6
|
+
sql,
|
|
7
|
+
type Selectable,
|
|
8
|
+
type Insertable,
|
|
9
|
+
type Updateable,
|
|
10
|
+
type Kysely,
|
|
11
|
+
type ExpressionBuilder,
|
|
12
|
+
} from "kysely";
|
|
13
|
+
export { type, type Type } from "arktype";
|
|
14
|
+
export { configure } from "arktype/config";
|
|
15
|
+
export { JsonValidationError } from "./validation-error.js";
|
|
16
|
+
export type { DbFieldMeta } from "./env.js";
|
|
8
17
|
export type {
|
|
9
18
|
DatabaseOptions,
|
|
10
19
|
SchemaRecord,
|
|
@@ -16,4 +25,4 @@ export type {
|
|
|
16
25
|
DatabaseSchema,
|
|
17
26
|
JsonValidation,
|
|
18
27
|
SqliteMasterRow,
|
|
19
|
-
} from "./types";
|
|
28
|
+
} from "./types.js";
|
package/src/migration/diff.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ColumnSchema, IntrospectedTable, ColumnCopy, MigrationOp } from "./types";
|
|
1
|
+
import type { ColumnSchema, IntrospectedTable, ColumnCopy, MigrationOp } from "./types.js";
|
|
2
2
|
|
|
3
3
|
export interface DesiredColumn extends ColumnSchema {
|
|
4
4
|
addable: boolean;
|
|
@@ -42,7 +42,11 @@ export class Differ {
|
|
|
42
42
|
const existingTable = this.existing.get(table.name);
|
|
43
43
|
|
|
44
44
|
if (!existingTable) {
|
|
45
|
-
this.ops.push({
|
|
45
|
+
this.ops.push({
|
|
46
|
+
type: "CreateTable",
|
|
47
|
+
table: table.name,
|
|
48
|
+
sql: table.sql,
|
|
49
|
+
});
|
|
46
50
|
this.rebuiltTables.add(table.name);
|
|
47
51
|
continue;
|
|
48
52
|
}
|
|
@@ -101,13 +105,21 @@ export class Differ {
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
if (!existing.notnull && col.notnull && col.defaultValue !== null) {
|
|
104
|
-
columnCopies.push({
|
|
108
|
+
columnCopies.push({
|
|
109
|
+
name: col.name,
|
|
110
|
+
expr: `COALESCE("${col.name}", ${col.defaultValue})`,
|
|
111
|
+
});
|
|
105
112
|
} else {
|
|
106
113
|
columnCopies.push({ name: col.name, expr: `"${col.name}"` });
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
this.ops.push({
|
|
117
|
+
this.ops.push({
|
|
118
|
+
type: "RebuildTable",
|
|
119
|
+
table: table.name,
|
|
120
|
+
createSql: table.sql,
|
|
121
|
+
columnCopies,
|
|
122
|
+
});
|
|
111
123
|
this.rebuiltTables.add(table.name);
|
|
112
124
|
}
|
|
113
125
|
|
|
@@ -134,7 +146,11 @@ export class Differ {
|
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
for (const col of newColumns) {
|
|
137
|
-
this.ops.push({
|
|
149
|
+
this.ops.push({
|
|
150
|
+
type: "AddColumn",
|
|
151
|
+
table: table.name,
|
|
152
|
+
columnDef: col.columnDef,
|
|
153
|
+
});
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -163,7 +179,12 @@ export class Differ {
|
|
|
163
179
|
|
|
164
180
|
if (this.rebuiltTables.has(table.name)) {
|
|
165
181
|
for (const idx of tableIndexes) {
|
|
166
|
-
this.ops.push({
|
|
182
|
+
this.ops.push({
|
|
183
|
+
type: "CreateIndex",
|
|
184
|
+
table: table.name,
|
|
185
|
+
columns: idx.columns,
|
|
186
|
+
sql: idx.sql,
|
|
187
|
+
});
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
continue;
|
|
@@ -180,7 +201,12 @@ export class Differ {
|
|
|
180
201
|
|
|
181
202
|
for (const idx of tableIndexes) {
|
|
182
203
|
if (!existingNames.has(idx.name)) {
|
|
183
|
-
this.ops.push({
|
|
204
|
+
this.ops.push({
|
|
205
|
+
type: "CreateIndex",
|
|
206
|
+
table: table.name,
|
|
207
|
+
columns: idx.columns,
|
|
208
|
+
sql: idx.sql,
|
|
209
|
+
});
|
|
184
210
|
}
|
|
185
211
|
}
|
|
186
212
|
|
package/src/migration/execute.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import type { IntrospectedColumn, IntrospectedIndex, IntrospectedTable } from "./types.js";
|
|
3
4
|
|
|
4
5
|
type TableListRow = {
|
|
5
6
|
name: string;
|
|
@@ -131,7 +132,8 @@ export class Introspector {
|
|
|
131
132
|
onDelete: fk?.onDelete ?? null,
|
|
132
133
|
hasNulls:
|
|
133
134
|
!isNotnull &&
|
|
134
|
-
this.db.prepare(`SELECT 1 FROM "${table}" WHERE "${col.name}" IS NULL LIMIT 1`).get() !==
|
|
135
|
+
this.db.prepare(`SELECT 1 FROM "${table}" WHERE "${col.name}" IS NULL LIMIT 1`).get() !==
|
|
136
|
+
null,
|
|
135
137
|
});
|
|
136
138
|
}
|
|
137
139
|
|
package/src/plugin.ts
CHANGED
|
@@ -3,48 +3,95 @@ 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
|
-
|
|
13
|
-
ValueNode,
|
|
11
|
+
CastNode,
|
|
14
12
|
ColumnNode,
|
|
15
13
|
IdentifierNode,
|
|
16
|
-
ReferenceNode,
|
|
17
14
|
ParensNode,
|
|
18
|
-
|
|
15
|
+
ReferenceNode,
|
|
19
16
|
SelectQueryNode,
|
|
17
|
+
TableNode,
|
|
20
18
|
} from "kysely";
|
|
21
19
|
|
|
22
|
-
import { JsonParseError } from "./errors";
|
|
23
|
-
import
|
|
24
|
-
import { JsonValidationError } from "./validation-error";
|
|
20
|
+
import { JsonParseError } from "./errors.js";
|
|
21
|
+
import { JsonValidationError } from "./validation-error.js";
|
|
25
22
|
|
|
26
23
|
export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
|
|
27
24
|
export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
|
|
28
25
|
|
|
29
26
|
type ResolvedCoercion = { table: string; coercion: ColumnCoercion };
|
|
30
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
|
+
|
|
31
79
|
const typePreservingAggregateFunctions = new Set(["max", "min"]);
|
|
32
80
|
|
|
33
|
-
export class
|
|
34
|
-
private
|
|
81
|
+
export class ResultHydrationPlugin implements KyselyPlugin {
|
|
82
|
+
private queryPlans = new WeakMap<QueryId, QueryPlan>();
|
|
35
83
|
|
|
36
84
|
constructor(
|
|
37
85
|
private columns: ColumnsMap,
|
|
38
86
|
private tableColumns: Map<string, Set<string>>,
|
|
39
|
-
private validation:
|
|
87
|
+
private validation: { onRead: boolean },
|
|
40
88
|
) {}
|
|
41
89
|
|
|
42
90
|
transformQuery: KyselyPlugin["transformQuery"] = (args) => {
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
91
|
+
this.queryPlans.set(args.queryId, {
|
|
92
|
+
table: this.getTableFromNode(args.node),
|
|
93
|
+
selectionPlans: this.getSelectionPlans(args.node),
|
|
94
|
+
});
|
|
48
95
|
|
|
49
96
|
return args.node;
|
|
50
97
|
};
|
|
@@ -98,116 +145,115 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
98
145
|
}
|
|
99
146
|
}
|
|
100
147
|
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
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);
|
|
104
153
|
}
|
|
154
|
+
}
|
|
105
155
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return;
|
|
156
|
+
private hydrateCoercion(plan: CoercionPlan, value: unknown) {
|
|
157
|
+
if (value === null || value === undefined) {
|
|
158
|
+
return value;
|
|
110
159
|
}
|
|
111
160
|
|
|
112
|
-
|
|
161
|
+
if (plan.coercion === "boolean") {
|
|
162
|
+
if (typeof value === "number") {
|
|
163
|
+
return value === 1;
|
|
164
|
+
}
|
|
113
165
|
|
|
114
|
-
|
|
115
|
-
return;
|
|
166
|
+
return value;
|
|
116
167
|
}
|
|
117
168
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!coercion || typeof coercion === "string") {
|
|
122
|
-
continue;
|
|
169
|
+
if (plan.coercion === "date") {
|
|
170
|
+
if (typeof value === "number") {
|
|
171
|
+
return new Date(value * 1000);
|
|
123
172
|
}
|
|
124
173
|
|
|
125
|
-
|
|
174
|
+
return value;
|
|
126
175
|
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private *writeValues(node: RootOperationNode) {
|
|
130
|
-
if (node.kind === "InsertQueryNode") {
|
|
131
|
-
const columns = node.columns?.map((c) => c.column.name);
|
|
132
|
-
|
|
133
|
-
if (!columns || !node.values || !ValuesNode.is(node.values)) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const col = columns[i]!;
|
|
177
|
+
const parsed =
|
|
178
|
+
typeof value === "string" ? this.parseJson(plan.table, plan.column, value) : value;
|
|
140
179
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
180
|
+
if (this.validation.onRead) {
|
|
181
|
+
this.validateJsonValue(plan.table, plan.column, parsed, plan.coercion.schema);
|
|
182
|
+
}
|
|
145
183
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
}
|
|
184
|
+
return parsed;
|
|
185
|
+
}
|
|
150
186
|
|
|
151
|
-
|
|
187
|
+
private parseStructuredValue(table: string, column: string, value: unknown) {
|
|
188
|
+
if (value === null || value === undefined) {
|
|
189
|
+
return value;
|
|
152
190
|
}
|
|
153
191
|
|
|
154
|
-
if (
|
|
155
|
-
return;
|
|
192
|
+
if (typeof value === "string") {
|
|
193
|
+
return this.parseJson(table, column, value);
|
|
156
194
|
}
|
|
157
195
|
|
|
158
|
-
|
|
159
|
-
if (ColumnNode.is(update.column) && ValueNode.is(update.value)) {
|
|
160
|
-
yield [update.column.column.name, update.value.value] as [string, unknown];
|
|
161
|
-
}
|
|
162
|
-
}
|
|
196
|
+
return value;
|
|
163
197
|
}
|
|
164
198
|
|
|
165
|
-
private
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
row[col] = row[col] === 1;
|
|
169
|
-
}
|
|
199
|
+
private isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
200
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
201
|
+
}
|
|
170
202
|
|
|
171
|
-
|
|
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;
|
|
172
208
|
}
|
|
173
209
|
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
210
|
+
for (const [field, fieldPlan] of plan.fields) {
|
|
211
|
+
if (!(field in parsed)) {
|
|
212
|
+
continue;
|
|
177
213
|
}
|
|
178
214
|
|
|
179
|
-
|
|
215
|
+
parsed[field] = this.hydrateValue(fieldPlan, parsed[field]);
|
|
180
216
|
}
|
|
181
217
|
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
226
|
}
|
|
185
227
|
|
|
186
|
-
|
|
228
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
229
|
+
const item = parsed[i];
|
|
187
230
|
|
|
188
|
-
|
|
231
|
+
if (!this.isPlainObject(item)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
189
234
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
235
|
+
for (const [field, fieldPlan] of plan.fields) {
|
|
236
|
+
if (!(field in item)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
195
239
|
|
|
196
|
-
|
|
197
|
-
|
|
240
|
+
item[field] = this.hydrateValue(fieldPlan, item[field]);
|
|
241
|
+
}
|
|
198
242
|
}
|
|
199
243
|
|
|
200
|
-
|
|
244
|
+
return parsed;
|
|
201
245
|
}
|
|
202
246
|
|
|
203
|
-
private
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
247
|
+
private hydrateValue(plan: ValuePlan, value: unknown): unknown {
|
|
248
|
+
if (plan.kind === "coercion") {
|
|
249
|
+
return this.hydrateCoercion(plan, value);
|
|
250
|
+
}
|
|
208
251
|
|
|
209
|
-
|
|
252
|
+
if (plan.kind === "object") {
|
|
253
|
+
return this.hydrateObject(plan, value);
|
|
210
254
|
}
|
|
255
|
+
|
|
256
|
+
return this.hydrateArray(plan, value);
|
|
211
257
|
}
|
|
212
258
|
|
|
213
259
|
private getIdentifierName(node: OperationNode | undefined) {
|
|
@@ -301,17 +347,115 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
301
347
|
return match;
|
|
302
348
|
}
|
|
303
349
|
|
|
304
|
-
private
|
|
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(
|
|
430
|
+
node: OperationNode,
|
|
431
|
+
scope: Map<string, string>,
|
|
432
|
+
output: string | null,
|
|
433
|
+
): ValuePlan | null {
|
|
305
434
|
if (AliasNode.is(node)) {
|
|
306
|
-
return this.
|
|
435
|
+
return this.resolveSelectionPlan(node.node, scope, output ?? this.getIdentifierName(node.alias));
|
|
307
436
|
}
|
|
308
437
|
|
|
309
438
|
if (ReferenceNode.is(node) || ColumnNode.is(node)) {
|
|
310
|
-
|
|
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
|
+
};
|
|
311
455
|
}
|
|
312
456
|
|
|
313
457
|
if (SelectQueryNode.is(node)) {
|
|
314
|
-
return this.
|
|
458
|
+
return this.resolveScalarSubqueryPlan(node, output);
|
|
315
459
|
}
|
|
316
460
|
|
|
317
461
|
if (AggregateFunctionNode.is(node)) {
|
|
@@ -322,29 +466,30 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
322
466
|
return null;
|
|
323
467
|
}
|
|
324
468
|
|
|
325
|
-
return this.
|
|
469
|
+
return this.resolveSelectionPlan(node.aggregated[0]!, scope, output);
|
|
326
470
|
}
|
|
327
471
|
|
|
328
472
|
if (ParensNode.is(node)) {
|
|
329
|
-
return this.
|
|
473
|
+
return this.resolveSelectionPlan(node.node, scope, output);
|
|
330
474
|
}
|
|
331
475
|
|
|
332
476
|
if (CastNode.is(node)) {
|
|
333
477
|
return null;
|
|
334
478
|
}
|
|
335
479
|
|
|
480
|
+
if (this.isRawNode(node)) {
|
|
481
|
+
return this.resolveJsonHelperPlan(node, output);
|
|
482
|
+
}
|
|
483
|
+
|
|
336
484
|
return null;
|
|
337
485
|
}
|
|
338
486
|
|
|
339
|
-
private
|
|
487
|
+
private resolveScalarSubqueryPlan(node: SelectQueryNode, output: string | null) {
|
|
340
488
|
if (!node.selections || node.selections.length !== 1) {
|
|
341
489
|
return null;
|
|
342
490
|
}
|
|
343
491
|
|
|
344
|
-
return this.
|
|
345
|
-
node.selections[0]!.selection,
|
|
346
|
-
this.getTableScope(node),
|
|
347
|
-
);
|
|
492
|
+
return this.resolveSelectionPlan(node.selections[0]!.selection, this.getTableScope(node), output);
|
|
348
493
|
}
|
|
349
494
|
|
|
350
495
|
private getSelectionOutputName(node: OperationNode) {
|
|
@@ -363,8 +508,8 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
363
508
|
return null;
|
|
364
509
|
}
|
|
365
510
|
|
|
366
|
-
private
|
|
367
|
-
const result = new Map<string,
|
|
511
|
+
private getSelectionPlans(node: RootOperationNode) {
|
|
512
|
+
const result = new Map<string, ValuePlan>();
|
|
368
513
|
|
|
369
514
|
if (node.kind !== "SelectQueryNode" || !node.selections) {
|
|
370
515
|
return result;
|
|
@@ -379,65 +524,86 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
379
524
|
continue;
|
|
380
525
|
}
|
|
381
526
|
|
|
382
|
-
const
|
|
527
|
+
const plan = this.resolveSelectionPlan(selectionNode.selection, scope, output);
|
|
383
528
|
|
|
384
|
-
if (
|
|
385
|
-
result.set(output,
|
|
529
|
+
if (plan) {
|
|
530
|
+
result.set(output, plan);
|
|
386
531
|
}
|
|
387
532
|
}
|
|
388
533
|
|
|
389
534
|
return result;
|
|
390
535
|
}
|
|
391
536
|
|
|
392
|
-
|
|
393
|
-
const
|
|
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
|
+
}
|
|
394
542
|
|
|
395
|
-
|
|
396
|
-
|
|
543
|
+
row[column] = this.hydrateCoercion({
|
|
544
|
+
kind: "coercion",
|
|
545
|
+
table,
|
|
546
|
+
column,
|
|
547
|
+
coercion,
|
|
548
|
+
}, row[column]);
|
|
397
549
|
}
|
|
550
|
+
}
|
|
398
551
|
|
|
399
|
-
|
|
552
|
+
transformResult: KyselyPlugin["transformResult"] = async (args) => {
|
|
553
|
+
const plan = this.queryPlans.get(args.queryId);
|
|
400
554
|
|
|
401
|
-
if (!
|
|
555
|
+
if (!plan) {
|
|
402
556
|
return args.result;
|
|
403
557
|
}
|
|
404
558
|
|
|
405
|
-
const mainCols = this.columns.get(table);
|
|
406
|
-
const mainTableColumns = this.tableColumns.get(table);
|
|
407
|
-
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;
|
|
408
561
|
|
|
409
562
|
for (const row of args.result.rows) {
|
|
410
|
-
if (mainCols) {
|
|
411
|
-
this.
|
|
563
|
+
if (plan.table && mainCols) {
|
|
564
|
+
this.coerceMainRow(plan.table, row, mainCols);
|
|
412
565
|
}
|
|
413
566
|
|
|
414
|
-
for (const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (resolved) {
|
|
418
|
-
this.coerceSingle(resolved.table, row, col, resolved.coercion);
|
|
567
|
+
for (const [column, selectionPlan] of plan.selectionPlans) {
|
|
568
|
+
if (!(column in row)) {
|
|
419
569
|
continue;
|
|
420
570
|
}
|
|
421
571
|
|
|
422
|
-
|
|
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)) {
|
|
423
581
|
continue;
|
|
424
582
|
}
|
|
425
583
|
|
|
426
584
|
for (const [otherTable, otherCols] of this.columns) {
|
|
427
|
-
if (otherTable === table) {
|
|
585
|
+
if (otherTable === plan.table) {
|
|
428
586
|
continue;
|
|
429
587
|
}
|
|
430
588
|
|
|
431
|
-
const coercion = otherCols.get(
|
|
589
|
+
const coercion = otherCols.get(column);
|
|
432
590
|
|
|
433
|
-
if (coercion) {
|
|
434
|
-
|
|
435
|
-
break;
|
|
591
|
+
if (!coercion) {
|
|
592
|
+
continue;
|
|
436
593
|
}
|
|
594
|
+
|
|
595
|
+
row[column] = this.hydrateCoercion({
|
|
596
|
+
kind: "coercion",
|
|
597
|
+
table: otherTable,
|
|
598
|
+
column,
|
|
599
|
+
coercion,
|
|
600
|
+
}, row[column]);
|
|
601
|
+
|
|
602
|
+
break;
|
|
437
603
|
}
|
|
438
604
|
}
|
|
439
605
|
}
|
|
440
606
|
|
|
441
|
-
return
|
|
607
|
+
return args.result;
|
|
442
608
|
};
|
|
443
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
|
+
}
|