48 Commits

Author SHA1 Message Date
c7cfb9557d v0.3.4-rel.001 2026-08-31 00:52:56 +02:00
c27428d6c0 v0.3.4-pre.013 2026-08-31 00:46:52 +02:00
dfa01b1381 v0.3.4-pre.012 2026-08-31 00:33:51 +02:00
5fc7f65b2c v0.3.4-pre.011 2026-08-30 23:56:11 +02:00
bbf804b7fc v0.3.4-pre.010 2026-08-30 23:46:37 +02:00
13687ec2fe v0.3.4-pre.009 2026-08-30 23:39:43 +02:00
b756da5149 v0.3.4-pre.008-fix.001 2026-08-30 23:25:12 +02:00
04234926ce v0.3.4-pre.008 2026-08-30 23:15:48 +02:00
bfc69f5631 v0.3.4-pre.007-fix.001 2026-08-30 22:54:53 +02:00
902b5fba99 v0.3.4-pre.007 2026-08-30 22:47:33 +02:00
fccb7d876c v0.3.4-pre.006 2026-08-30 22:31:02 +02:00
50d4142797 v0.3.4-pre.005 2026-08-30 22:17:47 +02:00
eb6dbc31e8 v0.3.4-pre.004-fix.001 2026-08-30 22:06:24 +02:00
fb8b585aa2 v0.3.4-pre.004 2026-08-30 21:57:23 +02:00
dad18c16d5 v0.3.4-pre.003-fix.001 2026-08-30 21:45:30 +02:00
a704a5722e v0.3.4-pre.003 2026-08-30 21:37:02 +02:00
dfa3723a7f v0.3.4-pre.002-fix.001 2026-08-30 21:17:38 +02:00
6bd593f467 v0.3.4-pre.002 2026-08-30 21:11:23 +02:00
03ad0ba063 v0.3.4-pre.001-fix.001 2026-08-30 19:29:05 +02:00
d95b1095e5 v0.3.4-pre.001 2026-08-30 19:21:15 +02:00
bcce89f076 v0.3.3-rel.001 2026-08-30 18:26:36 +02:00
66d5b03495 v0.3.3-pre.013 2026-08-30 18:03:06 +02:00
7af751c888 v0.3.3-pre.012-fix.001 2026-08-30 17:46:49 +02:00
9f2d5ea704 v0.3.3-pre.012 2026-08-30 17:02:44 +02:00
8428b75b96 v0.3.3-pre.011 2026-08-30 16:37:28 +02:00
282b616a0e v0.3.3-pre.010-fix.001 2026-08-30 16:07:51 +02:00
12eaf7a3de v0.3.3-pre.010 2026-08-30 16:06:20 +02:00
f28e4d87f0 v0.3.3-pre.009-fix.006 2026-08-30 15:53:07 +02:00
ba2e52f841 v0.3.3-pre.009-fix.005 2026-08-30 15:46:03 +02:00
5f1938c7c3 v0.3.3-pre.009-fix.004 2026-08-30 15:05:58 +02:00
5e2f959c01 v0.3.3-pre.009-fix.003 2026-08-30 14:56:51 +02:00
8da04717b6 v0.3.3-pre.009-fix.002 2026-08-30 14:48:41 +02:00
c094828704 v0.3.3-pre.009-fix.001 2026-08-30 14:43:00 +02:00
ce658e58f7 v0.3.3-pre.009 2026-08-30 14:29:33 +02:00
7f0a642972 v0.3.3-pre.008-fix.001 2026-08-30 14:10:43 +02:00
84ab2b3651 v0.3.3-pre.008 2026-08-30 14:10:10 +02:00
1f0b202135 v0.3.3-pre.007-fix.001 2026-08-30 13:43:58 +02:00
aa56a12846 v0.3.3-pre.007 2026-08-30 13:37:22 +02:00
10996f11f7 v0.3.3-pre.006-fix.001 2026-08-30 13:13:01 +02:00
1352a61d70 v0.3.3-pre.006 2026-08-30 13:06:33 +02:00
66a3926adb v0.3.3-pre.005 2026-08-30 12:42:50 +02:00
917e602a87 v0.3.3-pre.004 2026-08-30 11:00:07 +02:00
56fadb364a v0.3.3-pre.003-fix.003 2026-08-30 10:36:22 +02:00
8fa1c8ce8d v0.3.3-pre.003-fix.002 2026-08-30 10:26:38 +02:00
c17e78c6a8 v0.3.3-pre.003-fix.001 2026-08-30 10:21:48 +02:00
61bf7ba468 v0.3.3-pre.003 2026-08-30 09:22:53 +02:00
9712c7e1f7 v0.3.3-pre.002 2026-08-30 08:41:26 +02:00
a560df80ce v0.3.3-pre.001 2026-08-30 07:59:00 +02:00
172 changed files with 27158 additions and 495 deletions

View File

