Files
STTracker/docs/plans/001-phase1-sdd/reports/task-1-report.md
T
Lucas WintherandClaude Opus 5.5 be04fab098 docs: phase 1 handoff, execution ledger and task reports
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>
2026-11-02 04:53:02 +01:00

7.1 KiB
Raw Blame History

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.