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>
7.1 KiB
Task 1 report: packages/db
Status: DONE_WITH_CONCERNS (concerns are minor; see the end)
Branch: feat/phase1, base fb4ff36
Commits
6c5a9fbchore: workspace package stubs with approved dependencies8248a2afeat(db): Bun.SQL Kysely dialect for sqlite and postgres27ec24efeat(core): shared domain error classes
What was built
-
packages/db/src/dialect.ts:type Dialect = "sqlite" | "postgres"anddialectOf(url), which throws on an unknown scheme.BunSqlDialectis one KyselyDialectovernew SQL(url). It picks the Sqlite or Postgres adapter, compiler and introspector by URL scheme.executeQuerycallsrunner.unsafe(sql, params). It setsnumAffectedRowsonly for INSERT/UPDATE/DELETE/MERGE, andinsertIdonly for SQLite INSERT.- Transactions are
begin/commit/rollbackon the held connection. On Postgres,begincan 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 reservationonreserve().- Postgres uses
sql.reserve()as the brief says. - SQLite uses the single
SQLinstance as its connection, guarded by a mutex. The lock is held fromacquireConnectiontoreleaseConnection, so a transaction never interleaves with other queries. - This is recorded in DECISIONS.
- Postgres uses
-
db.ts:createDb(url): Promise<Db>, withDb { k: Kysely<Database>; dialect; close() }.- SQLite: runs
PRAGMA foreign_keys = ON, plusPRAGMA journal_mode = WALunless the URL is:memory:. - Postgres: runs
select 1to fail fast on a bad URL. - If init fails, the Kysely instance is destroyed before the error is rethrown.
- SQLite: runs
-
ids.ts:newId()returnsBun.randomUUIDv7();nowIso()returnsnew Date().toISOString(). -
json.ts:toJsonandfromJson<T>(s, fallback). The fallback is returned for null, undefined or invalid JSON. -
migrate.ts:migrateToLatest(db)runs a KyselyMigrator(imported fromkysely/migration, as Kysely 0.29 requires) with a static provider overmigrations/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: placeholderexport interface Database {}. -
testing.ts:forEachDialect(name, fn)registersdescribe("<name> [sqlite]"), plus[postgres]whenTEST_POSTGRES_URLis set.beforeEachbuilds a fresh migrated DB: SQLite uses:memory:; Postgres runscreate database "st_<uuid hex>"through an admin connection and uses the derived URL.afterEachcloses the DB.afterAlldrops every created databasewith (force).
-
packages/core/src/errors.ts:AuthError(code),ValidationError(field, reason)(codevalidation),CryptoError(message)(codecrypto),ImportError(code)andNotFoundError(what)(codenot_found).- All of them extend Error and have a readonly
codeand anameset.
- All of them extend Error and have a readonly
-
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 installwas run andbun.lockcommitted.bun install --frozen-lockfilereports 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/dbis updated.
Files changed
bun.lockdocs/ARCHITECTURE.md,docs/DECISIONS.mdapps/{server,web}/package.jsonpackages/{core,db,plugin-sdk}/package.jsonplugins/{trakt,hardcover,mangabaka,csv,listenbrainz,lastfm}/package.jsonpackages/db/src/{dialect,db,ids,json,migrate,schema,testing}.ts,packages/db/src/migrations/index.tspackages/db/test/db.test.tspackages/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
fromJsonfallback 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.dbwithjournal_mode=walandforeign_keys=1.createDb("mysql://x")throwsUnsupported 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/finallyin its connection provider, so a failing query cannot leave the SQLite lock held. numAffectedRowsis not set for SELECTs, because Bun'scountthere is the row count.- Postgres
count(*)comes back as a string (bigint). The test usesNumber(). 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 (
streamQueryyields 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
- SQLite has no
reserve(). That is why it uses a single connection behind a mutex, which deviates from the brief's "acquireConnection returnssql.reserve()". The public interface is unchanged. - Postgres test cost.
create databasetakes 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 perdescribewith a schema reset per test. That would change the harness contract, so I did not do it. - ULID wording.
AGENTS.md(Rules: "IDs are ULIDs") still says ULID. The DECISIONS entry says to read it as UUIDv7. EditingAGENTS.mdwas outside my ruling, so the maintainer or controller may want a one-word fix. - Kysely 0.29 import path.
Migrator,MigrationandMigrationProvidermust be imported fromkysely/migration; the root exports are deprecated type errors. Task 2's migration files should importMigrationfromkysely/migration.