@pikku/kysely-bun-sqlite 0.12.4 → 0.12.7

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,85 @@
1
1
  # @pikku/kysely-bun-sqlite
2
2
 
3
+ ## 0.12.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 746ed6a: fix: one coercion plugin, not three
8
+
9
+ The Kysely coercion plugin existed in three copies — the CLI's local database,
10
+ `@pikku/kysely-node-sqlite` and `@pikku/kysely-bun-sqlite` — and all three had
11
+ drifted apart. Only the CLI's resolved a column against the tables the query
12
+ actually named, so two tables that disagree on the kind of a same-named column
13
+ coerced correctly in local development and silently did not at runtime; only
14
+ bun's dropped a genuinely ambiguous column instead of letting the last table
15
+ processed win.
16
+
17
+ The single implementation now lives in `@pikku/kysely`, which all three already
18
+ depended on, and keeps both behaviours: table-qualified resolution first, an
19
+ ambiguity-safe bare-name fallback second.
20
+
21
+ `ColumnKind` — the value type of the generated `coercion.gen.ts` — is
22
+ `'date' | 'bool' | 'json'`. The CLI's fourth member, `uuid`, was never a
23
+ coercion kind: the codegen excludes it from the map by construction, because a
24
+ UUID is a string in both Postgres and SQLite. It is now `AnnotationKind` in the
25
+ CLI, the union a column may declare in `db/annotations.ts`, of which
26
+ `ColumnKind` is the coercible subset.
27
+
28
+ `@pikku/cli` also drops its unused dependency on the Node-only
29
+ `@pikku/kysely-node-sqlite`.
30
+
31
+ - Updated dependencies [5a1a962]
32
+ - Updated dependencies [746ed6a]
33
+ - @pikku/core@0.12.86
34
+ - @pikku/kysely@0.13.18
35
+
36
+ ## 0.12.6
37
+
38
+ ### Patch Changes
39
+
40
+ - 6eef0a0: Bump every dependency to its latest compatible minor/patch across the monorepo.
41
+ - Updated dependencies [7722ceb]
42
+ - Updated dependencies [375c1ff]
43
+ - Updated dependencies [02a70cd]
44
+ - Updated dependencies [aeef159]
45
+ - Updated dependencies [a281de6]
46
+ - Updated dependencies [266e3bc]
47
+ - Updated dependencies [02a70cd]
48
+ - Updated dependencies [786dae5]
49
+ - Updated dependencies [6eef0a0]
50
+ - Updated dependencies [3561d67]
51
+ - Updated dependencies [a91c433]
52
+ - Updated dependencies [02a70cd]
53
+ - Updated dependencies [9537f74]
54
+ - Updated dependencies [2b57ca8]
55
+ - Updated dependencies [266e3bc]
56
+ - Updated dependencies [9fce0f1]
57
+ - Updated dependencies [83683a0]
58
+ - Updated dependencies [456c88b]
59
+ - Updated dependencies [456c88b]
60
+ - Updated dependencies [c127273]
61
+ - @pikku/core@0.12.85
62
+ - @pikku/kysely@0.13.17
63
+ - @pikku/kysely-sqlite@0.12.13
64
+
65
+ ## 0.12.5
66
+
67
+ ### Patch Changes
68
+
69
+ - 0686513: Declare `@pikku/core` as a peer dependency, matching `@pikku/kysely-sqlite`.
70
+
71
+ The package depends on `@pikku/kysely-sqlite`, which requires `@pikku/core` as a
72
+ peer, but never declared that requirement itself — so an install resolved
73
+ without complaint and the missing peer only surfaced at runtime. It is now
74
+ declared the same way its sibling declares it.
75
+
76
+ - Updated dependencies [7406bfe]
77
+ - Updated dependencies [6794681]
78
+ - Updated dependencies [a7fcd2e]
79
+ - @pikku/core@0.12.84
80
+ - @pikku/kysely@0.13.16
81
+ - @pikku/kysely-sqlite@0.12.12
82
+
3
83
  ## 0.12.4
4
84
 
5
85
  ### Patch Changes
@@ -2,4 +2,4 @@ export { BunSqliteDatabase } 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';
5
- export { createCoercionPlugin, type ColumnKind, type CreateCoercionPluginOptions, } from './coercion-plugin.js';
5
+ export { createCoercionPlugin, type ColumnKind, type CoercionMap, type CreateCoercionPluginOptions, } from '@pikku/kysely';
package/dist/src/index.js CHANGED
@@ -2,4 +2,4 @@ export { BunSqliteDatabase } 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';
5
- export { createCoercionPlugin, } from './coercion-plugin.js';
5
+ export { createCoercionPlugin, } from '@pikku/kysely';
@@ -1 +1 @@
1
- {"root":["../src/bun-sqlite-adapter.ts","../src/coercion-plugin.ts","../src/create-bun-sqlite-kysely.ts","../src/index.ts","../src/register-functions.ts"],"version":"6.0.3"}
1
+ {"root":["../src/bun-sqlite-adapter.ts","../src/create-bun-sqlite-kysely.ts","../src/index.ts","../src/register-functions.ts"],"version":"6.0.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely-bun-sqlite",
3
- "version": "0.12.4",
3
+ "version": "0.12.7",
4
4
  "description": "Kysely driver for Pikku backed by the built-in bun:sqlite module",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -19,12 +19,16 @@
19
19
  "bun": ">=1.0.0"
20
20
  },
