Three changes today, all on the backlog data layer that moved to Postgres a couple of days ago. The first was assist backlog export / import. Now that everything lives in one shared Postgres database rather than per-repo SQLite files, I wanted a way to snapshot the whole thing and move it between machines. Both commands lean on Postgres’ COPY for speed: export streams each table out via COPY ... TO STDOUT and frames it into a single self-contained file, and import reads it back via COPY ... FROM STDIN. The dump format is a JSON header — { format, version, tables } — followed by per-table sections prefixed with @table <name> <byteLength>. The byte-length framing keeps each table’s COPY payload verbatim regardless of its contents, so the round-trip is faithful. import validates the header against the expected schema and confirms before replacing existing data.

The bigger change was migrating the whole data layer from hand-written pg queries to Drizzle. The old BacklogDb interface wrapped raw SQL strings with ? placeholders; every loader and saver now builds queries through Drizzle’s query builder against a typed backlogSchema. The win that motivated it was batching — loadAllItems used to loop over every item firing separate queries for comments, links, and plan, which is fine for a handful of items but scales badly. With Drizzle’s relational queries those collapse into far fewer round-trips. The migration touched a lot of files (BacklogDbBacklogOrm, getBacklogDbgetBacklogOrm, and most of the backlog command surface) but the shape stayed the same. A nice side effect: tests now run against PGlite — real Postgres compiled to WASM — instead of mocking out SQLite, so the SQL is validated against the production dialect without needing an external database. The Drizzle client is structurally identical across the node-postgres and PGlite drivers, so the test client is just cast to the production type.

The last one was cosmetic. The backlog list shows which repo each item came from, and after the Postgres move that label was the full normalized origin (github.com/org/repo). I trimmed it down — originDisplayLabels resolves the shortest unambiguous name across the whole list, so a remote shows just its bare repo name (repo) unless two listed remotes share that name, in which case both fall back to the disambiguating org/repo form. Local repos always show their final path segment. The decision is made across the full set being displayed, so a name only gets shortened when it’s genuinely unique in that view.