# Delta `0.3.3-pre.006` — pagination keyset et cursor V1 RawTransaction ## 1. Base et gate d'entrée Base opérateur obligatoire : ```text 0.3.3-pre.5 ``` Le gate opérateur fourni le 2026-08-30 est entièrement vert : ```text cargo fmt --all PASS scripts/audit_rust_workspace_rules.py PASS scripts/audit_markdown_tables.py PASS cargo check --workspace PASS cargo clippy --workspace --all-targets PASS cargo test -p ksp-store-api PASS cargo test -p ksp-store-lib PASS cargo test -p ksp-store-postgres-lib PASS (30 unit backend, live foundation ignoré) cargo test -p ksp-config-lib PASS (128 unit + ownership/public API) cargo check -p ksp-store-lib --no-default-features PASS ``` La tranche peut donc ouvrir la pagination sans rouvrir l'écriture atomique acquise en `pre.005`. ## 2. Version ```text workspace.package.version = 0.3.3-pre.6 ``` ## 3. Scope exact Cette tranche ajoute uniquement la navigation backend PostgreSQL des transactions RAW : ```text PostgresBackend::list_raw_transactions RawTransactionQuery RawPage RawPageCursor backend-private V1 ``` Ne sont pas ouverts : ```text transitions Full -> Archived -> Purged compare-and-transition de rétention implémentations complètes des six traits RawTransaction* dispatch ksp-store-lib RawAccountState worker/job policy ``` ## 4. Ordre physique et keyset L'ordre total reste celui figé en `pre.001` : ```text Ascending = (slot ASC, signature ASC) Descending = (slot DESC, signature DESC) ``` Les deux requêtes privées utilisent : ```text retention_state <> 'purged' slot >= start_inclusive si présent slot <= end_inclusive si présent (slot, signature) > cursor pour Ascending (slot, signature) < cursor pour Descending LIMIT requested + 1 ``` Aucun `OFFSET` n'est utilisé. La signature 64 octets constitue le tie-breaker déterministe pour plusieurs transactions au même slot. Le prédicat et l'ordre correspondent à l'index V001 existant : ```text ix_ksp_raw_transactions_slot_signature ON ksp_raw_transactions (slot, signature) WHERE retention_state <> 'purged' ``` Aucune migration n'est modifiée. ## 5. Cursor V1 Le cursor est implémenté dans un sous-module privé au domaine `raw_transaction`. Format fixe : ```text magic 4 bytes = KSPT version 1 byte = 1 last_slot 8 bytes = u64 big-endian last_signature 64 bytes binding_digest 32 bytes = SHA-256 -------------------------------------- total 109 bytes ``` Le digest utilise le domaine séparé : ```text KSP/raw-transaction-cursor/v1 ``` Il lie de façon non ambiguë : ```text longueur du network + network direction flag start + start slot si présent flag end + end slot si présent last_slot last_signature ``` Le digest n'est ni un secret ni un mécanisme d'autorisation. Il sert uniquement à détecter corruption et replay sous un autre contexte de navigation. Le decode rejette avant `pool.get()` : ```text taille != 109 magic incorrect version inconnue digest divergent network différent range différente direction différente last_slot hors range bytes hostiles bornés par RawPageCursor ``` Ces cas sont classés `PostgresBackendErrorKind::QueryInvalid` avec phase statique uniquement. ## 6. Limite physique réelle `RawPageLimit` reste sans cap métier KSP. Le backend doit obtenir une row supplémentaire pour déterminer la présence d'une continuation : ```text LIMIT requested + 1 ``` La borne physique exacte retenue sur PostgreSQL est donc : ```text requested <= i64::MAX - 1 ``` Une valeur supérieure produit : ```text PostgresBackendErrorKind::PageLimitUnsupported ``` Aucun clamp vers `100`, `500`, `1000` ou une autre taille de worker/job n'est introduit. ## 7. Décodage des pages Toutes les rows retournées par le statement, y compris la row de probe `limit+1`, passent par le mapping fallible : ```text signature BYTEA -> [u8; 64] slot NUMERIC(20,0) -> texte décimal -> u64 ``` Une corruption physique n'est donc pas masquée simplement parce qu'elle se trouve sur la row supplémentaire. Si plus de `requested` rows sont valides : 1. la row de probe est retirée du résultat ; 2. le cursor est construit depuis la dernière row réellement retournée ; 3. la page contient exactement au plus `requested` références. Les tombstones `Purged` ne sont jamais listés. ## 8. Concurrence et snapshot Chaque page est cohérente au niveau de son statement PostgreSQL, mais la navigation ne prétend pas fournir un snapshot inter-pages. Une mutation concurrente qui insère une clé ordonnée avant le cursor déjà consommé peut ne pas être vue par cette navigation. Cette propriété est désormais explicitement documentée et n'est pas confondue avec une garantie de replay transactionnel. ## 9. Erreurs sûres Deux kinds backend supplémentaires sont matérialisés : ```text PageLimitUnsupported QueryInvalid ``` Comme les autres erreurs physiques, ils ne conservent que : ```text kind phase &'static str ``` Aucun cursor, network hostile, signature, query SQL, bind, SQLSTATE ou texte PostgreSQL n'est rendu. ## 10. Tests et canaris Les tests unitaires ajoutés couvrent : ```text round-trip cursor V1 exact 109 bytes exacts magic/version exacts replay autre network replay autre direction replay autre range last_slot hors range taille hostile 1/108/109/110/4096 mutation magic/version/digest borne page i64::MAX - 1 rejet i64::MAX ``` Les canaris d'intégration figent : ```text keyset > / < ASC/DESC total pas d'OFFSET exclusion Purged index V001 compatible pas de cap 500/1000 pas de DELETE pas de transition de rétention pas d'implémentation complète de trait prématurée ``` La pagination PostgreSQL réelle multi-page avec ties/ranges reste réservée au gate live opt-in `pre.009`. ## 11. Migrations Les migrations sont byte-identiques à `pre.005` : ```text V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 ``` V001 conserve exactement : ```text 4 tables 35 contraintes 1 index 40 ressources ``` ## 12. Fichiers ajoutés/modifiés ```text Cargo.toml crates/ksp-store-postgres-lib/README.md crates/ksp-store-postgres-lib/USAGE.md crates/ksp-store-postgres-lib/src/error.rs crates/ksp-store-postgres-lib/src/lib.rs crates/ksp-store-postgres-lib/src/raw_transaction.rs crates/ksp-store-postgres-lib/src/raw_transaction/cursor.rs crates/ksp-store-postgres-lib/src/runtime.rs crates/ksp-store-postgres-lib/tests/dependency_boundary.rs crates/ksp-store-postgres-lib/tests/hardening_completeness.rs crates/ksp-store-postgres-lib/tests/public_api.rs crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md deltas/0.3.3/pre.006.md ``` Aucun fichier n'est supprimé. ## 13. Audits exécutables dans l'environnement de génération ```text python3 scripts/audit_rust_workspace_rules.py python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.3 ``` Les commandes Cargo/rustfmt ne sont pas disponibles dans l'environnement de génération. Elles doivent rester `NON EXÉCUTÉES` jusqu'au gate opérateur ; elles ne doivent jamais être présentées comme PASS sans sortie réelle. ## 14. Gate opérateur ```bash cargo fmt --all python3 scripts/audit_rust_workspace_rules.py python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.3 cargo check --workspace cargo clippy --workspace --all-targets cargo test -p ksp-store-api cargo test -p ksp-store-lib cargo test -p ksp-store-postgres-lib cargo test -p ksp-config-lib cargo check -p ksp-store-lib --no-default-features ``` ## 15. Suite après gate vert ```text 0.3.3-pre.007 — archive/purge/tombstone/compare-and-transition + Compacted unsupported stable ```