Add Postgres backend support

Add Postgres backend support

#24 in Riparion/riparion-cms — merged 2026-05-31

Summary

  • cargo build --no-default-features --features server,postgres now produces a working server end-to-end. SQLite remains the default; existing data/blog.db files are preserved bit-for-bit on upgrade (sqlx-sqlite's chrono decoder accepts the legacy 'YYYY-MM-DD HH:MM:SS' timestamp format unchanged, so no backfill).
  • Migrations split into per-backend dirs (migrations/{sqlite,postgres}/) and tracked by sqlx::migrate!. Riparion uses timestamp-versioned filenames (20260601000000_blog_init.sql) so it shares _sqlx_migrations with arium's 1..9 rows without colliding.
  • New src/db/dialect.rs covers the few SQL fragments that diverge between backends (NOW, RANDOM_HEX_16, now_offset(n)). Placeholders unified on $N (sqlx-sqlite accepts it — verified by sqlx_sqlite_accepts_dollar_placeholders). INSERT OR IGNORE rewritten to INSERT ... ON CONFLICT DO NOTHING, last_insert_rowid() to RETURNING id — both work in both backends.
  • Row-struct timestamp fields converted from String to Option<DateTime<Utc>>. Helpers to_rfc3339 / fmt_date take &DateTime<Utc>.
  • FTS gets a cfg-gated search_sql builder in db/posts.rs (FTS5 MATCH/rank for SQLite, tsvector @@/ts_rank for Postgres).
  • CI: clippy job matrixed on [sqlite, postgres]; new pg-migrate job with a services: postgres:16-alpine block runs pg_migrations_apply (new postgres-only test) against a live DB to catch dialect SQL regressions.
  • Docker: Dockerfile takes a BACKEND={sqlite|postgres} build arg; new docker-compose.postgres.yml overlay adds a postgres:16-alpine sidecar with healthcheck. docker-compose.yml gains DX_RATE_LIMIT env passthrough.

Required upstream: Riparion/arium#22 (users.id/roles.id as BIGSERIAL, User.id widened to i64). Already merged; picked up by the Cargo.lock bump in this PR.

End-to-end verification steps live in TODO_VERIFICATION.md.

Test plan

  • cargo fmt --check
  • cargo clippy --no-default-features --features server,sqlite --all-targets -- -D warnings -A clippy::too_many_arguments
  • cargo clippy --no-default-features --features server,postgres --all-targets -- -D warnings -A clippy::too_many_arguments
  • cargo clippy --no-default-features --features web --target wasm32-unknown-unknown -- -D warnings -A clippy::too_many_arguments -A dead_code
  • cargo test --no-default-features --features server,sqlite — 26 passed
  • cargo test --no-default-features --features server,postgres pg_migrations_apply against postgres:16-alpine — passed
  • SQLite upgrade preservation: existing data/blog.db (25 posts / 33 comments / 5 subscribers, pre-branch timestamps) renders the home page, post detail, comments, and Atom feed without errors. _sqlx_migrations gains exactly one new row (20260601000000 | blog init).
  • SQLite fresh seed: create / edit / publish a post → appears on /; updates updated_at via the {NOW} dialect helper; cascades on delete.
  • Postgres docker stack (docker-compose.yml + docker-compose.postgres.yml): seed planted 3 users, 4 categories, 10 tags, 25 posts, 30 comments, 5 subscribers. Atom feed renders with RFC3339-Z timestamps decoded from TIMESTAMPTZ. Login succeeded after the upstream arium fix landed.
  • PR's own CI run goes green (clippy matrix + sqlite test + pg-migrate + wasm + audit + machete + gitleaks)

🤖 Generated with Claude Code

Last updated 2026-06-01