Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7cfb9557d | |||
| c27428d6c0 | |||
| dfa01b1381 | |||
| 5fc7f65b2c | |||
| bbf804b7fc | |||
| 13687ec2fe | |||
| b756da5149 | |||
| 04234926ce | |||
| bfc69f5631 | |||
| 902b5fba99 | |||
| fccb7d876c | |||
| 50d4142797 | |||
| eb6dbc31e8 | |||
| fb8b585aa2 | |||
| dad18c16d5 | |||
| a704a5722e | |||
| dfa3723a7f | |||
| 6bd593f467 | |||
| 03ad0ba063 | |||
| d95b1095e5 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,8 +1,20 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 22 -->
|
||||
<!-- version: 23 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.3.4 — Store/PostgreSQL RawAccountState + complétude RAW — 2026-08-31
|
||||
|
||||
`0.3.4` complète la seconde vertical slice RAW physique sur le couple `ksp-store-lib` / `ksp-store-postgres-lib` et ferme la conformance PostgreSQL des **10 capabilities** backend-agnostic de `ksp-store-api` : les six capabilities `RawTransaction*` acquises en `0.3.3` restent intactes et les quatre capabilities `RawAccountStateRead`, `RawAccountStateWrite`, `RawAccountObservationRead` et `RawAccountObservationWrite` sont désormais implémentées par `PostgresBackend` puis dispatchées par la façade `Store`. La séparation reste stricte : les consommateurs ordinaires passent par `ksp-store-lib`, le backend PostgreSQL conserve SQL/driver/pool/TLS/migrations privés, et la façade reste compilable/testable sans backend via `--no-default-features`.
|
||||
|
||||
La migration additive V002 introduit `ksp_raw_account_states` et `ksp_raw_account_observations` au-dessus de V000/V001 sans modifier leurs bytes. L'identité canonique account est `(pubkey, slot, state_hash)` : plusieurs états d'un même compte dans un même slot restent représentables lorsque `state_hash` diffère, tandis qu'une collision sur la référence complète déclenche une comparaison exacte de `lamports`, `owner`, `executable`, `rent_epoch` et `data` avant de conclure à l'idempotence ou à `store_api.raw_conflict`. Les `u64` physiques utilisent `NUMERIC(20,0)`, les clés/hashes/signatures utilisent des `BYTEA` de largeur contrainte, et les bytes account restent complets jusqu'à la borne KSP de 16 MiB. V002 contient exactement 32 ressources gérées et son checksum final est `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`; V000 et V001 restent respectivement `d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450` et `31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51`.
|
||||
|
||||
L'acquisition `RawAccountState + RawAccountObservation` est transactionnelle : `INSERT ... ON CONFLICT DO NOTHING`, lecture/verrouillage du canonical en collision, comparaison exacte et rollback complet lorsque l'observation diverge. Une observation supplémentaire vérifie la référence existante sous transaction et ne crée jamais implicitement son state. Les métadonnées Yellowstone `is_startup`, `transaction_signature` et `write_version` restent optionnelles et observation-only ; aucune FK transaction n'est inventée. La navigation account utilise une keyset `(slot, pubkey, state_hash)` ASC/DESC, avec filtre pubkey optionnel et cursor KSPA V1 opaque de 109 octets lié au réseau, au filtre, au range, à la direction et à la famille afin d'empêcher les replays cross-query/cross-family. Aucun `OFFSET`, plafond métier de batch, index owner/provider/time ou lifecycle destructif account n'est introduit.
|
||||
|
||||
Les gates de clôture valident audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config, `cargo test --workspace`, façade sans feature PostgreSQL et graphes Cargo. Le live `postgres_raw_account_live` passe sur **PostgreSQL 17** avec bootstrap V000/V001/V002, round-trip des bytes et du domaine `u64`, idempotence/conflit, états distincts même pubkey+slot, observations/metadata optionnelles, pagination/cursors, concurrence, annulation/rollback et coexistence `RawTransaction`. La réconciliation documentaire finale est ensuite validée sans réouvrir code ni migrations.
|
||||
|
||||
`prompts/024-V0_3_5_START_PROMPT.md` ouvre `0.3.5` sur `ksp-interface-lib` uniquement. Cette release doit réauditer les surfaces d'acquisition actuelles et matérialiser seulement les modèles passifs/event-only réellement partagés, sans recopier `RawTransaction`/`RawAccountState`, sans créer un event bus et sans déplacer les DTOs provider/transport. Les candidats logs, slot/root/slotsUpdates, transaction status et vote sont traités par matrice sémantique ; une famille reste reportée si la convergence ou le consumer réel n'est pas démontré. L'archive historique kbot3 n'est pas requise pour ce gate : les sources de vérité sont la base KSP stable et les contrats officiels actuels des transports concernés.
|
||||
|
||||
## 0.3.3 — Store/PostgreSQL RawTransaction vertical slice — 2026-08-30
|
||||
|
||||
`0.3.3` complète la première vertical slice RAW physique sur le couple `ksp-store-lib` / `ksp-store-postgres-lib` sans modifier les contrats backend-agnostic acquis dans `ksp-store-api`. `PostgresBackend` et la façade `Store` implémentent désormais les six capabilities `RawTransactionRead`, `RawTransactionWrite`, `RawTransactionObservationRead`, `RawTransactionObservationWrite`, `RawTransactionRetentionRead` et `RawTransactionRetentionWrite`. Une base PostgreSQL reste liée à un unique `RawNetworkId` par `ksp_store_identity`; le mauvais réseau est refusé avant I/O, les slots `u64` sont conservés exactement en `NUMERIC(20,0)`, et la migration logique V001 reste découpée en ressources tables/contraintes/indexes avec vérification de compatibilité du schéma effectif.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 363
|
||||
# version: 382
|
||||
|
||||
[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"
|
||||
version = "0.3.4"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 97 -->
|
||||
<!-- version: 98 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -96,8 +96,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
- [X] `0.3.1` — `ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
||||
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
||||
- [ ] `0.3.4` — Étendre le même couple avec `RawAccountState` + `RawAccountObservation` : quatre capabilities account, migration additive au-dessus de V000/V001, acquisition state+observation atomique, idempotence/conflit, get/list cursorisé, puis complétude des dix capabilities RAW, indexes justifiés par les queries et hardening PostgreSQL cross-family final.
|
||||
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/events réellement partagés par les premiers consumers d’acquisition, sans dupliquer les modèles persistants de `ksp-store-api`.
|
||||
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
||||
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/event-only dont une matrice des surfaces HTTP/WS/Yellowstone/Helius démontre la sémantique réellement partagée et le besoin consumer ; auditer notamment logs, slot/root/slotsUpdates, transaction status et vote, sans dupliquer `RawTransaction`/`RawAccountState`, sans event bus et sans dépendance Interface vers Transport/Store.
|
||||
- [ ] `0.3.6` — Introduire `ksp-job-api` et un premier job de backfill historique concret consommant `ksp-store-lib`, avec policy/batch-size/progression possédés par le job et non par Store.
|
||||
- [ ] `0.3.7` — Introduire une application spécialisée de backfill/inspection RAW.
|
||||
- [ ] Compléter ensuite la couche RAW avec le worker/service live, son contrôle et les outils d’exploitation réellement nécessaires avant de passer à la couche de normalisation générique suivante.
|
||||
@@ -105,7 +105,7 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
### TODO/IDEAS — taxonomie N1, processing et rétention
|
||||
|
||||
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique reste réservée à `0.3.4`.
|
||||
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
|
||||
- [ ] **TODO** — `TransactionStatusObservation` : réauditer `signatureSubscribe`, `getSignatureStatuses`, Yellowstone TransactionStatus et extensions provider lorsqu’un consumer réel apparaît ; ne pas fusionner snapshot, transition et update dans un modèle Option-soup.
|
||||
- [ ] **TODO** — logs realtime : conserver `logMessages` dans `RawTransaction` jusqu’à la décomposition STRUCTURAL ; traiter `logsSubscribe` comme event-only candidat et décider son contrat passif dans `ksp-interface-lib`, sans table Store par défaut. Le format canonique d’un wake-up « donnée persistée disponible » reste distinct et appartient à `ksp-store-api` conformément à `KSP-NOTIFY-*`, mais ne sera matérialisé qu’avec un publisher/consumer réel.
|
||||
- [ ] **TODO** — slot/root/slotsUpdates et vote : ne créer un modèle passif commun que si un consumer realtime réel et une sémantique cross-ledger/provider justifient le contrat ; aucune persistence Store par défaut.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-store-lib/README.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# ksp-store-lib
|
||||
|
||||
@@ -19,7 +19,7 @@ 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é ;
|
||||
- les dix capabilities RAW de `ksp-store-api` dispatchées vers le backend compilé : six `RawTransaction*` et quatre `RawAccount*` ;
|
||||
- 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.
|
||||
|
||||
@@ -63,9 +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.
|
||||
|
||||
## Surface RawTransaction
|
||||
## Surface RAW
|
||||
|
||||
`Store` implémente les six capabilities transactionnelles acquises dans `ksp-store-api` :
|
||||
`Store` implémente exactement dix capabilities RAW backend-neutral :
|
||||
|
||||
```text
|
||||
RawTransactionRead
|
||||
@@ -74,24 +74,29 @@ RawTransactionObservationRead
|
||||
RawTransactionObservationWrite
|
||||
RawTransactionRetentionRead
|
||||
RawTransactionRetentionWrite
|
||||
RawAccountStateRead
|
||||
RawAccountStateWrite
|
||||
RawAccountObservationRead
|
||||
RawAccountObservationWrite
|
||||
```
|
||||
|
||||
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.
|
||||
Pour `RawTransaction`, la façade fournit la lecture canonique et des observations, l'acquisition atomique transaction+observation, l'ajout idempotent d'observations, la pagination keyset et les transitions de rétention demandées par le caller.
|
||||
|
||||
La façade fournit ainsi :
|
||||
Pour `RawAccountState`, elle fournit :
|
||||
|
||||
- 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.
|
||||
- lecture d'un état complet par référence durable `(network, pubkey, slot, state_hash)` ;
|
||||
- pagination keyset des références dans l'ordre total `(slot, pubkey, state_hash)`, avec filtre pubkey optionnel ;
|
||||
- écriture atomique état+observation avec idempotence exacte et conflit sur contenu divergent ;
|
||||
- lecture et ajout d'observations account ;
|
||||
- validation réseau avant dispatch pour les opérations dont l'input porte explicitement un réseau.
|
||||
|
||||
Les cursors restent opaques et propres à leur famille. Store ne leur attribue aucune sémantique de batch, priorité ou scheduling.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
Sont volontairement hors de cette surface :
|
||||
|
||||
- persistence/query/rétention PostgreSQL de `RawAccountState` ;
|
||||
- rétention, archivage, purge, delete ou compaction 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.
|
||||
@@ -105,4 +110,6 @@ Sont volontairement hors de cette surface :
|
||||
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — plan de fondation ;
|
||||
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — validation de fondation ;
|
||||
- [`../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md`](../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md) — plan `RawTransaction` ;
|
||||
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction`.
|
||||
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction` ;
|
||||
- [`../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md`](../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md) — design `RawAccountState` et complétude RAW ;
|
||||
- [`../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md`](../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md) — validation `RawAccountState` et conformance 10/10.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-store-lib/USAGE.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Utilisation de ksp-store-lib
|
||||
|
||||
@@ -268,7 +268,98 @@ async fn archive_transaction(
|
||||
|
||||
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
|
||||
## 11. Lire et paginer les états account RAW
|
||||
|
||||
Importer `RawAccountStateRead` donne accès à la lecture par référence et à la navigation déterministe.
|
||||
|
||||
```rust
|
||||
use ksp_store_lib::RawAccountStateRead;
|
||||
|
||||
async fn read_account_state(
|
||||
store: &ksp_store_lib::Store,
|
||||
reference: &ksp_store_lib::RawAccountStateReference,
|
||||
) -> ksp_store_lib::Result<std::option::Option<ksp_store_lib::RawAccountState>> {
|
||||
return store.get_raw_account_state(reference).await;
|
||||
}
|
||||
```
|
||||
|
||||
Pour une première page :
|
||||
|
||||
```rust
|
||||
use ksp_store_lib::RawAccountStateRead;
|
||||
|
||||
async fn first_account_page(
|
||||
store: &ksp_store_lib::Store,
|
||||
network: ksp_store_lib::RawNetworkId,
|
||||
pubkey: std::option::Option<ksp_store_lib::Pubkey>,
|
||||
) -> ksp_store_lib::Result<ksp_store_lib::RawPage<ksp_store_lib::RawAccountStateReference>> {
|
||||
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::RawAccountStateQuery::new(
|
||||
network,
|
||||
pubkey,
|
||||
slots,
|
||||
ksp_store_lib::RawSortDirection::Ascending,
|
||||
ksp_store_lib::RawPageRequest::first(limit),
|
||||
);
|
||||
|
||||
return store.list_raw_account_states(&query).await;
|
||||
}
|
||||
```
|
||||
|
||||
La continuation utilise uniquement `RawPage::next_cursor()` puis `RawPageRequest::after`. Le cursor account est opaque et lié au réseau, au filtre pubkey, à la direction et aux bornes de slots de la query qui l'a produit.
|
||||
|
||||
## 12. Persister une acquisition account
|
||||
|
||||
L'état canonique et son observation initiale sont une seule opération atomique.
|
||||
|
||||
```rust
|
||||
use ksp_store_lib::RawAccountStateWrite;
|
||||
|
||||
async fn persist_account_acquisition(
|
||||
store: &ksp_store_lib::Store,
|
||||
state: ksp_store_lib::RawAccountState,
|
||||
observation: ksp_store_lib::RawAccountObservation,
|
||||
) -> ksp_store_lib::Result<ksp_store_lib::RawAcquisitionWriteOutcome> {
|
||||
return store.persist_raw_account_acquisition(state, observation).await;
|
||||
}
|
||||
```
|
||||
|
||||
`state.reference()` et `observation.account()` doivent désigner exactement le même état et le réseau du `Store`. Une répétition byte-identique est idempotente ; un contenu divergent sous la même identité retourne `ERROR_CODE_RAW_CONFLICT`.
|
||||
|
||||
## 13. Lire et ajouter une observation account
|
||||
|
||||
```rust
|
||||
use ksp_store_lib::RawAccountObservationRead;
|
||||
use ksp_store_lib::RawAccountObservationWrite;
|
||||
|
||||
async fn use_account_observation(
|
||||
store: &ksp_store_lib::Store,
|
||||
key: &ksp_store_lib::RawObservationKey,
|
||||
observation: ksp_store_lib::RawAccountObservation,
|
||||
) -> ksp_store_lib::Result<ksp_store_lib::RawObservationWriteOutcome> {
|
||||
let existing = store.get_raw_account_observation(key).await;
|
||||
if let std::result::Result::Err(error) = existing {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
return store.record_raw_account_observation(observation).await;
|
||||
}
|
||||
```
|
||||
|
||||
`record_raw_account_observation` ne crée jamais implicitement l'état canonique ; une référence absente retourne `ERROR_CODE_RAW_REFERENCE_NOT_FOUND`. `RawObservationKey` ne contient pas de réseau : la lecture par clé reste liée au backend mono-réseau déjà ouvert.
|
||||
|
||||
## 14. 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.
|
||||
|
||||
@@ -286,8 +377,8 @@ store.postgres_retention_compaction_unsupported
|
||||
|
||||
Les conflits et queries invalides utilisent les codes backend-neutral `store_api.raw_conflict` et `store_api.raw_query_invalid`.
|
||||
|
||||
## 12. Limites de la façade
|
||||
## 15. Limites de la façade
|
||||
|
||||
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`.
|
||||
La façade ne fournit pas d'accès public au SQL, au pool, aux clients ou transactions PostgreSQL. Elle dispatch les dix capabilities RAW de l'API commune, mais ne fournit aucune capability de rétention, archivage, purge, delete ou compaction account.
|
||||
|
||||
La taille de page est une primitive de navigation. Les décisions de batch, priorité, backlog et scheduling appartiennent aux workers/jobs, pas à Store.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-lib/src/lib.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -8,9 +8,10 @@
|
||||
//! Common backend-neutral Store runtime facade for KSP.
|
||||
//!
|
||||
//! 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.
|
||||
//! backend-neutral capability dispatch. `0.3.3-pre.008` completed the six PostgreSQL
|
||||
//! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities
|
||||
//! on both the physical backend and this common facade, completing the RAW inventory at
|
||||
//! ten capabilities without exposing physical types.
|
||||
//!
|
||||
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
||||
//! an optional implementation dependency. No backend implementation type is
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-lib/src/store.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
/// Opaque common Store runtime facade.
|
||||
///
|
||||
@@ -116,6 +116,156 @@ impl std::fmt::Debug for Store {
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawAccountObservationRead for Store {
|
||||
fn get_raw_account_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::RawAccountObservation>>> {
|
||||
return std::boxed::Box::pin(async move {
|
||||
#[cfg(feature = "postgres")]
|
||||
{
|
||||
return match &self.runtime {
|
||||
StoreRuntime::Postgres(backend) => {
|
||||
let result = backend.get_raw_account_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::RawAccountObservationWrite for Store {
|
||||
fn record_raw_account_observation<'a>(
|
||||
&'a self,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawObservationWriteOutcome>> {
|
||||
let network_check = validate_operation_network(&self.network, observation.account().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_account_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::RawAccountStateRead for Store {
|
||||
fn get_raw_account_state<'a>(
|
||||
&'a self,
|
||||
reference: &'a ksp_store_api::RawAccountStateReference,
|
||||
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawAccountState>>> {
|
||||
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_account_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 list_raw_account_states<'a>(
|
||||
&'a self,
|
||||
query: &'a ksp_store_api::RawAccountStateQuery,
|
||||
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>>> {
|
||||
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_account_states(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::RawAccountStateWrite for Store {
|
||||
fn persist_raw_account_acquisition<'a>(
|
||||
&'a self,
|
||||
state: ksp_store_api::RawAccountState,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawAcquisitionWriteOutcome>> {
|
||||
let state_network = validate_operation_network(&self.network, state.reference().network(), self.backend_kind);
|
||||
if let std::result::Result::Err(error) = state_network {
|
||||
return std::boxed::Box::pin(async move {
|
||||
return std::result::Result::Err(error);
|
||||
});
|
||||
}
|
||||
let observation_network = validate_operation_network(&self.network, observation.account().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_account_acquisition(state, observation).await;
|
||||
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
|
||||
},
|
||||
};
|
||||
}
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
{
|
||||
let _ = state;
|
||||
let _ = observation;
|
||||
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawTransactionRead for Store {
|
||||
fn get_raw_transaction<'a>(
|
||||
&'a self,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -60,34 +60,28 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_facade_dispatches_six_raw_transaction_capabilities_without_physical_leak() {
|
||||
fn pre_008_facade_dispatches_exact_ten_raw_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::RawAccountObservationRead for Store",
|
||||
"impl ksp_store_api::RawAccountObservationWrite for Store",
|
||||
"impl ksp_store_api::RawAccountStateRead for Store",
|
||||
"impl ksp_store_api::RawAccountStateWrite for Store",
|
||||
"impl ksp_store_api::RawTransactionObservationRead for Store",
|
||||
"impl ksp_store_api::RawTransactionObservationWrite for Store",
|
||||
"impl ksp_store_api::RawTransactionRead for Store",
|
||||
"impl ksp_store_api::RawTransactionRetentionRead for Store",
|
||||
"impl ksp_store_api::RawTransactionRetentionWrite for Store",
|
||||
"impl ksp_store_api::RawTransactionWrite 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}");
|
||||
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 10);
|
||||
for forbidden in ["tokio_postgres::", "deadpool_postgres::", "CREATE TABLE", "INSERT INTO", "UPDATE ksp_", "DELETE FROM"] {
|
||||
assert!(!store.contains(forbidden), "pre.008 facade leaked physical backend material: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-lib/tests/hardening_completeness.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -84,6 +84,27 @@ fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
|
||||
return names;
|
||||
}
|
||||
|
||||
fn raw_capability_trait_names<'a>(source: &'a str, implementor: &str) -> std::vec::Vec<&'a str> {
|
||||
let mut names = std::vec::Vec::new();
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with("impl ksp_store_api::Raw") || !trimmed.contains(implementor) {
|
||||
continue;
|
||||
}
|
||||
let trait_tail = match trimmed.strip_prefix("impl ksp_store_api::") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let trait_name = match trait_tail.split(" for ").next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
names.push(trait_name);
|
||||
}
|
||||
names.sort_unstable();
|
||||
return names;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
@@ -263,27 +284,40 @@ fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_ha
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_facade_raw_transaction_capability_inventory_is_exact_and_raw_account_scope_stays_closed() {
|
||||
fn pre_010_facade_raw_capability_inventory_is_exactly_ten() {
|
||||
let store = include_str!("../src/store.rs");
|
||||
let capability_impls = [
|
||||
"impl ksp_store_api::RawTransactionRead for Store",
|
||||
"impl ksp_store_api::RawTransactionWrite for Store",
|
||||
"impl ksp_store_api::RawAccountObservationRead for Store",
|
||||
"impl ksp_store_api::RawAccountObservationWrite for Store",
|
||||
"impl ksp_store_api::RawAccountStateRead for Store",
|
||||
"impl ksp_store_api::RawAccountStateWrite for Store",
|
||||
"impl ksp_store_api::RawTransactionObservationRead for Store",
|
||||
"impl ksp_store_api::RawTransactionObservationWrite for Store",
|
||||
"impl ksp_store_api::RawTransactionRead for Store",
|
||||
"impl ksp_store_api::RawTransactionRetentionRead for Store",
|
||||
"impl ksp_store_api::RawTransactionRetentionWrite for Store",
|
||||
"impl ksp_store_api::RawTransactionWrite for Store",
|
||||
];
|
||||
for implementation in capability_impls {
|
||||
assert_eq!(store.matches(implementation).count(), 1, "unexpected Store capability implementation inventory: {implementation}");
|
||||
}
|
||||
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",
|
||||
] {
|
||||
assert!(!store.contains(forbidden), "RawAccountState scope opened in Store during RawTransaction hardening: {forbidden}");
|
||||
}
|
||||
assert_eq!(store.matches("validate_operation_network(").count(), 9);
|
||||
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 10);
|
||||
assert_eq!(store.matches("validate_operation_network(").count(), 14);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_facade_and_backend_raw_capability_sets_match_exactly_without_account_retention() {
|
||||
let store = include_str!("../src/store.rs");
|
||||
let backend = include_str!("../../ksp-store-postgres-lib/src/runtime.rs");
|
||||
let store_traits = raw_capability_trait_names(store, " for Store");
|
||||
let backend_traits = raw_capability_trait_names(backend, " for PostgresBackend");
|
||||
assert_eq!(store_traits.len(), 10);
|
||||
assert_eq!(backend_traits.len(), 10);
|
||||
assert_eq!(store_traits, backend_traits);
|
||||
for forbidden in ["RawAccountRetentionRead", "RawAccountRetentionWrite", "RawAccountDelete", "RawAccountCompaction"] {
|
||||
assert!(!store_traits.contains(&forbidden), "unexpected account capability added to Store: {forbidden}");
|
||||
assert!(!backend_traits.contains(&forbidden), "unexpected account capability added to PostgreSQL backend: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-lib/tests/public_api.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -74,21 +74,25 @@ fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts()
|
||||
return;
|
||||
}
|
||||
|
||||
fn assert_raw_transaction_capabilities<T>()
|
||||
fn assert_raw_capabilities<T>()
|
||||
where
|
||||
T: ksp_store_lib::RawTransactionRead
|
||||
+ ksp_store_lib::RawTransactionWrite
|
||||
T: ksp_store_lib::RawAccountObservationRead
|
||||
+ ksp_store_lib::RawAccountObservationWrite
|
||||
+ ksp_store_lib::RawAccountStateRead
|
||||
+ ksp_store_lib::RawAccountStateWrite
|
||||
+ ksp_store_lib::RawTransactionObservationRead
|
||||
+ ksp_store_lib::RawTransactionObservationWrite
|
||||
+ ksp_store_lib::RawTransactionRead
|
||||
+ ksp_store_lib::RawTransactionRetentionRead
|
||||
+ ksp_store_lib::RawTransactionRetentionWrite,
|
||||
+ ksp_store_lib::RawTransactionRetentionWrite
|
||||
+ ksp_store_lib::RawTransactionWrite,
|
||||
{
|
||||
let _marker = std::marker::PhantomData::<T>;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_store_facade_implements_all_six_raw_transaction_capabilities() {
|
||||
assert_raw_transaction_capabilities::<ksp_store_lib::Store>();
|
||||
fn pre_008_store_facade_implements_exact_raw_capability_set_10_of_10() {
|
||||
assert_raw_capabilities::<ksp_store_lib::Store>();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-store-postgres-lib/README.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# ksp-store-postgres-lib
|
||||
|
||||
@@ -18,7 +18,7 @@ La crate possède seule pour PostgreSQL :
|
||||
- le bootstrap et le moteur de migrations privé KSP ;
|
||||
- la table metadata `ksp_store_schema_migrations` ;
|
||||
- l'advisory transaction lock borné des migrations ;
|
||||
- le schéma physique et les statements privés `RawTransaction` ;
|
||||
- les schémas physiques et statements privés `RawTransaction` et `RawAccountState` ;
|
||||
- les snapshots runtime/health sûrs destinés au bridge de façade ;
|
||||
- la fermeture explicite du pool et son fallback `Drop` best-effort ;
|
||||
- la classification d'erreurs backend sans conserver le texte d'erreur PostgreSQL.
|
||||
@@ -63,7 +63,7 @@ VerifyFull
|
||||
|
||||
Le moteur de migrations embarqué vérifie version logique, 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.
|
||||
|
||||
Le bootstrap metadata est conservé comme migration V000. La migration logique V001 matérialise le schéma `RawTransaction` en ressources séparées `tables/`, `constraints/` et `indexes/` afin que le backend puisse vérifier leur compatibilité effective sans transformer les fichiers SQL en parser généraliste.
|
||||
Le bootstrap metadata est conservé comme migration V000. La migration logique V001 matérialise le schéma `RawTransaction` et V002 le schéma `RawAccountState`, chacune en ressources séparées `tables/`, `constraints/` et `indexes/` afin que le backend puisse vérifier leur compatibilité effective sans transformer les fichiers SQL en parser généraliste.
|
||||
|
||||
La base est liée à un seul `RawNetworkId` via `ksp_store_identity`. Une migration enregistrée mais physiquement divergente est un mismatch ; les réparations additives sûres dépendent de `schema_autoupdate`.
|
||||
|
||||
@@ -130,11 +130,43 @@ Le backend verrouille la ligne canonique avec `FOR UPDATE`, compare l'état cour
|
||||
|
||||
Toute transition impliquant `Compacted` est rejetée avec `RetentionCompactionUnsupported` tant qu'aucune représentation compactée réelle n'est implémentée.
|
||||
|
||||
## Lectures RAW account
|
||||
|
||||
Le backend expose :
|
||||
|
||||
```text
|
||||
get_raw_account_state
|
||||
get_raw_account_observation
|
||||
```
|
||||
|
||||
`get_raw_account_state` reconstruit l'état complet à partir de `(pubkey, slot, state_hash)` sans narrowing du domaine `u64`. Les bytes `pubkey`, `owner` et `state_hash` sont revalidés à leur largeur exacte et `data` reste un `BYTEA` complet, vide autorisé, borné par le contrat Store API.
|
||||
|
||||
`get_raw_account_observation` reconstruit la provenance commune et les métadonnées account optionnelles, notamment `is_startup`, `transaction_signature` et `write_version`. La signature est une metadata fixed-width et ne crée aucune FK vers la famille transaction.
|
||||
|
||||
## Écritures RAW account
|
||||
|
||||
Le backend expose :
|
||||
|
||||
```text
|
||||
persist_raw_account_acquisition
|
||||
record_raw_account_observation
|
||||
```
|
||||
|
||||
L'acquisition état+observation est transactionnelle. Les inserts utilisent `ON CONFLICT ... DO NOTHING`, puis verrouillent et comparent le contenu gagnant avant de conclure `AlreadyPresent` ou `Conflict`; aucun `DO UPDATE` n'est utilisé. Une collision divergente d'observation fait échouer toute l'acquisition et rollback un éventuel nouvel état.
|
||||
|
||||
L'ajout d'une observation vérifie que l'état référencé existe déjà et ne crée jamais implicitement cet état.
|
||||
|
||||
## Pagination RAW account
|
||||
|
||||
`list_raw_account_states` parcourt les références selon l'ordre total `(slot, pubkey, state_hash)`, en ASC ou DESC, avec filtre pubkey optionnel. La continuation est keyset, sans `OFFSET`.
|
||||
|
||||
Le cursor `KSPA` est opaque et lié au réseau, au filtre pubkey, à la direction, aux bornes de slots et à la dernière clé complète. Il est distinct du cursor transaction `KSPT`.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
La crate ne contient :
|
||||
|
||||
- aucune implémentation PostgreSQL des capabilities `RawAccount*` ;
|
||||
- aucune rétention, archive, purge, suppression ou compaction account ;
|
||||
- aucune orchestration worker/job ;
|
||||
- aucun transport d'acquisition ou decoder Program ;
|
||||
- aucune policy autonome de batch, priorité ou rétention.
|
||||
@@ -147,4 +179,6 @@ La crate ne contient :
|
||||
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — décisions pool/TLS/migrations ;
|
||||
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — validation de fondation ;
|
||||
- [`../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md`](../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md) — design `RawTransaction` ;
|
||||
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction`.
|
||||
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction` ;
|
||||
- [`../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md`](../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md) — design `RawAccountState` et complétude RAW ;
|
||||
- [`../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md`](../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md) — validation `RawAccountState` et conformance RAW.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Utilisation de ksp-store-postgres-lib
|
||||
|
||||
@@ -202,7 +202,58 @@ Le backend applique la transition choisie par le caller ; il ne décide pas de l
|
||||
|
||||
Une transition impliquant `Compacted` est refusée avec `PostgresBackendErrorKind::RetentionCompactionUnsupported` tant qu'aucune représentation compactée réelle n'est disponible.
|
||||
|
||||
## 11. Classifier les erreurs sans fuite
|
||||
## 11. Lire et paginer les états account
|
||||
|
||||
```rust
|
||||
async fn read_account_state(
|
||||
backend: &ksp_store_postgres_lib::PostgresBackend,
|
||||
reference: &ksp_store_api::RawAccountStateReference,
|
||||
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, ksp_store_postgres_lib::PostgresBackendError> {
|
||||
return backend.get_raw_account_state(reference).await;
|
||||
}
|
||||
```
|
||||
|
||||
Pour la navigation, construire un `RawAccountStateQuery` puis appeler :
|
||||
|
||||
```rust
|
||||
async fn list_account_states(
|
||||
backend: &ksp_store_postgres_lib::PostgresBackend,
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>, ksp_store_postgres_lib::PostgresBackendError> {
|
||||
return backend.list_raw_account_states(query).await;
|
||||
}
|
||||
```
|
||||
|
||||
La pagination est keyset sur `(slot, pubkey, state_hash)` avec filtre pubkey optionnel. Le cursor `KSPA` est opaque, lié au contexte de query et distinct du cursor transaction.
|
||||
|
||||
## 12. Persister une acquisition account
|
||||
|
||||
```rust
|
||||
async fn persist_account_acquisition(
|
||||
backend: &ksp_store_postgres_lib::PostgresBackend,
|
||||
state: ksp_store_api::RawAccountState,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
|
||||
return backend.persist_raw_account_acquisition(state, observation).await;
|
||||
}
|
||||
```
|
||||
|
||||
Le backend exige le même réseau et la même référence complète entre l'état et l'observation avant l'I/O métier. L'opération est atomique et idempotente par comparaison exacte du contenu persistant ; un contenu divergent produit `PostgresBackendErrorKind::Conflict` sans overwrite silencieux.
|
||||
|
||||
## 13. Lire et ajouter une observation account
|
||||
|
||||
```rust
|
||||
async fn record_account_observation(
|
||||
backend: &ksp_store_postgres_lib::PostgresBackend,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
|
||||
return backend.record_raw_account_observation(observation).await;
|
||||
}
|
||||
```
|
||||
|
||||
La lecture correspondante utilise `get_raw_account_observation`. L'ajout exige un état déjà durable et retourne `ReferenceNotFound` lorsqu'il manque. Les métadonnées Yellowstone optionnelles sont conservées sans créer de couplage physique vers `RawTransaction`.
|
||||
|
||||
## 14. Classifier les erreurs sans fuite
|
||||
|
||||
```rust
|
||||
fn classify(error: &ksp_store_postgres_lib::PostgresBackendError) {
|
||||
@@ -234,8 +285,8 @@ fn classify(error: &ksp_store_postgres_lib::PostgresBackendError) {
|
||||
|
||||
`PostgresBackendError` conserve uniquement une classification KSP et une phase statique. Ne pas reconstruire de diagnostic utilisateur à partir d'une erreur brute PostgreSQL.
|
||||
|
||||
## 12. Limites du backend direct
|
||||
## 15. Limites du backend direct
|
||||
|
||||
Le backend ne lit aucune variable d'environnement et ne possède aucune sélection de target Config. Les applications, jobs et workers doivent normalement passer par `ksp-store-lib`.
|
||||
|
||||
Les capabilities `RawAccount*` ne sont pas implémentées par ce backend. Les décisions de batch, priorité, backlog, scheduling et policy de rétention restent hors de sa responsabilité.
|
||||
Les dix capabilities RAW communes sont implémentées par ce backend. Les décisions de batch, priorité, backlog, scheduling et policy de rétention restent hors de sa responsabilité ; aucune rétention/archivage/purge account n'est fournie.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'pk_ksp_raw_account_states'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT pk_ksp_raw_account_states PRIMARY KEY (pubkey, slot, state_hash);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'pk_ksp_raw_account_observations'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT pk_ksp_raw_account_observations PRIMARY KEY (observation_key);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,14 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_ksp_raw_account_observations_state'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations
|
||||
ADD CONSTRAINT fk_ksp_raw_account_observations_state FOREIGN KEY (account_pubkey, account_slot, account_state_hash)
|
||||
REFERENCES ksp_raw_account_states(pubkey, slot, state_hash) ON DELETE RESTRICT;
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_pubkey'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_pubkey CHECK (octet_length(pubkey) = 32);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_slot'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_slot CHECK (slot >= 0 AND slot <= 18446744073709551615);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_state_hash'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_state_hash CHECK (octet_length(state_hash) = 32);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_lamports'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_lamports CHECK (lamports >= 0 AND lamports <= 18446744073709551615);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_owner'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_owner CHECK (octet_length(owner) = 32);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_rent_epoch'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_rent_epoch CHECK (rent_epoch >= 0 AND rent_epoch <= 18446744073709551615);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_states_data'
|
||||
AND conrelid = to_regclass('ksp_raw_account_states')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_data CHECK (octet_length(data) <= 16777216);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_key'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_key CHECK (octet_length(observation_key) = 32);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_account_pubkey'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_pubkey CHECK (octet_length(account_pubkey) = 32);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_account_slot'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_slot CHECK (account_slot >= 0 AND account_slot <= 18446744073709551615);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_account_state_hash'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_state_hash CHECK (octet_length(account_state_hash) = 32);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_provider'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_provider CHECK (
|
||||
octet_length(provider) >= 1 AND octet_length(provider) <= 128
|
||||
AND provider ~ '^[A-Za-z0-9_.:-]+$'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_protocol'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_protocol CHECK (
|
||||
octet_length(protocol) >= 1 AND octet_length(protocol) <= 128
|
||||
AND protocol ~ '^[A-Za-z0-9_.:-]+$'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_method'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_method CHECK (
|
||||
octet_length(acquisition_method) >= 1 AND octet_length(acquisition_method) <= 128
|
||||
AND acquisition_method ~ '^[A-Za-z0-9_.:-]+$'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_origin'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_origin CHECK (origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay');
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_received_at'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_received_at CHECK (received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,18 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_capture_session'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_capture_session CHECK (
|
||||
capture_session_id IS NULL
|
||||
OR (
|
||||
octet_length(capture_session_id) >= 1 AND octet_length(capture_session_id) <= 128
|
||||
AND capture_session_id ~ '^[A-Za-z0-9_.:-]+$'
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,18 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_commitment'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_commitment CHECK (
|
||||
commitment IS NULL
|
||||
OR (
|
||||
octet_length(commitment) >= 1 AND octet_length(commitment) <= 128
|
||||
AND commitment ~ '^[A-Za-z0-9_.:-]+$'
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,18 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_endpoint'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_endpoint CHECK (
|
||||
endpoint_id IS NULL
|
||||
OR (
|
||||
octet_length(endpoint_id) >= 1 AND octet_length(endpoint_id) <= 128
|
||||
AND endpoint_id ~ '^[A-Za-z0-9_.:-]+$'
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,18 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_filter'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_filter CHECK (
|
||||
filter_id IS NULL
|
||||
OR (
|
||||
octet_length(filter_id) >= 1 AND octet_length(filter_id) <= 128
|
||||
AND filter_id ~ '^[A-Za-z0-9_.:-]+$'
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_observed_at'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_observed_at CHECK (
|
||||
observed_at_unix_millis IS NULL
|
||||
OR observed_at_unix_millis >= 0 AND observed_at_unix_millis <= 253402300799999
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,12 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_time_order'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_time_order CHECK (observed_at_unix_millis IS NULL OR observed_at_unix_millis <= received_at_unix_millis);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_source_hash'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_source_hash CHECK (
|
||||
source_payload_hash IS NULL
|
||||
OR octet_length(source_payload_hash) = 32
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_source_size'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_source_size CHECK (
|
||||
source_payload_size_bytes IS NULL
|
||||
OR source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_transaction_signature'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_transaction_signature CHECK (
|
||||
transaction_signature IS NULL
|
||||
OR octet_length(transaction_signature) = 64
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,15 @@
|
||||
DO $ksp$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'ck_ksp_raw_account_observations_write_version'
|
||||
AND conrelid = to_regclass('ksp_raw_account_observations')
|
||||
) THEN
|
||||
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_write_version CHECK (
|
||||
write_version IS NULL
|
||||
OR write_version >= 0 AND write_version <= 18446744073709551615
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$ksp$;
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX IF NOT EXISTS ix_ksp_raw_account_states_slot_pubkey_state_hash
|
||||
ON ksp_raw_account_states (slot, pubkey, state_hash);
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS ksp_raw_account_states (
|
||||
pubkey BYTEA NOT NULL,
|
||||
slot NUMERIC(20, 0) NOT NULL,
|
||||
state_hash BYTEA NOT NULL,
|
||||
lamports NUMERIC(20, 0) NOT NULL,
|
||||
owner BYTEA NOT NULL,
|
||||
executable BOOLEAN NOT NULL,
|
||||
rent_epoch NUMERIC(20, 0) NOT NULL,
|
||||
data BYTEA NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS pubkey BYTEA NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS slot NUMERIC(20, 0) NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS state_hash BYTEA NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS lamports NUMERIC(20, 0) NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS owner BYTEA NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS executable BOOLEAN NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS rent_epoch NUMERIC(20, 0) NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS data BYTEA NOT NULL;
|
||||
@@ -0,0 +1,41 @@
|
||||
CREATE TABLE IF NOT EXISTS ksp_raw_account_observations (
|
||||
observation_key BYTEA NOT NULL,
|
||||
account_pubkey BYTEA NOT NULL,
|
||||
account_slot NUMERIC(20, 0) NOT NULL,
|
||||
account_state_hash BYTEA NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
protocol TEXT NOT NULL,
|
||||
acquisition_method TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
received_at_unix_millis BIGINT NOT NULL,
|
||||
capture_session_id TEXT NULL,
|
||||
commitment TEXT NULL,
|
||||
endpoint_id TEXT NULL,
|
||||
filter_id TEXT NULL,
|
||||
observed_at_unix_millis BIGINT NULL,
|
||||
source_payload_hash BYTEA NULL,
|
||||
source_payload_size_bytes BIGINT NULL,
|
||||
is_startup BOOLEAN NULL,
|
||||
transaction_signature BYTEA NULL,
|
||||
write_version NUMERIC(20, 0) NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS observation_key BYTEA NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS account_pubkey BYTEA NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS account_slot NUMERIC(20, 0) NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS account_state_hash BYTEA NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS protocol TEXT NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS acquisition_method TEXT NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS origin TEXT NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS received_at_unix_millis BIGINT NOT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS capture_session_id TEXT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS commitment TEXT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS endpoint_id TEXT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS filter_id TEXT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS observed_at_unix_millis BIGINT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS source_payload_hash BYTEA NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS source_payload_size_bytes BIGINT NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS is_startup BOOLEAN NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS transaction_signature BYTEA NULL;
|
||||
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS write_version NUMERIC(20, 0) NULL;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||
// version: 14
|
||||
// version: 21
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -21,6 +21,18 @@
|
||||
//! 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.
|
||||
//! `0.3.4-pre.002` registers additive V002 and its two minimal RAW account
|
||||
//! tables with canonical state/observation PKs and the observation-state FK.
|
||||
//! `0.3.4-pre.003` completes V002 with exact physical bounds, one unfiltered
|
||||
//! navigation index, external-schema compatibility and the bounded prerelease
|
||||
//! checksum transition from the provisional `pre.002` schema. `0.3.4-pre.004`
|
||||
//! adds backend-private RAW account state/observation read mapping and hostile-row
|
||||
//! guards. `0.3.4-pre.005` adds atomic account state+observation acquisition writes
|
||||
//! with exact idempotence/conflict classification. `0.3.4-pre.006` adds additional
|
||||
//! account observations guarded by the existing state reference. `0.3.4-pre.007` adds
|
||||
//! deterministic account keyset pagination with the fixed `KSPA` cursor. `0.3.4-pre.008`
|
||||
//! implements the four `RawAccount*` capabilities directly on `PostgresBackend`, completing
|
||||
//! the backend RAW capability inventory at ten without exposing physical PostgreSQL types.
|
||||
//!
|
||||
//! 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
|
||||
@@ -30,6 +42,7 @@ mod constants;
|
||||
mod error;
|
||||
mod health;
|
||||
mod migration;
|
||||
mod raw_account;
|
||||
mod raw_transaction;
|
||||
mod runtime;
|
||||
mod schema;
|
||||
@@ -59,6 +72,22 @@ pub(crate) use self::health::probe_health;
|
||||
pub(crate) use self::migration::bootstrap;
|
||||
/// Current embedded migration version consumed by the private health probe.
|
||||
pub(crate) use self::migration::current_migration_version;
|
||||
/// Private RAW account cursor decoder consumed by the physical RAW account module.
|
||||
pub(crate) use self::raw_account::cursor::decode_raw_account_cursor;
|
||||
/// Private RAW account cursor encoder consumed by the physical RAW account module.
|
||||
pub(crate) use self::raw_account::cursor::encode_raw_account_cursor;
|
||||
/// Private physical account page-limit converter consumed by the physical RAW account module.
|
||||
pub(crate) use self::raw_account::cursor::raw_account_physical_page_limit;
|
||||
/// Private RAW account observation reader consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_account::get_raw_account_observation;
|
||||
/// Private RAW account state reader consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_account::get_raw_account_state;
|
||||
/// Private RAW account-state list reader consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_account::list_raw_account_states;
|
||||
/// Private atomic RAW account acquisition writer consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_account::persist_raw_account_acquisition;
|
||||
/// Private additional RAW account observation writer consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_account::record_raw_account_observation;
|
||||
/// Private RAW transaction cursor decoder consumed by the physical RAW module.
|
||||
pub(crate) use self::raw_transaction::cursor::decode_raw_transaction_cursor;
|
||||
/// Private RAW transaction cursor encoder consumed by the physical RAW module.
|
||||
@@ -93,11 +122,15 @@ pub(crate) use self::schema::SchemaResourceState;
|
||||
pub(crate) use self::schema::V000_RESOURCES;
|
||||
/// Private V001 schema resource inventory consumed by the migration engine.
|
||||
pub(crate) use self::schema::V001_RESOURCES;
|
||||
/// Private V002 schema resource inventory consumed by the migration engine.
|
||||
pub(crate) use self::schema::V002_RESOURCES;
|
||||
/// Private physical schema resource inspector consumed by the migration engine.
|
||||
pub(crate) use self::schema::inspect_resource;
|
||||
/// Private V001 adoption probe consumed by the migration engine.
|
||||
pub(crate) use self::schema::managed_v001_objects_exist;
|
||||
/// Private external-schema compatibility gate consumed by the migration engine.
|
||||
/// Private managed-schema adoption probe consumed by the migration engine.
|
||||
pub(crate) use self::schema::managed_schema_objects_exist;
|
||||
/// Private V001 external-schema compatibility gate consumed by the migration engine.
|
||||
pub(crate) use self::schema::verify_v001_external_compatibility;
|
||||
/// Private V002 external-schema compatibility gate consumed by the migration engine.
|
||||
pub(crate) use self::schema::verify_v002_external_compatibility;
|
||||
|
||||
const _: &str = crate::TRACING_TARGET;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/migration.rs
|
||||
// version: 7
|
||||
// version: 10
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
@@ -9,6 +9,7 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
|
||||
checksum: MigrationChecksum::LegacySql(include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql")),
|
||||
hook: MigrationHook::None,
|
||||
name: "bootstrap",
|
||||
previous_checksums: &[],
|
||||
resources: crate::V000_RESOURCES,
|
||||
version: 0,
|
||||
},
|
||||
@@ -16,13 +17,23 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
|
||||
checksum: MigrationChecksum::Resources,
|
||||
hook: MigrationHook::StoreIdentity,
|
||||
name: "raw_transaction",
|
||||
previous_checksums: &[],
|
||||
resources: crate::V001_RESOURCES,
|
||||
version: 1,
|
||||
},
|
||||
EmbeddedMigration {
|
||||
checksum: MigrationChecksum::Resources,
|
||||
hook: MigrationHook::None,
|
||||
name: "raw_account_state",
|
||||
previous_checksums: &[V002_PROVISIONAL_CHECKSUM_PRE_002],
|
||||
resources: crate::V002_RESOURCES,
|
||||
version: 2,
|
||||
},
|
||||
];
|
||||
const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
|
||||
const HISTORY_INSERT_SQL: &str = "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)";
|
||||
const HISTORY_LOAD_SQL: &str = "SELECT version, name, checksum FROM ksp_store_schema_migrations ORDER BY version";
|
||||
const HISTORY_UPDATE_CHECKSUM_SQL: &str = "UPDATE ksp_store_schema_migrations SET checksum = $1 WHERE version = $2 AND name = $3 AND checksum = $4";
|
||||
const IDENTITY_INSERT_SQL: &str = "INSERT INTO ksp_store_identity (singleton, network) VALUES (1, $1)";
|
||||
const IDENTITY_LOAD_SQL: &str = "SELECT singleton, network FROM ksp_store_identity ORDER BY singleton LIMIT 2";
|
||||
const LOCK_POLL_INTERVAL_MS: u64 = 25;
|
||||
@@ -33,6 +44,7 @@ const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
AND table_type = 'BASE TABLE'
|
||||
)"#;
|
||||
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
|
||||
const V002_PROVISIONAL_CHECKSUM_PRE_002: &str = "30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55";
|
||||
|
||||
struct AppliedMigration {
|
||||
checksum: std::string::String,
|
||||
@@ -51,6 +63,7 @@ struct EmbeddedMigration {
|
||||
checksum: MigrationChecksum,
|
||||
hook: MigrationHook,
|
||||
name: &'static str,
|
||||
previous_checksums: &'static [&'static str],
|
||||
resources: &'static [crate::SchemaResource],
|
||||
version: i64,
|
||||
}
|
||||
@@ -133,7 +146,7 @@ async fn bootstrap_inner(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (next_index, mutation_mode) = if metadata_exists {
|
||||
let (next_index, mutation_mode, applied_history) = if metadata_exists {
|
||||
let metadata_result = crate::inspect_resource(&transaction, &crate::V000_RESOURCES[0]).await;
|
||||
match metadata_result {
|
||||
std::result::Result::Ok(crate::SchemaResourceState::Compatible) => {},
|
||||
@@ -148,18 +161,18 @@ async fn bootstrap_inner(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS);
|
||||
let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS, schema_autoupdate);
|
||||
let index = match validation_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
(index, SchemaMutationMode::Update)
|
||||
(index, SchemaMutationMode::Update, std::option::Option::Some(history))
|
||||
} else {
|
||||
if !schema_autocreate {
|
||||
log_schema_block("schema_autocreate_disabled");
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_autocreate_disabled"));
|
||||
}
|
||||
let managed_result = crate::managed_v001_objects_exist(&transaction).await;
|
||||
let managed_result = crate::managed_schema_objects_exist(&transaction).await;
|
||||
let managed_objects_exist = match managed_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -168,12 +181,18 @@ async fn bootstrap_inner(
|
||||
log_schema_block("schema_adoption_disabled");
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_adoption_disabled"));
|
||||
}
|
||||
(0, SchemaMutationMode::Create)
|
||||
(0, SchemaMutationMode::Create, std::option::Option::None)
|
||||
};
|
||||
let existing_schema_result = verify_or_repair_applied_migrations(&transaction, next_index, schema_autoupdate).await;
|
||||
if let std::result::Result::Err(error) = existing_schema_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::option::Option::Some(history) = applied_history.as_deref() {
|
||||
let checksum_result = reconcile_applied_history_checksums(&transaction, history, next_index, schema_autoupdate).await;
|
||||
if let std::result::Result::Err(error) = checksum_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
let existing_hook_result = run_applied_migration_hooks(&transaction, network, next_index).await;
|
||||
if let std::result::Result::Err(error) = existing_hook_result {
|
||||
return std::result::Result::Err(error);
|
||||
@@ -521,12 +540,14 @@ async fn set_statement_timeout(
|
||||
}
|
||||
|
||||
async fn verify_migration_contract(transaction: &deadpool_postgres::Transaction<'_>, version: i64) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if version == 1 {
|
||||
let result = crate::verify_v001_external_compatibility(transaction).await;
|
||||
if let std::result::Result::Err(error) = result {
|
||||
log_schema_block(error.phase());
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let result = match version {
|
||||
1 => crate::verify_v001_external_compatibility(transaction).await,
|
||||
2 => crate::verify_v002_external_compatibility(transaction).await,
|
||||
_ => std::result::Result::Ok(()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = result {
|
||||
log_schema_block(error.phase());
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -575,6 +596,41 @@ async fn verify_or_repair_applied_migrations(
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
async fn reconcile_applied_history_checksums(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
history: &[AppliedMigration],
|
||||
applied_count: usize,
|
||||
schema_autoupdate: bool,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if !schema_autoupdate {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
for (index, applied) in history.iter().take(applied_count).enumerate() {
|
||||
let migration = &EMBEDDED_MIGRATIONS[index];
|
||||
let expected_checksum = migration_checksum(migration);
|
||||
if applied.checksum == expected_checksum {
|
||||
continue;
|
||||
}
|
||||
if !migration.previous_checksums.contains(&applied.checksum.as_str()) {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
|
||||
}
|
||||
let result = transaction.execute(HISTORY_UPDATE_CHECKSUM_SQL, &[&expected_checksum, &migration.version, &migration.name, &applied.checksum]).await;
|
||||
match result {
|
||||
std::result::Result::Ok(1) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
migration_version = migration.version,
|
||||
"upgraded known prerelease PostgreSQL Store migration checksum after schema reconciliation"
|
||||
);
|
||||
},
|
||||
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_checksum_update"));
|
||||
},
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn schema_autoupdate_disabled_error() -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "schema_autoupdate_disabled");
|
||||
}
|
||||
@@ -608,7 +664,11 @@ fn validate_embedded_registry(migrations: &[EmbeddedMigration]) -> std::result::
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_history(history: &[AppliedMigration], migrations: &[EmbeddedMigration]) -> std::result::Result<usize, crate::PostgresBackendError> {
|
||||
fn validate_history(
|
||||
history: &[AppliedMigration],
|
||||
migrations: &[EmbeddedMigration],
|
||||
allow_previous_checksums: bool,
|
||||
) -> std::result::Result<usize, crate::PostgresBackendError> {
|
||||
if history.is_empty() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
|
||||
}
|
||||
@@ -623,7 +683,8 @@ fn validate_history(history: &[AppliedMigration], migrations: &[EmbeddedMigratio
|
||||
}
|
||||
let expected = &migrations[index];
|
||||
let expected_checksum = migration_checksum(expected);
|
||||
if applied.version != expected.version || applied.name != expected.name || applied.checksum != expected_checksum {
|
||||
let previous_checksum_matches = allow_previous_checksums && expected.previous_checksums.contains(&applied.checksum.as_str());
|
||||
if applied.version != expected.version || applied.name != expected.name || (applied.checksum != expected_checksum && !previous_checksum_matches) {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
|
||||
}
|
||||
index += 1;
|
||||
|
||||
962
crates/ksp-store-postgres-lib/src/raw_account.rs
Normal file
962
crates/ksp-store-postgres-lib/src/raw_account.rs
Normal file
@@ -0,0 +1,962 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
// version: 5
|
||||
|
||||
pub(crate) mod cursor;
|
||||
|
||||
const GET_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubkey, account_slot::text AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::text AS write_version_text FROM ksp_raw_account_observations WHERE observation_key = $1";
|
||||
const GET_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3";
|
||||
const INSERT_ACCOUNT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_account_observations (observation_key, account_pubkey, account_slot, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version) VALUES ($1, $2, $3::TEXT::NUMERIC, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::TEXT::NUMERIC) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
|
||||
const INSERT_ACCOUNT_STATE_SQL: &str = "INSERT INTO ksp_raw_account_states (pubkey, slot, state_hash, lamports, owner, executable, rent_epoch, data) VALUES ($1, $2::TEXT::NUMERIC, $3, $4::TEXT::NUMERIC, $5, $6, $7::TEXT::NUMERIC, $8) ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING pubkey";
|
||||
const LIST_ACCOUNT_STATES_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, pubkey, state_hash) > ($3::TEXT::NUMERIC, $4::BYTEA, $5::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $6";
|
||||
const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) > ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $7";
|
||||
const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) < ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot DESC, pubkey DESC, state_hash DESC LIMIT $7";
|
||||
const LIST_ACCOUNT_STATES_DESC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, pubkey, state_hash) < ($3::TEXT::NUMERIC, $4::BYTEA, $5::BYTEA)) ORDER BY slot DESC, pubkey DESC, state_hash DESC LIMIT $6";
|
||||
const LOCK_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubkey, account_slot::text AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::text AS write_version_text FROM ksp_raw_account_observations WHERE observation_key = $1 FOR UPDATE";
|
||||
const LOCK_ACCOUNT_REFERENCE_SQL: &str =
|
||||
"SELECT 1 FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3 FOR KEY SHARE";
|
||||
const LOCK_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3 FOR UPDATE";
|
||||
|
||||
struct RawAccountListDbRow {
|
||||
pubkey: std::vec::Vec<u8>,
|
||||
slot_text: std::string::String,
|
||||
state_hash: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
struct RawAccountObservationDbRow {
|
||||
account_pubkey: std::vec::Vec<u8>,
|
||||
account_slot_text: std::string::String,
|
||||
account_state_hash: std::vec::Vec<u8>,
|
||||
acquisition_method: std::string::String,
|
||||
capture_session_id: std::option::Option<std::string::String>,
|
||||
commitment: std::option::Option<std::string::String>,
|
||||
endpoint_id: std::option::Option<std::string::String>,
|
||||
filter_id: std::option::Option<std::string::String>,
|
||||
is_startup: std::option::Option<bool>,
|
||||
observation_key: std::vec::Vec<u8>,
|
||||
observed_at_unix_millis: std::option::Option<i64>,
|
||||
origin: std::string::String,
|
||||
protocol: std::string::String,
|
||||
provider: std::string::String,
|
||||
received_at_unix_millis: i64,
|
||||
source_payload_hash: std::option::Option<std::vec::Vec<u8>>,
|
||||
source_payload_size_bytes: std::option::Option<i64>,
|
||||
transaction_signature: std::option::Option<std::vec::Vec<u8>>,
|
||||
write_version_text: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
struct RawAccountStateDbRow {
|
||||
data: std::vec::Vec<u8>,
|
||||
executable: bool,
|
||||
lamports_text: std::string::String,
|
||||
owner: std::vec::Vec<u8>,
|
||||
pubkey: std::vec::Vec<u8>,
|
||||
rent_epoch_text: std::string::String,
|
||||
slot_text: std::string::String,
|
||||
state_hash: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
/// Reads one complete canonical RAW account state from the physical PostgreSQL backend.
|
||||
pub(crate) async fn get_raw_account_state(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
reference: &ksp_store_api::RawAccountStateReference,
|
||||
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
|
||||
let network_result = ensure_network(network, reference, "raw_account_state_network");
|
||||
if let std::result::Result::Err(error) = network_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let client_result = pool.get().await;
|
||||
let client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
|
||||
};
|
||||
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
|
||||
let slot_text = reference.slot().to_string();
|
||||
let state_hash = reference.state_hash();
|
||||
let state_hash_bytes: &[u8] = state_hash.as_bytes();
|
||||
let rows_result = client.query(GET_ACCOUNT_STATE_SQL, &[&pubkey_bytes, &slot_text, &state_hash_bytes]).await;
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_state_query"));
|
||||
},
|
||||
};
|
||||
if rows.is_empty() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
if rows.len() != 1 {
|
||||
return std::result::Result::Err(data_invalid("raw_account_state_cardinality"));
|
||||
}
|
||||
let row = match rows.first() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_state_cardinality")),
|
||||
};
|
||||
let physical = match raw_account_state_db_row(row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded = match decode_raw_account_state_row(network, physical) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if decoded.reference() != reference {
|
||||
return std::result::Result::Err(data_invalid("raw_account_state_reference"));
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::Some(decoded));
|
||||
}
|
||||
|
||||
/// Lists deterministic canonical RAW account-state references using PostgreSQL keyset pagination.
|
||||
pub(crate) async fn list_raw_account_states(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>, crate::PostgresBackendError> {
|
||||
if query.network() != network {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, "raw_account_list_network"));
|
||||
}
|
||||
let (requested_usize, sql_limit) = match crate::raw_account_physical_page_limit(query.page().limit().get()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded_cursor = match query.page().cursor() {
|
||||
std::option::Option::Some(value) => match crate::decode_raw_account_cursor(query, value) {
|
||||
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let client_result = pool.get().await;
|
||||
let client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
|
||||
};
|
||||
let slots = query.slots();
|
||||
let start_text = slots.start_inclusive().map(|value| return value.to_string());
|
||||
let end_text = slots.end_inclusive().map(|value| return value.to_string());
|
||||
let cursor_slot_text = decoded_cursor.as_ref().map(|value| return value.last_slot.to_string());
|
||||
let cursor_pubkey = decoded_cursor.as_ref().map(|value| return value.last_pubkey.to_vec());
|
||||
let cursor_state_hash = decoded_cursor.as_ref().map(|value| return value.last_state_hash.to_vec());
|
||||
let rows_result = match query.pubkey() {
|
||||
std::option::Option::Some(pubkey) => {
|
||||
let pubkey_bytes: &[u8] = pubkey.as_ref();
|
||||
let sql = match query.direction() {
|
||||
ksp_store_api::RawSortDirection::Ascending => LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL,
|
||||
ksp_store_api::RawSortDirection::Descending => LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL,
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(
|
||||
crate::PostgresBackendErrorKind::QueryInvalid,
|
||||
"raw_account_list_direction",
|
||||
));
|
||||
},
|
||||
};
|
||||
client.query(sql, &[&pubkey_bytes, &start_text, &end_text, &cursor_slot_text, &cursor_pubkey, &cursor_state_hash, &sql_limit]).await
|
||||
},
|
||||
std::option::Option::None => {
|
||||
let sql = match query.direction() {
|
||||
ksp_store_api::RawSortDirection::Ascending => LIST_ACCOUNT_STATES_ASC_SQL,
|
||||
ksp_store_api::RawSortDirection::Descending => LIST_ACCOUNT_STATES_DESC_SQL,
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(
|
||||
crate::PostgresBackendErrorKind::QueryInvalid,
|
||||
"raw_account_list_direction",
|
||||
));
|
||||
},
|
||||
};
|
||||
client.query(sql, &[&start_text, &end_text, &cursor_slot_text, &cursor_pubkey, &cursor_state_hash, &sql_limit]).await
|
||||
},
|
||||
};
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_list_query"));
|
||||
},
|
||||
};
|
||||
let mut decoded = std::vec::Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let physical = match raw_account_list_db_row(&row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let item = match decode_raw_account_list_row(network, physical) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
decoded.push(item);
|
||||
}
|
||||
let has_more = decoded.len() > requested_usize;
|
||||
if has_more {
|
||||
decoded.truncate(requested_usize);
|
||||
}
|
||||
let next_cursor = if has_more {
|
||||
let last = match decoded.last() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_list_page")),
|
||||
};
|
||||
match crate::encode_raw_account_cursor(query, last.0, last.1.pubkey(), &last.1.state_hash()) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
} else {
|
||||
std::option::Option::None
|
||||
};
|
||||
let items = decoded.into_iter().map(|value| return value.1).collect();
|
||||
return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor));
|
||||
}
|
||||
|
||||
/// Reads one persisted RAW account observation by producer-owned idempotence key.
|
||||
pub(crate) async fn get_raw_account_observation(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
observation_key: &ksp_store_api::RawObservationKey,
|
||||
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountObservation>, crate::PostgresBackendError> {
|
||||
let client_result = pool.get().await;
|
||||
let client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
|
||||
};
|
||||
let observation_key_bytes: &[u8] = observation_key.as_bytes();
|
||||
let rows_result = client.query(GET_ACCOUNT_OBSERVATION_SQL, &[&observation_key_bytes]).await;
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_observation_query"));
|
||||
},
|
||||
};
|
||||
if rows.is_empty() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
if rows.len() != 1 {
|
||||
return std::result::Result::Err(data_invalid("raw_account_observation_cardinality"));
|
||||
}
|
||||
let row = match rows.first() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_observation_cardinality")),
|
||||
};
|
||||
let physical = match raw_account_observation_db_row(row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded = match decode_raw_account_observation_row(network, physical) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if decoded.observation_key() != *observation_key {
|
||||
return std::result::Result::Err(data_invalid("raw_account_observation_key"));
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::Some(decoded));
|
||||
}
|
||||
|
||||
/// Persists one canonical RAW account state and its acquisition observation atomically.
|
||||
pub(crate) async fn persist_raw_account_acquisition(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
state: ksp_store_api::RawAccountState,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, crate::PostgresBackendError> {
|
||||
let input_result = ensure_acquisition_inputs(network, &state, &observation);
|
||||
if let std::result::Result::Err(error) = input_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let client_result = pool.get().await;
|
||||
let mut client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
|
||||
};
|
||||
let sql_transaction_result = client.transaction().await;
|
||||
let sql_transaction = match sql_transaction_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_acquisition_begin")),
|
||||
};
|
||||
let insert_result = insert_account_state(&sql_transaction, &state).await;
|
||||
let inserted = match insert_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let entity_outcome = if inserted {
|
||||
ksp_store_api::RawEntityWriteOutcome::Inserted
|
||||
} else {
|
||||
let locked_result = load_locked_account_state(&sql_transaction, network, state.reference()).await;
|
||||
let locked = match locked_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(data_invalid("raw_account_acquisition_conflict_missing")),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !raw_account_states_equal(&locked, &state) {
|
||||
return std::result::Result::Err(conflict("raw_account_acquisition_content_conflict"));
|
||||
}
|
||||
ksp_store_api::RawEntityWriteOutcome::AlreadyPresent
|
||||
};
|
||||
let observation_result = persist_account_observation_row(&sql_transaction, network, &observation).await;
|
||||
let observation_outcome = match observation_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let commit_result = sql_transaction.commit().await;
|
||||
if commit_result.is_err() {
|
||||
return std::result::Result::Err(write_failed("raw_account_acquisition_commit"));
|
||||
}
|
||||
return std::result::Result::Ok(ksp_store_api::RawAcquisitionWriteOutcome::new(entity_outcome, observation_outcome));
|
||||
}
|
||||
|
||||
/// Persists one additional acquisition observation for an already durable RAW account state.
|
||||
pub(crate) async fn record_raw_account_observation(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
|
||||
let input_result = ensure_observation_write_input(network, &observation);
|
||||
if let std::result::Result::Err(error) = input_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let client_result = pool.get().await;
|
||||
let mut client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
|
||||
};
|
||||
let sql_transaction_result = client.transaction().await;
|
||||
let sql_transaction = match sql_transaction_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_begin")),
|
||||
};
|
||||
let account = observation.account();
|
||||
let account_pubkey_bytes: &[u8] = account.pubkey().as_ref();
|
||||
let account_slot_text = account.slot().to_string();
|
||||
let account_state_hash = account.state_hash();
|
||||
let account_state_hash_bytes: &[u8] = account_state_hash.as_bytes();
|
||||
let reference_result = sql_transaction
|
||||
.query_opt(LOCK_ACCOUNT_REFERENCE_SQL, &[&account_pubkey_bytes, &account_slot_text.as_str(), &account_state_hash_bytes])
|
||||
.await;
|
||||
match reference_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(_)) => {},
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(reference_not_found("raw_account_observation_reference"));
|
||||
},
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_lock_reference")),
|
||||
}
|
||||
let observation_result = persist_account_observation_row(&sql_transaction, network, &observation).await;
|
||||
let outcome = match observation_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let commit_result = sql_transaction.commit().await;
|
||||
if commit_result.is_err() {
|
||||
return std::result::Result::Err(write_failed("raw_account_observation_commit"));
|
||||
}
|
||||
return std::result::Result::Ok(outcome);
|
||||
}
|
||||
|
||||
fn raw_account_list_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountListDbRow, crate::PostgresBackendError> {
|
||||
let pubkey = match row.try_get::<_, std::vec::Vec<u8>>("pubkey") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_list_decode")),
|
||||
};
|
||||
let slot_text = match row.try_get::<_, std::string::String>("slot_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_list_decode")),
|
||||
};
|
||||
let state_hash = match row.try_get::<_, std::vec::Vec<u8>>("state_hash") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_list_decode")),
|
||||
};
|
||||
return std::result::Result::Ok(RawAccountListDbRow { pubkey, slot_text, state_hash });
|
||||
}
|
||||
|
||||
fn decode_raw_account_list_row(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
row: RawAccountListDbRow,
|
||||
) -> std::result::Result<(u64, ksp_store_api::RawAccountStateReference), crate::PostgresBackendError> {
|
||||
let pubkey = match fixed_bytes::<32>(row.pubkey, "raw_account_list_pubkey") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot = match decode_u64_decimal(row.slot_text.as_str(), "raw_account_list_slot") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let state_hash = match fixed_bytes::<32>(row.state_hash, "raw_account_list_state_hash") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
|
||||
return std::result::Result::Ok((slot, reference));
|
||||
}
|
||||
|
||||
fn raw_account_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountObservationDbRow, crate::PostgresBackendError> {
|
||||
let account_pubkey: std::vec::Vec<u8> = match row.try_get("account_pubkey") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let account_slot_text: std::string::String = match row.try_get("account_slot_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let account_state_hash: std::vec::Vec<u8> = match row.try_get("account_state_hash") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let acquisition_method: std::string::String = match row.try_get("acquisition_method") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let capture_session_id: std::option::Option<std::string::String> = match row.try_get("capture_session_id") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let commitment: std::option::Option<std::string::String> = match row.try_get("commitment") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let endpoint_id: std::option::Option<std::string::String> = match row.try_get("endpoint_id") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let filter_id: std::option::Option<std::string::String> = match row.try_get("filter_id") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let is_startup: std::option::Option<bool> = match row.try_get("is_startup") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let observation_key: std::vec::Vec<u8> = match row.try_get("observation_key") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let observed_at_unix_millis: std::option::Option<i64> = match row.try_get("observed_at_unix_millis") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let origin: std::string::String = match row.try_get("origin") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let protocol: std::string::String = match row.try_get("protocol") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let provider: std::string::String = match row.try_get("provider") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let received_at_unix_millis: i64 = match row.try_get("received_at_unix_millis") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let source_payload_hash: std::option::Option<std::vec::Vec<u8>> = match row.try_get("source_payload_hash") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let source_payload_size_bytes: std::option::Option<i64> = match row.try_get("source_payload_size_bytes") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let transaction_signature: std::option::Option<std::vec::Vec<u8>> = match row.try_get("transaction_signature") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
let write_version_text: std::option::Option<std::string::String> = match row.try_get("write_version_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
|
||||
};
|
||||
return std::result::Result::Ok(RawAccountObservationDbRow {
|
||||
account_pubkey,
|
||||
account_slot_text,
|
||||
account_state_hash,
|
||||
acquisition_method,
|
||||
capture_session_id,
|
||||
commitment,
|
||||
endpoint_id,
|
||||
filter_id,
|
||||
is_startup,
|
||||
observation_key,
|
||||
observed_at_unix_millis,
|
||||
origin,
|
||||
protocol,
|
||||
provider,
|
||||
received_at_unix_millis,
|
||||
source_payload_hash,
|
||||
source_payload_size_bytes,
|
||||
transaction_signature,
|
||||
write_version_text,
|
||||
});
|
||||
}
|
||||
|
||||
fn raw_account_state_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountStateDbRow, crate::PostgresBackendError> {
|
||||
let data: std::vec::Vec<u8> = match row.try_get("data") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let executable: bool = match row.try_get("executable") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let lamports_text: std::string::String = match row.try_get("lamports_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let owner: std::vec::Vec<u8> = match row.try_get("owner") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let pubkey: std::vec::Vec<u8> = match row.try_get("pubkey") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let rent_epoch_text: std::string::String = match row.try_get("rent_epoch_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let slot_text: std::string::String = match row.try_get("slot_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
let state_hash: std::vec::Vec<u8> = match row.try_get("state_hash") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
|
||||
};
|
||||
return std::result::Result::Ok(RawAccountStateDbRow { data, executable, lamports_text, owner, pubkey, rent_epoch_text, slot_text, state_hash });
|
||||
}
|
||||
|
||||
fn decode_raw_account_observation_row(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
row: RawAccountObservationDbRow,
|
||||
) -> std::result::Result<ksp_store_api::RawAccountObservation, crate::PostgresBackendError> {
|
||||
let observation_key = match fixed_bytes::<32>(row.observation_key, "raw_account_observation_key") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::RawObservationKey::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pubkey = match fixed_bytes::<32>(row.account_pubkey, "raw_account_observation_pubkey") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot = match decode_u64_decimal(row.account_slot_text.as_str(), "raw_account_observation_slot") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let state_hash = match fixed_bytes::<32>(row.account_state_hash, "raw_account_observation_state_hash") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provider = match decode_provenance_code(row.provider) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let protocol = match decode_provenance_code(row.protocol) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let acquisition_method = match decode_provenance_code(row.acquisition_method) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let origin = match decode_origin(row.origin.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let received_at = match decode_timestamp_i64(row.received_at_unix_millis, "raw_account_observation_received_at") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, acquisition_method, origin, received_at);
|
||||
provenance = match row.capture_session_id {
|
||||
std::option::Option::Some(value) => match decode_provenance_code(value) {
|
||||
std::result::Result::Ok(code) => provenance.with_capture_session_id(code),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
provenance = match row.commitment {
|
||||
std::option::Option::Some(value) => match decode_provenance_code(value) {
|
||||
std::result::Result::Ok(code) => provenance.with_commitment(code),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
provenance = match row.endpoint_id {
|
||||
std::option::Option::Some(value) => match decode_provenance_code(value) {
|
||||
std::result::Result::Ok(code) => provenance.with_endpoint_id(code),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
provenance = match row.filter_id {
|
||||
std::option::Option::Some(value) => match decode_provenance_code(value) {
|
||||
std::result::Result::Ok(code) => provenance.with_filter_id(code),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
provenance = match row.observed_at_unix_millis {
|
||||
std::option::Option::Some(value) => {
|
||||
let timestamp = match decode_timestamp_i64(value, "raw_account_observation_observed_at") {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
match provenance.try_with_observed_at(timestamp) {
|
||||
std::result::Result::Ok(updated) => updated,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_time_order")),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
provenance = match row.source_payload_hash {
|
||||
std::option::Option::Some(value) => {
|
||||
let hash = match fixed_bytes::<32>(value, "raw_account_observation_source_hash") {
|
||||
std::result::Result::Ok(decoded) => ksp_store_api::RawContentHash::new(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
provenance.with_source_payload_hash(hash)
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
provenance = match row.source_payload_size_bytes {
|
||||
std::option::Option::Some(value) => {
|
||||
let size = match decode_u64_i64(value, "raw_account_observation_source_size") {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
match provenance.try_with_source_payload_size_bytes(size) {
|
||||
std::result::Result::Ok(updated) => updated,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_source_size")),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => provenance,
|
||||
};
|
||||
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
|
||||
let mut observation = ksp_store_api::RawAccountObservation::new(observation_key, reference, provenance);
|
||||
observation = match row.is_startup {
|
||||
std::option::Option::Some(value) => observation.with_is_startup(value),
|
||||
std::option::Option::None => observation,
|
||||
};
|
||||
observation = match row.transaction_signature {
|
||||
std::option::Option::Some(value) => {
|
||||
let signature = match fixed_bytes::<64>(value, "raw_account_observation_transaction_signature") {
|
||||
std::result::Result::Ok(decoded) => ksp_store_api::RawTransactionSignature::new(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
observation.with_transaction_signature(signature)
|
||||
},
|
||||
std::option::Option::None => observation,
|
||||
};
|
||||
observation = match row.write_version_text {
|
||||
std::option::Option::Some(value) => match decode_u64_decimal(value.as_str(), "raw_account_observation_write_version") {
|
||||
std::result::Result::Ok(decoded) => observation.with_write_version(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => observation,
|
||||
};
|
||||
return std::result::Result::Ok(observation);
|
||||
}
|
||||
|
||||
fn decode_raw_account_state_row(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
row: RawAccountStateDbRow,
|
||||
) -> std::result::Result<ksp_store_api::RawAccountState, crate::PostgresBackendError> {
|
||||
let pubkey = match fixed_bytes::<32>(row.pubkey, "raw_account_state_pubkey") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot = match decode_u64_decimal(row.slot_text.as_str(), "raw_account_state_slot") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let state_hash = match fixed_bytes::<32>(row.state_hash, "raw_account_state_hash") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let lamports = match decode_u64_decimal(row.lamports_text.as_str(), "raw_account_state_lamports") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = match fixed_bytes::<32>(row.owner, "raw_account_state_owner") {
|
||||
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rent_epoch = match decode_u64_decimal(row.rent_epoch_text.as_str(), "raw_account_state_rent_epoch") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
|
||||
return match ksp_store_api::RawAccountState::try_new(reference, lamports, owner, row.executable, rent_epoch, row.data.into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_state_model")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn insert_account_state(
|
||||
sql_transaction: &deadpool_postgres::Transaction<'_>,
|
||||
state: &ksp_store_api::RawAccountState,
|
||||
) -> std::result::Result<bool, crate::PostgresBackendError> {
|
||||
let reference = state.reference();
|
||||
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
|
||||
let slot_text = reference.slot().to_string();
|
||||
let state_hash = reference.state_hash();
|
||||
let state_hash_bytes: &[u8] = state_hash.as_bytes();
|
||||
let lamports_text = state.lamports().to_string();
|
||||
let owner_bytes: &[u8] = state.owner().as_ref();
|
||||
let executable = state.executable();
|
||||
let rent_epoch_text = state.rent_epoch().to_string();
|
||||
let data = state.data();
|
||||
let row_result = sql_transaction
|
||||
.query_opt(
|
||||
INSERT_ACCOUNT_STATE_SQL,
|
||||
&[&pubkey_bytes, &slot_text.as_str(), &state_hash_bytes, &lamports_text.as_str(), &owner_bytes, &executable, &rent_epoch_text.as_str(), &data],
|
||||
)
|
||||
.await;
|
||||
return match row_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(_)) => std::result::Result::Ok(true),
|
||||
std::result::Result::Ok(std::option::Option::None) => std::result::Result::Ok(false),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(write_failed("raw_account_acquisition_insert_state")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn load_locked_account_state(
|
||||
sql_transaction: &deadpool_postgres::Transaction<'_>,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
reference: &ksp_store_api::RawAccountStateReference,
|
||||
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
|
||||
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
|
||||
let slot_text = reference.slot().to_string();
|
||||
let state_hash = reference.state_hash();
|
||||
let state_hash_bytes: &[u8] = state_hash.as_bytes();
|
||||
let row_result = sql_transaction.query_opt(LOCK_ACCOUNT_STATE_SQL, &[&pubkey_bytes, &slot_text.as_str(), &state_hash_bytes]).await;
|
||||
let row = match row_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Ok(std::option::Option::None),
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_acquisition_lock_state")),
|
||||
};
|
||||
let physical = match raw_account_state_db_row(&row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match decode_raw_account_state_row(network, physical) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
async fn persist_account_observation_row(
|
||||
sql_transaction: &deadpool_postgres::Transaction<'_>,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
observation: &ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
|
||||
let provenance = observation.provenance();
|
||||
let observation_key = observation.observation_key();
|
||||
let observation_key_bytes: &[u8] = observation_key.as_bytes();
|
||||
let account = observation.account();
|
||||
let account_pubkey_bytes: &[u8] = account.pubkey().as_ref();
|
||||
let account_slot_text = account.slot().to_string();
|
||||
let account_state_hash = account.state_hash();
|
||||
let account_state_hash_bytes: &[u8] = account_state_hash.as_bytes();
|
||||
let origin = match encode_origin(provenance.origin()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let received_at = match i64::try_from(provenance.received_at().unix_millis()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_received_at_encode")),
|
||||
};
|
||||
let observed_at = match provenance.observed_at() {
|
||||
std::option::Option::Some(value) => match i64::try_from(value.unix_millis()) {
|
||||
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_observed_at_encode")),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let source_payload_size = match provenance.source_payload_size_bytes() {
|
||||
std::option::Option::Some(value) => match i64::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_source_size_encode")),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let capture_session_id = provenance.capture_session_id().map(|value| return value.as_str());
|
||||
let commitment = provenance.commitment().map(|value| return value.as_str());
|
||||
let endpoint_id = provenance.endpoint_id().map(|value| return value.as_str());
|
||||
let filter_id = provenance.filter_id().map(|value| return value.as_str());
|
||||
let source_payload_hash = provenance.source_payload_hash();
|
||||
let source_payload_hash_bytes: std::option::Option<&[u8]> = source_payload_hash.as_ref().map(|value| return &value.as_bytes()[..]);
|
||||
let is_startup = observation.is_startup();
|
||||
let transaction_signature = observation.transaction_signature();
|
||||
let transaction_signature_bytes: std::option::Option<&[u8]> = transaction_signature.as_ref().map(|value| return &value.as_bytes()[..]);
|
||||
let write_version_text = observation.write_version().map(|value| return value.to_string());
|
||||
let row_result = sql_transaction
|
||||
.query_opt(
|
||||
INSERT_ACCOUNT_OBSERVATION_SQL,
|
||||
&[
|
||||
&observation_key_bytes,
|
||||
&account_pubkey_bytes,
|
||||
&account_slot_text.as_str(),
|
||||
&account_state_hash_bytes,
|
||||
&provenance.provider().as_str(),
|
||||
&provenance.protocol().as_str(),
|
||||
&provenance.acquisition_method().as_str(),
|
||||
&origin,
|
||||
&received_at,
|
||||
&capture_session_id,
|
||||
&commitment,
|
||||
&endpoint_id,
|
||||
&filter_id,
|
||||
&observed_at,
|
||||
&source_payload_hash_bytes,
|
||||
&source_payload_size,
|
||||
&is_startup,
|
||||
&transaction_signature_bytes,
|
||||
&write_version_text,
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let inserted = match row_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(_)) => true,
|
||||
std::result::Result::Ok(std::option::Option::None) => false,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_insert")),
|
||||
};
|
||||
if inserted {
|
||||
return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::Inserted);
|
||||
}
|
||||
let existing_result = sql_transaction.query_opt(LOCK_ACCOUNT_OBSERVATION_SQL, &[&observation_key_bytes]).await;
|
||||
let existing_row = match existing_result {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(data_invalid("raw_account_observation_conflict_missing")),
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_conflict_query")),
|
||||
};
|
||||
let physical = match raw_account_observation_db_row(&existing_row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let stored = match decode_raw_account_observation_row(network, physical) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if stored.eq(observation) {
|
||||
return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::AlreadyPresent);
|
||||
}
|
||||
return std::result::Result::Err(conflict("raw_account_observation_content_conflict"));
|
||||
}
|
||||
|
||||
fn ensure_acquisition_inputs(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
state: &ksp_store_api::RawAccountState,
|
||||
observation: &ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let state_network_result = ensure_network(network, state.reference(), "raw_account_acquisition_state_network");
|
||||
if let std::result::Result::Err(error) = state_network_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let observation_network_result = ensure_network(network, observation.account(), "raw_account_acquisition_observation_network");
|
||||
if let std::result::Result::Err(error) = observation_network_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if observation.account() != state.reference() {
|
||||
return std::result::Result::Err(conflict("raw_account_acquisition_reference_mismatch"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn ensure_observation_write_input(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
observation: &ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
return ensure_network(network, observation.account(), "raw_account_observation_write_network");
|
||||
}
|
||||
|
||||
fn raw_account_states_equal(left: &ksp_store_api::RawAccountState, right: &ksp_store_api::RawAccountState) -> bool {
|
||||
return left.reference() == right.reference()
|
||||
&& left.lamports() == right.lamports()
|
||||
&& left.owner() == right.owner()
|
||||
&& left.executable() == right.executable()
|
||||
&& left.rent_epoch() == right.rent_epoch()
|
||||
&& left.data() == right.data();
|
||||
}
|
||||
|
||||
fn encode_origin(origin: ksp_store_api::RawAcquisitionOrigin) -> std::result::Result<&'static str, crate::PostgresBackendError> {
|
||||
return match origin {
|
||||
ksp_store_api::RawAcquisitionOrigin::Backfill => std::result::Result::Ok("backfill"),
|
||||
ksp_store_api::RawAcquisitionOrigin::Import => std::result::Result::Ok("import"),
|
||||
ksp_store_api::RawAcquisitionOrigin::Live => std::result::Result::Ok("live"),
|
||||
ksp_store_api::RawAcquisitionOrigin::Repair => std::result::Result::Ok("repair"),
|
||||
ksp_store_api::RawAcquisitionOrigin::Replay => std::result::Result::Ok("replay"),
|
||||
_ => std::result::Result::Err(data_invalid("raw_account_observation_origin_encode")),
|
||||
};
|
||||
}
|
||||
|
||||
fn conflict(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::Conflict, phase);
|
||||
}
|
||||
|
||||
fn decode_origin(value: &str) -> std::result::Result<ksp_store_api::RawAcquisitionOrigin, crate::PostgresBackendError> {
|
||||
return match value {
|
||||
"backfill" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Backfill),
|
||||
"import" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Import),
|
||||
"live" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Live),
|
||||
"repair" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Repair),
|
||||
"replay" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Replay),
|
||||
_ => std::result::Result::Err(data_invalid("raw_account_observation_origin")),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_provenance_code(value: std::string::String) -> std::result::Result<ksp_store_api::RawProvenanceCode, crate::PostgresBackendError> {
|
||||
return match ksp_store_api::RawProvenanceCode::new(value) {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_provenance_code")),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_timestamp_i64(value: i64, phase: &'static str) -> std::result::Result<ksp_store_api::RawTimestamp, crate::PostgresBackendError> {
|
||||
let unsigned = match u64::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid(phase)),
|
||||
};
|
||||
return match ksp_store_api::RawTimestamp::from_unix_millis(unsigned) {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_u64_decimal(value: &str, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> {
|
||||
return match value.parse::<u64>() {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_u64_i64(value: i64, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> {
|
||||
return match u64::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
|
||||
};
|
||||
}
|
||||
|
||||
fn fixed_bytes<const N: usize>(value: std::vec::Vec<u8>, phase: &'static str) -> std::result::Result<[u8; N], crate::PostgresBackendError> {
|
||||
return match <[u8; N]>::try_from(value.as_slice()) {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
|
||||
};
|
||||
}
|
||||
|
||||
fn ensure_network(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
reference: &ksp_store_api::RawAccountStateReference,
|
||||
phase: &'static str,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if reference.network() != network {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, phase));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn data_invalid(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::DataInvalid, phase);
|
||||
}
|
||||
|
||||
fn reference_not_found(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReferenceNotFound, phase);
|
||||
}
|
||||
|
||||
fn write_failed(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/raw_account.rs"]
|
||||
mod tests;
|
||||
230
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
Normal file
230
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
|
||||
// version: 1
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
const CURSOR_BYTES: usize = 109;
|
||||
const CURSOR_DIGEST_OFFSET: usize = 77;
|
||||
const CURSOR_DOMAIN: &[u8] = b"KSP/raw-account-state-cursor/v1";
|
||||
const CURSOR_MAGIC: &[u8; 4] = b"KSPA";
|
||||
const CURSOR_VERSION: u8 = 1;
|
||||
const MAX_POSTGRES_PAGE_LIMIT: u64 = 9_223_372_036_854_775_806;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
/// Decoded backend-private continuation key extracted from one validated RAW account cursor.
|
||||
pub(crate) struct RawAccountDecodedCursor {
|
||||
/// Last canonical account pubkey returned by the previous page.
|
||||
pub(crate) last_pubkey: [u8; 32],
|
||||
/// Last canonical account-state hash returned by the previous page.
|
||||
pub(crate) last_state_hash: [u8; 32],
|
||||
/// Last canonical account slot returned by the previous page.
|
||||
pub(crate) last_slot: u64,
|
||||
}
|
||||
|
||||
/// Decodes and validates one backend-private RAW account cursor against its query binding.
|
||||
pub(crate) fn decode_raw_account_cursor(
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
cursor: &ksp_store_api::RawPageCursor,
|
||||
) -> std::result::Result<RawAccountDecodedCursor, crate::PostgresBackendError> {
|
||||
let bytes = cursor.as_bytes();
|
||||
if bytes.len() != CURSOR_BYTES {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_size"));
|
||||
}
|
||||
if bytes.get(0..4) != std::option::Option::Some(CURSOR_MAGIC.as_ref()) {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_magic"));
|
||||
}
|
||||
if bytes.get(4).copied() != std::option::Option::Some(CURSOR_VERSION) {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_version"));
|
||||
}
|
||||
let slot_bytes = match bytes.get(5..13) {
|
||||
std::option::Option::Some(value) => match <[u8; 8]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_slot")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_slot")),
|
||||
};
|
||||
let last_slot = u64::from_be_bytes(slot_bytes);
|
||||
let last_pubkey = match bytes.get(13..45) {
|
||||
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
|
||||
};
|
||||
let last_state_hash = match bytes.get(45..CURSOR_DIGEST_OFFSET) {
|
||||
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_state_hash")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_state_hash")),
|
||||
};
|
||||
let range_result = validate_slot_in_range(query.slots(), last_slot);
|
||||
if let std::result::Result::Err(error) = range_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let pubkey_result = validate_pubkey_filter(query.pubkey(), &last_pubkey);
|
||||
if let std::result::Result::Err(error) = pubkey_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let expected_digest = match binding_digest(query, last_slot, &last_pubkey, &last_state_hash) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let stored_digest = match bytes.get(CURSOR_DIGEST_OFFSET..CURSOR_BYTES) {
|
||||
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_digest")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_digest")),
|
||||
};
|
||||
if stored_digest != expected_digest {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_binding"));
|
||||
}
|
||||
return std::result::Result::Ok(RawAccountDecodedCursor { last_pubkey, last_state_hash, last_slot });
|
||||
}
|
||||
|
||||
/// Encodes one backend-private RAW account cursor bound to the supplied query context.
|
||||
pub(crate) fn encode_raw_account_cursor(
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
last_slot: u64,
|
||||
last_pubkey: &ksp_store_api::Pubkey,
|
||||
last_state_hash: &ksp_store_api::RawContentHash,
|
||||
) -> std::result::Result<ksp_store_api::RawPageCursor, crate::PostgresBackendError> {
|
||||
let range_result = validate_slot_in_range(query.slots(), last_slot);
|
||||
if let std::result::Result::Err(error) = range_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let pubkey_bytes = match <[u8; 32]>::try_from(last_pubkey.as_ref()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
|
||||
};
|
||||
let pubkey_result = validate_pubkey_filter(query.pubkey(), &pubkey_bytes);
|
||||
if let std::result::Result::Err(error) = pubkey_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let digest = match binding_digest(query, last_slot, &pubkey_bytes, last_state_hash.as_bytes()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut bytes = std::vec::Vec::with_capacity(CURSOR_BYTES);
|
||||
bytes.extend_from_slice(CURSOR_MAGIC);
|
||||
bytes.push(CURSOR_VERSION);
|
||||
bytes.extend_from_slice(&last_slot.to_be_bytes());
|
||||
bytes.extend_from_slice(&pubkey_bytes);
|
||||
bytes.extend_from_slice(last_state_hash.as_bytes());
|
||||
bytes.extend_from_slice(&digest);
|
||||
if bytes.len() != CURSOR_BYTES {
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_encode_size"));
|
||||
}
|
||||
return match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(query_invalid("raw_account_cursor_encode")),
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts one caller page size into the exact PostgreSQL LIMIT+1 representation.
|
||||
pub(crate) fn raw_account_physical_page_limit(requested: u64) -> std::result::Result<(usize, i64), crate::PostgresBackendError> {
|
||||
if requested > MAX_POSTGRES_PAGE_LIMIT {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
|
||||
}
|
||||
let requested_usize = match usize::try_from(requested) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
|
||||
},
|
||||
};
|
||||
let requested_plus_one = match requested.checked_add(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
|
||||
},
|
||||
};
|
||||
let sql_limit = match i64::try_from(requested_plus_one) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok((requested_usize, sql_limit));
|
||||
}
|
||||
|
||||
fn binding_digest(
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
last_slot: u64,
|
||||
last_pubkey: &[u8; 32],
|
||||
last_state_hash: &[u8; 32],
|
||||
) -> std::result::Result<[u8; 32], crate::PostgresBackendError> {
|
||||
let direction = match query.direction() {
|
||||
ksp_store_api::RawSortDirection::Ascending => 0_u8,
|
||||
ksp_store_api::RawSortDirection::Descending => 1_u8,
|
||||
_ => return std::result::Result::Err(query_invalid("raw_account_cursor_direction")),
|
||||
};
|
||||
let network_len = match u64::try_from(query.network().as_str().len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_network")),
|
||||
};
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(CURSOR_DOMAIN);
|
||||
hasher.update(network_len.to_be_bytes());
|
||||
hasher.update(query.network().as_str().as_bytes());
|
||||
hash_optional_pubkey(&mut hasher, query.pubkey());
|
||||
hasher.update([direction]);
|
||||
hash_optional_slot(&mut hasher, query.slots().start_inclusive());
|
||||
hash_optional_slot(&mut hasher, query.slots().end_inclusive());
|
||||
hasher.update(last_slot.to_be_bytes());
|
||||
hasher.update(last_pubkey);
|
||||
hasher.update(last_state_hash);
|
||||
let finalized = hasher.finalize();
|
||||
let mut digest = [0_u8; 32];
|
||||
digest.copy_from_slice(finalized.as_ref());
|
||||
return std::result::Result::Ok(digest);
|
||||
}
|
||||
|
||||
fn hash_optional_pubkey(hasher: &mut sha2::Sha256, value: std::option::Option<&ksp_store_api::Pubkey>) {
|
||||
match value {
|
||||
std::option::Option::Some(pubkey) => {
|
||||
hasher.update([1_u8]);
|
||||
hasher.update(pubkey.as_ref());
|
||||
},
|
||||
std::option::Option::None => hasher.update([0_u8]),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn hash_optional_slot(hasher: &mut sha2::Sha256, value: std::option::Option<u64>) {
|
||||
match value {
|
||||
std::option::Option::Some(slot) => {
|
||||
hasher.update([1_u8]);
|
||||
hasher.update(slot.to_be_bytes());
|
||||
},
|
||||
std::option::Option::None => hasher.update([0_u8]),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn query_invalid(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, phase);
|
||||
}
|
||||
|
||||
fn validate_pubkey_filter(filter: std::option::Option<&ksp_store_api::Pubkey>, last_pubkey: &[u8; 32]) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if let std::option::Option::Some(pubkey) = filter
|
||||
&& pubkey.as_ref() != last_pubkey.as_slice()
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey_filter"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_slot_in_range(range: ksp_store_api::RawSlotRange, slot: u64) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if let std::option::Option::Some(start) = range.start_inclusive()
|
||||
&& slot < start
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_slot_range"));
|
||||
}
|
||||
if let std::option::Option::Some(end) = range.end_inclusive()
|
||||
&& slot > end
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_account_cursor_slot_range"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||
// version: 10
|
||||
// version: 15
|
||||
|
||||
const APPLICATION_NAME: &str = "ksp-store";
|
||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||
@@ -332,6 +332,47 @@ impl PostgresBackend {
|
||||
return crate::probe_health(&self.pool).await;
|
||||
}
|
||||
|
||||
/// Reads one complete canonical RAW account state without exposing physical PostgreSQL row types.
|
||||
pub async fn get_raw_account_state(
|
||||
&self,
|
||||
reference: &ksp_store_api::RawAccountStateReference,
|
||||
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
|
||||
return crate::get_raw_account_state(&self.pool, &self.network, reference).await;
|
||||
}
|
||||
|
||||
/// Lists deterministic canonical RAW account-state references with a backend-owned opaque continuation cursor.
|
||||
pub async fn list_raw_account_states(
|
||||
&self,
|
||||
query: &ksp_store_api::RawAccountStateQuery,
|
||||
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>, crate::PostgresBackendError> {
|
||||
return crate::list_raw_account_states(&self.pool, &self.network, query).await;
|
||||
}
|
||||
|
||||
/// Reads one persisted RAW account observation by producer-owned idempotence key.
|
||||
pub async fn get_raw_account_observation(
|
||||
&self,
|
||||
observation_key: &ksp_store_api::RawObservationKey,
|
||||
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountObservation>, crate::PostgresBackendError> {
|
||||
return crate::get_raw_account_observation(&self.pool, &self.network, observation_key).await;
|
||||
}
|
||||
|
||||
/// Persists one complete RAW account state and its acquisition observation atomically.
|
||||
pub async fn persist_raw_account_acquisition(
|
||||
&self,
|
||||
state: ksp_store_api::RawAccountState,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, crate::PostgresBackendError> {
|
||||
return crate::persist_raw_account_acquisition(&self.pool, &self.network, state, observation).await;
|
||||
}
|
||||
|
||||
/// Persists one additional RAW account observation for an already durable account state.
|
||||
pub async fn record_raw_account_observation(
|
||||
&self,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
|
||||
return crate::record_raw_account_observation(&self.pool, &self.network, observation).await;
|
||||
}
|
||||
|
||||
/// Reads one canonical RAW transaction without exposing physical PostgreSQL row types.
|
||||
pub async fn get_raw_transaction(
|
||||
&self,
|
||||
@@ -428,6 +469,65 @@ impl std::fmt::Debug for PostgresBackend {
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawAccountObservationRead for PostgresBackend {
|
||||
fn get_raw_account_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::RawAccountObservation>>> {
|
||||
return std::boxed::Box::pin(async move {
|
||||
let result = PostgresBackend::get_raw_account_observation(self, observation_key).await;
|
||||
return result.map_err(map_capability_error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawAccountObservationWrite for PostgresBackend {
|
||||
fn record_raw_account_observation<'a>(
|
||||
&'a self,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> 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_account_observation(self, observation).await;
|
||||
return result.map_err(map_capability_error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawAccountStateRead for PostgresBackend {
|
||||
fn get_raw_account_state<'a>(
|
||||
&'a self,
|
||||
reference: &'a ksp_store_api::RawAccountStateReference,
|
||||
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawAccountState>>> {
|
||||
return std::boxed::Box::pin(async move {
|
||||
let result = PostgresBackend::get_raw_account_state(self, reference).await;
|
||||
return result.map_err(map_capability_error);
|
||||
});
|
||||
}
|
||||
|
||||
fn list_raw_account_states<'a>(
|
||||
&'a self,
|
||||
query: &'a ksp_store_api::RawAccountStateQuery,
|
||||
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>>> {
|
||||
return std::boxed::Box::pin(async move {
|
||||
let result = PostgresBackend::list_raw_account_states(self, query).await;
|
||||
return result.map_err(map_capability_error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawAccountStateWrite for PostgresBackend {
|
||||
fn persist_raw_account_acquisition<'a>(
|
||||
&'a self,
|
||||
state: ksp_store_api::RawAccountState,
|
||||
observation: ksp_store_api::RawAccountObservation,
|
||||
) -> 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_account_acquisition(self, state, observation).await;
|
||||
return result.map_err(map_capability_error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_store_api::RawTransactionRead for PostgresBackend {
|
||||
fn get_raw_transaction<'a>(
|
||||
&'a self,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/schema.rs
|
||||
// version: 7
|
||||
// version: 9
|
||||
|
||||
/// Immutable V000 physical schema resource inventory.
|
||||
pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource {
|
||||
@@ -370,7 +370,7 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
|
||||
access_method: "btree",
|
||||
key_fragment: "slot,signature",
|
||||
name: "ix_ksp_raw_transactions_slot_signature",
|
||||
predicate_fragment: "retention_state<>'purged'",
|
||||
predicate_fragment: std::option::Option::Some("retention_state<>'purged'"),
|
||||
table: "ksp_raw_transactions",
|
||||
unique: false,
|
||||
}),
|
||||
@@ -378,6 +378,308 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
|
||||
sql: include_str!("../migrations/v001_raw_transaction/indexes/001_ix_ksp_raw_transactions_slot_signature.sql"),
|
||||
},
|
||||
];
|
||||
/// Final V002 physical schema resource inventory for RAW account state persistence.
|
||||
pub(crate) const V002_RESOURCES: &[SchemaResource] = &[
|
||||
SchemaResource {
|
||||
id: "tables/001_ksp_raw_account_states.sql",
|
||||
object: SchemaObjectContract::Table(TableContract {
|
||||
columns: RAW_ACCOUNT_STATES_COLUMNS,
|
||||
name: "ksp_raw_account_states",
|
||||
primary_key_columns: std::option::Option::None,
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "tables/002_ksp_raw_account_observations.sql",
|
||||
object: SchemaObjectContract::Table(TableContract {
|
||||
columns: RAW_ACCOUNT_OBSERVATIONS_COLUMNS,
|
||||
name: "ksp_raw_account_observations",
|
||||
primary_key_columns: std::option::Option::None,
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/001_pk_ksp_raw_account_states.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "p", name: "pk_ksp_raw_account_states", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/001_pk_ksp_raw_account_states.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/002_pk_ksp_raw_account_observations.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "p",
|
||||
name: "pk_ksp_raw_account_observations",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/002_pk_ksp_raw_account_observations.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/003_fk_ksp_raw_account_observations_state.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "f",
|
||||
name: "fk_ksp_raw_account_observations_state",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/004_ck_ksp_raw_account_states_pubkey.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_pubkey", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/005_ck_ksp_raw_account_states_slot.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_slot", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/006_ck_ksp_raw_account_states_state_hash.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_states_state_hash",
|
||||
table: "ksp_raw_account_states",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/007_ck_ksp_raw_account_states_lamports.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_lamports", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/008_ck_ksp_raw_account_states_owner.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_owner", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/009_ck_ksp_raw_account_states_rent_epoch.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_states_rent_epoch",
|
||||
table: "ksp_raw_account_states",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/010_ck_ksp_raw_account_states_data.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_data", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/011_ck_ksp_raw_account_observations_key.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_key",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_account_pubkey",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/013_ck_ksp_raw_account_observations_account_slot.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_account_slot",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_account_state_hash",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/015_ck_ksp_raw_account_observations_provider.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_provider",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/016_ck_ksp_raw_account_observations_protocol.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_protocol",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/017_ck_ksp_raw_account_observations_method.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_method",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/018_ck_ksp_raw_account_observations_origin.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_origin",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/019_ck_ksp_raw_account_observations_received_at.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_received_at",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/020_ck_ksp_raw_account_observations_capture_session.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_capture_session",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/021_ck_ksp_raw_account_observations_commitment.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_commitment",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/022_ck_ksp_raw_account_observations_endpoint.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_endpoint",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/023_ck_ksp_raw_account_observations_filter.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_filter",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/024_ck_ksp_raw_account_observations_observed_at.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_observed_at",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/025_ck_ksp_raw_account_observations_time_order.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_time_order",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/026_ck_ksp_raw_account_observations_source_hash.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_source_hash",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/027_ck_ksp_raw_account_observations_source_size.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_source_size",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_transaction_signature",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/029_ck_ksp_raw_account_observations_write_version.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_write_version",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql",
|
||||
object: SchemaObjectContract::Index(IndexContract {
|
||||
access_method: "btree",
|
||||
key_fragment: "slot,pubkey,state_hash",
|
||||
name: "ix_ksp_raw_account_states_slot_pubkey_state_hash",
|
||||
predicate_fragment: std::option::Option::None,
|
||||
table: "ksp_raw_account_states",
|
||||
unique: false,
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql"),
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMN_LOAD_SQL: &str = r#"SELECT column_name::TEXT, udt_name::TEXT, (is_nullable = 'YES') AS nullable, numeric_precision::INTEGER, numeric_scale::INTEGER, column_default::TEXT, is_identity::TEXT, is_generated::TEXT
|
||||
FROM information_schema.columns
|
||||
@@ -404,7 +706,7 @@ WHERE ns.nspname = current_schema() AND table_rel.relname = $1 AND index_rel.rel
|
||||
const MANAGED_OBJECT_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads')
|
||||
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads', 'ksp_raw_account_states', 'ksp_raw_account_observations')
|
||||
AND table_type = 'BASE TABLE'
|
||||
)"#;
|
||||
const PRIMARY_KEY_LOAD_SQL: &str = r#"SELECT string_agg(att.attname::TEXT, ',' ORDER BY key_part.ord)::TEXT
|
||||
@@ -464,7 +766,7 @@ struct IndexContract {
|
||||
access_method: &'static str,
|
||||
key_fragment: &'static str,
|
||||
name: &'static str,
|
||||
predicate_fragment: &'static str,
|
||||
predicate_fragment: std::option::Option<&'static str>,
|
||||
table: &'static str,
|
||||
unique: bool,
|
||||
}
|
||||
@@ -710,6 +1012,199 @@ const RAW_TRANSACTION_OBSERVATIONS_COLUMNS: &[ColumnContract] = &[
|
||||
udt_name: "int8",
|
||||
},
|
||||
];
|
||||
const RAW_ACCOUNT_STATES_COLUMNS: &[ColumnContract] = &[
|
||||
ColumnContract {
|
||||
name: "pubkey",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "slot",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::Some(20),
|
||||
numeric_scale: std::option::Option::Some(0),
|
||||
udt_name: "numeric",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "state_hash",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "lamports",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::Some(20),
|
||||
numeric_scale: std::option::Option::Some(0),
|
||||
udt_name: "numeric",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "owner",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "executable",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bool",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "rent_epoch",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::Some(20),
|
||||
numeric_scale: std::option::Option::Some(0),
|
||||
udt_name: "numeric",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "data",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
];
|
||||
const RAW_ACCOUNT_OBSERVATIONS_COLUMNS: &[ColumnContract] = &[
|
||||
ColumnContract {
|
||||
name: "observation_key",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "account_pubkey",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "account_slot",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::Some(20),
|
||||
numeric_scale: std::option::Option::Some(0),
|
||||
udt_name: "numeric",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "account_state_hash",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "provider",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "protocol",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "acquisition_method",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "origin",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "received_at_unix_millis",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "int8",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "capture_session_id",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "commitment",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "endpoint_id",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "filter_id",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "text",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "observed_at_unix_millis",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "int8",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "source_payload_hash",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "source_payload_size_bytes",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "int8",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "is_startup",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bool",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "transaction_signature",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "bytea",
|
||||
},
|
||||
ColumnContract {
|
||||
name: "write_version",
|
||||
nullable: true,
|
||||
numeric_precision: std::option::Option::Some(20),
|
||||
numeric_scale: std::option::Option::Some(0),
|
||||
udt_name: "numeric",
|
||||
},
|
||||
];
|
||||
const RAW_TRANSACTION_ARCHIVE_PAYLOADS_COLUMNS: &[ColumnContract] = &[
|
||||
ColumnContract {
|
||||
name: "signature",
|
||||
@@ -727,6 +1222,7 @@ const RAW_TRANSACTION_ARCHIVE_PAYLOADS_COLUMNS: &[ColumnContract] = &[
|
||||
},
|
||||
];
|
||||
const V001_TABLE_NAMES: &[&str] = &["ksp_store_identity", "ksp_raw_transactions", "ksp_raw_transaction_observations", "ksp_raw_transaction_archive_payloads"];
|
||||
const V002_TABLE_NAMES: &[&str] = &["ksp_raw_account_states", "ksp_raw_account_observations"];
|
||||
|
||||
/// Inspects one embedded schema resource against the effective PostgreSQL catalog.
|
||||
pub(crate) async fn inspect_resource(
|
||||
@@ -740,8 +1236,8 @@ pub(crate) async fn inspect_resource(
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns whether any V001-managed base table already exists in the active schema.
|
||||
pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
|
||||
/// Returns whether any KSP-managed Store base table already exists in the active schema.
|
||||
pub(crate) async fn managed_schema_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
|
||||
let result = transaction.query_one(MANAGED_OBJECT_EXISTS_SQL, &[]).await;
|
||||
let row = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -757,7 +1253,22 @@ pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::
|
||||
pub(crate) async fn verify_v001_external_compatibility(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
for table in V001_TABLE_NAMES {
|
||||
return verify_external_compatibility(transaction, V001_TABLE_NAMES, V001_RESOURCES).await;
|
||||
}
|
||||
|
||||
/// Rejects external V002 schema extensions that can constrain or mutate KSP writes.
|
||||
pub(crate) async fn verify_v002_external_compatibility(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
return verify_external_compatibility(transaction, V002_TABLE_NAMES, V002_RESOURCES).await;
|
||||
}
|
||||
|
||||
async fn verify_external_compatibility(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
tables: &[&str],
|
||||
resources: &[SchemaResource],
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
for table in tables {
|
||||
let table = *table;
|
||||
let constraint_rows = transaction.query(UNEXPECTED_CONSTRAINTS_SQL, &[&table]).await;
|
||||
let constraint_rows = match constraint_rows {
|
||||
@@ -782,11 +1293,11 @@ pub(crate) async fn verify_v001_external_compatibility(
|
||||
) => (name, kind, validated, deferrable, deferred, definition),
|
||||
_ => return schema_query_error("schema_constraint_inventory_decode"),
|
||||
};
|
||||
if is_expected_constraint(table, name.as_str()) {
|
||||
if is_expected_constraint(resources, table, name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let definition = normalize_catalog_sql(definition.as_str());
|
||||
if !validated || deferrable || deferred || !matches_expected_constraint_definition(table, kind.as_str(), definition.as_str()) {
|
||||
if !validated || deferrable || deferred || !matches_expected_constraint_definition(resources, table, kind.as_str(), definition.as_str()) {
|
||||
return schema_incompatible("schema_external_constraint");
|
||||
}
|
||||
}
|
||||
@@ -887,9 +1398,10 @@ async fn inspect_index(
|
||||
};
|
||||
let definition = normalize_catalog_sql(definition.as_str());
|
||||
let predicate = predicate.map(|value| return normalize_catalog_sql(value.as_str()));
|
||||
let predicate_matches = match predicate.as_deref() {
|
||||
std::option::Option::Some(value) => value.contains(contract.predicate_fragment),
|
||||
std::option::Option::None => false,
|
||||
let predicate_matches = match (contract.predicate_fragment, predicate.as_deref()) {
|
||||
(std::option::Option::Some(expected), std::option::Option::Some(value)) => value.contains(expected),
|
||||
(std::option::Option::None, std::option::Option::None) => true,
|
||||
(std::option::Option::Some(_), std::option::Option::None) | (std::option::Option::None, std::option::Option::Some(_)) => false,
|
||||
};
|
||||
if unique != contract.unique || access_method != contract.access_method || !definition.contains(contract.key_fragment) || !predicate_matches {
|
||||
return std::result::Result::Ok(SchemaResourceState::Incompatible);
|
||||
@@ -1105,8 +1617,8 @@ fn expected_constraint_definition(resource_sql: &str, name: &str) -> std::option
|
||||
return std::option::Option::Some(normalize_catalog_sql(definition));
|
||||
}
|
||||
|
||||
fn is_expected_constraint(table: &str, name: &str) -> bool {
|
||||
for resource in V001_RESOURCES {
|
||||
fn is_expected_constraint(resources: &[SchemaResource], table: &str, name: &str) -> bool {
|
||||
for resource in resources {
|
||||
let contract = match resource.object {
|
||||
SchemaObjectContract::Constraint(value) => value,
|
||||
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,
|
||||
@@ -1118,8 +1630,8 @@ fn is_expected_constraint(table: &str, name: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn matches_expected_constraint_definition(table: &str, kind: &str, definition: &str) -> bool {
|
||||
for resource in V001_RESOURCES {
|
||||
fn matches_expected_constraint_definition(resources: &[SchemaResource], table: &str, kind: &str, definition: &str) -> bool {
|
||||
for resource in resources {
|
||||
let contract = match resource.object {
|
||||
SchemaObjectContract::Constraint(value) => value,
|
||||
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
// version: 14
|
||||
// version: 24
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -23,6 +23,7 @@ fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_faca
|
||||
let bootstrap_sql = include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql");
|
||||
assert!(migration.contains("crate::V000_RESOURCES"));
|
||||
assert!(migration.contains("crate::V001_RESOURCES"));
|
||||
assert!(migration.contains("crate::V002_RESOURCES"));
|
||||
assert!(schema.contains("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql"));
|
||||
assert!(schema.contains("../migrations/v001_raw_transaction/tables/001_ksp_store_identity.sql"));
|
||||
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
|
||||
@@ -42,6 +43,7 @@ fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private()
|
||||
assert!(crate_root.contains("mod runtime;"));
|
||||
assert!(crate_root.contains("mod schema;"));
|
||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for forbidden in [
|
||||
"pub mod ",
|
||||
"ksp_store_lib",
|
||||
@@ -53,7 +55,6 @@ fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private()
|
||||
] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}");
|
||||
}
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for forbidden in [
|
||||
"std::env",
|
||||
"dotenv",
|
||||
@@ -113,7 +114,7 @@ fn pre_003_fix_001_migration_engine_uses_split_schema_contract_and_binds_network
|
||||
assert!(observation_table.contains("CREATE TABLE IF NOT EXISTS ksp_raw_transaction_observations"));
|
||||
assert!(archive_table.contains("CREATE TABLE IF NOT EXISTS ksp_raw_transaction_archive_payloads"));
|
||||
assert!(index.contains("CREATE INDEX IF NOT EXISTS ix_ksp_raw_transactions_slot_signature"));
|
||||
for forbidden in ["impl ksp_store_api::RawTransaction", "repository", "sqlx", "RawAccountState"] {
|
||||
for forbidden in ["impl ksp_store_api::RawTransaction", "impl ksp_store_api::RawAccount", "repository", "sqlx"] {
|
||||
assert!(!migration.contains(forbidden), "repository/cross-scope implementation leaked into migration engine: {forbidden}");
|
||||
assert!(!schema.contains(forbidden), "repository/cross-scope implementation leaked into schema contract: {forbidden}");
|
||||
}
|
||||
@@ -124,6 +125,110 @@ fn pre_003_fix_001_migration_engine_uses_split_schema_contract_and_binds_network
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_v002_schema_is_complete_and_keeps_capability_implementation_out_of_schema_layers() {
|
||||
let migration = include_str!("../src/migration.rs");
|
||||
let schema = include_str!("../src/schema.rs");
|
||||
let states = include_str!("../migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql");
|
||||
let observations = include_str!("../migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql");
|
||||
let slot_check = include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql");
|
||||
let index = include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
|
||||
assert!(migration.contains("name: \"raw_account_state\""));
|
||||
assert!(migration.contains("resources: crate::V002_RESOURCES"));
|
||||
assert!(migration.contains("V002_PROVISIONAL_CHECKSUM_PRE_002"));
|
||||
assert!(migration.contains("HISTORY_UPDATE_CHECKSUM_SQL"));
|
||||
assert!(migration.contains("reconcile_applied_history_checksums"));
|
||||
assert!(schema.contains("pub(crate) const V002_RESOURCES"));
|
||||
assert!(schema.contains("verify_v002_external_compatibility"));
|
||||
assert!(schema.contains("ksp_raw_account_states', 'ksp_raw_account_observations"));
|
||||
assert!(states.contains("ksp_raw_account_states"));
|
||||
assert!(observations.contains("ksp_raw_account_observations"));
|
||||
assert!(slot_check.contains("slot >= 0 AND slot <= 18446744073709551615"));
|
||||
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
|
||||
assert!(!index.contains("WHERE"));
|
||||
assert!(!migration.contains("impl ksp_store_api::RawAccount"));
|
||||
assert!(!schema.contains("impl ksp_store_api::RawAccount"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_trait_impls_out_of_sql_module() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
let raw = include_str!("../src/raw_account.rs");
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
assert!(crate_root.contains("mod raw_account;"));
|
||||
assert!(!crate_root.contains("pub mod raw_account"));
|
||||
for required in [
|
||||
"GET_ACCOUNT_STATE_SQL",
|
||||
"GET_ACCOUNT_OBSERVATION_SQL",
|
||||
"INSERT_ACCOUNT_STATE_SQL",
|
||||
"INSERT_ACCOUNT_OBSERVATION_SQL",
|
||||
"LOCK_ACCOUNT_STATE_SQL",
|
||||
"LOCK_ACCOUNT_OBSERVATION_SQL",
|
||||
"ON CONFLICT (pubkey, slot, state_hash) DO NOTHING",
|
||||
"ON CONFLICT (observation_key) DO NOTHING",
|
||||
"FOR UPDATE",
|
||||
"persist_raw_account_acquisition",
|
||||
"raw_account_states_equal",
|
||||
"RawAccountState::try_new",
|
||||
"RawAccountObservation::new",
|
||||
"PostgresBackendErrorKind::Conflict",
|
||||
"PostgresBackendErrorKind::DataInvalid",
|
||||
"PostgresBackendErrorKind::WrongNetwork",
|
||||
"PostgresBackendErrorKind::WriteFailed",
|
||||
] {
|
||||
assert!(raw.contains(required), "missing private RAW account acquisition contract: {required}");
|
||||
}
|
||||
assert!(runtime.contains("pub async fn persist_raw_account_acquisition"));
|
||||
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET "] {
|
||||
assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}");
|
||||
}
|
||||
for implementation 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!(!raw.contains(implementation), "account capability implementation leaked into SQL/mapping module: {implementation}");
|
||||
assert_eq!(runtime.matches(implementation).count(), 1, "missing or duplicated account runtime bridge implementation: {implementation}");
|
||||
}
|
||||
for forbidden in ["std::env", "dotenv", "ksp_store_lib", "ksp_config_lib", "sqlx::", "SELECT *"] {
|
||||
assert!(!raw.contains(forbidden), "RAW account module contains forbidden ownership/query material: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_raw_account_additional_observation_is_reference_guarded_cancellation_safe_and_keeps_destructive_scope_closed() {
|
||||
let raw = include_str!("../src/raw_account.rs");
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for required in [
|
||||
"LOCK_ACCOUNT_REFERENCE_SQL",
|
||||
"FOR KEY SHARE",
|
||||
"record_raw_account_observation",
|
||||
"reference_not_found(\"raw_account_observation_reference\")",
|
||||
"persist_account_observation_row",
|
||||
"RawObservationWriteOutcome",
|
||||
"raw_account_observation_commit",
|
||||
] {
|
||||
assert!(raw.contains(required), "missing pre.006 additional-observation contract: {required}");
|
||||
}
|
||||
assert!(runtime.contains("pub async fn record_raw_account_observation"));
|
||||
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET "] {
|
||||
assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}");
|
||||
}
|
||||
for implementation 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!(!raw.contains(implementation), "account capability implementation leaked into SQL/mapping module: {implementation}");
|
||||
assert_eq!(runtime.matches(implementation).count(), 1, "missing or duplicated account runtime bridge implementation: {implementation}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_raw_read_sql_and_mapping_remain_backend_private() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
@@ -217,6 +322,59 @@ fn pre_006_raw_pagination_is_keyset_cursor_bound_and_policy_free() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_raw_account_pagination_is_keyset_cursor_bound_and_policy_free() {
|
||||
let raw = include_str!("../src/raw_account.rs");
|
||||
let cursor = include_str!("../src/raw_account/cursor.rs");
|
||||
let index = include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
|
||||
for required in [
|
||||
"LIST_ACCOUNT_STATES_ASC_SQL",
|
||||
"LIST_ACCOUNT_STATES_DESC_SQL",
|
||||
"LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL",
|
||||
"LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL",
|
||||
"(slot, pubkey, state_hash) >",
|
||||
"(slot, pubkey, state_hash) <",
|
||||
"ORDER BY slot ASC, pubkey ASC, state_hash ASC",
|
||||
"ORDER BY slot DESC, pubkey DESC, state_hash DESC",
|
||||
"list_raw_account_states",
|
||||
] {
|
||||
assert!(raw.contains(required), "missing pre.007 account keyset pagination contract: {required}");
|
||||
}
|
||||
for required in [
|
||||
"CURSOR_BYTES: usize = 109",
|
||||
"b\"KSPA\"",
|
||||
"CURSOR_VERSION: u8 = 1",
|
||||
"KSP/raw-account-state-cursor/v1",
|
||||
"sha2::Sha256",
|
||||
"query.network().as_str()",
|
||||
"query.pubkey()",
|
||||
"query.direction()",
|
||||
"query.slots().start_inclusive()",
|
||||
"query.slots().end_inclusive()",
|
||||
"last_slot",
|
||||
"last_pubkey",
|
||||
"last_state_hash",
|
||||
"PageLimitUnsupported",
|
||||
"9_223_372_036_854_775_806",
|
||||
] {
|
||||
assert!(cursor.contains(required), "missing pre.007 account cursor/binding contract: {required}");
|
||||
}
|
||||
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
|
||||
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000"] {
|
||||
assert!(!raw.contains(forbidden), "pre.007 account source contains forbidden pagination/policy material: {forbidden}");
|
||||
assert!(!cursor.contains(forbidden), "pre.007 account cursor contains forbidden pagination/policy material: {forbidden}");
|
||||
}
|
||||
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!(!raw.contains(forbidden), "account trait implementation leaked into raw account SQL/mapping module: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compaction() {
|
||||
let raw = include_str!("../src/raw_transaction.rs");
|
||||
@@ -250,25 +408,22 @@ fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compactio
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_backend_trait_implementations_stay_in_runtime_bridge_and_raw_account_scope_stays_closed() {
|
||||
fn pre_008_backend_trait_implementations_cover_exact_ten_raw_capabilities_in_runtime_bridge() {
|
||||
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",
|
||||
for implementation in [
|
||||
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionObservationRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
|
||||
] {
|
||||
assert!(!runtime.contains(forbidden), "pre.008 opened RawAccount capability scope prematurely: {forbidden}");
|
||||
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL RAW capability inventory: {implementation}");
|
||||
}
|
||||
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 10);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
// version: 11
|
||||
// version: 17
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -109,7 +109,8 @@ fn assert_pre_io_rejection(connection_uri: &str, tls_mode: ksp_store_postgres_li
|
||||
#[test]
|
||||
fn pre_009_backend_modules_exports_and_manifest_dependencies_are_exact() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod raw_transaction;", "mod runtime;", "mod schema;"] {
|
||||
for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod raw_account;", "mod raw_transaction;", "mod runtime;", "mod schema;"]
|
||||
{
|
||||
assert!(crate_root.contains(required), "missing PostgreSQL backend module: {required}");
|
||||
}
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
@@ -172,6 +173,8 @@ fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
|
||||
runtime,
|
||||
include_str!("../src/migration.rs"),
|
||||
include_str!("../src/health.rs"),
|
||||
include_str!("../src/raw_account.rs"),
|
||||
include_str!("../src/raw_account/cursor.rs"),
|
||||
include_str!("../src/raw_transaction.rs"),
|
||||
include_str!("../src/raw_transaction/cursor.rs"),
|
||||
] {
|
||||
@@ -183,7 +186,7 @@ fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_implementation() {
|
||||
fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_and_keeps_account_impls_in_runtime_bridge() {
|
||||
let production = std::format!(
|
||||
"{}
|
||||
{}
|
||||
@@ -192,11 +195,13 @@ fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_im
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
{}",
|
||||
include_str!("../src/error.rs"),
|
||||
include_str!("../src/health.rs"),
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/migration.rs"),
|
||||
include_str!("../src/raw_account.rs"),
|
||||
include_str!("../src/raw_transaction.rs"),
|
||||
include_str!("../src/raw_transaction/cursor.rs"),
|
||||
include_str!("../src/runtime.rs"),
|
||||
@@ -219,9 +224,8 @@ fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_im
|
||||
"ksp_store_lib",
|
||||
"ksp_config_lib",
|
||||
"sqlx::",
|
||||
"impl ksp_store_api::RawAccount",
|
||||
] {
|
||||
assert!(!production.contains(forbidden), "forbidden backend ownership/reverse-edge/RawAccount material detected: {forbidden}");
|
||||
assert!(!production.contains(forbidden), "forbidden backend ownership/reverse-edge 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"));
|
||||
@@ -231,6 +235,41 @@ fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_im
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_live_raw_account_proof_is_opt_in_isolated_secret_safe_and_cross_family() {
|
||||
let live = include_str!("postgres_raw_account_live.rs");
|
||||
for required in [
|
||||
"#[ignore = \"opt-in real PostgreSQL RawAccountState proof; reads one dedicated URI from stdin\"]",
|
||||
"std::io::stdin().read_line",
|
||||
"managed_schema_preexisting_refusal",
|
||||
"prove_schema_update_policy",
|
||||
"prove_state_extremes",
|
||||
"MAX_RAW_ACCOUNT_DATA_BYTES",
|
||||
"u64::MAX",
|
||||
"prove_atomic_idempotence_and_conflicts",
|
||||
"prove_additional_observations",
|
||||
"ReferenceNotFound",
|
||||
"prove_pagination_and_cursors",
|
||||
"KSPT",
|
||||
"prove_cross_family_coexistence",
|
||||
"persist_raw_transaction_acquisition",
|
||||
"prove_concurrent_identical_insert",
|
||||
"prove_concurrent_divergent_insert",
|
||||
"prove_cancellation_rollback",
|
||||
"task.abort()",
|
||||
"cleanup_verification",
|
||||
"ksp_raw_account_states",
|
||||
"ksp_raw_account_observations",
|
||||
"ksp_raw_transactions",
|
||||
] {
|
||||
assert!(live.contains(required), "missing pre.009 RawAccount live proof guard/scenario: {required}");
|
||||
}
|
||||
for forbidden in ["std::env", "KSP_SECRET_", "PGPASSWORD", "connection_uri = %", "connection_uri = ?", "println!(uri", "eprintln!(uri"] {
|
||||
assert!(!live.contains(forbidden), "pre.009 RawAccount live proof contains forbidden secret/environment material: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
|
||||
let live = include_str!("postgres_raw_transaction_live.rs");
|
||||
@@ -247,6 +286,8 @@ fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
|
||||
"prove_cancellation_rollback",
|
||||
"task.abort()",
|
||||
"cleanup_verification",
|
||||
"ksp_raw_account_states",
|
||||
"ksp_raw_account_observations",
|
||||
] {
|
||||
assert!(live.contains(required), "missing pre.009 live proof guard/scenario: {required}");
|
||||
}
|
||||
@@ -257,29 +298,28 @@ fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_raw_transaction_capability_implementation_inventory_is_exact_and_raw_account_scope_stays_closed() {
|
||||
fn pre_010_raw_capability_implementation_inventory_is_exactly_ten() {
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
let capability_impls = [
|
||||
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionObservationRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend",
|
||||
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
|
||||
];
|
||||
for implementation in capability_impls {
|
||||
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL capability implementation inventory: {implementation}");
|
||||
}
|
||||
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), "RawAccountState scope opened during RawTransaction hardening: {forbidden}");
|
||||
}
|
||||
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 10);
|
||||
let migration = include_str!("../src/migration.rs");
|
||||
assert!(!migration.contains("ksp_raw_account"));
|
||||
assert!(migration.contains("raw_account_state"));
|
||||
assert!(migration.contains("crate::V002_RESOURCES"));
|
||||
assert!(!runtime.contains("mod raw_account"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -302,3 +342,73 @@ fn pre_010_raw_transaction_private_sql_keeps_keyset_navigation_and_bounded_state
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_raw_account_private_sql_is_non_destructive_keyset_and_family_local() {
|
||||
let source = include_str!("../src/raw_account.rs");
|
||||
let cursor = include_str!("../src/raw_account/cursor.rs");
|
||||
for required in [
|
||||
"ON CONFLICT (pubkey, slot, state_hash) DO NOTHING",
|
||||
"ON CONFLICT (observation_key) DO NOTHING",
|
||||
"FOR UPDATE",
|
||||
"FOR KEY SHARE",
|
||||
"ORDER BY slot ASC, pubkey ASC, state_hash ASC",
|
||||
"ORDER BY slot DESC, pubkey DESC, state_hash DESC",
|
||||
"(slot, pubkey, state_hash) >",
|
||||
"(slot, pubkey, state_hash) <",
|
||||
] {
|
||||
assert!(source.contains(required), "required hardened RawAccount SQL contract missing: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
" OFFSET ",
|
||||
"SELECT *",
|
||||
"ON CONFLICT DO UPDATE",
|
||||
"UPDATE ksp_raw_account",
|
||||
"DELETE FROM ksp_raw_account",
|
||||
"TRUNCATE",
|
||||
"batch_size",
|
||||
"priority",
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "forbidden RawAccount scope/policy SQL detected: {forbidden}");
|
||||
}
|
||||
for required in ["b\"KSPA\"", "KSP/raw-account-state-cursor/v1", "CURSOR_BYTES: usize = 109"] {
|
||||
assert!(cursor.contains(required), "missing hardened RawAccount cursor family contract: {required}");
|
||||
}
|
||||
assert!(!cursor.contains("b\"KSPT\""));
|
||||
let state_fk = include_str!("../migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql");
|
||||
let transaction_signature = include_str!("../migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql");
|
||||
assert!(state_fk.contains("REFERENCES ksp_raw_account_states(pubkey, slot, state_hash)"));
|
||||
assert!(!state_fk.contains("ksp_raw_transactions"));
|
||||
assert!(transaction_signature.contains("octet_length(transaction_signature) = 64"));
|
||||
assert!(!transaction_signature.contains("REFERENCES"));
|
||||
assert!(!transaction_signature.contains("ksp_raw_transactions"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_v002_schema_inventory_is_exact_family_local_and_query_justified() {
|
||||
let schema = include_str!("../src/schema.rs");
|
||||
let v002_tail = match schema.split("pub(crate) const V002_RESOURCES: &[SchemaResource] = &[").nth(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("V002 resource inventory missing"),
|
||||
};
|
||||
let v002 = match v002_tail.split("\n];").next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("V002 resource inventory terminator missing"),
|
||||
};
|
||||
assert_eq!(v002.matches("SchemaResource {").count(), 32);
|
||||
assert_eq!(v002.matches("SchemaObjectContract::Table(").count(), 2);
|
||||
assert_eq!(v002.matches("kind: \"p\"").count(), 2);
|
||||
assert_eq!(v002.matches("kind: \"f\"").count(), 1);
|
||||
assert_eq!(v002.matches("kind: \"c\"").count(), 26);
|
||||
assert_eq!(v002.matches("SchemaObjectContract::Index(").count(), 1);
|
||||
assert_eq!(v002.matches("predicate_fragment: std::option::Option::None").count(), 1);
|
||||
assert!(v002.contains("key_fragment: \"slot,pubkey,state_hash\""));
|
||||
assert!(!v002.contains("ksp_raw_transactions"));
|
||||
assert!(!v002.contains("ksp_raw_transaction_observations"));
|
||||
for forbidden in ["owner", "provider", "received_at_unix_millis", "acquisition_method"] {
|
||||
let index_marker = std::format!("key_fragment: \"{forbidden}");
|
||||
assert!(!v002.contains(index_marker.as_str()), "unjustified V002 business index detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -153,7 +153,7 @@ async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str,
|
||||
}
|
||||
*owns_schema = true;
|
||||
let initial_health = initial.health().await;
|
||||
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(1) || initial_health.pending_migration_count() != 0 {
|
||||
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(2) || initial_health.pending_migration_count() != 0 {
|
||||
return std::result::Result::Err(LiveFailure::new("initial_health"));
|
||||
}
|
||||
let initial_close = close_backend(initial).await;
|
||||
@@ -167,7 +167,7 @@ async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str,
|
||||
};
|
||||
let idempotent_health = idempotent.health().await;
|
||||
if !idempotent_health.is_ready()
|
||||
|| idempotent_health.migration_version() != std::option::Option::Some(1)
|
||||
|| idempotent_health.migration_version() != std::option::Option::Some(2)
|
||||
|| idempotent_health.pending_migration_count() != 0
|
||||
{
|
||||
return std::result::Result::Err(LiveFailure::new("idempotent_health"));
|
||||
@@ -250,7 +250,7 @@ async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let final_health = final_backend.health().await;
|
||||
if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(1) || final_health.pending_migration_count() != 0 {
|
||||
if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(2) || final_health.pending_migration_count() != 0 {
|
||||
return std::result::Result::Err(LiveFailure::new("final_health"));
|
||||
}
|
||||
return close_backend(final_backend).await;
|
||||
|
||||
1216
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
Normal file
1216
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
|
||||
// version: 3
|
||||
// version: 5
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -18,7 +18,9 @@ const LIVE_INDEX_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
AND indexname = 'ix_ksp_raw_transactions_slot_signature'
|
||||
)"#;
|
||||
const LIVE_LOCK_OBSERVATION_SQL: &str = "SELECT observation_key FROM ksp_raw_transaction_observations WHERE observation_key = $1 FOR UPDATE";
|
||||
const LIVE_MANAGED_SCHEMA_DROP_SQL: &str = r#"DROP TABLE IF EXISTS ksp_raw_transaction_observations;
|
||||
const LIVE_MANAGED_SCHEMA_DROP_SQL: &str = r#"DROP TABLE IF EXISTS ksp_raw_account_observations;
|
||||
DROP TABLE IF EXISTS ksp_raw_account_states;
|
||||
DROP TABLE IF EXISTS ksp_raw_transaction_observations;
|
||||
DROP TABLE IF EXISTS ksp_raw_transaction_archive_payloads;
|
||||
DROP TABLE IF EXISTS ksp_raw_transactions;
|
||||
DROP TABLE IF EXISTS ksp_store_identity;
|
||||
@@ -31,7 +33,9 @@ const LIVE_MANAGED_SCHEMA_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
'ksp_store_identity',
|
||||
'ksp_raw_transactions',
|
||||
'ksp_raw_transaction_observations',
|
||||
'ksp_raw_transaction_archive_payloads'
|
||||
'ksp_raw_transaction_archive_payloads',
|
||||
'ksp_raw_account_states',
|
||||
'ksp_raw_account_observations'
|
||||
)
|
||||
AND table_type = 'BASE TABLE'
|
||||
)"#;
|
||||
@@ -150,7 +154,7 @@ async fn run_raw_transaction_scenario(admin: &mut tokio_postgres::Client, uri: &
|
||||
};
|
||||
*owns_schema = true;
|
||||
let initial_health = initial.health().await;
|
||||
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(1) || initial_health.pending_migration_count() != 0 {
|
||||
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(2) || initial_health.pending_migration_count() != 0 {
|
||||
return std::result::Result::Err(LiveFailure::new("initial_health"));
|
||||
}
|
||||
let wrong_network_result = open_backend_result(uri, "testnet", true, true).await;
|
||||
@@ -211,7 +215,7 @@ async fn run_raw_transaction_scenario(admin: &mut tokio_postgres::Client, uri: &
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reopened_health = reopened.health().await;
|
||||
if !reopened_health.is_ready() || reopened_health.migration_version() != std::option::Option::Some(1) {
|
||||
if !reopened_health.is_ready() || reopened_health.migration_version() != std::option::Option::Some(2) {
|
||||
return std::result::Result::Err(LiveFailure::new("reopen_health"));
|
||||
}
|
||||
let reference_result = raw_reference(10);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||
// version: 9
|
||||
// version: 14
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -75,6 +75,31 @@ fn pre_003_retention_compaction_error_code_matches_store_contract_value() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_raw_account_read_bridge_uses_only_backend_independent_models() {
|
||||
let _state = ksp_store_postgres_lib::PostgresBackend::get_raw_account_state;
|
||||
let _observation = ksp_store_postgres_lib::PostgresBackend::get_raw_account_observation;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_raw_account_acquisition_bridge_uses_only_backend_independent_models_and_outcomes() {
|
||||
let _acquisition = ksp_store_postgres_lib::PostgresBackend::persist_raw_account_acquisition;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_raw_account_observation_write_bridge_uses_only_backend_independent_models_and_outcomes() {
|
||||
let _observation = ksp_store_postgres_lib::PostgresBackend::record_raw_account_observation;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_raw_account_list_bridge_uses_backend_independent_query_page_and_reference_models() {
|
||||
let _list = ksp_store_postgres_lib::PostgresBackend::list_raw_account_states;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_raw_read_bridge_uses_only_backend_independent_models() {
|
||||
let _get = ksp_store_postgres_lib::PostgresBackend::get_raw_transaction;
|
||||
@@ -103,21 +128,25 @@ fn pre_007_raw_retention_write_bridge_uses_backend_independent_transition_and_ou
|
||||
return;
|
||||
}
|
||||
|
||||
fn assert_raw_transaction_capabilities<T>()
|
||||
fn assert_raw_capabilities<T>()
|
||||
where
|
||||
T: ksp_store_api::RawTransactionRead
|
||||
+ ksp_store_api::RawTransactionWrite
|
||||
T: ksp_store_api::RawAccountObservationRead
|
||||
+ ksp_store_api::RawAccountObservationWrite
|
||||
+ ksp_store_api::RawAccountStateRead
|
||||
+ ksp_store_api::RawAccountStateWrite
|
||||
+ ksp_store_api::RawTransactionObservationRead
|
||||
+ ksp_store_api::RawTransactionObservationWrite
|
||||
+ ksp_store_api::RawTransactionRead
|
||||
+ ksp_store_api::RawTransactionRetentionRead
|
||||
+ ksp_store_api::RawTransactionRetentionWrite,
|
||||
+ ksp_store_api::RawTransactionRetentionWrite
|
||||
+ ksp_store_api::RawTransactionWrite,
|
||||
{
|
||||
let _marker = std::marker::PhantomData::<T>;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_postgres_backend_implements_all_six_raw_transaction_capabilities() {
|
||||
assert_raw_transaction_capabilities::<ksp_store_postgres_lib::PostgresBackend>();
|
||||
fn pre_008_postgres_backend_implements_exact_raw_capability_set_10_of_10() {
|
||||
assert_raw_capabilities::<ksp_store_postgres_lib::PostgresBackend>();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
|
||||
// version: 5
|
||||
// version: 8
|
||||
|
||||
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
|
||||
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
|
||||
@@ -10,6 +10,7 @@ fn embedded(version: i64, name: &'static str, resources: &'static [crate::Schema
|
||||
checksum: super::MigrationChecksum::Resources,
|
||||
hook: super::MigrationHook::None,
|
||||
name,
|
||||
previous_checksums: &[],
|
||||
resources,
|
||||
version,
|
||||
};
|
||||
@@ -17,7 +18,7 @@ fn embedded(version: i64, name: &'static str, resources: &'static [crate::Schema
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_embedded_registry_keeps_v000_checksum_and_uses_resource_owned_v001() {
|
||||
assert_eq!(super::EMBEDDED_MIGRATIONS.len(), 2);
|
||||
assert!(super::EMBEDDED_MIGRATIONS.len() >= 2);
|
||||
let v000 = &super::EMBEDDED_MIGRATIONS[0];
|
||||
assert_eq!(v000.version, 0);
|
||||
assert_eq!(v000.name, "bootstrap");
|
||||
@@ -32,7 +33,54 @@ fn pre_003_fix_001_embedded_registry_keeps_v000_checksum_and_uses_resource_owned
|
||||
assert_eq!(v001.resources.len(), 40);
|
||||
assert_eq!(super::migration_checksum(v001), "31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51");
|
||||
assert!(super::validate_embedded_registry(super::EMBEDDED_MIGRATIONS).is_ok());
|
||||
assert_eq!(crate::current_migration_version(), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_v002_registry_is_complete_and_keeps_v000_v001_checksums_stable() {
|
||||
assert_eq!(super::EMBEDDED_MIGRATIONS.len(), 3);
|
||||
let v000 = &super::EMBEDDED_MIGRATIONS[0];
|
||||
let v001 = &super::EMBEDDED_MIGRATIONS[1];
|
||||
let v002 = &super::EMBEDDED_MIGRATIONS[2];
|
||||
assert_eq!(super::migration_checksum(v000), "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
|
||||
assert_eq!(super::migration_checksum(v001), "31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51");
|
||||
assert_eq!(v002.version, 2);
|
||||
assert_eq!(v002.name, "raw_account_state");
|
||||
assert_eq!(v002.hook, super::MigrationHook::None);
|
||||
assert_eq!(v002.previous_checksums, &[super::V002_PROVISIONAL_CHECKSUM_PRE_002]);
|
||||
assert_eq!(v002.resources.len(), 32);
|
||||
assert_eq!(super::migration_checksum(v002), "ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e");
|
||||
assert!(super::validate_embedded_registry(super::EMBEDDED_MIGRATIONS).is_ok());
|
||||
assert_eq!(crate::current_migration_version(), 2);
|
||||
let full = [
|
||||
applied(0, v000.name, super::migration_checksum(v000).as_str()),
|
||||
applied(1, v001.name, super::migration_checksum(v001).as_str()),
|
||||
applied(2, v002.name, super::migration_checksum(v002).as_str()),
|
||||
];
|
||||
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(3));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_v002_provisional_checksum_requires_schema_autoupdate_and_rejects_unknown_history() {
|
||||
let v000 = &super::EMBEDDED_MIGRATIONS[0];
|
||||
let v001 = &super::EMBEDDED_MIGRATIONS[1];
|
||||
let v002 = &super::EMBEDDED_MIGRATIONS[2];
|
||||
let provisional = [
|
||||
applied(0, v000.name, super::migration_checksum(v000).as_str()),
|
||||
applied(1, v001.name, super::migration_checksum(v001).as_str()),
|
||||
applied(2, v002.name, super::V002_PROVISIONAL_CHECKSUM_PRE_002),
|
||||
];
|
||||
let strict = super::validate_history(&provisional, super::EMBEDDED_MIGRATIONS, false);
|
||||
assert_eq!(strict.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
|
||||
assert_eq!(super::validate_history(&provisional, super::EMBEDDED_MIGRATIONS, true).ok(), std::option::Option::Some(3));
|
||||
let unknown = [
|
||||
applied(0, v000.name, super::migration_checksum(v000).as_str()),
|
||||
applied(1, v001.name, super::migration_checksum(v001).as_str()),
|
||||
applied(2, v002.name, "unknown-v002-checksum"),
|
||||
];
|
||||
let unknown_result = super::validate_history(&unknown, super::EMBEDDED_MIGRATIONS, true);
|
||||
assert_eq!(unknown_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,10 +131,10 @@ fn pre_003_fix_001_ordered_registry_accepts_v000_prefix_and_full_v001_history()
|
||||
let v001 = super::EMBEDDED_MIGRATIONS[1];
|
||||
let v000_checksum = super::migration_checksum(&v000);
|
||||
let prefix = [applied(0, v000.name, v000_checksum.as_str())];
|
||||
assert_eq!(super::validate_history(&prefix, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(1));
|
||||
assert_eq!(super::validate_history(&prefix, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(1));
|
||||
let v001_checksum = super::migration_checksum(&v001);
|
||||
let full = [applied(0, v000.name, v000_checksum.as_str()), applied(1, v001.name, v001_checksum.as_str())];
|
||||
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(2));
|
||||
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(2));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,7 +163,7 @@ fn pre_003_fix_001_divergent_missing_or_gapped_history_is_terminal_mismatch() {
|
||||
let missing: [super::AppliedMigration; 0] = [];
|
||||
let missing_v000 = [applied(1, v001.name, v001_checksum.as_str())];
|
||||
for history in [&wrong_name[..], &wrong_checksum[..], &missing[..], &missing_v000[..]] {
|
||||
let result = super::validate_history(history, super::EMBEDDED_MIGRATIONS);
|
||||
let result = super::validate_history(history, super::EMBEDDED_MIGRATIONS, false);
|
||||
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
|
||||
}
|
||||
return;
|
||||
@@ -125,10 +173,17 @@ fn pre_003_fix_001_divergent_missing_or_gapped_history_is_terminal_mismatch() {
|
||||
fn pre_003_fix_001_newer_history_is_rejected_without_down_migration() {
|
||||
let v000 = super::EMBEDDED_MIGRATIONS[0];
|
||||
let v001 = super::EMBEDDED_MIGRATIONS[1];
|
||||
let v002 = super::EMBEDDED_MIGRATIONS[2];
|
||||
let v000_checksum = super::migration_checksum(&v000);
|
||||
let v001_checksum = super::migration_checksum(&v001);
|
||||
let history = [applied(0, v000.name, v000_checksum.as_str()), applied(1, v001.name, v001_checksum.as_str()), applied(2, "future", "future-checksum")];
|
||||
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS);
|
||||
let v002_checksum = super::migration_checksum(&v002);
|
||||
let history = [
|
||||
applied(0, v000.name, v000_checksum.as_str()),
|
||||
applied(1, v001.name, v001_checksum.as_str()),
|
||||
applied(2, v002.name, v002_checksum.as_str()),
|
||||
applied(3, "future", "future-checksum"),
|
||||
];
|
||||
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS, false);
|
||||
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
|
||||
return;
|
||||
}
|
||||
|
||||
525
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
Normal file
525
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
Normal file
@@ -0,0 +1,525 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
|
||||
// version: 5
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn page_query(
|
||||
network_name: &str,
|
||||
pubkey: std::option::Option<ksp_store_api::Pubkey>,
|
||||
start: std::option::Option<u64>,
|
||||
end: std::option::Option<u64>,
|
||||
direction: ksp_store_api::RawSortDirection,
|
||||
) -> ksp_store_api::RawAccountStateQuery {
|
||||
let network = match ksp_store_api::RawNetworkId::new(network_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
|
||||
};
|
||||
let slots = match ksp_store_api::RawSlotRange::new(start, end) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid slot range rejected: {error:?}"),
|
||||
};
|
||||
let limit = match ksp_store_api::RawPageLimit::new(2) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid page limit rejected: {error:?}"),
|
||||
};
|
||||
return ksp_store_api::RawAccountStateQuery::new(network, pubkey, slots, direction, ksp_store_api::RawPageRequest::first(limit));
|
||||
}
|
||||
|
||||
fn state_row() -> super::RawAccountStateDbRow {
|
||||
return super::RawAccountStateDbRow {
|
||||
data: vec![1, 2, 3, 4],
|
||||
executable: true,
|
||||
lamports_text: u64::MAX.to_string(),
|
||||
owner: vec![2; 32],
|
||||
pubkey: vec![1; 32],
|
||||
rent_epoch_text: u64::MAX.to_string(),
|
||||
slot_text: u64::MAX.to_string(),
|
||||
state_hash: vec![3; 32],
|
||||
};
|
||||
}
|
||||
|
||||
fn observation_row() -> super::RawAccountObservationDbRow {
|
||||
return super::RawAccountObservationDbRow {
|
||||
account_pubkey: vec![1; 32],
|
||||
account_slot_text: u64::MAX.to_string(),
|
||||
account_state_hash: vec![3; 32],
|
||||
acquisition_method: "account_subscribe".to_owned(),
|
||||
capture_session_id: std::option::Option::Some("session_1".to_owned()),
|
||||
commitment: std::option::Option::Some("confirmed".to_owned()),
|
||||
endpoint_id: std::option::Option::Some("endpoint_1".to_owned()),
|
||||
filter_id: std::option::Option::Some("filter_1".to_owned()),
|
||||
is_startup: std::option::Option::Some(true),
|
||||
observation_key: vec![4; 32],
|
||||
observed_at_unix_millis: std::option::Option::Some(1_700_000_000_000),
|
||||
origin: "live".to_owned(),
|
||||
protocol: "yellowstone_grpc".to_owned(),
|
||||
provider: "publicnode".to_owned(),
|
||||
received_at_unix_millis: 1_700_000_000_001,
|
||||
source_payload_hash: std::option::Option::Some(vec![5; 32]),
|
||||
source_payload_size_bytes: std::option::Option::Some(67_108_864),
|
||||
transaction_signature: std::option::Option::Some(vec![6; 64]),
|
||||
write_version_text: std::option::Option::Some(u64::MAX.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_state_row_round_trips_complete_u64_domain_and_bytes() {
|
||||
let network = network();
|
||||
let decoded = super::decode_raw_account_state_row(&network, state_row());
|
||||
let state = match decoded {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account state row rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(state.reference().network().as_str(), "devnet");
|
||||
assert_eq!(state.reference().pubkey(), &ksp_store_api::Pubkey::new_from_array([1_u8; 32]));
|
||||
assert_eq!(state.reference().slot(), u64::MAX);
|
||||
assert_eq!(state.reference().state_hash(), ksp_store_api::RawContentHash::new([3_u8; 32]));
|
||||
assert_eq!(state.lamports(), u64::MAX);
|
||||
assert_eq!(state.owner(), &ksp_store_api::Pubkey::new_from_array([2_u8; 32]));
|
||||
assert!(state.executable());
|
||||
assert_eq!(state.rent_epoch(), u64::MAX);
|
||||
assert_eq!(state.data(), &[1, 2, 3, 4]);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_state_accepts_empty_data_and_rejects_oversized_data() {
|
||||
let network = network();
|
||||
let mut empty = state_row();
|
||||
empty.data = std::vec::Vec::new();
|
||||
let empty = super::decode_raw_account_state_row(&network, empty);
|
||||
assert!(matches!(empty, std::result::Result::Ok(_)));
|
||||
let mut oversized = state_row();
|
||||
oversized.data = vec![0_u8; ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES + 1];
|
||||
let oversized = super::decode_raw_account_state_row(&network, oversized);
|
||||
assert_eq!(oversized.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_state_rejects_hostile_width_and_decimal_rows_without_echo() {
|
||||
let network = network();
|
||||
let mut bad_pubkey = state_row();
|
||||
bad_pubkey.pubkey = vec![0xA5; 31];
|
||||
let error = super::decode_raw_account_state_row(&network, bad_pubkey).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
|
||||
let mut bad_slot = state_row();
|
||||
bad_slot.slot_text = "18446744073709551616-HOSTILE".to_owned();
|
||||
let error = match super::decode_raw_account_state_row(&network, bad_slot) {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => panic!("hostile slot unexpectedly decoded"),
|
||||
};
|
||||
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::DataInvalid);
|
||||
assert!(!std::format!("{error:?}").contains("HOSTILE"));
|
||||
let mut bad_lamports = state_row();
|
||||
bad_lamports.lamports_text = "-1".to_owned();
|
||||
assert_eq!(
|
||||
super::decode_raw_account_state_row(&network, bad_lamports).err().map(|value| return value.kind()),
|
||||
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_observation_round_trips_complete_provenance_and_yellowstone_metadata() {
|
||||
let network = network();
|
||||
let decoded = super::decode_raw_account_observation_row(&network, observation_row());
|
||||
let observation = match decoded {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account observation row rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(observation.observation_key(), ksp_store_api::RawObservationKey::new([4_u8; 32]));
|
||||
assert_eq!(observation.account().network().as_str(), "devnet");
|
||||
assert_eq!(observation.account().pubkey(), &ksp_store_api::Pubkey::new_from_array([1_u8; 32]));
|
||||
assert_eq!(observation.account().slot(), u64::MAX);
|
||||
assert_eq!(observation.account().state_hash(), ksp_store_api::RawContentHash::new([3_u8; 32]));
|
||||
assert_eq!(observation.provenance().provider().as_str(), "publicnode");
|
||||
assert_eq!(observation.provenance().protocol().as_str(), "yellowstone_grpc");
|
||||
assert_eq!(observation.provenance().acquisition_method().as_str(), "account_subscribe");
|
||||
assert_eq!(observation.provenance().capture_session_id().map(|value| return value.as_str()), std::option::Option::Some("session_1"));
|
||||
assert_eq!(observation.provenance().commitment().map(|value| return value.as_str()), std::option::Option::Some("confirmed"));
|
||||
assert_eq!(observation.provenance().endpoint_id().map(|value| return value.as_str()), std::option::Option::Some("endpoint_1"));
|
||||
assert_eq!(observation.provenance().filter_id().map(|value| return value.as_str()), std::option::Option::Some("filter_1"));
|
||||
assert_eq!(observation.provenance().received_at().unix_millis(), 1_700_000_000_001);
|
||||
assert_eq!(observation.provenance().observed_at().map(|value| return value.unix_millis()), std::option::Option::Some(1_700_000_000_000));
|
||||
assert_eq!(observation.provenance().source_payload_hash(), std::option::Option::Some(ksp_store_api::RawContentHash::new([5_u8; 32])));
|
||||
assert_eq!(observation.provenance().source_payload_size_bytes(), std::option::Option::Some(ksp_store_api::MAX_RAW_SOURCE_PAYLOAD_BYTES));
|
||||
assert_eq!(observation.is_startup(), std::option::Option::Some(true));
|
||||
assert_eq!(observation.transaction_signature(), std::option::Option::Some(ksp_store_api::RawTransactionSignature::new([6_u8; 64])));
|
||||
assert_eq!(observation.write_version(), std::option::Option::Some(u64::MAX));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_observation_keeps_optional_metadata_absent() {
|
||||
let network = network();
|
||||
let mut row = observation_row();
|
||||
row.capture_session_id = std::option::Option::None;
|
||||
row.commitment = std::option::Option::None;
|
||||
row.endpoint_id = std::option::Option::None;
|
||||
row.filter_id = std::option::Option::None;
|
||||
row.observed_at_unix_millis = std::option::Option::None;
|
||||
row.source_payload_hash = std::option::Option::None;
|
||||
row.source_payload_size_bytes = std::option::Option::None;
|
||||
row.is_startup = std::option::Option::None;
|
||||
row.transaction_signature = std::option::Option::None;
|
||||
row.write_version_text = std::option::Option::None;
|
||||
let decoded = super::decode_raw_account_observation_row(&network, row);
|
||||
let observation = match decoded {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid sparse account observation rejected: {error:?}"),
|
||||
};
|
||||
assert!(observation.provenance().capture_session_id().is_none());
|
||||
assert!(observation.provenance().commitment().is_none());
|
||||
assert!(observation.provenance().endpoint_id().is_none());
|
||||
assert!(observation.provenance().filter_id().is_none());
|
||||
assert!(observation.provenance().observed_at().is_none());
|
||||
assert!(observation.provenance().source_payload_hash().is_none());
|
||||
assert!(observation.provenance().source_payload_size_bytes().is_none());
|
||||
assert!(observation.is_startup().is_none());
|
||||
assert!(observation.transaction_signature().is_none());
|
||||
assert!(observation.write_version().is_none());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_observation_rejects_hostile_rows_without_echoing_values() {
|
||||
let network = network();
|
||||
let mut bad_signature = observation_row();
|
||||
bad_signature.transaction_signature = std::option::Option::Some(vec![8; 63]);
|
||||
assert_eq!(
|
||||
super::decode_raw_account_observation_row(&network, bad_signature).err().map(|value| return value.kind()),
|
||||
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
|
||||
);
|
||||
let mut bad_write_version = observation_row();
|
||||
bad_write_version.write_version_text = std::option::Option::Some("18446744073709551616-HOSTILE".to_owned());
|
||||
let error = match super::decode_raw_account_observation_row(&network, bad_write_version) {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => panic!("hostile write version unexpectedly decoded"),
|
||||
};
|
||||
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::DataInvalid);
|
||||
assert!(!std::format!("{error:?}").contains("HOSTILE"));
|
||||
let mut bad_origin = observation_row();
|
||||
bad_origin.origin = "hostile-origin".to_owned();
|
||||
assert_eq!(
|
||||
super::decode_raw_account_observation_row(&network, bad_origin).err().map(|value| return value.kind()),
|
||||
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
|
||||
);
|
||||
let mut bad_time = observation_row();
|
||||
bad_time.observed_at_unix_millis = std::option::Option::Some(bad_time.received_at_unix_millis + 1);
|
||||
assert_eq!(
|
||||
super::decode_raw_account_observation_row(&network, bad_time).err().map(|value| return value.kind()),
|
||||
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_account_wrong_network_guard_is_pre_io_and_static() {
|
||||
let backend = network();
|
||||
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
|
||||
};
|
||||
let reference = ksp_store_api::RawAccountStateReference::new(
|
||||
foreign,
|
||||
ksp_store_api::Pubkey::new_from_array([1_u8; 32]),
|
||||
1,
|
||||
ksp_store_api::RawContentHash::new([2_u8; 32]),
|
||||
);
|
||||
let error = super::ensure_network(&backend, &reference, "raw_account_state_network").err();
|
||||
let error = match error {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("wrong-network account reference unexpectedly accepted"),
|
||||
};
|
||||
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::WrongNetwork);
|
||||
assert_eq!(error.phase(), "raw_account_state_network");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_account_acquisition_input_guard_requires_network_and_exact_reference() {
|
||||
let backend = network();
|
||||
let valid_state = match super::decode_raw_account_state_row(&backend, state_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account state rejected: {error:?}"),
|
||||
};
|
||||
let valid_observation = match super::decode_raw_account_observation_row(&backend, observation_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account observation rejected: {error:?}"),
|
||||
};
|
||||
assert!(super::ensure_acquisition_inputs(&backend, &valid_state, &valid_observation).is_ok());
|
||||
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
|
||||
};
|
||||
let foreign_state = match super::decode_raw_account_state_row(&foreign, state_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid foreign account state rejected: {error:?}"),
|
||||
};
|
||||
let foreign_observation = match super::decode_raw_account_observation_row(&foreign, observation_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid foreign account observation rejected: {error:?}"),
|
||||
};
|
||||
let error = super::ensure_acquisition_inputs(&backend, &foreign_state, &foreign_observation).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
|
||||
let mut mismatched_row = observation_row();
|
||||
mismatched_row.account_state_hash = vec![9; 32];
|
||||
let mismatched_observation = match super::decode_raw_account_observation_row(&backend, mismatched_row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid mismatched observation model rejected: {error:?}"),
|
||||
};
|
||||
let error = super::ensure_acquisition_inputs(&backend, &valid_state, &mismatched_observation).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::Conflict));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_account_state_idempotence_compares_complete_content_not_state_hash_only() {
|
||||
let backend = network();
|
||||
let first = match super::decode_raw_account_state_row(&backend, state_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account state rejected: {error:?}"),
|
||||
};
|
||||
let identical = match super::decode_raw_account_state_row(&backend, state_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid identical account state rejected: {error:?}"),
|
||||
};
|
||||
assert!(super::raw_account_states_equal(&first, &identical));
|
||||
let mut divergent_row = state_row();
|
||||
divergent_row.lamports_text = (u64::MAX - 1).to_string();
|
||||
let divergent = match super::decode_raw_account_state_row(&backend, divergent_row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid divergent account state rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(first.reference(), divergent.reference());
|
||||
assert!(!super::raw_account_states_equal(&first, &divergent));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_account_observation_idempotence_compares_optional_metadata_exactly() {
|
||||
let backend = network();
|
||||
let first = match super::decode_raw_account_observation_row(&backend, observation_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account observation rejected: {error:?}"),
|
||||
};
|
||||
let identical = match super::decode_raw_account_observation_row(&backend, observation_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid identical account observation rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(first, identical);
|
||||
let mut divergent_row = observation_row();
|
||||
divergent_row.write_version_text = std::option::Option::Some((u64::MAX - 1).to_string());
|
||||
let divergent = match super::decode_raw_account_observation_row(&backend, divergent_row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid divergent account observation rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(first.observation_key(), divergent.observation_key());
|
||||
assert_eq!(first.account(), divergent.account());
|
||||
assert_ne!(first, divergent);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_account_origin_encoding_is_exact_and_static() {
|
||||
let cases = [
|
||||
(ksp_store_api::RawAcquisitionOrigin::Backfill, "backfill"),
|
||||
(ksp_store_api::RawAcquisitionOrigin::Import, "import"),
|
||||
(ksp_store_api::RawAcquisitionOrigin::Live, "live"),
|
||||
(ksp_store_api::RawAcquisitionOrigin::Repair, "repair"),
|
||||
(ksp_store_api::RawAcquisitionOrigin::Replay, "replay"),
|
||||
];
|
||||
for (origin, expected) in cases {
|
||||
let encoded = match super::encode_origin(origin) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account origin rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(encoded, expected);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_additional_observation_input_guard_rejects_wrong_network_before_io() {
|
||||
let backend = network();
|
||||
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
|
||||
};
|
||||
let observation = match super::decode_raw_account_observation_row(&foreign, observation_row()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid foreign account observation rejected: {error:?}"),
|
||||
};
|
||||
let error = super::ensure_observation_write_input(&backend, &observation).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_additional_observation_race_and_cancellation_contract_is_transaction_bounded() {
|
||||
let source = include_str!("../src/raw_account.rs");
|
||||
let start = match source.find("pub(crate) async fn record_raw_account_observation") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("record_raw_account_observation function missing"),
|
||||
};
|
||||
let tail = &source[start..];
|
||||
let end = match tail.find("fn raw_account_observation_db_row") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("record_raw_account_observation boundary missing"),
|
||||
};
|
||||
let function = &tail[..end];
|
||||
let begin = match function.find("client.transaction().await") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation transaction begin missing"),
|
||||
};
|
||||
let key_share = match function.find("LOCK_ACCOUNT_REFERENCE_SQL") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation reference lock missing"),
|
||||
};
|
||||
let persist = match function.find("persist_account_observation_row") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation insert/compare missing"),
|
||||
};
|
||||
let commit = match function.find("sql_transaction.commit().await") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("additional observation commit missing"),
|
||||
};
|
||||
assert!(begin < key_share);
|
||||
assert!(key_share < persist);
|
||||
assert!(persist < commit);
|
||||
assert!(!function.contains("tokio::spawn"));
|
||||
assert!(!function.contains("std::mem::forget"));
|
||||
assert!(!function.contains("ON CONFLICT DO UPDATE"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_account_cursor_v1_round_trip_is_fixed_109_bytes_and_exact() {
|
||||
let pubkey = ksp_store_api::Pubkey::new_from_array([7_u8; 32]);
|
||||
let query = page_query(
|
||||
"devnet",
|
||||
std::option::Option::Some(pubkey),
|
||||
std::option::Option::Some(10),
|
||||
std::option::Option::Some(20),
|
||||
ksp_store_api::RawSortDirection::Ascending,
|
||||
);
|
||||
let state_hash = ksp_store_api::RawContentHash::new([9_u8; 32]);
|
||||
let cursor = match crate::encode_raw_account_cursor(&query, 15, &pubkey, &state_hash) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(cursor.as_bytes().len(), 109);
|
||||
assert_eq!(cursor.as_bytes().get(0..4), std::option::Option::Some(b"KSPA".as_ref()));
|
||||
assert_eq!(cursor.as_bytes().get(4).copied(), std::option::Option::Some(1));
|
||||
let decoded = match crate::decode_raw_account_cursor(&query, &cursor) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account cursor decoding rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(decoded.last_slot, 15);
|
||||
assert_eq!(decoded.last_pubkey, [7_u8; 32]);
|
||||
assert_eq!(decoded.last_state_hash, [9_u8; 32]);
|
||||
assert_eq!(std::format!("{cursor:?}"), "RawPageCursor { len: 109 }");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_account_cursor_rejects_replay_across_query_context_and_transaction_family() {
|
||||
let pubkey = ksp_store_api::Pubkey::new_from_array([4_u8; 32]);
|
||||
let query = page_query(
|
||||
"devnet",
|
||||
std::option::Option::Some(pubkey),
|
||||
std::option::Option::Some(10),
|
||||
std::option::Option::Some(20),
|
||||
ksp_store_api::RawSortDirection::Ascending,
|
||||
);
|
||||
let state_hash = ksp_store_api::RawContentHash::new([5_u8; 32]);
|
||||
let cursor = match crate::encode_raw_account_cursor(&query, 15, &pubkey, &state_hash) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
let other_network = page_query("mainnet-beta", Some(pubkey), Some(10), Some(20), ksp_store_api::RawSortDirection::Ascending);
|
||||
let other_pubkey =
|
||||
page_query("devnet", Some(ksp_store_api::Pubkey::new_from_array([6_u8; 32])), Some(10), Some(20), ksp_store_api::RawSortDirection::Ascending);
|
||||
let other_direction = page_query("devnet", Some(pubkey), Some(10), Some(20), ksp_store_api::RawSortDirection::Descending);
|
||||
let other_range = page_query("devnet", Some(pubkey), Some(11), Some(20), ksp_store_api::RawSortDirection::Ascending);
|
||||
for candidate in [&other_network, &other_pubkey, &other_direction, &other_range] {
|
||||
let error = crate::decode_raw_account_cursor(candidate, &cursor).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
|
||||
}
|
||||
let transaction_query = match ksp_store_api::RawSlotRange::new(Some(10), Some(20)) {
|
||||
std::result::Result::Ok(slots) => {
|
||||
let limit = match ksp_store_api::RawPageLimit::new(2) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid transaction page limit rejected: {error:?}"),
|
||||
};
|
||||
ksp_store_api::RawTransactionQuery::new(network(), slots, ksp_store_api::RawSortDirection::Ascending, ksp_store_api::RawPageRequest::first(limit))
|
||||
},
|
||||
std::result::Result::Err(error) => panic!("valid transaction slot range rejected: {error:?}"),
|
||||
};
|
||||
let signature = ksp_store_api::RawTransactionSignature::new([8_u8; 64]);
|
||||
let transaction_cursor = match crate::encode_raw_transaction_cursor(&transaction_query, 15, &signature) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid transaction cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
let error = crate::decode_raw_account_cursor(&query, &transaction_cursor).err();
|
||||
assert_eq!(error.map(|value| return value.phase()), Some("raw_account_cursor_magic"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_account_cursor_hostile_size_magic_version_and_digest_are_rejected() {
|
||||
let query = page_query("devnet", None, None, None, ksp_store_api::RawSortDirection::Ascending);
|
||||
for size in [1_usize, 108, 109, 110, 4_096] {
|
||||
let raw = match ksp_store_api::RawPageCursor::try_new(vec![0_u8; size].into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("bounded hostile cursor rejected by API before backend test: {error:?}"),
|
||||
};
|
||||
assert!(crate::decode_raw_account_cursor(&query, &raw).is_err());
|
||||
}
|
||||
let pubkey = ksp_store_api::Pubkey::new_from_array([3_u8; 32]);
|
||||
let state_hash = ksp_store_api::RawContentHash::new([2_u8; 32]);
|
||||
let valid = match crate::encode_raw_account_cursor(&query, 42, &pubkey, &state_hash) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
for index in [0_usize, 4, 108] {
|
||||
let mut bytes = valid.as_bytes().to_vec();
|
||||
bytes[index] ^= 0xff;
|
||||
let hostile = match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("fixed-size hostile cursor rejected by API before backend test: {error:?}"),
|
||||
};
|
||||
let error = crate::decode_raw_account_cursor(&query, &hostile).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_account_page_limit_exposes_only_the_real_postgres_limit_plus_one_boundary() {
|
||||
let maximum = crate::raw_account_physical_page_limit(9_223_372_036_854_775_806);
|
||||
assert_eq!(maximum, Ok((9_223_372_036_854_775_806_usize, i64::MAX)));
|
||||
let rejected = crate::raw_account_physical_page_limit(9_223_372_036_854_775_807).err();
|
||||
assert_eq!(rejected.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::PageLimitUnsupported));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_account_list_row_decodes_full_reference_without_integer_narrowing() {
|
||||
let row = super::RawAccountListDbRow { pubkey: vec![1_u8; 32], slot_text: u64::MAX.to_string(), state_hash: vec![2_u8; 32] };
|
||||
let decoded = match super::decode_raw_account_list_row(&network(), row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid account list row rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(decoded.0, u64::MAX);
|
||||
assert_eq!(decoded.1.pubkey(), &ksp_store_api::Pubkey::new_from_array([1_u8; 32]));
|
||||
assert_eq!(decoded.1.slot(), u64::MAX);
|
||||
assert_eq!(decoded.1.state_hash(), ksp_store_api::RawContentHash::new([2_u8; 32]));
|
||||
return;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/schema.rs
|
||||
// version: 4
|
||||
// version: 7
|
||||
|
||||
fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn {
|
||||
return super::ActualColumn {
|
||||
@@ -48,6 +48,100 @@ fn pre_003_fix_001_v001_resources_are_split_and_idempotent_by_object_family() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_v002_resources_are_complete_bounded_and_have_one_unfiltered_navigation_index() {
|
||||
assert_eq!(crate::V002_RESOURCES.len(), 32);
|
||||
let ids = crate::V002_RESOURCES.iter().map(|resource| return resource.id).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(ids.iter().filter(|id| return id.starts_with("tables/")).count(), 2);
|
||||
assert_eq!(ids.iter().filter(|id| return id.starts_with("constraints/")).count(), 29);
|
||||
assert_eq!(ids.iter().filter(|id| return id.starts_with("indexes/")).count(), 1);
|
||||
assert!(ids[..2].iter().all(|id| return id.starts_with("tables/")));
|
||||
assert!(ids[2..31].iter().all(|id| return id.starts_with("constraints/")));
|
||||
assert_eq!(ids[31], "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
|
||||
let mut unique = std::collections::BTreeSet::<&str>::new();
|
||||
for id in &ids {
|
||||
assert!(unique.insert(id), "duplicate V002 resource: {id}");
|
||||
}
|
||||
let sql = crate::V002_RESOURCES.iter().map(|resource| return resource.sql).collect::<std::vec::Vec<_>>().concat();
|
||||
let normalized_sql = sql.split_ascii_whitespace().collect::<std::vec::Vec<_>>().join(" ");
|
||||
for required in [
|
||||
"CREATE TABLE IF NOT EXISTS ksp_raw_account_states",
|
||||
"CREATE TABLE IF NOT EXISTS ksp_raw_account_observations",
|
||||
"slot NUMERIC(20, 0) NOT NULL",
|
||||
"lamports NUMERIC(20, 0) NOT NULL",
|
||||
"rent_epoch NUMERIC(20, 0) NOT NULL",
|
||||
"write_version NUMERIC(20, 0) NULL",
|
||||
"PRIMARY KEY (pubkey, slot, state_hash)",
|
||||
"PRIMARY KEY (observation_key)",
|
||||
"FOREIGN KEY (account_pubkey, account_slot, account_state_hash) REFERENCES ksp_raw_account_states(pubkey, slot, state_hash) ON DELETE RESTRICT",
|
||||
"octet_length(pubkey) = 32",
|
||||
"slot >= 0 AND slot <= 18446744073709551615",
|
||||
"octet_length(state_hash) = 32",
|
||||
"lamports >= 0 AND lamports <= 18446744073709551615",
|
||||
"octet_length(owner) = 32",
|
||||
"rent_epoch >= 0 AND rent_epoch <= 18446744073709551615",
|
||||
"octet_length(data) <= 16777216",
|
||||
"octet_length(observation_key) = 32",
|
||||
"octet_length(account_pubkey) = 32",
|
||||
"account_slot >= 0 AND account_slot <= 18446744073709551615",
|
||||
"octet_length(account_state_hash) = 32",
|
||||
"octet_length(provider) >= 1 AND octet_length(provider) <= 128",
|
||||
"octet_length(protocol) >= 1 AND octet_length(protocol) <= 128",
|
||||
"octet_length(acquisition_method) >= 1 AND octet_length(acquisition_method) <= 128",
|
||||
"origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay'",
|
||||
"received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999",
|
||||
"observed_at_unix_millis IS NULL OR observed_at_unix_millis <= received_at_unix_millis",
|
||||
"source_payload_size_bytes IS NULL OR source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864",
|
||||
"transaction_signature IS NULL OR octet_length(transaction_signature) = 64",
|
||||
"write_version IS NULL OR write_version >= 0 AND write_version <= 18446744073709551615",
|
||||
"CREATE INDEX IF NOT EXISTS ix_ksp_raw_account_states_slot_pubkey_state_hash ON ksp_raw_account_states (slot, pubkey, state_hash)",
|
||||
] {
|
||||
assert!(normalized_sql.contains(required), "V002 final physical contract is missing: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"ON DELETE CASCADE",
|
||||
"BIGSERIAL",
|
||||
"slot BIGINT",
|
||||
"lamports BIGINT",
|
||||
"rent_epoch BIGINT",
|
||||
"write_version BIGINT",
|
||||
"data TEXT",
|
||||
"CREATE UNIQUE INDEX",
|
||||
] {
|
||||
assert!(!normalized_sql.contains(forbidden), "V002 contains forbidden physical policy/index shape: {forbidden}");
|
||||
}
|
||||
let index_resource = &crate::V002_RESOURCES[31];
|
||||
let contract = match index_resource.object {
|
||||
super::SchemaObjectContract::Index(value) => value,
|
||||
super::SchemaObjectContract::Constraint(_) | super::SchemaObjectContract::Table(_) => panic!("V002 final resource must be the navigation index"),
|
||||
};
|
||||
assert_eq!(contract.key_fragment, "slot,pubkey,state_hash");
|
||||
assert_eq!(contract.predicate_fragment, std::option::Option::None);
|
||||
assert!(!contract.unique);
|
||||
assert!(!index_resource.sql.contains("WHERE"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_v002_external_compatibility_recognizes_only_owned_constraints() {
|
||||
assert!(super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_states", "ck_ksp_raw_account_states_slot"));
|
||||
assert!(super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_observations", "fk_ksp_raw_account_observations_state",));
|
||||
assert!(!super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_states", "external_check"));
|
||||
let slot_resource = crate::V002_RESOURCES.iter().find(|resource| return resource.id == "constraints/005_ck_ksp_raw_account_states_slot.sql");
|
||||
let slot_resource = match slot_resource {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("V002 slot constraint must remain embedded"),
|
||||
};
|
||||
let expected = super::expected_constraint_definition(slot_resource.sql, "ck_ksp_raw_account_states_slot");
|
||||
let expected = match expected {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("V002 slot constraint definition must be extractable"),
|
||||
};
|
||||
assert!(super::matches_expected_constraint_definition(crate::V002_RESOURCES, "ksp_raw_account_states", "c", expected.as_str()));
|
||||
assert!(!super::matches_expected_constraint_definition(crate::V001_RESOURCES, "ksp_raw_account_states", "c", expected.as_str()));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_external_extra_columns_are_accepted_only_when_they_cannot_block_ksp_inserts() {
|
||||
let nullable = actual_column("external_nullable", "text", true);
|
||||
|
||||
139
deltas/0.3.4/pre.001-fix.001.md
Normal file
139
deltas/0.3.4/pre.001-fix.001.md
Normal file
@@ -0,0 +1,139 @@
|
||||
<!-- file: deltas/0.3.4/pre.001-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.001-fix.001` — forecast souple éditable
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.4-pre.001
|
||||
workspace.package.version = 0.3.4-pre.1
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.001` du 30 août 2026 est entièrement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés Store/Config et `cargo check -p ksp-store-lib --no-default-features` passent.
|
||||
|
||||
## 2. Motif du correctif
|
||||
|
||||
Le sizing recalibré de `docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md` était présenté sous forme de tableau. Cette forme décrit correctement le forecast mais ne permet pas d'insérer proprement l'historique des correctifs sous la prerelease concernée.
|
||||
|
||||
Le projet possède déjà une convention documentaire adaptée : chaque prerelease du forecast est une sous-section `### pre.NNN` et chaque correctif éventuel est une sous-sous-section `#### pre.NNN-fix.MMM` placée directement sous sa tranche.
|
||||
|
||||
## 3. Correction
|
||||
|
||||
La section `Sizing recalibré` devient un forecast souple éditable :
|
||||
|
||||
```text
|
||||
## Sizing recalibré — prereleases souples
|
||||
### pre.NNN — objet
|
||||
#### pre.NNN-fix.MMM — correctif éventuel
|
||||
```
|
||||
|
||||
Chaque tranche expose désormais :
|
||||
|
||||
- un statut modifiable ;
|
||||
- son budget cible ;
|
||||
- sa responsabilité technique principale ;
|
||||
- un emplacement naturel pour les fixes successifs.
|
||||
|
||||
Le découpage `pre.001` à `pre.013` puis `rel.001`, les budgets et les responsabilités techniques restent inchangés. Le correctif ajoute seulement la hiérarchie documentaire nécessaire à l'historique futur.
|
||||
|
||||
`pre.001-fix.001` est lui-même enregistré sous `pre.001` dans ce forecast.
|
||||
|
||||
## 4. Version Cargo inchangée
|
||||
|
||||
Ce correctif est strictement documentaire. Conformément à `VER-ID-008` :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.1
|
||||
```
|
||||
|
||||
`Cargo.toml` n'est pas modifié.
|
||||
|
||||
## 5. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.001-fix.001.md
|
||||
```
|
||||
|
||||
## 6. Fichiers modifiés
|
||||
|
||||
```text
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
```
|
||||
|
||||
Le header documentaire du plan passe de `version: 1` à `version: 2` parce que ce fichier est réellement modifié.
|
||||
|
||||
## 7. Fichiers non modifiés
|
||||
|
||||
En particulier :
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
src/**
|
||||
tests/**
|
||||
config/**
|
||||
migrations/**
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
deltas/0.3.4/pre.001.md
|
||||
```
|
||||
|
||||
Le delta historique `pre.001.md` reste immuable ; le correctif est tracé uniquement dans le nouveau delta `pre.001-fix.001.md` et dans le forecast du plan.
|
||||
|
||||
## 8. Invariants techniques
|
||||
|
||||
Aucun changement n'est apporté au design V002 figé par `pre.001` :
|
||||
|
||||
```text
|
||||
2 tables seulement
|
||||
PK state = (pubkey, slot, state_hash)
|
||||
BYTEA pour les bytes/fixed-width
|
||||
NUMERIC(20,0) pour les u64 non bornés à i64
|
||||
atomicité state + observation
|
||||
idempotence par comparaison exacte
|
||||
pagination keyset (slot, pubkey, state_hash)
|
||||
cursor account V1 KSPA distinct de KSPT
|
||||
10 capabilities RAW finales = 6 transaction + 4 account
|
||||
```
|
||||
|
||||
## 9. Gate opérateur hérité
|
||||
|
||||
Le gate exécuté sur `0.3.4-pre.001` avant ce correctif documentaire est PASS :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (240 table(s), 134 file(s))
|
||||
cargo check --workspace: PASS
|
||||
cargo clippy --workspace --all-targets: PASS
|
||||
cargo test -p ksp-store-api: PASS
|
||||
cargo test -p ksp-store-lib: PASS
|
||||
cargo test -p ksp-store-postgres-lib: PASS
|
||||
cargo test -p ksp-config-lib: PASS
|
||||
cargo check -p ksp-store-lib --no-default-features: PASS
|
||||
```
|
||||
|
||||
Comme le fix ne touche ni code, ni build, ni runtime, ni config, ni migration, aucun nouveau live PostgreSQL n'est requis. L'audit Markdown doit simplement rester propre après la restructuration du plan.
|
||||
|
||||
## 10. Validation post-fix
|
||||
|
||||
Les audits statiques applicables ont été rejoués après la restructuration :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (239 table(s), 135 file(s))
|
||||
```
|
||||
|
||||
La diminution d'une table correspond exactement au remplacement du tableau de sizing par des sous-sections ; le fichier delta supplémentaire porte le nombre de fichiers Markdown audités à 135.
|
||||
|
||||
Contrôles de scope :
|
||||
|
||||
```text
|
||||
Cargo.toml : byte-identique à pre.001
|
||||
validation 021 : byte-identique à pre.001
|
||||
deltas/0.3.4/pre.001.md : byte-identique à pre.001
|
||||
```
|
||||
206
deltas/0.3.4/pre.001.md
Normal file
206
deltas/0.3.4/pre.001.md
Normal file
@@ -0,0 +1,206 @@
|
||||
<!-- file: deltas/0.3.4/pre.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.001` — audit et physical design `RawAccountState` PostgreSQL
|
||||
|
||||
## 1. Objet
|
||||
|
||||
Ouverture de :
|
||||
|
||||
```text
|
||||
0.3.4 — Store/PostgreSQL RawAccountState + complétude/conformance RAW
|
||||
```
|
||||
|
||||
Cette tranche reste volontairement un gate de lecture/audit/design/sizing. Aucun SQL V002, aucune table account, aucun repository account et aucune impl capability account ne sont ajoutés.
|
||||
|
||||
## 2. Base
|
||||
|
||||
Base canonique :
|
||||
|
||||
```text
|
||||
v0.3.3
|
||||
workspace.package.version = 0.3.3
|
||||
```
|
||||
|
||||
Le gate opérateur fourni avec la reprise est propre sur audits Rust/Markdown, checks, Clippy et suites ciblées/workspace de la release stable ; les live tests restent opt-in/ignored.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Version workspace après overlay :
|
||||
|
||||
```text
|
||||
0.3.4-pre.1
|
||||
```
|
||||
|
||||
Label de livraison :
|
||||
|
||||
```text
|
||||
0.3.4-pre.001
|
||||
```
|
||||
|
||||
## 4. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
deltas/0.3.4/pre.001.md
|
||||
```
|
||||
|
||||
## 5. Fichier modifié
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
```
|
||||
|
||||
Modifications :
|
||||
|
||||
```text
|
||||
workspace.package.version : 0.3.3 -> 0.3.4-pre.1
|
||||
file header version : 363 -> 364
|
||||
```
|
||||
|
||||
## 6. Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## 7. Audit effectué
|
||||
|
||||
Ont été relus avant design : règles globales/KSP/Rust/dépendances/docs/versioning ; architectures Store/data/execution/workers ; plans/validations `0.3.1`, `0.3.2`, `0.3.3` ; source/tests Store API/façade/backend ; Config `std.store` ; migrations V000/V001 ; archive kbot3 ciblée account.
|
||||
|
||||
Inventaire confirmé :
|
||||
|
||||
```text
|
||||
10 capabilities RAW contractuelles
|
||||
6 RawTransaction implémentées physiquement dans PostgresBackend + Store
|
||||
4 RawAccount encore non implémentées et explicitement fermées par canaries 0.3.3
|
||||
```
|
||||
|
||||
Checksums recalculés :
|
||||
|
||||
```text
|
||||
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
```
|
||||
|
||||
## 8. Décisions majeures
|
||||
|
||||
### 8.1 V002
|
||||
|
||||
V002 sera additive, sous `migrations/v002_raw_account_state/`, avec seulement :
|
||||
|
||||
```text
|
||||
ksp_raw_account_states
|
||||
ksp_raw_account_observations
|
||||
```
|
||||
|
||||
V000/V001 restent byte-immutables.
|
||||
|
||||
### 8.2 Identité state
|
||||
|
||||
```text
|
||||
network API + (pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
Le réseau est le binding DB existant. PK physique `(pubkey, slot, state_hash)`. Pubkey/owner/hash en BYTEA fixed-width ; slot/lamports/rent_epoch en NUMERIC(20,0). Data account en BYTEA complet, vide autorisé, maximum 16 MiB.
|
||||
|
||||
### 8.3 Observation
|
||||
|
||||
L'observation référence le state par FK composite et conserve provenance + `is_startup` + `transaction_signature` + `write_version`. Ces metadata restent observation-only. Aucune FK transaction n'est ajoutée.
|
||||
|
||||
### 8.4 Atomicité/idempotence
|
||||
|
||||
Pattern retenu : transaction PostgreSQL, `INSERT ... ON CONFLICT DO NOTHING RETURNING`, puis `SELECT ... FOR UPDATE` et comparaison exacte sur collision. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*`, aucun overwrite silencieux.
|
||||
|
||||
`record_raw_account_observation` vérifie le state préexistant et ne le crée jamais implicitement.
|
||||
|
||||
### 8.5 Pagination
|
||||
|
||||
Ordre total :
|
||||
|
||||
```text
|
||||
(slot, pubkey, state_hash)
|
||||
```
|
||||
|
||||
Cursor V1 account fixe 109 bytes, magic `KSPA`, domaine `KSP/raw-account-state-cursor/v1`, digest lié au réseau, filtre pubkey, range, direction et dernière clé. Le cursor transaction `KSPT` est donc non rejouable.
|
||||
|
||||
Index métier additionnel unique :
|
||||
|
||||
```text
|
||||
(slot, pubkey, state_hash)
|
||||
```
|
||||
|
||||
Aucun index owner/provider/time/status.
|
||||
|
||||
## 9. Audit kbot3
|
||||
|
||||
Classification :
|
||||
|
||||
- **REPRENDRE** : invariants unicité/FK et principe d'index account+slot ; NUMERIC pour valeurs monétaires ;
|
||||
- **REDESSINER** : identité durable, BYTEA fixed-width, bytes complets, u64 exact, observation séparée, atomicité/conflict/cursor ;
|
||||
- **REPORTER** : normalisation CORE, processing ledger, batch 1000, indexes owner/provider/time/status ;
|
||||
- **REJETER** : sqlx/monolithe, BIGSERIAL comme identité publique, base64 physique, BIGINT narrowing, status/error RAW, maintenance destructive comme comportement normal.
|
||||
|
||||
Aucun code historique n'est copié.
|
||||
|
||||
## 10. Threat model
|
||||
|
||||
Le plan couvre mauvaise DB/réseau, fixed-width hostile, overflow u64, 16 MiB, observation orpheline, rollback partiel, même pubkey+slot multi-state, conflits exacts, collisions observation, races, cursor hostile/cross-query/cross-family, ordre pagination, metadata Yellowstone, fuite SQL/server text, cancellation et migration mismatch.
|
||||
|
||||
## 11. Sizing recalibré
|
||||
|
||||
Le forecast est étendu à `pre.013` afin de séparer V002 registry/tables de ses contraintes/schema compatibility, puis acquisition write de la concurrence/observation supplémentaire. Le gate technique final reste suivi d'une lane documentaire et d'une lane publication distinctes.
|
||||
|
||||
## 12. Hors scope conservé
|
||||
|
||||
```text
|
||||
ksp-store-api redesign
|
||||
account retention/archive/purge
|
||||
STRUCTURAL/CORE/DECODED/DOMAIN persistence
|
||||
Interface events
|
||||
workers/jobs/backfills
|
||||
batch/priorité/backlog policy
|
||||
nouveaux indexes sans query
|
||||
nouveaux backends/apps
|
||||
```
|
||||
|
||||
## 13. Questions ouvertes
|
||||
|
||||
Aucune question de contrat bloquante après `pre.001`. Les numéros exacts de ressources SQL et le découpage de helpers privés sont laissés à `pre.002/pre.003` sans pouvoir modifier les invariants figés.
|
||||
|
||||
## 14. Validations exécutées
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
Markdown table audit: clean (240 table(s), 134 file(s))
|
||||
|
||||
assertions structurelles locales
|
||||
workspace.package.version = 0.3.4-pre.1
|
||||
Cargo.toml header version = 364
|
||||
plan 025 / validation 021 / delta pre.001 présents
|
||||
aucun répertoire migrations/v002_raw_account_state
|
||||
aucun module backend raw_account créé
|
||||
```
|
||||
|
||||
## 15. Validations non exécutées dans le conteneur d'assemblage
|
||||
|
||||
L'exécutable `cargo` n'est pas disponible dans cet environnement. L'appel `cargo fmt --all` s'arrête donc immédiatement avec `cargo: command not found`; aucune commande Cargo post-overlay n'est déclarée PASS.
|
||||
|
||||
À exécuter côté opérateur :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
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 gate opérateur de la base `v0.3.3` fourni avec la reprise était intégralement vert avant cet overlay. Cet historique ne remplace pas le gate post-overlay ci-dessus.
|
||||
168
deltas/0.3.4/pre.002-fix.001.md
Normal file
168
deltas/0.3.4/pre.002-fix.001.md
Normal file
@@ -0,0 +1,168 @@
|
||||
<!-- file: deltas/0.3.4/pre.002-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.002-fix.001` — correction des canaris V002
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base opérateur obligatoire :
|
||||
|
||||
```text
|
||||
0.3.4-pre.002
|
||||
workspace.package.version = 0.3.4-pre.2
|
||||
```
|
||||
|
||||
Le gate opérateur du 2026-08-30 est propre sur les audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, Config et `ksp-store-lib --no-default-features`. Il échoue uniquement dans :
|
||||
|
||||
```text
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
```
|
||||
|
||||
avec 41 tests unitaires backend passants et deux canaris en échec.
|
||||
|
||||
## 2. Version
|
||||
|
||||
Ce correctif modifie deux sources Rust de tests. Conformément à `VER-ID-007` et `VER-ID-010` :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.2.fix.1
|
||||
```
|
||||
|
||||
Aucune version de migration n'est ajoutée ou changée.
|
||||
|
||||
## 3. Défaut 1 — canari `SchemaNewer` devenu obsolète
|
||||
|
||||
Le test historique :
|
||||
|
||||
```text
|
||||
pre_003_fix_001_newer_history_is_rejected_without_down_migration
|
||||
```
|
||||
|
||||
construisait V000, V001 puis une migration inconnue de version `2`. Ce scénario était valide quand V001 était la dernière migration embedded. Depuis `pre.002`, V002 occupe légitimement la version `2`; le même historique représente donc une divergence V002 et doit être classé `MigrationMismatch`, ce qui explique l'échec observé :
|
||||
|
||||
```text
|
||||
left: Some(MigrationMismatch)
|
||||
right: Some(SchemaNewer)
|
||||
```
|
||||
|
||||
Le canari est recalibré sans modifier `validate_history` : il construit le préfixe valide V000/V001/V002 avec leurs checksums réels, puis ajoute une migration inconnue de version `3`. Le scénario teste à nouveau exactement un historique plus récent que le registry KSP et conserve l'attente `SchemaNewer`.
|
||||
|
||||
## 4. Défaut 2 — assertion FK sensible au formatage SQL
|
||||
|
||||
Le canari :
|
||||
|
||||
```text
|
||||
pre_002_v002_resources_are_exactly_two_tables_plus_base_pk_fk_without_indexes_or_domain_checks
|
||||
```
|
||||
|
||||
cherchait la FK complète sous forme d'une chaîne contenant un espace entre la clause `FOREIGN KEY (...)` et `REFERENCES`. La ressource SQL V002 contient un retour à la ligne à cet endroit, sans différence sémantique PostgreSQL.
|
||||
|
||||
La ressource SQL n'est **pas** réécrite. Le test normalise uniquement les espaces ASCII du SQL embedded avant de vérifier ses fragments obligatoires/interdits. Il continue d'exiger :
|
||||
|
||||
- exactement 5 resources V002 ;
|
||||
- les 2 tables ;
|
||||
- les 2 PK ;
|
||||
- la FK composite avec `ON DELETE RESTRICT` ;
|
||||
- `NUMERIC(20,0)` pour les champs concernés ;
|
||||
- absence d'index, de `CHECK` de `pre.003`, de cascade, de `BIGSERIAL`, de `slot BIGINT` et de `data TEXT`.
|
||||
|
||||
## 5. Invariants préservés
|
||||
|
||||
Aucune ressource SQL n'est modifiée :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/**
|
||||
```
|
||||
|
||||
Le checksum V002 intermédiaire reste donc :
|
||||
|
||||
```text
|
||||
30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
|
||||
```
|
||||
|
||||
Restent également inchangés :
|
||||
|
||||
```text
|
||||
V000 / V001
|
||||
registry V002 et ses 5 resources
|
||||
src/migration.rs
|
||||
src/schema.rs
|
||||
ksp-store-api
|
||||
ksp-store-lib
|
||||
Config
|
||||
aucune capability RawAccount* backend/façade
|
||||
aucun repository raw_account
|
||||
aucun index/check de pre.003
|
||||
```
|
||||
|
||||
## 6. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/unit_tests/migration.rs
|
||||
crates/ksp-store-postgres-lib/unit_tests/schema.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
Fichier ajouté :
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.002-fix.001.md
|
||||
```
|
||||
|
||||
Aucun fichier supprimé.
|
||||
|
||||
## 7. Validations déjà fournies par le gate opérateur `pre.002`
|
||||
|
||||
PASS avant le correctif :
|
||||
|
||||
```text
|
||||
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.4
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
Backend avant correctif :
|
||||
|
||||
```text
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
41 passed; 2 failed
|
||||
```
|
||||
|
||||
## 8. Gate opérateur du correctif
|
||||
|
||||
À rejouer après extraction :
|
||||
|
||||
```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.4
|
||||
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
|
||||
```
|
||||
|
||||
Les commandes Cargo post-correctif ne sont pas exécutables dans l'environnement d'assemblage courant faute d'exécutable `cargo`.
|
||||
|
||||
## 9. Décisions
|
||||
|
||||
- corriger les canaris, pas la production ni V002 ;
|
||||
- préserver strictement le checksum de migration ;
|
||||
- conserver `SchemaNewer` pour un historique réellement au-delà de la dernière migration embedded ;
|
||||
- rendre l'assertion FK insensible uniquement au whitespace SQL, sans affaiblir le contrat de contenu ;
|
||||
- ne pas avancer `pre.003`.
|
||||
|
||||
## 10. Questions ouvertes
|
||||
|
||||
Aucune pour ce correctif. Le passage à `pre.003` dépend du gate opérateur vert de `pre.002-fix.001`.
|
||||
232
deltas/0.3.4/pre.002.md
Normal file
232
deltas/0.3.4/pre.002.md
Normal file
@@ -0,0 +1,232 @@
|
||||
<!-- file: deltas/0.3.4/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.002` — registry V002 et tables RAW account minimales
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.4-pre.001-fix.001
|
||||
workspace.package.version = 0.3.4-pre.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni pour `pre.001` est propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés Store/Config et `cargo check -p ksp-store-lib --no-default-features` passent. `pre.001-fix.001` est ensuite un correctif strictement documentaire ; ses audits statiques sont propres et il ne modifie pas Cargo/runtime.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Matérialiser uniquement la première fondation physique V002 décidée en `pre.001` :
|
||||
|
||||
```text
|
||||
migration logique V002 = raw_account_state
|
||||
2 tables
|
||||
2 primary keys
|
||||
1 foreign key observation -> state
|
||||
```
|
||||
|
||||
Cette tranche n'ouvre aucun repository account, aucun dispatch Store, aucun index métier et aucune contrainte de domaine/fixed-width complète.
|
||||
|
||||
## 3. Version Cargo
|
||||
|
||||
Cette prerelease modifie code et migration SQL. Conformément à `VER-ID-009` :
|
||||
|
||||
```text
|
||||
workspace.package.version : 0.3.4-pre.1 -> 0.3.4-pre.2
|
||||
Cargo.toml header : 364 -> 365
|
||||
```
|
||||
|
||||
## 4. Registry V002
|
||||
|
||||
`EMBEDDED_MIGRATIONS` contient désormais exactement trois migrations logiques ordonnées :
|
||||
|
||||
```text
|
||||
V000 bootstrap
|
||||
V001 raw_transaction
|
||||
V002 raw_account_state
|
||||
```
|
||||
|
||||
V002 utilise `MigrationChecksum::Resources`, `MigrationHook::None` et `V002_RESOURCES`. `current_migration_version()` passe de `1` à `2`.
|
||||
|
||||
Les checksums acquis restent inchangés :
|
||||
|
||||
```text
|
||||
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
```
|
||||
|
||||
Le checksum calculé sur les cinq resources V002 de cette tranche est :
|
||||
|
||||
```text
|
||||
30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
|
||||
```
|
||||
|
||||
Il est volontairement **provisoire de prerelease** : `pre.003` doit encore ajouter les contraintes de domaine et l'index prévus, traiter explicitement la transition depuis cet état intermédiaire puis figer le checksum V002 final avant toute preuve live account persistante.
|
||||
|
||||
## 5. Ressources physiques ajoutées
|
||||
|
||||
```text
|
||||
migrations/v002_raw_account_state/
|
||||
tables/
|
||||
001_ksp_raw_account_states.sql
|
||||
002_ksp_raw_account_observations.sql
|
||||
constraints/
|
||||
001_pk_ksp_raw_account_states.sql
|
||||
002_pk_ksp_raw_account_observations.sql
|
||||
003_fk_ksp_raw_account_observations_state.sql
|
||||
```
|
||||
|
||||
### 5.1 State
|
||||
|
||||
Colonnes matérialisées exactement selon le design `pre.001` :
|
||||
|
||||
```text
|
||||
pubkey BYTEA NOT NULL
|
||||
slot NUMERIC(20,0) NOT NULL
|
||||
state_hash BYTEA NOT NULL
|
||||
lamports NUMERIC(20,0) NOT NULL
|
||||
owner BYTEA NOT NULL
|
||||
executable BOOLEAN NOT NULL
|
||||
rent_epoch NUMERIC(20,0) NOT NULL
|
||||
data BYTEA NOT NULL
|
||||
```
|
||||
|
||||
PK :
|
||||
|
||||
```text
|
||||
(pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
### 5.2 Observation
|
||||
|
||||
Les champs de provenance existants sont conservés avec les optionalités account décidées, dont :
|
||||
|
||||
```text
|
||||
is_startup BOOLEAN NULL
|
||||
transaction_signature BYTEA NULL
|
||||
write_version NUMERIC(20,0) NULL
|
||||
```
|
||||
|
||||
PK : `(observation_key)`.
|
||||
|
||||
FK :
|
||||
|
||||
```text
|
||||
(account_pubkey, account_slot, account_state_hash)
|
||||
-> ksp_raw_account_states(pubkey, slot, state_hash)
|
||||
ON DELETE RESTRICT
|
||||
```
|
||||
|
||||
Aucune FK vers `ksp_raw_transactions` n'est ajoutée.
|
||||
|
||||
## 6. Scope explicitement non avancé
|
||||
|
||||
```text
|
||||
aucun CHECK fixed-width/u64/data/provenance account
|
||||
aucun index (slot,pubkey,state_hash)
|
||||
aucun index owner/provider/time/status
|
||||
aucun src/raw_account.rs
|
||||
aucun repository/read/write account
|
||||
aucune impl RawAccount* sur PostgresBackend
|
||||
aucune impl RawAccount* sur Store
|
||||
aucune modification ksp-store-api
|
||||
aucune retention/archive/purge account
|
||||
```
|
||||
|
||||
Les contraintes et l'index sont réservés à `pre.003` ; le mapping/read commence seulement à `pre.004`.
|
||||
|
||||
## 7. Tests/canaris ajustés
|
||||
|
||||
Les tests historiques qui interdisaient l'existence même de V002 sont recalibrés sans ouvrir les capabilities account :
|
||||
|
||||
- registry V000/V001/V002 exact et `current_migration_version = 2` ;
|
||||
- inventaire V002 exact `2 tables + 3 contraintes PK/FK` ;
|
||||
- absence de CHECK/index dans V002 `pre.002` ;
|
||||
- absence de module repository account et d'impl `RawAccount*` runtime ;
|
||||
- live tests transaction/foundation existants attendent désormais migration version `2` s'ils sont exécutés.
|
||||
|
||||
Les canaris qui interdisent les quatre implémentations `RawAccount*` restent actifs.
|
||||
|
||||
## 8. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql
|
||||
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql
|
||||
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/001_pk_ksp_raw_account_states.sql
|
||||
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/002_pk_ksp_raw_account_observations.sql
|
||||
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql
|
||||
deltas/0.3.4/pre.002.md
|
||||
```
|
||||
|
||||
## 9. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/src/lib.rs
|
||||
crates/ksp-store-postgres-lib/src/migration.rs
|
||||
crates/ksp-store-postgres-lib/src/schema.rs
|
||||
crates/ksp-store-postgres-lib/unit_tests/migration.rs
|
||||
crates/ksp-store-postgres-lib/unit_tests/schema.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/postgres_foundation_live.rs
|
||||
crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 10. Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## 11. Validations exécutées dans l'environnement d'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
Markdown table audit: clean (239 table(s), 136 file(s))
|
||||
|
||||
assertions structurelles locales
|
||||
DIFF_SCOPE=PASS
|
||||
HEADER_VERSIONS=PASS
|
||||
V000_V001_IMMUTABLE=PASS
|
||||
V002_MINIMAL_SCOPE=PASS
|
||||
V002_CHECKSUM=30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
|
||||
```
|
||||
|
||||
Le checksum V002 intermédiaire a été recalculé indépendamment à partir du domaine `ksp-migration-resources-v1\0`, des IDs ordonnés et des bytes exacts des cinq resources.
|
||||
|
||||
## 12. Validations non exécutées dans l'environnement d'assemblage
|
||||
|
||||
L'exécutable `cargo` n'est pas disponible dans cet environnement. Après application du delta, rejouer côté opérateur :
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
|
||||
Aucun live PostgreSQL account n'est revendiqué dans `pre.002`.
|
||||
|
||||
## 13. Décisions prises
|
||||
|
||||
- V002 reste additive et ne modifie aucun byte V000/V001.
|
||||
- Les PK/FK sont des resources séparées et inspectables par le moteur existant.
|
||||
- La table state porte directement les bytes D1 RAW ; aucune représentation base64 physique n'est introduite.
|
||||
- `transaction_signature` reste metadata d'observation sans FK transaction.
|
||||
- La tranche ne crée pas de deuxième vérité `space`, de surrogate ID, d'archive account ni de processing ledger.
|
||||
|
||||
## 14. Questions ouvertes
|
||||
|
||||
Aucune question de contrat nouvelle. `pre.003` doit finaliser la surface physique V002 déjà décidée : contraintes de domaine/fixed-width, index de navigation, compatibilité schema et checksum final, y compris la transition depuis l'état intermédiaire de cette prerelease.
|
||||
111
deltas/0.3.4/pre.003-fix.001.md
Normal file
111
deltas/0.3.4/pre.003-fix.001.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<!-- file: deltas/0.3.4/pre.003-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.003-fix.001` — conformité `RUST-FMT-104` du bloc migration
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.003
|
||||
workspace.package.version = 0.3.4-pre.3
|
||||
```
|
||||
|
||||
Le gate opérateur du 2026-08-30 confirme :
|
||||
|
||||
```text
|
||||
Markdown audit PASS, 239 tables / 138 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS, 45 tests
|
||||
cargo test -p ksp-config-lib PASS, 128 tests
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
Rust rule audit FAIL, 1 x RUST-FMT-104
|
||||
```
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Corriger l'unique violation de conformité restante de `pre.003` sans modifier le comportement de migration ni le schéma V002.
|
||||
|
||||
## 3. Correction
|
||||
|
||||
Dans `crates/ksp-store-postgres-lib/src/migration.rs`, le bloc homogène de constantes est remis dans l'ordre alphabétique attendu par `RUST-FMT-104` :
|
||||
|
||||
```text
|
||||
HISTORY_INSERT_SQL
|
||||
HISTORY_LOAD_SQL
|
||||
HISTORY_UPDATE_CHECKSUM_SQL
|
||||
```
|
||||
|
||||
`HISTORY_LOAD_SQL` est seulement déplacée avant `HISTORY_UPDATE_CHECKSUM_SQL`. Aucune valeur de constante, chaîne SQL ou logique de code n'est modifiée.
|
||||
|
||||
## 4. Version
|
||||
|
||||
Le correctif touche un fichier Rust ; `VER-ID-007/010` impose donc :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.3.fix.1
|
||||
label = 0.3.4-pre.003-fix.001
|
||||
```
|
||||
|
||||
## 5. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.003-fix.001.md
|
||||
```
|
||||
|
||||
## 6. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/src/migration.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 7. Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## 8. Invariants conservés
|
||||
|
||||
- V002 reste à 32 resources : 2 tables, 29 contraintes, 1 index ;
|
||||
- checksum V002 final inchangé : `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e` ;
|
||||
- checksum provisoire `pre.002` inchangé et bridge conditionnel inchangé ;
|
||||
- V000/V001 inchangées ;
|
||||
- aucune ressource SQL modifiée ;
|
||||
- aucun repository `raw_account`, dispatch Store ou implémentation `RawAccount*` ajouté ;
|
||||
- aucun changement fonctionnel, de migration, de runtime ou de configuration hors identifiant technique Cargo du fix.
|
||||
|
||||
## 9. Validations exécutées dans l'environnement d'assemblage
|
||||
|
||||
```text
|
||||
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.4
|
||||
```
|
||||
|
||||
## 10. Validations non exécutées dans l'environnement d'assemblage
|
||||
|
||||
`cargo` n'est pas disponible dans cet environnement. Le gate opérateur complet du fix reste à rejouer :
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 11. Décisions
|
||||
|
||||
Le défaut appartient strictement au couloir `pre.003` : il est donc corrigé par `pre.003-fix.001` et ne justifie pas l'ouverture de `pre.004`.
|
||||
|
||||
## 12. Questions ouvertes
|
||||
|
||||
Aucune.
|
||||
187
deltas/0.3.4/pre.003.md
Normal file
187
deltas/0.3.4/pre.003.md
Normal file
@@ -0,0 +1,187 @@
|
||||
<!-- file: deltas/0.3.4/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.003` — contraintes, index et compatibilité V002
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base appliquée :
|
||||
|
||||
```text
|
||||
0.3.4-pre.002-fix.001
|
||||
workspace.package.version = 0.3.4-pre.2.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, suites `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, `ksp-config-lib` et `cargo check -p ksp-store-lib --no-default-features` passent. Le backend PostgreSQL exécute notamment 43 tests unitaires sans échec après `pre.002-fix.001`.
|
||||
|
||||
## 2. Version
|
||||
|
||||
Cette tranche contient des changements Rust/SQL fonctionnels :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.3
|
||||
label = 0.3.4-pre.003
|
||||
```
|
||||
|
||||
## 3. Scope exécuté
|
||||
|
||||
`pre.003` ferme uniquement le schéma physique V002 et sa compatibilité de migration. Aucun repository account ni dispatch façade n'est ouvert.
|
||||
|
||||
Inventaire V002 final :
|
||||
|
||||
```text
|
||||
2 tables
|
||||
29 contraintes = 2 PK + 1 FK + 26 CHECK
|
||||
1 index non unique
|
||||
32 resources au total
|
||||
```
|
||||
|
||||
Les 26 `CHECK` ajoutés matérialisent les invariants déjà possédés par `ksp-store-api` :
|
||||
|
||||
- pubkeys, state hashes, observation keys et source hashes aux largeurs fixes attendues ;
|
||||
- transaction signature optionnelle à 64 bytes ;
|
||||
- `slot`, `lamports`, `rent_epoch`, `account_slot` et `write_version` dans le domaine `u64` exact, avec stockage `NUMERIC(20,0)` lorsque nécessaire ;
|
||||
- `data` account bornée à 16 MiB, vide autorisé ;
|
||||
- provenance logique obligatoire/optionnelle bornée à 1..128 octets avec l'alphabet sûr KSP ;
|
||||
- origin limité à `backfill`, `import`, `live`, `repair`, `replay` ;
|
||||
- timestamps bornés à `MAX_RAW_UNIX_MILLIS` et ordre `observed_at <= received_at` ;
|
||||
- source payload size optionnelle bornée à 64 MiB.
|
||||
|
||||
`is_startup` reste un `BOOLEAN NULL` sans contrainte artificielle supplémentaire.
|
||||
|
||||
## 4. Index de navigation
|
||||
|
||||
Un seul index métier est ajouté :
|
||||
|
||||
```text
|
||||
ix_ksp_raw_account_states_slot_pubkey_state_hash
|
||||
(slot, pubkey, state_hash)
|
||||
```
|
||||
|
||||
Il est non unique et sans prédicat. Aucun index owner/provider/method/time/status n'est introduit sans query publique qui le justifie.
|
||||
|
||||
## 5. Checksum V002 final et transition de prerelease
|
||||
|
||||
Checksum V002 intermédiaire écrit par `pre.002` :
|
||||
|
||||
```text
|
||||
30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
|
||||
```
|
||||
|
||||
Checksum V002 final calculé sur les 32 resources ordonnées :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
V000 et V001 restent byte-inchangées et conservent leurs checksums :
|
||||
|
||||
```text
|
||||
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
```
|
||||
|
||||
Le moteur accepte le checksum V002 intermédiaire **uniquement** avec `schema_update=update_if_needed`. Dans ce cas, sous la transaction de migration existante :
|
||||
|
||||
1. l'historique reconnaît uniquement le checksum provisoire explicitement connu ;
|
||||
2. les resources V002 manquantes sont matérialisées et réinspectées ;
|
||||
3. la compatibilité externe V002 est vérifiée ;
|
||||
4. l'entrée V002 de `ksp_store_schema_migrations` est remplacée par le checksum final avec un `UPDATE` conditionné par version, nom et ancien checksum ;
|
||||
5. le commit publie ensemble schéma et historique final.
|
||||
|
||||
Avec `schema_update=disabled`, le checksum provisoire reste un `MigrationMismatch`. Tout checksum V002 inconnu reste rejeté même avec update activé.
|
||||
|
||||
## 6. Schema compatibility V002
|
||||
|
||||
La vérification externe générique couvre désormais V001 et V002 avec leurs inventaires propres. Pour les deux tables account, elle conserve les mêmes principes que V001 :
|
||||
|
||||
- colonnes attendues et types physiques compatibles ;
|
||||
- contraintes KSP attendues reconnues par nom/définition ;
|
||||
- extension externe bloquante rejetée ;
|
||||
- unique index externe non adossé à une contrainte rejeté ;
|
||||
- triggers/rules externes rejetés.
|
||||
|
||||
Le probe d'adoption inclut désormais les deux tables V002 afin qu'un schéma KSP account existant sans metadata de migration ne soit pas considéré comme vide.
|
||||
|
||||
Le contrat d'index interne supporte explicitement les deux formes possédées : V001 avec prédicat `retention_state <> 'purged'`, V002 sans prédicat.
|
||||
|
||||
## 7. Canaries et tests modifiés
|
||||
|
||||
Les canaries backend prouvent désormais :
|
||||
|
||||
- 32 resources V002, ordonnées par tables/constraints/index ;
|
||||
- inventaire exact 2/29/1 ;
|
||||
- checksum final exact et checksums V000/V001 stables ;
|
||||
- acceptation conditionnelle du checksum provisoire et rejet des historiques arbitraires ;
|
||||
- bornes physiques majeures et absence de narrowing `u64` ;
|
||||
- index unique de navigation non filtré/non unique ;
|
||||
- compatibilité externe V002 ;
|
||||
- absence persistante de `src/raw_account.rs`, d'implémentation `RawAccount*` et de dispatch runtime account.
|
||||
|
||||
## 8. Scope négatif préservé
|
||||
|
||||
Cette tranche n'ajoute pas :
|
||||
|
||||
```text
|
||||
ksp-store-api change
|
||||
raw_account repository module
|
||||
RawAccount* implementation on PostgresBackend
|
||||
RawAccount* implementation on Store
|
||||
Store account dispatch
|
||||
account retention/archive/purge
|
||||
owner/provider/time/status indexes
|
||||
worker/job/backfill policy
|
||||
Transport -> Store coupling
|
||||
new backend
|
||||
```
|
||||
|
||||
## 9. Documentation
|
||||
|
||||
`docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md` conserve le forecast souple sous forme de sous-paragraphes `### pre.NNN` / `#### pre.NNN-fix.MMM`. `pre.002-fix.001` y passe au statut gate opérateur complet PASS et `pre.003` au statut réalisé / gate opérateur à rejouer.
|
||||
|
||||
`docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md` enregistre le gate vert de la base, l'inventaire V002 final, les checksums et la stratégie de transition de prerelease.
|
||||
|
||||
## 10. Validation exécutée dans l'environnement d'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
=> General Rust rule audit: clean
|
||||
=> Rust export completeness audit: 0 candidate(s)
|
||||
=> KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
=> Markdown table audit: clean (239 table(s), 138 file(s))
|
||||
```
|
||||
|
||||
Le calcul indépendant de checksum sur les bytes des resources V002 donne :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
`cargo`, `clippy` et les tests Rust ne sont pas disponibles dans l'environnement d'assemblage. Aucun résultat Cargo de `pre.003` n'est donc revendiqué ici.
|
||||
|
||||
## 11. Gate opérateur demandé
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
|
||||
Les preuves PostgreSQL live account restent hors de cette tranche et sont prévues plus tard dans le forecast.
|
||||
|
||||
## 12. Fichiers
|
||||
|
||||
Le delta contient uniquement `Cargo.toml`, les fichiers Rust/tests/schema réellement modifiés, les nouvelles resources V002, les deux documents de suivi modifiés et ce fichier `pre.003.md`. Aucun snapshot complet, aucune archive historique et aucun artefact de build ne sont inclus.
|
||||
|
||||
## 13. Verdict
|
||||
|
||||
`0.3.4-pre.003` : **READY FOR OPERATOR GATE**.
|
||||
119
deltas/0.3.4/pre.004-fix.001.md
Normal file
119
deltas/0.3.4/pre.004-fix.001.md
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- file: deltas/0.3.4/pre.004-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.004-fix.001` — conformité Clippy du mapping PostgreSQL account
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.4
|
||||
```
|
||||
|
||||
Le gate opérateur du 2026-08-30 confirme que `pre.004` compile et que les suites ciblées passent, mais `cargo clippy --workspace --all-targets` échoue sur le nouveau mapping `RawAccountState`.
|
||||
|
||||
## 2. Anomalies corrigées
|
||||
|
||||
Dans `src/raw_account.rs`, les deux helpers d'extraction physique utilisent 27 formes :
|
||||
|
||||
```text
|
||||
row.try_get(...).map_err(...)?
|
||||
```
|
||||
|
||||
Le profil workspace nie `clippy::question-mark-used` et `clippy::implicit-return`. Chaque occurrence génère donc deux erreurs Clippy, soit 54 diagnostics.
|
||||
|
||||
Le même gate signale un warning `unused_variables` pour `crate_root` dans `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
|
||||
|
||||
## 3. Correctif
|
||||
|
||||
Les 27 extractions sont dépliées en `match` typés :
|
||||
|
||||
```text
|
||||
Ok(value) -> value
|
||||
Err(_) -> return Err(DataInvalid)
|
||||
```
|
||||
|
||||
La logique et les phases d'erreur restent strictement identiques. Aucune macro, nouveau helper partagé ou nouvelle dépendance n'est introduit.
|
||||
|
||||
La variable `crate_root` réellement inutilisée est supprimée du seul canari concerné. Le canari `pre_004_raw_account_read_sql_and_mapping_remain_backend_private_and_read_only` conserve sa propre lecture de `src/lib.rs`, qui reste utilisée.
|
||||
|
||||
## 4. Version
|
||||
|
||||
Deux fichiers Rust sont modifiés. Conformément à `VER-ID-007/010`, le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.4.fix.1
|
||||
```
|
||||
|
||||
## 5. Scope inchangé
|
||||
|
||||
Le fix ne modifie pas :
|
||||
|
||||
```text
|
||||
GET_ACCOUNT_STATE_SQL
|
||||
GET_ACCOUNT_OBSERVATION_SQL
|
||||
signatures get_raw_account_state/get_raw_account_observation
|
||||
modèles ksp-store-api
|
||||
write SQL account
|
||||
pagination/cursor KSPA
|
||||
impl RawAccount*
|
||||
Store dispatch account
|
||||
migrations V000/V001/V002
|
||||
```
|
||||
|
||||
Le checksum V002 final reste :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 6. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 7. Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.004-fix.001.md
|
||||
```
|
||||
|
||||
## 8. Validations exécutées dans l'environnement d'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
PASS
|
||||
|
||||
grep '?' crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
aucune occurrence
|
||||
```
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage ; aucun gate Cargo post-fix n'est revendiqué PASS ici.
|
||||
|
||||
## 9. Gate opérateur à rejouer
|
||||
|
||||
```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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 10. Verdict
|
||||
|
||||
`pre.004-fix.001` reste strictement dans la responsabilité de `pre.004` : il corrige uniquement la conformité Clippy et l'hygiène du canari du mapping/read slice déjà introduit, sans ajouter de fonctionnalité.
|
||||
298
deltas/0.3.4/pre.004.md
Normal file
298
deltas/0.3.4/pre.004.md
Normal file
@@ -0,0 +1,298 @@
|
||||
<!-- file: deltas/0.3.4/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.004` — mapping PostgreSQL privé et lectures `RawAccountState`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.3.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 pour `pre.003-fix.001` est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 239 tables / 139 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS — 45 unit tests + canaris, live ignored
|
||||
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
V002 finale et son checksum sont donc considérés acquis avant l'ouverture du mapping account.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Implémenter la tranche de lecture de la vertical slice PostgreSQL `RawAccountState` sans ouvrir les writes, la pagination ni les implémentations de traits :
|
||||
|
||||
```text
|
||||
mapping SQL privé state
|
||||
mapping SQL privé observation
|
||||
get_raw_account_state
|
||||
get_raw_account_observation
|
||||
hostile-row guards
|
||||
```
|
||||
|
||||
Les méthodes du backend retournent uniquement des modèles `ksp-store-api`. Aucun `tokio_postgres::Row`, SQL, bind, SQLSTATE ou type physique ne traverse le bridge public.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.4
|
||||
```
|
||||
|
||||
Aucune crate ne redéfinit localement la version héritée.
|
||||
|
||||
## 4. Module physique privé
|
||||
|
||||
Nouveau module :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
```
|
||||
|
||||
Il possède les deux SELECT et les codecs physiques de cette tranche. `runtime.rs` ne contient aucun SQL métier et délègue les lectures au module privé.
|
||||
|
||||
La tranche est strictement read-only :
|
||||
|
||||
```text
|
||||
INSERT INTO absent
|
||||
UPDATE absent
|
||||
DELETE FROM absent
|
||||
ON CONFLICT absent
|
||||
FOR UPDATE absent
|
||||
list/cursor account absent
|
||||
```
|
||||
|
||||
Les écritures state+observation restent `pre.005`, l'observation supplémentaire `pre.006`, la pagination/cursor `pre.007` et les quatre `impl RawAccount*` `pre.008`.
|
||||
|
||||
## 5. `get_raw_account_state`
|
||||
|
||||
Le SELECT adresse exactement la clé physique :
|
||||
|
||||
```text
|
||||
(pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
Le mapping reconstruit :
|
||||
|
||||
```text
|
||||
network <- backend mono-réseau
|
||||
pubkey <- BYTEA exactement 32 bytes
|
||||
slot <- NUMERIC(20,0)::text -> u64
|
||||
state_hash <- BYTEA exactement 32 bytes
|
||||
lamports <- NUMERIC(20,0)::text -> u64
|
||||
owner <- BYTEA exactement 32 bytes
|
||||
executable <- BOOLEAN
|
||||
rent_epoch <- NUMERIC(20,0)::text -> u64
|
||||
data <- BYTEA exact -> RawAccountState::try_new
|
||||
```
|
||||
|
||||
Le chemin décimal couvre tout le domaine `u64`, y compris les valeurs supérieures à `i64::MAX` et `u64::MAX` lui-même. Aucun narrowing via `BIGINT` n'est introduit.
|
||||
|
||||
`RawAccountState::try_new` revalide la borne backend-agnostic des bytes account : data vide est valide ; une ligne hostile dépassant 16 MiB devient `DataInvalid`.
|
||||
|
||||
Après décodage, la référence reconstruite doit rester exactement égale à la référence demandée. Une cardinalité autre que 0/1 est également `DataInvalid`.
|
||||
|
||||
## 6. Garde réseau pré-I/O
|
||||
|
||||
`get_raw_account_state` vérifie avant `pool.get()` :
|
||||
|
||||
```text
|
||||
reference.network == backend.network -> continuer
|
||||
sinon -> WrongNetwork
|
||||
```
|
||||
|
||||
Le mauvais réseau ne consomme donc aucune connexion PostgreSQL et n'expose aucune valeur fournie.
|
||||
|
||||
## 7. `get_raw_account_observation`
|
||||
|
||||
`RawObservationKey` ne porte pas de réseau. Le réseau de la `RawAccountStateReference` reconstruite provient donc exclusivement du backend mono-réseau déjà lié par `ksp_store_identity`.
|
||||
|
||||
Le mapping couvre :
|
||||
|
||||
```text
|
||||
observation_key
|
||||
account.pubkey
|
||||
account.slot
|
||||
account.state_hash
|
||||
provider
|
||||
protocol
|
||||
acquisition_method
|
||||
origin
|
||||
received_at
|
||||
capture_session_id optionnel
|
||||
commitment optionnel
|
||||
endpoint_id optionnel
|
||||
filter_id optionnel
|
||||
observed_at optionnel
|
||||
source_payload_hash optionnel
|
||||
source_payload_size_bytes optionnel
|
||||
is_startup optionnel
|
||||
transaction_signature optionnelle
|
||||
write_version optionnel
|
||||
```
|
||||
|
||||
Spécificités physiques :
|
||||
|
||||
```text
|
||||
account.slot / write_version -> NUMERIC(20,0)::text -> u64
|
||||
transaction_signature -> NULL ou exactement 64 bytes
|
||||
hash/key/pubkey -> exactement 32 bytes
|
||||
received/observed -> BIGINT -> u64 -> RawTimestamp
|
||||
source payload size -> BIGINT -> u64 + borne API 64 MiB
|
||||
provenance codes -> constructeurs API fallibles
|
||||
origin -> cinq variantes API uniquement
|
||||
```
|
||||
|
||||
Les metadata Yellowstone restent strictement observation-only. Une colonne NULL reste `None`; aucune valeur `false`, zéro, signature ou write version n'est inventée.
|
||||
|
||||
## 8. Hostile-row guards
|
||||
|
||||
Les tests unitaires couvrent notamment :
|
||||
|
||||
```text
|
||||
state slot/lamports/rent_epoch = u64::MAX
|
||||
write_version = u64::MAX
|
||||
account data vide
|
||||
account data > 16 MiB -> DataInvalid
|
||||
pubkey de 31 bytes -> DataInvalid
|
||||
signature transaction de 63 bytes -> DataInvalid
|
||||
décimal négatif ou > u64::MAX -> DataInvalid
|
||||
origin hostile -> DataInvalid
|
||||
observed_at > received_at -> DataInvalid
|
||||
metadata optionnelles absentes -> None
|
||||
wrong network state reference -> WrongNetwork pré-I/O
|
||||
```
|
||||
|
||||
Les erreurs ne retiennent que `PostgresBackendErrorKind` + phase `&'static str`; les chaînes hostiles ne sont jamais interpolées dans l'erreur.
|
||||
|
||||
## 9. Surface backend
|
||||
|
||||
`PostgresBackend` expose désormais le bridge étroit :
|
||||
|
||||
```text
|
||||
get_raw_account_state
|
||||
get_raw_account_observation
|
||||
```
|
||||
|
||||
Ces méthodes ne constituent pas encore les implémentations de `RawAccountStateRead` et `RawAccountObservationRead` : le premier trait exige aussi `list_raw_account_states`, réservé à `pre.007`. Les quatre traits account seront ouverts ensemble en `pre.008` afin de passer directement de 6/10 à 10/10 capabilities physiques.
|
||||
|
||||
## 10. Canaries transformés
|
||||
|
||||
Les canaris historiques qui interdisaient tout module account sont ajustés sans perdre leur rôle :
|
||||
|
||||
- `dependency_boundary.rs` exige un module `raw_account` privé, read-only, sans pagination ni trait impl ;
|
||||
- `hardening_completeness.rs` inclut le nouveau module dans l'inventaire exact et dans les scans anti-secret/anti-reverse-edge ;
|
||||
- les interdictions des quatre `impl RawAccount* for PostgresBackend` restent actives ;
|
||||
- `public_api.rs` prouve que les deux méthodes du bridge n'utilisent que les modèles backend-agnostic.
|
||||
|
||||
Les canaris V000/health business-free restent inchangés.
|
||||
|
||||
## 11. Migrations
|
||||
|
||||
Aucune ressource sous `migrations/` n'est modifiée.
|
||||
|
||||
Checksums conservés :
|
||||
|
||||
```text
|
||||
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
Le bridge du checksum provisoire `pre.002` reste inchangé.
|
||||
|
||||
## 12. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
|
||||
deltas/0.3.4/pre.004.md
|
||||
```
|
||||
|
||||
## 13. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
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
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 14. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 15. Validations exécutées
|
||||
|
||||
Dans l'environnement d'assemblage :
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
PASS après alignement RustRover des tableaux touchés
|
||||
```
|
||||
|
||||
Le contrôle différentiel doit également confirmer qu'aucune ressource V000/V001/V002 n'a changé.
|
||||
|
||||
## 16. Validations non exécutées dans l'environnement d'assemblage
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans cet environnement. Ne sont donc pas revendiqués PASS ici :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
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
|
||||
```
|
||||
|
||||
## 17. Décisions prises
|
||||
|
||||
- Réutiliser la frontière éprouvée de `0.3.3-pre.004` : SQL/mapping privé, méthodes backend étroites, traits plus tard.
|
||||
- Garder les conversions `NUMERIC(20,0)` via `::text -> u64` afin de ne pas ajouter de dépendance décimale ni de narrowing.
|
||||
- Revalider les invariants API au read même si V002 possède déjà des CHECK SQL ; la DB peut être externe, héritée ou hostile.
|
||||
- Ne pas factoriser prématurément les codecs transaction/account dans un nouveau module commun : les familles ont des formes différentes et `pre.004` doit rester bornée.
|
||||
- Ne pas ouvrir `RawAccountStateRead` avant que `list_raw_account_states` existe ; l'inventaire de capabilities reste volontairement 6/10 jusqu'à `pre.008`.
|
||||
|
||||
## 18. Questions ouvertes
|
||||
|
||||
Aucune question bloquante nouvelle. Les détails des writes, races et cursor restent ceux figés par le plan `pre.001` et appartiennent respectivement à `pre.005`, `pre.006` et `pre.007`.
|
||||
|
||||
## 19. Gate opérateur attendu
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
|
||||
Aucun test live PostgreSQL account n'est demandé dans cette tranche ; la preuve live complète reste `pre.009`.
|
||||
308
deltas/0.3.4/pre.005.md
Normal file
308
deltas/0.3.4/pre.005.md
Normal file
@@ -0,0 +1,308 @@
|
||||
<!-- file: deltas/0.3.4/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.005` — acquisition atomique et idempotence `RawAccountState`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.4.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 pour `pre.004-fix.001` est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 239 tables / 141 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS — 52 unit tests + canaris, live ignored
|
||||
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
Le mapping/read account de `pre.004` est donc considéré acquis avant l'ouverture des writes d'acquisition.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Implémenter uniquement l'écriture atomique du couple :
|
||||
|
||||
```text
|
||||
RawAccountState
|
||||
+
|
||||
RawAccountObservation
|
||||
```
|
||||
|
||||
avec les invariants figés par `pre.001` :
|
||||
|
||||
```text
|
||||
guards réseau/référence avant I/O
|
||||
state insert idempotent
|
||||
collision state -> FOR UPDATE + comparaison exacte
|
||||
observation insert idempotente
|
||||
collision observation -> FOR UPDATE + comparaison exacte
|
||||
commit unique
|
||||
aucun overwrite silencieux
|
||||
```
|
||||
|
||||
L'observation supplémentaire sans retransmettre les bytes account reste réservée à `pre.006`.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.5
|
||||
```
|
||||
|
||||
Aucune crate ne redéfinit localement la version héritée.
|
||||
|
||||
## 4. Préconditions avant I/O
|
||||
|
||||
`persist_raw_account_acquisition` valide avant `pool.get()` :
|
||||
|
||||
```text
|
||||
state.reference.network == backend.network
|
||||
observation.account.network == backend.network
|
||||
observation.account == state.reference
|
||||
```
|
||||
|
||||
Un mauvais réseau devient `WrongNetwork`. Une référence observation/state divergente devient `Conflict`. Aucune connexion PostgreSQL n'est consommée avant ces gardes.
|
||||
|
||||
## 5. Insertion canonique du state
|
||||
|
||||
Le state utilise :
|
||||
|
||||
```text
|
||||
INSERT INTO ksp_raw_account_states (...)
|
||||
ON CONFLICT (pubkey, slot, state_hash) DO NOTHING
|
||||
RETURNING pubkey
|
||||
```
|
||||
|
||||
Les domaines `u64` restent encodés sans narrowing via chaînes décimales et cast PostgreSQL `TEXT::NUMERIC` :
|
||||
|
||||
```text
|
||||
slot
|
||||
lamports
|
||||
rent_epoch
|
||||
```
|
||||
|
||||
Les pubkeys/hash restent en bytes fixes et `data` est persistée sans base64 ni copie structurale.
|
||||
|
||||
Si l'INSERT gagne :
|
||||
|
||||
```text
|
||||
RawEntityWriteOutcome::Inserted
|
||||
```
|
||||
|
||||
Si l'identité existe déjà, le backend charge la ligne canonique sous :
|
||||
|
||||
```text
|
||||
FOR UPDATE
|
||||
```
|
||||
|
||||
et compare exactement :
|
||||
|
||||
```text
|
||||
reference
|
||||
lamports
|
||||
owner
|
||||
executable
|
||||
rent_epoch
|
||||
data
|
||||
```
|
||||
|
||||
Le `state_hash` ne suffit donc jamais à déclarer l'idempotence. Contenu identique -> `AlreadyPresent`; contenu divergent -> `Conflict`.
|
||||
|
||||
## 6. Insertion idempotente de l'observation
|
||||
|
||||
L'observation est encodée avec toute la provenance et toutes les metadata optionnelles :
|
||||
|
||||
```text
|
||||
observation_key
|
||||
account pubkey/slot/state_hash
|
||||
provider
|
||||
protocol
|
||||
acquisition_method
|
||||
origin
|
||||
received_at
|
||||
capture_session_id
|
||||
commitment
|
||||
endpoint_id
|
||||
filter_id
|
||||
observed_at
|
||||
source_payload_hash
|
||||
source_payload_size_bytes
|
||||
is_startup
|
||||
transaction_signature
|
||||
write_version
|
||||
```
|
||||
|
||||
Le SQL utilise :
|
||||
|
||||
```text
|
||||
ON CONFLICT (observation_key) DO NOTHING
|
||||
```
|
||||
|
||||
En cas de collision, la ligne existante est relue `FOR UPDATE`, décodée avec le codec hostile-row de `pre.004`, puis comparée au `RawAccountObservation` entrant complet. Identique -> `AlreadyPresent`; divergent -> `Conflict`.
|
||||
|
||||
Aucun champ optionnel n'est ignoré dans l'idempotence.
|
||||
|
||||
## 7. Atomicité
|
||||
|
||||
State et observation vivent dans la même transaction PostgreSQL :
|
||||
|
||||
```text
|
||||
BEGIN
|
||||
state insert/compare
|
||||
observation insert/compare
|
||||
COMMIT
|
||||
```
|
||||
|
||||
Si un nouveau state a été inséré mais que l'observation collide avec un contenu divergent, le chemin retourne une erreur avant commit. Le drop de la transaction annule alors l'insertion du state : aucune moitié d'acquisition durable n'est admise.
|
||||
|
||||
Aucun pré-check `has_*`, aucun `ON CONFLICT DO UPDATE`, aucun `UPDATE` et aucun `DELETE` account n'est introduit.
|
||||
|
||||
## 8. Surface backend
|
||||
|
||||
`PostgresBackend` expose le bridge étroit :
|
||||
|
||||
```text
|
||||
persist_raw_account_acquisition(
|
||||
RawAccountState,
|
||||
RawAccountObservation,
|
||||
) -> RawAcquisitionWriteOutcome
|
||||
```
|
||||
|
||||
Le bridge reste backend-agnostic et n'expose aucun type PostgreSQL.
|
||||
|
||||
Cette méthode n'implémente pas encore `RawAccountStateWrite`. Les quatre traits account restent volontairement absents jusqu'à `pre.008` afin d'ouvrir la conformance physique 10/10 en une seule tranche.
|
||||
|
||||
## 9. Tests et canaries
|
||||
|
||||
Les tests unitaires ajoutés couvrent :
|
||||
|
||||
```text
|
||||
guard réseau state
|
||||
référence observation/state exacte
|
||||
state identique -> comparaison vraie
|
||||
même référence/hash + lamports divergent -> comparaison fausse
|
||||
observation identique -> égalité exacte
|
||||
même clé/référence + write_version divergent -> non-identique
|
||||
encodage exact des cinq RawAcquisitionOrigin
|
||||
```
|
||||
|
||||
Le canari `dependency_boundary` exige maintenant :
|
||||
|
||||
```text
|
||||
INSERT_ACCOUNT_STATE_SQL
|
||||
INSERT_ACCOUNT_OBSERVATION_SQL
|
||||
LOCK_ACCOUNT_STATE_SQL
|
||||
LOCK_ACCOUNT_OBSERVATION_SQL
|
||||
ON CONFLICT ... DO NOTHING
|
||||
FOR UPDATE
|
||||
persist_raw_account_acquisition
|
||||
```
|
||||
|
||||
et continue d'interdire :
|
||||
|
||||
```text
|
||||
UPDATE / DELETE account
|
||||
ON CONFLICT DO UPDATE
|
||||
record_raw_account_observation
|
||||
list_raw_account_states
|
||||
OFFSET
|
||||
les quatre impl RawAccount*
|
||||
```
|
||||
|
||||
`public_api.rs` vérifie le nouveau bridge backend-agnostic.
|
||||
|
||||
## 10. Scope explicitement reporté
|
||||
|
||||
```text
|
||||
record_raw_account_observation pre.006
|
||||
races/cancellation ciblées pre.006
|
||||
list_raw_account_states pre.007
|
||||
cursor KSPA pre.007
|
||||
4 impl RawAccount* PostgresBackend pre.008
|
||||
4 dispatch Store pre.008
|
||||
live PostgreSQL account pre.009
|
||||
```
|
||||
|
||||
## 11. Migrations
|
||||
|
||||
Aucune ressource sous `migrations/` n'est modifiée.
|
||||
|
||||
Checksums conservés :
|
||||
|
||||
```text
|
||||
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 12. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/src/lib.rs
|
||||
crates/ksp-store-postgres-lib/src/raw_account.rs
|
||||
crates/ksp-store-postgres-lib/src/runtime.rs
|
||||
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 13. Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.005.md
|
||||
```
|
||||
|
||||
## 14. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 15. Validations exécutées dans l'environnement d'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
PASS
|
||||
```
|
||||
|
||||
Les contrôles différentiels doivent également confirmer l'identité byte-à-byte de V000/V001/V002 et l'absence de surface `pre.006+`.
|
||||
|
||||
## 16. Validations non exécutées dans l'environnement d'assemblage
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Aucun résultat Cargo post-`pre.005` n'est revendiqué ici.
|
||||
|
||||
Gate opérateur à rejouer :
|
||||
|
||||
```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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 17. Verdict
|
||||
|
||||
`pre.005` ouvre uniquement l'acquisition atomique `RawAccountState + RawAccountObservation`. L'observation supplémentaire, les races/cancellations dédiées, la pagination et les capabilities account restent fermées conformément au sizing souple de `0.3.4`.
|
||||
148
deltas/0.3.4/pre.006.md
Normal file
148
deltas/0.3.4/pre.006.md
Normal file
@@ -0,0 +1,148 @@
|
||||
<!-- file: deltas/0.3.4/pre.006.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.006` — observation account supplémentaire + contrat race/cancellation
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.5
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 pour `pre.005` est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 239 tables / 142 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS — 56 unit tests + canaris, live ignored
|
||||
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
L'acquisition atomique/idempotente de `pre.005` est donc considérée acquise.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Implémenter uniquement l'observation supplémentaire d'un `RawAccountState` déjà durable :
|
||||
|
||||
```text
|
||||
network guard avant I/O
|
||||
state reference obligatoire
|
||||
FOR KEY SHARE
|
||||
observation idempotente existante
|
||||
aucun state implicite
|
||||
aucun overwrite
|
||||
transaction unique
|
||||
```
|
||||
|
||||
La pagination account et les quatre implémentations de capabilities restent hors tranche.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.6
|
||||
```
|
||||
|
||||
## 4. Observation supplémentaire
|
||||
|
||||
`record_raw_account_observation` valide d'abord :
|
||||
|
||||
```text
|
||||
observation.account.network == backend.network
|
||||
```
|
||||
|
||||
avant `pool.get()`.
|
||||
|
||||
La transaction exécute ensuite :
|
||||
|
||||
```text
|
||||
SELECT 1
|
||||
FROM ksp_raw_account_states
|
||||
WHERE pubkey = $1
|
||||
AND slot = $2::TEXT::NUMERIC
|
||||
AND state_hash = $3
|
||||
FOR KEY SHARE
|
||||
```
|
||||
|
||||
Une référence absente retourne `ReferenceNotFound` avec phase statique `raw_account_observation_reference`. Aucun state n'est créé ou modifié.
|
||||
|
||||
## 5. Idempotence / conflit
|
||||
|
||||
Après le lock de référence, la tranche réutilise `persist_account_observation_row` de `pre.005` :
|
||||
|
||||
```text
|
||||
INSERT ... ON CONFLICT (observation_key) DO NOTHING
|
||||
collision -> SELECT ... FOR UPDATE
|
||||
identique -> AlreadyPresent
|
||||
divergent -> Conflict
|
||||
```
|
||||
|
||||
La comparaison porte sur le `RawAccountObservation` complet : référence, provenance et metadata optionnelles incluses.
|
||||
|
||||
## 6. Concurrence et cancellation unitaires
|
||||
|
||||
Les canaris de `pre.006` figent l'ordre :
|
||||
|
||||
```text
|
||||
client.transaction()
|
||||
FOR KEY SHARE référence
|
||||
persist_account_observation_row
|
||||
sql_transaction.commit()
|
||||
```
|
||||
|
||||
Le chemin ne détache aucune tâche (`tokio::spawn` absent), ne fuit pas le handle (`std::mem::forget` absent) et ne commit pas avant l'insertion/comparaison observation. Si la future est annulée avant le commit, le `Transaction` local est droppé et la transaction PostgreSQL non committée est rollback.
|
||||
|
||||
La preuve concurrente réelle avec tâches simultanées et `JoinHandle::abort()` reste explicitement réservée au live `pre.009`.
|
||||
|
||||
## 7. Bridge backend
|
||||
|
||||
`PostgresBackend` expose :
|
||||
|
||||
```text
|
||||
record_raw_account_observation(
|
||||
RawAccountObservation,
|
||||
) -> RawObservationWriteOutcome
|
||||
```
|
||||
|
||||
Le bridge reste construit uniquement avec des types `ksp-store-api`. `RawAccountObservationWrite` n'est pas encore implémenté ; l'ouverture des quatre traits account reste groupée en `pre.008`.
|
||||
|
||||
## 8. Scope négatif
|
||||
|
||||
Restent absents :
|
||||
|
||||
```text
|
||||
list_raw_account_states
|
||||
cursor KSPA
|
||||
OFFSET
|
||||
UPDATE / DELETE account
|
||||
ON CONFLICT DO UPDATE
|
||||
impl RawAccountStateRead for PostgresBackend
|
||||
impl RawAccountStateWrite for PostgresBackend
|
||||
impl RawAccountObservationRead for PostgresBackend
|
||||
impl RawAccountObservationWrite for PostgresBackend
|
||||
Store dispatch account
|
||||
```
|
||||
|
||||
Toutes les migrations V000/V001/V002 restent byte-inchangées. Le checksum V002 final reste :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 9. Validations d'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
|
||||
migrations V000/V001/V002 byte-identiques à pre.005 PASS
|
||||
scope pre.007+ absent PASS
|
||||
```
|
||||
|
||||
Le gate Cargo/Clippy/tests de `pre.006` doit être rejoué par l'opérateur.
|
||||
70
deltas/0.3.4/pre.007-fix.001.md
Normal file
70
deltas/0.3.4/pre.007-fix.001.md
Normal file
@@ -0,0 +1,70 @@
|
||||
<!-- file: deltas/0.3.4/pre.007-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.007-fix.001`
|
||||
|
||||
## Base
|
||||
|
||||
```text
|
||||
0.3.4-pre.007
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger exclusivement le gate opérateur de `pre.007` : cinq warnings Clippy `clone_on_copy` dans les tests cursor account et deux canaris historiques `pre.005/pre.006` qui interdisaient encore `list_raw_account_states` alors que cette surface est devenue légitime en `pre.007`.
|
||||
|
||||
## Modifications
|
||||
|
||||
- `workspace.package.version` passe à `0.3.4-pre.7.fix.1` ;
|
||||
- suppression des cinq `Pubkey::clone()` inutiles dans `unit_tests/raw_account.rs` ;
|
||||
- transformation des canaris historiques `pre.005/pre.006` pour conserver les interdictions destructives et les interdictions de capabilities `pre.008`, sans interdire la pagination account désormais possédée ;
|
||||
- mise à jour du plan et de la validation pour tracer le gate et le correctif.
|
||||
|
||||
## Invariants préservés
|
||||
|
||||
```text
|
||||
aucun changement src/raw_account.rs
|
||||
aucun changement src/raw_account/cursor.rs
|
||||
aucun changement src/runtime.rs
|
||||
aucun changement SQL runtime
|
||||
aucune migration V000/V001/V002 modifiée
|
||||
aucune impl RawAccount* ouverte
|
||||
aucun dispatch Store account ajouté
|
||||
```
|
||||
|
||||
Le checksum V002 reste :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.007-fix.001.md
|
||||
```
|
||||
|
||||
## Gate opérateur à rejouer
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
248
deltas/0.3.4/pre.007.md
Normal file
248
deltas/0.3.4/pre.007.md
Normal file
@@ -0,0 +1,248 @@
|
||||
<!-- file: deltas/0.3.4/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.007` — pagination account keyset + cursor `KSPA`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.6
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 pour `pre.006` est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 239 tables / 143 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS — 58 unit tests + canaris, live ignored
|
||||
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
L'observation supplémentaire account et son contrat transactionnel/cancellation sont donc considérés acquis.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Implémenter uniquement la navigation `RawAccountStateQuery` :
|
||||
|
||||
```text
|
||||
ordre total (slot, pubkey, state_hash)
|
||||
ASC / DESC complètement inversés
|
||||
filtre pubkey optionnel
|
||||
bornes de slot inclusives
|
||||
keyset pagination uniquement
|
||||
cursor account KSPA V1
|
||||
LIMIT + 1 sans policy KSP
|
||||
```
|
||||
|
||||
Les quatre capabilities account et leur dispatch `Store` restent hors tranche jusqu'à `pre.008`.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.7
|
||||
```
|
||||
|
||||
## 4. Requêtes physiques
|
||||
|
||||
`raw_account.rs` ajoute quatre SELECT privés :
|
||||
|
||||
```text
|
||||
sans pubkey : ASC / DESC
|
||||
avec pubkey : ASC / DESC
|
||||
```
|
||||
|
||||
L'ordre total est :
|
||||
|
||||
```text
|
||||
ASC (slot ASC, pubkey ASC, state_hash ASC)
|
||||
DESC (slot DESC, pubkey DESC, state_hash DESC)
|
||||
```
|
||||
|
||||
Les continuations utilisent exclusivement :
|
||||
|
||||
```text
|
||||
(slot, pubkey, state_hash) > (last_slot, last_pubkey, last_state_hash)
|
||||
(slot, pubkey, state_hash) < (last_slot, last_pubkey, last_state_hash)
|
||||
```
|
||||
|
||||
Sans filtre, l'index V002 `(slot, pubkey, state_hash)` porte la navigation. Avec filtre pubkey, la PK `(pubkey, slot, state_hash)` porte le préfixe account et le même ordre déterministe.
|
||||
|
||||
## 5. Cursor account V1
|
||||
|
||||
Nouveau module privé :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
|
||||
```
|
||||
|
||||
Format fixe :
|
||||
|
||||
```text
|
||||
0..4 magic KSPA
|
||||
4 version 1
|
||||
5..13 last_slot u64 big-endian
|
||||
13..45 last_pubkey 32 bytes
|
||||
45..77 last_state_hash 32 bytes
|
||||
77..109 SHA-256 binding digest
|
||||
```
|
||||
|
||||
Domaine :
|
||||
|
||||
```text
|
||||
KSP/raw-account-state-cursor/v1
|
||||
```
|
||||
|
||||
Le digest lie :
|
||||
|
||||
```text
|
||||
network
|
||||
pubkey filter presence/value
|
||||
direction
|
||||
slot.start_inclusive
|
||||
slot.end_inclusive
|
||||
last_slot
|
||||
last_pubkey
|
||||
last_state_hash
|
||||
```
|
||||
|
||||
Un cursor transaction `KSPT` ne peut donc pas être rejoué comme cursor account et échoue dès le magic.
|
||||
|
||||
## 6. Limite physique de page
|
||||
|
||||
Comme la verticale transaction, le backend applique `LIMIT + 1` uniquement pour détecter une page suivante. Aucun maximum fonctionnel KSP n'est ajouté.
|
||||
|
||||
La limite physique exacte reste :
|
||||
|
||||
```text
|
||||
9_223_372_036_854_775_806
|
||||
```
|
||||
|
||||
car `requested + 1` doit être représentable par un `i64` PostgreSQL.
|
||||
|
||||
## 7. Mapping de liste
|
||||
|
||||
Chaque row de liste revalide :
|
||||
|
||||
```text
|
||||
pubkey exact 32 bytes
|
||||
slot NUMERIC(20,0)::text -> u64 exact
|
||||
state_hash exact 32 bytes
|
||||
```
|
||||
|
||||
Le résultat public du bridge est exclusivement :
|
||||
|
||||
```text
|
||||
RawPage<RawAccountStateReference>
|
||||
```
|
||||
|
||||
Aucun row PostgreSQL, SQL ou cursor décodé ne traverse la frontière backend.
|
||||
|
||||
## 8. Bridge backend
|
||||
|
||||
`PostgresBackend` expose :
|
||||
|
||||
```text
|
||||
list_raw_account_states(
|
||||
&RawAccountStateQuery,
|
||||
) -> RawPage<RawAccountStateReference>
|
||||
```
|
||||
|
||||
Ce bridge reste une méthode étroite. `RawAccountStateRead`, `RawAccountStateWrite`, `RawAccountObservationRead` et `RawAccountObservationWrite` ne sont toujours pas implémentés sur `PostgresBackend`.
|
||||
|
||||
## 9. Scope négatif
|
||||
|
||||
Restent absents :
|
||||
|
||||
```text
|
||||
OFFSET
|
||||
batch max 500/1000
|
||||
priority / worker policy
|
||||
clamp/min artificiel
|
||||
UPDATE / DELETE account
|
||||
ON CONFLICT DO UPDATE
|
||||
impl RawAccountStateRead for PostgresBackend
|
||||
impl RawAccountStateWrite for PostgresBackend
|
||||
impl RawAccountObservationRead for PostgresBackend
|
||||
impl RawAccountObservationWrite for PostgresBackend
|
||||
Store dispatch account
|
||||
```
|
||||
|
||||
Toutes les migrations V000/V001/V002 restent byte-inchangées. Le checksum V002 final reste :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 10. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
|
||||
deltas/0.3.4/pre.007.md
|
||||
```
|
||||
|
||||
## 11. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/src/lib.rs
|
||||
crates/ksp-store-postgres-lib/src/raw_account.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/raw_account.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 12. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 13. Validations exécutées à l'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
|
||||
migrations V000/V001/V002 byte-identiques à pre.006 PASS
|
||||
scope pre.008 absent PASS
|
||||
```
|
||||
|
||||
Le gate Cargo/Clippy/tests de `pre.007` doit être rejoué par l'opérateur.
|
||||
|
||||
## 14. Validations non exécutées
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
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
|
||||
```
|
||||
|
||||
`cargo` n'est pas disponible dans l'environnement d'assemblage.
|
||||
|
||||
## 15. Décisions prises
|
||||
|
||||
- le cursor account reste distinct de `KSPT` par magic et domaine SHA-256 ;
|
||||
- le filtre pubkey fait partie du binding du cursor ;
|
||||
- les deux chemins SQL avec/sans pubkey sont séparés pour conserver une surface physique compatible avec les indexes possédés ;
|
||||
- aucune policy d'exécution n'est ajoutée à la pagination Store ;
|
||||
- les quatre capabilities account restent groupées pour `pre.008`.
|
||||
|
||||
## 16. Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.008`.
|
||||
72
deltas/0.3.4/pre.008-fix.001.md
Normal file
72
deltas/0.3.4/pre.008-fix.001.md
Normal file
@@ -0,0 +1,72 @@
|
||||
<!-- file: deltas/0.3.4/pre.008-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.008-fix.001`
|
||||
|
||||
## Base
|
||||
|
||||
```text
|
||||
0.3.4-pre.008
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger exclusivement le canari `crates/ksp-store-postgres-lib/tests/dependency_boundary.rs` qui empêche le gate `pre.008` de compiler : une déclaration `runtime` a été retirée du test d'ownership qui la consomme encore et laissée dans le test V002 où elle n'est plus utilisée depuis l'ouverture légitime des capabilities account.
|
||||
|
||||
## Modifications
|
||||
|
||||
- `workspace.package.version` passe à `0.3.4-pre.8.fix.1` ;
|
||||
- restauration de `let runtime = include_str!("../src/runtime.rs");` dans `pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private` ;
|
||||
- suppression de la déclaration désormais inutilisée dans `pre_003_v002_schema_is_complete_and_keeps_capability_implementation_out_of_schema_layers` ;
|
||||
- aucune assertion métier, capability ou règle d'ownership n'est modifiée ;
|
||||
- plan et validation réconciliés avec l'échec ciblé et le fix.
|
||||
|
||||
## Invariants préservés
|
||||
|
||||
```text
|
||||
PostgresBackend Raw* impls = 10
|
||||
Store Raw* impls = 10
|
||||
ksp-store-api inchangée
|
||||
ksp-store-lib production inchangée
|
||||
ksp-store-postgres-lib production inchangée
|
||||
raw_account.rs inchangé
|
||||
raw_account/cursor.rs inchangé
|
||||
runtime.rs inchangé
|
||||
migrations V000/V001/V002 byte-inchangées
|
||||
```
|
||||
|
||||
Checksum V002 inchangé :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.008-fix.001.md
|
||||
```
|
||||
|
||||
## Gate opérateur à rejouer
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
219
deltas/0.3.4/pre.008.md
Normal file
219
deltas/0.3.4/pre.008.md
Normal file
@@ -0,0 +1,219 @@
|
||||
<!-- file: deltas/0.3.4/pre.008.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.008` — capabilities account + conformance RAW 10/10
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.4-pre.007-fix.001
|
||||
workspace.package.version = 0.3.4-pre.7.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 pour `pre.007-fix.001` est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 239 tables / 145 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS — 63 unit tests + canaris, live ignored
|
||||
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
La verticale account possède donc déjà ses primitives physiques read/write/pagination validées avant ouverture des traits.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Ouvrir uniquement les quatre capabilities account existantes :
|
||||
|
||||
```text
|
||||
RawAccountStateRead
|
||||
RawAccountStateWrite
|
||||
RawAccountObservationRead
|
||||
RawAccountObservationWrite
|
||||
```
|
||||
|
||||
sur :
|
||||
|
||||
```text
|
||||
PostgresBackend
|
||||
Store
|
||||
```
|
||||
|
||||
et transformer les canaris pour fermer l'inventaire RAW à :
|
||||
|
||||
```text
|
||||
6 RawTransaction* + 4 RawAccount* = 10/10
|
||||
```
|
||||
|
||||
Aucun nouveau SQL, modèle, migration, type public ou comportement physique n'est ajouté.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.4-pre.8
|
||||
```
|
||||
|
||||
## 4. `PostgresBackend` — quatre traits account
|
||||
|
||||
`src/runtime.rs` implémente exactement une fois :
|
||||
|
||||
```text
|
||||
RawAccountStateRead
|
||||
RawAccountStateWrite
|
||||
RawAccountObservationRead
|
||||
RawAccountObservationWrite
|
||||
```
|
||||
|
||||
Chaque méthode adapte exclusivement le bridge étroit déjà présent :
|
||||
|
||||
```text
|
||||
get_raw_account_state
|
||||
list_raw_account_states
|
||||
persist_raw_account_acquisition
|
||||
get_raw_account_observation
|
||||
record_raw_account_observation
|
||||
```
|
||||
|
||||
Les erreurs physiques passent par `map_capability_error`, comme les six traits transaction. Aucun SQL ou type PostgreSQL n'entre dans l'API de capability.
|
||||
|
||||
## 5. `Store` — dispatch backend-neutral
|
||||
|
||||
`Store` implémente les mêmes quatre traits et délègue uniquement au backend compilé.
|
||||
|
||||
Guards réseau avant dispatch :
|
||||
|
||||
```text
|
||||
get state reference.network
|
||||
list state query.network
|
||||
persist state state.reference.network + observation.account.network
|
||||
record obs observation.account.network
|
||||
get obs aucun input réseau — RawObservationKey uniquement
|
||||
```
|
||||
|
||||
Le backend conserve en plus le guard d'égalité :
|
||||
|
||||
```text
|
||||
observation.account == state.reference
|
||||
```
|
||||
|
||||
avant `pool.get()` sur l'acquisition atomique.
|
||||
|
||||
Avec `postgres` désactivé, les dix traits restent implémentés sur `Store` et leur chemin runtime retourne `backend_not_compiled`, sans dépendance physique compilée.
|
||||
|
||||
## 6. Canaris transformés
|
||||
|
||||
Les canaris historiques anti-account ne sont pas supprimés. Ils deviennent des canaris de placement et de complétude :
|
||||
|
||||
```text
|
||||
PostgresBackend : exactement 10 impl Raw*
|
||||
Store : exactement 10 impl Raw*
|
||||
chaque impl account apparaît exactement une fois
|
||||
aucun impl account dans raw_account.rs
|
||||
aucun impl métier dans migration.rs/schema.rs
|
||||
aucun SQL/type PostgreSQL dans ksp-store-lib
|
||||
validate_operation_network occurrences Store = 14
|
||||
```
|
||||
|
||||
Les canaris `pre.005`, `pre.006` et `pre.007` conservent leurs interdictions destructives et de policy (`UPDATE`, `DELETE`, `ON CONFLICT DO UPDATE`, `OFFSET`, clamp/batch policy), sans exiger que les traits account restent fermés après leur tranche d'ouverture légitime.
|
||||
|
||||
## 7. Invariants préservés
|
||||
|
||||
```text
|
||||
ksp-store-api inchangée
|
||||
raw_account.rs inchangé
|
||||
raw_account/cursor.rs inchangé
|
||||
SQL runtime account inchangé
|
||||
V000/V001/V002 byte-inchangées
|
||||
aucun account retention/archive/purge
|
||||
aucun worker/job/backfill policy
|
||||
aucun Transport -> Store
|
||||
aucun nouveau backend
|
||||
```
|
||||
|
||||
Checksum V002 inchangé :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 8. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.008.md
|
||||
```
|
||||
|
||||
## 9. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
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-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
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 10. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 11. Validations exécutées à l'assemblage
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
|
||||
inventaire PostgresBackend Raw* = 10 PASS
|
||||
inventaire Store Raw* = 10 PASS
|
||||
validate_operation_network occurrences Store = 14 PASS
|
||||
migrations V000/V001/V002 byte-identiques à pre.007-fix.001 PASS
|
||||
raw_account.rs / raw_account/cursor.rs inchangés PASS
|
||||
ksp-store-api inchangée PASS
|
||||
```
|
||||
|
||||
`cargo`, `rustfmt`, Clippy et les tests Rust ne sont pas disponibles dans l'environnement d'assemblage ; le gate opérateur complet doit être rejoué.
|
||||
|
||||
## 12. Gate opérateur à rejouer
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 13. Décisions prises
|
||||
|
||||
- `pre.008` n'ajoute aucune primitive physique : elle ouvre uniquement les traits déjà prévus par `ksp-store-api`.
|
||||
- `RawAccountObservationRead` n'ajoute pas de faux guard réseau, car `RawObservationKey` ne porte pas de réseau.
|
||||
- La conformance 10/10 est vérifiée par inventaire exact, pas par simple présence partielle.
|
||||
- Les canaris historiques sont transformés vers les invariants encore pertinents au lieu d'être supprimés.
|
||||
- La preuve réelle PostgreSQL account reste réservée à `pre.009`.
|
||||
|
||||
## 14. Questions ouvertes
|
||||
|
||||
```text
|
||||
aucune question bloquante pour pre.008
|
||||
```
|
||||
216
deltas/0.3.4/pre.009.md
Normal file
216
deltas/0.3.4/pre.009.md
Normal file
@@ -0,0 +1,216 @@
|
||||
<!-- file: deltas/0.3.4/pre.009.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.009` — preuve PostgreSQL live `RawAccountState`
|
||||
|
||||
## 1. Base et gate d'entrée
|
||||
|
||||
Base opérateur obligatoire :
|
||||
|
||||
```text
|
||||
0.3.4-pre.008-fix.001
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 2026-08-30 est entièrement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `ksp-store-lib --no-default-features` passent. L'inventaire RAW 10/10 de `pre.008` est donc acquis.
|
||||
|
||||
## 2. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.9
|
||||
```
|
||||
|
||||
## 3. Nouveau test PostgreSQL réel account
|
||||
|
||||
La tranche ajoute :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
|
||||
```
|
||||
|
||||
Le test est `#[ignore]`, lit une URI PostgreSQL dédiée uniquement sur `stdin`, ne l'affiche jamais et refuse de démarrer lorsqu'une table KSP V000/V001/V002 existe déjà. Il ne nettoie que le schéma qu'il a prouvé absent avant son propre bootstrap.
|
||||
|
||||
PostgreSQL < 15 est refusé comme pour les preuves live existantes.
|
||||
|
||||
## 4. Bootstrap, réseau, migration V002 et schema update
|
||||
|
||||
La preuve couvre :
|
||||
|
||||
- bootstrap V000 + V001 + V002 sur base vide ;
|
||||
- health Ready avec migration 2 ;
|
||||
- refus d'une réouverture sous un autre `RawNetworkId` ;
|
||||
- réouverture idempotente ;
|
||||
- suppression contrôlée de `ix_ksp_raw_account_states_slot_pubkey_state_hash` ;
|
||||
- `schema_autoupdate=false` qui bloque le drift ;
|
||||
- `schema_autoupdate=true` qui recrée et revalide l'index account manquant.
|
||||
|
||||
Aucune ressource de migration n'est modifiée et aucun historique n'est réécrit.
|
||||
|
||||
## 5. Domaine account et round-trip
|
||||
|
||||
Le live matérialise les extrêmes figés par `pre.001` :
|
||||
|
||||
```text
|
||||
slot = u64::MAX
|
||||
lamports = u64::MAX
|
||||
rent_epoch = u64::MAX
|
||||
write_version = u64::MAX
|
||||
data vide = admise
|
||||
data 16 MiB exact = admise et relue complète
|
||||
```
|
||||
|
||||
La lecture réouverte doit restituer exactement la référence, le contenu commun et les metadata optionnelles sans narrowing.
|
||||
|
||||
## 6. Atomicité, idempotence, conflits et observations
|
||||
|
||||
La preuve couvre :
|
||||
|
||||
- acquisition state + observation `Inserted/Inserted` ;
|
||||
- replay exact `AlreadyPresent/AlreadyPresent` ;
|
||||
- même référence complète avec contenu divergent -> `Conflict` ;
|
||||
- même `(pubkey, slot)` avec deux `state_hash` distincts -> deux states durables ;
|
||||
- collision d'`observation_key` pendant une nouvelle acquisition -> `Conflict` et rollback du state nouvellement tenté ;
|
||||
- observation supplémentaire -> `Inserted`, puis replay exact -> `AlreadyPresent` ;
|
||||
- même observation key divergente -> `Conflict` ;
|
||||
- référence state absente -> `ReferenceNotFound` ;
|
||||
- round-trip exact de la provenance commune et des metadata Yellowstone `is_startup`, `transaction_signature`, `write_version`.
|
||||
|
||||
## 7. Pagination et cursor `KSPA`
|
||||
|
||||
Le test matérialise cinq states sur la plage `200..=202` et prouve :
|
||||
|
||||
```text
|
||||
ASC (slot, pubkey, state_hash)
|
||||
DESC (slot, pubkey, state_hash) intégralement inversé
|
||||
page size = 2
|
||||
filtre pubkey exact
|
||||
```
|
||||
|
||||
Il rejoue ensuite le cursor sous direction différente, corrompt son digest, puis fournit un token de famille transaction `KSPT`. Les trois cas doivent être `QueryInvalid`.
|
||||
|
||||
Aucun `OFFSET`, plafond fonctionnel de page, batch-size ou policy worker n'est introduit.
|
||||
|
||||
## 8. Concurrence réelle
|
||||
|
||||
Deux acquisitions identiques sont lancées en vraies tâches concurrentes : le résultat attendu est exactement un `Inserted` et un `AlreadyPresent`.
|
||||
|
||||
Deux acquisitions divergentes partageant la même référence `(pubkey, slot, state_hash)` sont ensuite lancées en concurrence : exactement une gagne et l'autre doit produire `Conflict`.
|
||||
|
||||
## 9. Cancellation réelle
|
||||
|
||||
Le test :
|
||||
|
||||
1. crée une observation account durable ;
|
||||
2. verrouille cette `observation_key` via une transaction PostgreSQL administrateur `FOR UPDATE` ;
|
||||
3. démarre une nouvelle acquisition qui insère son state dans une transaction non committée puis bloque sur la collision observation ;
|
||||
4. vérifie que l'opération reste bloquée au-delà du délai canari ;
|
||||
5. l'annule via `JoinHandle::abort()` ;
|
||||
6. libère le verrou administrateur ;
|
||||
7. vérifie physiquement que le state de l'acquisition annulée n'est pas durable.
|
||||
|
||||
Cette preuve exerce le rollback par cancellation réelle figé en `pre.006`.
|
||||
|
||||
## 10. Coexistence `RawTransaction`
|
||||
|
||||
Dans le même schéma V002, le live persiste une acquisition account et une acquisition transaction utilisant volontairement la même `RawObservationKey` dans leurs tables familiales distinctes. Les deux familles doivent rester lisibles indépendamment, et la transaction est aussi vérifiée physiquement avant le reopen final.
|
||||
|
||||
Le test historique `postgres_raw_transaction_live.rs` est réconcilié uniquement sur son inventaire d'isolation : son probe et son cleanup prennent désormais aussi en compte :
|
||||
|
||||
```text
|
||||
ksp_raw_account_states
|
||||
ksp_raw_account_observations
|
||||
```
|
||||
|
||||
Aucun scénario métier RawTransaction n'est modifié.
|
||||
|
||||
## 11. Isolation et cleanup
|
||||
|
||||
Les deux lives considèrent désormais l'inventaire complet de tables Store V002 :
|
||||
|
||||
```text
|
||||
ksp_store_schema_migrations
|
||||
ksp_store_identity
|
||||
ksp_raw_transactions
|
||||
ksp_raw_transaction_observations
|
||||
ksp_raw_transaction_archive_payloads
|
||||
ksp_raw_account_states
|
||||
ksp_raw_account_observations
|
||||
```
|
||||
|
||||
Ils refusent un schéma préexistant et ne suppriment que le schéma qu'ils ont eux-mêmes bootstrapé après preuve d'absence initiale.
|
||||
|
||||
## 12. Migrations
|
||||
|
||||
Aucune ressource V000/V001/V002 n'est modifiée.
|
||||
|
||||
Checksums attendus :
|
||||
|
||||
```text
|
||||
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 13. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 14. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
|
||||
deltas/0.3.4/pre.009.md
|
||||
```
|
||||
|
||||
Aucune suppression de fichier.
|
||||
|
||||
## 15. Validation exécutée dans l'environnement de génération
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean
|
||||
```
|
||||
|
||||
Cargo, rustfmt et PostgreSQL réel ne sont pas disponibles dans l'environnement de génération. Le gate opérateur standard et le live restent donc à exécuter localement.
|
||||
|
||||
## 16. Gate opérateur standard
|
||||
|
||||
```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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 17. Gate PostgreSQL live opt-in
|
||||
|
||||
Sur une base PostgreSQL dédiée et vide de toute table KSP gérée :
|
||||
|
||||
```bash
|
||||
printf '%s\n' '<URI_POSTGRES_DEDIEE>' | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture --test-threads=1
|
||||
```
|
||||
|
||||
Le live `RawTransaction` peut ensuite être rejoué séparément sur une base de nouveau vide :
|
||||
|
||||
```bash
|
||||
printf '%s\n' '<URI_POSTGRES_DEDIEE>' | cargo test -p ksp-store-postgres-lib --test postgres_raw_transaction_live -- --ignored --nocapture --test-threads=1
|
||||
```
|
||||
|
||||
## 18. Suite si les gates sont verts
|
||||
|
||||
```text
|
||||
0.3.4-pre.010 — hardening/completeness cross-family
|
||||
```
|
||||
49
deltas/0.3.4/pre.010.md
Normal file
49
deltas/0.3.4/pre.010.md
Normal file
@@ -0,0 +1,49 @@
|
||||
<!-- file: deltas/0.3.4/pre.010.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.010` — hardening et complétude cross-family
|
||||
|
||||
## Base
|
||||
|
||||
Base directe : `0.3.4-pre.009`. Le gate opérateur standard de `pre.009` est PASS et le live `postgres_raw_account_live` est PASS sur PostgreSQL 17 avec URI dédiée lue depuis stdin.
|
||||
|
||||
## Scope
|
||||
|
||||
Cette tranche est strictement limitée aux canaris de hardening et de complétude. Aucun runtime, SQL, migration, modèle API, capability ou export public n'est ajouté.
|
||||
|
||||
Modifications fonctionnelles de tests :
|
||||
|
||||
- `ksp-store-postgres-lib/tests/hardening_completeness.rs` fige la surface SQL account non destructive/keyset-only, la séparation `KSPA`/`KSPT`, l'absence de FK cross-family et l'inventaire V002 exact de 32 resources ;
|
||||
- `ksp-store-lib/tests/hardening_completeness.rs` compare directement les ensembles de capabilities RAW de `Store` et `PostgresBackend`, qui doivent être exactement identiques à dix traits, sans retention/delete/compaction account.
|
||||
|
||||
## Invariants préservés
|
||||
|
||||
```text
|
||||
ksp-store-api inchangée
|
||||
ksp-store-lib/src inchangé
|
||||
ksp-store-postgres-lib/src inchangé
|
||||
tests live inchangés
|
||||
migrations V000/V001/V002 byte-inchangées
|
||||
V002 checksum ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
RAW capabilities PostgresBackend 10
|
||||
RAW capabilities Store 10
|
||||
```
|
||||
|
||||
## Contrôles d'assemblage
|
||||
|
||||
Les audits Rust et Markdown doivent passer avant packaging. L'environnement d'assemblage ne possède pas Cargo ; le gate opérateur complet reste donc requis après application du delta.
|
||||
|
||||
## Gate opérateur à rejouer
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
```
|
||||
143
deltas/0.3.4/pre.011.md
Normal file
143
deltas/0.3.4/pre.011.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<!-- file: deltas/0.3.4/pre.011.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.011` — gate technique final RawAccountState
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.4-pre.10
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.010` du 30 août 2026 est entièrement vert : audits Rust/Markdown, workspace check, Clippy, suites ciblées Store API/façade/PostgreSQL/Config et compilation façade sans feature PostgreSQL passent.
|
||||
|
||||
La preuve live PostgreSQL 17 complète de référence reste celle de `pre.009`.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Ouvrir exclusivement le gate technique final de `0.3.4` sans nouveau développement fonctionnel.
|
||||
|
||||
Cette tranche ne modifie aucun :
|
||||
|
||||
```text
|
||||
src/**
|
||||
test fonctionnel
|
||||
migration SQL
|
||||
Config runtime
|
||||
manifest de crate
|
||||
dépendance runtime
|
||||
README/USAGE
|
||||
CHANGELOG/ROADMAP
|
||||
prompt suivant
|
||||
```
|
||||
|
||||
## 3. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.11
|
||||
```
|
||||
|
||||
Aucune autre propriété Cargo n'est modifiée.
|
||||
|
||||
## 4. Gate technique final attendu
|
||||
|
||||
```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.4
|
||||
|
||||
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-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
|
||||
cargo test --workspace
|
||||
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
`cargo clean` peut être exécuté avant ce gate pour une reconstruction froide ; il n'est pas un invariant fonctionnel de la release.
|
||||
|
||||
## 5. Replay PostgreSQL réel
|
||||
|
||||
Le live `RawAccountState` est rejoué comme preuve finale ciblée :
|
||||
|
||||
```bash
|
||||
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
|
||||
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture --test-threads=1
|
||||
unset KSP_PG_TEST_URI
|
||||
```
|
||||
|
||||
Ce live couvre déjà bootstrap/reopen V002, réparation de schéma, bornes u64/data, acquisition/idempotence/conflits, pagination/cursors, concurrence/cancellation et coexistence réelle avec `RawTransaction`. Les lives foundation et RawTransaction séparés ne sont donc pas requis par `pre.011`.
|
||||
|
||||
Le contrat de sécurité reste inchangé : URI via stdin, aucun echo de l'URI, refus d'un schéma KSP préexistant et cleanup uniquement de la surface créée après preuve d'absence initiale.
|
||||
|
||||
## 6. Graphes et packaging
|
||||
|
||||
Les graphes Cargo figent les frontières façade/backend/features et l'absence de dépendance PostgreSQL lorsque la feature correspondante n'est pas activée.
|
||||
|
||||
Aucun build Tauri final n'est imposé : `0.3.4` ne modifie ni resources applicatives, ni packaging Tauri, ni surface Config applicative.
|
||||
|
||||
## 7. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.011.md
|
||||
```
|
||||
|
||||
## 8. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 9. Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## 10. Migrations
|
||||
|
||||
Aucune ressource SQL n'est modifiée.
|
||||
|
||||
```text
|
||||
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 11. Validations exécutées dans l'environnement de génération
|
||||
|
||||
```text
|
||||
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.4
|
||||
contrôle statique version/payload
|
||||
contrôle byte-for-byte de migrations/
|
||||
contrôle byte-for-byte de ksp-store-api et des arbres src Store/PostgreSQL
|
||||
```
|
||||
|
||||
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération ; aucun gate Cargo n'est déclaré PASS ici.
|
||||
|
||||
## 12. Questions ouvertes
|
||||
|
||||
Aucune question de design. Tout défaut découvert par ce gate ouvre `pre.011-fix.NNN` avant la réconciliation documentaire.
|
||||
|
||||
## 13. Suite
|
||||
|
||||
Après gate technique, graphes et live entièrement verts :
|
||||
|
||||
```text
|
||||
0.3.4-pre.012 — réconciliation documentaire finale
|
||||
0.3.4-pre.013 — préparation publication + prompt 0.3.5
|
||||
0.3.4-rel.001 — publication stable
|
||||
```
|
||||
185
deltas/0.3.4/pre.012.md
Normal file
185
deltas/0.3.4/pre.012.md
Normal file
@@ -0,0 +1,185 @@
|
||||
<!-- file: deltas/0.3.4/pre.012.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.012` — réconciliation documentaire finale Store/PostgreSQL RAW
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.4-pre.011
|
||||
```
|
||||
|
||||
Le couloir technique est fermé. Le replay de `pre.011` confirme les tests façade sans backend, le workspace complet, les graphes Cargo et le live `RawAccountState` sur PostgreSQL 17.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Réconcilier exclusivement les documents durables avec la surface Store/PostgreSQL effectivement validée.
|
||||
|
||||
Cette tranche ne rouvre aucun comportement Rust, test fonctionnel, Config, schema, migration, dépendance ou feature et ne prépare pas encore la publication.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Conformément à `VER-ID-009`, cette prerelease non-fix synchronise :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.12
|
||||
```
|
||||
|
||||
Aucune autre propriété Cargo n'est modifiée.
|
||||
|
||||
## 4. Résultat technique hérité
|
||||
|
||||
```text
|
||||
Store/Postgres RAW capabilities 10/10
|
||||
RawTransaction capabilities 6
|
||||
RawAccount capabilities 4
|
||||
ksp-store-lib --no-default-features PASS
|
||||
cargo test --workspace PASS
|
||||
Cargo graphs façade/backend/features/duplicates exécutés
|
||||
postgres_raw_account_live PASS — PostgreSQL 17
|
||||
V002 resources 32
|
||||
V002 checksum ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
Les doublons Cargo du workspace sont transverses et ne révèlent aucune duplication propre au Store nécessitant une correction.
|
||||
|
||||
## 5. Réconciliation `ksp-store-lib`
|
||||
|
||||
Le README et le USAGE décrivent désormais durablement :
|
||||
|
||||
```text
|
||||
exactement 10 capabilities RAW
|
||||
6 RawTransaction* + 4 RawAccount*
|
||||
lecture RawAccountState par référence
|
||||
pagination keyset (slot,pubkey,state_hash), filtre pubkey optionnel
|
||||
acquisition account atomique et idempotence exacte
|
||||
lecture/ajout d'observation account
|
||||
cursors opaques family-local
|
||||
aucune rétention/archive/purge/delete/compaction account
|
||||
aucune policy batch/priorité/scheduling dans Store
|
||||
```
|
||||
|
||||
Les anciennes affirmations indiquant que les capabilities `RawAccount*` n'étaient pas dispatchées sont supprimées.
|
||||
|
||||
## 6. Réconciliation `ksp-store-postgres-lib`
|
||||
|
||||
Le README et le USAGE décrivent désormais :
|
||||
|
||||
```text
|
||||
V001 = schéma RawTransaction
|
||||
V002 = schéma RawAccountState
|
||||
lectures state/observation account
|
||||
acquisition state+observation atomique
|
||||
observation supplémentaire reference-guarded
|
||||
pagination keyset account et cursor KSPA
|
||||
aucun ON CONFLICT DO UPDATE
|
||||
aucune FK account transaction_signature -> RawTransaction
|
||||
aucune rétention destructive account
|
||||
```
|
||||
|
||||
La crate reste un backend physique derrière `ksp-store-lib`, jamais une seconde façade applicative.
|
||||
|
||||
## 7. Documents de release
|
||||
|
||||
Le plan `025` enregistre le PASS technique de `pre.011`, synchronise la version de travail sur `pre.012` et marque la réconciliation documentaire comme matérialisée.
|
||||
|
||||
La validation `021` ferme le verdict `pre.011` et ouvre le verdict documentaire `pre.012`.
|
||||
|
||||
`README.md` racine et les architectures Store ont été relus ; aucune affirmation devenue fausse n'y justifie une modification artificielle. `ksp-store-api` n'acquiert pas un README/USAGE uniquement pour cette tranche.
|
||||
|
||||
## 8. Hors scope préservé
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
prompt 0.3.5
|
||||
src/**
|
||||
tests/**
|
||||
config/**
|
||||
migrations/**
|
||||
account retention/archive/purge/delete/compaction
|
||||
workers/jobs/backfills
|
||||
batch/priorité/backlog policy
|
||||
nouveau backend
|
||||
Store Desk
|
||||
```
|
||||
|
||||
La préparation minimale `CHANGELOG`/`ROADMAP`/prompt reste réservée à `pre.013`.
|
||||
|
||||
## 9. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.4/pre.012.md
|
||||
```
|
||||
|
||||
## 10. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-lib/README.md
|
||||
crates/ksp-store-lib/USAGE.md
|
||||
crates/ksp-store-postgres-lib/README.md
|
||||
crates/ksp-store-postgres-lib/USAGE.md
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
## 11. Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## 12. Migrations
|
||||
|
||||
Aucune migration n'est modifiée.
|
||||
|
||||
```text
|
||||
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 13. Validations de génération
|
||||
|
||||
Les validations statiques applicables ont été rejouées après matérialisation de l'overlay :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (240 table(s), 151 file(s))
|
||||
protected src/tests/config/migrations byte-identical à pre.011: PASS
|
||||
```
|
||||
|
||||
Les exécutables Cargo/Rust ne sont pas disponibles dans l'environnement de génération ; aucun résultat Cargo post-overlay n'est revendiqué ici.
|
||||
|
||||
## 14. Gate opérateur après application
|
||||
|
||||
```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.4
|
||||
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
|
||||
```
|
||||
|
||||
Aucun nouveau live PostgreSQL, `cargo test --workspace` ou graphe Cargo n'est requis par cette tranche documentaire ; ces preuves sont déjà fermées par `pre.011`.
|
||||
|
||||
## 15. Questions ouvertes
|
||||
|
||||
Aucune pour le périmètre `0.3.4`.
|
||||
|
||||
## 16. Suite
|
||||
|
||||
Après gate documentaire vert :
|
||||
|
||||
```text
|
||||
0.3.4-pre.013 — préparation publication minimale + prompt 0.3.5
|
||||
0.3.4-rel.001 — stabilisation/tag v0.3.4
|
||||
```
|
||||
283
deltas/0.3.4/pre.013.md
Normal file
283
deltas/0.3.4/pre.013.md
Normal file
@@ -0,0 +1,283 @@
|
||||
<!-- file: deltas/0.3.4/pre.013.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-pre.013` — préparation de publication et prompt `0.3.5`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.4-pre.012
|
||||
workspace.package.version = 0.3.4-pre.12
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.012`, exécuté le **31 août 2026**, est entièrement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config et `cargo check -p ksp-store-lib --no-default-features` passent.
|
||||
|
||||
Le gate Markdown confirme :
|
||||
|
||||
```text
|
||||
240 tables / 151 fichiers
|
||||
```
|
||||
|
||||
`pre.012` n'a rouvert ni code fonctionnel ni migration ; la préparation de publication peut donc commencer sans correctif intermédiaire.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Dernière prerelease avant `rel.001`, strictement limitée à la préparation de publication minimale définie par `VER-LIFECYCLE-003` :
|
||||
|
||||
```text
|
||||
workspace.package.version -> 0.3.4-pre.13
|
||||
finalisation publication de CHANGELOG.md
|
||||
clôture de 0.3.4 et précision de 0.3.5 dans ROADMAP.md
|
||||
création de prompts/024-V0_3_5_START_PROMPT.md
|
||||
delta pre.013
|
||||
```
|
||||
|
||||
Aucun README, USAGE, plan, validation, architecture, règle, source Rust, test, Config, schema ou migration n'est rouvert.
|
||||
|
||||
## 3. Version
|
||||
|
||||
Conformément à `VER-ID-009` :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.13
|
||||
```
|
||||
|
||||
Le header du `Cargo.toml` racine est incrémenté uniquement parce que ce fichier est réellement modifié.
|
||||
|
||||
## 4. `CHANGELOG.md`
|
||||
|
||||
Une entrée stable `0.3.4` est ajoutée en tête et synthétise :
|
||||
|
||||
```text
|
||||
RAW capabilities Store/PostgreSQL 10/10
|
||||
4 RawAccount* ajoutées aux 6 RawTransaction*
|
||||
V002 additive RawAccountState/RawAccountObservation
|
||||
32 ressources V002
|
||||
checksum V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
V000/V001 immuables
|
||||
u64 exact NUMERIC(20,0)
|
||||
bytes account complets <= 16 MiB
|
||||
acquisition state+observation atomique
|
||||
idempotence/conflit exacts
|
||||
observation supplémentaire reference-guarded
|
||||
metadata Yellowstone observation-only
|
||||
pagination keyset (slot,pubkey,state_hash)
|
||||
cursor KSPA V1 109 bytes anti-replay
|
||||
aucune rétention destructive account
|
||||
live PostgreSQL 17 vert
|
||||
workspace + no-default-features + graphes verts
|
||||
```
|
||||
|
||||
Le changelog référence également le prompt `0.3.5` et précise que l'archive kbot3 n'y est pas requise.
|
||||
|
||||
## 5. `ROADMAP.md`
|
||||
|
||||
L'entrée `0.3.4` passe à `[X]` et décrit la surface stable effectivement validée.
|
||||
|
||||
L'entrée `0.3.5` est précisée sans préjuger du résultat de son `pre.001` :
|
||||
|
||||
```text
|
||||
ksp-interface-lib uniquement
|
||||
matrice HTTP/WS/Yellowstone/Helius
|
||||
logs / slot-root-slotsUpdates / transaction status / vote à auditer
|
||||
seuls les faits event-only réellement convergents et utiles sont matérialisés
|
||||
aucune duplication RawTransaction/RawAccountState
|
||||
aucun event bus
|
||||
aucune dépendance Interface -> Transport/Store
|
||||
```
|
||||
|
||||
Les étapes `0.3.6` et `0.3.7` restent inchangées.
|
||||
|
||||
## 6. Prompt `0.3.5`
|
||||
|
||||
Le nouveau prompt ouvre :
|
||||
|
||||
```text
|
||||
0.3.5 — ksp-interface-lib : modèles passifs/event-only réellement partagés
|
||||
```
|
||||
|
||||
Base autoritaire :
|
||||
|
||||
```text
|
||||
v0.3.4
|
||||
```
|
||||
|
||||
Le `pre.001` est réservé à :
|
||||
|
||||
```text
|
||||
lecture règles/architecture
|
||||
inventaire Interface actuel
|
||||
inventaire Store RAW 10/10
|
||||
inventaire des surfaces HTTP/WS/gRPC/provider event-like
|
||||
identification des consumers
|
||||
matrice producer -> nature de fait
|
||||
matrice de convergence sémantique
|
||||
ownership Interface/Transport/Store/composition
|
||||
anti-duplication RawTransaction/RawAccountState
|
||||
API sketch des seuls types admis
|
||||
bornes / Debug / Clone / non_exhaustive
|
||||
graphe cible
|
||||
threat model
|
||||
sizing
|
||||
plan 026 + validation 022
|
||||
```
|
||||
|
||||
Le prompt interdit une grande enum `Event`, l'Option-soup, les DTOs provider promus tels quels, un event bus, une nouvelle persistence et tout codec/logging runtime non justifié.
|
||||
|
||||
## 7. Archive historique
|
||||
|
||||
`0.3.5` **n'exige pas** :
|
||||
|
||||
```text
|
||||
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
|
||||
```
|
||||
|
||||
La raison est explicite dans le prompt : l'autorité de cette tranche est la sémantique actuelle déjà implémentée dans KSP, complétée par les sources officielles actuelles HTTP/WS/Yellowstone lorsque nécessaire. Un ancien modèle d'events ne doit pas piloter le contrat public par inertie.
|
||||
|
||||
## 8. Prévision souple `0.3.5`
|
||||
|
||||
Le prompt réserve :
|
||||
|
||||
```text
|
||||
pre.001 audit consumers/sémantiques/ownership + sizing
|
||||
pre.002 première famille passive retenue
|
||||
pre.003 famille supplémentaire seulement si justifiée
|
||||
pre.004 API publique + consumer externe + anti-duplication
|
||||
pre.005 hardening/completeness Interface
|
||||
pre.006 gate technique final
|
||||
pre.007 réconciliation documentaire
|
||||
pre.008 préparation publication
|
||||
rel.001 publication stable
|
||||
```
|
||||
|
||||
Le nombre réel de familles et de prereleases reste recalibrable par `pre.001` ; aucun quota minimal d'events n'est imposé.
|
||||
|
||||
## 9. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
prompts/024-V0_3_5_START_PROMPT.md
|
||||
deltas/0.3.4/pre.013.md
|
||||
```
|
||||
|
||||
## 10. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
```
|
||||
|
||||
## 11. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 12. Surfaces explicitement non rouvertes
|
||||
|
||||
```text
|
||||
README.md
|
||||
RULES.md
|
||||
crates/**
|
||||
docs/**
|
||||
config/**
|
||||
migrations/**
|
||||
prompts/001..023
|
||||
```
|
||||
|
||||
Hormis le nouveau delta, aucun fichier sous `docs/` ou `crates/` n'est modifié.
|
||||
|
||||
## 13. Migrations
|
||||
|
||||
Aucune migration n'est modifiée.
|
||||
|
||||
Les checksums stables restent :
|
||||
|
||||
```text
|
||||
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 14. Validations exécutées pendant la génération
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
-> General Rust rule audit: clean
|
||||
-> Rust export completeness audit: 0 candidate(s)
|
||||
-> KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
|
||||
-> Markdown table audit: clean (240 table(s), 153 file(s))
|
||||
|
||||
scope exact pre.012 -> pre.013
|
||||
-> 2 fichiers ajoutés
|
||||
-> 3 fichiers modifiés
|
||||
-> aucun fichier supprimé
|
||||
-> DIFF_SCOPE=PASS
|
||||
|
||||
crates/** inchangé
|
||||
-> PASS
|
||||
|
||||
docs/** inchangé
|
||||
-> PASS
|
||||
|
||||
config/** inchangé
|
||||
-> PASS
|
||||
|
||||
migrations/** byte-identiques
|
||||
-> PASS
|
||||
```
|
||||
|
||||
Les cinq fichiers de la lane minimale sont donc les seuls fichiers ajoutés/modifiés.
|
||||
|
||||
## 15. Validations non exécutées pendant la génération
|
||||
|
||||
La toolchain Cargo/Rust opérateur n'est pas supposée disponible dans l'environnement de génération. Aucun `cargo check`, Clippy ou test Cargo post-overlay n'est revendiqué sans exécution réelle.
|
||||
|
||||
Aucun live PostgreSQL supplémentaire n'est requis par cette tranche : `pre.013` ne modifie ni code, ni tests, ni migrations.
|
||||
|
||||
## 16. 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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 17. Décisions prises
|
||||
|
||||
```text
|
||||
0.3.4 est techniquement/documentairement fermé avant publication
|
||||
0.3.5 reste limité à ksp-interface-lib et à ses docs/tests propres
|
||||
les events persistants/replayables restent Store API
|
||||
les DTOs protocol/provider restent Transport
|
||||
aucun event bus n'est ouvert
|
||||
aucune archive kbot3 n'est requise pour 0.3.5
|
||||
les candidats event-only restent soumis à une matrice de convergence en pre.001
|
||||
```
|
||||
|
||||
## 18. Questions ouvertes
|
||||
|
||||
Aucune pour la publication `0.3.4`.
|
||||
|
||||
Les questions de sélection exacte des events appartiennent explicitement à `0.3.5-pre.001`.
|
||||
|
||||
## 19. Suite
|
||||
|
||||
Si le gate `pre.013` est propre :
|
||||
|
||||
```text
|
||||
0.3.4-rel.001
|
||||
```
|
||||
|
||||
La livraison `rel.001` reste mécanique et minimale : version Cargo stable + delta uniquement. Elle ne corrige aucun défaut documentaire ou fonctionnel.
|
||||
140
deltas/0.3.4/rel.001.md
Normal file
140
deltas/0.3.4/rel.001.md
Normal file
@@ -0,0 +1,140 @@
|
||||
<!-- file: deltas/0.3.4/rel.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.4-rel.001` — stabilisation et publication `v0.3.4`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.4-pre.013
|
||||
workspace.package.version = 0.3.4-pre.13
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.013`, exécuté le **31 août 2026**, est entièrement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config et `cargo check -p ksp-store-lib --no-default-features` passent.
|
||||
|
||||
Le gate Markdown confirme :
|
||||
|
||||
```text
|
||||
240 tables / 153 fichiers
|
||||
```
|
||||
|
||||
Aucun correctif intermédiaire n'est requis.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Stabiliser mécaniquement la release `0.3.4` après fermeture technique et documentaire complète de la prerelease.
|
||||
|
||||
Cette tranche ne corrige ni code, ni documentation fonctionnelle, ni tests, ni Config, ni migration.
|
||||
|
||||
## 3. Version
|
||||
|
||||
La seule modification existante est la stabilisation de la version workspace :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4
|
||||
```
|
||||
|
||||
Le header du `Cargo.toml` racine est incrémenté parce que ce fichier est réellement modifié.
|
||||
|
||||
## 4. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.4/rel.001.md
|
||||
```
|
||||
|
||||
## 5. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
```
|
||||
|
||||
## 6. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 7. Surfaces explicitement inchangées
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
README.md
|
||||
RULES.md
|
||||
prompts/024-V0_3_5_START_PROMPT.md
|
||||
crates/**
|
||||
docs/**
|
||||
config/**
|
||||
migrations/**
|
||||
tests/**
|
||||
```
|
||||
|
||||
La surface préparée en `pre.013` reste donc byte-identique, hors mécanique Cargo et ajout du présent delta.
|
||||
|
||||
## 8. Migrations
|
||||
|
||||
Aucune migration n'est modifiée.
|
||||
|
||||
Les checksums stables restent :
|
||||
|
||||
```text
|
||||
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 9. Invariants de publication
|
||||
|
||||
```text
|
||||
RAW PostgreSQL/Store = 10/10 capabilities
|
||||
RawTransaction inchangé
|
||||
RawAccountState/Observation validé
|
||||
V002 additive et figée
|
||||
ksp-store-lib --no-default-features vert
|
||||
aucune rétention destructive account
|
||||
aucune nouvelle dépendance ou surface publique
|
||||
prompt 0.3.5 déjà préparé en pre.013
|
||||
```
|
||||
|
||||
## 10. Validation de la tranche
|
||||
|
||||
`rel.001` est strictement mécanique et ne demande aucun nouveau live PostgreSQL.
|
||||
|
||||
Le gate opérateur de `pre.013` constitue la preuve technique finale de la release ; après application du présent delta, le contrôle attendu porte uniquement sur l'absence de dérive de publication.
|
||||
|
||||
Commande recommandée :
|
||||
|
||||
```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.4
|
||||
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
|
||||
```
|
||||
|
||||
## 11. Publication
|
||||
|
||||
Après gate propre sur cet overlay :
|
||||
|
||||
```text
|
||||
release stable = 0.3.4
|
||||
tag attendu = v0.3.4
|
||||
```
|
||||
|
||||
Aucun ancien tag stable ne doit être conservé si le workflow du dépôt impose uniquement le dernier tag stable.
|
||||
|
||||
## 12. Suite
|
||||
|
||||
La session suivante ouvre :
|
||||
|
||||
```text
|
||||
0.3.5 — ksp-interface-lib : modèles passifs/event-only réellement partagés
|
||||
prompts/024-V0_3_5_START_PROMPT.md
|
||||
```
|
||||
775
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
Normal file
775
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
Normal file
@@ -0,0 +1,775 @@
|
||||
<!-- file: docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md -->
|
||||
<!-- version: 18 -->
|
||||
|
||||
# Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
|
||||
|
||||
## 1. Statut de la release
|
||||
|
||||
`0.3.4-pre.001` a figé le design. `0.3.4-pre.002` a matérialisé la fondation physique V002 minimale puis `pre.002-fix.001` a corrigé deux canaris sans toucher au SQL. `0.3.4-pre.003` complète V002 avec les contraintes de domaine, l'index de navigation, la compatibilité de schéma et le checksum final ; `pre.003-fix.001` corrige uniquement l'ordre alphabétique du bloc `const` de `migration.rs` et son gate opérateur complet est PASS. `0.3.4-pre.004` ouvre le mapping PostgreSQL privé et les deux lectures `get` account ; `pre.004-fix.001` corrige uniquement leur conformité au profil Clippy KSP et un canari inutilisé. `pre.005` ajoute l'acquisition atomique, `pre.006` l'observation supplémentaire, `pre.007` la pagination/cursor account et `pre.007-fix.001` réconcilie ses canaris ; le gate opérateur complet du fix est PASS. `pre.008` ouvre les quatre capabilities account sur `PostgresBackend` puis leur dispatch dans `Store`, pour porter l'inventaire RAW à 10/10 sans nouveau SQL ; `pre.008-fix.001` corrige son unique canari mal réconcilié et son gate opérateur complet est PASS. `pre.009` ajoute la preuve PostgreSQL live account et réconcilie l'isolation du live RawTransaction avec le schéma V002 complet ; son gate standard et son live account sur PostgreSQL 17 sont PASS. `pre.010` ferme le hardening et la complétude cross-family sans modifier le runtime ou les migrations ; son gate opérateur complet est PASS. `pre.011` ouvre exclusivement le gate technique final, le replay live ciblé et les graphes Cargo avant toute réconciliation documentaire ; ses tests `--no-default-features`, le workspace complet, les graphes Cargo et le replay live account sur PostgreSQL 17 sont PASS. `pre.012` ouvre exclusivement la réconciliation documentaire finale sans rouvrir le code fonctionnel.
|
||||
|
||||
Base canonique auditée :
|
||||
|
||||
```text
|
||||
v0.3.3
|
||||
workspace.package.version = 0.3.3
|
||||
```
|
||||
|
||||
Version de travail de cette prerelease :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.4-pre.12
|
||||
label = 0.3.4-pre.012
|
||||
```
|
||||
|
||||
Décision de scope : `ksp-store-api` reste inchangée. L'inventaire backend/façade est désormais 10/10 et le couloir technique est fermé ; il ne reste que la réconciliation documentaire courante, puis la préparation de publication et la stabilisation.
|
||||
|
||||
## 2. Invariants hérités et frontières
|
||||
|
||||
La trajectoire reste :
|
||||
|
||||
```text
|
||||
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
```
|
||||
|
||||
`0.3.4` ferme seulement la complétude physique **RAW Store/PostgreSQL**. Store persiste et sert ; il ne choisit ni batch-size, ni priorité, ni backlog, ni retry job, ni stratégie de backfill.
|
||||
|
||||
Frontières inchangées :
|
||||
|
||||
```text
|
||||
ksp-store-lib -> ksp-store-api
|
||||
ksp-store-lib[postgres] -> ksp-store-postgres-lib
|
||||
ksp-store-postgres-lib -> ksp-store-api
|
||||
ksp-store-postgres-lib -X-> ksp-store-lib
|
||||
Config -> Store autorisé
|
||||
Store/backend -X-> Config
|
||||
Transport -X-> Store
|
||||
consumer ordinaire -> ksp-store-lib
|
||||
consumer ordinaire -X-> ksp-store-postgres-lib
|
||||
```
|
||||
|
||||
Une instance `Store` reste liée à un `RawNetworkId` et une base physique. Aucun multiplexage réseau/database n'est ajouté.
|
||||
|
||||
## 3. Inventaire de capabilities : 6/10 -> 10/10
|
||||
|
||||
| Capability | Impl physique stable | État base | Action 0.3.4 |
|
||||
|--------------------------------|-------------------------|---------------|--------------|
|
||||
| RawTransactionRead | PostgresBackend + Store | stable 0.3.3 | préserver |
|
||||
| RawTransactionWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
|
||||
| RawTransactionObservationRead | PostgresBackend + Store | stable 0.3.3 | préserver |
|
||||
| RawTransactionObservationWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
|
||||
| RawTransactionRetentionRead | PostgresBackend + Store | stable 0.3.3 | préserver |
|
||||
| RawTransactionRetentionWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
|
||||
| RawAccountStateRead | aucune impl physique | pending 0.3.4 | implémenter |
|
||||
| RawAccountStateWrite | aucune impl physique | pending 0.3.4 | implémenter |
|
||||
| RawAccountObservationRead | aucune impl physique | pending 0.3.4 | implémenter |
|
||||
| RawAccountObservationWrite | aucune impl physique | pending 0.3.4 | implémenter |
|
||||
|
||||
Canaries qui ferment encore volontairement la surface account dans `v0.3.3` :
|
||||
|
||||
- `crates/ksp-store-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for Store` ;
|
||||
- `crates/ksp-store-lib/tests/hardening_completeness.rs` fige six impl transaction et interdit les quatre account ;
|
||||
- `crates/ksp-store-postgres-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for PostgresBackend` ;
|
||||
- `crates/ksp-store-postgres-lib/tests/hardening_completeness.rs` interdit encore `impl ksp_store_api::RawAccount*` et toute matérialisation `ksp_raw_account` dans la slice transaction.
|
||||
|
||||
Ces canaries sont **transformées**, pas simplement supprimées, en `pre.008` : elles prouvent désormais exactement six capabilities transaction + quatre account. Les canaries qui imposent que V000 et `health.rs` restent business-free demeurent valides et ne doivent pas être assouplies.
|
||||
|
||||
### 3.1 Matrice détaillée des quatre capabilities account
|
||||
|
||||
| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live |
|
||||
|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------|
|
||||
| RawAccountStateRead | reference / query | Option<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
|
||||
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
|
||||
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
|
||||
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
|
||||
|
||||
## 4. Immutabilité V000/V001
|
||||
|
||||
Recalcul indépendant sur les bytes de l'archive stable :
|
||||
|
||||
```text
|
||||
V000 legacy SQL SHA-256 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 logical resources = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V001 resource count = 40
|
||||
```
|
||||
|
||||
L'algorithme V001 est celui du moteur stable : domaine `ksp-migration-resources-v1\0`, puis pour chaque ressource ordonnée `id + NUL + SQL bytes + NUL` avant SHA-256.
|
||||
|
||||
Règles V002 :
|
||||
|
||||
```text
|
||||
V000 : immuable
|
||||
V001 : immuable
|
||||
V002 : additive uniquement
|
||||
nom logique proposé : raw_account_state
|
||||
répertoire proposé : migrations/v002_raw_account_state/
|
||||
resources proposées : tables/ + constraints/ + indexes/
|
||||
hook migration supplémentaire : aucun
|
||||
```
|
||||
|
||||
Le binding réseau singleton V001 est réutilisé ; V002 ne duplique pas `network` dans chaque ligne métier.
|
||||
|
||||
## 5. Audit field-by-field du contrat account
|
||||
|
||||
### 5.1 `RawAccountStateReference` + `RawAccountState`
|
||||
|
||||
Matrice complète modèle -> physique :
|
||||
|
||||
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|
||||
|------------|----------------|-------------|--------------------|-----------------------|----------------------------------|------------------------|---------------------------------|
|
||||
| network | RawNetworkId | 1 | ksp_store_identity | exact backend network | binding runtime -> reference | aucune colonne row | aucun si guard exact |
|
||||
| pubkey | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
|
||||
| slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
|
||||
| state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes | fixed bytes -> RawContentHash | hash bytes -> BYTEA | aucun ; pas de recalcul backend |
|
||||
| lamports | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
|
||||
| owner | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
|
||||
| executable | bool | 1 | BOOLEAN | NOT NULL | bool direct | bool direct | aucun |
|
||||
| rent_epoch | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
|
||||
| data | Box<[u8]> | 1 | BYTEA | 0..=16 MiB | bytes + borne -> RawAccountState | bytes exacts -> BYTEA | base64/parse interdits |
|
||||
|
||||
Vue synthétique de projection :
|
||||
|
||||
| Champ API | Type API | Projection V002 | Invariant |
|
||||
|------------|---------------------------|------------------------------------------|-----------------------------------------------------------------|
|
||||
| network | RawNetworkId | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
|
||||
| pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
|
||||
| slot | u64 | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
|
||||
| state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
|
||||
| lamports | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
|
||||
| owner | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` |
|
||||
| executable | bool | BOOLEAN NOT NULL | mapping direct |
|
||||
| rent_epoch | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
|
||||
| data | Box<[u8]> | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
|
||||
|
||||
L'identité physique canonique retenue est :
|
||||
|
||||
```text
|
||||
PRIMARY KEY (pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
Le réseau fait partie de la référence API mais est porté physiquement par `ksp_store_identity` pour toute la base. Cette clé permet explicitement plusieurs états du même pubkey au même slot lorsque `state_hash` diffère.
|
||||
|
||||
`state_hash` est un digest **producer-owned**. Le backend ne le recalcule pas et ne suppose pas qu'il s'agit du hash de `data` seulement. L'idempotence vérifie donc le contenu complet malgré l'égalité du hash.
|
||||
|
||||
### 5.2 `RawAccountObservation`
|
||||
|
||||
Matrice complète modèle -> physique :
|
||||
|
||||
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|
||||
|---------------------------|---------------------------------|-------------|--------------------|------------------------|----------------------------|------------------------|---------------------------|
|
||||
| observation_key | RawObservationKey | 1 | BYTEA | exact 32 bytes | fixed bytes -> key | key bytes -> BYTEA | aucun |
|
||||
| account.pubkey | Pubkey | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
|
||||
| account.slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX + FK | decimal exact -> reference | decimal NUMERIC | narrowing interdit |
|
||||
| account.state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
|
||||
| provider | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
|
||||
| protocol | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
|
||||
| acquisition_method | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
|
||||
| origin | RawAcquisitionOrigin | 1 | TEXT | 5 valeurs stables | static decode | static encode | unknown DB -> DataInvalid |
|
||||
| received_at | RawTimestamp | 1 | BIGINT | 0..253402300799999 | checked i64 -> timestamp | u64 bounded -> i64 | borne API < i64::MAX |
|
||||
| capture_session_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| commitment | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| endpoint_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| filter_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| observed_at | Option<RawTimestamp> | 0..1 | BIGINT NULL | borne + <= received_at | NULL/checked timestamp | Option -> NULL/i64 | borne API < i64::MAX |
|
||||
| source_payload_hash | Option<RawContentHash> | 0..1 | BYTEA NULL | NULL ou 32 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucun |
|
||||
| source_payload_size_bytes | Option<u64> | 0..1 | BIGINT NULL | 0..=64 MiB | NULL/checked u64 | Option -> NULL/i64 | borne API < i64::MAX |
|
||||
| is_startup | Option<bool> | 0..1 | BOOLEAN NULL | nullable | NULL/bool | Option -> NULL/bool | ne pas inventer false |
|
||||
| transaction_signature | Option<RawTransactionSignature> | 0..1 | BYTEA NULL | NULL ou 64 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucune FK transaction |
|
||||
| write_version | Option<u64> | 0..1 | NUMERIC(20,0) NULL | NULL ou 0..u64::MAX | NULL/decimal exact | Option -> NULL/NUMERIC | narrowing interdit |
|
||||
|
||||
Vue synthétique de projection :
|
||||
|
||||
| Champ API | Type API | Projection V002 | Invariant |
|
||||
|---------------------------|---------------------------------|------------------------|--------------------------------------------------|
|
||||
| observation_key | RawObservationKey / 32 bytes | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
|
||||
| account.pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | FK composite vers state |
|
||||
| account.slot | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
|
||||
| account.state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | FK composite |
|
||||
| provider | RawProvenanceCode | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
|
||||
| protocol | RawProvenanceCode | TEXT NOT NULL | idem V001 |
|
||||
| acquisition_method | RawProvenanceCode | TEXT NOT NULL | idem V001 |
|
||||
| origin | RawAcquisitionOrigin | TEXT NOT NULL | backfill/import/live/repair/replay |
|
||||
| received_at | RawTimestamp | BIGINT NOT NULL | 0..253402300799999 |
|
||||
| capture_session_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| commitment | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| endpoint_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| filter_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| observed_at | Option<RawTimestamp> | BIGINT NULL | borne + `observed_at <= received_at` |
|
||||
| source_payload_hash | Option<RawContentHash> | BYTEA NULL | NULL ou 32 bytes |
|
||||
| source_payload_size_bytes | Option<u64> | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
|
||||
| is_startup | Option<bool> | BOOLEAN NULL | Yellowstone metadata observation-only |
|
||||
| transaction_signature | Option<RawTransactionSignature> | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
|
||||
| write_version | Option<u64> | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
|
||||
|
||||
La relation observation -> state est :
|
||||
|
||||
```text
|
||||
FOREIGN KEY (account_pubkey, account_slot, account_state_hash)
|
||||
REFERENCES ksp_raw_account_states(pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
Aucun `ON DELETE CASCADE` n'est nécessaire : l'API account ne possède ni delete ni retention. Aucune FK n'est créée vers `ksp_raw_transactions` pour `transaction_signature`; cette signature est une metadata source optionnelle et la transaction correspondante n'est pas garantie présente dans le Store.
|
||||
|
||||
`observation_key` est unique **dans la famille account**. Le contrat API expose des reads family-specific et ne définit aucune unicité globale transaction+account ; une registry globale ajouterait un couplage physique non requis.
|
||||
|
||||
### 5.3 `RawAccountStateQuery`
|
||||
|
||||
| Entrée query | Cardinalité | Projection physique |
|
||||
|-----------------------|------------------------|------------------------------------------------------------------|
|
||||
| network | obligatoire | guard DB mono-réseau + digest cursor |
|
||||
| pubkey | optionnel | filtre égalité ; aucune query owner |
|
||||
| slots.start_inclusive | optionnel | `slot >=` |
|
||||
| slots.end_inclusive | optionnel | `slot <=` |
|
||||
| direction | Ascending / Descending | ordre total entièrement inversé |
|
||||
| page.limit | u64 > 0 | limite physique PostgreSQL seulement ; `LIMIT + 1` |
|
||||
| page.cursor | opaque <= 4 KiB | V1 account fixe, authentification de contexte par digest SHA-256 |
|
||||
|
||||
Aucune query API par owner/provider/time/status n'existe. Aucun index pour ces dimensions ne sera introduit dans V002.
|
||||
|
||||
## 6. Audit kbot3 ciblé
|
||||
|
||||
L'archive historique contient notamment `k_sol_obs_account_observations`, `k_sol_core_account_states`, `AccountStateStore` et le pipeline de normalisation `account_state_normalization`. Cette surface mélangeait acquisition, normalisation CORE et processing policy ; elle n'est pas transposable telle quelle.
|
||||
|
||||
| Élément historique | Classement | Décision KSP |
|
||||
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
|
||||
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
|
||||
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
|
||||
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
|
||||
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
|
||||
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
|
||||
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
|
||||
| observation duplique l’état canonique | REDESSINER | observation référence un state complet séparé |
|
||||
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
|
||||
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
|
||||
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
|
||||
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
|
||||
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
|
||||
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
|
||||
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
|
||||
|
||||
Aucun code kbot3 ne sera copié. Les optional Yellowstone actuelles (`write_version`, `transaction_signature`, `is_startup`) n'existaient pas comme contrat durable équivalent dans cette persistence historique et restent donc définies uniquement par `ksp-store-api` actuel.
|
||||
|
||||
Points héritage supplémentaires explicitement classés :
|
||||
|
||||
- **REPRENDRE** : l'ancien repository utilisait déjà une transaction pour rendre atomiques state CORE + mutation observation + processing ledger ; seul le principe transactionnel est repris, jamais le couplage CORE/ledger.
|
||||
- **REPRENDRE** : l'unicité `observation_key` et la validation additive des contraintes/indexes sont de bons invariants physiques.
|
||||
- **REDESSINER** : `write_version`, `transaction_signature` et `is_startup` n'ont pas d'équivalent persistant account fiable dans cette surface historique ; V002 suit exclusivement les optionalités du contrat KSP actuel.
|
||||
- **REDESSINER** : l'idempotence historique `source_observation_id` + `ON CONFLICT DO UPDATE` est remplacée par clé producer-owned + comparaison exacte + conflit stable.
|
||||
- **REJETER** : les erreurs historiques interpolent l'erreur SQL dans `ks_core::Error::db(...)`; KSP conserve uniquement kind/phase statiques et sûres.
|
||||
|
||||
## 7. Design physique V002 figé par `pre.001`
|
||||
|
||||
### 7.1 Tables minimales
|
||||
|
||||
```text
|
||||
ksp_raw_account_states
|
||||
ksp_raw_account_observations
|
||||
```
|
||||
|
||||
Aucune table archive/tombstone account n'est ajoutée : `ksp-store-api` ne définit aucune capability de retention account.
|
||||
|
||||
### 7.2 Colonnes `ksp_raw_account_states`
|
||||
|
||||
```text
|
||||
pubkey BYTEA NOT NULL
|
||||
slot NUMERIC(20,0) NOT NULL
|
||||
state_hash BYTEA NOT NULL
|
||||
lamports NUMERIC(20,0) NOT NULL
|
||||
owner BYTEA NOT NULL
|
||||
executable BOOLEAN NOT NULL
|
||||
rent_epoch NUMERIC(20,0) NOT NULL
|
||||
data BYTEA NOT NULL
|
||||
```
|
||||
|
||||
Contraintes minimales : fixed width 32 pour `pubkey`, `owner`, `state_hash`; domaine u64 pour `slot`, `lamports`, `rent_epoch`; `octet_length(data) <= 16777216`; aucune borne basse sur data.
|
||||
|
||||
### 7.3 Colonnes `ksp_raw_account_observations`
|
||||
|
||||
```text
|
||||
observation_key BYTEA NOT NULL
|
||||
account_pubkey BYTEA NOT NULL
|
||||
account_slot NUMERIC(20,0) NOT NULL
|
||||
account_state_hash BYTEA NOT NULL
|
||||
provider TEXT NOT NULL
|
||||
protocol TEXT NOT NULL
|
||||
acquisition_method TEXT NOT NULL
|
||||
origin TEXT NOT NULL
|
||||
received_at_unix_millis BIGINT NOT NULL
|
||||
capture_session_id TEXT NULL
|
||||
commitment TEXT NULL
|
||||
endpoint_id TEXT NULL
|
||||
filter_id TEXT NULL
|
||||
observed_at_unix_millis BIGINT NULL
|
||||
source_payload_hash BYTEA NULL
|
||||
source_payload_size_bytes BIGINT NULL
|
||||
is_startup BOOLEAN NULL
|
||||
transaction_signature BYTEA NULL
|
||||
write_version NUMERIC(20,0) NULL
|
||||
```
|
||||
|
||||
Les contraintes provenance doivent être sémantiquement identiques à V001 : code 1..128 octets/alphabet sûr, origin fermé, timestamps bornés/ordonnés, source hash 32 bytes et source payload <= 64 MiB.
|
||||
|
||||
### 7.4 Indexes justifiés
|
||||
|
||||
Indexes provenant des PK :
|
||||
|
||||
```text
|
||||
PK ksp_raw_account_states(pubkey, slot, state_hash)
|
||||
PK ksp_raw_account_observations(observation_key)
|
||||
```
|
||||
|
||||
Un seul index métier additionnel est justifié par la query non filtrée :
|
||||
|
||||
```text
|
||||
ix_ksp_raw_account_states_slot_pubkey_state_hash(slot, pubkey, state_hash)
|
||||
```
|
||||
|
||||
Le PK couvre déjà efficacement la navigation lorsque `pubkey = ?` est imposé. Aucun index owner/provider/protocol/method/time/hash isolé n'est admis dans cette release.
|
||||
|
||||
## 8. Ordre total et pagination keyset
|
||||
|
||||
Ordre canonique :
|
||||
|
||||
```text
|
||||
Ascending : (slot ASC, pubkey ASC, state_hash ASC)
|
||||
Descending : (slot DESC, pubkey DESC, state_hash DESC)
|
||||
```
|
||||
|
||||
Le backend PostgreSQL compare `BYTEA` selon son ordre btree déterministe. Aucun `OFFSET` n'est utilisé.
|
||||
|
||||
Cursor account V1 proposé :
|
||||
|
||||
```text
|
||||
bytes 0..4 magic = "KSPA"
|
||||
byte 4 version = 1
|
||||
bytes 5..13 last_slot, u64 big-endian
|
||||
bytes 13..45 last_pubkey, 32 bytes
|
||||
bytes 45..77 last_state_hash, 32 bytes
|
||||
bytes 77..109 SHA-256 binding digest
|
||||
longueur fixe = 109 bytes
|
||||
```
|
||||
|
||||
Domaine de digest :
|
||||
|
||||
```text
|
||||
KSP/raw-account-state-cursor/v1
|
||||
```
|
||||
|
||||
Le digest lie au minimum : network, présence/valeur du filtre pubkey, direction, start/end slot, last slot, last pubkey, last state_hash. Le magic/domain distinct de `KSPT`/`KSP/raw-transaction-cursor/v1` rend le replay cross-family impossible même si les deux cursors ont 109 octets.
|
||||
|
||||
Le plafond physique PostgreSQL reste le précédent réel nécessaire à `LIMIT + 1` :
|
||||
|
||||
```text
|
||||
9_223_372_036_854_775_806
|
||||
```
|
||||
|
||||
Ce plafond n'est pas une policy métier KSP.
|
||||
|
||||
## 9. Algorithmes d'écriture
|
||||
|
||||
### 9.1 Acquisition state + observation atomiques
|
||||
|
||||
Préconditions avant I/O métier :
|
||||
|
||||
```text
|
||||
state.reference.network == backend.network
|
||||
observation.account.network == backend.network
|
||||
observation.account == state.reference
|
||||
```
|
||||
|
||||
Algorithme :
|
||||
|
||||
```text
|
||||
BEGIN
|
||||
INSERT state ... ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING ...
|
||||
if inserted:
|
||||
entity_outcome = Inserted
|
||||
else:
|
||||
SELECT canonical state BY PK FOR UPDATE
|
||||
decode + compare exact lamports/owner/executable/rent_epoch/data
|
||||
identical -> AlreadyPresent
|
||||
divergent -> ERROR_CODE_RAW_CONFLICT
|
||||
|
||||
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
|
||||
if inserted:
|
||||
observation_outcome = Inserted
|
||||
else:
|
||||
SELECT observation BY key FOR UPDATE
|
||||
decode + compare exact reference + provenance + all optional metadata
|
||||
identical -> AlreadyPresent
|
||||
divergent -> ERROR_CODE_RAW_CONFLICT
|
||||
COMMIT
|
||||
```
|
||||
|
||||
Une divergence d'observation après insertion d'un nouveau state fait rollback de ce state. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*` préalable, aucun overwrite silencieux.
|
||||
|
||||
### 9.2 Observation supplémentaire
|
||||
|
||||
```text
|
||||
validate network before I/O
|
||||
BEGIN
|
||||
SELECT state BY composite reference FOR KEY SHARE
|
||||
absent -> ReferenceNotFound
|
||||
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
|
||||
collision -> SELECT FOR UPDATE + exact compare
|
||||
COMMIT
|
||||
```
|
||||
|
||||
Cette opération ne crée jamais implicitement le state et ne retransmet pas ses bytes.
|
||||
|
||||
## 10. Matrice de concurrence
|
||||
|
||||
| Scénario | Résultat permis | Invariant |
|
||||
|------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------|
|
||||
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | attente unique + comparaison exacte ; aucun doublon |
|
||||
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | aucun overwrite ; résultat gagnant dépend de l’ordonnancement |
|
||||
| même pubkey+slot, state_hash différents | les deux states sont représentables | PK inclut state_hash |
|
||||
| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | transaction perdante rollback son state nouvellement créé |
|
||||
| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | state préexistant inchangé |
|
||||
| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | aucun update silencieux |
|
||||
| annulation après insertion state avant observation | aucune moitié durable | transaction PostgreSQL rollback/drop |
|
||||
|
||||
Le backend ne promet pas quel concurrent divergent gagne ; il promet qu'un seul contenu incompatible devient durable et que l'autre reçoit le conflit stable.
|
||||
|
||||
## 11. Taxonomie d'erreurs
|
||||
|
||||
Aucun nouveau `PostgresBackendErrorKind` n'est requis par le design actuel.
|
||||
|
||||
| Cas | Taxonomie stable | Règle |
|
||||
|---------------------------------------------------|------------------------------------------------------|---------------------------------|
|
||||
| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | guard avant SQL métier |
|
||||
| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | pas de state implicite |
|
||||
| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | message/context statiques |
|
||||
| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | aucun bytes hostile dans erreur |
|
||||
| page > limite physique PostgreSQL | PageLimitUnsupported | limite physique, pas policy KSP |
|
||||
| row DB incompatible/malformée | DataInvalid | aucune valeur hostile recopiée |
|
||||
| échec SELECT | ReadFailed | pas de server text exposé |
|
||||
| échec INSERT/transaction | WriteFailed | pas de server text exposé |
|
||||
| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | réutiliser taxonomy stable |
|
||||
|
||||
Les erreurs PostgreSQL externes ne doivent jamais être conservées comme source publique ni recopiées dans message/context.
|
||||
|
||||
## 12. Threat model `RawAccountState`
|
||||
|
||||
| Menace | Risque | Mitigation | Preuve prévue |
|
||||
|---------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|----------------------------|
|
||||
| URI pointe vers DB d’un autre réseau | corruption cross-network | binding `ksp_store_identity` + guard réseau avant opération | unit + live |
|
||||
| pubkey/hash/signature DB de mauvaise largeur | construction de modèle invalide | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows |
|
||||
| slot/lamports/rent_epoch/write_version > i64::MAX | narrowing/perte | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 |
|
||||
| data account > 16 MiB | allocation/persistence hostile | admission API + CHECK `octet_length <= 16777216` | unit + live boundary |
|
||||
| data account vide | rejet incorrect | aucune borne basse SQL ; vide explicitement valide | unit |
|
||||
| observation sans state | orphelin | FK composite + record pré-vérifie state | unit + live |
|
||||
| échec observation après state insert | demi-acquisition durable | une transaction PostgreSQL commune | live cancellation/rollback |
|
||||
| même ref, contenu divergent | overwrite silencieux | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent |
|
||||
| même pubkey/slot, hashes distincts | collision d’identité | PK `(pubkey,slot,state_hash)` | unit + live |
|
||||
| collision observation key | fusion de sources distinctes | comparaison exacte de tous les champs ; conflict sinon | unit + live |
|
||||
| cursor transaction injecté dans account | replay cross-family | magic/domain account distincts | unit cursor |
|
||||
| cursor account rejoué sur autre query | pagination incohérente | digest lie network/pubkey/range/direction/last key | unit cursor matrix |
|
||||
| ordre même slot ambigu | duplication/skip entre pages | ordre total `(slot,pubkey,state_hash)` | unit + live pagination |
|
||||
| metadata Yellowstone promue au state | couplage provider | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary |
|
||||
| transaction_signature impose transaction row | couplage cross-family non garanti | aucune FK vers `ksp_raw_transactions` | schema canary |
|
||||
| SQL/server text fuit | secret/PII leak | projection PostgresBackendError statique | hardening |
|
||||
| V000/V001 modifiées | history incompatible | checksums figés et canaries exactes | migration tests |
|
||||
| index non justifié | write amplification/bloat | un seul index métier V002 pour query unfiltered | schema completeness |
|
||||
|
||||
## 13. Compatibilité schema/migration
|
||||
|
||||
V002 devra suivre le moteur resource-owned de V001 : ressources ordonnées, checksums logiques, vérification catalogique et mutation selon les policies foundation existantes.
|
||||
|
||||
Principes :
|
||||
|
||||
```text
|
||||
schema_creation = disabled : aucune création implicite
|
||||
schema_creation = create_if_missing : bootstrap seulement selon contrat existant
|
||||
schema_update = disabled : historique incomplet -> mismatch sûr
|
||||
schema_update = update_if_needed : V002 appliquée additivement
|
||||
history > latest embedded : SchemaNewer
|
||||
history V000/V001 divergente : MigrationMismatch
|
||||
extra columns externes : acceptables seulement si elles ne peuvent bloquer les INSERT KSP
|
||||
colonnes/constraints/index requis incompatibles : mismatch avant ready
|
||||
```
|
||||
|
||||
Les numéros de fichiers SQL exacts sont une responsabilité de `pre.002/pre.003`; leur sémantique est figée ici, pas leurs noms séquentiels.
|
||||
|
||||
## 14. Mapping/read privé
|
||||
|
||||
Le module backend proposé est `raw_account` privé, analogue structurellement à `raw_transaction` mais sans retention/archive.
|
||||
|
||||
Il devra contenir :
|
||||
|
||||
```text
|
||||
mapping row -> RawAccountState
|
||||
mapping row -> RawAccountObservation
|
||||
mapping u64 <-> NUMERIC text sans narrowing
|
||||
get state par composite reference
|
||||
get observation par observation_key
|
||||
list references par keyset
|
||||
cursor account privé
|
||||
write acquisition/observation privé
|
||||
```
|
||||
|
||||
Aucun row/statement/client PostgreSQL ne traverse `ksp-store-postgres-lib` publiquement ni `ksp-store-lib`.
|
||||
|
||||
## 15. Stratégie de preuve PostgreSQL réelle
|
||||
|
||||
La preuve live restera `#[ignore]`, opt-in, avec une URI dédiée lue depuis stdin et sans environnement direct. Elle devra refuser de détruire des objets KSP préexistants non créés par le test.
|
||||
|
||||
Scénarios minimum :
|
||||
|
||||
```text
|
||||
bootstrap V000+V001+V002 et health ready
|
||||
réouverture idempotente du schéma
|
||||
state bytes vide et 16 MiB
|
||||
u64 au-delà de i64::MAX et u64::MAX pour colonnes NUMERIC
|
||||
acquisition identique puis divergente
|
||||
same pubkey + same slot + distinct state_hash
|
||||
observation supplémentaire / reference not found
|
||||
collision observation
|
||||
pagination filtered/unfiltered ASC/DESC multi-page
|
||||
cursor hostile / replay query / replay transaction
|
||||
concurrence identique/divergente
|
||||
cancellation entre state et observation -> rollback
|
||||
coexistence avec au moins un canari RawTransaction stable dans la même DB V002
|
||||
cleanup vérifié uniquement pour les objets créés par le test
|
||||
```
|
||||
|
||||
Aucun appel Solana réseau n'est nécessaire à cette preuve.
|
||||
|
||||
## 16. Conformance cross-family finale
|
||||
|
||||
À `pre.008+`, les canaries devront compter exactement :
|
||||
|
||||
```text
|
||||
PostgresBackend : 10 impl RAW
|
||||
Store : 10 impl RAW
|
||||
```
|
||||
|
||||
La façade doit continuer de n'exposer que les modèles/capabilities backend-agnostic. Le test `--no-default-features` de `ksp-store-lib` doit rester vert : un build sans backend PostgreSQL compile et rejette proprement un backend non compilé au runtime.
|
||||
|
||||
La conformance finale doit aussi démontrer que l'ajout account ne modifie pas les six comportements transaction : lecture/write/observation/list/retention/force-rehydrate restent couverts par leurs suites existantes.
|
||||
|
||||
## 17. Sizing recalibré — prereleases souples
|
||||
|
||||
Chaque tranche technique vise **environ 15 à 20 minutes de travail effectif** lorsque le sujet s'y prête. Cette durée est une cible de granularité et non une durée maximale : compilation, diagnostic, live PostgreSQL ou difficulté réelle peuvent légitimement prolonger une tranche.
|
||||
|
||||
Le forecast reste volontairement souple. Une tranche peut être scindée, fusionnée ou réordonnée par delta si la réalité technique l'exige, à condition de préserver les responsabilités de clôture. Le nombre de prereleases prévu n'impose ni une session de conversation par tranche, ni une clôture artificielle sur le numéro initialement prévu.
|
||||
|
||||
Chaque prerelease possède un statut directement modifiable. Lorsqu'un correctif d'une tranche est nécessaire, il est ajouté sous la prerelease concernée avec un titre `#### pre.NNN-fix.MMM`, ce qui conserve l'historique de progression et permet d'ajouter des fixes sans transformer le forecast en tableau.
|
||||
|
||||
### `pre.001` — Audit, kbot3, threat model et design V002
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Budget cible : **15-20 min**. Audit de la base `v0.3.3`, étude ciblée kbot3, threat model, design physique V002, sizing et création du plan/de la validation. Aucun SQL V002, aucune table account et aucun repository account ne sont avancés dans cette tranche.
|
||||
|
||||
#### `pre.001-fix.001` — Forecast souple éditable et hiérarchie des fixes
|
||||
|
||||
**Statut : réalisé ; correctif documentaire uniquement.**
|
||||
|
||||
Remplacement du sizing tabulaire par des sous-sections éditables avec statut explicite. Les futurs correctifs peuvent être insérés sous leur prerelease avec un titre `####`, sans modifier le découpage technique, le scope fonctionnel ni les responsabilités de clôture de `0.3.4`. Conformément à `VER-ID-008`, ce correctif documentaire ne modifie pas `workspace.package.version`, qui reste `0.3.4-pre.1`.
|
||||
|
||||
### `pre.002` — Registry V002 et tables minimales
|
||||
|
||||
**Statut : réalisé ; corrigé par `pre.002-fix.001`, gate opérateur complet PASS.**
|
||||
|
||||
Budget cible : **15-20 min**. V002 `raw_account_state` est enregistrée avec exactement cinq resources : deux tables, les PK `(pubkey, slot, state_hash)` / `(observation_key)` et la FK composite observation -> state. Aucun repository account ni dispatch façade n'est ajouté.
|
||||
|
||||
Le checksum resources calculable sur cet état intermédiaire est `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55`. Il est **provisoire de prerelease** : `pre.003` ajoute encore les contraintes de domaine et l'index prévus, puis fige le checksum V002 final avant toute preuve live account. V000/V001 restent strictement inchangées.
|
||||
|
||||
#### `pre.002-fix.001` — correction des canaris V002
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Le gate opérateur de `pre.002` compile le workspace et passe Clippy, Store API, Store façade, Config et `--no-default-features`, mais révèle deux canaris backend obsolètes. Le premier construisait encore un historique « futur » en version 2 alors que V002 occupe désormais cette version ; il est recalibré avec V002 valide suivie d'une version 3 inconnue. Le second comparait la FK V002 à une chaîne SQL sensible aux retours à la ligne ; il normalise désormais uniquement les espaces avant de vérifier le contrat physique.
|
||||
|
||||
Aucune ressource SQL, migration, table, contrainte, index, surface runtime ou capability n'est modifiée. Le checksum V002 intermédiaire reste donc exactement `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55`.
|
||||
|
||||
### `pre.003` — Contraintes, index et schema compatibility V002
|
||||
|
||||
**Statut : réalisé ; corrigé par `pre.003-fix.001`, gate opérateur complet PASS.**
|
||||
|
||||
Budget cible : **15-20 min**. V002 est complétée à **32 resources** : deux tables, 29 contraintes (PK/FK incluses) et un index non unique `(slot, pubkey, state_hash)` sans prédicat. Les contraintes matérialisent les bornes backend-agnostic existantes : fixed-width 32/64 bytes, domaines `u64` via `NUMERIC(20,0)`, data `<= 16 MiB`, codes de provenance 1..128 octets/alphabet sûr, timestamps bornés, ordre `observed_at <= received_at` et source payload `<= 64 MiB`.
|
||||
|
||||
Le checksum V002 final est `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. La transition depuis le checksum provisoire `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55` est acceptée **uniquement** sous `schema_update=update_if_needed` : le moteur répare/valide d'abord les resources V002 manquantes, vérifie la compatibilité externe puis remplace atomiquement le checksum d'historique dans la même transaction. `schema_update=disabled` conserve un mismatch sûr et tout checksum inconnu reste rejeté.
|
||||
|
||||
Le probe d'adoption reconnaît désormais aussi les deux tables V002. Aucun `src/raw_account.rs`, aucune implémentation `RawAccount*` et aucun dispatch Store ne sont ouverts dans cette tranche.
|
||||
|
||||
Gate de base `pre.002-fix.001` fourni le 2026-08-30 : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, suites `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, `ksp-config-lib` et `ksp-store-lib --no-default-features` sont tous PASS.
|
||||
|
||||
#### `pre.003-fix.001` — ordre alphabétique du bloc `const` migration
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Le gate opérateur de `pre.003` confirme `cargo check --workspace`, Clippy all-targets, `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib` (45 tests), `ksp-config-lib` et `ksp-store-lib --no-default-features`, mais l'audit Rust détecte une unique violation `RUST-FMT-104` dans `src/migration.rs`. Le correctif déplace seulement `HISTORY_LOAD_SQL` avant `HISTORY_UPDATE_CHECKSUM_SQL` afin de restaurer l'ordre alphabétique du bloc homogène de constantes.
|
||||
|
||||
Aucune valeur de constante, requête SQL, ressource V002, checksum, logique de migration, compatibilité de schéma, repository ou capability n'est modifiée. La version Cargo devient `0.3.4-pre.3.fix.1` conformément à `VER-ID-007/010`, car un fichier Rust est modifié.
|
||||
|
||||
### `pre.004` — Mapping privé et lectures `get`
|
||||
|
||||
**Statut : réalisé ; gate opérateur initial en échec ciblé sur un canari.**
|
||||
|
||||
Budget cible : **15-20 min**. Le module privé `src/raw_account.rs` possède désormais les deux SELECT de lecture et les codecs physiques de `RawAccountState` / `RawAccountObservation`. `PostgresBackend` expose uniquement `get_raw_account_state` et `get_raw_account_observation` avec des modèles `ksp-store-api` ; aucun `tokio_postgres::Row`, SQL ou type physique ne traverse le bridge.
|
||||
|
||||
Le mapping revalide les invariants même si PostgreSQL devait contenir une ligne hostile : `pubkey`/owner/hash/key en largeur exacte, signature optionnelle à 64 bytes, `slot`/`lamports`/`rent_epoch`/`write_version` via `NUMERIC(20,0)::text -> u64`, timestamps et source payload size bornés, provenance reconstruite via les constructeurs API et ordre `observed_at <= received_at`. Les bytes account sont admis via `RawAccountState::try_new`, donc data vide reste valide et `> 16 MiB` devient `DataInvalid`.
|
||||
|
||||
Le `RawAccountStateReference` reçu par `get` est vérifié contre le réseau mono-backend **avant `pool.get()`**. `RawObservationKey` ne porte pas de réseau ; le réseau de la référence account reconstruite provient exclusivement du backend déjà lié par `ksp_store_identity`.
|
||||
|
||||
La tranche reste strictement read-only : aucun `INSERT`, `UPDATE`, `DELETE`, `ON CONFLICT`, `FOR UPDATE`, cursor ou `list_raw_account_states`. Les quatre `impl RawAccount* for PostgresBackend` restent interdits jusqu'à `pre.008`; `pre.005` et `pre.006` possèdent encore les écritures.
|
||||
|
||||
#### `pre.004-fix.001` — conformité Clippy du mapping `Row`
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Le gate opérateur de `pre.004` confirme `cargo check --workspace`, les suites ciblées `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib` (52 tests), `ksp-config-lib` et `ksp-store-lib --no-default-features`, tandis que `cargo clippy --workspace --all-targets` rejette les 27 extractions `Row::try_get(...).map_err(...)?` du nouveau module. Chaque occurrence produit conjointement `clippy::question-mark-used` et `clippy::implicit-return`, conformément aux interdictions `RUST-ERR-002/003`. Le même gate signale aussi un `crate_root` inutilisé dans le canari `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
|
||||
|
||||
Le correctif déplie les extractions physiques en `match` typés avec retour `DataInvalid` explicite et supprime uniquement la variable de test inutilisée. Aucun SELECT, phase d'erreur, type physique, invariant de mapping, signature de bridge, test fonctionnel, migration ou checksum n'est modifié. La version Cargo devient `0.3.4-pre.4.fix.1` conformément à `VER-ID-007/010`, car deux fichiers Rust sont modifiés. Le gate opérateur complet du correctif est vert : audits Rust/Markdown, workspace check, Clippy all-targets, suites Store API/façade/PostgreSQL/Config et `--no-default-features` passent.
|
||||
|
||||
### `pre.005` — Acquisition atomique et idempotence
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Budget cible : **15-20 min**. L'acquisition account est désormais une transaction PostgreSQL unique : guard réseau et identité exacte avant I/O, `INSERT ... ON CONFLICT DO NOTHING` du state, verrouillage `FOR UPDATE` et comparaison exacte de `lamports/owner/executable/rent_epoch/data` en cas de collision, puis insertion idempotente de l'observation avec comparaison exacte de la référence, de toute la provenance et des metadata optionnelles. Une divergence retourne `Conflict` sans overwrite.
|
||||
|
||||
Si le state vient d'être inséré mais que l'observation collide avec un contenu divergent, le retour d'erreur abandonne la transaction et ne laisse donc aucun state orphelin durable. Le bridge étroit `PostgresBackend::persist_raw_account_acquisition` est ouvert sans implémenter `RawAccountStateWrite`; les quatre traits account restent fermés jusqu'à `pre.008`.
|
||||
|
||||
À la clôture de `pre.005`, `record_raw_account_observation`, `list_raw_account_states`, le cursor `KSPA`, tout `UPDATE`/`DELETE` account et tout `ON CONFLICT DO UPDATE` restent absents. Le gate opérateur complet du 2026-08-30 est vert : audits Rust/Markdown, workspace check, Clippy all-targets, suites Store API/façade/PostgreSQL/Config et `--no-default-features` passent.
|
||||
|
||||
### `pre.006` — Observation supplémentaire et concurrence unitaire
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet à rejouer.**
|
||||
|
||||
Budget cible : **15-20 min**. `PostgresBackend::record_raw_account_observation` ouvre le bridge étroit d'observation supplémentaire sans implémenter encore `RawAccountObservationWrite`. Le guard `observation.account.network == backend.network` est évalué avant `pool.get()`. La transaction verrouille ensuite la référence `(pubkey, slot, state_hash)` existante avec `SELECT 1 ... FOR KEY SHARE`; l'absence retourne `ReferenceNotFound` et aucun state n'est créé implicitement.
|
||||
|
||||
L'insertion réutilise exactement `persist_account_observation_row` de `pre.005` : `ON CONFLICT (observation_key) DO NOTHING`, puis `SELECT ... FOR UPDATE` et comparaison du `RawAccountObservation` complet sur collision. Une observation identique retourne `AlreadyPresent`; une collision divergente retourne `Conflict`, sans overwrite.
|
||||
|
||||
Les canaris unitaires figent l'ordre transactionnel `BEGIN -> FOR KEY SHARE -> insert/compare observation -> COMMIT`. Aucun `spawn`, `mem::forget` ou commit anticipé n'est admis dans ce chemin : si la future est annulée avant le commit, le `Transaction` local est droppé et PostgreSQL rollback la mutation non committée. Cette tranche prouve donc la structure de cancellation/race ; la preuve avec vraies tâches concurrentes et `JoinHandle::abort()` reste volontairement au live `pre.009`.
|
||||
|
||||
`list_raw_account_states`, le cursor `KSPA`, `OFFSET`, tout `UPDATE`/`DELETE` account, `ON CONFLICT DO UPDATE` et les quatre `impl RawAccount* for PostgresBackend` restent absents.
|
||||
|
||||
### `pre.007` — Liste account et cursor keyset V1
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS via `pre.007-fix.001`.**
|
||||
|
||||
Budget cible : **15-20 min**. `RawAccountStateQuery` est matérialisée dans le backend PostgreSQL avec navigation keyset sur l'ordre total `(slot, pubkey, state_hash)`, entièrement inversé en DESC. Les requêtes sans filtre utilisent l'index V002 `(slot, pubkey, state_hash)` ; les requêtes filtrées par pubkey conservent le même ordre total et sont compatibles avec la PK `(pubkey, slot, state_hash)`. Aucun `OFFSET` ni plafond fonctionnel KSP n'est ajouté.
|
||||
|
||||
Le cursor V1 account est fixe à **109 bytes** : magic `KSPA`, version `1`, `last_slot` u64 big-endian, `last_pubkey` 32 bytes, `last_state_hash` 32 bytes et digest SHA-256. Le domaine `KSP/raw-account-state-cursor/v1` lie le réseau, la présence/valeur du filtre pubkey, les bornes de slot, la direction et la dernière clé complète ; les replays cross-query et cross-family `KSPT` sont rejetés. La seule limite de page est la limite physique PostgreSQL nécessaire à `LIMIT + 1`, soit `9_223_372_036_854_775_806`.
|
||||
|
||||
`PostgresBackend::list_raw_account_states` expose uniquement `RawAccountStateQuery -> RawPage<RawAccountStateReference>`. Les quatre implémentations `RawAccount*` et le dispatch `Store` restent réservés à `pre.008`.
|
||||
|
||||
#### `pre.007-fix.001` — Réconciliation Clippy/canaris pagination
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Fix strictement borné aux tests/canaris : suppression de cinq `clone()` sur `Pubkey: Copy` dans les canaris cursor, et transformation des canaris hérités de `pre.005/pre.006` pour qu'ils continuent d'interdire `UPDATE`, `DELETE`, `ON CONFLICT DO UPDATE`, `OFFSET` et les quatre implémentations `RawAccount*`, sans interdire `list_raw_account_states` désormais possédée par `pre.007`. Aucun code de pagination, SQL métier ou migration n'est modifié.
|
||||
|
||||
### `pre.008` — Implémentations backend, dispatch Store et conformance 10/10
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet à rejouer.**
|
||||
|
||||
Budget cible : **15-20 min**. Les quatre capabilities account sont implémentées directement sur `PostgresBackend` en adaptant exclusivement les bridges déjà validés en `pre.004` à `pre.007`, puis sur `Store` avec le même mapping d'erreur et les mêmes guards réseau que la verticale transaction. Aucun nouveau SQL, migration, DTO ou type public n'est ajouté.
|
||||
|
||||
L'inventaire devient exactement :
|
||||
|
||||
```text
|
||||
6 RawTransaction* + 4 RawAccount* = 10/10
|
||||
PostgresBackend = 10 capabilities RAW
|
||||
Store = 10 capabilities RAW
|
||||
```
|
||||
|
||||
`RawAccountStateRead` délègue `get` et `list`; `RawAccountStateWrite` délègue l'acquisition atomique state+observation; `RawAccountObservationRead` délègue la lecture par observation key; `RawAccountObservationWrite` délègue l'observation supplémentaire avec référence state existante. La façade vérifie le réseau sur reference/query/state/observation avant dispatch lorsque l'input porte un réseau ; `RawAccountObservationRead` reste sans guard d'input réseau puisque sa clé n'en contient pas.
|
||||
|
||||
Les canaris historiques sont transformés pour vérifier l'emplacement des implémentations dans `runtime.rs`, l'absence d'implémentation directe dans `raw_account.rs`, l'inventaire exact 10/10 et l'absence de types/SQL PostgreSQL dans `ksp-store-lib`. `--no-default-features` doit conserver les dix traits sur `Store` tout en retournant `backend_not_compiled` au runtime faute de backend compilé.
|
||||
|
||||
#### `pre.008-fix.001` — Réconciliation du canari `dependency_boundary`
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Fix strictement borné à `tests/dependency_boundary.rs` : la déclaration `runtime = include_str!("../src/runtime.rs")` est restaurée dans le canari d'ownership `pre.005` qui l'utilise encore pour interdire l'environnement et le SQL physique dans `runtime.rs`, puis retirée du canari V002 `pre.003` où `pre.008` ne l'utilise plus. Aucune assertion de fond, capability, façade, runtime métier, SQL ou migration n'est modifiée.
|
||||
|
||||
### `pre.009` — Preuve PostgreSQL live account
|
||||
|
||||
**Statut : réalisé ; gate opérateur standard PASS et live account PASS sur PostgreSQL 17.**
|
||||
|
||||
Budget cible : **15-20 min**. La preuve réelle est matérialisée dans `tests/postgres_raw_account_live.rs`, ignored/opt-in et alimentée exclusivement par une URI dédiée lue depuis stdin sans écho. Elle refuse tout schéma KSP déjà présent, exige PostgreSQL >= 15, ouvre/ferme/re-ouvre le backend V002, vérifie la réparation de l'index account selon `schema_autoupdate`, puis nettoie uniquement le schéma qu'elle a prouvé absent avant démarrage.
|
||||
|
||||
La matrice live couvre : domaine `u64::MAX`, data vide et borne exacte 16 MiB, acquisition atomique, idempotence exacte, conflit de contenu sous même référence, deux `state_hash` distincts au même `(pubkey, slot)`, collision de clé observation avec rollback du state nouvellement inséré, observation supplémentaire et `ReferenceNotFound`, round-trip des metadata Yellowstone, pagination ASC/DESC filtrée/non filtrée, cursor `KSPA` hostile/cross-query et rejet cross-family `KSPT`, concurrence identique/divergente, cancellation réelle via `JoinHandle::abort()` pendant un conflit d'observation verrouillé, puis reopen.
|
||||
|
||||
La coexistence cross-family est prouvée dans le même schéma : une acquisition account et une acquisition `RawTransaction` peuvent utiliser la même `RawObservationKey` dans leurs tables familiales distinctes, restent lisibles indépendamment et survivent au reopen. Le live `RawTransaction` existant est également réconcilié pour considérer les deux tables V002 account dans son probe et son cleanup d'isolation ; aucune logique métier RawTransaction n'est modifiée.
|
||||
|
||||
### `pre.010` — Hardening et complétude cross-family
|
||||
|
||||
**Statut : réalisé ; gate opérateur complet PASS.**
|
||||
|
||||
Budget cible : **15-20 min**. La tranche n'ajoute aucun runtime, SQL, migration, capability ou type public. Elle ferme les canaris de sécurité, ownership et complétude cross-family autour de la surface effectivement validée.
|
||||
|
||||
Les canaris backend figent désormais explicitement : surface SQL account non destructive et keyset-only ; cursor `KSPA` distinct du cursor transaction `KSPT` ; V002 exactement 32 resources (2 tables, 2 PK, 1 FK state-local, 26 CHECK, 1 index non filtré) ; absence totale de table/FK/registry transaction dans V002 ; `transaction_signature` account reste une metadata fixed-width sans FK vers `ksp_raw_transactions`.
|
||||
|
||||
Les canaris façade comparent directement les noms des dix traits RAW implémentés par `Store` et `PostgresBackend`, imposent l'égalité exacte des deux ensembles et interdisent toute apparition implicite de retention/delete/compaction account. Les inventaires publics, dépendances, guards réseau, erreurs sûres, absence d'environnement/SQL physique dans la façade et placement des implémentations backend restent couverts par les canaris hérités.
|
||||
|
||||
`ksp-store-api`, tous les fichiers `src/`, les tests live et les migrations V000/V001/V002 restent byte-inchangés par cette tranche.
|
||||
|
||||
### `pre.011` — Gate technique final
|
||||
|
||||
**Statut : réalisé ; gate technique final PASS.**
|
||||
|
||||
Budget cible : **15-20 min**. La tranche n'ajoute aucun code, test fonctionnel, SQL, migration, capability, dépendance runtime, README/USAGE, CHANGELOG/ROADMAP ou prompt suivant. Elle synchronise uniquement la version de prerelease, le plan, la validation et son delta afin de rejouer le gate technique final sur la surface candidate déjà durcie.
|
||||
|
||||
Le replay de clôture confirme `cargo test -p ksp-store-lib --no-default-features`, `cargo test --workspace`, les graphes Cargo façade/backend/features/duplicates et `postgres_raw_account_live` avec URI dédiée via stdin. Le live account passe sur PostgreSQL 17 et traverse bootstrap/reopen V002, concurrence/cancellation et coexistence réelle avec `RawTransaction`; les lives foundation et RawTransaction séparés ne sont pas requis par cette tranche.
|
||||
|
||||
### `pre.012` — Réconciliation documentaire finale
|
||||
|
||||
**Statut : matérialisé ; gate documentaire opérateur à exécuter.**
|
||||
|
||||
Budget cible : **10-15 min**. Réconcilier les documents durables requis avec la surface effectivement validée, sans rouvrir le code fonctionnel ni préparer la publication. Les README/USAGE de `ksp-store-lib` et `ksp-store-postgres-lib` décrivent désormais la surface RAW 10/10 et les opérations `RawAccountState` ; le plan et la validation enregistrent le PASS technique de `pre.011`. `CHANGELOG.md`, `ROADMAP.md` et le prompt `0.3.5` restent réservés à `pre.013`.
|
||||
|
||||
### `pre.013` — Préparation de publication minimale
|
||||
|
||||
**Statut : planifié.**
|
||||
|
||||
Budget cible : **10-15 min**. Dernière prerelease : prompt `0.3.5`, `CHANGELOG.md` et `ROADMAP.md` uniquement hors mécanique Cargo/delta, conformément au couloir de publication.
|
||||
|
||||
### `rel.001` — Stabilisation et tag `v0.3.4`
|
||||
|
||||
**Statut : planifié.**
|
||||
|
||||
Budget cible : **5-10 min**. Stabilisation/tag après gate opérateur final, sans absorption de nouveau scope fonctionnel ou documentaire.
|
||||
|
||||
## 18. Hors scope explicite
|
||||
|
||||
```text
|
||||
modification ksp-store-api
|
||||
retention/archivage/purge account
|
||||
STRUCTURAL/CORE materialization
|
||||
Interface events
|
||||
workers/jobs/backfill executors
|
||||
batch-size/priorité/backlog policy
|
||||
Transport -> Store direct
|
||||
index owner/provider/time/status sans query
|
||||
compression account
|
||||
nouveau backend autre que PostgreSQL
|
||||
application Store Desk
|
||||
```
|
||||
|
||||
## 19. Questions ouvertes après `pre.001`
|
||||
|
||||
Aucune question **bloquante** de contrat ne subsiste.
|
||||
|
||||
Les détails suivants restent volontairement d'implémentation : numérotation exacte des ressources V002, découpage précis des helpers de mapping et noms internes des statements. Ils ne doivent pas modifier les invariants de ce plan.
|
||||
|
||||
## 20. Gate d'acceptation `pre.001`
|
||||
|
||||
`pre.001` est acceptable si :
|
||||
|
||||
```text
|
||||
base v0.3.3 vérifiée
|
||||
V000/V001 checksums recalculés et figés
|
||||
inventaire 6/10 -> 10/10 explicite
|
||||
field mapping account complet
|
||||
kbot3 classifié REPRENDRE/REDESSINER/REPORTER/REJETER
|
||||
V002 figée sans SQL écrit
|
||||
ordre/cursor figés
|
||||
algorithmes atomicité/idempotence/concurrence figés
|
||||
threat model complet
|
||||
forecast recalibré
|
||||
aucune modification fonctionnelle hors version prerelease
|
||||
audits statiques applicables verts ; gate Cargo opérateur rejoué
|
||||
```
|
||||
766
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
Normal file
766
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
Normal file
@@ -0,0 +1,766 @@
|
||||
<!-- file: docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md -->
|
||||
<!-- version: 17 -->
|
||||
|
||||
# Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
|
||||
|
||||
## 1. Portée
|
||||
|
||||
Cette matrice est ouverte par `0.3.4-pre.001`. `0.3.4-pre.002` matérialise la fondation V002 minimale et `pre.002-fix.001` corrige deux canaris sans modifier le SQL ; son gate opérateur complet est PASS. `0.3.4-pre.003` complète les contraintes de domaine, l'index de navigation, la compatibilité externe et le checksum V002 final ; `pre.003-fix.001` corrige uniquement l'ordre alphabétique du bloc `const` et son gate opérateur complet est PASS. `pre.004` à `pre.007` matérialisent successivement mapping/read, writes, observation supplémentaire et pagination account ; `pre.007-fix.001` clôt son gate opérateur en PASS. `pre.008` ouvre les quatre capabilities account dans `PostgresBackend` et `Store`, portant l'inventaire RAW statique à 10/10 ; `pre.008-fix.001` corrige son unique canari mal réconcilié et son gate opérateur complet est PASS. `pre.009` matérialise la preuve PostgreSQL live account, la concurrence/cancellation réelle et la coexistence RawTransaction sans modifier les migrations ; son gate standard et son live account sur PostgreSQL 17 sont PASS. `pre.010` ferme les canaris cross-family sans modifier le runtime et son gate opérateur complet est PASS. `pre.011` ouvre le gate technique final, le replay live ciblé et les graphes Cargo sans nouveau développement.
|
||||
|
||||
Base :
|
||||
|
||||
```text
|
||||
v0.3.3
|
||||
```
|
||||
|
||||
Cible release :
|
||||
|
||||
```text
|
||||
PostgresBackend = 10/10 capabilities RAW
|
||||
Store = 10/10 capabilities RAW
|
||||
```
|
||||
|
||||
## 2. Gate base `pre.001`
|
||||
|
||||
| Preuve | Attendu | État pre.001 |
|
||||
|------------------------------------|------------------|-------------------------------------------------|
|
||||
| workspace.package.version base | 0.3.3 | PASS audit |
|
||||
| V000 checksum | d29068b8…46a1450 | PASS recalcul indépendant |
|
||||
| V001 checksum | 31488cda…d72cc51 | PASS recalcul indépendant / 40 resources |
|
||||
| capabilities transaction physiques | 6 | PASS inventaire source |
|
||||
| capabilities account physiques | 0 | PASS inventaire source ; volontairement pending |
|
||||
| canaries anti-account 0.3.3 | présentes | PASS inventaire tests |
|
||||
| ksp-store-api change nécessaire | non | PASS audit |
|
||||
| SQL V002 créé en pre.001 | non | PASS scope |
|
||||
|
||||
## 3. Matrice capability finale
|
||||
|
||||
| Capability | Base v0.3.3 | Cible 0.3.4 | Preuve finale |
|
||||
|--------------------------------|-------------|-------------|-------------------------------|
|
||||
| RawTransactionRead | PASS | PASS | régression suite 0.3.3 |
|
||||
| RawTransactionWrite | PASS | PASS | régression suite 0.3.3 |
|
||||
| RawTransactionObservationRead | PASS | PASS | régression suite 0.3.3 |
|
||||
| RawTransactionObservationWrite | PASS | PASS | régression suite 0.3.3 |
|
||||
| RawTransactionRetentionRead | PASS | PASS | régression suite 0.3.3 |
|
||||
| RawTransactionRetentionWrite | PASS | PASS | régression suite 0.3.3 |
|
||||
| RawAccountStateRead | PENDING | PASS | tests backend + façade + live |
|
||||
| RawAccountStateWrite | PENDING | PASS | tests backend + façade + live |
|
||||
| RawAccountObservationRead | PENDING | PASS | tests backend + façade + live |
|
||||
| RawAccountObservationWrite | PENDING | PASS | tests backend + façade + live |
|
||||
|
||||
### 3.1 Contrat opérationnel détaillé des quatre capabilities account
|
||||
|
||||
| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live |
|
||||
|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------|
|
||||
| RawAccountStateRead | reference / query | Option<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
|
||||
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
|
||||
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
|
||||
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
|
||||
|
||||
## 4. Matrice `RawAccountState`
|
||||
|
||||
| Champ | Représentation décidée | Validation requise |
|
||||
|------------|------------------------------------------|-----------------------------------------------------------------|
|
||||
| network | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
|
||||
| pubkey | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
|
||||
| slot | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
|
||||
| state_hash | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
|
||||
| lamports | NUMERIC(20,0) NOT NULL | domaine u64 exact |
|
||||
| owner | BYTEA NOT NULL | `octet_length = 32` |
|
||||
| executable | BOOLEAN NOT NULL | mapping direct |
|
||||
| rent_epoch | NUMERIC(20,0) NOT NULL | domaine u64 exact |
|
||||
| data | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
|
||||
|
||||
Points structurants :
|
||||
|
||||
```text
|
||||
identity = network API + (pubkey, slot, state_hash)
|
||||
network SQL = singleton ksp_store_identity, pas colonne row
|
||||
PK state = (pubkey, slot, state_hash)
|
||||
state_hash n'est pas une preuve suffisante d'idempotence : comparaison contenu exacte obligatoire
|
||||
empty data = valide
|
||||
```
|
||||
|
||||
## 5. Matrice observation
|
||||
|
||||
| Champ | Représentation décidée | Validation requise |
|
||||
|---------------------------|------------------------|--------------------------------------------------|
|
||||
| observation_key | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
|
||||
| account.pubkey | BYTEA NOT NULL | FK composite vers state |
|
||||
| account.slot | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
|
||||
| account.state_hash | BYTEA NOT NULL | FK composite |
|
||||
| provider | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
|
||||
| protocol | TEXT NOT NULL | idem V001 |
|
||||
| acquisition_method | TEXT NOT NULL | idem V001 |
|
||||
| origin | TEXT NOT NULL | backfill/import/live/repair/replay |
|
||||
| received_at | BIGINT NOT NULL | 0..253402300799999 |
|
||||
| capture_session_id | TEXT NULL | mêmes checks V001 |
|
||||
| commitment | TEXT NULL | mêmes checks V001 |
|
||||
| endpoint_id | TEXT NULL | mêmes checks V001 |
|
||||
| filter_id | TEXT NULL | mêmes checks V001 |
|
||||
| observed_at | BIGINT NULL | borne + `observed_at <= received_at` |
|
||||
| source_payload_hash | BYTEA NULL | NULL ou 32 bytes |
|
||||
| source_payload_size_bytes | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
|
||||
| is_startup | BOOLEAN NULL | Yellowstone metadata observation-only |
|
||||
| transaction_signature | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
|
||||
| write_version | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
|
||||
|
||||
Invariants additionnels :
|
||||
|
||||
```text
|
||||
FK observation -> state composite
|
||||
record_raw_account_observation n'insère jamais le state
|
||||
transaction_signature observation-only, sans FK transaction
|
||||
write_version/is_startup observation-only
|
||||
observation_key family-local unique
|
||||
```
|
||||
|
||||
## 6. Query/pagination
|
||||
|
||||
| Élément | Décision | Canari requis |
|
||||
|-----------------|------------------------------------------------|--------------------------------|
|
||||
| ordre ASC | slot, pubkey, state_hash ASC | multi-page sans duplicate/skip |
|
||||
| ordre DESC | slot, pubkey, state_hash DESC | symétrique ASC |
|
||||
| filtre pubkey | égalité optionnelle | cursor lié au filtre |
|
||||
| slot range | inclusive | cursor lié aux deux bornes |
|
||||
| cursor magic | KSPA | rejette KSPT transaction |
|
||||
| cursor version | 1 | rejette version hostile |
|
||||
| cursor longueur | 109 bytes | rejette taille hostile |
|
||||
| cursor domain | KSP/raw-account-state-cursor/v1 | digest family-bound |
|
||||
| cursor digest | network+pubkey filter+range+direction+last key | anti-replay query |
|
||||
| pagination | keyset seulement | aucun OFFSET |
|
||||
| limit | limite physique PostgreSQL uniquement | pas de batch policy |
|
||||
|
||||
## 7. Migration/schema V002
|
||||
|
||||
| Objet | Décision / état physique | État |
|
||||
|----------------------------------|--------------------------------------------------------------------------|---------------------|
|
||||
| V000 | bytes/checksum inchangés | PASS |
|
||||
| V001 | 40 resources + checksum inchangés | PASS |
|
||||
| V002 logical name | `raw_account_state` | PASS pre.002 |
|
||||
| table state | `ksp_raw_account_states` | PASS pre.002 |
|
||||
| table observation | `ksp_raw_account_observations` | PASS pre.002 |
|
||||
| PK state | `(pubkey,slot,state_hash)` | PASS pre.002 |
|
||||
| PK observation | `(observation_key)` | PASS pre.002 |
|
||||
| FK observation -> state | `(account_pubkey,account_slot,account_state_hash)` -> state composite PK | PASS pre.002 |
|
||||
| contraintes domaine/fixed-width | 26 CHECK + PK/FK, bornes API exactes | PASS pre.003 |
|
||||
| index métier | `(slot,pubkey,state_hash)` non unique / non filtré | PASS pre.003 |
|
||||
| index owner/provider/time/status | aucun prévu | PASS negative scope |
|
||||
| archive/tombstone account | aucune | PASS negative scope |
|
||||
| V002 checksum intermédiaire | `30ac8749…26311f55` sur les 5 resources pre.002 | PROVISIONAL |
|
||||
| V002 checksum final | `ff21605e…bdb550e` sur 32 resources | PASS pre.003 |
|
||||
|
||||
## 8. Idempotence et concurrence
|
||||
|
||||
| Scénario | Résultat attendu | État |
|
||||
|------------------------------------------------------------|-------------------------------------------------------|-------------------|
|
||||
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | PENDING impl/live |
|
||||
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | PENDING impl/live |
|
||||
| même pubkey+slot, state_hash différents | les deux states sont représentables | PENDING impl/live |
|
||||
| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | PENDING impl/live |
|
||||
| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | PENDING impl/live |
|
||||
| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | PENDING impl/live |
|
||||
| annulation après insertion state avant observation | aucune moitié durable | PENDING impl/live |
|
||||
|
||||
Le test de contenu divergent doit comparer les fields complets et ne jamais traiter l'égalité de `state_hash` comme preuve suffisante.
|
||||
|
||||
## 9. Threat model
|
||||
|
||||
| Menace | Mitigation figée | Preuve |
|
||||
|---------------------------------------------------|-----------------------------------------------------------------------|----------------------------|
|
||||
| URI pointe vers DB d’un autre réseau | binding `ksp_store_identity` + guard réseau avant opération | unit + live |
|
||||
| pubkey/hash/signature DB de mauvaise largeur | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows |
|
||||
| slot/lamports/rent_epoch/write_version > i64::MAX | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 |
|
||||
| data account > 16 MiB | admission API + CHECK `octet_length <= 16777216` | unit + live boundary |
|
||||
| data account vide | aucune borne basse SQL ; vide explicitement valide | unit |
|
||||
| observation sans state | FK composite + record pré-vérifie state | unit + live |
|
||||
| échec observation après state insert | une transaction PostgreSQL commune | live cancellation/rollback |
|
||||
| même ref, contenu divergent | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent |
|
||||
| même pubkey/slot, hashes distincts | PK `(pubkey,slot,state_hash)` | unit + live |
|
||||
| collision observation key | comparaison exacte de tous les champs ; conflict sinon | unit + live |
|
||||
| cursor transaction injecté dans account | magic/domain account distincts | unit cursor |
|
||||
| cursor account rejoué sur autre query | digest lie network/pubkey/range/direction/last key | unit cursor matrix |
|
||||
| ordre même slot ambigu | ordre total `(slot,pubkey,state_hash)` | unit + live pagination |
|
||||
| metadata Yellowstone promue au state | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary |
|
||||
| transaction_signature impose transaction row | aucune FK vers `ksp_raw_transactions` | schema canary |
|
||||
| SQL/server text fuit | projection PostgresBackendError statique | hardening |
|
||||
| V000/V001 modifiées | checksums figés et canaries exactes | migration tests |
|
||||
| index non justifié | un seul index métier V002 pour query unfiltered | schema completeness |
|
||||
|
||||
## 10. Audit historique kbot3
|
||||
|
||||
| Élément | Classement | Disposition |
|
||||
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
|
||||
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
|
||||
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
|
||||
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
|
||||
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
|
||||
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
|
||||
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
|
||||
| observation duplique l’état canonique | REDESSINER | observation référence un state complet séparé |
|
||||
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
|
||||
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
|
||||
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
|
||||
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
|
||||
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
|
||||
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
|
||||
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
|
||||
|
||||
Le pipeline historique `k_sol_obs_account_observations -> k_sol_core_account_states` reste une source de comparaison, pas une architecture cible. La normalisation versionnée et ses caps de batch sont hors scope `0.3.4`.
|
||||
|
||||
## 11. Canaries `0.3.3` à faire évoluer
|
||||
|
||||
À l'ouverture effective de la surface account :
|
||||
|
||||
```text
|
||||
ksp-store-lib/tests/dependency_boundary.rs
|
||||
remplacer interdiction RawAccount par exigence des 4 impl + absence types SQL
|
||||
|
||||
ksp-store-lib/tests/hardening_completeness.rs
|
||||
remplacer inventaire 6 + RawAccount fermé par inventaire exact 10
|
||||
|
||||
ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
ajouter module raw_account privé et exiger les 4 impl dans runtime bridge
|
||||
conserver V000 et health business-free
|
||||
|
||||
ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
remplacer interdiction RawAccount globale par inventaire exact 10
|
||||
remplacer l'absence V002/account par checks de surface V002 minimale
|
||||
```
|
||||
|
||||
Les tests historiques nommés `pre_XXX` peuvent conserver leurs noms si le contrat qu'ils vérifient reste valide ; seul leur contenu doit être élargi lorsqu'il fige intentionnellement le scope `0.3.3`.
|
||||
|
||||
## 12. Taxonomie d'erreur à prouver
|
||||
|
||||
| Cas | Code/projection attendue | État |
|
||||
|---------------------------------------------------|------------------------------------------------------|--------------|
|
||||
| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | PENDING impl |
|
||||
| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | PENDING impl |
|
||||
| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | PENDING impl |
|
||||
| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | PENDING impl |
|
||||
| page > limite physique PostgreSQL | PageLimitUnsupported | PENDING impl |
|
||||
| row DB incompatible/malformée | DataInvalid | PENDING impl |
|
||||
| échec SELECT | ReadFailed | PENDING impl |
|
||||
| échec INSERT/transaction | WriteFailed | PENDING impl |
|
||||
| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | PENDING impl |
|
||||
|
||||
Aucune nouvelle variante backend n'est justifiée au gate `pre.001`.
|
||||
|
||||
## 13. Preuve live finale
|
||||
|
||||
Le test live account devra être opt-in/ignored, URI stdin, sans environnement ni fuite de secret. Il doit couvrir V002 et rejouer un canari transaction dans la même base afin de prouver la coexistence cross-family.
|
||||
|
||||
| Scénario live | Obligation |
|
||||
|-----------------------------------------|-------------|
|
||||
| schema update V001 -> V002 | obligatoire |
|
||||
| reopen idempotent | obligatoire |
|
||||
| u64 > i64::MAX / u64::MAX | obligatoire |
|
||||
| data empty / 16 MiB | obligatoire |
|
||||
| identical/divergent acquisition | obligatoire |
|
||||
| same pubkey+slot / distinct hash | obligatoire |
|
||||
| additional observation + missing ref | obligatoire |
|
||||
| collision observation | obligatoire |
|
||||
| keyset filtered/unfiltered ASC/DESC | obligatoire |
|
||||
| cursor hostile/cross-query/cross-family | obligatoire |
|
||||
| concurrency | obligatoire |
|
||||
| cancellation rollback | obligatoire |
|
||||
| RawTransaction regression canary | obligatoire |
|
||||
| cleanup isolation | obligatoire |
|
||||
|
||||
## 14. Sizing
|
||||
|
||||
| Tranche | Objet | État |
|
||||
|---------|-------------------------------------------------------------------|---------|
|
||||
| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | DONE |
|
||||
| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | PASS |
|
||||
| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | PASS |
|
||||
| pre.004 | mapping privé state/observation + get reads + hostile rows | PASS |
|
||||
| pre.005 | acquisition atomique state+observation + idempotence/conflict | PASS |
|
||||
| pre.006 | observation supplémentaire + races/cancellation unitaires | PASS |
|
||||
| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | PASS |
|
||||
| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | PASS |
|
||||
| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | CURRENT |
|
||||
| pre.010 | hardening/completeness cross-family + canaries ownership | PLANNED |
|
||||
| pre.011 | gate technique final + replay live ciblé + graphes | PLANNED |
|
||||
| pre.012 | réconciliation docs finale sans rouvrir le code | PLANNED |
|
||||
| pre.013 | publication prep minimale + prompt 0.3.5 | PLANNED |
|
||||
| rel.001 | stabilisation/tag après gate opérateur | PLANNED |
|
||||
|
||||
## 15. Negative scope final
|
||||
|
||||
| Surface | Doit rester absent en 0.3.4 |
|
||||
|--------------------------------------------|-----------------------------|
|
||||
| ksp-store-api redesign | oui |
|
||||
| account retention/archive/purge | oui |
|
||||
| CORE/STRUCTURAL persistence | oui |
|
||||
| workers/jobs/backfills | oui |
|
||||
| Store batch/priorité policy | oui |
|
||||
| owner/provider/time/status queries/indexes | oui |
|
||||
| Transport -> Store coupling | oui |
|
||||
| PostgreSQL types via ksp-store-lib | oui |
|
||||
| nouveau backend | oui |
|
||||
|
||||
## 16. Gate opérateur prévu pour clôture technique
|
||||
|
||||
```text
|
||||
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.4
|
||||
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
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
Les preuves live restent séparées et explicitement `--ignored --nocapture`.
|
||||
|
||||
## 17. Verdict `pre.002`
|
||||
|
||||
Fondation V002 minimale : **PASS statique**. Le registry logique V002 expose exactement cinq resources et aucune surface repository/runtime account.
|
||||
|
||||
Gate opérateur du 2026-08-30 : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, `ksp-store-api`, `ksp-store-lib`, `ksp-config-lib` et `ksp-store-lib --no-default-features` passent. `cargo test -p ksp-store-postgres-lib` exécute 43 tests unitaires et échoue uniquement sur deux canaris obsolètes :
|
||||
|
||||
- `pre_003_fix_001_newer_history_is_rejected_without_down_migration` attendait `SchemaNewer` avec une fausse version future `2`, désormais occupée par V002 ;
|
||||
- `pre_002_v002_resources_are_exactly_two_tables_plus_base_pk_fk_without_indexes_or_domain_checks` comparait la FK à une chaîne sensible au saut de ligne avant `REFERENCES`.
|
||||
|
||||
Ces deux défauts appartiennent strictement au couloir `pre.002` et sont corrigés par `pre.002-fix.001`. Ils ne remettent en cause ni les cinq resources V002 ni leur checksum.
|
||||
|
||||
Contraintes complètes/index/schema compatibility/checksum final : **PASS `pre.003`**. Mapping/lectures account : **PENDING `pre.004`** à ce verdict historique ; writes/pagination/capabilities restent `pre.005+`.
|
||||
|
||||
## 18. Correctif `pre.002-fix.001`
|
||||
|
||||
Le correctif applique deux recalibrages de tests seulement :
|
||||
|
||||
1. le canari d'historique construit désormais le préfixe valide V000/V001/V002 puis ajoute une version `3` inconnue, ce qui teste réellement le cas « schéma plus récent » ;
|
||||
2. le canari de fondation V002 normalise les espaces ASCII du SQL embedded avant ses assertions sémantiques, sans relâcher l'inventaire exact des cinq resources ni les interdictions `CREATE INDEX`, `CHECK`, cascade, `BIGSERIAL`, `BIGINT` ou `TEXT` pour les données account.
|
||||
|
||||
Invariants du correctif :
|
||||
|
||||
- aucune ressource sous `migrations/v002_raw_account_state/` modifiée ;
|
||||
- checksum V002 intermédiaire inchangé : `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55` ;
|
||||
- V000/V001 inchangées ;
|
||||
- aucun module repository account, dispatch Store ou implémentation `RawAccount*` ouvert ;
|
||||
- `workspace.package.version = 0.3.4-pre.2.fix.1` conformément à `VER-ID-007/010`, car des sources Rust de tests sont modifiées.
|
||||
|
||||
Gate opérateur du correctif du 2026-08-30 : **PASS complet** — audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts.
|
||||
|
||||
## 19. Verdict `pre.003`
|
||||
|
||||
V002 finale : **PASS fonctionnel opérateur ; fix de conformité Rust requis**.
|
||||
|
||||
Inventaire final :
|
||||
|
||||
```text
|
||||
V002 resources = 32
|
||||
tables = 2
|
||||
constraints = 29 (2 PK + 1 FK + 26 CHECK)
|
||||
indexes = 1
|
||||
final checksum = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
provisional pre.002= 30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
|
||||
```
|
||||
|
||||
Contraintes matérialisées : exactitude fixed-width des identités/hash/signature, domaines `u64` en `NUMERIC(20,0)`, data account `<= 16 MiB`, provenance logique 1..128 octets/alphabet sûr, origin fermé aux cinq variantes API, timestamps `0..MAX_RAW_UNIX_MILLIS`, `observed_at <= received_at`, hash source 32 bytes et source size `<= 64 MiB`. `is_startup` reste un booléen nullable sans contrainte artificielle.
|
||||
|
||||
L'unique index métier V002 est non unique et non filtré :
|
||||
|
||||
```text
|
||||
(slot, pubkey, state_hash)
|
||||
```
|
||||
|
||||
Aucun index owner/provider/method/time/status n'est ajouté.
|
||||
|
||||
Compatibilité de prerelease : le checksum provisoire V002 de `pre.002` n'est accepté que lorsque `schema_update=update_if_needed`. Dans ce cas, les resources manquantes sont matérialisées et vérifiées, la compatibilité externe V002 est contrôlée, puis l'entrée d'historique est mise à jour atomiquement vers le checksum final. Avec `schema_update=disabled`, le checksum provisoire est rejeté ; tout checksum non explicitement connu reste `MigrationMismatch`.
|
||||
|
||||
Le probe d'adoption couvre V001 + V002. V000/V001 et leurs checksums restent inchangés. Aucune surface repository/runtime account n'est ouverte : `src/raw_account.rs` absent, aucune implémentation `RawAccount*` et aucun dispatch Store.
|
||||
|
||||
Gate opérateur de `pre.003` du 2026-08-30 : `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (45 tests), Config et `--no-default-features` sont **PASS**. L'audit Markdown est **PASS**. L'unique défaut est `RUST-FMT-104` dans `src/migration.rs` : le bloc `const` n'est pas intégralement trié alphabétiquement.
|
||||
|
||||
## 20. Correctif `pre.003-fix.001`
|
||||
|
||||
Le correctif est strictement hygiénique : `HISTORY_LOAD_SQL` est déplacée avant `HISTORY_UPDATE_CHECKSUM_SQL`. Les valeurs, types et usages des constantes restent identiques.
|
||||
|
||||
Invariants du fix :
|
||||
|
||||
- aucune ressource V002 modifiée ;
|
||||
- checksum V002 final inchangé : `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e` ;
|
||||
- checksum provisoire `pre.002` et bridge de migration inchangés ;
|
||||
- aucune modification de schéma, repository, dispatch ou capability ;
|
||||
- `workspace.package.version = 0.3.4-pre.3.fix.1` conformément à `VER-ID-007/010`.
|
||||
|
||||
Gate opérateur de `pre.003-fix.001` du 2026-08-30 : **PASS complet** — audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (45 tests), Config et `--no-default-features` sont verts.
|
||||
|
||||
## 21. Verdict `pre.004`
|
||||
|
||||
Mapping PostgreSQL privé account : **PASS final via `pre.004-fix.001`**. Le gate initial `pre.004` avait échoué uniquement sur Clippy ; le gate complet du correctif est désormais vert.
|
||||
|
||||
Surface matérialisée :
|
||||
|
||||
```text
|
||||
src/raw_account.rs privé
|
||||
PostgresBackend::get_raw_account_state public bridge backend
|
||||
PostgresBackend::get_raw_account_observation public bridge backend
|
||||
RawAccount* trait impl 0/4 — volontairement fermé
|
||||
write SQL account absent
|
||||
pagination/cursor account absent
|
||||
migrations V000/V001/V002 inchangées
|
||||
```
|
||||
|
||||
Le mapping state couvre `slot`, `lamports` et `rent_epoch` sur tout le domaine `u64` via les projections `::text`, les largeurs exactes 32 bytes de pubkey/owner/state hash, le booléen `executable` et les bytes account complets. `RawAccountState::try_new` fournit la dernière admission backend-agnostic et transforme un payload > 16 MiB en `DataInvalid`; les comptes à data vide restent valides.
|
||||
|
||||
Le mapping observation reconstruit l'identité account, toute la provenance commune et les metadata optionnelles `is_startup`, `transaction_signature` et `write_version`. `write_version` conserve tout le domaine `u64` via `NUMERIC(20,0)::text`; une signature présente doit faire exactement 64 bytes. L'absence de metadata optionnelle reste `None`, sans valeur inventée.
|
||||
|
||||
Hostile-row guards déterministes couvrent notamment : largeur pubkey/hash/signature invalide, décimal négatif ou > `u64::MAX`, provenance/origin invalide, ordre temporel impossible et data oversized. Les erreurs restent `DataInvalid`/`ReadFailed`/`WrongNetwork` avec phases statiques et sans écho de valeur hostile.
|
||||
|
||||
Le garde réseau de `get_raw_account_state` est exécuté avant acquisition d'une connexion. `get_raw_account_observation` reçoit seulement un `RawObservationKey`; la référence reconstruite utilise donc le réseau mono-backend déjà validé à l'ouverture, comme prévu par le contrat.
|
||||
|
||||
Les canaris de scope prouvent encore l'absence de :
|
||||
|
||||
```text
|
||||
INSERT / UPDATE / DELETE account
|
||||
ON CONFLICT / FOR UPDATE account
|
||||
list_raw_account_states / cursor KSPA
|
||||
impl RawAccountStateRead/Write for PostgresBackend
|
||||
impl RawAccountObservationRead/Write for PostgresBackend
|
||||
Store dispatch account
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.004` du 2026-08-30 établit : `cargo check --workspace` PASS, suites `ksp-store-api`/`ksp-store-lib`/`ksp-store-postgres-lib`/`ksp-config-lib` PASS, `ksp-store-lib --no-default-features` PASS et audits Rust/Markdown PASS ; `cargo clippy --workspace --all-targets` FAIL uniquement sur les 27 usages de `?` du nouveau mapping, doublés par 27 retours implicites de closures, avec un warning séparé de variable `crate_root` inutilisée dans `dependency_boundary.rs`.
|
||||
|
||||
### 21.1. Correctif `pre.004-fix.001`
|
||||
|
||||
Le correctif remplace exclusivement les `map_err(...)?` de `raw_account_observation_db_row` et `raw_account_state_db_row` par des `match` typés avec retours explicites `DataInvalid`, conformément à `RUST-ERR-002/003`. Il retire également la déclaration `crate_root` inutilisée du canari `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
|
||||
|
||||
Invariants du fix :
|
||||
|
||||
- aucun SQL ou bind account modifié ;
|
||||
- aucun invariant de mapping ou phase d'erreur modifié ;
|
||||
- aucune migration V000/V001/V002 modifiée ;
|
||||
- checksum V002 final inchangé : `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e` ;
|
||||
- aucun write, cursor, trait account ou dispatch Store ouvert ;
|
||||
- `workspace.package.version = 0.3.4-pre.4.fix.1`.
|
||||
|
||||
Validations exécutées dans l'environnement d'assemblage du fix : audit Rust général/export/workspace **PASS**, audit Markdown **PASS**, absence de `?` dans `src/raw_account.rs` confirmée. Gate opérateur du 2026-08-30 : **PASS complet** — audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (52 tests), Config et `--no-default-features` sont verts.
|
||||
|
||||
## 22. Verdict `pre.005`
|
||||
|
||||
Acquisition atomique account : **PASS complet opérateur**.
|
||||
|
||||
Surface ajoutée :
|
||||
|
||||
```text
|
||||
PostgresBackend::persist_raw_account_acquisition
|
||||
INSERT state ON CONFLICT DO NOTHING
|
||||
SELECT state FOR UPDATE sur collision
|
||||
comparaison exacte du state canonique complet
|
||||
INSERT observation ON CONFLICT DO NOTHING
|
||||
SELECT observation FOR UPDATE sur collision
|
||||
comparaison exacte référence + provenance + metadata optionnelles
|
||||
COMMIT unique state + observation
|
||||
```
|
||||
|
||||
Les préconditions `state.reference.network == backend.network`, `observation.account.network == backend.network` et `observation.account == state.reference` sont évaluées avant `pool.get()`. Une divergence de référence ou de contenu est classée `Conflict`; un échec physique d'écriture reste `WriteFailed`, sans texte PostgreSQL conservé.
|
||||
|
||||
L'idempotence state ne fait aucune confiance exclusive à `state_hash` : un même `(pubkey, slot, state_hash)` n'est `AlreadyPresent` que si `lamports`, `owner`, `executable`, `rent_epoch` et `data` sont identiques. L'idempotence observation compare le modèle `RawAccountObservation` complet, y compris `is_startup`, `transaction_signature`, `write_version` et toute la provenance.
|
||||
|
||||
La transaction commune garantit qu'une collision divergente sur l'observation après insertion d'un nouveau state ne peut pas laisser ce state durable isolément. Aucun `has_*`, `ON CONFLICT DO UPDATE`, `UPDATE` ou `DELETE` account n'est introduit.
|
||||
|
||||
Canaries `pre.005` : guard réseau/référence, comparaison state complète, comparaison observation complète, encodage origin fermé, SQL `DO NOTHING + FOR UPDATE`, bridge backend-agnostic et absence des quatre implémentations `RawAccount*`.
|
||||
|
||||
Le gate opérateur du 2026-08-30 est **PASS complet** : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (56 tests unitaires), Config et `ksp-store-lib --no-default-features` sont verts.
|
||||
|
||||
Reste après `pre.005` : observation supplémentaire/concurrence ciblée (`pre.006`), pagination/cursor (`pre.007`) et implémentations/dispatch 10/10 (`pre.008`). Les migrations V000/V001/V002 restent inchangées et le checksum V002 final demeure `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
|
||||
|
||||
## 23. Verdict `pre.006`
|
||||
|
||||
Observation supplémentaire account : **PASS complet opérateur**.
|
||||
|
||||
Surface ajoutée :
|
||||
|
||||
```text
|
||||
PostgresBackend::record_raw_account_observation
|
||||
network guard avant pool.get()
|
||||
SELECT state reference FOR KEY SHARE
|
||||
state absent -> ReferenceNotFound
|
||||
INSERT observation ON CONFLICT DO NOTHING
|
||||
collision -> SELECT observation FOR UPDATE + comparaison exacte
|
||||
COMMIT unique
|
||||
```
|
||||
|
||||
Le state référencé doit être déjà durable : `record_raw_account_observation` ne crée jamais de `RawAccountState` et ne retransmet pas ses bytes. Le lock `FOR KEY SHARE` protège la référence pendant l'insertion observation/FK sans prendre un lock de contenu plus fort que nécessaire.
|
||||
|
||||
L'idempotence et le conflit réutilisent le chemin `persist_account_observation_row` déjà validé en `pre.005`. Une même clé avec observation identique produit `AlreadyPresent`; une même clé avec contenu/provenance/metadata divergents produit `Conflict`. Le réseau de l'observation est rejeté avant toute acquisition de connexion lorsqu'il diffère du backend.
|
||||
|
||||
La preuve de race/cancellation de cette tranche est volontairement unitaire/structurelle : les canaris imposent l'ordre `transaction -> FOR KEY SHARE -> persist/compare -> commit`, l'absence de commit anticipé et l'absence de tâche détachée ou de fuite volontaire du handle transactionnel. Une cancellation avant commit droppe donc le `Transaction` local et laisse le rollback à PostgreSQL. Les scénarios de vraies tâches concurrentes et `JoinHandle::abort()` restent au test live `pre.009`.
|
||||
|
||||
Restent fermés : `list_raw_account_states`, cursor `KSPA`, `OFFSET`, `UPDATE`/`DELETE`, `ON CONFLICT DO UPDATE`, les quatre implémentations `RawAccount*` sur `PostgresBackend` et tout dispatch account dans `Store`. Les migrations V000/V001/V002 sont inchangées ; le checksum V002 final reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
|
||||
## 24. Verdict `pre.007`
|
||||
|
||||
Pagination `RawAccountStateQuery` et cursor `KSPA` : **PASS complet opérateur via `pre.007-fix.001`**. Le gate initial avait isolé cinq warnings Clippy `clone_on_copy` et deux canaris historiques stales ; le fix les corrige sans toucher au code de pagination.
|
||||
|
||||
Ordre physique figé :
|
||||
|
||||
```text
|
||||
ASC (slot ASC, pubkey ASC, state_hash ASC)
|
||||
DESC (slot DESC, pubkey DESC, state_hash DESC)
|
||||
```
|
||||
|
||||
Deux familles de SELECT sont utilisées pour chaque direction : sans filtre pubkey, l'index V002 `(slot, pubkey, state_hash)` porte la navigation ; avec filtre pubkey, la PK `(pubkey, slot, state_hash)` porte le préfixe account puis le même ordre total. Les predicates de continuation utilisent exclusivement les comparaisons tuple `>` / `<`. Aucun `OFFSET`, batch maximum, priorité, clamp ou policy worker n'est introduit.
|
||||
|
||||
Le cursor account V1 est fixe à 109 bytes :
|
||||
|
||||
```text
|
||||
0..4 magic KSPA
|
||||
4 version 1
|
||||
5..13 last_slot u64 big-endian
|
||||
13..45 last_pubkey 32 bytes
|
||||
45..77 last_state_hash 32 bytes
|
||||
77..109 SHA-256 binding digest
|
||||
```
|
||||
|
||||
Le digest utilise le domaine `KSP/raw-account-state-cursor/v1` et lie le réseau, le filtre pubkey optionnel, la direction, les bornes de slot et la dernière clé complète. Les canaris couvrent round-trip, taille/magic/version/digest hostiles, replay réseau/pubkey/direction/plage et replay cross-family depuis `KSPT`.
|
||||
|
||||
La page conserve la sémantique Store existante : `LIMIT + 1` détecte la continuation, le backend n'impose aucun maximum fonctionnel KSP et expose seulement la borne PostgreSQL `9_223_372_036_854_775_806` nécessaire pour convertir `requested + 1` en `i64`. Les rows de liste revalident pubkey/hash à 32 bytes et `slot` via decimal exact `u64`, sans narrowing.
|
||||
|
||||
Surface runtime ajoutée :
|
||||
|
||||
```text
|
||||
PostgresBackend::list_raw_account_states(
|
||||
&RawAccountStateQuery,
|
||||
) -> RawPage<RawAccountStateReference>
|
||||
```
|
||||
|
||||
Les quatre `impl RawAccount* for PostgresBackend` et tout dispatch account dans `ksp-store-lib::Store` restent absents jusqu'à `pre.008`. Les migrations V000/V001/V002 restent byte-inchangées et le checksum V002 demeure `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
|
||||
|
||||
## 25. Verdict `pre.007-fix.001`
|
||||
|
||||
Le fix ne modifie ni `src/raw_account.rs`, ni `src/raw_account/cursor.rs`, ni le SQL runtime, ni aucune migration. Les cinq `Pubkey::clone()` inutiles des tests cursor sont supprimés. Les canaris `pre.005/pre.006` sont transformés pour accepter `list_raw_account_states`, devenu scope légitime en `pre.007`, tout en continuant d'interdire les surfaces destructives (`UPDATE`, `DELETE`, `ON CONFLICT DO UPDATE`, `OFFSET`) et les quatre implémentations `RawAccount*` réservées à `pre.008`.
|
||||
|
||||
Le checksum V002 reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. Le gate opérateur complet du 2026-08-30 est PASS : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (63 tests unitaires), Config et `--no-default-features` sont verts.
|
||||
|
||||
## 26. Verdict `pre.008`
|
||||
|
||||
Conformance RAW backend/façade : **10/10 matérialisée ; gate initial en échec ciblé sur le canari `dependency_boundary`**.
|
||||
|
||||
Inventaire exact attendu et désormais canarisé :
|
||||
|
||||
```text
|
||||
PostgresBackend
|
||||
RawTransactionRead
|
||||
RawTransactionWrite
|
||||
RawTransactionObservationRead
|
||||
RawTransactionObservationWrite
|
||||
RawTransactionRetentionRead
|
||||
RawTransactionRetentionWrite
|
||||
RawAccountStateRead
|
||||
RawAccountStateWrite
|
||||
RawAccountObservationRead
|
||||
RawAccountObservationWrite
|
||||
|
||||
Store
|
||||
même inventaire exact de 10 capabilities
|
||||
```
|
||||
|
||||
Les quatre implémentations backend ne créent aucune nouvelle primitive physique : elles adaptent les méthodes étroites `get_raw_account_state`, `list_raw_account_states`, `persist_raw_account_acquisition`, `get_raw_account_observation` et `record_raw_account_observation` déjà validées. Les quatre implémentations de `Store` délèguent uniquement au backend compilé, appliquent `validate_operation_network` aux inputs réseau-scopés et réutilisent `map_postgres_error`.
|
||||
|
||||
La lecture `RawAccountObservationRead` reste la seule capability account sans guard réseau pré-dispatch parce que `RawObservationKey` ne porte aucun réseau ; le backend mono-network reconstruit la référence avec son binding déjà validé. L'acquisition state+observation vérifie séparément les deux réseaux dans la façade puis conserve le guard d'égalité de référence backend avant `pool.get()`.
|
||||
|
||||
Canaris transformés :
|
||||
|
||||
```text
|
||||
inventaire exact PostgresBackend = 10
|
||||
inventaire exact Store = 10
|
||||
impl account uniquement dans runtime bridge backend
|
||||
aucun impl account direct dans raw_account.rs
|
||||
aucun SQL/type PostgreSQL dans ksp-store-lib
|
||||
validate_operation_network occurrences Store = 14
|
||||
feature postgres optionnelle inchangée
|
||||
no-default-features doit continuer à compiler
|
||||
```
|
||||
|
||||
Aucune migration V000/V001/V002 n'est modifiée ; le checksum V002 reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. `ksp-store-api` reste inchangée. La preuve réelle PostgreSQL de coexistence, concurrence et cancellation account reste en `pre.009`.
|
||||
|
||||
## 27. Verdict `pre.008-fix.001`
|
||||
|
||||
Le gate opérateur de `pre.008` confirme que les audits Rust/Markdown, le workspace check, `ksp-store-api`, `ksp-store-lib`, `ksp-config-lib` et `--no-default-features` passent. L'unique blocage est la compilation de `ksp-store-postgres-lib/tests/dependency_boundary.rs` : `runtime` est référencé dans le canari d'ownership `pre.005` sans y être déclaré, tandis qu'une déclaration devenue inutilisée subsiste dans le canari V002 `pre.003`.
|
||||
|
||||
Le fix déplace uniquement :
|
||||
|
||||
```text
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
```
|
||||
|
||||
vers le canari qui le consomme effectivement. Les assertions de placement 10/10 restent inchangées : les implémentations account demeurent uniques dans `runtime.rs`, absentes de `raw_account.rs`, et les couches migration/schema restent sans capability métier. Aucun fichier de production, SQL runtime ou migration V000/V001/V002 n'est modifié.
|
||||
|
||||
Le checksum V002 reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. Le gate opérateur complet du 2026-08-30 sur `0.3.4-pre.8.fix.1` est **PASS** : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (63 tests unitaires + 13 dependency + 7 hardening + 13 public), Config et `--no-default-features` sont verts.
|
||||
|
||||
## 28. Verdict `pre.009`
|
||||
|
||||
Preuve PostgreSQL live `RawAccountState` : **PASS complet opérateur**.
|
||||
|
||||
Nouveau test :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
|
||||
#[ignore = "opt-in real PostgreSQL RawAccountState proof; reads one dedicated URI from stdin"]
|
||||
```
|
||||
|
||||
Invariants d'exploitation :
|
||||
|
||||
```text
|
||||
URI dédiée lue depuis stdin uniquement
|
||||
aucune lecture d'environnement
|
||||
aucun affichage de l'URI
|
||||
refus si une table KSP managée existe déjà
|
||||
PostgreSQL >= 15
|
||||
cleanup uniquement après preuve d'absence initiale
|
||||
reopen health migration_version = 2
|
||||
```
|
||||
|
||||
Matrice matérialisée :
|
||||
|
||||
| Scénario | Preuve `pre.009` |
|
||||
|--------------------------------------------|--------------------------------------|
|
||||
| bootstrap/reopen V002 | oui |
|
||||
| schema_autoupdate index account | oui |
|
||||
| slot/lamports/rent_epoch/write_version u64 | oui, `u64::MAX` |
|
||||
| data account vide | oui |
|
||||
| data account 16 MiB exact | oui |
|
||||
| acquisition insert/idempotence | oui |
|
||||
| même référence / contenu divergent | `Conflict` |
|
||||
| même pubkey+slot / hashes distincts | oui |
|
||||
| collision observation + rollback state | oui |
|
||||
| observation supplémentaire identique | `AlreadyPresent` |
|
||||
| observation supplémentaire divergente | `Conflict` |
|
||||
| référence state absente | `ReferenceNotFound` |
|
||||
| metadata Yellowstone | round-trip exact |
|
||||
| pagination ASC/DESC | oui |
|
||||
| filtre pubkey | oui |
|
||||
| cursor cross-query / digest hostile | `QueryInvalid` |
|
||||
| cursor cross-family `KSPT` | `QueryInvalid` |
|
||||
| concurrence identique | Inserted + AlreadyPresent |
|
||||
| concurrence divergente | Inserted + Conflict |
|
||||
| cancellation réelle | `JoinHandle::abort()` + state absent |
|
||||
| coexistence RawTransaction | oui |
|
||||
| cleanup isolation | oui |
|
||||
|
||||
Le test `postgres_raw_transaction_live.rs` est ajusté uniquement sur son inventaire de schéma managé : il probe et supprime désormais aussi `ksp_raw_account_states` et `ksp_raw_account_observations`, nécessaires depuis V002. Aucun scénario métier RawTransaction n'est modifié.
|
||||
|
||||
Le gate standard du 2026-08-30 est PASS : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts. Le live account est ensuite PASS sur PostgreSQL 17 en 2,72 s, avec URI dédiée lue depuis stdin sans écho.
|
||||
|
||||
Les migrations V000/V001/V002 restent byte-inchangées ; le checksum V002 reste :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
## 29. Verdict `pre.010`
|
||||
|
||||
Hardening et complétude cross-family : **PASS opérateur complet**.
|
||||
|
||||
Aucun fichier de production n'est modifié. Les nouveaux canaris backend vérifient que le SQL account reste limité aux lectures, inserts idempotents, locks et pagination keyset ; ils interdisent `UPDATE`, `DELETE`, `TRUNCATE`, `OFFSET`, `ON CONFLICT DO UPDATE` et toute policy worker. Le cursor account reste `KSPA`, domaine `KSP/raw-account-state-cursor/v1`, 109 bytes, distinct de `KSPT`.
|
||||
|
||||
Le registre V002 est figé à exactement 32 resources : 2 tables, 2 PK, 1 FK observation->state, 26 CHECK et 1 index `(slot,pubkey,state_hash)` non filtré. Les canaris imposent l'absence de `ksp_raw_transactions` et `ksp_raw_transaction_observations` dans V002. Le CHECK de `transaction_signature` impose seulement 64 bytes lorsqu'elle est présente et aucune FK transaction.
|
||||
|
||||
La façade compare directement son inventaire de traits RAW avec celui de `PostgresBackend` : les deux ensembles doivent être strictement identiques et contenir exactement dix capabilities. Aucune capability account de retention, delete ou compaction n'est admise.
|
||||
|
||||
Les fichiers `ksp-store-api`, `ksp-store-lib/src`, `ksp-store-postgres-lib/src`, les tests live et les migrations V000/V001/V002 restent inchangés. Le checksum V002 demeure :
|
||||
|
||||
```text
|
||||
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
|
||||
```
|
||||
|
||||
Le gate opérateur du 2026-08-30 est PASS : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts. Les canaris supplémentaires de `pre.010` passent : inventaire RAW exact 10/10 des deux côtés, V002 exact à 32 resources, SQL account non destructif/keyset-only et séparation cross-family maintenue.
|
||||
|
||||
## 30. Verdict `pre.011`
|
||||
|
||||
Gate technique final : **PASS**.
|
||||
|
||||
Cette tranche ne modifie aucun code de production, test fonctionnel, SQL, migration, Config runtime, manifest de crate, dépendance runtime, README/USAGE, CHANGELOG/ROADMAP ou prompt suivant. Elle synchronise uniquement la version workspace et documente le gate final.
|
||||
|
||||
Matrice opérateur attendue :
|
||||
|
||||
```text
|
||||
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.4
|
||||
|
||||
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-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
|
||||
cargo test --workspace
|
||||
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
Replay PostgreSQL réel ciblé :
|
||||
|
||||
```bash
|
||||
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
|
||||
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture --test-threads=1
|
||||
unset KSP_PG_TEST_URI
|
||||
```
|
||||
|
||||
Le live account est la preuve finale ciblée : il couvre bootstrap/reopen V002, bornes u64/data, idempotence/conflits, pagination/cursors, concurrence/cancellation et coexistence `RawTransaction`. Les lives foundation et RawTransaction séparés ne sont pas requis par `pre.011`.
|
||||
|
||||
Le replay opérateur de clôture confirme :
|
||||
|
||||
```text
|
||||
cargo test -p ksp-store-lib --no-default-features PASS
|
||||
cargo test --workspace PASS
|
||||
cargo tree façade/backend/features/duplicates exécutés
|
||||
postgres_raw_account_live PASS — PostgreSQL 17, 3.00 s
|
||||
```
|
||||
|
||||
Les graphes ne révèlent pas de reverse edge Store ni de dépendance PostgreSQL lorsque la feature façade correspondante est désactivée. Les doublons Cargo observés appartiennent au workspace transverse et ne matérialisent aucune duplication propre à la verticale Store nécessitant un correctif. Le live final lit l'URI dédiée depuis stdin sans l'écho.
|
||||
|
||||
Le couloir technique est donc fermé ; aucun `pre.011-fix.NNN` n'est requis.
|
||||
|
||||
## 31. Verdict `pre.012`
|
||||
|
||||
Réconciliation documentaire finale : **matérialisée ; gate documentaire opérateur à exécuter**.
|
||||
|
||||
Documents durables réconciliés :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/README.md
|
||||
crates/ksp-store-lib/USAGE.md
|
||||
crates/ksp-store-postgres-lib/README.md
|
||||
crates/ksp-store-postgres-lib/USAGE.md
|
||||
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
|
||||
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
|
||||
```
|
||||
|
||||
La documentation décrit maintenant la surface réellement validée : dix capabilities RAW exactes, quatre capabilities `RawAccount*`, acquisition account atomique/idempotente, observation supplémentaire reference-guarded, pagination keyset `(slot,pubkey,state_hash)` avec cursor `KSPA`, séparation stricte avec `RawTransaction` et absence de rétention/destruction account.
|
||||
|
||||
Relus et volontairement inchangés :
|
||||
|
||||
```text
|
||||
README.md racine
|
||||
docs/architecture/003-COMPONENT_CONTRACTS.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||
ksp-config-lib README/USAGE
|
||||
ksp-store-api — aucun README/USAGE à créer artificiellement
|
||||
```
|
||||
|
||||
`CHANGELOG.md`, `ROADMAP.md` et le prompt de démarrage `0.3.5` restent hors de cette tranche et appartiennent à `pre.013`. Aucun `src/**`, test, Config, schema, migration ou dépendance n'est modifié.
|
||||
|
||||
1109
prompts/024-V0_3_5_START_PROMPT.md
Normal file
1109
prompts/024-V0_3_5_START_PROMPT.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user