Docs: the database through SeaORM

CLAUDE.md and the architecture notes described the SQL schema and migrate(),
both gone: the entities are the schema, create_missing makes what is missing,
and hand-written SQL has to run on SQLite and Postgres both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 19:11:24 +00:00
parent 611716d8b7
commit bd8f6ab855
2 changed files with 20 additions and 13 deletions

View File

@@ -125,7 +125,7 @@ Non-trivial logic leaves one runnable check behind. Pure functions (`merge_polic
subscriber. Two feeds publishing the same URL means only the first one scanned shows it. subscriber. Two feeds publishing the same URL means only the first one scanned shows it.
* **Read state lives in `entry_state`, per user, and nowhere else.** `entries` had `read`, `flagged` * **Read state lives in `entry_state`, per user, and nowhere else.** `entries` had `read`, `flagged`
and `position` columns from before accounts; two bugs came from queries still reading them and `position` columns from before accounts; two bugs came from queries still reading them
(retention, and the entry pruner), and `migrate()` now drops them. (retention, and the entry pruner), and they were dropped in 0.5.
* **The catalogue is config.toml; the subscriptions are in the database.** A feed exists once; * **The catalogue is config.toml; the subscriptions are in the database.** A feed exists once;
`subscriptions(user_id, feed_id)` says who wants it and with what settings. OPML children are `subscriptions(user_id, feed_id)` says who wants it and with what settings. OPML children are
derived and never written to config. derived and never written to config.
@@ -140,10 +140,16 @@ Non-trivial logic leaves one runnable check behind. Pure functions (`merge_polic
* `/api/settings` answering `200` does **not** mean the daemon is well — the web server is a * `/api/settings` answering `200` does **not** mean the daemon is well — the web server is a
different task. `ipx status` checks the control socket and the database; to see the worker different task. `ipx status` checks the control socket and the database; to see the worker
getting through its jobs, watch for `scan complete` in the log. getting through its jobs, watch for `scan complete` in the log.
* **Every `ipx` command runs `migrate()` when it opens the database**, the healthcheck's * **The database goes through SeaORM, and the entities in `src/entity.rs` are the schema.**
`ipx status` included. A migration that rewrites a big table (`DROP COLUMN`) takes seconds on `Db::open` creates any missing table or index from them (`create_missing`), on every `ipx`
production, and a command run meanwhile fails with `migrating schema`. It changes nothing; wait command, the healthcheck's `ipx status` included, so it must never write when nothing is
for `daemon started` in the log. Copy `state.db` aside before deploying one. missing: SeaORM's experimental schema sync dropped and remade an index on every open, the
write lock that took made `ipx status` time out behind a busy daemon, and it was removed for
it. A new column on an existing table needs its own `ALTER`; nothing adds one for you.
* **SQL written by hand in `db.rs` has to run on SQLite and Postgres both** (issue #18): `$1`
parameters, bound only if used; `ON CONFLICT`, not `INSERT OR IGNORE`; yes/no columns tested
as themselves (`NOT coalesce(s.read, false)`) and written as `true`/`false`, never compared to
1; no `rowid`, `GLOB` or `UPDATE OR IGNORE`. `Args` in `db.rs` builds the parameters.
## House style ## House style

View File

@@ -10,7 +10,8 @@ it to a running daemon.
|---|---|---| |---|---|---|
| `src/main.rs` | CLI, dispatch, scan loop, download policy | `iPXAgent.py` | | `src/main.rs` | CLI, dispatch, scan loop, download policy | `iPXAgent.py` |
| `src/config.rs` | TOML load/save, `General`/`Feed`/`Web`, intervals, slugs | `iPXSettings.py`, `feeds.plist` | | `src/config.rs` | TOML load/save, `General`/`Feed`/`Web`, intervals, slugs | `iPXSettings.py`, `feeds.plist` |
| `src/db.rs` | SQLite schema, migrations, every query | `.ipxd` plists, `history.dat`, `qmcache.dat` | | `src/db.rs` | Every query, through SeaORM; creates missing tables | `.ipxd` plists, `history.dat`, `qmcache.dat` |
| `src/entity.rs` | The tables, as SeaORM entities: the schema | — |
| `src/feed.rs` | Conditional GET, RSS/Atom/OPML parsing | `FeedData.__getFeed/__getEntries` | | `src/feed.rs` | Conditional GET, RSS/Atom/OPML parsing | `FeedData.__getFeed/__getEntries` |
| `src/download.rs` | Streaming download, naming, type sniffing, placement | `iPXDownloader.getFile` | | `src/download.rs` | Streaming download, naming, type sniffing, placement | `iPXDownloader.getFile` |
| `src/torrent.rs` | librqbit session, seeding limits, stall abort | vendored BitTorrent 4.2.1 | | `src/torrent.rs` | librqbit session, seeding limits, stall abort | vendored BitTorrent 4.2.1 |
@@ -65,14 +66,14 @@ entry_state user_id, feed_id, guid, read, flagged, position
``` ```
Read state is `entry_state` alone. `entries` had `read`, `flagged` and `position` columns from Read state is `entry_state` alone. `entries` had `read`, `flagged` and `position` columns from
before accounts; two bugs came from queries still reading them, and `migrate()` drops them from an before accounts; two bugs came from queries still reading them, and they were dropped in 0.5.
older database.
Schema changes: add the table or column to `SCHEMA`. `CREATE TABLE IF NOT EXISTS` leaves a table Schema changes: the tables are the entities in `src/entity.rs`, and `Db::open` creates whatever
that already exists alone, so a new column on one also goes in `migrate()`'s `wanted` list, and a table or index a database is missing from them (`db::create_missing`), with `IF NOT EXISTS`. It
retired one in its `retired` list; both are checked with `PRAGMA table_info`. Columns from before never alters a table that exists, so a new column on one needs its own `ALTER` in
0.3.0, the oldest version an upgrade may start from, need no entry. `Db::memory()` runs the same `create_missing`, or `sea-orm-migration` once there are several. `Db::memory()` builds its
path as `Db::open`, so a migration cannot pass the tests while missing in production. database the same way, so the tests run on the schema production gets. A database from before
0.7 takes its last columns from the old `migrate()`, so it upgrades through a 0.7 release first.
## Control socket ## Control socket