diff --git a/Cargo.toml b/Cargo.toml index 50814bc..ff06c48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 341 +# version: 342 [workspace] 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"] [workspace.package] -version = "0.3.2-pre.7" +version = "0.3.2-pre.8" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs b/crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs new file mode 100644 index 0000000..2a710a3 --- /dev/null +++ b/crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs @@ -0,0 +1,416 @@ +// file: crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Opt-in real PostgreSQL proof for the Store foundation runtime. +//! +//! The test reads one dedicated PostgreSQL URI from stdin, refuses to start +//! when the KSP migration metadata table already exists, never prints the URI, +//! creates no business table and cleans up only metadata it proved it created. + +const LIVE_BOOTSTRAP_SQL: &str = include_str!("../migrations/V000__bootstrap.sql"); +const LIVE_BROKEN_CHECKSUM_A: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const LIVE_BROKEN_CHECKSUM_B: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; +const LIVE_MAX_URI_BYTES: usize = 4_096; +const LIVE_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 LIVE_METADATA_DROP_SQL: &str = "DROP TABLE IF EXISTS ksp_store_schema_migrations"; +const LIVE_SENTINEL_CHECKSUM_SQL: &str = "SELECT checksum FROM ksp_store_schema_migrations WHERE version = 0"; +const LIVE_SENTINEL_INSERT_SQL: &str = + "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES (0, 'bootstrap', 'pre008_rollback_injected', CURRENT_TIMESTAMP)"; +const LIVE_SENTINEL_UPDATE_SQL: &str = "UPDATE ksp_store_schema_migrations SET checksum = $1 WHERE version = 0"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct LiveFailure { + phase: &'static str, +} + +impl LiveFailure { + const fn new(phase: &'static str) -> Self { + return Self { phase }; + } + + const fn phase(&self) -> &'static str { + return self.phase; + } +} + +#[test] +#[ignore = "opt-in real PostgreSQL foundation proof; reads one dedicated URI from stdin"] +fn pre_008_real_postgres_foundation_is_safe_idempotent_concurrent_and_recoverable() { + eprintln!("KSP Store PostgreSQL live proof: reading one dedicated URI from stdin without echoing it from the test."); + let uri_result = read_uri_from_stdin(); + let uri = match uri_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("PostgreSQL live input rejected at phase {}", error.phase()), + }; + let runtime_result = tokio::runtime::Builder::new_current_thread().enable_all().build(); + let runtime = match runtime_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => panic!("PostgreSQL live runtime could not be constructed"), + }; + let outcome = runtime.block_on(run_live_test(uri.as_str())); + if let std::result::Result::Err(error) = outcome { + panic!("PostgreSQL live foundation proof failed at safe phase {}", error.phase()); + } + return; +} + +fn read_uri_from_stdin() -> std::result::Result { + let mut input = std::string::String::new(); + let read_result = std::io::stdin().read_line(&mut input); + match read_result { + std::result::Result::Ok(0) | std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("stdin_read")), + std::result::Result::Ok(_) => {}, + } + let uri = input.trim().to_owned(); + if uri.is_empty() || uri.len() > LIVE_MAX_URI_BYTES { + return std::result::Result::Err(LiveFailure::new("stdin_uri")); + } + return std::result::Result::Ok(uri); +} + +async fn run_live_test(uri: &str) -> std::result::Result<(), LiveFailure> { + let admin_result = connect_admin(uri).await; + let mut admin = match admin_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let preexisting_result = metadata_exists(&admin).await; + let preexisting = match preexisting_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if preexisting { + return std::result::Result::Err(LiveFailure::new("metadata_preexisting_refusal")); + } + let major_result = postgres_major(&admin).await; + let major = match major_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if major < 15 { + return std::result::Result::Err(LiveFailure::new("postgres_major_unsupported")); + } + eprintln!("KSP Store PostgreSQL live proof: server major {major}"); + let mut owns_metadata = false; + let scenario = run_foundation_scenario(&mut admin, uri, &mut owns_metadata).await; + let cleanup = if owns_metadata { drop_metadata(&admin).await } else { std::result::Result::Ok(()) }; + if let std::result::Result::Err(error) = cleanup { + return std::result::Result::Err(error); + } + if let std::result::Result::Err(error) = scenario { + return std::result::Result::Err(error); + } + let remains_result = metadata_exists(&admin).await; + let remains = match remains_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if remains { + return std::result::Result::Err(LiveFailure::new("cleanup_verification")); + } + return std::result::Result::Ok(()); +} + +async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str, owns_metadata: &mut bool) -> std::result::Result<(), LiveFailure> { + let initial_result = open_backend(uri).await; + let initial = match initial_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let created_result = metadata_exists(admin).await; + let created = match created_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if !created { + return std::result::Result::Err(LiveFailure::new("initial_bootstrap_metadata")); + } + *owns_metadata = true; + let initial_health = initial.health().await; + if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(0) || initial_health.pending_migration_count() != 0 { + return std::result::Result::Err(LiveFailure::new("initial_health")); + } + let initial_close = close_backend(initial).await; + if let std::result::Result::Err(error) = initial_close { + return std::result::Result::Err(error); + } + let idempotent_result = open_backend(uri).await; + let idempotent = match idempotent_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let idempotent_health = idempotent.health().await; + if !idempotent_health.is_ready() + || idempotent_health.migration_version() != std::option::Option::Some(0) + || idempotent_health.pending_migration_count() != 0 + { + return std::result::Result::Err(LiveFailure::new("idempotent_health")); + } + let idempotent_close = close_backend(idempotent).await; + if let std::result::Result::Err(error) = idempotent_close { + return std::result::Result::Err(error); + } + let reset_result = drop_metadata(admin).await; + if let std::result::Result::Err(error) = reset_result { + return std::result::Result::Err(error); + } + let concurrent_result = concurrent_bootstrap(uri).await; + if let std::result::Result::Err(error) = concurrent_result { + return std::result::Result::Err(error); + } + let after_concurrent_result = metadata_exists(admin).await; + match after_concurrent_result { + std::result::Result::Ok(true) => {}, + std::result::Result::Ok(false) => return std::result::Result::Err(LiveFailure::new("concurrent_bootstrap_metadata")), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + let checksum_result = sentinel_checksum(admin).await; + let checksum = match checksum_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let broken_checksum = if checksum == LIVE_BROKEN_CHECKSUM_A { LIVE_BROKEN_CHECKSUM_B } else { LIVE_BROKEN_CHECKSUM_A }; + let corrupt_result = set_sentinel_checksum(admin, broken_checksum).await; + if let std::result::Result::Err(error) = corrupt_result { + return std::result::Result::Err(error); + } + let mismatch_settings = match settings(uri) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mismatch = ksp_store_postgres_lib::PostgresBackend::open(mismatch_settings).await; + match mismatch { + std::result::Result::Err(error) if error.kind() == ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {}, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("checksum_mismatch_classification")), + std::result::Result::Ok(backend) => { + let _ = close_backend(backend).await; + return std::result::Result::Err(LiveFailure::new("checksum_mismatch_accepted")); + }, + } + let restore_result = set_sentinel_checksum(admin, checksum.as_str()).await; + if let std::result::Result::Err(error) = restore_result { + return std::result::Result::Err(error); + } + let recovered_result = open_backend(uri).await; + let recovered = match recovered_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let recovered_health = recovered.health().await; + if !recovered_health.is_ready() { + return std::result::Result::Err(LiveFailure::new("checksum_recovery_health")); + } + let recovered_close = close_backend(recovered).await; + if let std::result::Result::Err(error) = recovered_close { + return std::result::Result::Err(error); + } + let rollback_reset = drop_metadata(admin).await; + if let std::result::Result::Err(error) = rollback_reset { + return std::result::Result::Err(error); + } + let rollback_result = prove_transaction_rollback(admin).await; + if let std::result::Result::Err(error) = rollback_result { + return std::result::Result::Err(error); + } + let absent_after_rollback = metadata_exists(admin).await; + match absent_after_rollback { + std::result::Result::Ok(false) => {}, + std::result::Result::Ok(true) => return std::result::Result::Err(LiveFailure::new("rollback_left_metadata")), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + let final_result = open_backend(uri).await; + let final_backend = match final_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let final_health = final_backend.health().await; + if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(0) || final_health.pending_migration_count() != 0 { + return std::result::Result::Err(LiveFailure::new("final_health")); + } + return close_backend(final_backend).await; +} + +fn settings(uri: &str) -> std::result::Result { + let network_result = ksp_store_api::RawNetworkId::new("devnet"); + let network = match network_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("network")), + }; + return std::result::Result::Ok(ksp_store_postgres_lib::PostgresBackendSettings::new( + network, + uri, + 4, + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, + true, + std::time::Duration::from_secs(30), + std::time::Duration::from_secs(10), + )); +} + +async fn open_backend(uri: &str) -> std::result::Result { + let settings_result = settings(uri); + let value = match settings_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return match ksp_store_postgres_lib::PostgresBackend::open(value).await { + std::result::Result::Ok(backend) => std::result::Result::Ok(backend), + std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("backend_open")), + }; +} + +async fn close_backend(backend: ksp_store_postgres_lib::PostgresBackend) -> std::result::Result<(), LiveFailure> { + return match backend.close(std::time::Duration::from_secs(5)).await { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("backend_close")), + }; +} + +async fn concurrent_bootstrap(uri: &str) -> std::result::Result<(), LiveFailure> { + let first_settings = match settings(uri) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let second_settings = match settings(uri) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let first = tokio::spawn(async move { + return ksp_store_postgres_lib::PostgresBackend::open(first_settings).await; + }); + let second = tokio::spawn(async move { + return ksp_store_postgres_lib::PostgresBackend::open(second_settings).await; + }); + let first_joined = first.await; + let second_joined = second.await; + let first_backend = match first_joined { + std::result::Result::Ok(std::result::Result::Ok(value)) => value, + _ => return std::result::Result::Err(LiveFailure::new("concurrent_first")), + }; + let second_backend = match second_joined { + std::result::Result::Ok(std::result::Result::Ok(value)) => value, + _ => { + let _ = close_backend(first_backend).await; + return std::result::Result::Err(LiveFailure::new("concurrent_second")); + }, + }; + let first_close = close_backend(first_backend).await; + let second_close = close_backend(second_backend).await; + if first_close.is_err() || second_close.is_err() { + return std::result::Result::Err(LiveFailure::new("concurrent_close")); + } + return std::result::Result::Ok(()); +} + +async fn connect_admin(uri: &str) -> std::result::Result { + let parsed = uri.parse::(); + let mut config = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("admin_config")), + }; + config.ssl_mode(tokio_postgres::config::SslMode::Disable); + config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres); + let connected = config.connect(tokio_postgres::NoTls).await; + let (client, connection) = match connected { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("admin_connect")), + }; + let _connection_task = tokio::spawn(async move { + let _result = connection.await; + return; + }); + return std::result::Result::Ok(client); +} + +async fn postgres_major(client: &tokio_postgres::Client) -> std::result::Result { + let row_result = client.query_one("SHOW server_version_num", &[]).await; + let row = match row_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version")), + }; + let value_result = row.try_get::(0); + let value = match value_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version_decode")), + }; + let parsed = value.parse::(); + let version_num = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version_parse")), + }; + return std::result::Result::Ok(version_num / 10_000); +} + +async fn metadata_exists(client: &tokio_postgres::Client) -> std::result::Result { + let row_result = client.query_one(LIVE_METADATA_EXISTS_SQL, &[]).await; + let row = match row_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("metadata_probe")), + }; + return match row.try_get::(0) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("metadata_probe_decode")), + }; +} + +async fn drop_metadata(client: &tokio_postgres::Client) -> std::result::Result<(), LiveFailure> { + return match client.batch_execute(LIVE_METADATA_DROP_SQL).await { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("metadata_cleanup")), + }; +} + +async fn sentinel_checksum(client: &tokio_postgres::Client) -> std::result::Result { + let row_result = client.query_one(LIVE_SENTINEL_CHECKSUM_SQL, &[]).await; + let row = match row_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("sentinel_read")), + }; + return match row.try_get::(0) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("sentinel_decode")), + }; +} + +async fn set_sentinel_checksum(client: &tokio_postgres::Client, checksum: &str) -> std::result::Result<(), LiveFailure> { + let update = client.execute(LIVE_SENTINEL_UPDATE_SQL, &[&checksum]).await; + return match update { + std::result::Result::Ok(1) => std::result::Result::Ok(()), + _ => std::result::Result::Err(LiveFailure::new("sentinel_update")), + }; +} + +async fn prove_transaction_rollback(client: &mut tokio_postgres::Client) -> std::result::Result<(), LiveFailure> { + 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(LiveFailure::new("rollback_begin")), + }; + let create = transaction.batch_execute(LIVE_BOOTSTRAP_SQL).await; + if create.is_err() { + return std::result::Result::Err(LiveFailure::new("rollback_create")); + } + let insert = transaction.batch_execute(LIVE_SENTINEL_INSERT_SQL).await; + if insert.is_err() { + return std::result::Result::Err(LiveFailure::new("rollback_insert")); + } + let injected = transaction.batch_execute("SELECT 1 / 0").await; + if injected.is_ok() { + return std::result::Result::Err(LiveFailure::new("rollback_injection_missing")); + } + drop(transaction); + return std::result::Result::Ok(()); +} diff --git a/deltas/0.3.2/pre.008.md b/deltas/0.3.2/pre.008.md new file mode 100644 index 0000000..533cdfe --- /dev/null +++ b/deltas/0.3.2/pre.008.md @@ -0,0 +1,134 @@ + + + +# Delta `0.3.2-pre.008` — PostgreSQL integration réelle opt-in + +## Base exacte + +Cette tranche s'applique sur `0.3.2-pre.007`, dont le gate opérateur du 2026-08-29 est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL, check `--no-default-features` et graphes Cargo. + +## Objet + +Matérialiser le premier test PostgreSQL réel de la fondation `0.3.2` sans ajouter de comportement de production et sans créer de table métier RAW. + +## Changements + +### Test live isolé + +Nouveau fichier : + +```text +crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs +``` + +Le test est `#[ignore]` et : + +```text +lit une URI PostgreSQL dédiée uniquement depuis stdin +borne l'entrée à 4096 octets +ne lit aucune variable d'environnement +ne loggue/imprime jamais l'URI +refuse PostgreSQL < 15 +n'imprime que le major PostgreSQL +refuse de démarrer si ksp_store_schema_migrations existe déjà +``` + +Le scénario réel couvre : + +```text +connexion PostgreSQL +bootstrap initial +health Ready version 0 / pending 0 +close explicite borné +réouverture idempotente +reset contrôlé de la metadata possédée par le test +deux bootstrap concurrents +checksum sentinel corrompu -> MigrationMismatch +restore checksum -> recovery Ready +rollback physique du V000 exact après erreur SQL injectée +re-bootstrap après rollback +cleanup final de la seule table metadata possédée par le test +``` + +### Rollback sans hook de production + +Aucun hook `cfg(test)`, feature de test ou API publique supplémentaire n'est ajouté à `ksp-store-postgres-lib`. + +Le test inclut le même `migrations/V000__bootstrap.sql`, l'exécute dans une transaction PostgreSQL de test, ajoute un sentinel transitoire, provoque une erreur SQL contrôlée puis droppe la transaction. `tokio-postgres 0.7.18` rollback implicitement une `Transaction` droppée. Le test prouve ensuite l'absence physique de la metadata avant un nouveau bootstrap normal. + +### Non-destructivité + +Le test ne contient aucun : + +```text +DROP DATABASE +DROP SCHEMA +CREATE DATABASE +CREATE SCHEMA +DDL RawTransaction +DDL RawAccountState +SQL CORE/DECODE/SPECIALIZED +``` + +Les `DROP TABLE` éventuels ciblent uniquement `ksp_store_schema_migrations`, après que le test a prouvé que cette relation n'existait pas avant son démarrage et qu'il en possède donc le cleanup. + +## Documentation de suivi + +Le plan et la matrice validation sont mis à jour pour : + +```text +enregistrer le gate opérateur pre.007 vert +figer la stratégie live réelle de pre.008 +remplacer l'idée d'un hook cfg(test) par une injection transactionnelle externe au runtime +marquer les critères live comme matérialisés mais non encore exécutés +ajouter la commande opérateur stdin sans environnement +``` + +## Hors scope confirmé + +Aucun changement de production dans : + +```text +ksp-store-lib +ksp-store-postgres-lib/src +ksp-store-api +Config +migrations committed +TLS/pool/runtime +``` + +Toujours aucun `RawTransaction` ou `RawAccountState` PostgreSQL. + +## Validation locale disponible + +La génération doit exécuter les audits Python et Markdown. Cargo n'est pas disponible dans l'environnement de génération ; le test live doit donc d'abord être compilé par le gate opérateur puis exécuté explicitement sur un PostgreSQL dédié. + +## Gate opérateur déterministe + +```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 +``` + +## Gate PostgreSQL réel opt-in + +Sur une database PostgreSQL dédiée dont `ksp_store_schema_migrations` est absente : + +```bash +read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo +printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1 +unset KSP_PG_TEST_URI +``` + +La variable shell n'est pas exportée ; le test lit uniquement stdin. + +## Suite + +Si les gates déterministes et live sont verts : `0.3.2-pre.009` — hardening, completeness et dependency matrix. diff --git a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md index 9c05f6d..ae03229 100644 --- a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +++ b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.2` — Store/PostgreSQL runtime foundation @@ -863,7 +863,7 @@ Mesures : conversion immédiate vers code KSP + contexte allowlisté ; source er ## 16. PostgreSQL integration test réel -Le test opt-in sera dans `ksp-store-postgres-lib/tests/` et ignoré par défaut. +Le test opt-in est matérialisé par `ksp-store-postgres-lib/tests/postgres_foundation_live.rs` et reste ignoré par défaut. Entrée : @@ -891,13 +891,15 @@ second bootstrap idempotent deux bootstrap concurrents après reset contrôlé corruption test-only du checksum sentinel -> mismatch explicite restore/reset contrôlé -migration failure injectée sous cfg(test) -> rollback/history inchangée +échec SQL contrôlé dans une transaction de test exécutant le V000 exact -> drop implicite de `tokio-postgres::Transaction` -> rollback DDL/history prouvé health ready close explicite borné cleanup metadata ``` -Le gate de référence visera PostgreSQL 18.6. Le test peut accepter un serveur >= 15, mais le rapport opérateur doit enregistrer le major réellement utilisé sans imprimer d'identité sensible. +Le gate de référence visera PostgreSQL 18.6. Le test accepte un serveur >= 15, n'imprime que le major et refuse tout serveur plus ancien. La connexion d'administration du test et le backend live utilisent `Disabled` afin que ce gate porte sur la fondation PostgreSQL/migrations ; la policy TLS `VerifyFull` reste prouvée séparément par les canaris déterministes de `pre.005`. Le test ne lit aucune variable d'environnement. + +La preuve de rollback n'ajoute aucun hook public ou feature de test à la crate de production : le test réutilise par `include_str!` le V000 committed exact, ouvre une transaction PostgreSQL, exécute le DDL et un sentinel transitoire, injecte ensuite une erreur SQL contrôlée puis droppe la transaction. `tokio-postgres 0.7.18` garantit le rollback implicite d'une `Transaction` droppée ; le test vérifie ensuite physiquement l'absence de la table metadata avant de relancer le bootstrap KSP normal. ## 17. Validation de dépendances et API @@ -1018,7 +1020,7 @@ Avec les quatre codes PostgreSQL ajoutés en `pre.005` puis les trois codes migr ### `pre.007` — Composition end-to-end + health -Statut : matérialisé par `0.3.2-pre.007`, gate Cargo opérateur requis. +Statut : matérialisé par `0.3.2-pre.007`, gate Cargo opérateur `PASS` le 2026-08-29. La façade ferme la projection runtime/health sans exposer de type physique : @@ -1036,7 +1038,28 @@ Avec `StoreHealthState`, `StoreRuntimeSnapshot`, `StoreHealthSnapshot` et `ERROR ### `pre.008` — PostgreSQL integration réelle -Ajouter/exécuter le smoke opt-in non destructif : initial/idempotent/concurrent/mismatch/failure/close sur serveur réel. +Statut : test opt-in matérialisé par `0.3.2-pre.008`; compilation/gates déterministes puis exécution PostgreSQL réelle opérateur requises. + +Le scénario `postgres_foundation_live` : + +```text +lit une URI dédiée uniquement depuis stdin +refuse si ksp_store_schema_migrations existe avant le test +refuse PostgreSQL < 15 et n'imprime que le major +bootstrap initial + health Ready + close borné +réouverture idempotente + health Ready +reset contrôlé de la metadata possédée par le test +deux bootstrap concurrents sérialisés par advisory lock +corruption checksum sentinel -> MigrationMismatch +restauration checksum -> recovery Ready +reset contrôlé +transaction test avec V000 exact + sentinel transitoire + erreur injectée +Drop Transaction -> rollback implicite -> metadata absente +re-bootstrap normal + health Ready + close borné +cleanup uniquement de la metadata prouvée créée par le test +``` + +Aucun hook test-only n'est ajouté au runtime de production et aucune table RAW métier n'est créée. ### `pre.009` — Hardening/completeness/dependency matrix @@ -1109,12 +1132,7 @@ Store Desk Aucune question architecturale ne bloque `pre.002`. -Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate : - -```text -DDL précis de ksp_store_schema_migrations en pre.006 -forme finale des snapshots health en pre.007 -``` +Les décisions foundation encore ouvertes après `pre.008` sont uniquement des preuves de hardening/completeness et de clôture ; le DDL metadata, le health/readiness et la stratégie du test PostgreSQL réel sont désormais matérialisés. Toute découverte qui exigerait : diff --git a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md index c145bed..8594597 100644 --- a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +++ b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.2` — Store/PostgreSQL runtime foundation @@ -207,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. -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 gate opérateur 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 / `PASS pre.007 opérateur` pour la composition health. ## 5. Config ownership @@ -331,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`. -Statut : `PASS pre.005-fix.001 opérateur / TODO gate pre.007 / TODO pre.008 live`. +Statut : `PASS pre.005-fix.001 opérateur / PASS pre.007 opérateur / TODO exécution live pre.008`. ## 7. TLS @@ -384,7 +384,7 @@ et aucune table métier RAW/CORE/DECODE/SPECIALIZED. `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 : `PASS pre.006 opérateur / TODO pre.008 live`. +Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. ### V32-MIG-002 — Version/checksum @@ -400,7 +400,7 @@ mismatch historique terminal `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 : `PASS pre.006 opérateur / TODO pre.008 live`. +Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. ### V32-MIG-003 — Concurrence @@ -408,7 +408,7 @@ Critère : deux runners concurrents sont sérialisés par advisory transaction l `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 : `PASS pre.006 opérateur / TODO pre.008 concurrence réelle`. +Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO concurrence live opérateur`. ### V32-MIG-004 — Atomicité/recovery @@ -416,7 +416,7 @@ Critère : échec d'une migration du run courant rollback DDL + history de ce ru `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 : `PASS pre.006 opérateur / TODO pre.008 rollback réel`. +Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO rollback live opérateur`. ### V32-MIG-005 — Newer runtime guard @@ -460,7 +460,7 @@ Critère : façade expose uniquement état/backend/network/counts/migration safe `pre.007` matérialise `StoreRuntimeSnapshot`, `StoreHealthState::{Ready, NotReady}` et `StoreHealthSnapshot`. Les compteurs pool sont bornés en `u32`; la version migration est optionnelle si le probe ne peut pas la lire. -Statut : `TODO gate opérateur pre.007`. +Statut : `PASS pre.007 opérateur`. ### V32-HEALTH-002 — Readiness réelle @@ -468,7 +468,7 @@ Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify `pre.007` conserve l'ouverture stricte acquise en `pre.006` et ajoute un probe borné par le `wait_timeout` du pool : acquisition, `SELECT 1`, lecture de version migration. Le smoke réel reste nécessaire pour prouver ce chemin contre PostgreSQL. -Statut : `TODO gate opérateur pre.007 / TODO pre.008 live`. +Statut : `PASS pre.007 opérateur / TODO exécution live pre.008`. ### V32-HEALTH-003 — Error redaction @@ -476,7 +476,7 @@ Critère : un health failure n'expose pas server error string, query text ou cre Le backend ne conserve que `PostgresBackendErrorKind`; la façade mappe vers un `ErrorCode` KSP optionnel. Le snapshot public ne contient aucune string serveur, URI, SQL, host, user, database ou handle. -Statut : `TODO gate opérateur pre.007 / TODO pre.009 hardening`. +Statut : `PASS pre.007 opérateur / TODO pre.009 hardening`. ## 10. PostgreSQL integration réelle @@ -488,10 +488,12 @@ Critères : #[ignore] URI lue depuis stdin aucun env requis -URI jamais imprimée +URI jamais imprimée par le test ``` -Statut : `TODO pre.008`. +`pre.008` ajoute `tests/postgres_foundation_live.rs`. Le test borne l'entrée à 4096 octets, ne conserve dans ses erreurs que des phases statiques et ne rend jamais l'erreur PostgreSQL brute. L'opérateur peut masquer la saisie côté shell puis piper une seule ligne sur stdin ; aucune variable d'environnement n'est lue par le test. + +Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. ### V32-LIVE-002 — Non destructif @@ -504,7 +506,9 @@ aucune table métier créée cleanup seulement de metadata créée par le test ``` -Statut : `TODO pre.008`. +Le test interroge `information_schema.tables` avant toute mutation. Il ne prend ownership du cleanup qu'après avoir prouvé que le bootstrap initial a créé la metadata alors que la précondition était absente. Les seuls `DROP` exécutés ciblent `ksp_store_schema_migrations` pendant les resets contrôlés et le cleanup final. + +Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. ### V32-LIVE-003 — Scénario foundation @@ -515,21 +519,24 @@ connect bootstrap initial bootstrap idempotent bootstrap concurrent -checksum mismatch -failure rollback +checksum mismatch + restore/recovery +failure rollback du V000 exact health ready close borné +cleanup metadata ``` -Statut : `TODO pre.008/pre.010`. +La preuve rollback évite tout hook public de test : une transaction `tokio-postgres` de test exécute le V000 exact par `include_str!`, insère un sentinel transitoire, provoque ensuite une erreur SQL puis est droppée. Le test vérifie que la metadata n'existe pas après le rollback implicite et que le bootstrap KSP normal peut repartir proprement. + +Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur puis revalidation pre.010`. ### V32-LIVE-004 — PostgreSQL support Critère : test refuse major < 15 et enregistre seulement le major safe réellement testé. -Cible release : PostgreSQL 18.6. +Cible release : PostgreSQL 18.6. `pre.008` interroge uniquement `SHOW server_version_num`, dérive le major et n'imprime aucune identité de serveur. -Statut : `TODO pre.008/pre.010`. +Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur puis revalidation pre.010`. ## 11. Security/adversarial @@ -561,7 +568,7 @@ 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`. `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 / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `TODO gate opérateur pre.006` pour migration / `TODO pre.009` hardening. +Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `PASS pre.006 opérateur` pour migration / `TODO pre.009` hardening. ### V32-SEC-004 — Feature mismatch avant I/O @@ -602,6 +609,16 @@ cargo tree -p ksp-store-postgres-lib --edges normal cargo tree --duplicates ``` +Gate PostgreSQL réel `pre.008` après les gates déterministes : + +```bash +read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo +printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1 +unset KSP_PG_TEST_URI +``` + +`KSP_PG_TEST_URI` est ici une variable shell non exportée servant uniquement à masquer la saisie ; le test lit exclusivement stdin et ne lit aucun environnement. + Gate technique final : ```bash