v0.3.2-pre.006

This commit is contained in:
2026-08-29 19:53:00 +02:00
parent c1dbaad88d
commit c4f56d9e85
18 changed files with 674 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/error.rs
// version: 1
// version: 2
/// Safe backend-local classification used by the Store facade for stable error mapping.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -11,6 +11,12 @@ pub enum PostgresBackendErrorKind {
ConnectFailed,
/// A bounded pool wait, create or recycle operation reached its deadline.
PoolTimeout,
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
MigrationFailed,
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
MigrationMismatch,
/// The database schema history contains a migration newer than this runtime understands.
SchemaNewer,
/// Explicit backend shutdown did not drain inside the supplied deadline.
ShutdownTimeout,
/// Verified TLS configuration or negotiation could not be established.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 3
// version: 4
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -7,9 +7,9 @@
//! Official PostgreSQL backend implementation for KSP Store.
//!
//! `0.3.2-pre.005` owns the physical `tokio-postgres` connection, bounded
//! Deadpool pool and explicit Rustls TLS policy. SQL migrations and business
//! persistence remain absent until their dedicated prereleases.
//! `0.3.2-pre.006` owns the physical `tokio-postgres` connection, bounded
//! Deadpool pool, explicit Rustls TLS policy and private KSP migration/bootstrap
//! engine. Business persistence remains absent from this foundation release.
//!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never
@@ -17,6 +17,7 @@
mod constants;
mod error;
mod migration;
mod runtime;
/// Safe backend-local error returned to the common Store facade.
@@ -32,5 +33,7 @@ pub use self::runtime::PostgresBackendTlsMode;
/// Crate-owned tracing target for PostgreSQL backend behavior.
pub(crate) use self::constants::TRACING_TARGET;
/// Private migration/bootstrap runner consumed by the physical backend runtime.
pub(crate) use self::migration::bootstrap;
const _: &str = crate::TRACING_TARGET;

View File

@@ -0,0 +1,312 @@
// file: crates/ksp-store-postgres-lib/src/migration.rs
// version: 1
use sha2::Digest; // rust-rules: trait-import
const ADVISORY_LOCK_KEY: i64 = 0x4b53_5053_544f_5245;
const BOOTSTRAP_MIGRATION_NAME: &str = "bootstrap";
const BOOTSTRAP_MIGRATION_SQL: &str = include_str!("../migrations/V000__bootstrap.sql");
const BOOTSTRAP_MIGRATION_VERSION: i64 = 0;
const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
const HISTORY_INSERT_SQL: &str = "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)";
const HISTORY_LOAD_SQL: &str = "SELECT version, name, checksum FROM ksp_store_schema_migrations ORDER BY version";
const LOCK_POLL_INTERVAL_MS: u64 = 25;
const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = 'ksp_store_schema_migrations'
AND table_type = 'BASE TABLE'
)"#;
const METADATA_PRIMARY_KEY_SQL: &str = r#"SELECT COUNT(*)::BIGINT,
COUNT(*) FILTER (WHERE kcu.column_name = 'version')::BIGINT
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_catalog = kcu.constraint_catalog
AND tc.constraint_schema = kcu.constraint_schema
AND tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = current_schema()
AND tc.table_name = 'ksp_store_schema_migrations'
AND tc.constraint_type = 'PRIMARY KEY'"#;
const METADATA_SHAPE_SQL: &str = r#"SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'ksp_store_schema_migrations'
ORDER BY ordinal_position"#;
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
struct AppliedMigration {
checksum: std::string::String,
name: std::string::String,
version: i64,
}
/// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client.
pub(crate) async fn bootstrap(
client: &mut deadpool_postgres::Client,
auto_migrate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let bounded = tokio::time::timeout(migration_timeout, bootstrap_inner(client, auto_migrate, migration_timeout, migration_lock_timeout)).await;
return match bounded {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_timeout"))
},
};
}
async fn bootstrap_inner(
client: &mut deadpool_postgres::Client,
auto_migrate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let transaction_result = client.transaction().await;
let transaction = match transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_begin"));
},
};
let lock_result = acquire_advisory_lock(&transaction, migration_lock_timeout).await;
if let std::result::Result::Err(error) = lock_result {
return std::result::Result::Err(error);
}
let timeout_result = set_statement_timeout(&transaction, migration_timeout).await;
if let std::result::Result::Err(error) = timeout_result {
return std::result::Result::Err(error);
}
let exists_result = metadata_exists(&transaction).await;
let metadata_exists = match exists_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let checksum = bootstrap_checksum();
if !metadata_exists {
if !auto_migrate {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_pending"));
}
let create_result = transaction.batch_execute(BOOTSTRAP_MIGRATION_SQL).await;
if create_result.is_err() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_create"));
}
let shape_result = verify_metadata_shape(&transaction).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
}
let insert_result = transaction.execute(HISTORY_INSERT_SQL, &[&BOOTSTRAP_MIGRATION_VERSION, &BOOTSTRAP_MIGRATION_NAME, &checksum]).await;
match insert_result {
std::result::Result::Ok(1) => {},
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_insert"));
},
}
} else {
let shape_result = verify_metadata_shape(&transaction).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
}
let history_result = load_history(&transaction).await;
let history = match history_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let validation_result = validate_history(history.as_slice(), checksum.as_str());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
let commit_result = transaction.commit().await;
return match commit_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_commit"))
},
};
}
async fn acquire_advisory_lock(
transaction: &deadpool_postgres::Transaction<'_>,
timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let started = tokio::time::Instant::now();
let deadline = match started.checked_add(timeout) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_timeout"));
},
};
loop {
let row_result = transaction.query_one("SELECT pg_try_advisory_xact_lock($1)", &[&ADVISORY_LOCK_KEY]).await;
let row = match row_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock"));
},
};
let acquired_result = row.try_get::<usize, bool>(0);
let acquired = match acquired_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_decode"));
},
};
if acquired {
return std::result::Result::Ok(());
}
let now = tokio::time::Instant::now();
if now >= deadline {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_timeout"));
}
let candidate = now + std::time::Duration::from_millis(LOCK_POLL_INTERVAL_MS);
let wake = if candidate < deadline { candidate } else { deadline };
tokio::time::sleep_until(wake).await;
}
}
async fn set_statement_timeout(
transaction: &deadpool_postgres::Transaction<'_>,
timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let timeout_value = format!("{}ms", timeout.as_millis());
let result = transaction.query_one(SET_STATEMENT_TIMEOUT_SQL, &[&timeout_value]).await;
return match result {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "statement_timeout"))
},
};
}
async fn metadata_exists(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
let result = transaction.query_one(METADATA_EXISTS_SQL, &[]).await;
let row = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe"));
},
};
return match row.try_get::<usize, bool>(0) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe_decode"))
},
};
}
async fn verify_metadata_shape(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<(), crate::PostgresBackendError> {
let result = transaction.query(METADATA_SHAPE_SQL, &[]).await;
let rows = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape"));
},
};
let expected = [("version", "bigint", "NO"), ("name", "text", "NO"), ("checksum", "text", "NO"), ("applied_at", "timestamp with time zone", "NO")];
let mut found = [false; 4];
for row in rows {
let column_result = row.try_get::<usize, std::string::String>(0);
let data_type_result = row.try_get::<usize, std::string::String>(1);
let nullable_result = row.try_get::<usize, std::string::String>(2);
let (column, data_type, nullable) = match (column_result, data_type_result, nullable_result) {
(std::result::Result::Ok(column), std::result::Result::Ok(data_type), std::result::Result::Ok(nullable)) => (column, data_type, nullable),
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape_decode"));
},
};
for (index, expected_row) in expected.iter().enumerate() {
if column == expected_row.0 {
if found[index] || data_type != expected_row.1 || nullable != expected_row.2 {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
}
found[index] = true;
break;
}
}
}
for required in found {
if !required {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
}
}
let key_result = transaction.query_one(METADATA_PRIMARY_KEY_SQL, &[]).await;
let key_row = match key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key"));
},
};
let key_count = key_row.try_get::<usize, i64>(0);
let version_count = key_row.try_get::<usize, i64>(1);
return match (key_count, version_count) {
(std::result::Result::Ok(1), std::result::Result::Ok(1)) => std::result::Result::Ok(()),
(std::result::Result::Ok(_), std::result::Result::Ok(_)) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_primary_key"))
},
_ => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key_decode")),
};
}
async fn load_history(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<std::vec::Vec<AppliedMigration>, crate::PostgresBackendError> {
let result = transaction.query(HISTORY_LOAD_SQL, &[]).await;
let rows = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_load"));
},
};
let mut history = std::vec::Vec::with_capacity(rows.len());
for row in rows {
let version_result = row.try_get::<usize, i64>(0);
let name_result = row.try_get::<usize, std::string::String>(1);
let checksum_result = row.try_get::<usize, std::string::String>(2);
match (version_result, name_result, checksum_result) {
(std::result::Result::Ok(version), std::result::Result::Ok(name), std::result::Result::Ok(checksum)) => {
history.push(AppliedMigration { checksum, name, version });
},
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_decode"));
},
}
}
return std::result::Result::Ok(history);
}
fn validate_history(history: &[AppliedMigration], expected_checksum: &str) -> std::result::Result<(), crate::PostgresBackendError> {
let mut sentinel_found = false;
for applied in history {
if applied.version > BOOTSTRAP_MIGRATION_VERSION {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::SchemaNewer, "history_newer"));
}
if applied.version < BOOTSTRAP_MIGRATION_VERSION {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_unknown"));
}
if applied.name != BOOTSTRAP_MIGRATION_NAME || applied.checksum != expected_checksum {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
}
sentinel_found = true;
}
if !sentinel_found {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
}
return std::result::Result::Ok(());
}
fn bootstrap_checksum() -> std::string::String {
let mut hasher = sha2::Sha256::new();
hasher.update(BOOTSTRAP_MIGRATION_SQL.as_bytes());
let digest = hasher.finalize();
let bytes = digest.as_slice();
let mut encoded = std::string::String::with_capacity(bytes.len() * 2);
for byte in bytes {
let value = *byte;
encoded.push(char::from(HEX_LOWER[(value >> 4) as usize]));
encoded.push(char::from(HEX_LOWER[(value & 0x0f) as usize]));
}
return encoded;
}
#[cfg(test)]
#[path = "../unit_tests/migration.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 1
// version: 2
const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
@@ -31,7 +31,10 @@ pub struct PostgresBackendSettings {
connect_timeout: std::time::Duration,
connection_uri: std::string::String,
create_timeout: std::time::Duration,
auto_migrate: bool,
max_connections: u32,
migration_lock_timeout: std::time::Duration,
migration_timeout: std::time::Duration,
network: ksp_store_api::RawNetworkId,
recycle_timeout: std::time::Duration,
tls_mode: PostgresBackendTlsMode,
@@ -50,12 +53,18 @@ impl PostgresBackendSettings {
create_timeout: std::time::Duration,
recycle_timeout: std::time::Duration,
tls_mode: PostgresBackendTlsMode,
auto_migrate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> Self {
return Self {
auto_migrate,
connect_timeout,
connection_uri: connection_uri.into(),
create_timeout,
max_connections,
migration_lock_timeout,
migration_timeout,
network,
recycle_timeout,
tls_mode,
@@ -82,7 +91,10 @@ impl std::fmt::Debug for PostgresBackendSettings {
.debug_struct("PostgresBackendSettings")
.field("network", &self.network)
.field("connection_uri", &"<redacted>")
.field("auto_migrate", &self.auto_migrate)
.field("max_connections", &self.max_connections)
.field("migration_timeout", &self.migration_timeout)
.field("migration_lock_timeout", &self.migration_lock_timeout)
.field("connect_timeout", &self.connect_timeout)
.field("wait_timeout", &self.wait_timeout)
.field("create_timeout", &self.create_timeout)
@@ -129,16 +141,27 @@ impl PostgresBackend {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let probe = pool.get().await;
match probe {
std::result::Result::Ok(client) => drop(client),
let mut client = match probe {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
}
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
network = settings.network().as_str(),
tls_mode = settings.tls_mode().code(),
"PostgreSQL Store backend established initial physical connection"
);
let bootstrap_result = crate::bootstrap(&mut client, settings.auto_migrate, settings.migration_timeout, settings.migration_lock_timeout).await;
if let std::result::Result::Err(error) = bootstrap_result {
return std::result::Result::Err(error);
}
drop(client);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
network = settings.network().as_str(),
auto_migrate = settings.auto_migrate,
"PostgreSQL Store migration/bootstrap foundation verified"
);
return std::result::Result::Ok(Self { network: settings.network, pool });
}