@pikku/kysely-bun-sqlite 0.12.8 → 0.12.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @pikku/kysely-bun-sqlite
2
2
 
3
+ ## 0.12.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 1fe79bc: A standalone build of a SQLite app now ships its `db.sqliteExtensions` (sqlite-vec's vec0 by default) inside the artifact, so a migration or query that uses them works in production the way it does under `pikku dev`. The node bundle loads them from `sqlite-extensions/` beside itself; a compiled bun binary embeds them and writes them out under `$PIKKU_DATA_DIR/.pikku-sqlite-extensions/` on start. The libraries are the build machine's, so an extension that cannot be resolved there fails the build; `[]` builds without them.
8
+
9
+ `createNodeSqliteKysely` and `createBunSqliteKysely` take an `extensions` list of library paths to load into the connection.
10
+
11
+ - Updated dependencies [1394385]
12
+ - Updated dependencies [7740547]
13
+ - @pikku/core@0.12.124
14
+ - @pikku/kysely-sqlite@0.12.15
15
+
16
+ ## 0.12.9
17
+
18
+ ### Patch Changes
19
+
20
+ - 1ab831e: Add `openBunSqliteDatabase`, which opens a `bun:sqlite` database as the `SqliteDatabase` Kysely's `SqliteDialect` binds to.
21
+
22
+ `createBunSqliteKysely` builds one Kysely over one database, so it cannot express several Kysely instances — differing only in their plugins — sharing a single underlying database. That combination is what lets a plugin-free instance (Better Auth needs unmangled column names) and a CamelCase one address the same tables, and until now an app that needed it had to reach for `better-sqlite3` instead.
23
+
24
+ Under Bun that fallback is not survivable: `better-sqlite3` ships a Node-ABI addon which Bun cannot load, and rather than throwing it aborts the process with `NAPI FATAL ERROR: Error::New napi_get_last_error_info`. In a `pikku dev --watch` this killed the dev server after codegen but before it ever listened, with no error naming the driver.
25
+
26
+ - Updated dependencies [1ab831e]
27
+ - @pikku/core@0.12.105
28
+
3
29
  ## 0.12.8
4
30
 
5
31
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import type { Database } from 'bun:sqlite';
1
+ import { Database } from 'bun:sqlite';
2
2
  import type { SqliteDatabase, SqliteStatement } from 'kysely';
3
3
  export declare class BunSqliteDatabase implements SqliteDatabase {
4
4
  private readonly db;
@@ -6,3 +6,14 @@ export declare class BunSqliteDatabase implements SqliteDatabase {
6
6
  prepare(sql: string): SqliteStatement;
7
7
  close(): void;
8
8
  }
9
+ /**
10
+ * Opens a `bun:sqlite` database as the `SqliteDatabase` Kysely's `SqliteDialect`
11
+ * binds to.
12
+ *
13
+ * `createBunSqliteKysely` covers the common case of one database behind one
14
+ * Kysely. This is for the case it cannot express: several Kysely instances —
15
+ * differing in their plugins — sharing a single underlying database, which is
16
+ * what lets a plugin-free instance (Better Auth needs unmangled column names)
17
+ * and a CamelCase one address the same tables.
18
+ */
19
+ export declare const openBunSqliteDatabase: (filename: string) => SqliteDatabase;
@@ -1,3 +1,4 @@
1
+ import { Database } from 'bun:sqlite';
1
2
  function coerce(v) {
2
3
  if (v === null || v === undefined)
3
4
  return null;
@@ -45,3 +46,14 @@ export class BunSqliteDatabase {
45
46
  this.db.close();
46
47
  }
47
48
  }
49
+ /**
50
+ * Opens a `bun:sqlite` database as the `SqliteDatabase` Kysely's `SqliteDialect`
51
+ * binds to.
52
+ *
53
+ * `createBunSqliteKysely` covers the common case of one database behind one
54
+ * Kysely. This is for the case it cannot express: several Kysely instances —
55
+ * differing in their plugins — sharing a single underlying database, which is
56
+ * what lets a plugin-free instance (Better Auth needs unmangled column names)
57
+ * and a CamelCase one address the same tables.
58
+ */
59
+ export const openBunSqliteDatabase = (filename) => new BunSqliteDatabase(new Database(filename));
@@ -18,5 +18,13 @@ export interface CreateBunSqliteKyselyOptions {
18
18
  * shows up as an error that explains itself.
19
19
  */
20
20
  functions?: SqliteFunctionMap;
21
+ /**
22
+ * Absolute paths of loadable SQLite extensions (sqlite-vec's `vec0`, say) to
23
+ * load before the database is handed to Kysely. Loaded through the C API;
24
+ * SQL's own `load_extension()` stays refused. On macOS this throws unless
25
+ * the process pointed bun at a SQLite that allows it first
26
+ * (`Database.setCustomSQLite`), since Apple's system one does not.
27
+ */
28
+ extensions?: string[];
21
29
  }
22
30
  export declare function createBunSqliteKysely<DB>(options: CreateBunSqliteKyselyOptions): Kysely<DB>;
@@ -4,6 +4,8 @@ import { BunSqliteDatabase } from './bun-sqlite-adapter.js';
4
4
  import { registerSqliteFunctions } from './register-functions.js';
5
5
  export function createBunSqliteKysely(options) {
6
6
  const db = new Database(options.filename);
7
+ for (const path of options.extensions ?? [])
8
+ db.loadExtension(path);
7
9
  if (options.functions)
8
10
  registerSqliteFunctions(db, options.functions);
9
11
  const plugins = [];
@@ -1,4 +1,4 @@
1
- export { BunSqliteDatabase } from './bun-sqlite-adapter.js';
1
+ export { BunSqliteDatabase, openBunSqliteDatabase, } from './bun-sqlite-adapter.js';
2
2
  export { createBunSqliteKysely, type CreateBunSqliteKyselyOptions, } from './create-bun-sqlite-kysely.js';
3
3
  export { registerSqliteFunctions } from './register-functions.js';
4
4
  export { SqliteFunctionsUnsupportedError, type SqliteFunctionMap, } from '@pikku/kysely-sqlite';
package/dist/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { BunSqliteDatabase } from './bun-sqlite-adapter.js';
1
+ export { BunSqliteDatabase, openBunSqliteDatabase, } from './bun-sqlite-adapter.js';
2
2
  export { createBunSqliteKysely, } from './create-bun-sqlite-kysely.js';
3
3
  export { registerSqliteFunctions } from './register-functions.js';
4
4
  export { SqliteFunctionsUnsupportedError, } from '@pikku/kysely-sqlite';
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@pikku/kysely-bun-sqlite",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/pikkujs/pikku.git",
7
+ "directory": "packages/services/kysely-bun-sqlite"
8
+ },
4
9
  "description": "Kysely driver for Pikku backed by the built-in bun:sqlite module",
5
10
  "author": "yasser.fadl@gmail.com",
6
11
  "license": "MIT",
@@ -20,15 +25,14 @@
20
25
  },
21
26
  "dependencies": {
22
27
  "@pikku/kysely": "^0.13.18",
23
- "@pikku/kysely-sqlite": "^0.12.13",
28
+ "@pikku/kysely-sqlite": "^0.12.15",
24
29
  "kysely": "^0.29.5"
25
30
  },
26
31
  "peerDependencies": {
27
- "@pikku/core": "^0.12.86"
32
+ "@pikku/core": "^0.12.124"
28
33
  },
29
34
  "devDependencies": {
30
- "@pikku/core": "^0.12.86",
31
- "@types/bun": "^1.4.0",
32
- "typescript": "^6.0.3"
35
+ "@pikku/core": "^0.12.124",
36
+ "@types/bun": "^1.4.0"
33
37
  }
34
- }
38
+ }
package/run-tests.sh CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/bin/bash
2
2
 
3
+ # Enable nullglob to handle cases where no files match the pattern
3
4
  shopt -s nullglob
4
5
 
6
+ # Initialize variables for options
5
7
  watch_mode=false
6
8
  coverage_mode=false
7
9
 