21
21
  "dependencies": {
22
- "@pikku/kysely": "^0.13.15",
23
- "@pikku/kysely-sqlite": "^0.12.11",
24
- "kysely": "^0.29.0"
22
+ "@pikku/kysely": "^0.13.18",
23
+ "@pikku/kysely-sqlite": "^0.12.13",
24
+ "kysely": "^0.29.5"
25
+ },
26
+ "peerDependencies": {
27
+ "@pikku/core": "^0.12.86"
25
28
  },
26
29
  "devDependencies": {
30
+ "@pikku/core": "^0.12.86",
27
31
  "@types/bun": "latest",
28
32
  "typescript": "^6.0.3"
29
33
  }
30
- }
34
+ }
@@ -1,95 +1,90 @@
1
1
  import { describe, test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import type { QueryResult, UnknownRow } from 'kysely'
4
- import { createCoercionPlugin } from './coercion-plugin.js'
3
+ import { createBunSqliteKysely, createCoercionPlugin } from './index.js'
4
+ import type { CoercionMap } from './index.js'
5
+ import { sql, type Kysely } from 'kysely'
5
6
 
6
- const runTransform = async (
7
- map: Parameters<typeof createCoercionPlugin>[0]['map'],
8
- rows: UnknownRow[]
9
- ): Promise<UnknownRow[]> => {
10
- const plugin = createCoercionPlugin({ map })
11
- const result = { rows } as QueryResult<UnknownRow>
12
- const out = await plugin.transformResult({
13
- result,
14
- queryId: { queryId: 'test' },
15
- } as Parameters<NonNullable<typeof plugin.transformResult>>[0])
16
- return out.rows as UnknownRow[]
7
+ interface TestDB {
8
+ users: { id: number; settings: unknown }
9
+ posts: { id: number; settings: unknown }
17
10
  }
18
11
 
19
- describe('createCoercionPlugin', () => {
20
- test('transformQuery passes the node through untouched', () => {
21
- const plugin = createCoercionPlugin({ map: {} })
22
- const node = { kind: 'SelectQueryNode' } as any
23
- assert.equal(
24
- plugin.transformQuery({ node, queryId: { queryId: 'x' } } as any),
25
- node
26
- )
27
- })
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
+ }
28
22
 
29
- test('coerces date columns from ISO strings to Date', async () => {
30
- const [row] = await runTransform({ users: { created_at: 'date' } }, [
31
- { created_at: '2026-06-26T00:00:00.000Z' },
32
- ])
33
- assert.ok(row.created_at instanceof Date)
34
- assert.equal(
35
- (row.created_at as Date).toISOString(),
36
- '2026-06-26T00:00:00.000Z'
37
- )
38
- })
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
+ }
39
35
 
40
- test('leaves unparseable date strings untouched', async () => {
41
- const [row] = await runTransform({ users: { created_at: 'date' } }, [
42
- { created_at: 'not-a-date' },
43
- ])
44
- assert.equal(row.created_at, 'not-a-date')
45
- })
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)
46
43
 
47
- test('coerces bool columns from 0/1 numbers and bigints', async () => {
48
- const rows = await runTransform(
49
- { users: { is_active: 'bool', has_pets: 'bool' } },
50
- [{ is_active: 1, has_pets: 0n }]
51
- )
52
- assert.equal(rows[0].is_active, true)
53
- assert.equal(rows[0].has_pets, false)
54
- })
44
+ const user = await db
45
+ .selectFrom('users')
46
+ .select('settings')
47
+ .executeTakeFirstOrThrow()
48
+ assert.deepEqual(user.settings, { theme: 'dark' })
55
49
 
56
- test('parses json columns and tolerates invalid json', async () => {
57
- const rows = await runTransform(
58
- { users: { meta: 'json', broken: 'json' } },
59
- [{ meta: '{"a":1}', broken: '{bad' }]
60
- )
61
- assert.deepEqual(rows[0].meta, { a: 1 })
62
- assert.equal(rows[0].broken, '{bad')
63
- })
50
+ const post = await db
51
+ .selectFrom('posts')
52
+ .select('settings')
53
+ .executeTakeFirstOrThrow()
54
+ assert.equal(post.settings, true)
64
55
 
65
- test('passes through null values and unmapped columns', async () => {
66
- const rows = await runTransform({ users: { created_at: 'date' } }, [
67
- { created_at: null, name: 'leave me' },
68
- ])
69
- assert.equal(rows[0].created_at, null)
70
- assert.equal(rows[0].name, 'leave me')
56
+ await db.destroy()
71
57
  })
72
58
 
73
- test('matches both snake_case and camelCase column names', async () => {
74
- const rows = await runTransform({ users: { created_at: 'date' } }, [
75
- { createdAt: '2026-06-26T00:00:00.000Z' },
76
- ])
77
- assert.ok(rows[0].createdAt instanceof Date)
78
- })
79
-
80
- test('leaves columns ambiguous across tables uncoerced', async () => {
81
- const rows = await runTransform(
82
- { users: { value: 'json' }, events: { value: 'bool' } },
83
- [{ value: '{"a":1}' }]
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
84
75
  )
85
- assert.equal(rows[0].value, '{"a":1}')
86
- })
87
-
88
- test('still coerces columns that agree across tables', async () => {
89
- const rows = await runTransform(
90
- { users: { active: 'bool' }, events: { active: 'bool' } },
91
- [{ active: 1 }]
76
+ await sql`insert into rows values ('2026-06-26T00:00:00.000Z', 0, '{"a":1}')`.execute(
77
+ db
92
78
  )
93
- assert.equal(rows[0].active, true)
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()
94
89
  })
95
90
  })
