@lobomfz/db 0.4.1 → 0.5.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 +55 -4
- package/package.json +1 -1
- package/src/database.ts +93 -385
- package/src/index.ts +3 -0
- package/src/migration/data-loss-error.ts +29 -0
- package/src/migration/diff.ts +79 -28
- package/src/migration/drift-error.ts +9 -0
- package/src/migration/execute.ts +36 -26
- package/src/migration/guided.ts +136 -0
- package/src/migration/introspect.ts +11 -6
- package/src/migration/registry.ts +43 -0
- package/src/migration/types.ts +42 -0
- package/src/schema-compiler.ts +362 -0
- package/src/types.ts +17 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { Type } from "arktype";
|
|
2
|
+
|
|
3
|
+
import type { DbFieldMeta } from "./env.js";
|
|
4
|
+
import type { FtsTableInfo } from "./fts/resolve.js";
|
|
5
|
+
import type { GeneratedPreset } from "./generated.js";
|
|
6
|
+
import type { DesiredColumn, DesiredTable } from "./migration/types.js";
|
|
7
|
+
import type { IndexDefinition, StructureProp } from "./types.js";
|
|
8
|
+
import type { VectorAnchorInfo } from "./vector-index/resolve.js";
|
|
9
|
+
|
|
10
|
+
type Prop = {
|
|
11
|
+
key: string;
|
|
12
|
+
kind: "required" | "optional";
|
|
13
|
+
domain?: string;
|
|
14
|
+
nullable?: boolean;
|
|
15
|
+
isBoolean?: boolean;
|
|
16
|
+
isInteger?: boolean;
|
|
17
|
+
isDate?: boolean;
|
|
18
|
+
isBlob?: boolean;
|
|
19
|
+
isJson?: boolean;
|
|
20
|
+
isVector?: boolean;
|
|
21
|
+
vectorDim?: number;
|
|
22
|
+
jsonSchema?: Type;
|
|
23
|
+
meta?: DbFieldMeta;
|
|
24
|
+
generated?: GeneratedPreset;
|
|
25
|
+
defaultValue?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const typeMap: Record<string, string> = {
|
|
29
|
+
string: "TEXT",
|
|
30
|
+
number: "REAL",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class SchemaCompiler {
|
|
34
|
+
compileTable(name: string, schema: Type, indexDefs: IndexDefinition[]) {
|
|
35
|
+
const props = this.parseSchemaProps(schema);
|
|
36
|
+
const columns = props.map((prop) => this.compileColumn(prop));
|
|
37
|
+
|
|
38
|
+
const indexes = indexDefs.map((indexDef) => ({
|
|
39
|
+
name: this.generateIndexName(name, indexDef.columns, indexDef.unique ?? false),
|
|
40
|
+
columns: indexDef.columns,
|
|
41
|
+
sql: this.generateCreateIndexSQL(name, indexDef),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const table: DesiredTable = {
|
|
45
|
+
name,
|
|
46
|
+
sql: this.generateCreateTableSQL(name, props),
|
|
47
|
+
columns,
|
|
48
|
+
indexes,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
table,
|
|
53
|
+
tableInfo: this.buildTableInfo(props),
|
|
54
|
+
vectorAnchor: this.buildVectorAnchorInfo(props),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createWriteSchema(schema: Type) {
|
|
59
|
+
const autoIncrementColumns = this.parseSchemaProps(schema)
|
|
60
|
+
.filter((prop) => prop.generated === "autoincrement")
|
|
61
|
+
.map((prop) => prop.key);
|
|
62
|
+
|
|
63
|
+
if (autoIncrementColumns.length === 0) {
|
|
64
|
+
return schema;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (schema as any).omit(...autoIncrementColumns) as Type;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private parseSchemaProps(schema: Type) {
|
|
71
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
72
|
+
|
|
73
|
+
if (!structureProps) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return structureProps.map((p) => this.normalizeProp(p, schema));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private normalizeProp(structureProp: StructureProp, parentSchema: Type) {
|
|
81
|
+
const { key, value: v, inner } = structureProp;
|
|
82
|
+
const kind: Prop["kind"] = structureProp.required ? "required" : "optional";
|
|
83
|
+
const defaultValue = inner.default;
|
|
84
|
+
|
|
85
|
+
const concrete = v.branches.filter((b) => b.unit !== null && b.domain !== "undefined");
|
|
86
|
+
const nullable = concrete.length < v.branches.length;
|
|
87
|
+
|
|
88
|
+
const branchMeta = v.branches.find((b) => b.meta && Object.keys(b.meta).length > 0)?.meta;
|
|
89
|
+
const meta = { ...branchMeta, ...v.meta };
|
|
90
|
+
const generated = meta._generated === "vector" ? undefined : meta._generated;
|
|
91
|
+
|
|
92
|
+
if (meta._generated === "vector") {
|
|
93
|
+
return {
|
|
94
|
+
key,
|
|
95
|
+
kind,
|
|
96
|
+
nullable,
|
|
97
|
+
isVector: true,
|
|
98
|
+
vectorDim: meta._vectorDim,
|
|
99
|
+
meta,
|
|
100
|
+
defaultValue,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (v.proto === Date || concrete.some((b) => b.proto === Date)) {
|
|
105
|
+
return { key, kind, nullable, isDate: true, generated, defaultValue };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (v.proto === Uint8Array || concrete.some((b) => b.proto === Uint8Array)) {
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
kind,
|
|
112
|
+
nullable,
|
|
113
|
+
isBlob: true,
|
|
114
|
+
meta,
|
|
115
|
+
generated,
|
|
116
|
+
defaultValue,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (concrete.length > 0 && concrete.every((b) => b.domain === "boolean")) {
|
|
121
|
+
return { key, kind, nullable, isBoolean: true, generated, defaultValue };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (concrete.some((b) => !!b.structure)) {
|
|
125
|
+
return {
|
|
126
|
+
key,
|
|
127
|
+
kind,
|
|
128
|
+
nullable,
|
|
129
|
+
isJson: true,
|
|
130
|
+
jsonSchema: (parentSchema as any).get(key) as Type,
|
|
131
|
+
meta,
|
|
132
|
+
generated,
|
|
133
|
+
defaultValue,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const branch = concrete[0];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
key,
|
|
141
|
+
kind,
|
|
142
|
+
nullable,
|
|
143
|
+
domain: branch?.domain,
|
|
144
|
+
isInteger: !!branch?.inner?.divisor,
|
|
145
|
+
meta,
|
|
146
|
+
generated,
|
|
147
|
+
defaultValue,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private compileColumn(prop: Prop): DesiredColumn {
|
|
152
|
+
const isNotNull = this.columnConstraint(prop) === "NOT NULL";
|
|
153
|
+
const defaultClause = this.defaultClause(prop);
|
|
154
|
+
const hasLiteralDefault = prop.generated !== "now" && defaultClause !== null;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: prop.key,
|
|
158
|
+
addable: !isNotNull || hasLiteralDefault,
|
|
159
|
+
columnDef: this.addColumnDef(prop),
|
|
160
|
+
nullableAddDef: this.nullableAddColumnDef(prop),
|
|
161
|
+
type: this.sqlType(prop),
|
|
162
|
+
notnull: isNotNull,
|
|
163
|
+
defaultValue: defaultClause
|
|
164
|
+
? defaultClause.replace("DEFAULT ", "").replace(/^\((.+)\)$/, "$1")
|
|
165
|
+
: null,
|
|
166
|
+
unique: !!prop.meta?.unique,
|
|
167
|
+
references: prop.meta?.references ?? null,
|
|
168
|
+
onDelete: prop.meta?.onDelete?.toUpperCase() ?? null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private sqlType(prop: Prop) {
|
|
173
|
+
if (prop.isJson) {
|
|
174
|
+
return "TEXT";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (prop.isBlob || prop.isVector) {
|
|
178
|
+
return "BLOB";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (prop.isDate || prop.isBoolean || prop.isInteger) {
|
|
182
|
+
return "INTEGER";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (prop.domain) {
|
|
186
|
+
return typeMap[prop.domain] ?? "TEXT";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return "TEXT";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private columnConstraint(prop: Prop) {
|
|
193
|
+
if (prop.generated === "autoincrement") {
|
|
194
|
+
return "PRIMARY KEY AUTOINCREMENT";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (prop.meta?.primaryKey) {
|
|
198
|
+
return "PRIMARY KEY";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (prop.kind === "required" && !prop.nullable) {
|
|
202
|
+
return "NOT NULL";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private defaultClause(prop: Prop) {
|
|
209
|
+
if (prop.generated === "now") {
|
|
210
|
+
return "DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER))";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (prop.defaultValue === undefined || prop.generated === "autoincrement") {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (prop.defaultValue === null) {
|
|
218
|
+
return "DEFAULT NULL";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof prop.defaultValue === "string") {
|
|
222
|
+
return `DEFAULT '${prop.defaultValue}'`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof prop.defaultValue === "number" || typeof prop.defaultValue === "boolean") {
|
|
226
|
+
return `DEFAULT ${prop.defaultValue}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Unsupported default value type: ${typeof prop.defaultValue} ${JSON.stringify(prop)}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private columnDef(prop: Prop) {
|
|
235
|
+
return [
|
|
236
|
+
`"${prop.key}"`,
|
|
237
|
+
this.sqlType(prop),
|
|
238
|
+
this.columnConstraint(prop),
|
|
239
|
+
prop.meta?.unique ? "UNIQUE" : null,
|
|
240
|
+
this.defaultClause(prop),
|
|
241
|
+
]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join(" ");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private referencesClause(prop: Prop) {
|
|
247
|
+
const ref = prop.meta?.references;
|
|
248
|
+
|
|
249
|
+
if (!ref) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const [table, column] = ref.split(".");
|
|
254
|
+
|
|
255
|
+
let clause = ` REFERENCES "${table}"("${column}")`;
|
|
256
|
+
|
|
257
|
+
if (prop.meta?.onDelete) {
|
|
258
|
+
clause += ` ON DELETE ${prop.meta.onDelete.toUpperCase()}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return clause;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private foreignKey(prop: Prop) {
|
|
265
|
+
const clause = this.referencesClause(prop);
|
|
266
|
+
|
|
267
|
+
if (!clause) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return `FOREIGN KEY ("${prop.key}")${clause}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private addColumnDef(prop: Prop) {
|
|
275
|
+
return `${this.columnDef(prop)}${this.referencesClause(prop) ?? ""}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private nullableAddColumnDef(prop: Prop) {
|
|
279
|
+
return `"${prop.key}" ${this.sqlType(prop)}${this.referencesClause(prop) ?? ""}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private generateCreateTableSQL(tableName: string, props: Prop[]) {
|
|
283
|
+
const columns: string[] = [];
|
|
284
|
+
const fks: string[] = [];
|
|
285
|
+
|
|
286
|
+
for (const prop of props) {
|
|
287
|
+
columns.push(this.columnDef(prop));
|
|
288
|
+
|
|
289
|
+
const fk = this.foreignKey(prop);
|
|
290
|
+
|
|
291
|
+
if (fk) {
|
|
292
|
+
fks.push(fk);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.concat(fks).join(", ")})`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private generateIndexName(tableName: string, columns: string[], unique: boolean) {
|
|
300
|
+
const prefix = unique ? "ux" : "ix";
|
|
301
|
+
|
|
302
|
+
return `${prefix}_${tableName}_${columns.join("_")}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private generateCreateIndexSQL(tableName: string, indexDef: IndexDefinition) {
|
|
306
|
+
const indexName = this.generateIndexName(tableName, indexDef.columns, indexDef.unique ?? false);
|
|
307
|
+
const unique = indexDef.unique ? "UNIQUE " : "";
|
|
308
|
+
const columns = indexDef.columns.map((c) => `"${c}"`).join(", ");
|
|
309
|
+
|
|
310
|
+
return `CREATE ${unique}INDEX "${indexName}" ON "${tableName}" (${columns})`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private buildTableInfo(props: Prop[]): FtsTableInfo {
|
|
314
|
+
const pkProp = props.find(
|
|
315
|
+
(p) => p.generated === "autoincrement" || p.meta?.primaryKey === true,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
columns: new Set(props.map((p) => p.key)),
|
|
320
|
+
pkColumn: pkProp?.key ?? null,
|
|
321
|
+
pkIsInteger: pkProp ? this.sqlType(pkProp) === "INTEGER" : false,
|
|
322
|
+
foreignKeys: props.flatMap((p) => {
|
|
323
|
+
const ref = p.meta?.references;
|
|
324
|
+
|
|
325
|
+
if (!ref) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const [referencedTable, referencedColumn] = ref.split(".");
|
|
330
|
+
|
|
331
|
+
if (!referencedTable || !referencedColumn) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Invalid references format "${ref}" for column "${p.key}": expected "table.column"`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return [{ column: p.key, referencedTable, referencedColumn }];
|
|
338
|
+
}),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private buildVectorAnchorInfo(props: Prop[]): VectorAnchorInfo {
|
|
343
|
+
const pkProp = props.find(
|
|
344
|
+
(p) => p.generated === "autoincrement" || p.meta?.primaryKey === true,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const vectorColumns = new Map<string, number>();
|
|
348
|
+
|
|
349
|
+
for (const p of props) {
|
|
350
|
+
if (p.isVector && typeof p.vectorDim === "number") {
|
|
351
|
+
vectorColumns.set(p.key, p.vectorDim);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
columns: new Set(props.map((p) => p.key)),
|
|
357
|
+
pkColumn: pkProp?.key ?? null,
|
|
358
|
+
pkIsInteger: pkProp ? this.sqlType(pkProp) === "INTEGER" : false,
|
|
359
|
+
vectorColumns,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { ColumnType, Generated } from "kysely";
|
|
1
|
+
import type { ColumnType, Generated, Kysely } from "kysely";
|
|
2
2
|
import type { Type } from "arktype";
|
|
3
3
|
|
|
4
|
+
import type { DbFieldMeta } from "./env.js";
|
|
5
|
+
|
|
4
6
|
export type ArkBranch = {
|
|
5
7
|
domain?: string;
|
|
6
8
|
proto?: unknown;
|
|
7
9
|
unit?: unknown;
|
|
8
10
|
structure?: unknown;
|
|
11
|
+
inner?: { divisor?: unknown };
|
|
12
|
+
meta?: DbFieldMeta;
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
export type StructureProp = {
|
|
@@ -15,6 +19,7 @@ export type StructureProp = {
|
|
|
15
19
|
value: Type & {
|
|
16
20
|
branches: ArkBranch[];
|
|
17
21
|
proto?: unknown;
|
|
22
|
+
meta: DbFieldMeta;
|
|
18
23
|
};
|
|
19
24
|
};
|
|
20
25
|
|
|
@@ -156,6 +161,15 @@ export type JsonValidation = {
|
|
|
156
161
|
onRead?: boolean;
|
|
157
162
|
};
|
|
158
163
|
|
|
164
|
+
export type DataMigration<
|
|
165
|
+
T extends SchemaRecord,
|
|
166
|
+
F extends FtsConfig<T> = FtsConfig<T>,
|
|
167
|
+
V extends VectorIndexConfig<T> = VectorIndexConfig<T>,
|
|
168
|
+
> = {
|
|
169
|
+
id: string;
|
|
170
|
+
run: (ctx: { db: Kysely<TablesFromSchemas<T, F, V>> }) => Promise<void> | void;
|
|
171
|
+
};
|
|
172
|
+
|
|
159
173
|
export type DatabaseOptions<
|
|
160
174
|
T extends SchemaRecord,
|
|
161
175
|
F extends FtsConfig<T> = FtsConfig<T>,
|
|
@@ -166,4 +180,6 @@ export type DatabaseOptions<
|
|
|
166
180
|
pragmas?: DatabasePragmas;
|
|
167
181
|
validation?: JsonValidation;
|
|
168
182
|
extensions?: DatabaseExtension[];
|
|
183
|
+
failOnDataLoss?: boolean;
|
|
184
|
+
migration?: DataMigration<T, F, V>;
|
|
169
185
|
};
|