@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.
@@ -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
  };