Captures the owner's requirements and decisions from the planning conversation, current branch/worktree state, open tasks 17-19, pending owner decisions and the controller's rulings so a new agent can resume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
7.1 KiB
Markdown
119 lines
7.1 KiB
Markdown
# Task 1 report: `packages/db`
|
||
|
||
**Status:** DONE_WITH_CONCERNS (concerns are minor; see the end)
|
||
**Branch:** `feat/phase1`, base fb4ff36
|
||
|
||
## Commits
|
||
- `6c5a9fb` chore: workspace package stubs with approved dependencies
|
||
- `8248a2a` feat(db): Bun.SQL Kysely dialect for sqlite and postgres
|
||
- `27ec24e` feat(core): shared domain error classes
|
||
|
||
## What was built
|
||
- **`packages/db/src/dialect.ts`**:
|
||
- `type Dialect = "sqlite" | "postgres"` and `dialectOf(url)`, which throws on an unknown scheme.
|
||
- `BunSqlDialect` is one Kysely `Dialect` over `new SQL(url)`. It picks the Sqlite or Postgres adapter, compiler and introspector by URL scheme.
|
||
- `executeQuery` calls `runner.unsafe(sql, params)`. It sets `numAffectedRows` only for INSERT/UPDATE/DELETE/MERGE, and `insertId` only for SQLite INSERT.
|
||
- Transactions are `begin`/`commit`/`rollback` on the held connection. On Postgres, `begin` can carry an isolation level and access mode.
|
||
- **Deviation from the brief (forced by Bun):** Bun's SQLite adapter throws `This adapter doesn't support connection reservation` on `reserve()`.
|
||
- Postgres uses `sql.reserve()` as the brief says.
|
||
- SQLite uses the single `SQL` instance as its connection, guarded by a mutex. The lock is held from `acquireConnection` to `releaseConnection`, so a transaction never interleaves with other queries.
|
||
- This is recorded in DECISIONS.
|
||
- **`db.ts`:** `createDb(url): Promise<Db>`, with `Db { k: Kysely<Database>; dialect; close() }`.
|
||
- SQLite: runs `PRAGMA foreign_keys = ON`, plus `PRAGMA journal_mode = WAL` unless the URL is `:memory:`.
|
||
- Postgres: runs `select 1` to fail fast on a bad URL.
|
||
- If init fails, the Kysely instance is destroyed before the error is rethrown.
|
||
- **`ids.ts`:** `newId()` returns `Bun.randomUUIDv7()`; `nowIso()` returns `new Date().toISOString()`.
|
||
- **`json.ts`:** `toJson` and `fromJson<T>(s, fallback)`. The fallback is returned for null, undefined or invalid JSON.
|
||
- **`migrate.ts`:** `migrateToLatest(db)` runs a Kysely `Migrator` (imported from `kysely/migration`, as Kysely 0.29 requires) with a static provider over `migrations/index.ts`. An empty set works. It throws with the name of the failing migration.
|
||
- **`migrations/index.ts`:** `export const migrations: Record<string, Migration> = {}`.
|
||
- **`schema.ts`:** placeholder `export interface Database {}`.
|
||
- **`testing.ts`:** `forEachDialect(name, fn)` registers `describe("<name> [sqlite]")`, plus `[postgres]` when `TEST_POSTGRES_URL` is set.
|
||
- `beforeEach` builds a fresh migrated DB: SQLite uses `:memory:`; Postgres runs `create database "st_<uuid hex>"` through an admin connection and uses the derived URL.
|
||
- `afterEach` closes the DB.
|
||
- `afterAll` drops every created database `with (force)`.
|
||
- **`packages/core/src/errors.ts`:** `AuthError(code)`, `ValidationError(field, reason)` (code `validation`), `CryptoError(message)` (code `crypto`), `ImportError(code)` and `NotFoundError(what)` (code `not_found`).
|
||
- All of them extend Error and have a readonly `code` and a `name` set.
|
||
- **Package stubs:** these get name, private, type module, `exports {"./*": "./src/*.ts"}`, and the dependencies from the ruling:
|
||
- packages: db, core, plugin-sdk;
|
||
- apps: server, web;
|
||
- plugins: trakt, hardcover, mangabaka, csv, listenbrainz, lastfm.
|
||
|
||
Versions are caret ranges of the npm latest on 2026-10-30:
|
||
- kysely ^0.29.7, zod ^4.6.7, fflate ^0.8.3;
|
||
- react and react-dom ^19.3.1, wouter ^3.13.0, @tanstack/react-query ^5.105.0;
|
||
- @types/react and @types/react-dom ^19.3.0.
|
||
|
||
`bun install` was run and `bun.lock` committed. `bun install --frozen-lockfile` reports no changes.
|
||
- **Docs:**
|
||
- DECISIONS: three new entries, newest first (single Bun.SQL dialect; JSON as text; UUIDv7 instead of ULID).
|
||
- ARCHITECTURE: the map row for `packages/db` is updated.
|
||
|
||
## Files changed
|
||
- `bun.lock`
|
||
- `docs/ARCHITECTURE.md`, `docs/DECISIONS.md`
|
||
- `apps/{server,web}/package.json`
|
||
- `packages/{core,db,plugin-sdk}/package.json`
|
||
- `plugins/{trakt,hardcover,mangabaka,csv,listenbrainz,lastfm}/package.json`
|
||
- `packages/db/src/{dialect,db,ids,json,migrate,schema,testing}.ts`, `packages/db/src/migrations/index.ts`
|
||
- `packages/db/test/db.test.ts`
|
||
- `packages/core/src/errors.ts`
|
||
|
||
## TDD evidence
|
||
**Tests:**
|
||
- Inside `forEachDialect`, per dialect:
|
||
- "round-trips a row";
|
||
- "transactions roll back";
|
||
- "transactions commit" (extra);
|
||
- "newId is time-sortable" (1000 ids, sorted and unique).
|
||
- Outside it, once: a `fromJson` fallback test.
|
||
|
||
RED, written before any source existed:
|
||
```
|
||
$ bun test packages/db
|
||
error: Cannot find module '../src/ids.ts' from '/home/user/STTracker/packages/db/test/db.test.ts'
|
||
0 pass
|
||
1 fail
|
||
1 error
|
||
```
|
||
GREEN on SQLite:
|
||
```
|
||
$ bun test packages/db
|
||
5 pass
|
||
0 fail
|
||
12 expect() calls
|
||
Ran 5 tests across 1 file. [371.00ms]
|
||
```
|
||
GREEN on SQLite and Postgres:
|
||
```
|
||
$ TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres bun test packages/db
|
||
9 pass
|
||
0 fail
|
||
20 expect() calls
|
||
Ran 9 tests across 1 file. [1.87s]
|
||
```
|
||
**Mutation check:** I temporarily changed `rollbackTransaction` to issue `commit`. "transactions roll back" then failed on both `[sqlite]` and `[postgres]`, and I restored the code. After the Postgres runs, no `st_%` databases were left over.
|
||
|
||
**Manual probe:**
|
||
- `createDb("sqlite://rel.db")` creates `./rel.db` with `journal_mode=wal` and `foreign_keys=1`.
|
||
- `createDb("mysql://x")` throws `Unsupported database URL scheme: mysql`.
|
||
|
||
## Typecheck
|
||
```
|
||
$ bun run typecheck
|
||
$ tsc --noEmit -p tsconfig.json
|
||
```
|
||
It exits clean with no output.
|
||
|
||
## Self-review findings
|
||
- The mutex is released through Kysely's `try/finally` in its connection provider, so a failing query cannot leave the SQLite lock held.
|
||
- `numAffectedRows` is not set for SELECTs, because Bun's `count` there is the row count.
|
||
- Postgres `count(*)` comes back as a string (bigint). The test uses `Number()`. Later tasks should do the same or cast in SQL.
|
||
- Not implemented, on purpose: savepoints (Kysely raises a clear error if they are used), and true streaming (`streamQuery` yields the whole result as one chunk). Both are recorded in DECISIONS.
|
||
- Fixed during review: one claim I could not verify, about how Bun.SQL returns `jsonb`, was removed from the DECISIONS text.
|
||
|
||
## Concerns
|
||
1. **SQLite has no `reserve()`.** That is why it uses a single connection behind a mutex, which deviates from the brief's "acquireConnection returns `sql.reserve()`". The public interface is unchanged.
|
||
2. **Postgres test cost.** `create database` takes about 100–300 ms per test, and once took about 1 s. One run of 4 PG tests took 11 s; reruns took 1.6–3.8 s. With many DB tests in later tasks, the Postgres suite may get slow. A faster option is one DB per `describe` with a schema reset per test. That would change the harness contract, so I did not do it.
|
||
3. **ULID wording.** `AGENTS.md` (Rules: "IDs are ULIDs") still says ULID. The DECISIONS entry says to read it as UUIDv7. Editing `AGENTS.md` was outside my ruling, so the maintainer or controller may want a one-word fix.
|
||
4. **Kysely 0.29 import path.** `Migrator`, `Migration` and `MigrationProvider` must be imported from `kysely/migration`; the root exports are deprecated type errors. Task 2's migration files should import `Migration` from `kysely/migration`.
|