Skip to content

feat: Postgres pool + first migration + /ready (Phase 1.b.2b)#3

Merged
InstaZDLL merged 4 commits into
mainfrom
feat/1.b.2b-postgres-pool
May 30, 2026
Merged

feat: Postgres pool + first migration + /ready (Phase 1.b.2b)#3
InstaZDLL merged 4 commits into
mainfrom
feat/1.b.2b-postgres-pool

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 30, 2026

Summary

Wire the server's storage layer end-to-end. The pool is online, migrations apply at boot, and the orchestrator can probe both liveness (`/health`) and readiness (`/ready`).

What's wired

Dependency: `waveflow-core` (RFC-001 §6.5)

waveflow-core = { git = \"https://github.com/InstaZDLL/WaveFlow\", rev = \"3ab43f07b925717d6e342046d2b782383e7d1b97\", default-features = false, features = [\"postgres\"] }

Pinned by rev for reproducibility — bump in tree when picking up a new core release. Cargo's git resolver finds the workspace member `waveflow-core` inside the desktop repo's `src-tauri/crates/core/` without needing a workspace manifest at the repo root.

Configuration

Env var Required Default Notes
`DATABASE_URL` yes sqlx connection string. No fallback — fail fast at boot.
`WAVEFLOW_DB_MAX_CONNECTIONS` no 20 Pool cap. Validated > 0.

Pool + migrations (`src/db.rs`)

  • `connect` opens a sqlx `PgPool` with 5 s acquire timeout, 10 min idle timeout.
  • `MIGRATOR` is the compile-time-embedded migrator pointing at `./migrations`.
  • `run_migrations` applies pending migrations; idempotent, immutable-checksum (sqlx aborts boot on hash mismatch).

First migration (`20260529000000_profile.sql`)

Mirrors the SQLite `profile` table shape (BIGSERIAL id, epoch-ms timestamps, indexed `last_used_at DESC`) so `PostgresProfileRepository` round-trips identical-shaped rows. Schema parity with the desktop's `src-tauri/migrations/app/` is what makes the repository trait work cross-backend.

`/ready` (`src/api/ready.rs`)

  • 200 `{status: "ready", db: "ok"}` when `SELECT 1` succeeds.
  • 503 `{status: "not_ready", db: }` when the pool can't reach Postgres.
  • Failures log at warn (not error) so a flapping DB shows up in dashboards without spamming.

CI: Postgres service container

  • ubuntu-latest gets a `postgres:17` service container + `DATABASE_URL=postgres://postgres:postgres@localhost:5432/waveflow_server_ci`. Full pipeline runs: fmt + clippy + check + test.
  • windows-latest stays as a compile guard (no service container, no `cargo test`). Service containers are Linux-only; waveflow-server is a Linux/macOS daemon by design.

Test harness

`#[sqlx::test(migrator = "waveflow_server::db::MIGRATOR")]` on every integration test:

  • creates a per-test database from `DATABASE_URL`
  • runs the embedded migrator
  • drops the database when the test exits

No manual fixture cleanup. `tests/support.rs` spawns the real `app(config, state)` on a kernel-assigned port.

Three integration tests: `/health` smoke + request-id propagation, `/ready` happy path, schema canary that asserts the `profile` table lands after migration.

Out of scope

  • CRUD endpoints (`/api/v1/profiles`, `/libraries`, …) — 1.b.4
  • OpenAPI spec via utoipa — 1.b.3
  • Dev-only `X-User-Id` auth middleware — 1.b.4
  • Real JWT verification — 1.d

Test plan

  • `cargo check --all-targets` ✅
  • `cargo fmt --check` ✅
  • `cargo clippy --all-targets --all-features -- -D warnings` ✅
  • CI (Linux): full `cargo test` against the Postgres 17 service container.
  • CI (Windows): build + clippy.
  • CodeRabbit review.

Signed-off-by: InstaZDLL github.105mh@8shield.net

Summary by CodeRabbit

Notes de version

  • Nouvelles fonctionnalités

    • Intégration Postgres complète avec contrôle du pool de connexions et sonde HTTP /ready
    • Migration initiale ajoutant la table profile
  • Documentation

    • README et .env.example mis à jour (variables d’environnement, exemple Docker, étapes de démarrage, comportement des sondes)
    • Prérequis précisés (Rust 1.94+, Postgres ≥15)
  • Tests

    • Tests d’intégration réorganisés pour utiliser une base Postgres provisionnée et un helper de lancement d’application
  • Chores

    • CI séparé Linux/Windows et toolchain/dépendances explicitement déclarées

Review Change Stack

Wire the server's storage layer end-to-end:

- `waveflow-core` consumed via git dep pinned at the merge SHA of
  `feat: postgres feature + PostgresProfileRepository` on the main
  repo (rev 3ab43f0). `default-features = false` + `features =
  ["postgres"]` pulls the Postgres impl without the sqlite drag.
- `Config` gains `DATABASE_URL` (required, no fallback) and
  `WAVEFLOW_DB_MAX_CONNECTIONS` (default 20). Both fail fast at boot
  on invalid values.
- `db::connect` opens a sqlx `PgPool` with sane defaults (5 s acquire
  timeout, 10 min idle timeout, capped by config). `db::run_migrations`
  applies the compile-time-embedded `MIGRATOR` — the same immutable-
  checksum discipline the desktop uses (`CLAUDE.md`).
- First migration `20260529000000_profile.sql` mirrors the SQLite
  `profile` table shape (BIGSERIAL id, epoch-ms timestamps, indexed
  `last_used_at DESC`) so `PostgresProfileRepository` round-trips
  identical-shaped rows.
- `lib::app` now takes an `AppState { db: PgPool }` threaded into the
  routers via `with_state`. `main.rs` connects + migrates before
  binding the listener — a schema failure aborts startup with a
  non-zero exit so the orchestrator backs off.
- New `GET /ready` handler probes the pool via `SELECT 1`. 200 when
  healthy, 503 when degraded — the unhappy path is a warn-level log,
  not a panic, so a transient Postgres blip self-heals.

CI: ubuntu-latest gets a Postgres 17 service container + DATABASE_URL
env so the full suite (incl. `#[sqlx::test]` integration tests) runs
on every PR. windows-latest stays as a compile guard — service
containers are Linux-only and waveflow-server is a Linux/macOS
daemon by design.

Tests: `health` + `ready` integration suites use `#[sqlx::test(migrator
= "waveflow_server::db::MIGRATOR")]` to get a fresh per-test database
provisioned by sqlx (no manual fixture cleanup). A schema canary
verifies the `profile` table lands so 1.b.4's CRUD work doesn't trip
on a silent migration drop.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

Ce PR intègre Postgres via sqlx : variables d'environnement et Config, module DB avec migrateur embarqué et ping, endpoint /ready, AppState partagé, harness et tests d'intégration, plus adaptations Cargo/CI/README.

Changes

Intégration de base de données Postgres

Layer / File(s) Résumé
Configuration et variables d'environnement
.env.example, src/config.rs
DATABASE_URL (obligatoire) et WAVEFLOW_DB_MAX_CONNECTIONS (optionnel, défaut 20) sont ajoutés et chargés/validés via Config::from_env().
Module base de données et migrations Postgres
src/db.rs, migrations/20260529000000_profile.sql
Ajout de MIGRATOR compilé, connect(config) créant un PgPool configuré (max connections, timeouts), run_migrations(pool) et ping(pool); migration crée la table profile et un index sur last_used_at.
Endpoint readiness probe avec diagnostic DB
src/api/ready.rs, src/api/mod.rs
Nouveau endpoint GET /ready via router(state) qui appelle db::ping(&state.db) et retourne 200 {status:"ready",db:"ok"} ou 503 {status:"not_ready",db:"unavailable"}; api::router accepte AppState et monte ready::router(state).
État applicatif et intégration au démarrage
src/lib.rs, src/main.rs
Introduction de AppState { db: PgPool }, changement de signature app(config, state), main.rs se connecte à la DB, exécute les migrations, construit AppState et l'injecte dans le routeur.
Harness de tests partagé et tests d'intégration refactorisés
tests/support.rs, tests/health.rs, tests/ready.rs
spawn_app(pool) lance l'app sur un port éphémère avec AppState de test. Les tests health sont refactorisés pour #[sqlx::test] et spawn_app; tests/ready.rs vérifie /ready et la création de profile.
Dépendances Cargo, workflow CI et documentation
Cargo.toml, .github/workflows/ci.yml, README.md
Ajout de waveflow-core (git, rev épinglé) et sqlx 0.9; rust-version = "1.94". CI séparé : job linux (service Postgres 17, tests complets) et job windows (clippy + cargo check). README mis à jour (Phase 1.b.2, guide .env/migrations/tests).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#1: Ajout du squelette Axum et /health — cette PR étend le routeur et y ajoute l'état DB partagé et /ready.

Poem

Postgres arrive, cliquetis des migrations,
Le pool veille, l'état prend sa position.
/ready répond, la DB murmure "ok",
Les tests s'élèvent, la CI garde le dock.
Phase 1.b.2 : 🌱 un pas vers la production.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit précisément les trois éléments clés du changement : pool Postgres, première migration, et sonde /ready (Phase 1.b.2b).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1.b.2b-postgres-pool

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL self-assigned this May 30, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 26-27: Le service Postgres utilise le tag flottant "postgres:17";
remplacez-le par l'image épinglée par digest (sha256) pour garantir des builds
reproductibles, par exemple remplacer "postgres:17" par "postgres@sha256:..." et
ajoutez un commentaire à côté "# 17.x" pour indiquer la version majeure suivie
(conserver ainsi la trace de la version tout en fixant le digest).

In `@Cargo.toml`:
- Line 27: The README MSRV and Cargo metadata are out of sync with the sqlx
dependency: sqlx = "0.9" requires Rust 1.94+, so update README.md to state MSRV
1.94+ (replace the 1.84+ mention) and add rust-version = "1.94" under the
[package] section of Cargo.toml to provide a clear compiler error for older
toolchains; also remove or clarify any references to "0.9.0-alpha.1" in the
README if they imply the crate is still alpha since version "0.9" targets the
stable 0.9.0 release.

In `@src/api/ready.rs`:
- Around line 42-55: The readiness handler currently returns the raw DB error
via db: err.to_string() in the Err(err) branch; instead keep the detailed error
only in the tracing::warn! log and return a generic value to the client (e.g.
db: "unavailable" or db: None). Edit the Err(err) arm in the readiness handler
(the Err(err) branch that constructs ReadyResponse) to replace err.to_string()
with a non-sensitive sentinel or None, and if ReadyResponse.db is a String
change it to Option<String> (or adjust its type) so the public /ready response
never exposes internal DB error details while preserving the existing
tracing::warn!(error = %err, ...) logging.

In `@src/db.rs`:
- Around line 3-8: The doc comments in src/db.rs reference a non-existent symbol
`MIGRATIONS_DIR`, causing broken intra-doc links; update the doc comments to
reference the actual exported symbol `MIGRATOR` (i.e., change occurrences of
[`MIGRATIONS_DIR`] to [`MIGRATOR`]) or alternatively spell out the literal path
(`./migrations`) in the text so rustdoc links resolve and warnings go away,
ensuring references match the public `pub static MIGRATOR: Migrator =
sqlx::migrate!("./migrations");`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d3e537d2-b221-4c9d-9569-8f2ebd90e46d

📥 Commits

Reviewing files that changed from the base of the PR and between 5887e9d and 6bd4bc2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock, !*.lock
📒 Files selected for processing (14)
  • .env.example
  • .github/workflows/ci.yml
  • Cargo.toml
  • README.md
  • migrations/20260529000000_profile.sql
  • src/api/mod.rs
  • src/api/ready.rs
  • src/config.rs
  • src/db.rs
  • src/lib.rs
  • src/main.rs
  • tests/health.rs
  • tests/ready.rs
  • tests/support.rs

Comment thread .github/workflows/ci.yml
Comment thread Cargo.toml
Comment thread src/api/ready.rs
Comment thread src/db.rs Outdated
Three valid issues, one skipped with rationale.

1. APPLIED — Declare MSRV explicitly. sqlx 0.9 documents
   `rust-version = "1.94.0"`; add the same to our package metadata so
   an older toolchain fails at dependency resolution with one clear
   line instead of a wall of rustc errors. README bumped from
   "1.84+" to "1.94+ (inherited from sqlx 0.9)".

2. APPLIED — `/ready` no longer leaks sqlx error detail. A sqlx error
   message can echo the connection URL host / user, and the typical
   /ready consumer is an unauthenticated load balancer. Keep the
   detail in `tracing::warn!` only; the body returns a fixed
   "unavailable" sentinel. `ReadyResponse.db` is now `&'static str`
   for both arms — the response shape becomes a tight enum on the
   wire.

3. APPLIED — `db.rs` module doc referenced `[`MIGRATIONS_DIR`]`, a
   symbol that doesn't exist. Rewrote to point at the actual public
   `MIGRATOR` and to spell out `./migrations` so rustdoc resolves
   cleanly.

4. SKIPPED — Pinning `postgres:17` to a sha256 digest for the CI
   service container. For ephemeral test infra (not a production
   deploy), the floating major tag picks up security patches
   automatically and the test surface — `SELECT 1` against a healthy
   pool — doesn't change between Postgres minor versions. Pinning
   would mean manual digest bumps with no security upside, just for
   the test runner.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (1)

33-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Corriger le contrat documenté de /ready en 503.

La doc indique encore db: <error>, alors que ce PR retire l’exposition des détails SQL côté HTTP. Mets à jour la réponse décrite en Line 33 pour refléter la nouvelle valeur stable (&'static str) et éviter une fausse API contract côté clients/tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 33, Met à jour le contrat documenté pour l'endpoint GET
/ready: remplacer la description actuelle qui expose l'erreur SQL par une valeur
constante &'static str; documente la réponse d'échec comme 503 {status:
"not_ready", db: "error"} (et conserve 200 {status: "ready", db: "ok"} pour le
succès) afin d'aligner README.md avec la suppression de l'exposition des détails
SQL.
src/api/ready.rs (1)

9-15: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Doc obsolète : le corps ne contient plus <error>.

La doc annonce 503 {status: "not_ready", db: <error>}, mais le handler renvoie désormais le sentinel fixe "unavailable" (le détail reste dans tracing::warn!). Mets la doc à jour pour refléter le contrat réel et éviter d'induire en erreur les consommateurs de /ready.

📝 Correctif proposé
-//! - `503 {status: "not_ready", db: <error>}` otherwise. A 503 here
+//! - `503 {status: "not_ready", db: "unavailable"}` otherwise. A 503 here
 //!   tells a Kubernetes / systemd-style probe to keep waiting / stop
 //!   routing traffic; it doesn't crash the process so a transient
 //!   Postgres blip self-heals.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/ready.rs` around lines 9 - 15, Update the doc comment in
src/api/ready.rs to reflect the current response contract: change the 503
description from `db: <error>` to the fixed sentinel `db: "unavailable"`
(keeping the 200 case as `db: "ok"`), and note that detailed error information
is emitted to tracing::warn! rather than returned in the body; locate the ready
handler comment at the top of the file and replace the outdated line so
consumers see the actual JSON shape returned by the ready endpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@README.md`:
- Line 33: Met à jour le contrat documenté pour l'endpoint GET /ready: remplacer
la description actuelle qui expose l'erreur SQL par une valeur constante
&'static str; documente la réponse d'échec comme 503 {status: "not_ready", db:
"error"} (et conserve 200 {status: "ready", db: "ok"} pour le succès) afin
d'aligner README.md avec la suppression de l'exposition des détails SQL.

In `@src/api/ready.rs`:
- Around line 9-15: Update the doc comment in src/api/ready.rs to reflect the
current response contract: change the 503 description from `db: <error>` to the
fixed sentinel `db: "unavailable"` (keeping the 200 case as `db: "ok"`), and
note that detailed error information is emitted to tracing::warn! rather than
returned in the body; locate the ready handler comment at the top of the file
and replace the outdated line so consumers see the actual JSON shape returned by
the ready endpoint.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4fdc78e0-cb95-4304-ae80-5f1e92c31d92

📥 Commits

Reviewing files that changed from the base of the PR and between 6bd4bc2 and 8fa989b.

📒 Files selected for processing (4)
  • Cargo.toml
  • README.md
  • src/api/ready.rs
  • src/db.rs

Follow-up on the earlier `/ready` body change (CR #3 round 2). The
handler now returns the fixed `"unavailable"` sentinel for the 503
case, but the README's published contract and the `api/ready.rs`
module doc still described the old `db: <error>` body — risk of
clients keying off a string that never appears.

Both docs now match the wire shape and note that sqlx error detail
stays in `tracing::warn!` only (rationale: unauthenticated probes
like load balancers shouldn't see connection URLs / credentials).

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/ready.rs (1)

65-73: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Déplace cette requête SQL hors du handler API.

probe_db est défini dans src/api/ready.rs avec une requête SQL brute. Pour rester aligné avec les frontières du projet, mets ce probe dans la couche DB/repository (p. ex. src/db.rs ou via un trait waveflow-core::repository) et garde le handler limité à l’orchestration HTTP.

As per coding guidelines « Privilégie les traits de waveflow-core::repository plutôt que des requêtes SQL en dur dans les handlers. »

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/ready.rs` around lines 65 - 73, La fonction probe_db dans
src/api/ready.rs contient une requête SQL brute (sqlx::query_scalar::<_,
i32>("SELECT 1").fetch_one(pool).await) et doit être déplacée vers la couche
DB/repository; extractez cette logic dans src/db.rs (ou implémentez un trait
dans waveflow-core::repository, p.ex. Repository::probe or
HealthRepository::ping) que prend &PgPool et retourne Result<(), sqlx::Error>,
puis appelez cette méthode depuis le handler readiness pour que probe_db (ou son
équivalent) réside dans la couche de persistance tandis que le handler HTTP
reste uniquement orchestrateur.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/api/ready.rs`:
- Around line 65-73: La fonction probe_db dans src/api/ready.rs contient une
requête SQL brute (sqlx::query_scalar::<_, i32>("SELECT
1").fetch_one(pool).await) et doit être déplacée vers la couche DB/repository;
extractez cette logic dans src/db.rs (ou implémentez un trait dans
waveflow-core::repository, p.ex. Repository::probe or HealthRepository::ping)
que prend &PgPool et retourne Result<(), sqlx::Error>, puis appelez cette
méthode depuis le handler readiness pour que probe_db (ou son équivalent) réside
dans la couche de persistance tandis que le handler HTTP reste uniquement
orchestrateur.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8f4dc6e-50b6-495f-8111-6fbd297b0c9d

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa989b and 0e32884.

📒 Files selected for processing (2)
  • README.md
  • src/api/ready.rs

The handler had a raw `SELECT 1` query inline, which violates the
project's no-SQL-in-handlers rule (the same boundary the desktop side
enforces between Tauri commands and waveflow-core). A repository
trait in waveflow-core would be over-engineering for a schema-agnostic
ping; instead expose it as `db::ping(&PgPool)` so future
infrastructure probes (e.g. Redis, plugin host) can land alongside.

`/ready` handler now reads as pure HTTP orchestration: call
`db::ping`, map the Result to (status, body). Wire shape unchanged
(`db: "ok"` / `db: "unavailable"`), tests are still green.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant