v0.3.2-pre.006
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 339
|
# version: 340
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.2-pre.5.fix.1"
|
version = "0.3.2-pre.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -14,7 +14,7 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
argon2 = { version = "^0.5", default-features = false }
|
argon2 = { version = "^0.6", default-features = false }
|
||||||
base64 = { version = "^0.23" }
|
base64 = { version = "^0.23" }
|
||||||
chacha20poly1305 = { version = "^0.11", default-features = false }
|
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||||
chrono = { version = "^0.4", default-features = false }
|
chrono = { version = "^0.4", default-features = false }
|
||||||
@@ -25,12 +25,13 @@ fs2 = { version = "^0.4" }
|
|||||||
futures-util = { version = "^0.3", default-features = false }
|
futures-util = { version = "^0.3", default-features = false }
|
||||||
getrandom = { version = "^0.4", default-features = false }
|
getrandom = { version = "^0.4", default-features = false }
|
||||||
http = { version = "^1.5", default-features = false }
|
http = { version = "^1.5", default-features = false }
|
||||||
jsonschema = { version = "^0.51", default-features = false }
|
jsonschema = { version = "^0.52", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
rustls = { version = "^0.23", default-features = false }
|
rustls = { version = "^0.23", default-features = false }
|
||||||
rustls-native-certs = { version = "^0.8", default-features = false }
|
rustls-native-certs = { version = "^0.8", default-features = false }
|
||||||
serde = { version = "^1.0" }
|
serde = { version = "^1.0" }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
|
sha2 = { version = "^0.11", default-features = false }
|
||||||
solana-keypair = { version = "^3.1", default-features = false }
|
solana-keypair = { version = "^3.1", default-features = false }
|
||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
solana-pubkey = { version = "^4.3", default-features = false }
|
||||||
tauri = { version = "^2.11" }
|
tauri = { version = "^2.11" }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/error.rs
|
// file: crates/ksp-store-lib/src/error.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Error code reserved for operations attempted after a Store backend has entered its closed state.
|
/// Error code reserved for operations attempted after a Store backend has entered its closed state.
|
||||||
pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed");
|
pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed");
|
||||||
@@ -11,8 +11,14 @@ pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_a
|
|||||||
pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid");
|
pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid");
|
||||||
/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details.
|
/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details.
|
||||||
pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed");
|
pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed");
|
||||||
|
/// Error code used when PostgreSQL migration/bootstrap execution fails without exposing server text or SQL.
|
||||||
|
pub const ERROR_CODE_POSTGRES_MIGRATION_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_failed");
|
||||||
|
/// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||||
|
pub const ERROR_CODE_POSTGRES_MIGRATION_MISMATCH: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_mismatch");
|
||||||
/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline.
|
/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline.
|
||||||
pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout");
|
pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout");
|
||||||
|
/// Error code used when PostgreSQL history contains a migration newer than this Store runtime understands.
|
||||||
|
pub const ERROR_CODE_POSTGRES_SCHEMA_NEWER: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_schema_newer");
|
||||||
/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely.
|
/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely.
|
||||||
pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed");
|
pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed");
|
||||||
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
|
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/lib.rs
|
// file: crates/ksp-store-lib/src/lib.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
//! Common backend-neutral Store runtime facade for KSP.
|
//! Common backend-neutral Store runtime facade for KSP.
|
||||||
//!
|
//!
|
||||||
//! `0.3.2-pre.005` materializes the first physical PostgreSQL runtime path:
|
//! `0.3.2-pre.006` materializes the physical PostgreSQL runtime plus its private
|
||||||
//! one bounded Deadpool pool, one startup connection proof and explicit TLS
|
//! migration/bootstrap foundation: bounded pool, explicit TLS, versioned SHA-256
|
||||||
//! policy. SQL migrations remain private future slices of this release.
|
//! history and no business persistence schema.
|
||||||
//!
|
//!
|
||||||
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
||||||
//! an optional implementation dependency. No backend implementation type is
|
//! an optional implementation dependency. No backend implementation type is
|
||||||
@@ -30,8 +30,14 @@ pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
|
|||||||
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
|
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
|
||||||
/// Error code used when PostgreSQL physical connection establishment fails.
|
/// Error code used when PostgreSQL physical connection establishment fails.
|
||||||
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
|
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
|
||||||
|
/// Error code used when PostgreSQL migration/bootstrap execution fails safely.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED;
|
||||||
|
/// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH;
|
||||||
/// Error code used when a bounded PostgreSQL pool operation reaches its deadline.
|
/// Error code used when a bounded PostgreSQL pool operation reaches its deadline.
|
||||||
pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT;
|
pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT;
|
||||||
|
/// Error code used when PostgreSQL schema history is newer than this Store runtime.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_SCHEMA_NEWER;
|
||||||
/// Error code used when PostgreSQL verified TLS setup or negotiation fails.
|
/// Error code used when PostgreSQL verified TLS setup or negotiation fails.
|
||||||
pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED;
|
pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED;
|
||||||
/// Error code used when Store settings violate backend-neutral bounds or invariants.
|
/// Error code used when Store settings violate backend-neutral bounds or invariants.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/store.rs
|
// file: crates/ksp-store-lib/src/store.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Opaque common Store runtime facade.
|
/// Opaque common Store runtime facade.
|
||||||
///
|
///
|
||||||
@@ -90,6 +90,7 @@ async fn open_postgres(
|
|||||||
shutdown_timeout: std::time::Duration,
|
shutdown_timeout: std::time::Duration,
|
||||||
settings: &crate::PostgresStoreSettings,
|
settings: &crate::PostgresStoreSettings,
|
||||||
) -> ksp_store_api::Result<Store> {
|
) -> ksp_store_api::Result<Store> {
|
||||||
|
let bootstrap = settings.bootstrap();
|
||||||
let pool = settings.pool();
|
let pool = settings.pool();
|
||||||
let tls_mode = match settings.tls_mode() {
|
let tls_mode = match settings.tls_mode() {
|
||||||
crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
|
crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
|
||||||
@@ -104,6 +105,9 @@ async fn open_postgres(
|
|||||||
pool.create_timeout(),
|
pool.create_timeout(),
|
||||||
pool.recycle_timeout(),
|
pool.recycle_timeout(),
|
||||||
tls_mode,
|
tls_mode,
|
||||||
|
bootstrap.auto_migrate(),
|
||||||
|
bootstrap.migration_timeout(),
|
||||||
|
bootstrap.migration_lock_timeout(),
|
||||||
);
|
);
|
||||||
let opened = ksp_store_postgres_lib::PostgresBackend::open(backend_settings).await;
|
let opened = ksp_store_postgres_lib::PostgresBackend::open(backend_settings).await;
|
||||||
return match opened {
|
return match opened {
|
||||||
@@ -136,6 +140,9 @@ fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backe
|
|||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => crate::ERROR_CODE_POSTGRES_SCHEMA_NEWER,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
|
||||||
_ => crate::ERROR_CODE_BACKEND_OPEN_FAILED,
|
_ => crate::ERROR_CODE_BACKEND_OPEN_FAILED,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/tests/public_api.rs
|
// file: crates/ksp-store-lib/tests/public_api.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -37,6 +37,9 @@ fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() {
|
|||||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid");
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed");
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout");
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout");
|
||||||
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_FAILED.code(), "postgres_migration_failed");
|
||||||
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch");
|
||||||
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_SCHEMA_NEWER.code(), "postgres_schema_newer");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed");
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
|
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
|
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-store-postgres-lib/Cargo.toml
|
# file: crates/ksp-store-postgres-lib/Cargo.toml
|
||||||
# version: 2
|
# version: 3
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-store-postgres-lib"
|
name = "ksp-store-postgres-lib"
|
||||||
@@ -13,6 +13,7 @@ ksp-logging-lib = { path = "../ksp-logging-lib" }
|
|||||||
ksp-store-api = { path = "../ksp-store-api" }
|
ksp-store-api = { path = "../ksp-store-api" }
|
||||||
rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] }
|
rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] }
|
||||||
rustls-native-certs.workspace = true
|
rustls-native-certs.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
tokio = { workspace = true, features = ["rt", "time"] }
|
tokio = { workspace = true, features = ["rt", "time"] }
|
||||||
tokio-postgres = { workspace = true, features = ["runtime"] }
|
tokio-postgres = { workspace = true, features = ["runtime"] }
|
||||||
tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] }
|
tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] }
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE ksp_store_schema_migrations (
|
||||||
|
version BIGINT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
applied_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/error.rs
|
// 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.
|
/// Safe backend-local classification used by the Store facade for stable error mapping.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -11,6 +11,12 @@ pub enum PostgresBackendErrorKind {
|
|||||||
ConnectFailed,
|
ConnectFailed,
|
||||||
/// A bounded pool wait, create or recycle operation reached its deadline.
|
/// A bounded pool wait, create or recycle operation reached its deadline.
|
||||||
PoolTimeout,
|
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.
|
/// Explicit backend shutdown did not drain inside the supplied deadline.
|
||||||
ShutdownTimeout,
|
ShutdownTimeout,
|
||||||
/// Verified TLS configuration or negotiation could not be established.
|
/// Verified TLS configuration or negotiation could not be established.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
//! Official PostgreSQL backend implementation for KSP Store.
|
//! Official PostgreSQL backend implementation for KSP Store.
|
||||||
//!
|
//!
|
||||||
//! `0.3.2-pre.005` owns the physical `tokio-postgres` connection, bounded
|
//! `0.3.2-pre.006` owns the physical `tokio-postgres` connection, bounded
|
||||||
//! Deadpool pool and explicit Rustls TLS policy. SQL migrations and business
|
//! Deadpool pool, explicit Rustls TLS policy and private KSP migration/bootstrap
|
||||||
//! persistence remain absent until their dedicated prereleases.
|
//! engine. Business persistence remains absent from this foundation release.
|
||||||
//!
|
//!
|
||||||
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
|
//! 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
|
//! common facade consumes only this crate's narrow backend bridge and never
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod migration;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
|
||||||
/// Safe backend-local error returned to the common Store facade.
|
/// 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.
|
/// Crate-owned tracing target for PostgreSQL backend behavior.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
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;
|
const _: &str = crate::TRACING_TARGET;
|
||||||
|
|||||||
312
crates/ksp-store-postgres-lib/src/migration.rs
Normal file
312
crates/ksp-store-postgres-lib/src/migration.rs
Normal 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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
const APPLICATION_NAME: &str = "ksp-store";
|
const APPLICATION_NAME: &str = "ksp-store";
|
||||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||||
@@ -31,7 +31,10 @@ pub struct PostgresBackendSettings {
|
|||||||
connect_timeout: std::time::Duration,
|
connect_timeout: std::time::Duration,
|
||||||
connection_uri: std::string::String,
|
connection_uri: std::string::String,
|
||||||
create_timeout: std::time::Duration,
|
create_timeout: std::time::Duration,
|
||||||
|
auto_migrate: bool,
|
||||||
max_connections: u32,
|
max_connections: u32,
|
||||||
|
migration_lock_timeout: std::time::Duration,
|
||||||
|
migration_timeout: std::time::Duration,
|
||||||
network: ksp_store_api::RawNetworkId,
|
network: ksp_store_api::RawNetworkId,
|
||||||
recycle_timeout: std::time::Duration,
|
recycle_timeout: std::time::Duration,
|
||||||
tls_mode: PostgresBackendTlsMode,
|
tls_mode: PostgresBackendTlsMode,
|
||||||
@@ -50,12 +53,18 @@ impl PostgresBackendSettings {
|
|||||||
create_timeout: std::time::Duration,
|
create_timeout: std::time::Duration,
|
||||||
recycle_timeout: std::time::Duration,
|
recycle_timeout: std::time::Duration,
|
||||||
tls_mode: PostgresBackendTlsMode,
|
tls_mode: PostgresBackendTlsMode,
|
||||||
|
auto_migrate: bool,
|
||||||
|
migration_timeout: std::time::Duration,
|
||||||
|
migration_lock_timeout: std::time::Duration,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
return Self {
|
return Self {
|
||||||
|
auto_migrate,
|
||||||
connect_timeout,
|
connect_timeout,
|
||||||
connection_uri: connection_uri.into(),
|
connection_uri: connection_uri.into(),
|
||||||
create_timeout,
|
create_timeout,
|
||||||
max_connections,
|
max_connections,
|
||||||
|
migration_lock_timeout,
|
||||||
|
migration_timeout,
|
||||||
network,
|
network,
|
||||||
recycle_timeout,
|
recycle_timeout,
|
||||||
tls_mode,
|
tls_mode,
|
||||||
@@ -82,7 +91,10 @@ impl std::fmt::Debug for PostgresBackendSettings {
|
|||||||
.debug_struct("PostgresBackendSettings")
|
.debug_struct("PostgresBackendSettings")
|
||||||
.field("network", &self.network)
|
.field("network", &self.network)
|
||||||
.field("connection_uri", &"<redacted>")
|
.field("connection_uri", &"<redacted>")
|
||||||
|
.field("auto_migrate", &self.auto_migrate)
|
||||||
.field("max_connections", &self.max_connections)
|
.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("connect_timeout", &self.connect_timeout)
|
||||||
.field("wait_timeout", &self.wait_timeout)
|
.field("wait_timeout", &self.wait_timeout)
|
||||||
.field("create_timeout", &self.create_timeout)
|
.field("create_timeout", &self.create_timeout)
|
||||||
@@ -129,16 +141,27 @@ impl PostgresBackend {
|
|||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let probe = pool.get().await;
|
let probe = pool.get().await;
|
||||||
match probe {
|
let mut client = match probe {
|
||||||
std::result::Result::Ok(client) => drop(client),
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
|
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
|
||||||
}
|
};
|
||||||
ksp_logging_lib::debug!(
|
ksp_logging_lib::debug!(
|
||||||
target: crate::TRACING_TARGET,
|
target: crate::TRACING_TARGET,
|
||||||
network = settings.network().as_str(),
|
network = settings.network().as_str(),
|
||||||
tls_mode = settings.tls_mode().code(),
|
tls_mode = settings.tls_mode().code(),
|
||||||
"PostgreSQL Store backend established initial physical connection"
|
"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 });
|
return std::result::Result::Ok(Self { network: settings.network, pool });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -10,12 +10,22 @@
|
|||||||
#[test]
|
#[test]
|
||||||
fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() {
|
fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() {
|
||||||
let manifest = include_str!("../Cargo.toml");
|
let manifest = include_str!("../Cargo.toml");
|
||||||
for required in ["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "tokio-postgres", "tokio-postgres-rustls"] {
|
for required in
|
||||||
|
["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "sha2", "tokio-postgres", "tokio-postgres-rustls"]
|
||||||
|
{
|
||||||
assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}");
|
assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}");
|
||||||
}
|
}
|
||||||
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] {
|
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] {
|
||||||
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
|
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
|
||||||
}
|
}
|
||||||
|
let migration = include_str!("../src/migration.rs");
|
||||||
|
let bootstrap_sql = include_str!("../migrations/V000__bootstrap.sql");
|
||||||
|
assert!(migration.contains("include_str!(\"../migrations/V000__bootstrap.sql\")"));
|
||||||
|
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
|
||||||
|
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
|
||||||
|
assert!(!migration.contains(forbidden), "business migration implementation leaked into foundation: {forbidden}");
|
||||||
|
assert!(!bootstrap_sql.contains(forbidden), "business schema leaked into foundation SQL: {forbidden}");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +33,7 @@ fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_faca
|
|||||||
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
|
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
|
||||||
let crate_root = include_str!("../src/lib.rs");
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
assert!(crate_root.contains("mod error;"));
|
assert!(crate_root.contains("mod error;"));
|
||||||
|
assert!(crate_root.contains("mod migration;"));
|
||||||
assert!(crate_root.contains("mod runtime;"));
|
assert!(crate_root.contains("mod runtime;"));
|
||||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
@@ -52,7 +63,6 @@ fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private()
|
|||||||
"UPDATE ",
|
"UPDATE ",
|
||||||
"DELETE FROM",
|
"DELETE FROM",
|
||||||
"SELECT ",
|
"SELECT ",
|
||||||
"mod migration",
|
|
||||||
] {
|
] {
|
||||||
assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}");
|
assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -22,6 +22,9 @@ fn pre_005_backend_bridge_is_constructible_without_io() {
|
|||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(5),
|
||||||
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||||
|
true,
|
||||||
|
std::time::Duration::from_secs(30),
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
);
|
);
|
||||||
assert_eq!(settings.network().as_str(), "devnet");
|
assert_eq!(settings.network().as_str(), "devnet");
|
||||||
assert_eq!(settings.tls_mode().code(), "verify_full");
|
assert_eq!(settings.tls_mode().code(), "verify_full");
|
||||||
@@ -36,9 +39,12 @@ fn pre_005_backend_error_projection_is_safe_and_static() {
|
|||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
|
||||||
];
|
];
|
||||||
assert_eq!(kinds.len(), 5);
|
assert_eq!(kinds.len(), 8);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
50
crates/ksp-store-postgres-lib/unit_tests/migration.rs
Normal file
50
crates/ksp-store-postgres-lib/unit_tests/migration.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
|
||||||
|
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bootstrap_migration_is_static_metadata_only_and_checksum_is_stable_sha256() {
|
||||||
|
assert_eq!(super::BOOTSTRAP_MIGRATION_VERSION, 0);
|
||||||
|
assert_eq!(super::BOOTSTRAP_MIGRATION_NAME, "bootstrap");
|
||||||
|
assert!(super::BOOTSTRAP_MIGRATION_SQL.contains("CREATE TABLE ksp_store_schema_migrations"));
|
||||||
|
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
|
||||||
|
assert!(!super::BOOTSTRAP_MIGRATION_SQL.contains(forbidden), "business schema leaked into bootstrap SQL: {forbidden}");
|
||||||
|
}
|
||||||
|
let checksum = super::bootstrap_checksum();
|
||||||
|
assert_eq!(checksum.len(), 64);
|
||||||
|
assert_eq!(checksum, "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matching_sentinel_history_is_accepted() {
|
||||||
|
let checksum = super::bootstrap_checksum();
|
||||||
|
let history = [applied(0, "bootstrap", checksum.as_str())];
|
||||||
|
assert!(super::validate_history(&history, checksum.as_str()).is_ok());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn divergent_or_missing_sentinel_is_terminal_mismatch() {
|
||||||
|
let checksum = super::bootstrap_checksum();
|
||||||
|
let wrong_name = [applied(0, "changed", checksum.as_str())];
|
||||||
|
let wrong_checksum = [applied(0, "bootstrap", "00")];
|
||||||
|
let missing: [super::AppliedMigration; 0] = [];
|
||||||
|
for history in [&wrong_name[..], &wrong_checksum[..], &missing[..]] {
|
||||||
|
let result = super::validate_history(history, checksum.as_str());
|
||||||
|
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newer_history_is_rejected_without_down_migration() {
|
||||||
|
let checksum = super::bootstrap_checksum();
|
||||||
|
let history = [applied(0, "bootstrap", checksum.as_str()), applied(1, "future", "future-checksum")];
|
||||||
|
let result = super::validate_history(&history, checksum.as_str());
|
||||||
|
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs
|
// file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
fn network() -> ksp_store_api::RawNetworkId {
|
fn network() -> ksp_store_api::RawNetworkId {
|
||||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
@@ -18,6 +18,9 @@ fn settings(connection_uri: &str, tls_mode: crate::PostgresBackendTlsMode) -> cr
|
|||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(5),
|
||||||
tls_mode,
|
tls_mode,
|
||||||
|
true,
|
||||||
|
std::time::Duration::from_secs(30),
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
143
deltas/0.3.2/pre.006.md
Normal file
143
deltas/0.3.2/pre.006.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<!-- file: deltas/0.3.2/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.2-pre.006` — migration/bootstrap PostgreSQL foundation
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
Base exacte : `0.3.2-pre.005-fix.001`.
|
||||||
|
|
||||||
|
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Les versions physiques réellement résolues observées sont `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2` et `tokio-postgres-rustls 0.14.0`.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Matérialiser le moteur privé de migration/bootstrap de la fondation PostgreSQL, sans aucune persistence métier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_store_schema_migrations uniquement
|
||||||
|
sentinel version 0
|
||||||
|
nom bootstrap immuable
|
||||||
|
SHA-256 du SQL exact
|
||||||
|
advisory transaction lock borné
|
||||||
|
statement timeout transaction-local
|
||||||
|
transaction unique
|
||||||
|
validation metadata/history
|
||||||
|
newer-runtime guard
|
||||||
|
rollback du run courant
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Dépendance SHA-256
|
||||||
|
|
||||||
|
`sha2 ^0.11` devient une dépendance workspace explicite et une dépendance directe de `ksp-store-postgres-lib`, car le backend calcule lui-même le checksum persistant.
|
||||||
|
|
||||||
|
Le digest SHA-256 est encodé explicitement en 64 caractères hex minuscules ; aucune dépendance de codec hex supplémentaire n'est ajoutée.
|
||||||
|
|
||||||
|
## 4. Migration bootstrap embarquée
|
||||||
|
|
||||||
|
La seule ressource SQL de production ajoutée est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-store-postgres-lib/migrations/V000__bootstrap.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle crée uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_store_schema_migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
avec les champs minimaux :
|
||||||
|
|
||||||
|
```text
|
||||||
|
version BIGINT PRIMARY KEY
|
||||||
|
name TEXT NOT NULL
|
||||||
|
checksum TEXT NOT NULL
|
||||||
|
applied_at TIMESTAMPTZ NOT NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune table/index RAW, CORE, DECODE ou SPECIALIZED n'est introduit.
|
||||||
|
|
||||||
|
## 5. Algorithme runtime
|
||||||
|
|
||||||
|
Après la première acquisition physique déjà introduite en `pre.005`, le backend conserve ce client dédié et exécute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
begin transaction
|
||||||
|
pg_try_advisory_xact_lock($1) en polling borné
|
||||||
|
set_config('statement_timeout', $1, true)
|
||||||
|
probe metadata
|
||||||
|
si absente et auto_migrate=false -> migration_pending sans DDL
|
||||||
|
si absente et auto_migrate=true -> V000 + sentinel dans la même transaction
|
||||||
|
si présente -> vérification forme minimale + PK version
|
||||||
|
load history ordered
|
||||||
|
missing/divergence -> migration mismatch
|
||||||
|
version > 0 -> schema newer
|
||||||
|
commit unique
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ensemble du bootstrap est aussi borné par `migration_timeout`. Une sortie avant `commit()` droppe la transaction ; le rollback PostgreSQL implicite protège DDL et history du run courant.
|
||||||
|
|
||||||
|
Les colonnes obligatoires de metadata sont vérifiées sans interdire des colonnes supplémentaires futures. Cela permet à un ancien runtime de lire une history plus récente et de produire `schema_newer` plutôt qu'un faux mismatch de forme.
|
||||||
|
|
||||||
|
## 6. Erreurs façade
|
||||||
|
|
||||||
|
Le bridge backend ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
MigrationFailed
|
||||||
|
MigrationMismatch
|
||||||
|
SchemaNewer
|
||||||
|
```
|
||||||
|
|
||||||
|
La façade mappe vers :
|
||||||
|
|
||||||
|
```text
|
||||||
|
store.postgres_migration_failed
|
||||||
|
store.postgres_migration_mismatch
|
||||||
|
store.postgres_schema_newer
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun texte serveur, SQL, URI ou credential n'est propagé.
|
||||||
|
|
||||||
|
## 7. Tests/canaris
|
||||||
|
|
||||||
|
Les tests unitaires sans serveur couvrent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
checksum exact/stable du V000
|
||||||
|
absence de schéma métier dans le SQL
|
||||||
|
sentinel valide
|
||||||
|
sentinel missing
|
||||||
|
nom/checksum divergent
|
||||||
|
history plus récente
|
||||||
|
bridge backend étendu
|
||||||
|
frontière dépendances + sha2 direct
|
||||||
|
```
|
||||||
|
|
||||||
|
La concurrence réelle, l'idempotence réelle et le rollback injecté restent au test PostgreSQL opt-in de `pre.008`.
|
||||||
|
|
||||||
|
## 8. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.2-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Gate opérateur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-store-postgres-lib
|
||||||
|
cargo test -p ksp-store-lib
|
||||||
|
cargo test -p ksp-store-lib --no-default-features
|
||||||
|
cargo check -p ksp-store-lib --no-default-features
|
||||||
|
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||||
|
cargo tree -p ksp-store-lib --edges normal
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Suite
|
||||||
|
|
||||||
|
Après gate propre, `pre.007` ferme la composition end-to-end et ajoute la projection health/readiness sûre, sans ouvrir encore les tables RAW métier.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
|
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
|
||||||
|
|
||||||
@@ -942,7 +942,7 @@ Matérialiser `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`,
|
|||||||
|
|
||||||
**Statut : matérialisé par `0.3.2-pre.003`, gate opérateur requis.**
|
**Statut : matérialisé par `0.3.2-pre.003`, gate opérateur requis.**
|
||||||
|
|
||||||
La tranche fixe :
|
La tranche fixe initialement :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
73 exports crate-root de ksp-store-lib : 60 reexports ksp-store-api + 13 éléments runtime Store
|
73 exports crate-root de ksp-store-lib : 60 reexports ksp-store-api + 13 éléments runtime Store
|
||||||
@@ -980,9 +980,34 @@ ksp-store-postgres-lib
|
|||||||
|
|
||||||
Après parsing, la policy KSP réécrit `application_name`, `connect_timeout`, `sslnegotiation` et `sslmode`; les `options=` serveur sont rejetées. `Disabled` force le plaintext, `VerifyFull` force TLS + roots système + identité serveur et interdit un target `hostaddr` sans `host` ainsi que les sockets Unix, sur lesquels PostgreSQL ne négocie pas TLS. Le pool applique `max_size` et deadlines `wait/create/recycle`, utilise le recycling `Verified`, et `Store::open` ne réussit qu'après une première acquisition physique. Les erreurs externes ne sont jamais conservées comme source/texte public. `Store::close` ferme et draine sous deadline; `Drop` reste best-effort. Aucun SQL KSP, migration ou table métier n'est introduit.
|
Après parsing, la policy KSP réécrit `application_name`, `connect_timeout`, `sslnegotiation` et `sslmode`; les `options=` serveur sont rejetées. `Disabled` force le plaintext, `VerifyFull` force TLS + roots système + identité serveur et interdit un target `hostaddr` sans `host` ainsi que les sockets Unix, sur lesquels PostgreSQL ne négocie pas TLS. Le pool applique `max_size` et deadlines `wait/create/recycle`, utilise le recycling `Verified`, et `Store::open` ne réussit qu'après une première acquisition physique. Les erreurs externes ne sont jamais conservées comme source/texte public. `Store::close` ferme et draine sous deadline; `Drop` reste best-effort. Aucun SQL KSP, migration ou table métier n'est introduit.
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.005-fix.001` est vert : audits, workspace check/Clippy, tests backend, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Les versions résolues observées sont `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2` et `tokio-postgres-rustls 0.14.0`.
|
||||||
|
|
||||||
### `pre.006` — Migration/bootstrap foundation
|
### `pre.006` — Migration/bootstrap foundation
|
||||||
|
|
||||||
Créer history metadata, sentinel, SHA-256, advisory lock borné, transaction, version/checksum/newer/missing handling et rollback. Aucun schéma RAW.
|
Statut : matérialisé par `0.3.2-pre.006`, gate Cargo opérateur à exécuter.
|
||||||
|
|
||||||
|
La tranche introduit un moteur privé `ksp-store-postgres-lib` sans crate de migration externe :
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha2 0.11.x explicite
|
||||||
|
V000__bootstrap.sql embarqué par include_str!
|
||||||
|
ksp_store_schema_migrations uniquement
|
||||||
|
version 0 / nom bootstrap / SHA-256 exact du SQL
|
||||||
|
transaction unique
|
||||||
|
pg_try_advisory_xact_lock($1) KSP fixe sous deadline
|
||||||
|
statement_timeout transaction-local via set_config(..., true)
|
||||||
|
forme metadata minimale vérifiée
|
||||||
|
history divergente/missing -> migration mismatch
|
||||||
|
version appliquée > 0 -> schema newer
|
||||||
|
aucun down migration
|
||||||
|
aucun schéma RAW
|
||||||
|
```
|
||||||
|
|
||||||
|
`auto_migrate = false` ne crée jamais la metadata absente : l'ouverture échoue avec un état `migration_pending` sûr. Si la metadata existe, elle est toujours vérifiée. Une table valide mais sans sentinel n'est jamais réparée implicitement : elle est considérée divergente. Les colonnes metadata obligatoires sont vérifiées sans interdire d'éventuelles colonnes supplémentaires futures, afin qu'un ancien runtime puisse encore lire l'historique puis classer correctement une version plus récente comme `schema_newer`.
|
||||||
|
|
||||||
|
Le bootstrap version `0` est spécial : lorsque la metadata est absente et l'auto-migration autorisée, le DDL embarqué est exécuté puis le sentinel est inséré dans la même transaction. Toute erreur ou expiration fait abandonner la transaction ; le rollback PostgreSQL implicite au drop protège DDL + history du run courant. Les preuves concurrentes et rollback injecté sur serveur réel restent à `pre.008`.
|
||||||
|
|
||||||
|
Avec les quatre codes PostgreSQL ajoutés en `pre.005` puis les trois codes migration de `pre.006`, la façade atteint désormais 80 exports crate-root : 60 réexports `ksp-store-api` et 20 éléments runtime Store.
|
||||||
|
|
||||||
### `pre.007` — Composition end-to-end + health
|
### `pre.007` — Composition end-to-end + health
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
|
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ Référence du gate :
|
|||||||
```text
|
```text
|
||||||
PostgreSQL 18.6
|
PostgreSQL 18.6
|
||||||
tokio-postgres 0.7.18
|
tokio-postgres 0.7.18
|
||||||
deadpool-postgres 0.14.1
|
deadpool-postgres 0.14.2
|
||||||
tokio-postgres-rustls 0.14.0
|
tokio-postgres-rustls 0.14.0
|
||||||
sha2 0.11.0
|
sha2 0.11.0
|
||||||
refinery 0.9.2 audité/rejeté
|
refinery 0.9.2 audité/rejeté
|
||||||
@@ -76,6 +76,8 @@ Le gate opérateur de `pre.003-fix.001`, fourni le 29 août 2026, est entièreme
|
|||||||
|
|
||||||
Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commentaires `.env.example`, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests `ksp-config-lib`, ownership, `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et 63 tests Config Desk passent. Cette base est l'entrée effective de `pre.005`.
|
Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commentaires `.env.example`, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests `ksp-config-lib`, ownership, `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et 63 tests Config Desk passent. Cette base est l'entrée effective de `pre.005`.
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.005-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 5 tests runtime backend, canaris de dépendances/API, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.006`.
|
||||||
|
|
||||||
## 3. Frontières Cargo
|
## 3. Frontières Cargo
|
||||||
|
|
||||||
### V32-DEP-001 — Façade -> API
|
### V32-DEP-001 — Façade -> API
|
||||||
@@ -138,7 +140,7 @@ consumer ordinaire -> ksp-store-postgres-lib
|
|||||||
|
|
||||||
Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root.
|
Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005 / TODO pre.009`.
|
Statut : `PASS pre.005-fix.001 opérateur / TODO pre.009`.
|
||||||
|
|
||||||
### V32-DEP-006 — `ksp-store-api` non régressé
|
### V32-DEP-006 — `ksp-store-api` non régressé
|
||||||
|
|
||||||
@@ -205,7 +207,7 @@ Drop best-effort seulement
|
|||||||
|
|
||||||
`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await`. `pre.005` remplace le stop de staging PostgreSQL par l'ouverture physique : un succès exige `pool.get().await` après construction du pool, puis `close(self)` ferme et draine ce pool sous la deadline configurée. Aucun pool/client n'est exposé par la façade.
|
`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await`. `pre.005` remplace le stop de staging PostgreSQL par l'ouverture physique : un succès exige `pool.get().await` après construction du pool, puis `close(self)` ferme et draine ce pool sous la deadline configurée. Aucun pool/client n'est exposé par la façade.
|
||||||
|
|
||||||
Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `TODO gate opérateur pre.005` pour l'ouverture et le shutdown physiques / `TODO pre.007` pour la composition health.
|
Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `PASS pre.005-fix.001 opérateur` pour l'ouverture et le shutdown physiques / `TODO pre.007` pour la composition health.
|
||||||
|
|
||||||
## 5. Config ownership
|
## 5. Config ownership
|
||||||
|
|
||||||
@@ -298,7 +300,7 @@ aucune valeur pathologique
|
|||||||
|
|
||||||
Matérialisé par `pre.005` : `deadpool-postgres` possède un pool `max_size` explicite et applique les deadlines `wait/create/recycle`; `tokio-postgres::Config` reçoit aussi le `connect_timeout` typé après parsing de l'URI.
|
Matérialisé par `pre.005` : `deadpool-postgres` possède un pool `max_size` explicite et applique les deadlines `wait/create/recycle`; `tokio-postgres::Config` reçoit aussi le `connect_timeout` typé après parsing de l'URI.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005`.
|
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||||
|
|
||||||
### V32-POOL-002 — Connection task ownership
|
### V32-POOL-002 — Connection task ownership
|
||||||
|
|
||||||
@@ -306,7 +308,7 @@ Critère : chaque connection future tokio-postgres est pilotée par le manager r
|
|||||||
|
|
||||||
`pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening.
|
`pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005 / TODO pre.009`.
|
Statut : `PASS pre.005-fix.001 opérateur / TODO pre.009`.
|
||||||
|
|
||||||
### V32-POOL-003 — Open failure safe
|
### V32-POOL-003 — Open failure safe
|
||||||
|
|
||||||
@@ -314,7 +316,7 @@ Critère : DNS/connect/auth/server errors ne copient ni URI ni texte remote arbi
|
|||||||
|
|
||||||
Matérialisé par `pre.005` : parsing/connexion/pool/TLS sont ramenés à une classification backend locale sans conserver le texte des erreurs externes ni l'URI. La façade mappe vers des codes `store.postgres_*` stables avec contexte statique sûr.
|
Matérialisé par `pre.005` : parsing/connexion/pool/TLS sont ramenés à une classification backend locale sans conserver le texte des erreurs externes ni l'URI. La façade mappe vers des codes `store.postgres_*` stables avec contexte statique sûr.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005`.
|
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||||
|
|
||||||
### V32-POOL-004 — Close
|
### V32-POOL-004 — Close
|
||||||
|
|
||||||
@@ -329,7 +331,7 @@ aucune tâche volontairement laissée orpheline
|
|||||||
|
|
||||||
`pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`.
|
`pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005 / TODO pre.007/pre.008`.
|
Statut : `PASS pre.005-fix.001 opérateur / TODO pre.007/pre.008`.
|
||||||
|
|
||||||
## 7. TLS
|
## 7. TLS
|
||||||
|
|
||||||
@@ -344,7 +346,7 @@ VerifyFull
|
|||||||
|
|
||||||
Matérialisé par `pre.005` : la surface physique accepte exactement `Disabled` et `VerifyFull`; l'URI parsée est ensuite normalisée vers `SslMode::Disable` ou `SslMode::Require`, donc elle ne peut pas modifier la policy typée.
|
Matérialisé par `pre.005` : la surface physique accepte exactement `Disabled` et `VerifyFull`; l'URI parsée est ensuite normalisée vers `SslMode::Disable` ou `SslMode::Require`, donc elle ne peut pas modifier la policy typée.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005`.
|
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||||
|
|
||||||
### V32-TLS-002 — VerifyFull
|
### V32-TLS-002 — VerifyFull
|
||||||
|
|
||||||
@@ -360,7 +362,7 @@ aucun fallback plaintext
|
|||||||
|
|
||||||
Matérialisé par `pre.005` : `VerifyFull` construit un `rustls::RootCertStore` à partir des roots système, utilise explicitement le provider AWS-LC, requiert un host TCP pour l'identité serveur, rejette `hostaddr` seul et les sockets Unix, et ne permet aucun fallback plaintext. Les erreurs de roots/certificats sont réduites à des compteurs sûrs.
|
Matérialisé par `pre.005` : `VerifyFull` construit un `rustls::RootCertStore` à partir des roots système, utilise explicitement le provider AWS-LC, requiert un host TCP pour l'identité serveur, rejette `hostaddr` seul et les sockets Unix, et ne permet aucun fallback plaintext. Les erreurs de roots/certificats sont réduites à des compteurs sûrs.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.005`.
|
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||||
|
|
||||||
### V32-TLS-003 — Pas de fichier TLS implicite
|
### V32-TLS-003 — Pas de fichier TLS implicite
|
||||||
|
|
||||||
@@ -380,7 +382,9 @@ ksp_store_schema_migrations
|
|||||||
|
|
||||||
et aucune table métier RAW/CORE/DECODE/SPECIALIZED.
|
et aucune table métier RAW/CORE/DECODE/SPECIALIZED.
|
||||||
|
|
||||||
Statut : `TODO pre.006`.
|
`pre.006` embarque exactement `migrations/V000__bootstrap.sql`, dont le seul DDL de production crée `ksp_store_schema_migrations`. Le canari source interdit les identifiants métier RAW/CORE/DECODE/SPECIALIZED dans le moteur et la ressource SQL.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.006 / TODO pre.008 live`.
|
||||||
|
|
||||||
### V32-MIG-002 — Version/checksum
|
### V32-MIG-002 — Version/checksum
|
||||||
|
|
||||||
@@ -394,25 +398,33 @@ sentinel bootstrap version 0
|
|||||||
mismatch historique terminal
|
mismatch historique terminal
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO pre.006`.
|
`pre.006` calcule explicitement SHA-256 sur les octets exacts du SQL embarqué puis encode les 32 octets en 64 caractères hex minuscules. Le sentinel `(0, bootstrap, checksum)` est inséré dans la même transaction que la création metadata. Les tests unitaires figent le checksum du SQL committed et couvrent sentinel valide, missing, nom/checksum divergents et historique plus récent.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.006 / TODO pre.008 live`.
|
||||||
|
|
||||||
### V32-MIG-003 — Concurrence
|
### V32-MIG-003 — Concurrence
|
||||||
|
|
||||||
Critère : deux runners concurrents sont sérialisés par advisory transaction lock avec attente bornée.
|
Critère : deux runners concurrents sont sérialisés par advisory transaction lock avec attente bornée.
|
||||||
|
|
||||||
Statut : `TODO pre.006/pre.008`.
|
`pre.006` utilise une clé KSP fixe et `pg_try_advisory_xact_lock($1)` dans une boucle bornée par `migration_lock_timeout`, avec polling de 25 ms maximum. Aucun lock bloquant illimité n'est utilisé.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.006 / TODO pre.008 concurrence réelle`.
|
||||||
|
|
||||||
### V32-MIG-004 — Atomicité/recovery
|
### V32-MIG-004 — Atomicité/recovery
|
||||||
|
|
||||||
Critère : échec d'une migration du run courant rollback DDL + history de ce run ; un rerun depuis état précédent reste sûr.
|
Critère : échec d'une migration du run courant rollback DDL + history de ce run ; un rerun depuis état précédent reste sûr.
|
||||||
|
|
||||||
Statut : `TODO pre.006/pre.008`.
|
`pre.006` place lock, metadata DDL, sentinel et validation dans une transaction unique ; toute sortie d'erreur avant `commit()` droppe la transaction et PostgreSQL rollback le run courant. Un timeout externe borne également l'ensemble du bootstrap. L'injection d'échec et la preuve physique du rollback restent au smoke réel.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.006 / TODO pre.008 rollback réel`.
|
||||||
|
|
||||||
### V32-MIG-005 — Newer runtime guard
|
### V32-MIG-005 — Newer runtime guard
|
||||||
|
|
||||||
Critère : migration appliquée inconnue/supérieure à la liste embarquée produit `STORE_POSTGRES_SCHEMA_NEWER`, sans down migration.
|
Critère : migration appliquée inconnue/supérieure à la liste embarquée produit `STORE_POSTGRES_SCHEMA_NEWER`, sans down migration.
|
||||||
|
|
||||||
Statut : `TODO pre.006`.
|
`pre.006` connaît uniquement la version `0`; toute history `> 0` est classée `SchemaNewer` par le backend puis `store.postgres_schema_newer` par la façade. Aucun chemin de down migration n'existe.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.006`.
|
||||||
|
|
||||||
### V32-MIG-006 — SQL injection
|
### V32-MIG-006 — SQL injection
|
||||||
|
|
||||||
@@ -424,7 +436,9 @@ values paramétrées
|
|||||||
aucun identifier physique user-configurable en 0.3.2
|
aucun identifier physique user-configurable en 0.3.2
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO pre.006/pre.009`.
|
`pre.006` garde le DDL versionné sous `include_str!` et toutes les values variables des requêtes de contrôle (`lock key`, `statement_timeout`, history values) passent par paramètres. Les noms physiques sont des constantes KSP, jamais des settings.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.006 / TODO pre.009 hardening`.
|
||||||
|
|
||||||
### V32-MIG-007 — No business capability
|
### V32-MIG-007 — No business capability
|
||||||
|
|
||||||
@@ -539,9 +553,9 @@ Statut : `TODO pre.009`.
|
|||||||
|
|
||||||
Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close.
|
Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close.
|
||||||
|
|
||||||
Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. Le parsing PostgreSQL et les timeouts physiques restent à `pre.005`.
|
Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. `pre.005` applique physiquement `connect/wait/create/recycle/shutdown`; `pre.006` applique physiquement `migration_timeout` et `migration_lock_timeout`.
|
||||||
|
|
||||||
Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `TODO pre.005 / TODO pre.009` pour les timeouts physiques.
|
Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `TODO gate opérateur pre.006` pour migration / `TODO pre.009` hardening.
|
||||||
|
|
||||||
### V32-SEC-004 — Feature mismatch avant I/O
|
### V32-SEC-004 — Feature mismatch avant I/O
|
||||||
|
|
||||||
@@ -553,7 +567,7 @@ Statut : `PASS pre.003-fix.001 opérateur / TODO pre.009`.
|
|||||||
|
|
||||||
Un serveur/test double qui renvoie un message contenant un canary ne doit pas le faire traverser l'erreur publique.
|
Un serveur/test double qui renvoie un message contenant un canary ne doit pas le faire traverser l'erreur publique.
|
||||||
|
|
||||||
Statut : `TODO pre.005/pre.009`.
|
Statut : bridge de sanitization matérialisé en `pre.005`; `TODO pre.009` pour le canary serveur hostile.
|
||||||
|
|
||||||
## 12. Gates Rust/workspace
|
## 12. Gates Rust/workspace
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user