@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 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.6",
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 nonNull = v.branches.filter((b) => b.unit !== null);
121
- 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
122
 
123
- if (v.proto === Date || nonNull.some((b) => b.proto === Date)) {
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 || nonNull.some((b) => b.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: v.meta,
137
+ meta,
134
138
  generated,
135
139
  defaultValue,
136
140
  };
137
141
  }
138
142
 
139
- if (nonNull.length > 0 && nonNull.every((b) => b.domain === "boolean")) {
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 (nonNull.some((b) => !!b.structure)) {
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: v.meta,
154
+ meta,
151
155
  generated,
152
156
  defaultValue,
153
157
  };
154
158
  }
155
159
 
156
- const branch = nonNull[0];
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: v.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(`Unsupported default value type: ${typeof prop.defaultValue}`);
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
- 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) {