v0.3.3-pre.008

This commit is contained in:
2026-08-30 14:10:10 +02:00
parent 1f0b202135
commit 84ab2b3651
21 changed files with 816 additions and 62 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-postgres-lib/README.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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 ;

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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`.

View File

@@ -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

View File

@@ -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<std::option::Option<ksp_store_api::RawTransaction>>> {
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<ksp_store_api::RawPage<ksp_store_api::RawTransactionReference>>> {
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<ksp_store_api::RawAcquisitionWriteOutcome>> {
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<std::option::Option<ksp_store_api::RawTransactionObservation>>> {
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<ksp_store_api::RawObservationWriteOutcome>> {
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<std::option::Option<ksp_store_api::RawRetentionState>>> {
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<std::option::Option<ksp_store_api::RawTransactionTombstone>>> {
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<ksp_store_api::RawRetentionWriteOutcome>> {
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();

View File

@@ -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;
}

View File

@@ -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"));

View File

@@ -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<T>()
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::<ksp_store_postgres_lib::PostgresBackend>();
return;
}

View File

@@ -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;
}