Centralize durable schema migration ownership

This commit is contained in:
ms2k 2026-07-19 01:20:18 +09:00
commit 069f3813ae
5 changed files with 354 additions and 350 deletions

View file

@ -18,4 +18,5 @@ time.workspace = true
tokio.workspace = true
[dev-dependencies]
remora-schema.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] }

View file

@ -1,38 +0,0 @@
-- DurableStore substrate tables owned by remora-durable.
-- Feature packages add their own migrations; they do not alter these tables.
CREATE TABLE mutation_records (
scope_subject TEXT NOT NULL,
scope_operation TEXT NOT NULL,
operation_key TEXT NOT NULL,
kind TEXT NOT NULL,
-- Opaque RemoraApiMutation eligibility / authority binding (attempt, fence,
-- worker incarnation, or other claim material). Compared for exact replay.
eligibility_claim BYTEA NOT NULL,
request_fingerprint BYTEA NOT NULL CHECK (octet_length(request_fingerprint) = 32),
response_status TEXT NOT NULL CHECK (response_status IN ('accepted', 'rejected')),
response_body JSONB NOT NULL,
effect_applied BOOLEAN NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (scope_subject, scope_operation, operation_key),
CHECK (effect_applied = (response_status = 'accepted'))
);
CREATE TABLE outbox_messages (
id BIGSERIAL PRIMARY KEY,
topic TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
claimed_by TEXT,
claimed_until TIMESTAMPTZ,
-- Monotonic per-row claim epoch; with claimed_by forms the ownership token.
claim_generation BIGINT NOT NULL DEFAULT 0,
projected_at TIMESTAMPTZ,
attempt_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT
);
CREATE INDEX outbox_messages_claimable_idx
ON outbox_messages (available_at, id)
WHERE projected_at IS NULL;

View file

@ -1,15 +1,13 @@
//! PostgreSQL DurableStore substrate.
//!
//! Provides the pool, migration runner, transaction helpers, generic mutation
//! ledger refining `RemoraApiMutation`, durable outbox claim/projection, and
//! integrity-protected keyset cursor primitives. Feature packages own their
//! domain schema additions.
//! Provides the pool, transaction helpers, generic mutation ledger refining
//! `RemoraApiMutation`, durable outbox claim/projection, and integrity-protected
//! keyset cursor primitives.
mod cas;
mod cursor;
mod error;
mod ledger;
mod migrate;
mod outbox;
mod pool;
mod tx;
@ -21,7 +19,6 @@ pub use ledger::{
MutationDisposition, MutationOutcome, MutationStatus, execute_mutation, execute_mutation_in_tx,
observe_mutation,
};
pub use migrate::{migrate, migrator};
pub use outbox::{
OutboxClaim, OutboxClaimFilter, OutboxMessage, claim_batch, claim_batch_in_tx, enqueue,
enqueue_after, mark_projected, mark_projected_pool, release_with_error, unprojected_count,

View file

@ -1,14 +0,0 @@
use sqlx::PgPool;
use crate::error::DurableError;
static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");
pub async fn migrate(pool: &PgPool) -> Result<(), DurableError> {
MIGRATOR.run(pool).await?;
Ok(())
}
pub fn migrator() -> &'static sqlx::migrate::Migrator {
&MIGRATOR
}

View file

@ -7,9 +7,10 @@ use remora_durable::{
ADVISORY_CLASS_APP, CursorCodec, DurableError, KeysetPosition, MutationDisposition,
MutationStatus, OutboxClaim, OutboxClaimFilter, PoolConfig, advisory_key, advisory_xact_lock,
cas_set_text, claim_batch, claim_batch_in_tx, connect, enqueue, enqueue_after,
execute_mutation, first_page_cursor, mark_projected, mark_projected_pool, migrate,
next_page_cursor, release_with_error, unprojected_count, with_transaction,
execute_mutation, first_page_cursor, mark_projected, mark_projected_pool, next_page_cursor,
release_with_error, unprojected_count, with_transaction,
};
use remora_schema::migrate as migrate_schema;
use serde_json::json;
use sqlx::PgPool;
use time::{Duration as TimeDuration, OffsetDateTime};
@ -39,7 +40,7 @@ async fn test_pool() -> Option<PgPool> {
)
.await
.expect("connect to postgres");
migrate(&pool).await.expect("run substrate migrations");
migrate_schema(&pool).await.expect("run schema migrations");
Some(pool)
}
@ -85,7 +86,7 @@ async fn migration_round_trip_is_idempotent() {
let Some(pool) = test_pool().await else {
return;
};
migrate(&pool).await.expect("second migrate");
migrate_schema(&pool).await.expect("second migrate");
let (table_count,): (i64,) = sqlx::query_as(
"SELECT COUNT(*)::bigint
FROM information_schema.tables
@ -97,8 +98,6 @@ async fn migration_round_trip_is_idempotent() {
.expect("count substrate tables");
assert_eq!(table_count, 2);
let applied = remora_durable::migrator().iter().count();
assert_eq!(applied, 1);
let (version_count,): (i64,) = sqlx::query_as("SELECT COUNT(*)::bigint FROM _sqlx_migrations")
.fetch_one(&pool)
.await