Skip to main content

ff_backend_postgres/
version.rs

1//! Boot-time schema-version check.
2//!
3//! **RFC-v0.7 Wave 0, Q12.** Mirrors the Valkey boot-time
4//! partition-count check (Q5). Reads the `_sqlx_migrations` ledger
5//! sqlx writes as part of `sqlx::migrate!` application and compares
6//! the latest applied version to `required`:
7//!
8//! * `ledger < required` → refuse to start
9//!   ([`EngineError::Unavailable { op: "schema_version" }`]).
10//! * `ledger == required` → pass.
11//! * `ledger > required` → pass + log-warn iff every intervening
12//!   migration is annotated `backward_compatible=true`. Wave 3
13//!   adds the annotation column to `_sqlx_migrations` (or a
14//!   sibling table); Wave 0 skips the annotation read and
15//!   unconditionally log-warns on a ledger-ahead match so the
16//!   boot-diagnostic channel is exercised. The destructive-migration
17//!   refuse-to-start branch also lands in Wave 3.
18
19use sqlx::PgPool;
20use sqlx::Row;
21use tracing::warn;
22
23use ff_core::engine_error::EngineError;
24
25use crate::error::map_sqlx_error;
26
27/// Compare the Postgres migration ledger against the binary's
28/// required schema version and enforce Q12's boot-time contract.
29///
30/// `required` is the migration version the caller's binary was
31/// built against (typically a `const REQUIRED_SCHEMA_VERSION: u32`
32/// ff-server embeds).
33///
34/// Wave 0 scope: implements the `<` (refuse) + `==` (pass) +
35/// `>` (warn) branches against sqlx's `_sqlx_migrations` table.
36/// Wave 3 extends the `>` branch to read the per-migration
37/// `backward_compatible` annotation and refuse-to-start on a
38/// destructive-ahead ledger.
39pub async fn check_schema_version(pool: &PgPool, required: u32) -> Result<(), EngineError> {
40    // `_sqlx_migrations(version BIGINT, ...)` is sqlx's own ledger.
41    // When the migrator has never run the table doesn't exist —
42    // treat that as "ledger = 0" (below any `required >= 1`).
43    let ledger: i64 = match sqlx::query(
44        "SELECT MAX(version) FROM _sqlx_migrations WHERE success = TRUE",
45    )
46    .fetch_optional(pool)
47    .await
48    {
49        Ok(Some(row)) => row.try_get::<Option<i64>, _>(0).ok().flatten().unwrap_or(0),
50        Ok(None) => 0,
51        // Missing-table (undefined_table): fresh database → ledger = 0.
52        Err(sqlx::Error::Database(ref db)) if db.code().as_deref() == Some("42P01") => 0,
53        Err(other) => return Err(map_sqlx_error(other)),
54    };
55    let required_i = required as i64;
56
57    if ledger < required_i {
58        return Err(EngineError::Unavailable {
59            op: "schema_version",
60        });
61    }
62    if ledger > required_i {
63        // Wave 3 will gate this on a per-migration
64        // `backward_compatible` annotation and refuse-to-start on
65        // a destructive-ahead ledger (Q12 branch 3). For Wave 0
66        // we unconditionally warn so the boot-diagnostic channel
67        // is exercised.
68        warn!(
69            ledger,
70            required = required_i,
71            "postgres schema ledger is ahead of binary — Wave 3 will gate this on the backward_compatible annotation"
72        );
73    }
74    Ok(())
75}