10
+ # Parse command-line options
8
11
  while [[ $# -gt 0 ]]; do
9
12
  case $1 in
10
13
  --watch)
@@ -22,33 +25,44 @@ while [[ $# -gt 0 ]]; do
22
25
  esac
23
26
  done
24
27
 
25
- files=()
26
- while IFS= read -r -d '' file; do
27
- files+=("$file")
28
- done < <(find src -type f -name "*.test.ts" -print0)
28
+ # Define the pattern to match your test files
29
+ pattern="src/*.test.ts"
29
30
 
31
+ # Expand the pattern into an array of files
32
+ files=($(find src -type f -name "*.test.ts"))
33
+
34
+ # Check if any files matched the pattern
30
35
  if [ ${#files[@]} -eq 0 ]; then
31
- echo "No test files found"
36
+ echo "No test files found matching pattern: $pattern"
37
+ if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
38
+ exit 1
39
+ fi
32
40
  exit 0
33
41
  fi
34
42
 
35
- if [ "$coverage_mode" = true ]; then
36
- # Bun writes coverage/lcov.info and instruments imported dist files too.
37
- # Re-emit a package-root lcov.info containing only src/ records so the
38
- # repo-wide unit-coverage merge — which expects <pkg>/lcov.info and prefixes
39
- # its SF paths — maps them correctly, exactly like the node packages'
40
- # --test-reporter=lcov output.
41
- bun test --coverage --coverage-reporter=lcov "${files[@]}"
42
- status=$?
43
- awk '/^SF:/{keep=/^SF:src\//} keep' coverage/lcov.info > lcov.info 2>/dev/null || true
44
- rm -rf coverage
45
- exit $status
46
- fi
47
-
48
- bun_cmd="bun test"
43
+ # Construct the bun command
44
+ #
45
+ # --parallel gives every test file its own worker process, which is what node's
46
+ # `--test` did. Several suites keep module-level caches (a TypeScript program, a
47
+ # temp fixture directory) that a shared process would leak between files.
48
+ #
49
+ # --timeout because bun caps a test at 5s by default where node had no cap, and
50
+ # the suites that build real TypeScript programs run well past that.
51
+ bun_cmd=(bun test --parallel --timeout 120000)
49
52
 
53
+ # Append options based on flags
50
54
  if [ "$watch_mode" = true ]; then
51
- bun_cmd="$bun_cmd --watch"
55
+ # --watch reruns in one process; parallel workers have nothing to watch.
56
+ bun_cmd=(bun test --watch --timeout 120000)
57
+ fi
58
+
59
+ if [ "$coverage_mode" = true ]; then
60
+ # --coverage-dir=. keeps lcov.info at the package root: CI merges every
61
+ # package's report by prefixing its SF paths with the file's own directory,
62
+ # so a nested coverage/ directory would re-root them one level too deep.
63
+ bun_cmd+=(--coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=.)
64
+ export PIKKU_TEST_COVERAGE=1
52
65
  fi
53
66
 
54
- $bun_cmd "${files[@]}"
67
+ # Execute the bun command with the expanded list of files
68
+ "${bun_cmd[@]}" "${files[@]}"
@@ -1,24 +0,0 @@
1
- import type { KyselyPlugin } from 'kysely';
2
- export type ColumnKind = 'date' | 'bool' | 'json';
3
- /**
4
- * Per-table column kind map. Keys are snake_case table names matching the
5
- * physical SQLite tables; inner keys are snake_case column names.
6
- * Generated by `pikku db migrate` from `-- @date|@bool|@json` annotations
7
- * and naming conventions (col ends in _at/_on → date; starts with is_/has_/can_ → bool).
8
- */
9
- export type CoercionMap = Record<string, Record<string, ColumnKind>>;
10
- export interface CreateCoercionPluginOptions {
11
- map: CoercionMap;
12
- }
13
- /**
14
- * Convert SQLite-stored values (TEXT/INTEGER) back into the logical types
15
- * app code expects (Date / boolean / parsed JSON).
16
- *
17
- * Write-side coercion (Date → ISO string, boolean → 0/1, object → JSON) is
18
- * handled in the NodeSqliteDatabase adapter (always-on), so this plugin is
19
- * read-only.
20
- *
21
- * Place AFTER CamelCasePlugin in the plugin array (or it handles both
22
- * orderings via the dual-keyed global map).
23
- */
24
- export declare function createCoercionPlugin(options: CreateCoercionPluginOptions): KyselyPlugin;
@@ -1,92 +0,0 @@
1
- function fromDb(value, kind) {
2
- if (value == null)
3
- return value;
4
- switch (kind) {
5
- case 'date':
6
- if (typeof value === 'string') {
7
- const d = new Date(value);
8
- return Number.isNaN(d.getTime()) ? value : d;
9
- }
10
- return value;
11
- case 'bool':
12
- if (typeof value === 'number')
13
- return value !== 0;
14
- if (typeof value === 'bigint')
15
- return value !== 0n;
16
- return value;
17
- case 'json':
18
- if (typeof value !== 'string')
19
- return value;
20
- try {
21
- return JSON.parse(value);
22
- }
23
- catch {
24
- return value;
25
- }
26
- }
27
- }
28
- function snakeToCamel(name) {
29
- return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
30
- }
31
- /**
32
- * Flatten the per-table map into a single column-name → kind lookup.
33
- * Indexed by BOTH snake_case and camelCase so it works regardless of
34
- * CamelCasePlugin ordering in the plugin array.
35
- *
36
- * When two tables disagree on the kind for the same column name, the column
37
- * is ambiguous (a joined/aliased query could carry either) and is left
38
- * uncoerced rather than letting whichever table was processed last win.
39
- */
40
- function buildGlobalMap(map) {
41
- const out = {};
42
- const ambiguous = new Set();
43
- const assign = (key, kind) => {
44
- const existing = out[key];
45
- if (existing !== undefined && existing !== kind) {
46
- ambiguous.add(key);
47
- return;
48
- }
49
- out[key] = kind;
50
- };
51
- for (const tbl of Object.values(map)) {
52
- for (const [col, kind] of Object.entries(tbl)) {
53
- assign(col, kind);
54
- assign(snakeToCamel(col), kind);
55
- }
56
- }
57
- for (const key of ambiguous)
58
- delete out[key];
59
- return out;
60
- }
61
- /**
62
- * Convert SQLite-stored values (TEXT/INTEGER) back into the logical types
63
- * app code expects (Date / boolean / parsed JSON).
64
- *
65
- * Write-side coercion (Date → ISO string, boolean → 0/1, object → JSON) is
66
- * handled in the NodeSqliteDatabase adapter (always-on), so this plugin is
67
- * read-only.
68
- *
69
- * Place AFTER CamelCasePlugin in the plugin array (or it handles both
70
- * orderings via the dual-keyed global map).
71
- */
72
- export function createCoercionPlugin(options) {
73
- const globalMap = buildGlobalMap(options.map);
74
- return {
75
- transformQuery(args) {
76
- return args.node;
77
- },
78
- async transformResult(args) {
79
- const out = [];
80
- for (const row of args.result.rows) {
81
- const next = { ...row };
82
- for (const [col, val] of Object.entries(row)) {
83
- const kind = globalMap[col];
84
- if (kind)
85
- next[col] = fromDb(val, kind);
86
- }
87
- out.push(next);
88
- }
89
- return { ...args.result, rows: out };
90
- },
91
- };
92
- }
@@ -1 +0,0 @@
1
- {"root":["../src/bun-sqlite-adapter.ts","../src/create-bun-sqlite-kysely.ts","../src/index.ts","../src/register-functions.ts"],"version":"6.0.3"}
@@ -1,176 +0,0 @@
1
- import { describe, test, beforeEach, afterEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { Database } from 'bun:sqlite'
4
- import { Kysely, SqliteDialect } from 'kysely'
5
- import { BunSqliteDatabase } from './bun-sqlite-adapter.js'
6
-
7
- interface TestDB {
8
- items: { id: number; name: string; active: number; score: number | null }
9
- }
10
-
11
- describe('BunSqliteDatabase', () => {
12
- let db: Database
13
- let kysely: Kysely<TestDB>
14
-
15
- beforeEach(() => {
16
- db = new Database(':memory:')
17
- kysely = new Kysely<TestDB>({
18
- dialect: new SqliteDialect({ database: new BunSqliteDatabase(db) }),
19
- })
20
- })
21
-
22
- afterEach(async () => {
23
- await kysely.destroy()
24
- })
25
-
26
- test('create table and insert a row', async () => {
27
- await kysely.schema
28
- .createTable('items')
29
- .addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
30
- .addColumn('name', 'text', (c) => c.notNull())
31
- .addColumn('active', 'integer', (c) => c.notNull())
32
- .addColumn('score', 'real')
33
- .execute()
34
-
35
- await kysely
36
- .insertInto('items')
37
- .values({ name: 'alpha', active: 1, score: 3.14 })
38
- .execute()
39
-
40
- const rows = await kysely.selectFrom('items').selectAll().execute()
41
- assert.equal(rows.length, 1)
42
- assert.equal(rows[0].name, 'alpha')
43
- assert.equal(rows[0].active, 1)
44
- assert.equal(rows[0].score, 3.14)
45
- })
46
-
47
- test('update and delete', async () => {
48
- await kysely.schema
49
- .createTable('items')
50
- .addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
51
- .addColumn('name', 'text', (c) => c.notNull())
52
- .addColumn('active', 'integer', (c) => c.notNull())
53
- .addColumn('score', 'real')
54
- .execute()
55
-
56
- await kysely
57
- .insertInto('items')
58
- .values([
59
- { name: 'a', active: 1, score: null },
60
- { name: 'b', active: 0, score: null },
61
- ])
62
- .execute()
63
-
64
- await kysely
65
- .updateTable('items')
66
- .set({ active: 1 })
67
- .where('name', '=', 'b')
68
- .execute()
69
- const updated = await kysely
70
- .selectFrom('items')
71
- .where('name', '=', 'b')
72
- .selectAll()
73
- .executeTakeFirstOrThrow()
74
- assert.equal(updated.active, 1)
75
-
76
- await kysely.deleteFrom('items').where('name', '=', 'a').execute()
77
- const remaining = await kysely.selectFrom('items').selectAll().execute()
78
- assert.equal(remaining.length, 1)
79
- assert.equal(remaining[0].name, 'b')
80
- })
81
-
82
- test('boolean coercion via coerce function', async () => {
83
- await kysely.schema
84
- .createTable('items')
85
- .addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
86
- .addColumn('name', 'text', (c) => c.notNull())
87
- .addColumn('active', 'integer', (c) => c.notNull())
88
- .addColumn('score', 'real')
89
- .execute()
90
-
91
- await kysely
92
- .insertInto('items')
93
- .values({
94
- name: 'bool-test',
95
- active: true as unknown as number,
96
- score: null,
97
- })
98
- .execute()
99
- const row = await kysely
100
- .selectFrom('items')
101
- .selectAll()
102
- .executeTakeFirstOrThrow()
103
- assert.equal(row.active, 1)
104
- })
105
-
106
- test('null values round-trip', async () => {
107
- await kysely.schema
108
- .createTable('items')
109
- .addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
110
- .addColumn('name', 'text', (c) => c.notNull())
111
- .addColumn('active', 'integer', (c) => c.notNull())
112
- .addColumn('score', 'real')
113
- .execute()
114
-
115
- await kysely
116
- .insertInto('items')
117
- .values({ name: 'nullable', active: 0, score: null })
118
- .execute()
119
- const row = await kysely
120
- .selectFrom('items')
121
- .selectAll()
122
- .executeTakeFirstOrThrow()
123
- assert.equal(row.score, null)
124
- })
125
-
126
- test('iterate returns rows lazily', async () => {
127
- await kysely.schema
128
- .createTable('items')
129
- .addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
130
- .addColumn('name', 'text', (c) => c.notNull())
131
- .addColumn('active', 'integer', (c) => c.notNull())
132
- .addColumn('score', 'real')
133
- .execute()
134
-
135
- await kysely
136
- .insertInto('items')
137
- .values([
138
- { name: 'x', active: 1, score: null },
139
- { name: 'y', active: 1, score: null },
140
- { name: 'z', active: 1, score: null },
141
- ])
142
- .execute()
143
-
144
- const collected: string[] = []
145
- const stmt = new BunSqliteDatabase(db).prepare(
146
- 'SELECT name FROM items ORDER BY id'
147
- )
148
- for (const row of stmt.iterate([])) {
149
- collected.push((row as any).name)
150
- }
151
- assert.deepEqual(collected, ['x', 'y', 'z'])
152
- })
153
-
154
- test('reader is false for writes so run() returns mutation metadata', async () => {
155
- await kysely.schema
156
- .createTable('items')
157
- .addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
158
- .addColumn('name', 'text', (c) => c.notNull())
159
- .addColumn('active', 'integer', (c) => c.notNull())
160
- .addColumn('score', 'real')
161
- .execute()
162
-
163
- const inserted = await kysely
164
- .insertInto('items')
165
- .values({ name: 'meta', active: 1, score: null })
166
- .executeTakeFirstOrThrow()
167
- assert.equal(inserted.insertId, 1n)
168
-
169
- const updated = await kysely
170
- .updateTable('items')
171
- .set({ active: 0 })
172
- .where('name', '=', 'meta')
173
- .executeTakeFirstOrThrow()
174
- assert.equal(updated.numUpdatedRows, 1n)
175
- })
176
- })
@@ -1,51 +0,0 @@
1
- import type { Database, Statement } from 'bun:sqlite'
2
- import type { SqliteDatabase, SqliteStatement } from 'kysely'
3
-
4
- function coerce(v: unknown): unknown {
5
- if (v === null || v === undefined) return null
6
- if (typeof v === 'boolean') return v ? 1 : 0
7
- if (v instanceof Date) return v.toISOString()
8
- if (typeof v === 'object') return JSON.stringify(v)
9
- return v
10
- }
11
-
12
- class BunSqliteStatement implements SqliteStatement {
13
- constructor(private readonly stmt: Statement) {}
14
-
15
- get reader(): boolean {
16
- return this.stmt.columnNames.length > 0
17
- }
18
-
19
- all(parameters: ReadonlyArray<unknown>): unknown[] {
20
- return this.stmt.all(...(parameters.map(coerce) as any))
21
- }
22
-
23
- *iterate(parameters: ReadonlyArray<unknown>): IterableIterator<unknown> {
24
- for (const row of this.stmt.iterate(...(parameters.map(coerce) as any))) {
25
- yield row
26
- }
27
- }
28
-
29
- run(parameters: ReadonlyArray<unknown>): {
30
- changes: number | bigint
31
- lastInsertRowid: number | bigint
32
- } {
33
- const result = this.stmt.run(...(parameters.map(coerce) as any))
34
- return {
35
- changes: result.changes,
36
- lastInsertRowid: result.lastInsertRowid,
37
- }
38
- }
39
- }
40
-
41
- export class BunSqliteDatabase implements SqliteDatabase {
42
- constructor(private readonly db: Database) {}
43
-
44
- prepare(sql: string): SqliteStatement {
45
- return new BunSqliteStatement(this.db.prepare(sql))
46
- }
47
-
48
- close(): void {
49
- this.db.close()
50
- }
51
- }
@@ -1,90 +0,0 @@
1
- import { describe, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { createBunSqliteKysely, createCoercionPlugin } from './index.js'
4
- import type { CoercionMap } from './index.js'
5
- import { sql, type Kysely } from 'kysely'
6
-
7
- interface TestDB {
8
- users: { id: number; settings: unknown }
9
- posts: { id: number; settings: unknown }
10
- }
11
-
12
- /**
13
- * The same map the CLI's local database builds from `coercion.gen.ts`: two
14
- * tables that disagree on the kind of a same-named column. Resolving it needs
15
- * the query's tables, which is what kept the CLI's copy and the runtime copies
16
- * from behaving identically.
17
- */
18
- const map: CoercionMap = {
19
- users: { settings: 'json' },
20
- posts: { settings: 'bool' },
21
- }
22
-
23
- const seed = async (db: Kysely<TestDB>) => {
24
- await sql`create table users (id integer primary key, settings text)`.execute(
25
- db
26
- )
27
- await sql`create table posts (id integer primary key, settings integer)`.execute(
28
- db
29
- )
30
- await sql`insert into users (id, settings) values (1, '{"theme":"dark"}')`.execute(
31
- db
32
- )
33
- await sql`insert into posts (id, settings) values (1, 1)`.execute(db)
34
- }
35
-
36
- describe('bun-sqlite coercion plugin', () => {
37
- test('resolves a colliding column against the queried table', async () => {
38
- const db = createBunSqliteKysely<TestDB>({
39
- filename: ':memory:',
40
- plugins: [createCoercionPlugin({ map })],
41
- })
42
- await seed(db)
43
-
44
- const user = await db
45
- .selectFrom('users')
46
- .select('settings')
47
- .executeTakeFirstOrThrow()
48
- assert.deepEqual(user.settings, { theme: 'dark' })
49
-
50
- const post = await db
51
- .selectFrom('posts')
52
- .select('settings')
53
- .executeTakeFirstOrThrow()
54
- assert.equal(post.settings, true)
55
-
56
- await db.destroy()
57
- })
58
-
59
- test('coerces date, bool and json columns read back from SQLite', async () => {
60
- const db = createBunSqliteKysely<{
61
- rows: { created_at: unknown; is_active: unknown; meta: unknown }
62
- }>({
63
- filename: ':memory:',
64
- camelCase: false,
65
- plugins: [
66
- createCoercionPlugin({
67
- map: {
68
- rows: { created_at: 'date', is_active: 'bool', meta: 'json' },
69
- },
70
- }),
71
- ],
72
- })
73
- await sql`create table rows (created_at text, is_active integer, meta text)`.execute(
74
- db
75
- )
76
- await sql`insert into rows values ('2026-06-26T00:00:00.000Z', 0, '{"a":1}')`.execute(
77
- db
78
- )
79
-
80
- const row = await db
81
- .selectFrom('rows')
82
- .selectAll()
83
- .executeTakeFirstOrThrow()
84
- assert.ok(row.created_at instanceof Date)
85
- assert.equal(row.is_active, false)
86
- assert.deepEqual(row.meta, { a: 1 })
87
-
88
- await db.destroy()
89
- })
90
- })
@@ -1,45 +0,0 @@
1
- import { describe, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { sql } from 'kysely'
4
- import { createBunSqliteKysely } from './create-bun-sqlite-kysely.js'
5
-
6
- interface TestDB {
7
- widgets: { id: number; displayName: string }
8
- }
9
-
10
- describe('createBunSqliteKysely', () => {
11
- test('creates a working in-memory Kysely with camelCase mapping by default', async () => {
12
- const db = createBunSqliteKysely<TestDB>({ filename: ':memory:' })
13
-
14
- await sql`CREATE TABLE widgets (id INTEGER PRIMARY KEY, display_name TEXT)`.execute(
15
- db
16
- )
17
- await db
18
- .insertInto('widgets')
19
- .values({ id: 1, displayName: 'hello' })
20
- .execute()
21
-
22
- const row = await db
23
- .selectFrom('widgets')
24
- .selectAll()
25
- .executeTakeFirstOrThrow()
26
-
27
- assert.equal(row.displayName, 'hello')
28
- await db.destroy()
29
- })
30
-
31
- test('can disable camelCase and layer extra plugins', async () => {
32
- const db = createBunSqliteKysely<{ t: { snake_col: string } }>({
33
- filename: ':memory:',
34
- camelCase: false,
35
- plugins: [],
36
- })
37
-
38
- await sql`CREATE TABLE t (snake_col TEXT)`.execute(db)
39
- await db.insertInto('t').values({ snake_col: 'raw' }).execute()
40
- const row = await db.selectFrom('t').selectAll().executeTakeFirstOrThrow()
41
-
42
- assert.equal(row.snake_col, 'raw')
43
- await db.destroy()
44
- })
45
- })
@@ -1,45 +0,0 @@
1
- import { Database } from 'bun:sqlite'
2
- import {
3
- Kysely,
4
- SqliteDialect,
5
- CamelCasePlugin,
6
- type KyselyPlugin,
7
- } from 'kysely'
8
- import type { SqliteFunctionMap } from '@pikku/kysely-sqlite'
9
- import { BunSqliteDatabase } from './bun-sqlite-adapter.js'
10
- import { registerSqliteFunctions } from './register-functions.js'
11
-
12
- export interface CreateBunSqliteKyselyOptions {
13
- /** Path to the SQLite file. Use ':memory:' for an in-memory DB. */
14
- filename: string
15
- /** Apply CamelCasePlugin so DB columns map to camelCase TS fields. Default true. */
16
- camelCase?: boolean
17
- /** Extra plugins to layer on top. */
18
- plugins?: KyselyPlugin[]
19
- /**
20
- * Accepted only so that setting it fails loudly. bun:sqlite cannot register
21
- * user-defined SQL functions, so passing any throws
22
- * SqliteFunctionsUnsupportedError here rather than leaving the queries that
23
- * call them to fail with "no such function" later.
24
- *
25
- * The option is declared — instead of simply absent — so that code shared
26
- * with `createNodeSqliteKysely` still type-checks and the incompatibility
27
- * shows up as an error that explains itself.
28
- */
29
- functions?: SqliteFunctionMap
30
- }
31
-
32
- export function createBunSqliteKysely<DB>(
33
- options: CreateBunSqliteKyselyOptions
34
- ): Kysely<DB> {
35
- const db = new Database(options.filename)
36
- if (options.functions) registerSqliteFunctions(db, options.functions)
37
- const plugins: KyselyPlugin[] = []
38
- if (options.camelCase ?? true) plugins.push(new CamelCasePlugin())
39
- if (options.plugins) plugins.push(...options.plugins)
40
-
41
- return new Kysely<DB>({
42
- dialect: new SqliteDialect({ database: new BunSqliteDatabase(db) }),
43
- plugins,
44
- })
45
- }
package/src/index.ts DELETED
@@ -1,16 +0,0 @@
1
- export { BunSqliteDatabase } from './bun-sqlite-adapter.js'
2
- export {
3
- createBunSqliteKysely,
4
- type CreateBunSqliteKyselyOptions,
5
- } from './create-bun-sqlite-kysely.js'
6
- export { registerSqliteFunctions } from './register-functions.js'
7
- export {
8
- SqliteFunctionsUnsupportedError,
9
- type SqliteFunctionMap,
10
- } from '@pikku/kysely-sqlite'
11
- export {
12
- createCoercionPlugin,
13
- type ColumnKind,
14
- type CoercionMap,
15
- type CreateCoercionPluginOptions,
16
- } from '@pikku/kysely'
@@ -1,54 +0,0 @@
1
- import { describe, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { Database } from 'bun:sqlite'
4
- import { SqliteFunctionsUnsupportedError } from '@pikku/kysely-sqlite'
5
- import { registerSqliteFunctions } from './register-functions.js'
6
- import { createBunSqliteKysely } from './create-bun-sqlite-kysely.js'
7
-
8
- describe('registerSqliteFunctions', () => {
9
- test('throws rather than silently skipping registration', () => {
10
- const db = new Database(':memory:')
11
- assert.throws(
12
- () => registerSqliteFunctions(db, { similarity: () => 0 }),
13
- SqliteFunctionsUnsupportedError
14
- )
15
- db.close()
16
- })
17
-
18
- test('names every requested function, so the error says what to port', () => {
19
- const db = new Database(':memory:')
20
- try {
21
- registerSqliteFunctions(db, {
22
- similarity: () => 0,
23
- levenshtein: () => 0,
24
- })
25
- assert.fail('expected a throw')
26
- } catch (error) {
27
- assert.ok(error instanceof SqliteFunctionsUnsupportedError)
28
- assert.deepEqual(error.functionNames, ['similarity', 'levenshtein'])
29
- assert.match(error.message, /similarity, levenshtein/)
30
- assert.match(error.message, /kysely-node-sqlite/)
31
- }
32
- db.close()
33
- })
34
-
35
- // The point of the whole change: the failure has to happen while the app is
36
- // wiring itself up, not on the first request that reaches a query using one.
37
- test('createBunSqliteKysely rejects `functions` at construction', () => {
38
- assert.throws(
39
- () =>
40
- createBunSqliteKysely({
41
- filename: ':memory:',
42
- functions: { similarity: () => 0 },
43
- }),
44
- SqliteFunctionsUnsupportedError
45
- )
46
- })
47
-
48
- test('omitting `functions` is unaffected', async () => {
49
- const db = createBunSqliteKysely<{ t: { id: number } }>({
50
- filename: ':memory:',
51
- })
52
- await db.destroy()
53
- })
54
- })
@@ -1,30 +0,0 @@
1
- import type { Database } from 'bun:sqlite'
2
- import {
3
- SqliteFunctionsUnsupportedError,
4
- type SqliteFunctionMap,
5
- } from '@pikku/kysely-sqlite'
6
-
7
- /**
8
- * The bun counterpart to `@pikku/kysely-node-sqlite`'s function of the same
9
- * name. It always throws: bun:sqlite exposes `loadExtension` but nothing
10
- * equivalent to node:sqlite's `db.function()`, so scalar UDFs cannot be
11
- * registered at all.
12
- *
13
- * It exists precisely so that the failure happens here. Without it the two
14
- * drivers differ silently — code written against node registers its functions,
15
- * the same code on bun does not, and the difference only surfaces as
16
- * `no such function: …` from whichever query calls one, which in practice means
17
- * one endpoint failing in production while the rest of the app looks healthy.
18
- *
19
- * Throwing at wiring time turns that into a startup error naming every function
20
- * involved.
21
- */
22
- export function registerSqliteFunctions(
23
- _db: Database,
24
- functions: SqliteFunctionMap
25
- ): never {
26
- throw new SqliteFunctionsUnsupportedError(
27
- 'bun:sqlite',
28
- Object.keys(functions)
29
- )
30
- }
package/tsconfig.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": ".",
5
- "module": "Node18",
6
- "outDir": "dist",
7
- "target": "esnext",
8
- "declaration": true,
9
- "types": ["bun"]
10
- },
11
- "include": ["src/**/*.ts"],
12
- "exclude": ["**/*.test.ts", "node_modules"]
13
- }