@@ -1,8 +1,32 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 20 --> <!-- version: 23 -->
# Changelog KSP # 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.
La persistence canonique utilise une transaction PostgreSQL unique pour `RawTransaction + observation`, tente l'insert sous contrainte d'unicité puis compare le contenu réel sous verrou en cas de collision : contenu identique -> idempotence, divergence -> `store_api.raw_conflict`. Les observations supplémentaires conservent leur provenance complète et ne créent jamais implicitement un canonical absent. La lecture reconstruit `Full` depuis le payload chaud et `Archived` depuis la relation archive ; `Purged` reste un tombstone minimal. La navigation est une keyset stricte `(slot, signature)` ASC/DESC sans `OFFSET`, avec cursor V1 opaque de 109 octets lié au réseau, à la direction, aux bornes et à la dernière clé. Aucun plafond métier de batch n'est introduit : seule la borne physique PostgreSQL de `LIMIT requested + 1` est appliquée.
La rétention physique supportée est `Full -> Archived -> Purged`, sérialisée par `FOR UPDATE`, avec outcomes `Applied`, `AlreadyAtTarget` et `ExpectedStateMismatch`. Le mode normal respecte le tombstone après purge ; `ForceRehydrate` reste explicite et atomique. `Compacted` demeure volontairement non supporté physiquement et retourne `store.postgres_retention_compaction_unsupported` plutôt que de présenter TOAST ou une compression implicite comme contrat KSP. Les erreurs backend sont réduites à des classes/phases statiques puis projetées vers des codes Store/API stables, sans URI, SQL, SQLSTATE, bind ni texte serveur.
La preuve PostgreSQL réelle a conduit à durcir l'introspection de schéma PostgreSQL 17 : canonicalisation ciblée des CHECK numériques reconstruits par le catalogue, conservation des littéraux texte, restauration des helpers de classification de schéma et distinction d'un drift d'une migration déjà enregistrée lorsque `schema_autoupdate=false`. Les ressources SQL V000/V001 et leurs checksums sont restés inchangés pendant ces corrections (`V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450`, `V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51`).
Le gate technique final `pre.011` passe audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, les tests ciblés Store/API/PostgreSQL/Config, les tests et checks façade avec `--no-default-features`, `cargo test --workspace` et les graphes Cargo. Le live `postgres_raw_transaction_live` est ensuite rejoué avec succès sur **PostgreSQL 17**, couvrant bootstrap/drift-repair, atomicité, concurrence identique/divergente, rollback sur collision et annulation, pagination/cursor, rétention/races, ForceRehydrate et réouverture durable. Aucun build Tauri supplémentaire n'est requis : `0.3.3` ne change ni resources applicatives ni packaging desktop. `RawAccountState` PostgreSQL et la complétude RAW restent réservés à `0.3.4`. `prompts/023-V0_3_4_START_PROMPT.md` ouvre cette slice suivante sur les quatre capabilities `RawAccount*`, une migration additive au-dessus de V000/V001, puis la conformance finale des dix capabilities RAW ; l'archive historique kbot3 y reste une source de comparaison ciblée account/observation, jamais une architecture à recopier.
## 0.3.2 — Store/PostgreSQL runtime foundation — 2026-08-30 ## 0.3.2 — Store/PostgreSQL runtime foundation — 2026-08-30
`0.3.2` introduit ensemble `ksp-store-lib` et `ksp-store-postgres-lib` comme fondation runtime/backend PostgreSQL au-dessus des contrats backend-agnostic de `ksp-store-api`, sans encore implémenter de capability RAW métier. La façade `Store` conserve un backend connu `Postgres` activé par défaut via Cargo feature, reste compilable avec `--no-default-features`, lie chaque instance à exactement un `RawNetworkId` et n'expose aucun `Pool`, `Client`, `Row`, `Statement`, driver ou SQL physique. `ksp-config-lib` possède désormais `std.store` avec trois targets nommés `devnet`, `mainnet` et `testnet`, chacun associé à un réseau explicite et à une URI PostgreSQL Secret indépendante ; Store/backend ne lisent directement ni `.env`, ni `KSP_*`, ni `PG*`, ni `.pgpass`. `0.3.2` introduit ensemble `ksp-store-lib` et `ksp-store-postgres-lib` comme fondation runtime/backend PostgreSQL au-dessus des contrats backend-agnostic de `ksp-store-api`, sans encore implémenter de capability RAW métier. La façade `Store` conserve un backend connu `Postgres` activé par défaut via Cargo feature, reste compilable avec `--no-default-features`, lie chaque instance à exactement un `RawNetworkId` et n'expose aucun `Pool`, `Client`, `Row`, `Statement`, driver ou SQL physique. `ksp-config-lib` possède désormais `std.store` avec trois targets nommés `devnet`, `mainnet` et `testnet`, chacun associé à un réseau explicite et à une URI PostgreSQL Secret indépendante ; Store/backend ne lisent directement ni `.env`, ni `KSP_*`, ni `PG*`, ni `.pgpass`.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 347 # version: 382
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.2" version = "0.3.4"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 95 --> <!-- version: 98 -->
# Roadmap KSP # Roadmap KSP
@@ -95,9 +95,9 @@ 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.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.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.
- [ ] `0.3.3`Étendre le même couple avec la vertical slice PostgreSQL `RawTransaction` complète : les six capabilities transaction/observation/rétention de `ksp-store-api`, persistence acquisition atomique, get/list cursorisé, idempotence/conflit, binding réseau, rétention/tombstone/force-rehydrate, concurrence et rollback validés sur PostgreSQL réel. - [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` + observation, puis fermer la complétude/conformance RAW cross-family, les indexes/migrations physiques nécessaires et le hardening PostgreSQL final. - [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/events réellement partagés par les premiers consumers dacquisition, sans dupliquer les modèles persistants de `ksp-store-api`. - [ ] `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.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. - [ ] `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 dexploitation réellement nécessaires avant de passer à la couche de normalisation générique suivante. - [ ] Compléter ensuite la couche RAW avec le worker/service live, son contrôle et les outils dexploitation 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/IDEAS — taxonomie N1, processing et rétention
- [ ] **TODO** — maintenir la matrice dadmission 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. - [ ] **TODO** — maintenir la matrice dadmission 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 lorsquun consumer réel apparaît ; ne pas fusionner snapshot, transition et update dans un modèle Option-soup. - [ ] **TODO**`TransactionStatusObservation` : réauditer `signatureSubscribe`, `getSignatureStatuses`, Yellowstone TransactionStatus et extensions provider lorsquun 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 dun wake-up « donnée persistée disponible » reste distinct et appartient à `ksp-store-api` conformément à `KSP-NOTIFY-*`, mais ne sera matérialisé quavec un publisher/consumer réel. - [ ] **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 dun wake-up « donnée persistée disponible » reste distinct et appartient à `ksp-store-api` conformément à `KSP-NOTIFY-*`, mais ne sera matérialisé quavec 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. - [ ] **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.
@@ -113,7 +113,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [ ] **REJET ACTUEL** — Yellowstone `Entry` : trop bas niveau et aucune destination replay/decomposition/event métier justifiant un modèle KSP nest identifiée. - [ ] **REJET ACTUEL** — Yellowstone `Entry` : trop bas niveau et aucune destination replay/decomposition/event métier justifiant un modèle KSP nest identifiée.
- [ ] **TODO** — processing ledger : reprendre lidée kbot2/kbot3 `stage + processor identity/version + input identity/hash + terminal status`, sans faire dun `processed: bool` la preuve durable unique ; prévoir force replay/version upgrades lorsque les processors seront ouverts. - [ ] **TODO** — processing ledger : reprendre lidée kbot2/kbot3 `stage + processor identity/version + input identity/hash + terminal status`, sans faire dun `processed: bool` la preuve durable unique ; prévoir force replay/version upgrades lorsque les processors seront ouverts.
- [X] lifecycle RAW logique — `RawRetentionState`, tombstone minimal, normal-skip et force-rehydrate sont stabilisés en `0.3.1` pour `RawTransaction`. - [X] lifecycle RAW logique — `RawRetentionState`, tombstone minimal, normal-skip et force-rehydrate sont stabilisés en `0.3.1` pour `RawTransaction`.
- [ ] **TODO** rétention physique : définir plus tard compression/archive backend, critères déligibilité fondés sur les preuves de processing et maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul quun RAW peut être purgé. - [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant quaucune représentation compactée réelle nexiste.
- [ ] **TODO** — policy de rétention/compaction : définir les critères déligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul quun RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée quavec un besoin réel.
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API. - [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de louverture de N2/N3 ; conserver lisolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique. - [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de louverture de N2/N3 ; conserver lisolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.

View File

@@ -1,5 +1,5 @@
{ {
"format_version": 1, "format_version": 2,
"default_profile": "devnet", "default_profile": "devnet",
"profiles": [ "profiles": [
{ {
@@ -19,7 +19,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },
@@ -43,7 +44,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },
@@ -67,7 +69,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.store:v1", "$id": "urn:ksp:schema:std.store:v2",
"title": "KSP standard Store configuration", "title": "KSP standard Store configuration",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -11,19 +11,45 @@
], ],
"properties": { "properties": {
"format_version": { "format_version": {
"const": 1 "enum": [
1,
2
]
}, },
"default_profile": { "default_profile": {
"$ref": "#/$defs/profileId" "$ref": "#/$defs/profileId"
}, },
"profiles": { "profiles": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 1
"items": {
"$ref": "#/$defs/profile"
}
} }
}, },
"oneOf": [
{
"properties": {
"format_version": {
"const": 1
},
"profiles": {
"items": {
"$ref": "#/$defs/profileV1"
}
}
}
},
{
"properties": {
"format_version": {
"const": 2
},
"profiles": {
"items": {
"$ref": "#/$defs/profileV2"
}
}
}
}
],
"$defs": { "$defs": {
"profileId": { "profileId": {
"type": "string", "type": "string",
@@ -40,7 +66,7 @@
"minimum": 100, "minimum": 100,
"maximum": 60000 "maximum": 60000
}, },
"profile": { "profileV1": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": [
@@ -60,11 +86,35 @@
"const": "postgres" "const": "postgres"
}, },
"postgres": { "postgres": {
"$ref": "#/$defs/postgres" "$ref": "#/$defs/postgresV1"
} }
} }
}, },
"postgres": { "profileV2": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"network",
"backend",
"postgres"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"network": {
"$ref": "#/$defs/networkId"
},
"backend": {
"const": "postgres"
},
"postgres": {
"$ref": "#/$defs/postgresV2"
}
}
},
"postgresV1": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": [
@@ -86,7 +136,38 @@
"$ref": "#/$defs/tls" "$ref": "#/$defs/tls"
}, },
"bootstrap": { "bootstrap": {
"$ref": "#/$defs/bootstrap" "$ref": "#/$defs/bootstrapV1"
},
"shutdown_timeout_ms": {
"type": "integer",
"minimum": 100,
"maximum": 30000
}
}
},
"postgresV2": {
"type": "object",
"additionalProperties": false,
"required": [
"connection_uri",
"pool",
"tls",
"bootstrap",
"shutdown_timeout_ms"
],
"properties": {
"connection_uri": {
"type": "string",
"minLength": 1
},
"pool": {
"$ref": "#/$defs/pool"
},
"tls": {
"$ref": "#/$defs/tls"
},
"bootstrap": {
"$ref": "#/$defs/bootstrapV2"
}, },
"shutdown_timeout_ms": { "shutdown_timeout_ms": {
"type": "integer", "type": "integer",
@@ -140,7 +221,7 @@
} }
} }
}, },
"bootstrap": { "bootstrapV1": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": [
@@ -163,6 +244,34 @@
"maximum": 120000 "maximum": 120000
} }
} }
},
"bootstrapV2": {
"type": "object",
"additionalProperties": false,
"required": [
"schema_autocreate",
"schema_autoupdate",
"migration_timeout_ms",
"migration_lock_timeout_ms"
],
"properties": {
"schema_autocreate": {
"type": "boolean"
},
"schema_autoupdate": {
"type": "boolean"
},
"migration_timeout_ms": {
"type": "integer",
"minimum": 1000,
"maximum": 300000
},
"migration_lock_timeout_ms": {
"type": "integer",
"minimum": 100,
"maximum": 120000
}
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"format_version": 1, "format_version": 2,
"default_profile": "devnet", "default_profile": "devnet",
"profiles": [ "profiles": [
{ {
@@ -19,7 +19,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },
@@ -43,7 +44,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },
@@ -67,7 +69,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/store.rs // file: crates/ksp-config-lib/src/store.rs
// version: 2 // version: 3
/// Effective standard Store configuration mapped to `ksp_store_lib::StoreSettings`. /// Effective standard Store configuration mapped to `ksp_store_lib::StoreSettings`.
pub struct ResolvedStoreConfig { pub struct ResolvedStoreConfig {
@@ -153,9 +153,11 @@ struct EffectivePostgresTlsSource {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
struct EffectivePostgresBootstrapSource { struct EffectivePostgresBootstrapSource {
auto_migrate: bool, auto_migrate: std::option::Option<bool>,
migration_lock_timeout_ms: u64, migration_lock_timeout_ms: u64,
migration_timeout_ms: u64, migration_timeout_ms: u64,
schema_autocreate: std::option::Option<bool>,
schema_autoupdate: std::option::Option<bool>,
} }
fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedStoreConfig> { fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedStoreConfig> {
@@ -178,7 +180,7 @@ fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &c
); );
}, },
}; };
if source.format_version != 1 { if source.format_version != 1 && source.format_version != 2 {
return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported")); return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported"));
} }
if source.profile_id != profile.profile_id() { if source.profile_id != profile.profile_id() {
@@ -199,11 +201,34 @@ fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &c
std::time::Duration::from_millis(source.postgres.pool.create_timeout_ms), std::time::Duration::from_millis(source.postgres.pool.create_timeout_ms),
std::time::Duration::from_millis(source.postgres.pool.recycle_timeout_ms), std::time::Duration::from_millis(source.postgres.pool.recycle_timeout_ms),
); );
let bootstrap = ksp_store_lib::PostgresBootstrapSettings::new( let bootstrap = match source.format_version {
source.postgres.bootstrap.auto_migrate, 1 => {
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms), let auto_migrate =
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms), match (source.postgres.bootstrap.auto_migrate, source.postgres.bootstrap.schema_autocreate, source.postgres.bootstrap.schema_autoupdate) {
); (std::option::Option::Some(value), std::option::Option::None, std::option::Option::None) => value,
_ => return std::result::Result::Err(effective_error(profile, "effective Store V1 bootstrap policy is invalid")),
};
ksp_store_lib::PostgresBootstrapSettings::new(
auto_migrate,
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms),
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms),
)
},
2 => {
let (schema_autocreate, schema_autoupdate) =
match (source.postgres.bootstrap.auto_migrate, source.postgres.bootstrap.schema_autocreate, source.postgres.bootstrap.schema_autoupdate) {
(std::option::Option::None, std::option::Option::Some(autocreate), std::option::Option::Some(autoupdate)) => (autocreate, autoupdate),
_ => return std::result::Result::Err(effective_error(profile, "effective Store V2 bootstrap policy is invalid")),
};
ksp_store_lib::PostgresBootstrapSettings::with_schema_policy(
schema_autocreate,
schema_autoupdate,
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms),
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms),
)
},
_ => return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported")),
};
let network = ksp_store_lib::RawNetworkId::new(source.network); let network = ksp_store_lib::RawNetworkId::new(source.network);
let network = match network { let network = match network {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,

View File

@@ -1,5 +1,5 @@
{ {
"format_version": 1, "format_version": 2,
"default_profile": "devnet", "default_profile": "devnet",
"profiles": [ "profiles": [
{ {
@@ -19,7 +19,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },
@@ -43,7 +44,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },
@@ -67,7 +69,8 @@
"mode": "verify_full" "mode": "verify_full"
}, },
"bootstrap": { "bootstrap": {
"auto_migrate": true, "schema_autocreate": true,
"schema_autoupdate": true,
"migration_timeout_ms": 30000, "migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000 "migration_lock_timeout_ms": 10000
}, },

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/store.rs // file: crates/ksp-config-lib/unit_tests/store.rs
// version: 2 // version: 3
#[test] #[test]
fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() { fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
@@ -32,6 +32,8 @@ fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
assert_eq!(postgres.pool().recycle_timeout(), std::time::Duration::from_millis(5_000)); assert_eq!(postgres.pool().recycle_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::VerifyFull); assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::VerifyFull);
assert!(postgres.bootstrap().auto_migrate()); assert!(postgres.bootstrap().auto_migrate());
assert!(postgres.bootstrap().schema_autocreate());
assert!(postgres.bootstrap().schema_autoupdate());
assert_eq!(postgres.bootstrap().migration_timeout(), std::time::Duration::from_millis(30_000)); assert_eq!(postgres.bootstrap().migration_timeout(), std::time::Duration::from_millis(30_000));
assert_eq!(postgres.bootstrap().migration_lock_timeout(), std::time::Duration::from_millis(10_000)); assert_eq!(postgres.bootstrap().migration_lock_timeout(), std::time::Duration::from_millis(10_000));
} }
@@ -140,6 +142,90 @@ fn named_store_targets_select_one_network_and_database_without_runtime_multiplex
return; return;
} }
#[test]
fn pre_003_fix_001_v1_auto_migrate_remains_backward_readable_and_maps_both_schema_policies() {
for auto_migrate in [false, true] {
let fixture = tempfile::tempdir();
assert!(fixture.is_ok());
let fixture = match fixture {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let source = committed_document_value();
let mut source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
source["format_version"] = serde_json::Value::from(1);
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
if let std::option::Option::Some(profiles) = profiles {
for profile in profiles {
let bootstrap = profile.pointer_mut("/postgres/bootstrap").and_then(serde_json::Value::as_object_mut);
if let std::option::Option::Some(bootstrap) = bootstrap {
bootstrap.remove("schema_autocreate");
bootstrap.remove("schema_autoupdate");
bootstrap.insert("auto_migrate".to_owned(), serde_json::Value::Bool(auto_migrate));
}
}
}
let engine = fixture_engine_with_document(fixture.path(), &source);
assert!(engine.is_ok());
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "Store Config V1 compatibility mapping failed: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved
&& let ksp_store_lib::StoreBackendSettings::Postgres(postgres) = resolved.settings().backend()
{
assert_eq!(postgres.bootstrap().schema_autocreate(), auto_migrate);
assert_eq!(postgres.bootstrap().schema_autoupdate(), auto_migrate);
assert_eq!(postgres.bootstrap().auto_migrate(), auto_migrate);
}
}
return;
}
#[test]
fn pre_003_fix_001_v2_schema_creation_and_update_policies_map_independently() {
let fixture = tempfile::tempdir();
assert!(fixture.is_ok());
let fixture = match fixture {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let source = committed_document_value();
let mut source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
if let std::option::Option::Some(profiles) = profiles
&& let std::option::Option::Some(profile) = profiles.first_mut()
{
profile["postgres"]["bootstrap"]["schema_autocreate"] = serde_json::Value::Bool(false);
profile["postgres"]["bootstrap"]["schema_autoupdate"] = serde_json::Value::Bool(true);
}
let engine = fixture_engine_with_document(fixture.path(), &source);
assert!(engine.is_ok());
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "Store Config V2 split schema policy mapping failed: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved
&& let ksp_store_lib::StoreBackendSettings::Postgres(postgres) = resolved.settings().backend()
{
assert!(!postgres.bootstrap().schema_autocreate());
assert!(postgres.bootstrap().schema_autoupdate());
}
return;
}
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> { fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root(); let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-lib/README.md --> <!-- file: crates/ksp-store-lib/README.md -->
<!-- version: 1 --> <!-- version: 5 -->
# ksp-store-lib # ksp-store-lib
@@ -19,6 +19,8 @@ Elle expose aux consumers une surface backend-neutral, réexporte les contrats R
- `Store::health().await` pour la readiness portable et bornée ; - `Store::health().await` pour la readiness portable et bornée ;
- `Store::close(self).await` pour la fermeture explicite 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 ; - le mapping des erreurs backend vers des codes Store stables sans exposer les erreurs physiques ;
- 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. - les réexports crate-root de `ksp-store-api` nécessaires aux consumers ordinaires.
## Une instance = un réseau ## Une instance = un réseau
@@ -29,7 +31,7 @@ Une instance `Store` représente exactement :
1 Store = 1 RawNetworkId + 1 backend physique sélectionné 1 Store = 1 RawNetworkId + 1 backend physique sélectionné
``` ```
Le runtime Store n'est pas un multiplexeur multi-database ou multi-réseau. La sélection d'un target nommé appartient à Config. Le document `std.store` peut donc définir plusieurs targets indépendants — par exemple Devnet, Mainnet et Testnet — mais un appel à `Store::open` reçoit les settings d'un seul target. Le runtime Store n'est pas un multiplexeur multi-database ou multi-réseau. La sélection d'un target nommé appartient à Config. Le document `std.store` peut donc définir plusieurs targets indépendants, mais un appel à `Store::open` reçoit les settings d'un seul target.
Cette séparation permet d'utiliser des bases PostgreSQL distinctes par réseau tout en conservant le réseau dans l'identité logique des données RAW. Cette séparation permet d'utiliser des bases PostgreSQL distinctes par réseau tout en conservant le réseau dans l'identité logique des données RAW.
@@ -61,35 +63,53 @@ 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. `ksp-config-lib` possède `std.store`, la résolution des secrets et la sélection du target. Il construit ensuite un `StoreSettings` backend-neutral. L'URI PostgreSQL reste nécessaire au runtime mais n'a aucun getter public dans `ksp-store-lib` et son `Debug` est redacted.
Les targets committed sont actuellement : ## Surface RAW
`Store` implémente exactement dix capabilities RAW backend-neutral :
```text ```text
devnet -> network devnet -> base indépendante RawTransactionRead
mainnet -> network mainnet-beta -> base indépendante RawTransactionWrite
testnet -> network testnet -> base indépendante RawTransactionObservationRead
RawTransactionObservationWrite
RawTransactionRetentionRead
RawTransactionRetentionWrite
RawAccountStateRead
RawAccountStateWrite
RawAccountObservationRead
RawAccountObservationWrite
``` ```
Les credentials restent dans les variables `KSP_SECRET_STORE_*_POSTGRES_URI` ou le `.env` possédé par Config. 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.
## Surface actuelle et hors périmètre Pour `RawAccountState`, elle fournit :
La fondation runtime ne fournit encore aucune implémentation PostgreSQL des capabilities métier RAW de `ksp-store-api`. - 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 : Sont volontairement hors de cette surface :
- persistence/query/rétention PostgreSQL de `RawTransaction` ; - rétention, archivage, purge, delete ou compaction de `RawAccountState` ;
- persistence/query/rétention PostgreSQL de `RawAccountState` ;
- batch-size, priorité, backlog ou policy de worker/job ; - batch-size, priorité, backlog ou policy de worker/job ;
- transport d'acquisition, Program decoding et materialization ; - transport d'acquisition, Program decoding et materialization ;
- exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL. - exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL.
Les premières vertical slices métier sont ajoutées séparément afin que la façade runtime reste stable et backend-neutral.
## Documentation ## Documentation
- [`USAGE.md`](USAGE.md) — construction des settings, ouverture, health et fermeture ; - [`USAGE.md`](USAGE.md) — guide pratique de construction, lifecycle et capabilities Store ;
- [`../ksp-store-postgres-lib/README.md`](../ksp-store-postgres-lib/README.md) — responsabilité du backend PostgreSQL physique ; - [`../ksp-store-postgres-lib/README.md`](../ksp-store-postgres-lib/README.md) — responsabilité du backend PostgreSQL physique ;
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store committed ; - [`../../config/std.store.json`](../../config/std.store.json) — targets Store committed ;
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture durable Store ; - [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture durable Store ;
- [`../../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/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) — matrice de validation. - [`../../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/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.

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/error.rs // file: crates/ksp-store-lib/src/error.rs
// version: 4 // version: 6
/// Error code reserved for operations attempted after a Store backend has entered its closed state. /// Error code reserved for operations attempted after a Store backend has entered its closed state.
pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed"); pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed");
@@ -11,19 +11,34 @@ pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_a
pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid"); pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid");
/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details. /// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details.
pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed"); pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed");
/// Error code used when PostgreSQL returns persisted RAW data incompatible with the stable Store contract.
pub const ERROR_CODE_POSTGRES_DATA_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_data_invalid");
/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely. /// Error code used when a lightweight PostgreSQL health/readiness probe fails safely.
pub const ERROR_CODE_POSTGRES_HEALTH_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_health_failed"); pub const ERROR_CODE_POSTGRES_HEALTH_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_health_failed");
/// Error code used when PostgreSQL migration/bootstrap execution fails without exposing server text or SQL. /// Error code used when PostgreSQL migration/bootstrap execution fails without exposing server text or SQL.
pub const ERROR_CODE_POSTGRES_MIGRATION_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_failed"); pub const ERROR_CODE_POSTGRES_MIGRATION_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_failed");
/// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history. /// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history.
pub const ERROR_CODE_POSTGRES_MIGRATION_MISMATCH: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_mismatch"); pub const ERROR_CODE_POSTGRES_MIGRATION_MISMATCH: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_mismatch");
/// Error code used when the requested RAW page size exceeds the exact PostgreSQL LIMIT representation boundary.
pub const ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_page_limit_unsupported");
/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline. /// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline.
pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout"); pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout");
/// Error code used when a PostgreSQL RAW read fails without exposing SQL, bind values or server text.
pub const ERROR_CODE_POSTGRES_READ_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_read_failed");
/// Error code used when PostgreSQL cannot represent a requested RAW retention compaction state.
pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode =
ksp_store_api::ErrorCode::new("store", "postgres_retention_compaction_unsupported");
/// Error code used when PostgreSQL history contains a migration newer than this Store runtime understands. /// Error code used when PostgreSQL history contains a migration newer than this Store runtime understands.
pub const ERROR_CODE_POSTGRES_SCHEMA_NEWER: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_schema_newer"); pub const ERROR_CODE_POSTGRES_SCHEMA_NEWER: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_schema_newer");
/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely. /// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely.
pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed"); pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed");
/// Error code used when a PostgreSQL RAW write fails without exposing SQL, bind values or server text.
pub const ERROR_CODE_POSTGRES_WRITE_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_write_failed");
/// Error code used when a RAW write requires a canonical reference that is not durable.
pub const ERROR_CODE_RAW_REFERENCE_NOT_FOUND: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "raw_reference_not_found");
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants. /// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid"); pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid");
/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound. /// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.
pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout"); pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout");
/// Error code used when a network-scoped Store operation targets a network different from the opened Store binding.
pub const ERROR_CODE_WRONG_NETWORK: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "wrong_network");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/lib.rs // file: crates/ksp-store-lib/src/lib.rs
// version: 6 // version: 9
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -7,10 +7,11 @@
//! Common backend-neutral Store runtime facade for KSP. //! Common backend-neutral Store runtime facade for KSP.
//! //!
//! `0.3.2-pre.007` closes the physical PostgreSQL runtime composition with a //! The runtime facade owns backend selection, lifecycle, safe diagnostics and
//! portable safe runtime snapshot and lightweight health/readiness projection, //! backend-neutral capability dispatch. `0.3.3-pre.008` completed the six PostgreSQL
//! while retaining the private migration/bootstrap foundation and no business //! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities
//! persistence schema. //! 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 //! The default `postgres` feature compiles the official PostgreSQL backend as
//! an optional implementation dependency. No backend implementation type is //! an optional implementation dependency. No backend implementation type is
@@ -32,22 +33,36 @@ pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID; pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
/// Error code used when PostgreSQL physical connection establishment fails. /// Error code used when PostgreSQL physical connection establishment fails.
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED; pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
/// Error code used when PostgreSQL returns persisted RAW data incompatible with the Store contract.
pub use self::error::ERROR_CODE_POSTGRES_DATA_INVALID;
/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely. /// Error code used when a lightweight PostgreSQL health/readiness probe fails safely.
pub use self::error::ERROR_CODE_POSTGRES_HEALTH_FAILED; pub use self::error::ERROR_CODE_POSTGRES_HEALTH_FAILED;
/// Error code used when PostgreSQL migration/bootstrap execution fails safely. /// Error code used when PostgreSQL migration/bootstrap execution fails safely.
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED; pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED;
/// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history. /// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history.
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH; pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH;
/// Error code used when a requested RAW page size exceeds PostgreSQL's exact physical LIMIT boundary.
pub use self::error::ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED;
/// Error code used when a bounded PostgreSQL pool operation reaches its deadline. /// Error code used when a bounded PostgreSQL pool operation reaches its deadline.
pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT; pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT;
/// Error code used when a PostgreSQL RAW read statement fails safely.
pub use self::error::ERROR_CODE_POSTGRES_READ_FAILED;
/// Error code used when PostgreSQL cannot represent a requested RAW retention compaction state.
pub use self::error::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED;
/// Error code used when PostgreSQL schema history is newer than this Store runtime. /// Error code used when PostgreSQL schema history is newer than this Store runtime.
pub use self::error::ERROR_CODE_POSTGRES_SCHEMA_NEWER; pub use self::error::ERROR_CODE_POSTGRES_SCHEMA_NEWER;
/// Error code used when PostgreSQL verified TLS setup or negotiation fails. /// Error code used when PostgreSQL verified TLS setup or negotiation fails.
pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED; pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED;
/// Error code used when a PostgreSQL RAW write statement or transaction fails safely.
pub use self::error::ERROR_CODE_POSTGRES_WRITE_FAILED;
/// Error code used when a RAW write requires a canonical reference that is not durable.
pub use self::error::ERROR_CODE_RAW_REFERENCE_NOT_FOUND;
/// Error code used when Store settings violate backend-neutral bounds or invariants. /// Error code used when Store settings violate backend-neutral bounds or invariants.
pub use self::error::ERROR_CODE_SETTINGS_INVALID; pub use self::error::ERROR_CODE_SETTINGS_INVALID;
/// Error code used when explicit Store shutdown exceeds its configured deadline. /// Error code used when explicit Store shutdown exceeds its configured deadline.
pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT; pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT;
/// Error code used when a network-scoped operation targets a network different from the Store binding.
pub use self::error::ERROR_CODE_WRONG_NETWORK;
/// Portable Store health/readiness projection containing only safe diagnostics. /// Portable Store health/readiness projection containing only safe diagnostics.
pub use self::health::StoreHealthSnapshot; pub use self::health::StoreHealthSnapshot;
/// Portable Store health state independent from physical backend types. /// Portable Store health state independent from physical backend types.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/settings.rs // file: crates/ksp-store-lib/src/settings.rs
// version: 3 // version: 4
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000; const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_MAX_CONNECTIONS: u32 = 8; const DEFAULT_MAX_CONNECTIONS: u32 = 8;
@@ -153,22 +153,49 @@ impl std::default::Default for PostgresPoolSettings {
/// Bounded PostgreSQL bootstrap settings owned by the Store facade. /// Bounded PostgreSQL bootstrap settings owned by the Store facade.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PostgresBootstrapSettings { pub struct PostgresBootstrapSettings {
auto_migrate: bool,
migration_lock_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration,
migration_timeout: std::time::Duration, migration_timeout: std::time::Duration,
schema_autocreate: bool,
schema_autoupdate: bool,
} }
impl PostgresBootstrapSettings { impl PostgresBootstrapSettings {
/// Creates explicit bootstrap behavior and migration deadlines. /// Creates bootstrap settings using the legacy single migration switch for source compatibility.
///
/// The supplied value is mapped to both schema auto-creation and schema auto-update. New code should prefer
/// [`Self::with_schema_policy`] when these policies need to differ.
#[must_use] #[must_use]
pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self { pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self {
return Self { auto_migrate, migration_lock_timeout, migration_timeout }; return Self::with_schema_policy(auto_migrate, auto_migrate, migration_timeout, migration_lock_timeout);
} }
/// Returns whether pending KSP-owned migrations may be applied during Store opening. /// Creates explicit schema creation/update policy and migration deadlines.
#[must_use]
pub const fn with_schema_policy(
schema_autocreate: bool,
schema_autoupdate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> Self {
return Self { migration_lock_timeout, migration_timeout, schema_autocreate, schema_autoupdate };
}
/// Returns the legacy pending-migration switch, mapped to the schema auto-update policy.
#[must_use] #[must_use]
pub const fn auto_migrate(&self) -> bool { pub const fn auto_migrate(&self) -> bool {
return self.auto_migrate; return self.schema_autoupdate;
}
/// Returns whether an absent KSP-managed schema may be created or adopted during Store opening.
#[must_use]
pub const fn schema_autocreate(&self) -> bool {
return self.schema_autocreate;
}
/// Returns whether pending migrations and safe additive schema repairs may be applied during Store opening.
#[must_use]
pub const fn schema_autoupdate(&self) -> bool {
return self.schema_autoupdate;
} }
/// Returns the bounded wait allowed for the private PostgreSQL migration lock. /// Returns the bounded wait allowed for the private PostgreSQL migration lock.
@@ -205,7 +232,8 @@ impl PostgresBootstrapSettings {
impl std::default::Default for PostgresBootstrapSettings { impl std::default::Default for PostgresBootstrapSettings {
fn default() -> Self { fn default() -> Self {
return Self::new( return Self::with_schema_policy(
true,
true, true,
std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS), std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS),
std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS), std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/store.rs // file: crates/ksp-store-lib/src/store.rs
// version: 4 // version: 7
/// Opaque common Store runtime facade. /// Opaque common Store runtime facade.
/// ///
@@ -116,6 +116,411 @@ 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,
reference: &'a ksp_store_api::RawTransactionReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawTransaction>>> {
let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.get_raw_transaction(reference).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = reference;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
fn list_raw_transactions<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawTransactionReference>>> {
let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.list_raw_transactions(query).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = query;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionWrite for Store {
fn persist_raw_transaction_acquisition<'a>(
&'a self,
transaction: ksp_store_api::RawTransaction,
observation: ksp_store_api::RawTransactionObservation,
mode: ksp_store_api::RawTransactionAcquisitionMode,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawAcquisitionWriteOutcome>> {
let transaction_network = validate_operation_network(&self.network, transaction.reference().network(), self.backend_kind);
if let std::result::Result::Err(error) = transaction_network {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
let observation_network = validate_operation_network(&self.network, observation.transaction().network(), self.backend_kind);
if let std::result::Result::Err(error) = observation_network {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.persist_raw_transaction_acquisition(transaction, observation, mode).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = transaction;
let _ = observation;
let _ = mode;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionObservationRead for Store {
fn get_raw_transaction_observation<'a>(
&'a self,
observation_key: &'a ksp_store_api::RawObservationKey,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawTransactionObservation>>> {
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.get_raw_transaction_observation(observation_key).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = observation_key;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionObservationWrite for Store {
fn record_raw_transaction_observation<'a>(
&'a self,
observation: ksp_store_api::RawTransactionObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawObservationWriteOutcome>> {
let network_check = validate_operation_network(&self.network, observation.transaction().network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.record_raw_transaction_observation(observation).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = observation;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionRetentionRead for Store {
fn get_raw_transaction_retention_state<'a>(
&'a self,
reference: &'a ksp_store_api::RawTransactionReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawRetentionState>>> {
let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.get_raw_transaction_retention_state(reference).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = reference;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
fn get_raw_transaction_tombstone<'a>(
&'a self,
reference: &'a ksp_store_api::RawTransactionReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawTransactionTombstone>>> {
let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.get_raw_transaction_tombstone(reference).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = reference;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionRetentionWrite for Store {
fn transition_raw_transaction_retention<'a>(
&'a self,
transition: ksp_store_api::RawTransactionRetentionTransition,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawRetentionWriteOutcome>> {
let network_check = validate_operation_network(&self.network, transition.reference().network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.transition_raw_transaction_retention(transition).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = transition;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
fn validate_operation_network(
store_network: &ksp_store_api::RawNetworkId,
operation_network: &ksp_store_api::RawNetworkId,
backend_kind: crate::StoreBackendKind,
) -> ksp_store_api::Result<()> {
if store_network != operation_network {
return std::result::Result::Err(
ksp_store_api::Error::new(crate::ERROR_CODE_WRONG_NETWORK, "Store operation targeted a different logical network")
.with_context("backend", backend_kind.code())
.with_context("network", store_network.as_str()),
);
}
return std::result::Result::Ok(());
}
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
enum StoreRuntime { enum StoreRuntime {
Postgres(ksp_store_postgres_lib::PostgresBackend), Postgres(ksp_store_postgres_lib::PostgresBackend),
@@ -134,7 +539,7 @@ async fn open_postgres(
crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
crate::PostgresTlsMode::VerifyFull => ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, crate::PostgresTlsMode::VerifyFull => ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
}; };
let backend_settings = ksp_store_postgres_lib::PostgresBackendSettings::new( let backend_settings = ksp_store_postgres_lib::PostgresBackendSettings::with_schema_policy(
network.clone(), network.clone(),
settings.connection_uri(), settings.connection_uri(),
pool.max_connections(), pool.max_connections(),
@@ -143,7 +548,8 @@ async fn open_postgres(
pool.create_timeout(), pool.create_timeout(),
pool.recycle_timeout(), pool.recycle_timeout(),
tls_mode, tls_mode,
bootstrap.auto_migrate(), bootstrap.schema_autocreate(),
bootstrap.schema_autoupdate(),
bootstrap.migration_timeout(), bootstrap.migration_timeout(),
bootstrap.migration_lock_timeout(), bootstrap.migration_lock_timeout(),
); );
@@ -175,7 +581,7 @@ async fn open_postgres(
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error { fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error {
let code = postgres_error_code(error.kind()); let code = postgres_error_code(error.kind());
return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed") return ksp_store_api::Error::new(code, "PostgreSQL Store backend operation failed")
.with_context("backend", backend_kind.code()) .with_context("backend", backend_kind.code())
.with_context("network", network) .with_context("network", network)
.with_context("phase", error.phase()); .with_context("phase", error.phase());
@@ -214,13 +620,22 @@ fn postgres_error_code(kind: ksp_store_postgres_lib::PostgresBackendErrorKind) -
return match kind { return match kind {
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID, ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID,
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED, ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::Conflict => ksp_store_api::ERROR_CODE_RAW_CONFLICT,
ksp_store_postgres_lib::PostgresBackendErrorKind::DataInvalid => crate::ERROR_CODE_POSTGRES_DATA_INVALID,
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => crate::ERROR_CODE_POSTGRES_HEALTH_FAILED, ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => crate::ERROR_CODE_POSTGRES_HEALTH_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT, ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED, ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH, ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH,
ksp_store_postgres_lib::PostgresBackendErrorKind::PageLimitUnsupported => crate::ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED,
ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid => ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID,
ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed => crate::ERROR_CODE_POSTGRES_READ_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound => crate::ERROR_CODE_RAW_REFERENCE_NOT_FOUND,
ksp_store_postgres_lib::PostgresBackendErrorKind::RetentionCompactionUnsupported => crate::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED,
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => crate::ERROR_CODE_POSTGRES_SCHEMA_NEWER, ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => crate::ERROR_CODE_POSTGRES_SCHEMA_NEWER,
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT, ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT,
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED, ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::WriteFailed => crate::ERROR_CODE_POSTGRES_WRITE_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::WrongNetwork => crate::ERROR_CODE_WRONG_NETWORK,
_ => crate::ERROR_CODE_BACKEND_OPEN_FAILED, _ => crate::ERROR_CODE_BACKEND_OPEN_FAILED,
}; };
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs // file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 6 // version: 8
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -58,3 +58,30 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
} }
return; return;
} }
#[test]
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::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}");
}
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;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/hardening_completeness.rs // file: crates/ksp-store-lib/tests/hardening_completeness.rs
// version: 1 // version: 6
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -84,6 +84,27 @@ fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
return names; 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] #[test]
fn pre_009_facade_modules_and_crate_root_exports_are_exact() { fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
let crate_root = include_str!("../src/lib.rs"); let crate_root = include_str!("../src/lib.rs");
@@ -98,20 +119,27 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
"ERROR_CODE_BACKEND_OPEN_FAILED", "ERROR_CODE_BACKEND_OPEN_FAILED",
"ERROR_CODE_POSTGRES_CONFIG_INVALID", "ERROR_CODE_POSTGRES_CONFIG_INVALID",
"ERROR_CODE_POSTGRES_CONNECT_FAILED", "ERROR_CODE_POSTGRES_CONNECT_FAILED",
"ERROR_CODE_POSTGRES_DATA_INVALID",
"ERROR_CODE_POSTGRES_HEALTH_FAILED", "ERROR_CODE_POSTGRES_HEALTH_FAILED",
"ERROR_CODE_POSTGRES_MIGRATION_FAILED", "ERROR_CODE_POSTGRES_MIGRATION_FAILED",
"ERROR_CODE_POSTGRES_MIGRATION_MISMATCH", "ERROR_CODE_POSTGRES_MIGRATION_MISMATCH",
"ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED",
"ERROR_CODE_POSTGRES_POOL_TIMEOUT", "ERROR_CODE_POSTGRES_POOL_TIMEOUT",
"ERROR_CODE_POSTGRES_READ_FAILED",
"ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED",
"ERROR_CODE_POSTGRES_SCHEMA_NEWER", "ERROR_CODE_POSTGRES_SCHEMA_NEWER",
"ERROR_CODE_POSTGRES_TLS_FAILED", "ERROR_CODE_POSTGRES_TLS_FAILED",
"ERROR_CODE_POSTGRES_WRITE_FAILED",
"ERROR_CODE_RAW_CONFLICT", "ERROR_CODE_RAW_CONFLICT",
"ERROR_CODE_RAW_MODEL_INVALID", "ERROR_CODE_RAW_MODEL_INVALID",
"ERROR_CODE_RAW_PAYLOAD_INVALID", "ERROR_CODE_RAW_PAYLOAD_INVALID",
"ERROR_CODE_RAW_PROVENANCE_INVALID", "ERROR_CODE_RAW_PROVENANCE_INVALID",
"ERROR_CODE_RAW_QUERY_INVALID", "ERROR_CODE_RAW_QUERY_INVALID",
"ERROR_CODE_RAW_REFERENCE_NOT_FOUND",
"ERROR_CODE_RAW_RETENTION_INVALID", "ERROR_CODE_RAW_RETENTION_INVALID",
"ERROR_CODE_SETTINGS_INVALID", "ERROR_CODE_SETTINGS_INVALID",
"ERROR_CODE_SHUTDOWN_TIMEOUT", "ERROR_CODE_SHUTDOWN_TIMEOUT",
"ERROR_CODE_WRONG_NETWORK",
"Error", "Error",
"ErrorCode", "ErrorCode",
"ErrorContext", "ErrorContext",
@@ -180,7 +208,7 @@ fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
]; ];
expected.sort_unstable(); expected.sort_unstable();
assert_eq!(actual.as_slice(), expected.as_slice()); assert_eq!(actual.as_slice(), expected.as_slice());
assert_eq!(actual.len(), 84); assert_eq!(actual.len(), 91);
return; return;
} }
@@ -254,3 +282,42 @@ fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_ha
} }
return; return;
} }
#[test]
fn pre_010_facade_raw_capability_inventory_is_exactly_ten() {
let store = include_str!("../src/store.rs");
let capability_impls = [
"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}");
}
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;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/public_api.rs // file: crates/ksp-store-lib/tests/public_api.rs
// version: 5 // version: 8
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -38,14 +38,21 @@ fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() {
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_DATA_INVALID.code(), "postgres_data_invalid");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_HEALTH_FAILED.code(), "postgres_health_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_HEALTH_FAILED.code(), "postgres_health_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_READ_FAILED.code(), "postgres_read_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED.code(), "postgres_retention_compaction_unsupported");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_FAILED.code(), "postgres_migration_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_FAILED.code(), "postgres_migration_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED.code(), "postgres_page_limit_unsupported");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_SCHEMA_NEWER.code(), "postgres_schema_newer"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_SCHEMA_NEWER.code(), "postgres_schema_newer");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_WRITE_FAILED.code(), "postgres_write_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_RAW_REFERENCE_NOT_FOUND.code(), "raw_reference_not_found");
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed"); assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout"); assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
assert_eq!(ksp_store_lib::ERROR_CODE_WRONG_NETWORK.code(), "wrong_network");
return; return;
} }
@@ -66,3 +73,26 @@ fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts()
let _runtime = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreRuntimeSnapshot>>(); let _runtime = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreRuntimeSnapshot>>();
return; return;
} }
fn assert_raw_capabilities<T>()
where
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::RawTransactionWrite,
{
let _marker = std::marker::PhantomData::<T>;
return;
}
#[test]
fn pre_008_store_facade_implements_exact_raw_capability_set_10_of_10() {
assert_raw_capabilities::<ksp_store_lib::Store>();
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/unit_tests/settings.rs // file: crates/ksp-store-lib/unit_tests/settings.rs
// version: 3 // version: 4
fn valid_network() -> crate::RawNetworkId { fn valid_network() -> crate::RawNetworkId {
return match crate::RawNetworkId::new("devnet") { return match crate::RawNetworkId::new("devnet") {
@@ -27,6 +27,12 @@ fn defaults_match_the_pre_001_runtime_bounds() {
assert_eq!(pool.recycle_timeout(), std::time::Duration::from_millis(5_000)); assert_eq!(pool.recycle_timeout(), std::time::Duration::from_millis(5_000));
let bootstrap = crate::PostgresBootstrapSettings::default(); let bootstrap = crate::PostgresBootstrapSettings::default();
assert!(bootstrap.auto_migrate()); assert!(bootstrap.auto_migrate());
assert!(bootstrap.schema_autocreate());
assert!(bootstrap.schema_autoupdate());
let split_policy =
crate::PostgresBootstrapSettings::with_schema_policy(false, true, std::time::Duration::from_millis(30_000), std::time::Duration::from_millis(10_000));
assert!(!split_policy.schema_autocreate());
assert!(split_policy.schema_autoupdate());
assert_eq!(bootstrap.migration_timeout(), std::time::Duration::from_millis(30_000)); assert_eq!(bootstrap.migration_timeout(), std::time::Duration::from_millis(30_000));
assert_eq!(bootstrap.migration_lock_timeout(), std::time::Duration::from_millis(10_000)); assert_eq!(bootstrap.migration_lock_timeout(), std::time::Duration::from_millis(10_000));
let store = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings())); let store = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/unit_tests/store.rs // file: crates/ksp-store-lib/unit_tests/store.rs
// version: 4 // version: 6
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T { fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut future = std::boxed::Box::pin(future); let mut future = std::boxed::Box::pin(future);
@@ -55,3 +55,53 @@ fn known_postgres_without_feature_is_rejected_before_io() {
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED)); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
return; return;
} }
#[test]
fn pre_008_operation_network_guard_rejects_mismatch_without_echoing_requested_network() {
let store_network = valid_network();
let hostile = match crate::RawNetworkId::new("other-network") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid alternate network rejected: {error:?}"),
};
let result = super::validate_operation_network(&store_network, &hostile, crate::StoreBackendKind::Postgres);
let error = match result {
std::result::Result::Err(value) => value,
std::result::Result::Ok(()) => panic!("wrong operation network unexpectedly accepted"),
};
assert_eq!(error.code(), crate::ERROR_CODE_WRONG_NETWORK);
assert!(!std::format!("{error:?}").contains("other-network"));
return;
}
#[cfg(feature = "postgres")]
#[test]
fn pre_010_postgres_error_code_mapping_covers_every_current_backend_kind() {
let cases = [
(ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid, crate::ERROR_CODE_POSTGRES_CONFIG_INVALID),
(ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed, crate::ERROR_CODE_POSTGRES_CONNECT_FAILED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout, crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT),
(ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed, crate::ERROR_CODE_POSTGRES_HEALTH_FAILED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::Conflict, ksp_store_api::ERROR_CODE_RAW_CONFLICT),
(ksp_store_postgres_lib::PostgresBackendErrorKind::DataInvalid, crate::ERROR_CODE_POSTGRES_DATA_INVALID),
(ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed, crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::PageLimitUnsupported, crate::ERROR_CODE_POSTGRES_PAGE_LIMIT_UNSUPPORTED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch, crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH),
(ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid, ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID),
(ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed, crate::ERROR_CODE_POSTGRES_READ_FAILED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound, crate::ERROR_CODE_RAW_REFERENCE_NOT_FOUND),
(
ksp_store_postgres_lib::PostgresBackendErrorKind::RetentionCompactionUnsupported,
crate::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED,
),
(ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer, crate::ERROR_CODE_POSTGRES_SCHEMA_NEWER),
(ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout, crate::ERROR_CODE_SHUTDOWN_TIMEOUT),
(ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed, crate::ERROR_CODE_POSTGRES_TLS_FAILED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::WriteFailed, crate::ERROR_CODE_POSTGRES_WRITE_FAILED),
(ksp_store_postgres_lib::PostgresBackendErrorKind::WrongNetwork, crate::ERROR_CODE_WRONG_NETWORK),
];
assert_eq!(cases.len(), 18);
for (kind, expected) in cases {
assert_eq!(super::postgres_error_code(kind), expected);
}
return;
}

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-store-postgres-lib/README.md --> <!-- file: crates/ksp-store-postgres-lib/README.md -->
<!-- version: 1 --> <!-- version: 11 -->
# ksp-store-postgres-lib # ksp-store-postgres-lib
`ksp-store-postgres-lib` est le backend PostgreSQL physique officiel du Store KSP. `ksp-store-postgres-lib` est le backend PostgreSQL physique officiel du Store KSP.
La crate implémente la fondation connexion/pool/TLS/migrations/health derrière `ksp-store-lib`. Elle dépend directement de `ksp-store-api` mais ne dépend jamais de la façade `ksp-store-lib`. La crate implémente connexion, pool, TLS, migrations, health et persistence RAW derrière `ksp-store-lib`. Elle dépend directement de `ksp-store-api` mais ne dépend jamais de la façade `ksp-store-lib`.
## Responsabilités ## Responsabilités
@@ -15,10 +15,10 @@ La crate possède seule pour PostgreSQL :
- le pool borné `deadpool-postgres` ; - le pool borné `deadpool-postgres` ;
- la policy TLS physique avec Rustls ; - la policy TLS physique avec Rustls ;
- les roots système et le provider cryptographique AWS-LC ; - les roots système et le provider cryptographique AWS-LC ;
- le bootstrap/moteur de migrations privé KSP ; - le bootstrap et le moteur de migrations privé KSP ;
- la table metadata `ksp_store_schema_migrations` ; - la table metadata `ksp_store_schema_migrations` ;
- le sentinel `V000__bootstrap.sql` et son checksum SHA-256 ;
- l'advisory transaction lock borné des migrations ; - l'advisory transaction lock borné des migrations ;
- les schémas physiques et statements privés `RawTransaction` et `RawAccountState` ;
- les snapshots runtime/health sûrs destinés au bridge de façade ; - 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 fermeture explicite du pool et son fallback `Drop` best-effort ;
- la classification d'erreurs backend sans conserver le texte d'erreur PostgreSQL. - la classification d'erreurs backend sans conserver le texte d'erreur PostgreSQL.
@@ -31,7 +31,7 @@ Les applications, jobs et workers KSP ne dépendent normalement pas de cette cra
consumer -> ksp-store-lib -> [feature postgres] ksp-store-postgres-lib consumer -> ksp-store-lib -> [feature postgres] ksp-store-postgres-lib
``` ```
La surface publique de cette crate existe pour le bridge inter-crates et les tests d'intégration backend. Elle ne constitue pas une seconde façade Store. La surface publique de cette crate existe pour le bridge inter-crates et les tests/intégrations backend. Elle ne constitue pas une seconde façade Store.
`ksp-store-postgres-lib` ne réexporte pas `tokio-postgres`, Deadpool ou Rustls. `ksp-store-postgres-lib` ne réexporte pas `tokio-postgres`, Deadpool ou Rustls.
@@ -57,33 +57,15 @@ Disabled
VerifyFull VerifyFull
``` ```
`VerifyFull` exige : `VerifyFull` exige TLS, roots système, certificat valide et vérification de l'identité serveur. Une configuration ne permettant pas de vérifier cette identité, comme `hostaddr` seul, est rejetée.
- TLS ; ## Migrations et schéma
- roots système ;
- certificat valide ;
- vérification de l'identité serveur ;
- aucune dégradation automatique en plaintext.
Les configurations ne permettant pas de vérifier une identité serveur, comme `hostaddr` seul, sont rejetées. 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.
## Migrations 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 fondation embarque uniquement : 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`.
```text
migrations/V000__bootstrap.sql
```
Elle crée la metadata privée :
```text
ksp_store_schema_migrations
```
Le moteur vérifie version, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime.
Aucune migration métier RAW n'appartient à cette fondation.
## Health et erreurs ## Health et erreurs
@@ -100,25 +82,103 @@ Le texte d'erreur PostgreSQL, l'URI, SQL et les valeurs bind ne traversent pas c
## Support PostgreSQL ## Support PostgreSQL
La politique de support de `0.3.2` fixe PostgreSQL 15 comme major minimal. Le test live de fondation refuse explicitement un serveur plus ancien ; le backend ne fixe aucun plafond arbitraire de major PostgreSQL. La compatibilité de migration reste basée sur le schéma KSP. Le major minimal supporté est PostgreSQL 15. Le backend ne fixe aucun plafond arbitraire de major ; la compatibilité opérationnelle reste fondée sur le contrat de schéma KSP et l'introspection du catalogue.
La preuve opérateur réelle et le major effectivement exercé sont conservés dans la matrice de validation, pas dans cette documentation durable. ## Lectures RAW transaction
## Hors périmètre actuel Le backend expose :
La crate ne contient encore : ```text
get_raw_transaction
get_raw_transaction_observation
get_raw_transaction_retention_state
get_raw_transaction_tombstone
```
- aucune implémentation PostgreSQL des capabilities `RawTransaction*` ; Le SQL et les rows restent privés. Le mapping PostgreSQL est fallible et couvre notamment `NUMERIC(20,0) -> u64`, `BIGINT -> u32/u64`, timestamps bornés, bytes de taille fixe et codes de provenance.
- aucune implémentation PostgreSQL des capabilities `RawAccount*` ;
- aucun repository métier RAW ; `Full` lit le payload chaud, `Archived` le reconstruit depuis la relation archive et `Purged` retourne `None`; le tombstone reste accessible séparément.
- aucune table/index métier ;
## Écritures RAW transaction
Le backend expose :
```text
persist_raw_transaction_acquisition
record_raw_transaction_observation
```
L'acquisition canonique et son observation initiale sont commises dans une seule transaction PostgreSQL. Les clés uniques physiques fournissent l'admission idempotente ; après un conflit unique, le backend verrouille la ligne gagnante et compare le contenu réel avant de conclure `AlreadyPresent` ou `Conflict`.
Un tombstone `Purged` compatible produit `SkippedPurged/NotRecorded` en mode normal. `ForceRehydrate` restaure explicitement le payload `Full` et l'observation dans la même transaction.
## Pagination RAW transaction
`list_raw_transactions` parcourt les références canoniques récupérables avec un ordre total `(slot, signature)`. Les tombstones `Purged` sont exclus.
La continuation est une keyset stricte, jamais un `OFFSET`. Le cursor backend V1 est opaque et lié au réseau, à la direction et aux bornes de slots de la query. Store n'impose aucun plafond métier arbitraire à la taille de page ; seule la limitation physique du `LIMIT + 1` PostgreSQL est exposée.
## Rétention RAW transaction
`transition_raw_transaction_retention` applique les transitions physiques :
```text
Full -> Archived -> Purged
```
Le backend verrouille la ligne canonique avec `FOR UPDATE`, compare l'état courant à l'état attendu et applique la mutation atomiquement. L'archivage conserve le payload exact dans la relation archive ; la purge conserve seulement le tombstone minimal.
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 rétention, archive, purge, suppression ou compaction account ;
- aucune orchestration worker/job ; - aucune orchestration worker/job ;
- aucun transport d'acquisition ou decoder Program. - aucun transport d'acquisition ou decoder Program ;
- aucune policy autonome de batch, priorité ou rétention.
## Documentation ## Documentation
- [`USAGE.md`](USAGE.md) — bridge physique et lifecycle ; - [`USAGE.md`](USAGE.md) — guide pratique du bridge physique et de ses capabilities ;
- [`../ksp-store-lib/README.md`](../ksp-store-lib/README.md) — façade runtime destinée aux consumers ; - [`../ksp-store-lib/README.md`](../ksp-store-lib/README.md) — façade runtime destinée aux consumers ;
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture Store ; - [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture Store ;
- [`../../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/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) — preuves déterministes et PostgreSQL réel. - [`../../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/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.

View File

@@ -1,15 +1,13 @@
<!-- file: crates/ksp-store-postgres-lib/USAGE.md --> <!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
<!-- version: 1 --> <!-- version: 11 -->
# Utilisation de ksp-store-postgres-lib # Utilisation de ksp-store-postgres-lib
## 1. Quand utiliser cette crate directement ## 1. Quand dépendre directement du backend
Le consumer applicatif normal utilise `ksp-store-lib`. Le consumer applicatif normal utilise `ksp-store-lib`.
Une dépendance directe à `ksp-store-postgres-lib` est réservée aux composants qui implémentent ou testent le bridge physique PostgreSQL. La crate backend ne doit pas devenir une façade parallèle. Une dépendance directe à `ksp-store-postgres-lib` est réservée aux composants qui implémentent, intègrent ou testent le bridge physique PostgreSQL. Cette crate ne doit pas devenir une façade Store parallèle.
Un tel composant doit déclarer explicitement le backend et `ksp-store-api`, car `PostgresBackendSettings::new` reçoit le `RawNetworkId` backend-neutral sans le réexporter :
```toml ```toml
[dependencies] [dependencies]
@@ -17,16 +15,18 @@ ksp-store-api = { path = "../ksp-store-api" }
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib" } ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib" }
``` ```
## 2. Construire le bridge physique Le backend reçoit les modèles et traits backend-neutral de `ksp-store-api`; il ne réexporte pas `tokio-postgres`, Deadpool ou Rustls.
`PostgresBackendSettings` reçoit des valeurs déjà possédées et validées par la couche appelante. L'URI est sensible et son `Debug` est redacted. ## 2. Construire les settings physiques
Pour distinguer création initiale et mise à jour du schéma, utiliser `PostgresBackendSettings::with_schema_policy` :
```rust ```rust
fn backend_settings( fn backend_settings(
network: ksp_store_api::RawNetworkId, network: ksp_store_api::RawNetworkId,
connection_uri: std::string::String, connection_uri: std::string::String,
) -> ksp_store_postgres_lib::PostgresBackendSettings { ) -> ksp_store_postgres_lib::PostgresBackendSettings {
return ksp_store_postgres_lib::PostgresBackendSettings::new( return ksp_store_postgres_lib::PostgresBackendSettings::with_schema_policy(
network, network,
connection_uri, connection_uri,
8, 8,
@@ -36,15 +36,18 @@ fn backend_settings(
std::time::Duration::from_secs(5), std::time::Duration::from_secs(5),
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
true, true,
true,
std::time::Duration::from_secs(30), std::time::Duration::from_secs(30),
std::time::Duration::from_secs(10), std::time::Duration::from_secs(10),
); );
} }
``` ```
Le backend reçoit un seul `RawNetworkId`. Une instance physique n'est pas un routeur multi-réseau. `schema_autocreate` autorise l'initialisation d'un Store vierge. `schema_autoupdate` autorise les migrations pending et les réparations additives sûres d'une migration déjà enregistrée. Le constructeur `new(..., auto_migrate, ...)` existe pour les callers utilisant encore un switch unique et applique cette valeur aux deux politiques.
## 3. Ouvrir, sonder et fermer L'URI est sensible : elle n'est jamais rendue par `Debug`.
## 3. Ouvrir, sonder et fermer le backend
```rust ```rust
async fn use_backend( async fn use_backend(
@@ -63,93 +66,227 @@ async fn use_backend(
let _waiting = runtime.pool_waiting(); let _waiting = runtime.pool_waiting();
let health = backend.health().await; let health = backend.health().await;
let _ready = health.ready(); let _ready = health.is_ready();
let _migration_version = health.migration_version(); let _migration_version = health.migration_version();
let _pending = health.pending_migration_count(); let _pending = health.pending_migration_count();
let _safe_error_kind = health.last_error_kind(); let _safe_error_kind = health.error_kind();
return backend.close(std::time::Duration::from_secs(5)).await; return backend.close(std::time::Duration::from_secs(5)).await;
} }
``` ```
`open` prouve la connexion et le bootstrap avant de retourner. `close` ferme le pool puis attend son drain dans la deadline fournie. `open` valide la configuration, construit le pool, prouve une connexion et vérifie/applique le bootstrap avant de retourner. `close` ferme le pool et attend son drain dans la deadline fournie.
Une instance physique est liée à un seul `RawNetworkId`.
## 4. Choisir le mode TLS ## 4. Choisir le mode TLS
### `VerifyFull` Pour une connexion PostgreSQL protégée :
À utiliser pour les connexions PostgreSQL protégées :
```rust ```rust
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull
``` ```
Le backend charge les roots système et vérifie certificat + identité serveur. Il rejette une configuration ne fournissant pas d'identité vérifiable. `VerifyFull` impose TLS, les roots système et la vérification de l'identité serveur. Une configuration ne fournissant pas d'identité vérifiable est rejetée.
### `Disabled` Pour une topologie explicitement non chiffrée :
```rust ```rust
ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled
``` ```
Ce mode désactive explicitement TLS. Il ne doit être utilisé que lorsque la topologie de déploiement justifie clairement une connexion non chiffrée. La policy typée choisie par KSP prime sur les paramètres SSL de l'URI.
La valeur typée choisie par KSP prime sur les paramètres SSL de l'URI. ## 5. Lire une transaction et ses métadonnées
## 5. Bootstrap et migrations Les méthodes backend retournent uniquement des modèles `ksp-store-api`.
Le backend embarque son propre moteur de migrations. Le seul artefact initial est :
```text
migrations/V000__bootstrap.sql
```
Le bootstrap maintient :
```text
ksp_store_schema_migrations
version
name
checksum SHA-256
```
Le runner est transactionnel et sérialisé par advisory transaction lock. Une divergence de checksum/nom/version ou une history plus récente est terminale ; aucun down migration automatique n'est exécuté.
`auto_migrate = false` permet de vérifier l'état sans appliquer de migration pending.
## 6. Classifier les erreurs sans fuite
```rust ```rust
match error.kind() { async fn read_transaction_state(
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => {} backend: &ksp_store_postgres_lib::PostgresBackend,
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => {} reference: &ksp_store_api::RawTransactionReference,
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => {} ) -> std::result::Result<std::option::Option<ksp_store_api::RawTransaction>, ksp_store_postgres_lib::PostgresBackendError> {
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => {} let retention = backend.get_raw_transaction_retention_state(reference).await;
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => {} if let std::result::Result::Err(error) = retention {
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {} return std::result::Result::Err(error);
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => {} }
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => {} let tombstone = backend.get_raw_transaction_tombstone(reference).await;
_ => {} if let std::result::Result::Err(error) = tombstone {
return std::result::Result::Err(error);
}
return backend.get_raw_transaction(reference).await;
} }
let _safe_phase = error.phase();
``` ```
Ne pas reconstruire un diagnostic utilisateur à partir de l'erreur brute PostgreSQL : cette erreur n'est volontairement pas conservée par le bridge. `Full` lit le payload chaud, `Archived` reconstruit le payload depuis l'archive et `Purged` retourne `None`. Un tombstone purgé reste lisible séparément.
## 7. Ce que cette crate ne permet pas encore Un réseau différent de celui du backend est rejeté avant acquisition d'un client du pool.
La fondation physique n'implémente pas les traits `RawTransaction*` ou `RawAccount*` de `ksp-store-api`. ## 6. Lire une observation
Un backend ouvert et healthy prouve uniquement : ```rust
async fn read_observation(
```text backend: &ksp_store_postgres_lib::PostgresBackend,
connexion/pool key: &ksp_store_api::RawObservationKey,
TLS selon policy ) -> std::result::Result<std::option::Option<ksp_store_api::RawTransactionObservation>, ksp_store_postgres_lib::PostgresBackendError> {
bootstrap/history return backend.get_raw_transaction_observation(key).await;
health/readiness }
close borné
``` ```
Il ne prouve aucune persistence métier RAW. Les rows PostgreSQL, SQLSTATE, statements et valeurs de bind ne traversent jamais cette API.
## 7. Persister une acquisition canonique
```rust
async fn persist_acquisition(
backend: &ksp_store_postgres_lib::PostgresBackend,
transaction: ksp_store_api::RawTransaction,
observation: ksp_store_api::RawTransactionObservation,
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
return backend
.persist_raw_transaction_acquisition(
transaction,
observation,
ksp_store_api::RawTransactionAcquisitionMode::Normal,
)
.await;
}
```
L'opération est atomique : le canonique et son observation initiale sont tous deux durables ou aucun ne l'est. Une identité déjà présente avec un contenu identique est idempotente ; un contenu divergent retourne `PostgresBackendErrorKind::Conflict` sans overwrite silencieux.
Pour un tombstone purgé compatible, le mode `Normal` ne restaure pas le payload. `ForceRehydrate` doit être demandé explicitement pour rétablir un payload `Full`.
## 8. Ajouter une observation à un canonique existant
```rust
async fn record_observation(
backend: &ksp_store_postgres_lib::PostgresBackend,
observation: ksp_store_api::RawTransactionObservation,
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
return backend.record_raw_transaction_observation(observation).await;
}
```
Cette opération ne crée jamais la transaction canonique. Une référence absente retourne `ReferenceNotFound`; une transaction purgée produit l'outcome `NotRecorded` prévu par l'API.
## 9. Paginer les transactions
```rust
async fn list_transactions(
backend: &ksp_store_postgres_lib::PostgresBackend,
query: &ksp_store_api::RawTransactionQuery,
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawTransactionReference>, ksp_store_postgres_lib::PostgresBackendError> {
return backend.list_raw_transactions(query).await;
}
```
La navigation est keyset sur `(slot, signature)` et exclut les tombstones `Purged`. Le cursor retourné est opaque et lié au réseau, à la direction et aux bornes de slots de la query qui l'a produit.
Le backend n'utilise pas `OFFSET` et n'impose pas de plafond métier arbitraire. La seule borne exposée ici provient de la représentation physique de `LIMIT + 1` dans PostgreSQL.
## 10. Appliquer une transition de rétention
```rust
async fn apply_retention(
backend: &ksp_store_postgres_lib::PostgresBackend,
transition: ksp_store_api::RawTransactionRetentionTransition,
) -> std::result::Result<ksp_store_api::RawRetentionWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
return backend.transition_raw_transaction_retention(transition).await;
}
```
Le backend applique la transition choisie par le caller ; il ne décide pas de la policy d'éligibilité. Les transitions physiques prises en charge sont `Full -> Archived` puis `Archived -> Purged`.
Une transition impliquant `Compacted` est refusée avec `PostgresBackendErrorKind::RetentionCompactionUnsupported` tant qu'aucune représentation compactée réelle n'est disponible.
## 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) {
match error.kind() {
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::Conflict => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::DataInvalid => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::PageLimitUnsupported => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::RetentionCompactionUnsupported => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::WriteFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::WrongNetwork => {}
_ => {}
}
let _safe_phase = error.phase();
}
```
`PostgresBackendError` conserve uniquement une classification KSP et une phase statique. Ne pas reconstruire de diagnostic utilisateur à partir d'une erreur brute PostgreSQL.
## 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 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.

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pk_ksp_store_identity'
AND conrelid = to_regclass('ksp_store_identity')
) THEN
ALTER TABLE ksp_store_identity ADD CONSTRAINT pk_ksp_store_identity PRIMARY KEY (singleton);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_store_identity_singleton'
AND conrelid = to_regclass('ksp_store_identity')
) THEN
ALTER TABLE ksp_store_identity ADD CONSTRAINT ck_ksp_store_identity_singleton CHECK (singleton = 1);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_store_identity_network'
AND conrelid = to_regclass('ksp_store_identity')
) THEN
ALTER TABLE ksp_store_identity ADD CONSTRAINT ck_ksp_store_identity_network CHECK (
octet_length(network) >= 1 AND octet_length(network) <= 128
AND network ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pk_ksp_raw_transactions'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT pk_ksp_raw_transactions PRIMARY KEY (signature);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_signature'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_signature CHECK (octet_length(signature) = 64);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_slot'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_slot CHECK (slot >= 0 AND slot <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_block_time'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_block_time CHECK (
block_time_unix_millis IS NULL
OR block_time_unix_millis >= 0 AND block_time_unix_millis <= 253402300799999
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_format_id'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_format_id CHECK (
octet_length(format_id) >= 1 AND octet_length(format_id) <= 128
AND format_id ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_format_version'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_format_version CHECK (format_version >= 1 AND format_version <= 4294967295);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_content_hash'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_content_hash CHECK (octet_length(content_hash) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_payload'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_payload CHECK (
payload IS NULL
OR octet_length(payload) >= 1 AND octet_length(payload) <= 16777216
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_retention_state'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_retention_state CHECK ((retention_state = 'full' OR retention_state = 'archived' OR retention_state = 'purged'));
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_payload_state'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_payload_state CHECK (
(retention_state = 'full' AND payload IS NOT NULL)
OR ((retention_state = 'archived' OR retention_state = 'purged') AND payload IS NULL)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transactions_purged_block_time'
AND conrelid = to_regclass('ksp_raw_transactions')
) THEN
ALTER TABLE ksp_raw_transactions ADD CONSTRAINT ck_ksp_raw_transactions_purged_block_time CHECK (
retention_state <> 'purged'
OR block_time_unix_millis IS NULL
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pk_ksp_raw_transaction_observations'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT pk_ksp_raw_transaction_observations PRIMARY KEY (observation_key);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_ksp_raw_transaction_observations_transaction'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT fk_ksp_raw_transaction_observations_transaction FOREIGN KEY (transaction_signature) REFERENCES ksp_raw_transactions(signature) ON DELETE RESTRICT;
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_key'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_key CHECK (octet_length(observation_key) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_signature'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_signature CHECK (octet_length(transaction_signature) = 64);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_provider'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_provider CHECK (
octet_length(provider) >= 1 AND octet_length(provider) <= 128
AND provider ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_protocol'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_protocol CHECK (
octet_length(protocol) >= 1 AND octet_length(protocol) <= 128
AND protocol ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_method'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_origin'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_origin CHECK ((origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay'));
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_received_at'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_received_at CHECK (received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_capture_session'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_commitment'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_endpoint'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_filter'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_observed_at'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_time_order'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_time_order CHECK (
observed_at_unix_millis IS NULL
OR observed_at_unix_millis <= received_at_unix_millis
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_source_hash'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_observations_source_hash CHECK (
source_payload_hash IS NULL
OR octet_length(source_payload_hash) = 32
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_observations_source_size'
AND conrelid = to_regclass('ksp_raw_transaction_observations')
) THEN
ALTER TABLE ksp_raw_transaction_observations ADD CONSTRAINT ck_ksp_raw_transaction_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$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pk_ksp_raw_transaction_archive_payloads'
AND conrelid = to_regclass('ksp_raw_transaction_archive_payloads')
) THEN
ALTER TABLE ksp_raw_transaction_archive_payloads ADD CONSTRAINT pk_ksp_raw_transaction_archive_payloads PRIMARY KEY (signature);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_ksp_raw_transaction_archive_payloads_transaction'
AND conrelid = to_regclass('ksp_raw_transaction_archive_payloads')
) THEN
ALTER TABLE ksp_raw_transaction_archive_payloads ADD CONSTRAINT fk_ksp_raw_transaction_archive_payloads_transaction FOREIGN KEY (signature) REFERENCES ksp_raw_transactions(signature) ON DELETE RESTRICT;
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_archive_payloads_signature'
AND conrelid = to_regclass('ksp_raw_transaction_archive_payloads')
) THEN
ALTER TABLE ksp_raw_transaction_archive_payloads ADD CONSTRAINT ck_ksp_raw_transaction_archive_payloads_signature CHECK (octet_length(signature) = 64);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_transaction_archive_payloads_payload'
AND conrelid = to_regclass('ksp_raw_transaction_archive_payloads')
) THEN
ALTER TABLE ksp_raw_transaction_archive_payloads ADD CONSTRAINT ck_ksp_raw_transaction_archive_payloads_payload CHECK (octet_length(payload) >= 1 AND octet_length(payload) <= 16777216);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,3 @@
CREATE INDEX IF NOT EXISTS ix_ksp_raw_transactions_slot_signature
ON ksp_raw_transactions (slot, signature)
WHERE retention_state <> 'purged';

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS ksp_store_identity (
singleton SMALLINT NOT NULL,
network TEXT NOT NULL
);
ALTER TABLE ksp_store_identity ADD COLUMN IF NOT EXISTS singleton SMALLINT NOT NULL;
ALTER TABLE ksp_store_identity ADD COLUMN IF NOT EXISTS network TEXT NOT NULL;

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS ksp_raw_transactions (
signature BYTEA NOT NULL,
slot NUMERIC(20, 0) NOT NULL,
block_time_unix_millis BIGINT NULL,
format_id TEXT NOT NULL,
format_version BIGINT NOT NULL,
content_hash BYTEA NOT NULL,
payload BYTEA NULL,
retention_state TEXT NOT NULL
);
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS signature BYTEA NOT NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS slot NUMERIC(20, 0) NOT NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS block_time_unix_millis BIGINT NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS format_id TEXT NOT NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS format_version BIGINT NOT NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS content_hash BYTEA NOT NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS payload BYTEA NULL;
ALTER TABLE ksp_raw_transactions ADD COLUMN IF NOT EXISTS retention_state TEXT NOT NULL;

View File

@@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS ksp_raw_transaction_observations (
observation_key BYTEA NOT NULL,
transaction_signature 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
);
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS observation_key BYTEA NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS transaction_signature BYTEA NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS protocol TEXT NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS acquisition_method TEXT NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS origin TEXT NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS received_at_unix_millis BIGINT NOT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS capture_session_id TEXT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS commitment TEXT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS endpoint_id TEXT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS filter_id TEXT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS observed_at_unix_millis BIGINT NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS source_payload_hash BYTEA NULL;
ALTER TABLE ksp_raw_transaction_observations ADD COLUMN IF NOT EXISTS source_payload_size_bytes BIGINT NULL;

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS ksp_raw_transaction_archive_payloads (
signature BYTEA NOT NULL,
payload BYTEA NOT NULL
);
ALTER TABLE ksp_raw_transaction_archive_payloads ADD COLUMN IF NOT EXISTS signature BYTEA NOT NULL;
ALTER TABLE ksp_raw_transaction_archive_payloads ADD COLUMN IF NOT EXISTS payload BYTEA NOT NULL;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
// file: crates/ksp-store-postgres-lib/src/error.rs // file: crates/ksp-store-postgres-lib/src/error.rs
// version: 3 // version: 8
/// Stable KSP error code reserved for PostgreSQL retention transitions that require unsupported physical compaction.
pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode =
ksp_store_api::ErrorCode::new("store", "postgres_retention_compaction_unsupported");
/// Safe backend-local classification used by the Store facade for stable error mapping. /// Safe backend-local classification used by the Store facade for stable error mapping.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -13,16 +17,34 @@ pub enum PostgresBackendErrorKind {
PoolTimeout, PoolTimeout,
/// A lightweight PostgreSQL health/readiness probe failed without exposing server text or SQL. /// A lightweight PostgreSQL health/readiness probe failed without exposing server text or SQL.
HealthFailed, HealthFailed,
/// A canonical RAW identity or observation key already exists with divergent durable content.
Conflict,
/// PostgreSQL returned stored RAW data that cannot be represented by the stable Store API contract.
DataInvalid,
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL. /// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
MigrationFailed, MigrationFailed,
/// The requested RAW page size cannot be represented by PostgreSQL LIMIT plus the continuation probe row.
PageLimitUnsupported,
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history. /// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
MigrationMismatch, MigrationMismatch,
/// A bounded backend-private RAW query or opaque cursor is invalid for the requested navigation context.
QueryInvalid,
/// A PostgreSQL RAW read statement failed without exposing server text, SQL or bind values.
ReadFailed,
/// A RAW write requires an existing canonical reference that is not durable.
ReferenceNotFound,
/// The requested RAW retention transition requires a compacted representation unsupported by PostgreSQL.
RetentionCompactionUnsupported,
/// The database schema history contains a migration newer than this runtime understands. /// The database schema history contains a migration newer than this runtime understands.
SchemaNewer, SchemaNewer,
/// Explicit backend shutdown did not drain inside the supplied deadline. /// Explicit backend shutdown did not drain inside the supplied deadline.
ShutdownTimeout, ShutdownTimeout,
/// Verified TLS configuration or negotiation could not be established. /// Verified TLS configuration or negotiation could not be established.
TlsFailed, TlsFailed,
/// A PostgreSQL RAW write statement or transaction failed without exposing server text, SQL or bind values.
WriteFailed,
/// A network-scoped RAW operation targeted a network different from the backend binding.
WrongNetwork,
} }
/// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase. /// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs // file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 5 // version: 21
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -7,10 +7,32 @@
//! Official PostgreSQL backend implementation for KSP Store. //! Official PostgreSQL backend implementation for KSP Store.
//! //!
//! `0.3.2-pre.007` owns the physical `tokio-postgres` connection, bounded //! The backend owns the physical `tokio-postgres` connection, bounded Deadpool
//! Deadpool pool, explicit Rustls TLS policy, private KSP migration/bootstrap //! pool, explicit Rustls TLS policy, private KSP migration/bootstrap engine and
//! engine and safe lightweight health/readiness probe. Business persistence //! safe lightweight health/readiness probe. `0.3.3-pre.003-fix.001` splits
//! remains absent from this foundation release. //! migrations into versioned physical resources and verifies the effective
//! PostgreSQL schema contract before readiness. `0.3.3-pre.004` adds exact
//! backend-private RAW transaction/observation/retention read mapping.
//! `0.3.3-pre.005` adds atomic canonical/observation writes, real idempotence
//! checks and safe conflict classification without exposing PostgreSQL rows or
//! SQL through the public bridge. `0.3.3-pre.006` adds deterministic keyset
//! pagination with a fixed opaque cursor bound to network, range and direction.
//! `0.3.3-pre.007` adds atomic `Full -> Archived -> Purged` retention transitions
//! with compare-and-transition outcomes and explicit rejection of `Compacted`.
//! `0.3.3-pre.008` implements all six `RawTransaction*` capabilities directly on
//! `PostgresBackend` while preserving the existing narrow backend bridge.
//! `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 //! 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 //! common facade consumes only this crate's narrow backend bridge and never
@@ -20,8 +42,13 @@ mod constants;
mod error; mod error;
mod health; mod health;
mod migration; mod migration;
mod raw_account;
mod raw_transaction;
mod runtime; mod runtime;
mod schema;
/// Stable KSP error code for unsupported PostgreSQL retention compaction.
pub use self::error::ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED;
/// Safe backend-local error returned to the common Store facade. /// Safe backend-local error returned to the common Store facade.
pub use self::error::PostgresBackendError; pub use self::error::PostgresBackendError;
/// Safe backend-local error classification used by the common Store facade. /// Safe backend-local error classification used by the common Store facade.
@@ -45,9 +72,65 @@ pub(crate) use self::health::probe_health;
pub(crate) use self::migration::bootstrap; pub(crate) use self::migration::bootstrap;
/// Current embedded migration version consumed by the private health probe. /// Current embedded migration version consumed by the private health probe.
pub(crate) use self::migration::current_migration_version; 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.
pub(crate) use self::raw_transaction::cursor::encode_raw_transaction_cursor;
/// Private physical page-limit converter consumed by the physical RAW module.
pub(crate) use self::raw_transaction::cursor::raw_transaction_physical_page_limit;
/// Private RAW transaction reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::get_raw_transaction;
/// Private RAW transaction observation reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::get_raw_transaction_observation;
/// Private RAW transaction retention-state reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::get_raw_transaction_retention_state;
/// Private RAW transaction tombstone reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::get_raw_transaction_tombstone;
/// Private RAW transaction list reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::list_raw_transactions;
/// Private atomic RAW transaction acquisition writer consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::persist_raw_transaction_acquisition;
/// Private additional RAW transaction observation writer consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::record_raw_transaction_observation;
/// Private RAW transaction retention transition writer consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::transition_raw_transaction_retention;
/// Private Deadpool error mapper shared with the health probe. /// Private Deadpool error mapper shared with the health probe.
pub(crate) use self::runtime::map_pool_error; pub(crate) use self::runtime::map_pool_error;
/// Private Deadpool status projector shared with the health probe. /// Private Deadpool status projector shared with the health probe.
pub(crate) use self::runtime::runtime_snapshot_from_status; pub(crate) use self::runtime::runtime_snapshot_from_status;
/// Private physical schema resource descriptor consumed by the migration engine.
pub(crate) use self::schema::SchemaResource;
/// Private physical schema resource compatibility state consumed by the migration engine.
pub(crate) use self::schema::SchemaResourceState;
/// Private V000 schema resource inventory consumed by the migration engine.
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 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; const _: &str = crate::TRACING_TARGET;

View File

@@ -1,15 +1,41 @@
// file: crates/ksp-store-postgres-lib/src/migration.rs // file: crates/ksp-store-postgres-lib/src/migration.rs
// version: 2 // version: 10
use sha2::Digest; // rust-rules: trait-import use sha2::Digest; // rust-rules: trait-import
const ADVISORY_LOCK_KEY: i64 = 0x4b53_5053_544f_5245; const ADVISORY_LOCK_KEY: i64 = 0x4b53_5053_544f_5245;
const BOOTSTRAP_MIGRATION_NAME: &str = "bootstrap"; const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
const BOOTSTRAP_MIGRATION_SQL: &str = include_str!("../migrations/V000__bootstrap.sql"); EmbeddedMigration {
const BOOTSTRAP_MIGRATION_VERSION: i64 = 0; 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,
},
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 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_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_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; const LOCK_POLL_INTERVAL_MS: u64 = 25;
const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS ( const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
SELECT 1 FROM information_schema.tables SELECT 1 FROM information_schema.tables
@@ -17,22 +43,8 @@ const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
AND table_name = 'ksp_store_schema_migrations' AND table_name = 'ksp_store_schema_migrations'
AND table_type = 'BASE TABLE' AND table_type = 'BASE TABLE'
)"#; )"#;
const METADATA_PRIMARY_KEY_SQL: &str = r#"SELECT COUNT(*)::BIGINT,
COUNT(*) FILTER (WHERE kcu.column_name = 'version')::BIGINT
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_catalog = kcu.constraint_catalog
AND tc.constraint_schema = kcu.constraint_schema
AND tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = current_schema()
AND tc.table_name = 'ksp_store_schema_migrations'
AND tc.constraint_type = 'PRIMARY KEY'"#;
const METADATA_SHAPE_SQL: &str = r#"SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'ksp_store_schema_migrations'
ORDER BY ordinal_position"#;
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)"; const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
const V002_PROVISIONAL_CHECKSUM_PRE_002: &str = "30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55";
struct AppliedMigration { struct AppliedMigration {
checksum: std::string::String, checksum: std::string::String,
@@ -40,20 +52,64 @@ struct AppliedMigration {
version: i64, version: i64,
} }
#[derive(Clone, Copy)]
enum MigrationChecksum {
LegacySql(&'static str),
Resources,
}
#[derive(Clone, Copy)]
struct EmbeddedMigration {
checksum: MigrationChecksum,
hook: MigrationHook,
name: &'static str,
previous_checksums: &'static [&'static str],
resources: &'static [crate::SchemaResource],
version: i64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MigrationHook {
None,
StoreIdentity,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MigrationHookContext {
AppliedNow,
Existing,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SchemaMutationMode {
Create,
Update,
}
/// Returns the latest migration version embedded by this backend runtime. /// Returns the latest migration version embedded by this backend runtime.
#[must_use] #[must_use]
pub(crate) const fn current_migration_version() -> i64 { pub(crate) const fn current_migration_version() -> i64 {
return BOOTSTRAP_MIGRATION_VERSION; return EMBEDDED_MIGRATIONS[EMBEDDED_MIGRATIONS.len() - 1].version;
} }
/// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client. /// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client.
pub(crate) async fn bootstrap( pub(crate) async fn bootstrap(
client: &mut deadpool_postgres::Client, client: &mut deadpool_postgres::Client,
auto_migrate: bool, network: &ksp_store_api::RawNetworkId,
schema_autocreate: bool,
schema_autoupdate: bool,
migration_timeout: std::time::Duration, migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> { ) -> std::result::Result<(), crate::PostgresBackendError> {
let bounded = tokio::time::timeout(migration_timeout, bootstrap_inner(client, auto_migrate, migration_timeout, migration_lock_timeout)).await; let registry_result = validate_embedded_registry(EMBEDDED_MIGRATIONS);
if let std::result::Result::Err(error) = registry_result {
return std::result::Result::Err(error);
}
let bounded = tokio::time::timeout(
migration_timeout,
bootstrap_inner(client, network, schema_autocreate, schema_autoupdate, migration_timeout, migration_lock_timeout),
)
.await;
return match bounded { return match bounded {
std::result::Result::Ok(result) => result, std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => { std::result::Result::Err(_) => {
@@ -64,7 +120,9 @@ pub(crate) async fn bootstrap(
async fn bootstrap_inner( async fn bootstrap_inner(
client: &mut deadpool_postgres::Client, client: &mut deadpool_postgres::Client,
auto_migrate: bool, network: &ksp_store_api::RawNetworkId,
schema_autocreate: bool,
schema_autoupdate: bool,
migration_timeout: std::time::Duration, migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> { ) -> std::result::Result<(), crate::PostgresBackendError> {
@@ -88,41 +146,65 @@ async fn bootstrap_inner(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let checksum = bootstrap_checksum(); let (next_index, mutation_mode, applied_history) = if metadata_exists {
if !metadata_exists { let metadata_result = crate::inspect_resource(&transaction, &crate::V000_RESOURCES[0]).await;
if !auto_migrate { match metadata_result {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_pending")); std::result::Result::Ok(crate::SchemaResourceState::Compatible) => {},
} std::result::Result::Ok(crate::SchemaResourceState::Missing | crate::SchemaResourceState::Incompatible) => {
let create_result = transaction.batch_execute(BOOTSTRAP_MIGRATION_SQL).await; log_schema_block("metadata_incompatible");
if create_result.is_err() { return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_incompatible"));
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_create"));
}
let shape_result = verify_metadata_shape(&transaction).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
}
let insert_result = transaction.execute(HISTORY_INSERT_SQL, &[&BOOTSTRAP_MIGRATION_VERSION, &BOOTSTRAP_MIGRATION_NAME, &checksum]).await;
match insert_result {
std::result::Result::Ok(1) => {},
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_insert"));
}, },
} std::result::Result::Err(error) => return std::result::Result::Err(error),
} else {
let shape_result = verify_metadata_shape(&transaction).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
} }
let history_result = load_history(&transaction).await; let history_result = load_history(&transaction).await;
let history = match history_result { let history = match history_result {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let validation_result = validate_history(history.as_slice(), checksum.as_str()); let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS, schema_autoupdate);
if let std::result::Result::Err(error) = validation_result { 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, 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_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),
};
if managed_objects_exist && !schema_autoupdate {
log_schema_block("schema_adoption_disabled");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_adoption_disabled"));
}
(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); 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);
}
if next_index < EMBEDDED_MIGRATIONS.len() && mutation_mode == SchemaMutationMode::Update && !schema_autoupdate {
log_schema_block("migration_pending");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_pending"));
}
let apply_result = apply_pending_migrations(&transaction, network, next_index, mutation_mode).await;
if let std::result::Result::Err(error) = apply_result {
return std::result::Result::Err(error);
}
let commit_result = transaction.commit().await; let commit_result = transaction.commit().await;
return match commit_result { return match commit_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()), std::result::Result::Ok(()) => std::result::Result::Ok(()),
@@ -171,86 +253,104 @@ async fn acquire_advisory_lock(
} }
} }
async fn set_statement_timeout( async fn apply_migration(
transaction: &deadpool_postgres::Transaction<'_>, transaction: &deadpool_postgres::Transaction<'_>,
timeout: std::time::Duration, network: &ksp_store_api::RawNetworkId,
migration: &EmbeddedMigration,
mutation_mode: SchemaMutationMode,
) -> std::result::Result<(), crate::PostgresBackendError> { ) -> std::result::Result<(), crate::PostgresBackendError> {
let timeout_value = format!("{}ms", timeout.as_millis()); for resource in migration.resources {
let result = transaction.query_one(SET_STATEMENT_TIMEOUT_SQL, &[&timeout_value]).await; let result = ensure_resource(transaction, resource, mutation_mode, false).await;
return match result { if let std::result::Result::Err(error) = result {
std::result::Result::Ok(_) => std::result::Result::Ok(()), return std::result::Result::Err(error);
std::result::Result::Err(_) => { }
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "statement_timeout")) }
let contract_result = verify_migration_contract(transaction, migration.version).await;
if let std::result::Result::Err(error) = contract_result {
return std::result::Result::Err(error);
}
let hook_result = run_migration_hook(transaction, network, migration.hook, MigrationHookContext::AppliedNow).await;
if let std::result::Result::Err(error) = hook_result {
return std::result::Result::Err(error);
}
let checksum = migration_checksum(migration);
let insert_result = transaction.execute(HISTORY_INSERT_SQL, &[&migration.version, &migration.name, &checksum]).await;
return match insert_result {
std::result::Result::Ok(1) => std::result::Result::Ok(()),
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_insert"))
}, },
}; };
} }
async fn metadata_exists(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> { async fn apply_pending_migrations(
let result = transaction.query_one(METADATA_EXISTS_SQL, &[]).await; transaction: &deadpool_postgres::Transaction<'_>,
let row = match result { network: &ksp_store_api::RawNetworkId,
std::result::Result::Ok(value) => value, next_index: usize,
std::result::Result::Err(_) => { mutation_mode: SchemaMutationMode,
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe")); ) -> std::result::Result<(), crate::PostgresBackendError> {
}, let mut index = next_index;
}; while index < EMBEDDED_MIGRATIONS.len() {
return match row.try_get::<usize, bool>(0) { let migration = &EMBEDDED_MIGRATIONS[index];
std::result::Result::Ok(value) => std::result::Result::Ok(value), let result = apply_migration(transaction, network, migration, mutation_mode).await;
std::result::Result::Err(_) => { if let std::result::Result::Err(error) = result {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe_decode")) return std::result::Result::Err(error);
}, }
}; index += 1;
}
return std::result::Result::Ok(());
} }
async fn verify_metadata_shape(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<(), crate::PostgresBackendError> { async fn ensure_resource(
let result = transaction.query(METADATA_SHAPE_SQL, &[]).await; transaction: &deadpool_postgres::Transaction<'_>,
let rows = match result { resource: &crate::SchemaResource,
mutation_mode: SchemaMutationMode,
applied_history: bool,
) -> std::result::Result<(), crate::PostgresBackendError> {
let state_result = crate::inspect_resource(transaction, resource).await;
let state = match state_result {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => { std::result::Result::Err(error) => return std::result::Result::Err(error),
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape"));
},
}; };
let expected = [("version", "bigint", "NO"), ("name", "text", "NO"), ("checksum", "text", "NO"), ("applied_at", "timestamp with time zone", "NO")]; match state {
let mut found = [false; 4]; crate::SchemaResourceState::Compatible => return std::result::Result::Ok(()),
for row in rows { crate::SchemaResourceState::Incompatible => {
let column_result = row.try_get::<usize, std::string::String>(0); log_schema_resource_block(resource.id, "incompatible");
let data_type_result = row.try_get::<usize, std::string::String>(1); return std::result::Result::Err(crate::PostgresBackendError::new(
let nullable_result = row.try_get::<usize, std::string::String>(2); crate::PostgresBackendErrorKind::MigrationMismatch,
let (column, data_type, nullable) = match (column_result, data_type_result, nullable_result) { "schema_resource_incompatible",
(std::result::Result::Ok(column), std::result::Result::Ok(data_type), std::result::Result::Ok(nullable)) => (column, data_type, nullable), ));
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape_decode"));
},
};
for (index, expected_row) in expected.iter().enumerate() {
if column == expected_row.0 {
if found[index] || data_type != expected_row.1 || nullable != expected_row.2 {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
}
found[index] = true;
break;
}
}
}
for required in found {
if !required {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
}
}
let key_result = transaction.query_one(METADATA_PRIMARY_KEY_SQL, &[]).await;
let key_row = match key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key"));
}, },
}; crate::SchemaResourceState::Missing => {},
let key_count = key_row.try_get::<usize, i64>(0); }
let version_count = key_row.try_get::<usize, i64>(1); if applied_history && !resource.repair_existing {
return match (key_count, version_count) { log_schema_resource_block(resource.id, "repair_forbidden");
(std::result::Result::Ok(1), std::result::Result::Ok(1)) => std::result::Result::Ok(()), return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "schema_resource_missing"));
(std::result::Result::Ok(_), std::result::Result::Ok(_)) => { }
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_primary_key")) if applied_history && mutation_mode != SchemaMutationMode::Update {
log_schema_resource_block(resource.id, "repair_mode_invalid");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "schema_resource_missing"));
}
if applied_history {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
resource_id = resource.id,
"repairing missing PostgreSQL Store schema resource under schema_autoupdate policy"
);
}
let execute_result = transaction.batch_execute(resource.sql).await;
if execute_result.is_err() {
log_schema_resource_block(resource.id, "apply_failed");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_resource_apply"));
}
let verified_result = crate::inspect_resource(transaction, resource).await;
return match verified_result {
std::result::Result::Ok(crate::SchemaResourceState::Compatible) => std::result::Result::Ok(()),
std::result::Result::Ok(crate::SchemaResourceState::Missing | crate::SchemaResourceState::Incompatible) => {
log_schema_resource_block(resource.id, "post_apply_incompatible");
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, resource.id))
}, },
_ => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key_decode")), std::result::Result::Err(error) => std::result::Result::Err(error),
}; };
} }
@@ -279,31 +379,46 @@ async fn load_history(transaction: &deadpool_postgres::Transaction<'_>) -> std::
return std::result::Result::Ok(history); return std::result::Result::Ok(history);
} }
fn validate_history(history: &[AppliedMigration], expected_checksum: &str) -> std::result::Result<(), crate::PostgresBackendError> { async fn metadata_exists(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
let mut sentinel_found = false; let result = transaction.query_one(METADATA_EXISTS_SQL, &[]).await;
for applied in history { let row = match result {
if applied.version > BOOTSTRAP_MIGRATION_VERSION { std::result::Result::Ok(value) => value,
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::SchemaNewer, "history_newer")); std::result::Result::Err(_) => {
} return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe"));
if applied.version < BOOTSTRAP_MIGRATION_VERSION { },
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_unknown")); };
} return match row.try_get::<usize, bool>(0) {
if applied.name != BOOTSTRAP_MIGRATION_NAME || applied.checksum != expected_checksum { std::result::Result::Ok(value) => std::result::Result::Ok(value),
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged")); std::result::Result::Err(_) => {
} std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe_decode"))
sentinel_found = true; },
} };
if !sentinel_found {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
}
return std::result::Result::Ok(());
} }
fn bootstrap_checksum() -> std::string::String { fn migration_checksum(migration: &EmbeddedMigration) -> std::string::String {
return match migration.checksum {
MigrationChecksum::LegacySql(sql) => checksum_bytes(sql.as_bytes()),
MigrationChecksum::Resources => {
let mut hasher = sha2::Sha256::new();
hasher.update(b"ksp-migration-resources-v1\0");
for resource in migration.resources {
hasher.update(resource.id.as_bytes());
hasher.update([0]);
hasher.update(resource.sql.as_bytes());
hasher.update([0]);
}
return encode_digest(hasher.finalize().as_slice());
},
};
}
fn checksum_bytes(bytes: &[u8]) -> std::string::String {
let mut hasher = sha2::Sha256::new(); let mut hasher = sha2::Sha256::new();
hasher.update(BOOTSTRAP_MIGRATION_SQL.as_bytes()); hasher.update(bytes);
let digest = hasher.finalize(); return encode_digest(hasher.finalize().as_slice());
let bytes = digest.as_slice(); }
fn encode_digest(bytes: &[u8]) -> std::string::String {
let mut encoded = std::string::String::with_capacity(bytes.len() * 2); let mut encoded = std::string::String::with_capacity(bytes.len() * 2);
for byte in bytes { for byte in bytes {
let value = *byte; let value = *byte;
@@ -313,6 +428,283 @@ fn bootstrap_checksum() -> std::string::String {
return encoded; return encoded;
} }
async fn run_applied_migration_hooks(
transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
applied_count: usize,
) -> std::result::Result<(), crate::PostgresBackendError> {
let mut index = 0_usize;
while index < applied_count {
let migration = &EMBEDDED_MIGRATIONS[index];
let result = run_migration_hook(transaction, network, migration.hook, MigrationHookContext::Existing).await;
if let std::result::Result::Err(error) = result {
return std::result::Result::Err(error);
}
index += 1;
}
return std::result::Result::Ok(());
}
async fn run_migration_hook(
transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
hook: MigrationHook,
context: MigrationHookContext,
) -> std::result::Result<(), crate::PostgresBackendError> {
return match hook {
MigrationHook::None => std::result::Result::Ok(()),
MigrationHook::StoreIdentity => bind_store_identity(transaction, network, context).await,
};
}
async fn bind_store_identity(
transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
context: MigrationHookContext,
) -> std::result::Result<(), crate::PostgresBackendError> {
let first_read = load_store_identity(transaction).await;
let first = match first_read {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if first.is_empty() && context == MigrationHookContext::AppliedNow {
let insert_result = transaction.execute(IDENTITY_INSERT_SQL, &[&network.as_str()]).await;
match insert_result {
std::result::Result::Ok(1) => {},
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "store_identity_insert"));
},
}
let second_read = load_store_identity(transaction).await;
let second = match second_read {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return validate_store_identity(second.as_slice(), network);
}
return validate_store_identity(first.as_slice(), network);
}
async fn load_store_identity(
transaction: &deadpool_postgres::Transaction<'_>,
) -> std::result::Result<std::vec::Vec<tokio_postgres::Row>, crate::PostgresBackendError> {
let rows_result = transaction.query(IDENTITY_LOAD_SQL, &[]).await;
return match rows_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "store_identity_read"))
},
};
}
fn validate_store_identity(rows: &[tokio_postgres::Row], network: &ksp_store_api::RawNetworkId) -> std::result::Result<(), crate::PostgresBackendError> {
if rows.len() != 1 {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "store_identity_count"));
}
let row = &rows[0];
let singleton_result = row.try_get::<usize, i16>(0);
let network_result = row.try_get::<usize, std::string::String>(1);
let (singleton, stored_network) = match (singleton_result, network_result) {
(std::result::Result::Ok(singleton), std::result::Result::Ok(stored_network)) => (singleton, stored_network),
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "store_identity_decode"));
},
};
if singleton != 1 {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "store_identity_singleton"));
}
let stored_network = match ksp_store_api::RawNetworkId::new(stored_network) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "store_identity_network"));
},
};
if stored_network.as_str() != network.as_str() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "store_identity_network"));
}
return std::result::Result::Ok(());
}
async fn set_statement_timeout(
transaction: &deadpool_postgres::Transaction<'_>,
timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let timeout_value = format!("{}ms", timeout.as_millis());
let result = transaction.query_one(SET_STATEMENT_TIMEOUT_SQL, &[&timeout_value]).await;
return match result {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "statement_timeout"))
},
};
}
async fn verify_migration_contract(transaction: &deadpool_postgres::Transaction<'_>, version: i64) -> std::result::Result<(), crate::PostgresBackendError> {
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(());
}
async fn verify_or_repair_applied_migrations(
transaction: &deadpool_postgres::Transaction<'_>,
applied_count: usize,
schema_autoupdate: bool,
) -> std::result::Result<(), crate::PostgresBackendError> {
let mut index = 0_usize;
while index < applied_count {
let migration = &EMBEDDED_MIGRATIONS[index];
for resource in migration.resources {
let state_result = crate::inspect_resource(transaction, resource).await;
let state = match state_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match state {
crate::SchemaResourceState::Compatible => {},
crate::SchemaResourceState::Incompatible => {
log_schema_resource_block(resource.id, "incompatible");
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::MigrationMismatch,
"schema_resource_incompatible",
));
},
crate::SchemaResourceState::Missing => {
if !schema_autoupdate {
log_schema_resource_block(resource.id, "schema_autoupdate_disabled");
return std::result::Result::Err(schema_autoupdate_disabled_error());
}
let repair_result = ensure_resource(transaction, resource, SchemaMutationMode::Update, true).await;
if let std::result::Result::Err(error) = repair_result {
return std::result::Result::Err(error);
}
},
}
}
let contract_result = verify_migration_contract(transaction, migration.version).await;
if let std::result::Result::Err(error) = contract_result {
return std::result::Result::Err(error);
}
index += 1;
}
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");
}
fn validate_embedded_registry(migrations: &[EmbeddedMigration]) -> std::result::Result<(), crate::PostgresBackendError> {
if migrations.is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "registry_empty"));
}
let mut expected_version = 0_i64;
for migration in migrations {
if migration.version != expected_version || migration.name.is_empty() || migration.resources.is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "registry_invalid"));
}
for (resource_index, resource) in migration.resources.iter().enumerate() {
if resource.id.is_empty() || resource.sql.is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "registry_invalid"));
}
for previous in &migration.resources[..resource_index] {
if previous.id == resource.id {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "registry_invalid"));
}
}
}
expected_version = match expected_version.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "registry_invalid"));
},
};
}
return std::result::Result::Ok(());
}
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"));
}
let latest_version = migrations[migrations.len() - 1].version;
let mut index = 0_usize;
for applied in history {
if applied.version > latest_version {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::SchemaNewer, "history_newer"));
}
if index >= migrations.len() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::SchemaNewer, "history_newer"));
}
let expected = &migrations[index];
let expected_checksum = migration_checksum(expected);
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;
}
return std::result::Result::Ok(index);
}
fn log_schema_block(phase: &'static str) {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, phase, "PostgreSQL Store schema compatibility gate blocked automatic opening");
}
fn log_schema_resource_block(resource_id: &'static str, reason: &'static str) {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
resource_id,
reason,
"PostgreSQL Store schema resource requires manual reconciliation"
);
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/migration.rs"] #[path = "../unit_tests/migration.rs"]
mod tests; mod tests;

View 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;

View 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(());
}

Some files were not shown because too many files have changed in this diff Show More