@lobomfz/db 0.3.5 → 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 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 | Strategy |
84
- |---|---|
85
- | New table | `CREATE TABLE` |
86
- | Removed table | `DROP TABLE` |
87
- | New nullable column | `ALTER TABLE ADD 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 | 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` |
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.5",
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
@@ -1,20 +1,22 @@
1
1
  import { Database as BunDatabase } from "bun:sqlite";
2
- import { Kysely, ParseJSONResultsPlugin } from "kysely";
3
- import { BunSqliteDialect } from "./dialect/dialect";
2
+
4
3
  import type { Type } from "arktype";
5
- import type { GeneratedPreset } from "./generated";
6
- import type { DbFieldMeta } from "./env";
7
- import { DeserializePlugin, type ColumnCoercion, type ColumnsMap } from "./plugin";
4
+ import { Kysely, ParseJSONResultsPlugin } 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 { DeserializePlugin, type ColumnCoercion, type ColumnsMap } from "./plugin.js";
8
13
  import type {
9
14
  DatabaseOptions,
10
15
  IndexDefinition,
11
16
  SchemaRecord,
12
17
  TablesFromSchemas,
13
18
  DatabasePragmas,
14
- } from "./types";
15
- import { Introspector } from "./migration/introspect";
16
- import { Differ, type DesiredTable } from "./migration/diff";
17
- import { Executor } from "./migration/execute";
19
+ } from "./types.js";
18
20
 
