feat(db): Bun.SQL Kysely dialect for sqlite and postgres

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 15:30:49 +01:00
co-authored by Claude Opus 5.5
parent 6c5a9fbad9
commit 8248a2ac27
11 changed files with 403 additions and 1 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ user layer or files a Changes-inbox event.
| `apps/server` | `Bun.serve`: `/api/v1`, `/ingest/<plugin>`, job worker, static web | planned |
| `apps/web` | React app (HTML imports) | planned |
| `packages/core` | domain services: entries, matching, change engine, history, export | planned |
| `packages/db` | Kysely, SQLite/Postgres dialects, migrations | planned |
| `packages/db` | Kysely over one Bun.SQL dialect (SQLite + Postgres), ids, JSON helpers, migrations, `forEachDialect` test harness | exists (schema pending) |
| `packages/plugin-sdk` | plugin contract, `SourceRecord`, http/rate-limit helpers, fixture test harness | planned |
| `plugins/<id>` | one source integration each (hardcover, trakt, mangabaka, listenbrainz, lastfm, csv) | planned |
| `docs/design/mockups` | approved UI mockups (reference only) | exists |
+35
View File
@@ -4,6 +4,41 @@ Append-only. Never edit or delete an entry. Supersede it with a new one that lin
One entry per decision a future maintainer would otherwise have to reverse-engineer.
No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry (date, title, line number).
## 2026-10-30: A single Kysely dialect over Bun.SQL for SQLite and Postgres
**Status:** accepted
**Context:** Kysely ships no driver for Bun. `Bun.SQL` speaks both `sqlite://` and `postgres://` behind one API.
**Decision:**
- `packages/db/src/dialect.ts` is one Kysely `Dialect` whose driver wraps `new SQL(url)`, choosing Kysely's
SQLite or Postgres adapter, compiler and introspector by URL scheme.
- Postgres connections come from `sql.reserve()`. Bun's SQLite adapter does not support `reserve()`, so SQLite
uses the single `SQL` instance as its only connection, serialised by a mutex so transactions never interleave.
- Transactions are plain `begin`/`commit`/`rollback` on the held connection.
**Alternatives:**
- Separate dialects over `bun:sqlite` and `Bun.sql`: two drivers to maintain for the same job.
- Community Bun dialect packages: an extra dependency, and none covers both databases.
**Consequences:** SQLite is effectively single-connection (fine for a self-hosted app). Savepoints and real
streaming are not implemented; `streamQuery` returns the whole result as one chunk.
## 2026-10-30: JSON columns are stored as text on both dialects
**Status:** accepted
**Context:** Spec §11 calls for `jsonb`. SQLite has no JSON type, so `jsonb` on Postgres would need
per-dialect read/write code in every service that touches a JSON column.
**Decision:** JSON columns are `text` everywhere, written with `toJson()` and read with `fromJson()` from
`@sttracker/db/json`. `fromJson` returns a caller-supplied fallback for null or invalid text.
**Alternatives:** `jsonb` on Postgres with per-dialect read/write code.
**Consequences:** No JSON operators or indexes in SQL; filtering on JSON content happens in app code. Can be
superseded later by a migration to `jsonb` if a query needs it.
## 2026-10-30: UUIDv7 instead of ULID for IDs
**Status:** accepted
**Context:** Spec and `AGENTS.md` say IDs are ULIDs generated in app code. Bun has `Bun.randomUUIDv7()` built in.
**Decision:** `newId()` in `@sttracker/db/ids` returns `Bun.randomUUIDv7()`. IDs are stored as `text`.
**Alternatives:** ULID, which needs a dependency or hand-written encoder.
**Consequences:** IDs are equally time-sortable as strings with no extra dependency. They are 36 characters
instead of 26. Where `AGENTS.md` or the spec say "ULID", read "UUIDv7".
## 2026-10-30: Product design decisions recorded in spec 001
**Status:** accepted
**Context:** Brainstorming with the owner produced the v1 design.