@lobomfz/db 0.3.6 → 0.3.8
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 +26 -20
- 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 +21 -9
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.8",
|
|
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
|
@@ -3,20 +3,20 @@ import { Database as BunDatabase } from "bun:sqlite";
|
|
|
3
3
|
import type { Type } from "arktype";
|
|
4
4
|
import { Kysely, ParseJSONResultsPlugin } from "kysely";
|
|
5
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 { DeserializePlugin, type ColumnCoercion, type ColumnsMap } from "./plugin";
|
|
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 { DeserializePlugin, 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
20
|
|
|
21
21
|
type ArkBranch = {
|
|
22
22
|
domain?: string;
|
|
@@ -24,6 +24,7 @@ type ArkBranch = {
|
|
|
24
24
|
unit?: unknown;
|
|
25
25
|
structure?: unknown;
|
|
26
26
|
inner?: { divisor?: unknown };
|
|
27
|
+
meta?: DbFieldMeta & { _generated?: GeneratedPreset };
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
type StructureProp = {
|
|
@@ -114,46 +115,49 @@ export class Database<T extends SchemaRecord> {
|
|
|
114
115
|
private normalizeProp(structureProp: StructureProp, parentSchema: Type) {
|
|
115
116
|
const { key, value: v, inner } = structureProp;
|
|
116
117
|
const kind: Prop["kind"] = structureProp.required ? "required" : "optional";
|
|
117
|
-
const generated = v.meta._generated;
|
|
118
118
|
const defaultValue = inner.default;
|
|
119
119
|
|
|
120
|
-
const
|
|
121
|
-
const nullable =
|
|
120
|
+
const concrete = v.branches.filter((b) => b.unit !== null && b.domain !== "undefined");
|
|
121
|
+
const nullable = concrete.length < v.branches.length;
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
const branchMeta = v.branches.find((b) => b.meta && Object.keys(b.meta).length > 0)?.meta;
|
|
124
|
+
const meta = { ...branchMeta, ...v.meta };
|
|
125
|
+
const generated = meta._generated;
|
|
126
|
+
|
|
127
|
+
if (v.proto === Date || concrete.some((b) => b.proto === Date)) {
|
|
124
128
|
return { key, kind, nullable, isDate: true, generated, defaultValue };
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
if (v.proto === Uint8Array ||
|
|
131
|
+
if (v.proto === Uint8Array || concrete.some((b) => b.proto === Uint8Array)) {
|
|
128
132
|
return {
|
|
129
133
|
key,
|
|
130
134
|
kind,
|
|
131
135
|
nullable,
|
|
132
136
|
isBlob: true,
|
|
133
|
-
meta
|
|
137
|
+
meta,
|
|
134
138
|
generated,
|
|
135
139
|
defaultValue,
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
if (
|
|
143
|
+
if (concrete.length > 0 && concrete.every((b) => b.domain === "boolean")) {
|
|
140
144
|
return { key, kind, nullable, isBoolean: true, generated, defaultValue };
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
if (
|
|
147
|
+
if (concrete.some((b) => !!b.structure)) {
|
|
144
148
|
return {
|
|
145
149
|
key,
|
|
146
150
|
kind,
|
|
147
151
|
nullable,
|
|
148
152
|
isJson: true,
|
|
149
153
|
jsonSchema: (parentSchema as any).get(key) as Type,
|
|
150
|
-
meta
|
|
154
|
+
meta,
|
|
151
155
|
generated,
|
|
152
156
|
defaultValue,
|
|
153
157
|
};
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
const branch =
|
|
160
|
+
const branch = concrete[0];
|
|
157
161
|
|
|
158
162
|
return {
|
|
159
163
|
key,
|
|
@@ -161,7 +165,7 @@ export class Database<T extends SchemaRecord> {
|
|
|
161
165
|
nullable,
|
|
162
166
|
domain: branch?.domain,
|
|
163
167
|
isInteger: !!branch?.inner?.divisor,
|
|
164
|
-
meta
|
|
168
|
+
meta,
|
|
165
169
|
generated,
|
|
166
170
|
defaultValue,
|
|
167
171
|
};
|
|
@@ -224,7 +228,9 @@ export class Database<T extends SchemaRecord> {
|
|
|
224
228
|
return `DEFAULT ${prop.defaultValue}`;
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
throw new Error(
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Unsupported default value type: ${typeof prop.defaultValue} ${JSON.stringify(prop)}`,
|
|
233
|
+
);
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ValuesNode,
|
|
13
13
|
ValueNode,
|
|
14
14
|
ColumnNode,
|
|
15
|
+
DefaultInsertValueNode,
|
|
15
16
|
IdentifierNode,
|
|
16
17
|
ReferenceNode,
|
|
17
18
|
ParensNode,
|
|
@@ -19,9 +20,9 @@ import {
|
|
|
19
20
|
SelectQueryNode,
|
|
20
21
|
} from "kysely";
|
|
21
22
|
|
|
22
|
-
import { JsonParseError } from "./errors";
|
|
23
|
-
import type { JsonValidation } from "./types";
|
|
24
|
-
import { JsonValidationError } from "./validation-error";
|
|
23
|
+
import { JsonParseError } from "./errors.js";
|
|
24
|
+
import type { JsonValidation } from "./types.js";
|
|
25
|
+
import { JsonValidationError } from "./validation-error.js";
|
|
25
26
|
|
|
26
27
|
export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
|
|
27
28
|
export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
|
|
@@ -144,7 +145,18 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
const raw = valueList.values[i];
|
|
147
|
-
|
|
148
|
+
|
|
149
|
+
if (!raw || DefaultInsertValueNode.is(raw)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
yield [col, ValueNode.is(raw) ? raw.value : raw] as [string, unknown];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
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];
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
|
|
@@ -301,7 +313,10 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
301
313
|
return match;
|
|
302
314
|
}
|
|
303
315
|
|
|
304
|
-
private resolveSelectionCoercion(
|
|
316
|
+
private resolveSelectionCoercion(
|
|
317
|
+
node: OperationNode,
|
|
318
|
+
scope: Map<string, string>,
|
|
319
|
+
): ResolvedCoercion | null {
|
|
305
320
|
if (AliasNode.is(node)) {
|
|
306
321
|
return this.resolveSelectionCoercion(node.node, scope);
|
|
307
322
|
}
|
|
@@ -341,10 +356,7 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
341
356
|
return null;
|
|
342
357
|
}
|
|
343
358
|
|
|
344
|
-
return this.resolveSelectionCoercion(
|
|
345
|
-
node.selections[0]!.selection,
|
|
346
|
-
this.getTableScope(node),
|
|
347
|
-
);
|
|
359
|
+
return this.resolveSelectionCoercion(node.selections[0]!.selection, this.getTableScope(node));
|
|
348
360
|
}
|
|
349
361
|
|
|
350
362
|
private getSelectionOutputName(node: OperationNode) {
|