Backend DB To use PostgreSQL instead of SQLite #18
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Currently everything is in either a toml config or a single sqlite database. Move everything into a PostgreSQL database instead.
DB Migrationto Backend DB To use PostgreSQL instead of SQLitePlan
Three phases, each deployed and checked before the next.
Today: one SQLite file (290 MB; 7 tables, about 115,000 rows, mostly item text) behind one global lock; 70 queries in
db.rs, called from about 140 places inmain.rsandweb.rs, all from async code.config.tomlholds both the start-up settings and data the app edits at runtime: 130 feeds and the server-wide settings.Decisions
sea-orm-migration. Async, and one set of code runs on SQLite and Postgres, so porting the code and switching the database happen separately.contentproject, not the shared Immich server. ipx gets its own login role and database; the test database lives on the same container.config.toml: bind address, token, trusted proxies, torrent, paths. They decide who gets in and are needed before any database connection. The feed catalogue and the server-wide settings move into the database.Phase 1: port to SeaORM, still on SQLite
state.dbopens as it is.db.rskeeps its functions but runs them through the ORM; the call sites gain.await. The item list's dynamic sorts, its size and type subqueries, and the duplicate-enclosure merge stay hand-written SQL, written to run on both databases.Phase 2: switch to Postgres
IPX_DATABASE_URLpickssqlite://…orpostgres://….ipx importcopies SQLite into an empty Postgres database through the same entities, then resets the id counters and checks the row counts.state.dbandconfig.tomlaside, import, deploy, check counts and a scan. Rollback: point the URL back at the untouched file.Phase 3: feeds and server settings out of
config.tomlfeeds(OPML children become ordinary rows); the server settings get a one-rowsettingstable.ipx import-configloads the current file. Update the docs.Risks
password_envstays available.pg_dump), and ipx has to reconnect when it restarts.Progress update
Done
Phase 1: SeaORM, still on SQLite. On the
seaormbranch (pushed, not merged or deployed).src/entity.rs: the seven tables as SeaORM entities, now the schema. On open, ipx creates whatever table or index is missing (CREATE ... IF NOT EXISTS) and never alters or drops anything.rusqlite, the SQL schema andmigrate()are gone.users_name_loweron every open, so everyipxcommand took a write lock. Under loadipx status(the healthcheck) then failed 7 times in 15; the old code failed none. Replaced with create-if-missing: 15 in 15 again.Phase 2: Postgres. Same branch, not deployed.
postgres:18server (port 5433): anipodderxlogin role owningipodderx(empty, for the real data) andipodderx_test. Connection strings are in/src/.envasIPX_DATABASE_URLandIPX_TEST_DATABASE_URL; the app never uses the superuser.IPX_DATABASE_URLpicks the database: Postgres when set, the SQLite file otherwise. Passwords are kept out of error messages.IPX_TEST_DATABASE_URLset, each test gets its own Postgres schema.ipx copy-db <state.db>copies SQLite into an empty Postgres database in one transaction, then resets the id counters. A production copy went across in 14 seconds, every row count and column fingerprint identical (text lengths, timestamps, sizes, ids, every yes/no flag).NULLS FIRST/NULLS LAST, matching SQLite; a new test fails on Postgres without it.Worth knowing
en_USlocale ("émile" next to "e"), where SQLite used byte order ("émile" after "zebra"). An improvement, but the browser test that asserts byte order would fail on Postgres; the browser suite still runs on SQLite.postgrescontainer has restart policyno: after a reboot it won't come back, and ipx won't start without it. Settingrestart: unless-stoppedin thedatabasesproject would fix that.pg_dumpofipodderx.Next
Confirm the loose end above.
Merge
seaormtomain.Cut over:
state.dbaside;ipx copy-dbintoipodderx;IPX_DATABASE_URLto the ipodderx service in thecontentcompose file;Rollback: remove the variable and redeploy the old image; the SQLite file is untouched.
Phase 3 later: the feed catalogue and server settings out of
config.tomlinto the database.Loose end settled: the 210-versus-222 feed count was my test, not the code. At start-up ipx retires the feeds of an OPML that has left config.toml: kept if they have downloaded files, dropped if not. On my test machine none of production's files exist, so the reaper marks them missing and the next start-up drops those feeds. My runs were cut off by timeouts at different points in that clean-up. Run to completion on fresh copies, the old and new code give the same result, identical row for row: 193 feeds, the same 119 davewiner feeds retired. In production the files exist, so nothing changes there.
It did show old removed feeds still in the database, which the copy would carry into Postgres unless cleared first:
Waiting on a decision about whether to clear them before the copy.
Phase 2 done: production runs on Postgres as of 2026-09-18 21:44 (Toronto), merged to main in
c8df148.ipx copy-dbwith the new image (5 s);ipx statusanswers from it (162 feeds, 583 downloaded), a forced scan wrote to it, the web UI reads from it, and state.db has not been written since the copy.ipodderxlogin has a new password, kept in /mnt/fast/appdata/ipodderx/database.env (root-only) and passed in withenv_filein the content compose file (backup: compose.yaml.bak-pre-postgres-20260918);192.168.1.130:5000/ipodderx:pre-postgres. state.db is untouched.Still to do: the
postgrescontainer (project databases) has restart policyno, so it won't come back after a reboot, and ipx won't start without it; it also needs a pg_dump backup. Phase 3 (feeds and server settings out of config.toml) keeps this issue open.Phase 3 done, deployed (
8849ba6); this finishes the issue.catalogueandsettings.ipx add/rm/importmakes goes to the database.Found along the way: Postgres sends a notice for every CREATE ... IF NOT EXISTS on something existing (11 on each open). sqlx logs them, and tracing-subscriber's per-layer filters then silently dropped the next line ipx logged. ipx's connections now ask for warnings and above only.
Before the deploy: a pg_dump through db-backup, and config.toml.bak-pre-phase3-20260918.