diff --git a/Cargo.toml b/Cargo.toml index 202639f..46b1ab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 355 +# version: 356 [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.3-pre.7.fix.1" +version = "0.3.3-pre.8" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-lib/README.md b/crates/ksp-store-lib/README.md index acf56f1..3aa4731 100644 --- a/crates/ksp-store-lib/README.md +++ b/crates/ksp-store-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-store-lib @@ -19,6 +19,8 @@ Elle expose aux consumers une surface backend-neutral, réexporte les contrats R - `Store::health().await` pour la readiness portable et bornée ; - `Store::close(self).await` pour la fermeture explicite bornée ; - le mapping des erreurs backend vers des codes Store stables sans exposer les erreurs physiques ; +- les six capabilities `RawTransaction*` dispatchées vers le backend compilé ; +- une validation réseau backend-neutral avant dispatch pour toutes les opérations qui portent explicitement un réseau ; - les réexports crate-root de `ksp-store-api` nécessaires aux consumers ordinaires. ## Une instance = un réseau @@ -73,17 +75,27 @@ Les credentials restent dans les variables `KSP_SECRET_STORE_*_POSTGRES_URI` ou ## Surface actuelle et hors périmètre -La fondation runtime ne fournit encore aucune implémentation PostgreSQL des capabilities métier RAW de `ksp-store-api`. +Depuis `0.3.3-pre.008`, `Store` implémente les six capabilities transactionnelles acquises dans `ksp-store-api` : + +```text +RawTransactionRead +RawTransactionWrite +RawTransactionObservationRead +RawTransactionObservationWrite +RawTransactionRetentionRead +RawTransactionRetentionWrite +``` + +Le consumer continue à manipuler uniquement les modèles et outcomes backend-neutral. Les erreurs physiques PostgreSQL sont projetées vers des codes Store stables tels que `store.wrong_network`, `store.raw_reference_not_found`, `store.postgres_read_failed`, `store.postgres_write_failed`, `store.postgres_data_invalid` et `store.postgres_page_limit_unsupported`. Sont volontairement hors de cette surface : -- persistence/query/rétention PostgreSQL de `RawTransaction` ; - persistence/query/rétention PostgreSQL de `RawAccountState` ; - batch-size, priorité, backlog ou policy de worker/job ; - transport d'acquisition, Program decoding et materialization ; - exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL. -Les premières vertical slices métier sont ajoutées séparément afin que la façade runtime reste stable et backend-neutral. +La vertical slice `RawTransaction` est désormais dispatchée ; `RawAccountState` reste la tranche suivante de `0.3.4` afin que la façade conserve une progression explicite par capability. ## Documentation diff --git a/crates/ksp-store-lib/USAGE.md b/crates/ksp-store-lib/USAGE.md index 01a44a0..410aa0d 100644 --- a/crates/ksp-store-lib/USAGE.md +++ b/crates/ksp-store-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-store-lib @@ -160,7 +160,8 @@ recycle_timeout() Valeurs par défaut : ```text -auto_migrate true +schema_autocreate true +schema_autoupdate true migration_timeout 30 s migration_lock_timeout 10 s ``` @@ -168,11 +169,14 @@ migration_lock_timeout 10 s Getters : ```text -auto_migrate() +schema_autocreate() +schema_autoupdate() migration_timeout() migration_lock_timeout() ``` +Le constructeur de compatibilité `new(..., auto_migrate, ...)` continue de mapper cette valeur sur les deux politiques, mais les nouveaux callers doivent préférer les deux switches séparés. + ### `StoreSettings` La surface expose : @@ -212,8 +216,37 @@ runtime snapshot Aucun snapshot n'expose URI, host, user, database, SQL, handle backend ou texte d'erreur PostgreSQL. -## 7. Limite fonctionnelle actuelle +## 7. Utiliser les capabilities RawTransaction -`ksp-store-lib` réexporte les modèles et traits RAW de `ksp-store-api`, mais le backend PostgreSQL de la fondation n'implémente encore aucune capability `RawTransaction*` ou `RawAccount*`. +Depuis `0.3.3-pre.008`, `Store` implémente directement les six traits `RawTransaction*`. Le consumer importe le trait correspondant puis appelle la méthode sur la façade : -Les consumers ne doivent donc pas interpréter la disponibilité du runtime PostgreSQL comme une persistence métier déjà présente. +```rust +use ksp_store_lib::RawTransactionRead; + +async fn read_transaction( + store: &ksp_store_lib::Store, + reference: &ksp_store_lib::RawTransactionReference, +) -> ksp_store_lib::Result> { + return store.get_raw_transaction(reference).await; +} +``` + +Le même pattern s'applique à l'écriture, aux observations et à la rétention. Les opérations portant un réseau explicite sont validées contre le réseau du `Store` avant dispatch vers le backend. + +Codes runtime principaux : + +```text +store.wrong_network +store.raw_reference_not_found +store.postgres_read_failed +store.postgres_write_failed +store.postgres_data_invalid +store.postgres_page_limit_unsupported +store.postgres_retention_compaction_unsupported +``` + +Les conflits et queries invalides conservent les codes API acquis `store_api.raw_conflict` et `store_api.raw_query_invalid`. + +## 8. Limite fonctionnelle actuelle + +La façade n'implémente pas encore les capabilities `RawAccount*`. Elles appartiennent à la vertical slice `0.3.4`. diff --git a/crates/ksp-store-lib/src/error.rs b/crates/ksp-store-lib/src/error.rs index 7cf40b1..70db02f 100644 --- a/crates/ksp-store-lib/src/error.rs +++ b/crates/ksp-store-lib/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/error.rs -// version: 5 +// version: 6 /// 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"); @@ -11,14 +11,20 @@ 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"); /// 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"); +/// Error code used when PostgreSQL returns persisted RAW data incompatible with the stable Store contract. +pub const ERROR_CODE_POSTGRES_DATA_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_data_invalid"); /// Error code used when a lightweight PostgreSQL health/readiness probe fails safely. pub const ERROR_CODE_POSTGRES_HEALTH_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_health_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 the requested RAW page size exceeds the exact PostgreSQL LIMIT representation boundary. +pub const ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_page_limit_unsupported"); /// 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"); +/// Error code used when a PostgreSQL RAW read fails without exposing SQL, bind values or server text. +pub const ERROR_CODE_POSTGRES_READ_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_read_failed"); /// Error code used when PostgreSQL cannot represent a requested RAW retention compaction state. pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_retention_compaction_unsupported"); @@ -26,7 +32,13 @@ pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::E 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. pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed"); +/// Error code used when a PostgreSQL RAW write fails without exposing SQL, bind values or server text. +pub const ERROR_CODE_POSTGRES_WRITE_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_write_failed"); +/// Error code used when a RAW write requires a canonical reference that is not durable. +pub const ERROR_CODE_RAW_REFERENCE_NOT_FOUND: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "raw_reference_not_found"); /// Error code used when backend-neutral Store settings violate runtime bounds or invariants. pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid"); /// Error code used when a Store cannot complete its explicit shutdown inside the configured bound. pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout"); +/// Error code used when a network-scoped Store operation targets a network different from the opened Store binding. +pub const ERROR_CODE_WRONG_NETWORK: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "wrong_network"); diff --git a/crates/ksp-store-lib/src/lib.rs b/crates/ksp-store-lib/src/lib.rs index 370f39b..502e0e9 100644 --- a/crates/ksp-store-lib/src/lib.rs +++ b/crates/ksp-store-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/lib.rs -// version: 7 +// version: 8 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -7,10 +7,10 @@ //! Common backend-neutral Store runtime facade for KSP. //! -//! `0.3.2-pre.007` closes the physical PostgreSQL runtime composition with a -//! portable safe runtime snapshot and lightweight health/readiness projection, -//! while retaining the private migration/bootstrap foundation and no business -//! persistence schema. +//! The runtime facade owns backend selection, lifecycle, safe diagnostics and +//! backend-neutral capability dispatch. `0.3.3-pre.008` completes the PostgreSQL +//! `RawTransaction` vertical slice by implementing the six transaction capabilities +//! on both the physical backend and this common facade without exposing physical types. //! //! The default `postgres` feature compiles the official PostgreSQL backend as //! an optional implementation dependency. No backend implementation type is @@ -32,24 +32,36 @@ pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED; pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID; /// Error code used when PostgreSQL physical connection establishment fails. pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED; +/// Error code used when PostgreSQL returns persisted RAW data incompatible with the Store contract. +pub use self::error::ERROR_CODE_POSTGRES_DATA_INVALID; /// Error code used when a lightweight PostgreSQL health/readiness probe fails safely. pub use self::error::ERROR_CODE_POSTGRES_HEALTH_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 requested RAW page size exceeds PostgreSQL's exact physical LIMIT boundary. +pub use self::error::ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED; /// Error code used when a bounded PostgreSQL pool operation reaches its deadline. pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT; +/// Error code used when a PostgreSQL RAW read statement fails safely. +pub use self::error::ERROR_CODE_POSTGRES_READ_FAILED; /// Error code used when PostgreSQL cannot represent a requested RAW retention compaction state. pub use self::error::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED; /// 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. pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED; +/// Error code used when a PostgreSQL RAW write statement or transaction fails safely. +pub use self::error::ERROR_CODE_POSTGRES_WRITE_FAILED; +/// Error code used when a RAW write requires a canonical reference that is not durable. +pub use self::error::ERROR_CODE_RAW_REFERENCE_NOT_FOUND; /// Error code used when Store settings violate backend-neutral bounds or invariants. pub use self::error::ERROR_CODE_SETTINGS_INVALID; /// Error code used when explicit Store shutdown exceeds its configured deadline. pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT; +/// Error code used when a network-scoped operation targets a network different from the Store binding. +pub use self::error::ERROR_CODE_WRONG_NETWORK; /// Portable Store health/readiness projection containing only safe diagnostics. pub use self::health::StoreHealthSnapshot; /// Portable Store health state independent from physical backend types. diff --git a/crates/ksp-store-lib/src/store.rs b/crates/ksp-store-lib/src/store.rs index 5e18a7f..efde194 100644 --- a/crates/ksp-store-lib/src/store.rs +++ b/crates/ksp-store-lib/src/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/store.rs -// version: 5 +// version: 6 /// Opaque common Store runtime facade. /// @@ -116,6 +116,261 @@ impl std::fmt::Debug for Store { } } +impl ksp_store_api::RawTransactionRead for Store { + fn get_raw_transaction<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind); + if let std::result::Result::Err(error) = network_check { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.get_raw_transaction(reference).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = reference; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } + + fn list_raw_transactions<'a>( + &'a self, + query: &'a ksp_store_api::RawTransactionQuery, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind); + if let std::result::Result::Err(error) = network_check { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.list_raw_transactions(query).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = query; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } +} + +impl ksp_store_api::RawTransactionWrite for Store { + fn persist_raw_transaction_acquisition<'a>( + &'a self, + transaction: ksp_store_api::RawTransaction, + observation: ksp_store_api::RawTransactionObservation, + mode: ksp_store_api::RawTransactionAcquisitionMode, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + let transaction_network = validate_operation_network(&self.network, transaction.reference().network(), self.backend_kind); + if let std::result::Result::Err(error) = transaction_network { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + let observation_network = validate_operation_network(&self.network, observation.transaction().network(), self.backend_kind); + if let std::result::Result::Err(error) = observation_network { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.persist_raw_transaction_acquisition(transaction, observation, mode).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = transaction; + let _ = observation; + let _ = mode; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } +} + +impl ksp_store_api::RawTransactionObservationRead for Store { + fn get_raw_transaction_observation<'a>( + &'a self, + observation_key: &'a ksp_store_api::RawObservationKey, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.get_raw_transaction_observation(observation_key).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = observation_key; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } +} + +impl ksp_store_api::RawTransactionObservationWrite for Store { + fn record_raw_transaction_observation<'a>( + &'a self, + observation: ksp_store_api::RawTransactionObservation, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + let network_check = validate_operation_network(&self.network, observation.transaction().network(), self.backend_kind); + if let std::result::Result::Err(error) = network_check { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.record_raw_transaction_observation(observation).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = observation; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } +} + +impl ksp_store_api::RawTransactionRetentionRead for Store { + fn get_raw_transaction_retention_state<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind); + if let std::result::Result::Err(error) = network_check { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.get_raw_transaction_retention_state(reference).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = reference; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } + + fn get_raw_transaction_tombstone<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind); + if let std::result::Result::Err(error) = network_check { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.get_raw_transaction_tombstone(reference).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = reference; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } +} + +impl ksp_store_api::RawTransactionRetentionWrite for Store { + fn transition_raw_transaction_retention<'a>( + &'a self, + transition: ksp_store_api::RawTransactionRetentionTransition, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + let network_check = validate_operation_network(&self.network, transition.reference().network(), self.backend_kind); + if let std::result::Result::Err(error) = network_check { + return std::boxed::Box::pin(async move { + return std::result::Result::Err(error); + }); + } + return std::boxed::Box::pin(async move { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let result = backend.transition_raw_transaction_retention(transition).await; + result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str())) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + let _ = transition; + return std::result::Result::Err(unavailable_runtime_error(self.backend_kind)); + } + }); + } +} + +fn validate_operation_network( + store_network: &ksp_store_api::RawNetworkId, + operation_network: &ksp_store_api::RawNetworkId, + backend_kind: crate::StoreBackendKind, +) -> ksp_store_api::Result<()> { + if store_network != operation_network { + return std::result::Result::Err( + ksp_store_api::Error::new(crate::ERROR_CODE_WRONG_NETWORK, "Store operation targeted a different logical network") + .with_context("backend", backend_kind.code()) + .with_context("network", store_network.as_str()), + ); + } + return std::result::Result::Ok(()); +} + #[cfg(feature = "postgres")] enum StoreRuntime { Postgres(ksp_store_postgres_lib::PostgresBackend), @@ -176,7 +431,7 @@ async fn open_postgres( #[cfg(feature = "postgres")] fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error { let code = postgres_error_code(error.kind()); - return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed") + return ksp_store_api::Error::new(code, "PostgreSQL Store backend operation failed") .with_context("backend", backend_kind.code()) .with_context("network", network) .with_context("phase", error.phase()); @@ -215,13 +470,22 @@ fn postgres_error_code(kind: ksp_store_postgres_lib::PostgresBackendErrorKind) - return match kind { 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::Conflict => ksp_store_api::ERROR_CODE_RAW_CONFLICT, + ksp_store_postgres_lib::PostgresBackendErrorKind::DataInvalid => crate::ERROR_CODE_POSTGRES_DATA_INVALID, ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => crate::ERROR_CODE_POSTGRES_HEALTH_FAILED, 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::PageLimitUnsupported => crate::ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED, + ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid => ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID, + ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed => crate::ERROR_CODE_POSTGRES_READ_FAILED, + ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound => crate::ERROR_CODE_RAW_REFERENCE_NOT_FOUND, + ksp_store_postgres_lib::PostgresBackendErrorKind::RetentionCompactionUnsupported => crate::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED, 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::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED, + ksp_store_postgres_lib::PostgresBackendErrorKind::WriteFailed => crate::ERROR_CODE_POSTGRES_WRITE_FAILED, + ksp_store_postgres_lib::PostgresBackendErrorKind::WrongNetwork => crate::ERROR_CODE_WRONG_NETWORK, _ => crate::ERROR_CODE_BACKEND_OPEN_FAILED, }; } diff --git a/crates/ksp-store-lib/tests/dependency_boundary.rs b/crates/ksp-store-lib/tests/dependency_boundary.rs index 26955c6..d4a417f 100644 --- a/crates/ksp-store-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/dependency_boundary.rs -// version: 6 +// version: 7 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -58,3 +58,36 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() { } return; } + +#[test] +fn pre_008_facade_dispatches_six_raw_transaction_capabilities_without_physical_leak() { + let store = include_str!("../src/store.rs"); + for required in [ + "impl ksp_store_api::RawTransactionRead for Store", + "impl ksp_store_api::RawTransactionWrite for Store", + "impl ksp_store_api::RawTransactionObservationRead for Store", + "impl ksp_store_api::RawTransactionObservationWrite for Store", + "impl ksp_store_api::RawTransactionRetentionRead for Store", + "impl ksp_store_api::RawTransactionRetentionWrite for Store", + "validate_operation_network", + "StoreRuntime::Postgres(backend)", + "map_postgres_error", + ] { + assert!(store.contains(required), "missing pre.008 Store capability dispatch contract: {required}"); + } + for forbidden in [ + "impl ksp_store_api::RawAccountStateRead for Store", + "impl ksp_store_api::RawAccountStateWrite for Store", + "impl ksp_store_api::RawAccountObservationRead for Store", + "impl ksp_store_api::RawAccountObservationWrite for Store", + "tokio_postgres::", + "deadpool_postgres::", + "CREATE TABLE", + "INSERT INTO", + "UPDATE ksp_", + "DELETE FROM", + ] { + assert!(!store.contains(forbidden), "pre.008 facade leaked physical or RawAccount scope: {forbidden}"); + } + return; +} diff --git a/crates/ksp-store-lib/tests/hardening_completeness.rs b/crates/ksp-store-lib/tests/hardening_completeness.rs index 029a219..d6f6bde 100644 --- a/crates/ksp-store-lib/tests/hardening_completeness.rs +++ b/crates/ksp-store-lib/tests/hardening_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/hardening_completeness.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -98,21 +98,27 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() { "ERROR_CODE_BACKEND_OPEN_FAILED", "ERROR_CODE_POSTGRES_CONFIG_INVALID", "ERROR_CODE_POSTGRES_CONNECT_FAILED", + "ERROR_CODE_POSTGRES_DATA_INVALID", "ERROR_CODE_POSTGRES_HEALTH_FAILED", "ERROR_CODE_POSTGRES_MIGRATION_FAILED", "ERROR_CODE_POSTGRES_MIGRATION_MISMATCH", + "ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED", "ERROR_CODE_POSTGRES_POOL_TIMEOUT", + "ERROR_CODE_POSTGRES_READ_FAILED", "ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED", "ERROR_CODE_POSTGRES_SCHEMA_NEWER", "ERROR_CODE_POSTGRES_TLS_FAILED", + "ERROR_CODE_POSTGRES_WRITE_FAILED", "ERROR_CODE_RAW_CONFLICT", "ERROR_CODE_RAW_MODEL_INVALID", "ERROR_CODE_RAW_PAYLOAD_INVALID", "ERROR_CODE_RAW_PROVENANCE_INVALID", "ERROR_CODE_RAW_QUERY_INVALID", + "ERROR_CODE_RAW_REFERENCE_NOT_FOUND", "ERROR_CODE_RAW_RETENTION_INVALID", "ERROR_CODE_SETTINGS_INVALID", "ERROR_CODE_SHUTDOWN_TIMEOUT", + "ERROR_CODE_WRONG_NETWORK", "Error", "ErrorCode", "ErrorContext", @@ -181,7 +187,7 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() { ]; expected.sort_unstable(); assert_eq!(actual.as_slice(), expected.as_slice()); - assert_eq!(actual.len(), 85); + assert_eq!(actual.len(), 91); return; } diff --git a/crates/ksp-store-lib/tests/public_api.rs b/crates/ksp-store-lib/tests/public_api.rs index 838140e..6676f3d 100644 --- a/crates/ksp-store-lib/tests/public_api.rs +++ b/crates/ksp-store-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/public_api.rs -// version: 6 +// version: 7 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -38,15 +38,21 @@ fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() { assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed"); 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_DATA_INVALID.code(), "postgres_data_invalid"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_HEALTH_FAILED.code(), "postgres_health_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout"); + assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_READ_FAILED.code(), "postgres_read_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED.code(), "postgres_retention_compaction_unsupported"); 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_PAGE_LIMIT_UNSUPPORTED.code(), "postgres_page_limit_unsupported"); 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_WRITE_FAILED.code(), "postgres_write_failed"); + assert_eq!(ksp_store_lib::ERROR_CODE_RAW_REFERENCE_NOT_FOUND.code(), "raw_reference_not_found"); 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_WRONG_NETWORK.code(), "wrong_network"); return; } @@ -67,3 +73,21 @@ fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts() let _runtime = std::mem::size_of::>(); return; } + +fn assert_raw_transaction_capabilities() +where + T: ksp_store_lib::RawTransactionRead + + ksp_store_lib::RawTransactionWrite + + ksp_store_lib::RawTransactionObservationRead + + ksp_store_lib::RawTransactionObservationWrite + + ksp_store_lib::RawTransactionRetentionRead + + ksp_store_lib::RawTransactionRetentionWrite, +{ + return; +} + +#[test] +fn pre_008_store_facade_implements_all_six_raw_transaction_capabilities() { + assert_raw_transaction_capabilities::(); + return; +} diff --git a/crates/ksp-store-lib/unit_tests/store.rs b/crates/ksp-store-lib/unit_tests/store.rs index 07c3891..bc2ee93 100644 --- a/crates/ksp-store-lib/unit_tests/store.rs +++ b/crates/ksp-store-lib/unit_tests/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/unit_tests/store.rs -// version: 4 +// version: 5 fn poll_ready(future: impl std::future::Future) -> T { let mut future = std::boxed::Box::pin(future); @@ -55,3 +55,20 @@ fn known_postgres_without_feature_is_rejected_before_io() { assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED)); return; } + +#[test] +fn pre_008_operation_network_guard_rejects_mismatch_without_echoing_requested_network() { + let store_network = valid_network(); + let hostile = match crate::RawNetworkId::new("other-network") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid alternate network rejected: {error:?}"), + }; + let result = super::validate_operation_network(&store_network, &hostile, crate::StoreBackendKind::Postgres); + let error = match result { + std::result::Result::Err(value) => value, + std::result::Result::Ok(()) => panic!("wrong operation network unexpectedly accepted"), + }; + assert_eq!(error.code(), crate::ERROR_CODE_WRONG_NETWORK); + assert!(!std::format!("{error:?}").contains("other-network")); + return; +} diff --git a/crates/ksp-store-postgres-lib/README.md b/crates/ksp-store-postgres-lib/README.md index 8d752ed..84bbaa9 100644 --- a/crates/ksp-store-postgres-lib/README.md +++ b/crates/ksp-store-postgres-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-store-postgres-lib @@ -83,7 +83,7 @@ ksp_store_schema_migrations Le moteur vérifie version, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime. -V001 possède désormais le schéma physique `RawTransaction` et son contrat de compatibilité. Les lectures exactes sont acquises depuis `pre.004`; `pre.005` ajoute les écritures atomiques transaction + observation, l'idempotence réelle et la classification de conflit. `pre.006` ajoute la navigation keyset déterministe sur l'index `(slot, signature)` et son cursor opaque lié à la requête. `pre.007` ajoute les transitions de rétention atomiques `Full -> Archived -> Purged` et le rejet explicite de `Compacted` tant qu'aucune représentation compacte réelle n'existe. +V001 possède désormais le schéma physique `RawTransaction` et son contrat de compatibilité. Les lectures exactes sont acquises depuis `pre.004`; `pre.005` ajoute les écritures atomiques transaction + observation, l'idempotence réelle et la classification de conflit. `pre.006` ajoute la navigation keyset déterministe sur l'index `(slot, signature)` et son cursor opaque lié à la requête. `pre.007` ajoute les transitions de rétention atomiques `Full -> Archived -> Purged` et le rejet explicite de `Compacted`. `pre.008` ferme la conformance backend en implémentant les six traits `RawTransaction*` directement sur `PostgresBackend`. ## Health et erreurs @@ -165,7 +165,7 @@ Toute transition dont `expected` ou `target` vaut `Compacted` est rejetée avant La crate ne contient encore : -- aucune implémentation complète des six traits `RawTransaction*` de `ksp-store-api` ; +- aucune capability `RawAccount*` ; - aucun dispatch métier dans `ksp-store-lib` ; - aucune implémentation PostgreSQL des capabilities `RawAccount*` ; - aucune orchestration worker/job ; diff --git a/crates/ksp-store-postgres-lib/USAGE.md b/crates/ksp-store-postgres-lib/USAGE.md index 0486a51..73350e8 100644 --- a/crates/ksp-store-postgres-lib/USAGE.md +++ b/crates/ksp-store-postgres-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-store-postgres-lib @@ -219,14 +219,21 @@ Les transitions physiques supportées sont `Full -> Archived` puis `Archived -> Une transition impliquant `Compacted` est rejetée avant acquisition du pool avec `PostgresBackendErrorKind::RetentionCompactionUnsupported`. Le code KSP correspondant est `ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED`; aucune compression transparente PostgreSQL n'est présentée comme une représentation compactée KSP. -## 11. Ce que cette crate ne permet pas encore +## 11. Capabilities traitées directement -La tranche ne fournit pas encore : +Depuis `0.3.3-pre.008`, `PostgresBackend` implémente directement : ```text -implémentations complètes des six traits RawTransaction* -dispatch métier ksp-store-lib -capabilities RawAccount* +RawTransactionRead +RawTransactionWrite +RawTransactionObservationRead +RawTransactionObservationWrite +RawTransactionRetentionRead +RawTransactionRetentionWrite ``` -Ces surfaces sont ajoutées dans les prereleases suivantes avant le dispatch `ksp-store-lib`. +Cette conformance est principalement utile aux tests backend et à la façade. Le consumer applicatif normal continue à dépendre de `ksp-store-lib`, qui dispatch les mêmes six capabilities sans exposer `PostgresBackend`. + +## 12. Ce que cette crate ne permet pas encore + +La tranche ne fournit pas les capabilities `RawAccount*`. Elles appartiennent à `0.3.4`. diff --git a/crates/ksp-store-postgres-lib/src/lib.rs b/crates/ksp-store-postgres-lib/src/lib.rs index 82ace4f..6712f2a 100644 --- a/crates/ksp-store-postgres-lib/src/lib.rs +++ b/crates/ksp-store-postgres-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/src/lib.rs -// version: 13 +// version: 14 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -19,6 +19,8 @@ //! pagination with a fixed opaque cursor bound to network, range and direction. //! `0.3.3-pre.007` adds atomic `Full -> Archived -> Purged` retention transitions //! with compare-and-transition outcomes and explicit rejection of `Compacted`. +//! `0.3.3-pre.008` implements all six `RawTransaction*` capabilities directly on +//! `PostgresBackend` while preserving the existing narrow backend bridge. //! //! 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 diff --git a/crates/ksp-store-postgres-lib/src/runtime.rs b/crates/ksp-store-postgres-lib/src/runtime.rs index 4e8b7ad..0d20c7b 100644 --- a/crates/ksp-store-postgres-lib/src/runtime.rs +++ b/crates/ksp-store-postgres-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/src/runtime.rs -// version: 9 +// version: 10 const APPLICATION_NAME: &str = "ksp-store"; const MAX_CONNECTION_URI_BYTES: usize = 4_096; @@ -428,6 +428,126 @@ impl std::fmt::Debug for PostgresBackend { } } +impl ksp_store_api::RawTransactionRead for PostgresBackend { + fn get_raw_transaction<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::get_raw_transaction(self, reference).await; + return result.map_err(map_capability_error); + }); + } + + fn list_raw_transactions<'a>( + &'a self, + query: &'a ksp_store_api::RawTransactionQuery, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::list_raw_transactions(self, query).await; + return result.map_err(map_capability_error); + }); + } +} + +impl ksp_store_api::RawTransactionWrite for PostgresBackend { + fn persist_raw_transaction_acquisition<'a>( + &'a self, + transaction: ksp_store_api::RawTransaction, + observation: ksp_store_api::RawTransactionObservation, + mode: ksp_store_api::RawTransactionAcquisitionMode, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::persist_raw_transaction_acquisition(self, transaction, observation, mode).await; + return result.map_err(map_capability_error); + }); + } +} + +impl ksp_store_api::RawTransactionObservationRead for PostgresBackend { + fn get_raw_transaction_observation<'a>( + &'a self, + observation_key: &'a ksp_store_api::RawObservationKey, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::get_raw_transaction_observation(self, observation_key).await; + return result.map_err(map_capability_error); + }); + } +} + +impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend { + fn record_raw_transaction_observation<'a>( + &'a self, + observation: ksp_store_api::RawTransactionObservation, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::record_raw_transaction_observation(self, observation).await; + return result.map_err(map_capability_error); + }); + } +} + +impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend { + fn get_raw_transaction_retention_state<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::get_raw_transaction_retention_state(self, reference).await; + return result.map_err(map_capability_error); + }); + } + + fn get_raw_transaction_tombstone<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::get_raw_transaction_tombstone(self, reference).await; + return result.map_err(map_capability_error); + }); + } +} + +impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend { + fn transition_raw_transaction_retention<'a>( + &'a self, + transition: ksp_store_api::RawTransactionRetentionTransition, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + return std::boxed::Box::pin(async move { + let result = PostgresBackend::transition_raw_transaction_retention(self, transition).await; + return result.map_err(map_capability_error); + }); + } +} + +fn map_capability_error(error: crate::PostgresBackendError) -> ksp_store_api::Error { + let code = match error.kind() { + crate::PostgresBackendErrorKind::ConfigInvalid => ksp_store_api::ErrorCode::new("store", "postgres_config_invalid"), + crate::PostgresBackendErrorKind::ConnectFailed => ksp_store_api::ErrorCode::new("store", "postgres_connect_failed"), + crate::PostgresBackendErrorKind::Conflict => ksp_store_api::ERROR_CODE_RAW_CONFLICT, + crate::PostgresBackendErrorKind::DataInvalid => ksp_store_api::ErrorCode::new("store", "postgres_data_invalid"), + crate::PostgresBackendErrorKind::HealthFailed => ksp_store_api::ErrorCode::new("store", "postgres_health_failed"), + crate::PostgresBackendErrorKind::MigrationFailed => ksp_store_api::ErrorCode::new("store", "postgres_migration_failed"), + crate::PostgresBackendErrorKind::MigrationMismatch => ksp_store_api::ErrorCode::new("store", "postgres_migration_mismatch"), + crate::PostgresBackendErrorKind::PageLimitUnsupported => ksp_store_api::ErrorCode::new("store", "postgres_page_limit_unsupported"), + crate::PostgresBackendErrorKind::PoolTimeout => ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout"), + crate::PostgresBackendErrorKind::QueryInvalid => ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID, + crate::PostgresBackendErrorKind::ReadFailed => ksp_store_api::ErrorCode::new("store", "postgres_read_failed"), + crate::PostgresBackendErrorKind::ReferenceNotFound => ksp_store_api::ErrorCode::new("store", "raw_reference_not_found"), + crate::PostgresBackendErrorKind::RetentionCompactionUnsupported => crate::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED, + crate::PostgresBackendErrorKind::SchemaNewer => ksp_store_api::ErrorCode::new("store", "postgres_schema_newer"), + crate::PostgresBackendErrorKind::ShutdownTimeout => ksp_store_api::ErrorCode::new("store", "shutdown_timeout"), + crate::PostgresBackendErrorKind::TlsFailed => ksp_store_api::ErrorCode::new("store", "postgres_tls_failed"), + crate::PostgresBackendErrorKind::WriteFailed => ksp_store_api::ErrorCode::new("store", "postgres_write_failed"), + crate::PostgresBackendErrorKind::WrongNetwork => ksp_store_api::ErrorCode::new("store", "wrong_network"), + }; + return ksp_store_api::Error::new(code, "PostgreSQL Store capability operation failed") + .with_context("backend", "postgres") + .with_context("phase", error.phase()); +} + impl std::ops::Drop for PostgresBackend { fn drop(&mut self) { self.pool.close(); diff --git a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs index 8cff0f9..c37caa2 100644 --- a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs -// version: 13 +// version: 14 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -248,3 +248,27 @@ fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compactio } return; } + +#[test] +fn pre_008_backend_trait_implementations_stay_in_runtime_bridge_and_raw_account_scope_stays_closed() { + let runtime = include_str!("../src/runtime.rs"); + for required in [ + "impl ksp_store_api::RawTransactionRead for PostgresBackend", + "impl ksp_store_api::RawTransactionWrite for PostgresBackend", + "impl ksp_store_api::RawTransactionObservationRead for PostgresBackend", + "impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend", + "impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend", + "impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend", + ] { + assert!(runtime.contains(required), "missing pre.008 PostgreSQL capability implementation: {required}"); + } + for forbidden in [ + "impl ksp_store_api::RawAccountStateRead for PostgresBackend", + "impl ksp_store_api::RawAccountStateWrite for PostgresBackend", + "impl ksp_store_api::RawAccountObservationRead for PostgresBackend", + "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", + ] { + assert!(!runtime.contains(forbidden), "pre.008 opened RawAccount capability scope prematurely: {forbidden}"); + } + return; +} diff --git a/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs b/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs index 434c37c..dba09cc 100644 --- a/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs +++ b/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs -// version: 7 +// version: 8 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -183,7 +183,7 @@ fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() { } #[test] -fn pre_009_backend_has_no_env_bypass_or_direct_store_trait_implementation() { +fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_implementation() { let production = std::format!( "{} {} @@ -219,10 +219,9 @@ fn pre_009_backend_has_no_env_bypass_or_direct_store_trait_implementation() { "ksp_store_lib", "ksp_config_lib", "sqlx::", - "impl ksp_store_api::RawTransaction", "impl ksp_store_api::RawAccount", ] { - assert!(!production.contains(forbidden), "forbidden backend ownership/direct-trait material detected: {forbidden}"); + assert!(!production.contains(forbidden), "forbidden backend ownership/reverse-edge/RawAccount material detected: {forbidden}"); } let bootstrap_sql = include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql"); assert!(bootstrap_sql.contains("ksp_store_schema_migrations")); diff --git a/crates/ksp-store-postgres-lib/tests/public_api.rs b/crates/ksp-store-postgres-lib/tests/public_api.rs index 593f238..dd96396 100644 --- a/crates/ksp-store-postgres-lib/tests/public_api.rs +++ b/crates/ksp-store-postgres-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/tests/public_api.rs -// version: 8 +// version: 9 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -102,3 +102,21 @@ fn pre_007_raw_retention_write_bridge_uses_backend_independent_transition_and_ou let _transition = ksp_store_postgres_lib::PostgresBackend::transition_raw_transaction_retention; return; } + +fn assert_raw_transaction_capabilities() +where + T: ksp_store_api::RawTransactionRead + + ksp_store_api::RawTransactionWrite + + ksp_store_api::RawTransactionObservationRead + + ksp_store_api::RawTransactionObservationWrite + + ksp_store_api::RawTransactionRetentionRead + + ksp_store_api::RawTransactionRetentionWrite, +{ + return; +} + +#[test] +fn pre_008_postgres_backend_implements_all_six_raw_transaction_capabilities() { + assert_raw_transaction_capabilities::(); + return; +} diff --git a/crates/ksp-store-postgres-lib/unit_tests/runtime.rs b/crates/ksp-store-postgres-lib/unit_tests/runtime.rs index 77bebdd..4caae98 100644 --- a/crates/ksp-store-postgres-lib/unit_tests/runtime.rs +++ b/crates/ksp-store-postgres-lib/unit_tests/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs -// version: 3 +// version: 4 fn network() -> ksp_store_api::RawNetworkId { return match ksp_store_api::RawNetworkId::new("devnet") { @@ -100,3 +100,27 @@ fn libpq_server_options_are_rejected_in_foundation_runtime() { assert_eq!(error.map(|value| return value.phase()), std::option::Option::Some("server_options")); return; } + +#[test] +fn pre_008_capability_error_mapping_uses_stable_store_and_store_api_codes() { + let cases = [ + (crate::PostgresBackendErrorKind::Conflict, ksp_store_api::ERROR_CODE_RAW_CONFLICT), + (crate::PostgresBackendErrorKind::DataInvalid, ksp_store_api::ErrorCode::new("store", "postgres_data_invalid")), + (crate::PostgresBackendErrorKind::PageLimitUnsupported, ksp_store_api::ErrorCode::new("store", "postgres_page_limit_unsupported")), + (crate::PostgresBackendErrorKind::QueryInvalid, ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID), + (crate::PostgresBackendErrorKind::ReadFailed, ksp_store_api::ErrorCode::new("store", "postgres_read_failed")), + (crate::PostgresBackendErrorKind::ReferenceNotFound, ksp_store_api::ErrorCode::new("store", "raw_reference_not_found")), + (crate::PostgresBackendErrorKind::RetentionCompactionUnsupported, crate::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED), + (crate::PostgresBackendErrorKind::WriteFailed, ksp_store_api::ErrorCode::new("store", "postgres_write_failed")), + (crate::PostgresBackendErrorKind::WrongNetwork, ksp_store_api::ErrorCode::new("store", "wrong_network")), + ]; + for (kind, expected) in cases { + let backend = crate::PostgresBackendError::new(kind, "pre_008_canary"); + let mapped = super::map_capability_error(backend); + assert_eq!(mapped.code(), expected); + let rendered = std::format!("{mapped:?}"); + assert!(!rendered.contains("postgresql://")); + assert!(!rendered.contains("SELECT ")); + } + return; +} diff --git a/deltas/0.3.3/pre.008.md b/deltas/0.3.3/pre.008.md new file mode 100644 index 0000000..4e401d0 --- /dev/null +++ b/deltas/0.3.3/pre.008.md @@ -0,0 +1,135 @@ + + + +# Delta `0.3.3-pre.008` — conformance capabilities et dispatch Store + +## 1. Base et gate d'entrée + +Base opérateur obligatoire : + +```text +0.3.3-pre.7.fix.1 +``` + +Le gate opérateur fourni le 2026-08-30 est entièrement vert : audits Rust/Markdown, workspace check, Clippy, tests Store API/façade/PostgreSQL/Config et `ksp-store-lib --no-default-features` passent. `pre.007` est donc acquise. + +## 2. Version + +```text +workspace.package.version = 0.3.3-pre.8 +``` + +## 3. Six capabilities sur `PostgresBackend` + +`PostgresBackend` implémente désormais directement : + +```text +RawTransactionRead +RawTransactionWrite +RawTransactionObservationRead +RawTransactionObservationWrite +RawTransactionRetentionRead +RawTransactionRetentionWrite +``` + +Les implémentations adaptent les méthodes physiques déjà acquises en `pre.004` à `pre.007` vers `StoreApiFuture` et `ksp_store_api::Result`. Aucun SQL n'est déplacé hors du backend et aucune capability `RawAccount*` n'est ouverte. + +## 4. Six dispatches sur `Store` + +`Store` implémente les mêmes six traits et dispatch vers `StoreRuntime::Postgres` lorsque la feature `postgres` est compilée. Aucun type `PostgresBackend`, pool, client, row, statement ou SQL n'entre dans la surface publique de `ksp-store-lib`. + +Les consumers ordinaires continuent donc à dépendre uniquement de `ksp-store-lib`. + +## 5. Validation réseau pré-I/O + +La façade valide avant dispatch toutes les opérations dont l'input porte un réseau : + +```text +get/list transaction +persist acquisition +record observation +retention read +retention transition +``` + +Une divergence retourne `store.wrong_network` sans rendre le réseau hostile. `RawTransactionObservationRead` reçoit uniquement une `RawObservationKey`; son scope réseau est celui de l'instance Store mono-réseau. + +## 6. Taxonomie d'erreurs stabilisée + +```text +Conflict -> store_api.raw_conflict +QueryInvalid -> store_api.raw_query_invalid +WrongNetwork -> store.wrong_network +ReferenceNotFound -> store.raw_reference_not_found +ReadFailed -> store.postgres_read_failed +WriteFailed -> store.postgres_write_failed +DataInvalid -> store.postgres_data_invalid +PageLimitUnsupported -> store.postgres_page_limit_unsupported +RetentionCompactionUnsupported -> store.postgres_retention_compaction_unsupported +``` + +Le mapping conserve seulement backend, réseau propre de l'instance et phase statique. Aucun SQLSTATE, SQL, bind ou texte serveur n'est exposé. + +## 7. Feature mismatch + +Le contrat `default = ["postgres"]` reste inchangé. Sans default feature, `Store::open` conserve le rejet pré-I/O `store.backend_not_compiled`; le gate `cargo check -p ksp-store-lib --no-default-features` reste obligatoire. + +## 8. Migrations + +Aucune ressource V000/V001 n'est modifiée. Checksums attendus : + +```text +V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 +``` + +## 9. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-store-lib/README.md +crates/ksp-store-lib/USAGE.md +crates/ksp-store-lib/src/error.rs +crates/ksp-store-lib/src/lib.rs +crates/ksp-store-lib/src/store.rs +crates/ksp-store-lib/tests/dependency_boundary.rs +crates/ksp-store-lib/tests/hardening_completeness.rs +crates/ksp-store-lib/tests/public_api.rs +crates/ksp-store-lib/unit_tests/store.rs +crates/ksp-store-postgres-lib/README.md +crates/ksp-store-postgres-lib/USAGE.md +crates/ksp-store-postgres-lib/src/lib.rs +crates/ksp-store-postgres-lib/src/runtime.rs +crates/ksp-store-postgres-lib/tests/dependency_boundary.rs +crates/ksp-store-postgres-lib/tests/hardening_completeness.rs +crates/ksp-store-postgres-lib/tests/public_api.rs +crates/ksp-store-postgres-lib/unit_tests/runtime.rs +docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md +docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md +deltas/0.3.3/pre.008.md +``` + +Aucune suppression de fichier. + +## 10. Gate opérateur demandé + +```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.3 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +``` + +Le test PostgreSQL live reste ignoré dans ce gate ; la preuve métier/concurrence complète appartient à `pre.009`. + +## 11. Suite si gate vert + +```text +0.3.3-pre.009 — preuve PostgreSQL live complète RawTransaction +``` diff --git a/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md b/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md index 7b763c4..78cd03b 100644 --- a/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md +++ b/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.3` — Store/PostgreSQL RawTransaction vertical slice @@ -1148,11 +1148,17 @@ Matérialisation : ### `0.3.3-pre.008` — façade `ksp-store-lib` -- six implémentations/dispatches ; -- validation réseau pré-I/O ; -- erreurs sûres ; -- feature mismatch/no-default-features ; -- aucune fuite de type PostgreSQL. +Tranche matérialisée : + +- les six traits `RawTransaction*` sont implémentés directement sur `PostgresBackend` en adaptant le bridge physique existant vers `StoreApiFuture`/`ksp_store_api::Result` ; +- `Store` implémente les mêmes six traits et dispatch uniquement vers le backend compilé ; +- les opérations portant `RawNetworkId` valident le réseau dans la façade avant tout appel backend ; `RawTransactionObservationRead` n'a aucun réseau dans son input et reste naturellement scoped par l'instance mono-réseau ; +- `Conflict` et `QueryInvalid` conservent les codes API `store_api.raw_conflict` / `store_api.raw_query_invalid` ; +- les erreurs runtime stabilisées sont `store.wrong_network`, `store.raw_reference_not_found`, `store.postgres_read_failed`, `store.postgres_write_failed`, `store.postgres_data_invalid`, `store.postgres_page_limit_unsupported` et le code compaction acquis ; +- les erreurs conservent uniquement backend/network propre de l'instance + phase statique, jamais SQL/SQLSTATE/binds/texte serveur ; +- `cargo check -p ksp-store-lib --no-default-features` reste un gate obligatoire et `Store::open` sans backend compilé conserve `store.backend_not_compiled` ; +- aucun trait `RawAccount*`, type PostgreSQL, SQL ou reverse edge backend -> façade n'est ouvert ; +- V000/V001 restent byte-identiques. ### `0.3.3-pre.009` — preuve PostgreSQL live diff --git a/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md b/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md index 2a95543..c231483 100644 --- a/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md +++ b/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.3` — Store/PostgreSQL RawTransaction vertical slice @@ -42,12 +42,12 @@ Le test live est `#[ignore]` dans les gates ordinaires mais sa preuve réelle an | Capacité | Backend PostgreSQL | Façade Store | Preuve finale | |----------------------------------|--------------------|--------------|---------------------------| -| `RawTransactionRead` | À FAIRE | À FAIRE | unit + integration + live | -| `RawTransactionWrite` | À FAIRE | À FAIRE | unit + concurrency live | -| `RawTransactionObservationRead` | À FAIRE | À FAIRE | unit + live | -| `RawTransactionObservationWrite` | À FAIRE | À FAIRE | unit + concurrency live | -| `RawTransactionRetentionRead` | À FAIRE | À FAIRE | unit + live | -| `RawTransactionRetentionWrite` | À FAIRE | À FAIRE | unit + race live | +| `RawTransactionRead` | PASS pre.008 | PASS pre.008 | unit + integration + live | +| `RawTransactionWrite` | PASS pre.008 | PASS pre.008 | unit + concurrency live | +| `RawTransactionObservationRead` | PASS pre.008 | PASS pre.008 | unit + live | +| `RawTransactionObservationWrite` | PASS pre.008 | PASS pre.008 | unit + concurrency live | +| `RawTransactionRetentionRead` | PASS pre.008 | PASS pre.008 | unit + live | +| `RawTransactionRetentionWrite` | PASS pre.008 | PASS pre.008 | unit + race live | Aucune capacité `RawAccountState`, structurale N2, job ou worker n'est admise dans cette matrice. @@ -608,13 +608,19 @@ cap 500/1000 dans Store pagination - `RetentionCompactionUnsupported` stable, aucune représentation `compacted` SQL : PASS statique/unit ; - preuve des races et atomicité PostgreSQL réelle : différée à `pre.009` ; - premier gate opérateur : FAIL local sur un canari `pre.005` devenu obsolète après ouverture légitime du `DELETE` de purge en `pre.007` ; -- `pre.007-fix.001` retire uniquement cette interdiction historique globale et conserve le garde-fou contre les implémentations directes des traits Store ; gate opérateur à rejouer. +- `pre.007-fix.001` retire uniquement cette interdiction historique globale et conserve le garde-fou jusqu'à ouverture de `pre.008` ; gate opérateur complet PASS le 2026-08-30. ### `pre.008` -- façade/feature dispatch ; -- no physical leak ; -- pre-I/O mismatch. +- six implémentations `RawTransaction*` sur `PostgresBackend` : PASS statique/compile-gate à exécuter ; +- six dispatches correspondants sur `Store` : PASS statique/compile-gate à exécuter ; +- validation réseau façade pré-dispatch : PASS unit/static ; +- `RawTransactionObservationRead` reste scoped par l'instance mono-réseau car l'input ne porte aucun réseau ; +- mapping `Conflict -> store_api.raw_conflict`, `QueryInvalid -> store_api.raw_query_invalid` : PASS statique ; +- codes `wrong_network`, `raw_reference_not_found`, read/write/data/page-limit PostgreSQL stabilisés : PASS public canary ; +- aucune fuite PostgreSQL/SQL/env/reverse edge et aucun `RawAccount*` : PASS boundary canaries ; +- `--no-default-features` reste requis au gate opérateur ; +- V000/V001 inchangées. ### `pre.009` @@ -640,7 +646,7 @@ cap 500/1000 dans Store pagination - publication stable. -## 22. Gate courant `pre.007` +## 22. Gate courant `pre.008` ```bash cargo fmt --all