19
21
  type ArkBranch = {
20
22
  domain?: string;
@@ -22,6 +24,7 @@ type ArkBranch = {
22
24
  unit?: unknown;
23
25
  structure?: unknown;
24
26
  inner?: { divisor?: unknown };
27
+ meta?: DbFieldMeta & { _generated?: GeneratedPreset };
25
28
  };
26
29
 
27
30
  type StructureProp = {
@@ -43,6 +46,7 @@ type Prop = {
43
46
  isBoolean?: boolean;
44
47
  isInteger?: boolean;
45
48
  isDate?: boolean;
49
+ isBlob?: boolean;
46
50
  isJson?: boolean;
47
51
  jsonSchema?: Type;
48
52
  meta?: DbFieldMeta;
@@ -83,7 +87,10 @@ export class Database<T extends SchemaRecord> {
83
87
 
84
88
  this.kysely = new Kysely<TablesFromSchemas<T>>({
85
89
  dialect: new BunSqliteDialect({ database: this.sqlite }),
86
- plugins: [new DeserializePlugin(this.columns, this.tableColumns, validation), new ParseJSONResultsPlugin()],
90
+ plugins: [
91
+ new DeserializePlugin(this.columns, this.tableColumns, validation),
92
+ new ParseJSONResultsPlugin(),
93
+ ],
87
94
  });
88
95
  }
89
96
 
@@ -108,34 +115,49 @@ export class Database<T extends SchemaRecord> {
108
115
  private normalizeProp(structureProp: StructureProp, parentSchema: Type) {
109
116
  const { key, value: v, inner } = structureProp;
110
117
  const kind: Prop["kind"] = structureProp.required ? "required" : "optional";
111
- const generated = v.meta._generated;
112
118
  const defaultValue = inner.default;
113
119
 
114
- const nonNull = v.branches.filter((b) => b.unit !== null);
115
- const nullable = nonNull.length < v.branches.length;
120
+ const concrete = v.branches.filter((b) => b.unit !== null && b.domain !== "undefined");
121
+ const nullable = concrete.length < v.branches.length;
122
+
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;
116
126
 
117
- if (v.proto === Date || nonNull.some((b) => b.proto === Date)) {
127
+ if (v.proto === Date || concrete.some((b) => b.proto === Date)) {
118
128
  return { key, kind, nullable, isDate: true, generated, defaultValue };
119
129
  }
120
130
 
121
- if (nonNull.length > 0 && nonNull.every((b) => b.domain === "boolean")) {
131
+ if (v.proto === Uint8Array || concrete.some((b) => b.proto === Uint8Array)) {
132
+ return {
133
+ key,
134
+ kind,
135
+ nullable,
136
+ isBlob: true,
137
+ meta,
138
+ generated,
139
+ defaultValue,
140
+ };
141
+ }
142
+
143
+ if (concrete.length > 0 && concrete.every((b) => b.domain === "boolean")) {
122
144
  return { key, kind, nullable, isBoolean: true, generated, defaultValue };
123
145
  }
124
146
 
125
- if (nonNull.some((b) => !!b.structure)) {
147
+ if (concrete.some((b) => !!b.structure)) {
126
148
  return {
127
149
  key,
128
150
  kind,
129
151
  nullable,
130
152
  isJson: true,
131
153
  jsonSchema: (parentSchema as any).get(key) as Type,
132
- meta: v.meta,
154
+ meta,
133
155
  generated,
134
156
  defaultValue,
135
157
  };
136
158
  }
137
159
 
138
- const branch = nonNull[0];
160
+ const branch = concrete[0];
139
161
 
140
162
  return {
141
163
  key,
@@ -143,7 +165,7 @@ export class Database<T extends SchemaRecord> {
143
165
  nullable,
144
166
  domain: branch?.domain,
145
167
  isInteger: !!branch?.inner?.divisor,
146
- meta: v.meta,
168
+ meta,
147
169
  generated,
148
170
  defaultValue,
149
171
  };
@@ -154,6 +176,10 @@ export class Database<T extends SchemaRecord> {
154
176
  return "TEXT";
155
177
  }
156
178
 
179
+ if (prop.isBlob) {
180
+ return "BLOB";
181
+ }
182
+
157
183
  if (prop.isDate || prop.isBoolean || prop.isInteger) {
158
184
  return "INTEGER";
159
185
  }
@@ -202,7 +228,9 @@ export class Database<T extends SchemaRecord> {
202
228
  return `DEFAULT ${prop.defaultValue}`;
203
229
  }
204
230
 
205
- throw new Error(`Unsupported default value type: ${typeof prop.defaultValue}`);
231
+ throw new Error(
232
+ `Unsupported default value type: ${typeof prop.defaultValue} ${JSON.stringify(prop)}`,
233
+ );
206
234
  }
207
235
 
208
236
  private columnDef(prop: Prop) {
@@ -328,8 +356,8 @@ export class Database<T extends SchemaRecord> {
328
356
  type: this.sqlType(prop),
329
357
  notnull: isNotNull,
330
358
  defaultValue: defaultClause
331
- ? defaultClause.replace("DEFAULT ", "").replace(/^\((.+)\)$/, "$1")
332
- : null,
359
+ ? defaultClause.replace("DEFAULT ", "").replace(/^\((.+)\)$/, "$1")
360
+ : null,
333
361
  unique: !!prop.meta?.unique,
334
362
  references: prop.meta?.references ?? null,
335
363
  onDelete: prop.meta?.onDelete?.toUpperCase() ?? null,
@@ -342,7 +370,12 @@ export class Database<T extends SchemaRecord> {
342
370
  sql: this.generateCreateIndexSQL(name, indexDef),
343
371
  }));
344
372
 
345
- desiredTables.push({ name, sql: this.generateCreateTableSQL(name, props), columns, indexes });
373
+ desiredTables.push({
374
+ name,
375
+ sql: this.generateCreateTableSQL(name, props),
376
+ columns,
377
+ indexes,
378
+ });
346
379
  }
347
380
 
348
381
  const existing = new Introspector(this.sqlite).introspect();
@@ -1,6 +1,8 @@
1
1
  import type { Database } from "bun:sqlite";
2
+
2
3
  import type { CompiledQuery, DatabaseConnection, QueryResult } from "kysely";
3
- import { serializeParam } from "./serialize";
4
+
5
+ import { serializeParam } from "./serialize.js";
4
6
 
5
7
  export class BunSqliteConnection implements DatabaseConnection {
6
8
  readonly #db: Database;
@@ -9,8 +9,9 @@ import {
9
9
  type Kysely,
10
10
  type QueryCompiler,
11
11
  } from "kysely";
12
- import type { BunSqliteDialectConfig } from "./config";
13
- import { BunSqliteDriver } from "./driver";
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;
@@ -1,8 +1,10 @@
1
1
  import type { Database } from "bun:sqlite";
2
+
2
3
  import { CompiledQuery, type DatabaseConnection, type Driver } from "kysely";
3
- import type { BunSqliteDialectConfig } from "./config";
4
- import { BunSqliteConnection } from "./connection";
5
- import { ConnectionMutex } from "./mutex";
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
@@ -3,6 +3,7 @@ export type DbFieldMeta = {
3
3
  unique?: boolean;
4
4
  references?: `${string}.${string}`;
5
5
  onDelete?: "cascade" | "set null" | "restrict";
6
+ _generated?: "autoincrement" | "now";
6
7
  };
7
8
 
8
9
  declare global {
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" } as any)
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 { sql, type Selectable, type Insertable, type Updateable, type Kysely } from "kysely";
5
- export { type } from "arktype";
6
- export { JsonValidationError } from "./validation-error";
7
- export type { DbFieldMeta } from "./env";
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";
@@ -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({ type: "CreateTable", table: table.name, sql: table.sql });
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({ name: col.name, expr: `COALESCE("${col.name}", ${col.defaultValue})` });
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({ type: "RebuildTable", table: table.name, createSql: table.sql, columnCopies });
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({ type: "AddColumn", table: table.name, columnDef: col.columnDef });
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({ type: "CreateIndex", table: table.name, columns: idx.columns, sql: idx.sql });
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({ type: "CreateIndex", table: table.name, columns: idx.columns, sql: idx.sql });
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
 
@@ -1,5 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
- import type { MigrationOp, RebuildTableOp } from "./types";
2
+
3
+ import type { MigrationOp, RebuildTableOp } from "./types.js";
3
4
 
4
5
  export class Executor {
5
6
  constructor(
@@ -1,5 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
- import type { IntrospectedColumn, IntrospectedIndex, IntrospectedTable } from "./types";
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() !== null,
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
- yield [col, raw && ValueNode.is(raw) ? raw.value : raw] as [string, unknown];
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(node: OperationNode, scope: Map<string, string>): ResolvedCoercion | null {
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) {