v0.3.3-pre.012-fix.001

This commit is contained in:
2026-08-30 17:46:49 +02:00
parent 9f2d5ea704
commit 7af751c888
8 changed files with 511 additions and 393 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-lib/README.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# ksp-store-lib
@@ -31,7 +31,7 @@ Une instance `Store` représente exactement :
1 Store = 1 RawNetworkId + 1 backend physique sélectionné
```
Le runtime Store n'est pas un multiplexeur multi-database ou multi-réseau. La sélection d'un target nommé appartient à Config. Le document `std.store` peut donc définir plusieurs targets indépendants — par exemple Devnet, Mainnet et Testnet — mais un appel à `Store::open` reçoit les settings d'un seul target.
Le runtime Store n'est pas un multiplexeur multi-database ou multi-réseau. La sélection d'un target nommé appartient à Config. Le document `std.store` peut donc définir plusieurs targets indépendants, mais un appel à `Store::open` reçoit les settings d'un seul target.
Cette séparation permet d'utiliser des bases PostgreSQL distinctes par réseau tout en conservant le réseau dans l'identité logique des données RAW.
@@ -63,19 +63,9 @@ Store ne lit ni `.env`, ni variables `KSP_*` / `KSPB_*`, ni variables/fichiers i
`ksp-config-lib` possède `std.store`, la résolution des secrets et la sélection du target. Il construit ensuite un `StoreSettings` backend-neutral. L'URI PostgreSQL reste nécessaire au runtime mais n'a aucun getter public dans `ksp-store-lib` et son `Debug` est redacted.
Les targets committed sont actuellement :
## Surface RawTransaction
```text
devnet -> network devnet -> base indépendante
mainnet -> network mainnet-beta -> base indépendante
testnet -> network testnet -> base indépendante
```
Les credentials restent dans les variables `KSP_SECRET_STORE_*_POSTGRES_URI` ou le `.env` possédé par Config.
## Surface actuelle et hors périmètre
Depuis `0.3.3`, `Store` implémente les six capabilities transactionnelles acquises dans `ksp-store-api` :
`Store` implémente les six capabilities transactionnelles acquises dans `ksp-store-api` :
```text
RawTransactionRead
@@ -86,7 +76,18 @@ 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`.
Le consumer manipule uniquement les modèles et outcomes backend-neutral. Les erreurs physiques PostgreSQL sont projetées vers des codes Store stables sans exposer le backend.
La façade fournit ainsi :
- lecture d'une transaction canonique, de ses observations et de sa rétention ;
- écriture atomique transaction + observation ;
- ajout idempotent d'observations ;
- pagination keyset déterministe par cursor opaque ;
- application de transitions de rétention demandées par le caller ;
- validation réseau avant dispatch lorsqu'un input porte explicitement son réseau.
## Hors périmètre
Sont volontairement hors de cette surface :
@@ -95,11 +96,9 @@ Sont volontairement hors de cette surface :
- transport d'acquisition, Program decoding et materialization ;
- exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL.
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
- [`USAGE.md`](USAGE.md) — construction des settings, ouverture, health et fermeture ;
- [`USAGE.md`](USAGE.md) — guide pratique de construction, lifecycle et capabilities Store ;
- [`../ksp-store-postgres-lib/README.md`](../ksp-store-postgres-lib/README.md) — responsabilité du backend PostgreSQL physique ;
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store committed ;
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture durable Store ;

View File

@@ -1,36 +1,58 @@
<!-- file: crates/ksp-store-lib/USAGE.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Utilisation de ksp-store-lib
## 1. Dépendance et features
## 1. Dépendance et backend compilé
Le consumer runtime normal dépend uniquement de la façade :
Le consumer runtime dépend de la façade commune :
```toml
[dependencies]
ksp-store-lib = { path = "../ksp-store-lib" }
```
La feature par défaut est :
La feature par défaut compile le backend PostgreSQL :
```text
postgres
```
Pour construire un binaire sans backend physique :
Pour compiler la façade sans backend physique :
```toml
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
```
Dans ce mode, le type PostgreSQL reste connu par la surface de settings mais `Store::open` retourne `ERROR_CODE_BACKEND_NOT_COMPILED` avant toute I/O si PostgreSQL est sélectionné.
Dans ce mode, les settings PostgreSQL restent représentables, mais `Store::open` retourne `ERROR_CODE_BACKEND_NOT_COMPILED` avant toute I/O si PostgreSQL est sélectionné.
Un consumer ordinaire ne dépend pas directement de `ksp-store-postgres-lib`.
Un consumer applicatif ordinaire ne dépend pas directement de `ksp-store-postgres-lib`.
## 2. Construire des settings PostgreSQL programmatiquement
## 2. Obtenir les settings depuis Config
La construction directe est utile pour les tests, outils internes ou compositions qui n'utilisent pas `ksp-config-lib`.
Le chemin applicatif recommandé passe par `ksp-config-lib`, propriétaire de `std.store`, de la résolution `.env` et des secrets.
```rust
fn resolve_store_settings(
engine: &ksp_config_lib::ConfigDocumentEngine,
environment: &ksp_config_lib::ConfigEnvironment,
target: std::option::Option<&str>,
) -> ksp_core_lib::Result<ksp_store_lib::StoreSettings> {
let resolved = engine.load_resolved_store_config(target, environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(resolved.into_settings());
}
```
Chaque `StoreSettings` sélectionne exactement un réseau logique et un backend physique. La sélection d'un target nommé appartient à Config ; `Store` ne route pas automatiquement entre plusieurs targets.
## 3. Construire des settings programmatiquement
La construction directe est utile pour les tests et outils qui ne passent pas par Config.
```rust
fn programmatic_store_settings(connection_uri: std::string::String) -> ksp_store_lib::Result<ksp_store_lib::StoreSettings> {
@@ -52,8 +74,7 @@ fn programmatic_store_settings(connection_uri: std::string::String) -> ksp_store
ksp_store_lib::StoreBackendSettings::Postgres(postgres),
);
let validation = settings.validate();
if let std::result::Result::Err(error) = validation {
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(error);
}
@@ -61,11 +82,11 @@ fn programmatic_store_settings(connection_uri: std::string::String) -> ksp_store
}
```
`PostgresStoreSettings` ne fournit volontairement aucun getter public de l'URI. Son `Debug` remplace cette valeur par `<redacted>`.
`PostgresStoreSettings` ne fournit aucun getter public de l'URI. Son `Debug` remplace cette valeur par `<redacted>`.
## 3. Ouvrir et fermer un Store
## 4. Ouvrir, sonder et fermer un Store
`Store::open` est async et ne retourne un succès qu'après que le backend compilé a prouvé sa fondation runtime.
`Store::open` est async et ne retourne un succès qu'après validation des settings, ouverture du backend compilé et bootstrap requis.
```rust
async fn use_store(settings: ksp_store_lib::StoreSettings) -> ksp_store_lib::Result<()> {
@@ -95,130 +116,11 @@ async fn use_store(settings: ksp_store_lib::StoreSettings) -> ksp_store_lib::Res
}
```
`Store::close(self)` consomme l'instance afin qu'une fermeture explicite ne puisse pas être suivie d'une nouvelle opération via la même valeur.
`Store::close(self)` consomme l'instance. Une fermeture explicite ne peut donc pas être suivie d'une nouvelle opération via la même valeur.
## 4. Construire les settings depuis Config
## 5. Lire une transaction RAW
Le chemin applicatif recommandé utilise `ksp-config-lib`, propriétaire du document `std.store`, de `.env` et des secrets.
Après construction du `ConfigDocumentEngine` :
```rust
fn resolve_store_settings(
engine: &ksp_config_lib::ConfigDocumentEngine,
environment: &ksp_config_lib::ConfigEnvironment,
target: std::option::Option<&str>,
) -> ksp_core_lib::Result<ksp_store_lib::StoreSettings> {
let resolved = engine.load_resolved_store_config(target, environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(resolved.into_settings());
}
```
Targets committed :
```text
devnet -> RawNetworkId("devnet")
mainnet -> RawNetworkId("mainnet-beta")
testnet -> RawNetworkId("testnet")
```
Chaque target peut utiliser une URI PostgreSQL distincte. `default_profile` sélectionne un seul target ; `Store` ne route pas automatiquement entre plusieurs targets.
## 5. Settings disponibles
### `PostgresPoolSettings`
Valeurs par défaut :
```text
max_connections 8
connect_timeout 10 s
wait_timeout 5 s
create_timeout 10 s
recycle_timeout 5 s
```
Les getters sont :
```text
max_connections()
connect_timeout()
wait_timeout()
create_timeout()
recycle_timeout()
```
`validate()` vérifie les bornes sans I/O.
### `PostgresBootstrapSettings`
Valeurs par défaut :
```text
schema_autocreate true
schema_autoupdate true
migration_timeout 30 s
migration_lock_timeout 10 s
```
Getters :
```text
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 :
```text
backend()
backend_kind()
network()
shutdown_timeout()
validate()
```
`StoreSettings::new` permet de choisir explicitement le timeout de shutdown. `StoreSettings::with_default_shutdown` utilise la borne commune par défaut de 5 secondes.
## 6. Health et diagnostics
`StoreRuntimeSnapshot` est synchrone et ne déclenche aucune I/O. Il expose uniquement :
```text
backend_kind
network
pool_capacity
pool_size
pool_available
pool_waiting
```
`StoreHealthSnapshot` ajoute une probe async bornée :
```text
state = Ready | NotReady
migration_version
pending_migration_count
last_error_code
runtime snapshot
```
Aucun snapshot n'expose URI, host, user, database, SQL, handle backend ou texte d'erreur PostgreSQL.
## 7. Utiliser les capabilities RawTransaction
Depuis `0.3.3`, `Store` implémente directement les six traits `RawTransaction*`. Le consumer importe le trait correspondant puis appelle la méthode sur la façade :
Importer le trait correspondant suffit pour utiliser la façade :
```rust
use ksp_store_lib::RawTransactionRead;
@@ -231,9 +133,146 @@ async fn read_transaction(
}
```
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.
Une transaction absente retourne `None`. Une transaction `Purged` retourne également `None` pour le payload canonique ; son tombstone reste accessible via la capability de rétention.
Codes runtime principaux :
## 6. Paginer les références de transactions
La pagination est keyset et utilise un cursor opaque. Le consumer ne doit pas interpréter ses bytes.
```rust
use ksp_store_lib::RawTransactionRead;
async fn first_transaction_page(
store: &ksp_store_lib::Store,
network: ksp_store_lib::RawNetworkId,
) -> ksp_store_lib::Result<ksp_store_lib::RawPage<ksp_store_lib::RawTransactionReference>> {
let limit = ksp_store_lib::RawPageLimit::new(100);
let limit = match limit {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slots = ksp_store_lib::RawSlotRange::new(std::option::Option::None, std::option::Option::None);
let slots = match slots {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let query = ksp_store_lib::RawTransactionQuery::new(
network,
slots,
ksp_store_lib::RawSortDirection::Ascending,
ksp_store_lib::RawPageRequest::first(limit),
);
return store.list_raw_transactions(&query).await;
}
```
Pour continuer, recopier le cursor retourné par `RawPage::next_cursor()` dans `RawPageRequest::after`. Le réseau, la direction et les bornes de slots doivent rester identiques à ceux de la query ayant produit le cursor.
## 7. Persister une acquisition canonique
La transaction canonique et son observation initiale forment une seule opération atomique.
```rust
use ksp_store_lib::RawTransactionWrite;
async fn persist_acquisition(
store: &ksp_store_lib::Store,
transaction: ksp_store_lib::RawTransaction,
observation: ksp_store_lib::RawTransactionObservation,
) -> ksp_store_lib::Result<ksp_store_lib::RawAcquisitionWriteOutcome> {
return store
.persist_raw_transaction_acquisition(
transaction,
observation,
ksp_store_lib::RawTransactionAcquisitionMode::Normal,
)
.await;
}
```
Le mode `Normal` respecte un tombstone `Purged`. `ForceRehydrate` doit être choisi explicitement lorsqu'un caller veut restaurer un payload purgé et que l'identité retenue est compatible.
Un contenu divergent sous la même identité produit `ERROR_CODE_RAW_CONFLICT`; Store ne remplace jamais silencieusement le contenu gagnant.
## 8. Lire et ajouter une observation
Une observation supplémentaire référence une transaction canonique déjà durable.
```rust
use ksp_store_lib::RawTransactionObservationRead;
use ksp_store_lib::RawTransactionObservationWrite;
async fn use_observation(
store: &ksp_store_lib::Store,
key: &ksp_store_lib::RawObservationKey,
observation: ksp_store_lib::RawTransactionObservation,
) -> ksp_store_lib::Result<ksp_store_lib::RawObservationWriteOutcome> {
let existing = store.get_raw_transaction_observation(key).await;
if let std::result::Result::Err(error) = existing {
return std::result::Result::Err(error);
}
return store.record_raw_transaction_observation(observation).await;
}
```
`record_raw_transaction_observation` ne crée pas implicitement le canonique. Une référence absente est signalée par `ERROR_CODE_RAW_REFERENCE_NOT_FOUND`.
## 9. Lire la rétention et le tombstone
```rust
use ksp_store_lib::RawTransactionRetentionRead;
async fn read_retention(
store: &ksp_store_lib::Store,
reference: &ksp_store_lib::RawTransactionReference,
) -> ksp_store_lib::Result<std::option::Option<ksp_store_lib::RawRetentionState>> {
let tombstone = store.get_raw_transaction_tombstone(reference).await;
if let std::result::Result::Err(error) = tombstone {
return std::result::Result::Err(error);
}
return store.get_raw_transaction_retention_state(reference).await;
}
```
Le tombstone minimal est utile uniquement lorsque le payload a été purgé ; il ne remplace pas le modèle canonique lorsqu'un payload est encore disponible.
## 10. Appliquer une transition de rétention
La policy qui décide qu'une transition est autorisée appartient au caller. Store applique uniquement la transition demandée de manière atomique.
```rust
use ksp_store_lib::RawTransactionRetentionWrite;
async fn archive_transaction(
store: &ksp_store_lib::Store,
reference: ksp_store_lib::RawTransactionReference,
) -> ksp_store_lib::Result<ksp_store_lib::RawRetentionWriteOutcome> {
let transition = ksp_store_lib::RawTransactionRetentionTransition::try_new(
reference,
ksp_store_lib::RawRetentionState::Full,
ksp_store_lib::RawRetentionState::Archived,
);
let transition = match transition {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return store.transition_raw_transaction_retention(transition).await;
}
```
Le backend PostgreSQL supporte physiquement `Full -> Archived -> Purged`. Une transition impliquant `Compacted` est rejetée par ce backend tant qu'aucune représentation compactée réelle n'est implémentée.
## 11. Diagnostics et erreurs
Les snapshots et erreurs de façade n'exposent ni URI, host, user, database, SQL, handle backend, valeur de bind ni texte d'erreur PostgreSQL.
Les codes Store utiles incluent notamment :
```text
store.wrong_network
@@ -245,17 +284,10 @@ 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`.
Les conflits et queries invalides utilisent les codes backend-neutral `store_api.raw_conflict` et `store_api.raw_query_invalid`.
## 8. Limite fonctionnelle actuelle
## 12. Limites de la façade
La façade n'implémente pas encore les capabilities `RawAccount*`. Elles appartiennent à la vertical slice `0.3.4`.
La façade ne fournit pas d'accès public au SQL, au pool, aux clients ou transactions PostgreSQL. Les capabilities `RawAccount*` réexportées par l'API commune ne sont pas encore dispatchées par `Store`.
## 9. Preuve finale `0.3.3`
La conformance de la façade est verrouillée par les tests publics/hardening avec et sans feature PostgreSQL. Le gate technique final `0.3.3-pre.011` a également rejoué le workspace complet et la preuve PostgreSQL réelle `RawTransaction` sur PostgreSQL 17.
Références durables :
- [`../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md`](../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md) ;
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md).
La taille de page est une primitive de navigation. Les décisions de batch, priorité, backlog et scheduling appartiennent aux workers/jobs, pas à Store.