package/src/index.ts CHANGED
@@ -11,5 +11,6 @@ export {
11
11
  export {
12
12
  createCoercionPlugin,
13
13
  type ColumnKind,
14
+ type CoercionMap,
14
15
  type CreateCoercionPluginOptions,
15
- } from './coercion-plugin.js'
16
+ } from '@pikku/kysely'
@@ -1,106 +0,0 @@
1
- import type { KyselyPlugin, UnknownRow } from 'kysely'
2
-
3
- export type ColumnKind = 'date' | 'bool' | 'json'
4
-
5
- /**
6
- * Per-table column kind map. Keys are snake_case table names matching the
7
- * physical SQLite tables; inner keys are snake_case column names.
8
- * Generated by `pikku db migrate` from `-- @date|@bool|@json` annotations
9
- * and naming conventions (col ends in _at/_on → date; starts with is_/has_/can_ → bool).
10
- */
11
- export type CoercionMap = Record<string, Record<string, ColumnKind>>
12
-
13
- export interface CreateCoercionPluginOptions {
14
- map: CoercionMap
15
- }
16
-
17
- function fromDb(value: unknown, kind: ColumnKind): unknown {
18
- if (value == null) return value
19
- switch (kind) {
20
- case 'date':
21
- if (typeof value === 'string') {
22
- const d = new Date(value)
23
- return Number.isNaN(d.getTime()) ? value : d
24
- }
25
- return value
26
- case 'bool':
27
- if (typeof value === 'number') return value !== 0
28
- if (typeof value === 'bigint') return value !== 0n
29
- return value
30
- case 'json':
31
- if (typeof value !== 'string') return value
32
- try {
33
- return JSON.parse(value)
34
- } catch {
35
- return value
36
- }
37
- }
38
- }
39
-
40
- function snakeToCamel(name: string): string {
41
- return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
42
- }
43
-
44
- /**
45
- * Flatten the per-table map into a single column-name → kind lookup.
46
- * Indexed by BOTH snake_case and camelCase so it works regardless of
47
- * CamelCasePlugin ordering in the plugin array.
48
- *
49
- * When two tables disagree on the kind for the same column name, the column
50
- * is ambiguous (a joined/aliased query could carry either) and is left
51
- * uncoerced rather than letting whichever table was processed last win.
52
- */
53
- function buildGlobalMap(map: CoercionMap): Record<string, ColumnKind> {
54
- const out: Record<string, ColumnKind> = {}
55
- const ambiguous = new Set<string>()
56
- const assign = (key: string, kind: ColumnKind) => {
57
- const existing = out[key]
58
- if (existing !== undefined && existing !== kind) {
59
- ambiguous.add(key)
60
- return
61
- }
62
- out[key] = kind
63
- }
64
- for (const tbl of Object.values(map)) {
65
- for (const [col, kind] of Object.entries(tbl)) {
66
- assign(col, kind)
67
- assign(snakeToCamel(col), kind)
68
- }
69
- }
70
- for (const key of ambiguous) delete out[key]
71
- return out
72
- }
73
-
74
- /**
75
- * Convert SQLite-stored values (TEXT/INTEGER) back into the logical types
76
- * app code expects (Date / boolean / parsed JSON).
77
- *
78
- * Write-side coercion (Date → ISO string, boolean → 0/1, object → JSON) is
79
- * handled in the NodeSqliteDatabase adapter (always-on), so this plugin is
80
- * read-only.
81
- *
82
- * Place AFTER CamelCasePlugin in the plugin array (or it handles both
83
- * orderings via the dual-keyed global map).
84
- */
85
- export function createCoercionPlugin(
86
- options: CreateCoercionPluginOptions
87
- ): KyselyPlugin {
88
- const globalMap = buildGlobalMap(options.map)
89
- return {
90
- transformQuery(args) {
91
- return args.node
92
- },
93
- async transformResult(args) {
94
- const out: UnknownRow[] = []
95
- for (const row of args.result.rows as UnknownRow[]) {
96
- const next: UnknownRow = { ...row }
97
- for (const [col, val] of Object.entries(row)) {
98
- const kind = globalMap[col]
99
- if (kind) next[col] = fromDb(val, kind)
100
- }
101
- out.push(next)
102
- }
103
- return { ...args.result, rows: out }
104
- },
105
- }
106
- }