v0.3.8-pre.005

This commit is contained in:
2026-09-03 13:09:25 +02:00
parent 3bb73e81e4
commit bf73481180
17 changed files with 891 additions and 43 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 451 # version: 452
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-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-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-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.8-pre.4.fix.1" version = "0.3.8-pre.5"
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: crates/ksp-store-lib/src/lib.rs // file: crates/ksp-store-lib/src/lib.rs
// version: 10 // version: 11
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -12,7 +12,8 @@
//! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities //! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities
//! on both the physical backend and this common facade, completing the canonical RAW runtime inventory at //! on both the physical backend and this common facade, completing the canonical RAW runtime inventory at
//! ten capabilities without exposing physical types. The `0.3.8` inspection //! ten capabilities without exposing physical types. The `0.3.8` inspection
//! contracts are reexported here before backend/facade dispatch is opened. //! contracts are reexported here. `0.3.8-pre.004` dispatches transaction inspection
//! and `0.3.8-pre.005` completes inspection dispatch with RAW account states.
//! //!
//! 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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/store.rs // file: crates/ksp-store-lib/src/store.rs
// version: 8 // version: 9
/// Opaque common Store runtime facade. /// Opaque common Store runtime facade.
/// ///
@@ -228,6 +228,36 @@ impl ksp_store_api::RawAccountStateRead for Store {
} }
} }
impl ksp_store_api::RawAccountStateInspectionRead for Store {
fn inspect_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>>> {
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.inspect_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 { impl ksp_store_api::RawAccountStateWrite for Store {
fn persist_raw_account_acquisition<'a>( fn persist_raw_account_acquisition<'a>(
&'a self, &'a self,

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: 9 // version: 10
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -60,11 +60,12 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
} }
#[test] #[test]
fn v0_3_8_pre_004_facade_dispatches_eleven_raw_capabilities_without_physical_leak() { fn v0_3_8_pre_005_facade_dispatches_twelve_raw_capabilities_without_physical_leak() {
let store = include_str!("../src/store.rs"); let store = include_str!("../src/store.rs");
for required in [ for required in [
"impl ksp_store_api::RawAccountObservationRead for Store", "impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store", "impl ksp_store_api::RawAccountObservationWrite for Store",
"impl ksp_store_api::RawAccountStateInspectionRead for Store",
"impl ksp_store_api::RawAccountStateRead for Store", "impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store", "impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawTransactionInspectionRead for Store", "impl ksp_store_api::RawTransactionInspectionRead for Store",
@@ -80,7 +81,7 @@ fn v0_3_8_pre_004_facade_dispatches_eleven_raw_capabilities_without_physical_lea
] { ] {
assert!(store.contains(required), "missing pre.008 Store capability dispatch contract: {required}"); assert!(store.contains(required), "missing pre.008 Store capability dispatch contract: {required}");
} }
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 11); assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 12);
for forbidden in ["tokio_postgres::", "deadpool_postgres::", "CREATE TABLE", "INSERT INTO", "UPDATE ksp_", "DELETE FROM"] { 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}"); assert!(!store.contains(forbidden), "pre.008 facade leaked physical backend material: {forbidden}");
} }

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: 8 // version: 9
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -292,11 +292,12 @@ fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_ha
} }
#[test] #[test]
fn v0_3_8_pre_004_facade_raw_capability_inventory_is_exactly_eleven() { fn v0_3_8_pre_005_facade_raw_capability_inventory_is_exactly_twelve() {
let store = include_str!("../src/store.rs"); let store = include_str!("../src/store.rs");
let capability_impls = [ let capability_impls = [
"impl ksp_store_api::RawAccountObservationRead for Store", "impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store", "impl ksp_store_api::RawAccountObservationWrite for Store",
"impl ksp_store_api::RawAccountStateInspectionRead for Store",
"impl ksp_store_api::RawAccountStateRead for Store", "impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store", "impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawTransactionInspectionRead for Store", "impl ksp_store_api::RawTransactionInspectionRead for Store",
@@ -310,22 +311,22 @@ fn v0_3_8_pre_004_facade_raw_capability_inventory_is_exactly_eleven() {
for implementation in capability_impls { for implementation in capability_impls {
assert_eq!(store.matches(implementation).count(), 1, "unexpected Store capability implementation inventory: {implementation}"); assert_eq!(store.matches(implementation).count(), 1, "unexpected Store capability implementation inventory: {implementation}");
} }
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 11); assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 12);
assert_eq!(store.matches("validate_operation_network(").count(), 15); assert_eq!(store.matches("validate_operation_network(").count(), 16);
return; return;
} }
#[test] #[test]
fn v0_3_8_pre_004_facade_and_backend_capability_sets_match_with_transaction_inspection_only() { fn v0_3_8_pre_005_facade_and_backend_capability_sets_match_with_both_inspection_capabilities() {
let store = include_str!("../src/store.rs"); let store = include_str!("../src/store.rs");
let backend = include_str!("../../ksp-store-postgres-lib/src/runtime.rs"); let backend = include_str!("../../ksp-store-postgres-lib/src/runtime.rs");
let store_traits = raw_capability_trait_names(store, " for Store"); let store_traits = raw_capability_trait_names(store, " for Store");
let backend_traits = raw_capability_trait_names(backend, " for PostgresBackend"); let backend_traits = raw_capability_trait_names(backend, " for PostgresBackend");
assert_eq!(store_traits.len(), 11); assert_eq!(store_traits.len(), 12);
assert_eq!(backend_traits.len(), 11); assert_eq!(backend_traits.len(), 12);
assert_eq!(store_traits, backend_traits); assert_eq!(store_traits, backend_traits);
assert!(store_traits.contains(&"RawTransactionInspectionRead")); assert!(store_traits.contains(&"RawTransactionInspectionRead"));
assert!(!store_traits.contains(&"RawAccountStateInspectionRead")); assert!(store_traits.contains(&"RawAccountStateInspectionRead"));
for forbidden in ["RawAccountRetentionRead", "RawAccountRetentionWrite", "RawAccountDelete", "RawAccountCompaction"] { for forbidden in ["RawAccountRetentionRead", "RawAccountRetentionWrite", "RawAccountDelete", "RawAccountCompaction"] {
assert!(!store_traits.contains(&forbidden), "unexpected account capability added to Store: {forbidden}"); 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}"); assert!(!backend_traits.contains(&forbidden), "unexpected account capability added to PostgreSQL backend: {forbidden}");

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: 10 // version: 11
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -81,6 +81,7 @@ fn assert_raw_capabilities<T>()
where where
T: ksp_store_lib::RawAccountObservationRead T: ksp_store_lib::RawAccountObservationRead
+ ksp_store_lib::RawAccountObservationWrite + ksp_store_lib::RawAccountObservationWrite
+ ksp_store_lib::RawAccountStateInspectionRead
+ ksp_store_lib::RawAccountStateRead + ksp_store_lib::RawAccountStateRead
+ ksp_store_lib::RawAccountStateWrite + ksp_store_lib::RawAccountStateWrite
+ ksp_store_lib::RawTransactionInspectionRead + ksp_store_lib::RawTransactionInspectionRead
@@ -96,7 +97,7 @@ where
} }
#[test] #[test]
fn v0_3_8_pre_004_store_facade_implements_raw_transaction_inspection_as_capability_11_of_11() { fn v0_3_8_pre_005_store_facade_implements_both_inspection_capabilities_as_12_of_12() {
assert_raw_capabilities::<ksp_store_lib::Store>(); assert_raw_capabilities::<ksp_store_lib::Store>();
return; return;
} }

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: 22 // version: 23
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -33,8 +33,9 @@
//! deterministic account keyset pagination with the fixed `KSPA` cursor. `0.3.4-pre.008` //! deterministic account keyset pagination with the fixed `KSPA` cursor. `0.3.4-pre.008`
//! implements the four `RawAccount*` capabilities directly on `PostgresBackend`, completing //! implements the four `RawAccount*` capabilities directly on `PostgresBackend`, completing
//! the backend RAW capability inventory at ten without exposing physical PostgreSQL types. //! the backend RAW capability inventory at ten without exposing physical PostgreSQL types.
//! `0.3.8-pre.004` adds the payload-free random-access RawTransaction inspection //! `0.3.8-pre.004` adds payload-free random-access RawTransaction inspection;
//! capability with exact counts while preserving keyset traversal unchanged. //! `0.3.8-pre.005` adds the corresponding data-free RawAccountState inspection
//! with exact counts while preserving both keyset traversal families unchanged.
//! //!
//! 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
@@ -86,6 +87,8 @@ pub(crate) use self::raw_account::get_raw_account_observation;
pub(crate) use self::raw_account::get_raw_account_state; pub(crate) use self::raw_account::get_raw_account_state;
/// Private RAW account-state list reader consumed by the physical backend runtime. /// Private RAW account-state list reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::list_raw_account_states; pub(crate) use self::raw_account::list_raw_account_states;
/// Private data-free RAW account-state inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::inspect_raw_account_states;
/// Private atomic RAW account acquisition writer consumed by the physical backend runtime. /// Private atomic RAW account acquisition writer consumed by the physical backend runtime.
pub(crate) use self::raw_account::persist_raw_account_acquisition; pub(crate) use self::raw_account::persist_raw_account_acquisition;
/// Private additional RAW account observation writer consumed by the physical backend runtime. /// Private additional RAW account observation writer consumed by the physical backend runtime.
@@ -104,10 +107,10 @@ pub(crate) use self::raw_transaction::get_raw_transaction_observation;
pub(crate) use self::raw_transaction::get_raw_transaction_retention_state; pub(crate) use self::raw_transaction::get_raw_transaction_retention_state;
/// Private RAW transaction tombstone reader consumed by the physical backend runtime. /// Private RAW transaction tombstone reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::get_raw_transaction_tombstone; pub(crate) use self::raw_transaction::get_raw_transaction_tombstone;
/// Private payload-free RAW transaction inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::inspect_raw_transactions;
/// Private RAW transaction list reader consumed by the physical backend runtime. /// Private RAW transaction list reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::list_raw_transactions; pub(crate) use self::raw_transaction::list_raw_transactions;
/// Private payload-free RAW transaction inspection reader consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::inspect_raw_transactions;
/// Private atomic RAW transaction acquisition writer consumed by the physical backend runtime. /// Private atomic RAW transaction acquisition writer consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::persist_raw_transaction_acquisition; pub(crate) use self::raw_transaction::persist_raw_transaction_acquisition;
/// Private additional RAW transaction observation writer consumed by the physical backend runtime. /// Private additional RAW transaction observation writer consumed by the physical backend runtime.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/raw_account.rs // file: crates/ksp-store-postgres-lib/src/raw_account.rs
// version: 5 // version: 6
pub(crate) mod cursor; pub(crate) mod cursor;
@@ -7,6 +7,8 @@ const GET_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubke
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 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_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 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 INSPECT_ACCOUNT_STATES_ASC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_account_states WHERE ($1::BYTEA IS NULL OR pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC)), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL AND $2::TEXT IS NULL AND $3::TEXT IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_account_states) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.pubkey IS NOT NULL AS page_present, page.pubkey, page.slot_text, page.state_hash, page.lamports_text, page.owner, page.executable, page.rent_epoch_text, page.data_length_bytes FROM counts LEFT JOIN LATERAL (SELECT account_row.pubkey, account_row.slot::TEXT AS slot_text, account_row.state_hash, account_row.lamports::TEXT AS lamports_text, account_row.owner, account_row.executable, account_row.rent_epoch::TEXT AS rent_epoch_text, OCTET_LENGTH(account_row.data)::BIGINT AS data_length_bytes FROM ksp_raw_account_states AS account_row WHERE ($1::BYTEA IS NULL OR account_row.pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR account_row.slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR account_row.slot <= $3::TEXT::NUMERIC) ORDER BY account_row.slot ASC, account_row.pubkey ASC, account_row.state_hash ASC LIMIT $4 OFFSET $5) AS page ON TRUE";
const INSPECT_ACCOUNT_STATES_DESC_SQL: &str = "WITH filtered_count AS (SELECT COUNT(*)::TEXT AS filtered_count_text FROM ksp_raw_account_states WHERE ($1::BYTEA IS NULL OR pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC)), counts AS (SELECT filtered_count_text, CASE WHEN $1::BYTEA IS NULL AND $2::TEXT IS NULL AND $3::TEXT IS NULL THEN filtered_count_text ELSE (SELECT COUNT(*)::TEXT FROM ksp_raw_account_states) END AS total_count_text FROM filtered_count) SELECT counts.total_count_text, counts.filtered_count_text, page.pubkey IS NOT NULL AS page_present, page.pubkey, page.slot_text, page.state_hash, page.lamports_text, page.owner, page.executable, page.rent_epoch_text, page.data_length_bytes FROM counts LEFT JOIN LATERAL (SELECT account_row.pubkey, account_row.slot::TEXT AS slot_text, account_row.state_hash, account_row.lamports::TEXT AS lamports_text, account_row.owner, account_row.executable, account_row.rent_epoch::TEXT AS rent_epoch_text, OCTET_LENGTH(account_row.data)::BIGINT AS data_length_bytes FROM ksp_raw_account_states AS account_row WHERE ($1::BYTEA IS NULL OR account_row.pubkey = $1::BYTEA) AND ($2::TEXT IS NULL OR account_row.slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR account_row.slot <= $3::TEXT::NUMERIC) ORDER BY account_row.slot DESC, account_row.pubkey DESC, account_row.state_hash DESC LIMIT $4 OFFSET $5) AS page ON TRUE";
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_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_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_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";
@@ -22,6 +24,20 @@ struct RawAccountListDbRow {
state_hash: std::vec::Vec<u8>, state_hash: std::vec::Vec<u8>,
} }
struct RawAccountInspectionDbRow {
data_length_bytes: std::option::Option<i64>,
executable: std::option::Option<bool>,
filtered_count_text: std::string::String,
lamports_text: std::option::Option<std::string::String>,
owner: std::option::Option<std::vec::Vec<u8>>,
page_present: bool,
pubkey: std::option::Option<std::vec::Vec<u8>>,
rent_epoch_text: std::option::Option<std::string::String>,
slot_text: std::option::Option<std::string::String>,
state_hash: std::option::Option<std::vec::Vec<u8>>,
total_count_text: std::string::String,
}
struct RawAccountObservationDbRow { struct RawAccountObservationDbRow {
account_pubkey: std::vec::Vec<u8>, account_pubkey: std::vec::Vec<u8>,
account_slot_text: std::string::String, account_slot_text: std::string::String,
@@ -203,6 +219,107 @@ pub(crate) async fn list_raw_account_states(
return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor)); return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor));
} }
/// Inspects one data-free random-access RAW account-state window with exact logical counts.
pub(crate) async fn inspect_raw_account_states(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
query: &ksp_store_api::RawAccountStateInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>, crate::PostgresBackendError> {
if query.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::WrongNetwork,
"raw_account_inspection_network",
));
}
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => INSPECT_ACCOUNT_STATES_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => INSPECT_ACCOUNT_STATES_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_account_inspection_direction",
));
},
};
let (sql_limit, sql_offset) = match raw_account_inspection_sql_window(query.page()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => 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 = query.pubkey().map(|value| return value.to_bytes().to_vec());
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 rows_result = client.query(sql, &[&pubkey_bytes, &start_text, &end_text, &sql_limit, &sql_offset]).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_inspection_query"));
},
};
if rows.is_empty() {
return std::result::Result::Err(data_invalid("raw_account_inspection_cardinality"));
}
let row_count = rows.len();
let mut total_items = std::option::Option::None;
let mut filtered_items = std::option::Option::None;
let mut items = std::vec::Vec::new();
for row in rows {
let physical = match raw_account_inspection_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_total = match decode_u64_decimal(physical.total_count_text.as_str(), "raw_account_inspection_total") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let row_filtered = match decode_u64_decimal(physical.filtered_count_text.as_str(), "raw_account_inspection_filtered") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match (total_items, filtered_items) {
(std::option::Option::None, std::option::Option::None) => {
total_items = std::option::Option::Some(row_total);
filtered_items = std::option::Option::Some(row_filtered);
},
(std::option::Option::Some(total), std::option::Option::Some(filtered)) if total == row_total && filtered == row_filtered => {},
_ => return std::result::Result::Err(data_invalid("raw_account_inspection_counts")),
}
if physical.page_present {
let summary = match decode_raw_account_inspection_summary(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
items.push(summary);
} else if row_count != 1 || !raw_account_inspection_empty_page_is_clean(&physical) {
return std::result::Result::Err(data_invalid("raw_account_inspection_page"));
}
}
let total_items = match total_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_inspection_total")),
};
let filtered_items = match filtered_items {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_inspection_filtered")),
};
let item_count = match u64::try_from(items.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_page")),
};
if item_count > query.page().limit().get() {
return std::result::Result::Err(data_invalid("raw_account_inspection_page"));
}
return match ksp_store_api::RawInspectionPage::try_new(items, total_items, filtered_items) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_inspection_page")),
};
}
/// Reads one persisted RAW account observation by producer-owned idempotence key. /// Reads one persisted RAW account observation by producer-owned idempotence key.
pub(crate) async fn get_raw_account_observation( pub(crate) async fn get_raw_account_observation(
pool: &deadpool_postgres::Pool, pool: &deadpool_postgres::Pool,
@@ -913,6 +1030,182 @@ fn decode_timestamp_i64(value: i64, phase: &'static str) -> std::result::Result<
}; };
} }
fn raw_account_inspection_empty_page_is_clean(row: &RawAccountInspectionDbRow) -> bool {
return !row.page_present
&& row.data_length_bytes.is_none()
&& row.executable.is_none()
&& row.lamports_text.is_none()
&& row.owner.is_none()
&& row.pubkey.is_none()
&& row.rent_epoch_text.is_none()
&& row.slot_text.is_none()
&& row.state_hash.is_none();
}
fn raw_account_inspection_sql_window(
page: ksp_store_api::RawInspectionPageRequest,
) -> std::result::Result<(i64, i64), crate::PostgresBackendError> {
let sql_limit = match i64::try_from(page.limit().get()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::PageLimitUnsupported,
"raw_account_inspection_limit",
));
},
};
let sql_offset = match i64::try_from(page.offset()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_account_inspection_offset",
));
},
};
return std::result::Result::Ok((sql_limit, sql_offset));
}
fn raw_account_inspection_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountInspectionDbRow, crate::PostgresBackendError> {
let data_length_bytes = match row.try_get::<_, std::option::Option<i64>>("data_length_bytes") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let executable = match row.try_get::<_, std::option::Option<bool>>("executable") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let filtered_count_text = match row.try_get::<_, std::string::String>("filtered_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let lamports_text = match row.try_get::<_, std::option::Option<std::string::String>>("lamports_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let owner = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("owner") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let page_present = match row.try_get::<_, bool>("page_present") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let pubkey = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("pubkey") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let rent_epoch_text = match row.try_get::<_, std::option::Option<std::string::String>>("rent_epoch_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let slot_text = match row.try_get::<_, std::option::Option<std::string::String>>("slot_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
let state_hash = match row.try_get::<_, std::option::Option<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_inspection_decode")),
};
let total_count_text = match row.try_get::<_, std::string::String>("total_count_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_decode")),
};
return std::result::Result::Ok(RawAccountInspectionDbRow {
data_length_bytes,
executable,
filtered_count_text,
lamports_text,
owner,
page_present,
pubkey,
rent_epoch_text,
slot_text,
state_hash,
total_count_text,
});
}
fn decode_raw_account_inspection_summary(
network: &ksp_store_api::RawNetworkId,
row: RawAccountInspectionDbRow,
) -> std::result::Result<ksp_store_api::RawAccountStateSummary, crate::PostgresBackendError> {
if !row.page_present {
return std::result::Result::Err(data_invalid("raw_account_inspection_page"));
}
let pubkey = match inspection_required(row.pubkey, "raw_account_inspection_pubkey") {
std::result::Result::Ok(value) => match fixed_bytes::<32>(value, "raw_account_inspection_pubkey") {
std::result::Result::Ok(bytes) => ksp_store_api::Pubkey::new_from_array(bytes),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot_text = match inspection_required(row.slot_text, "raw_account_inspection_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot = match decode_u64_decimal(slot_text.as_str(), "raw_account_inspection_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash_raw = match inspection_required(row.state_hash, "raw_account_inspection_state_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash = match fixed_bytes::<32>(state_hash_raw, "raw_account_inspection_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_text = match inspection_required(row.lamports_text, "raw_account_inspection_lamports") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let lamports = match decode_u64_decimal(lamports_text.as_str(), "raw_account_inspection_lamports") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_raw = match inspection_required(row.owner, "raw_account_inspection_owner") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match fixed_bytes::<32>(owner_raw, "raw_account_inspection_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 executable = match inspection_required(row.executable, "raw_account_inspection_executable") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_epoch_text = match inspection_required(row.rent_epoch_text, "raw_account_inspection_rent_epoch") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_epoch = match decode_u64_decimal(rent_epoch_text.as_str(), "raw_account_inspection_rent_epoch") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let data_length_raw = match inspection_required(row.data_length_bytes, "raw_account_inspection_data_length") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let data_length_bytes = match u64::try_from(data_length_raw) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_inspection_data_length")),
};
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
return match ksp_store_api::RawAccountStateSummary::try_new(reference, lamports, owner, executable, rent_epoch, data_length_bytes) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_inspection_summary")),
};
}
fn inspection_required<T>(value: std::option::Option<T>, phase: &'static str) -> std::result::Result<T, crate::PostgresBackendError> {
return match value {
std::option::Option::Some(inner) => std::result::Result::Ok(inner),
std::option::Option::None => std::result::Result::Err(data_invalid(phase)),
};
}
fn decode_u64_decimal(value: &str, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> { fn decode_u64_decimal(value: &str, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> {
return match value.parse::<u64>() { return match value.parse::<u64>() {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded), std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs // file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 16 // version: 17
const APPLICATION_NAME: &str = "ksp-store"; const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096; const MAX_CONNECTION_URI_BYTES: usize = 4_096;
@@ -348,6 +348,14 @@ impl PostgresBackend {
return crate::list_raw_account_states(&self.pool, &self.network, query).await; return crate::list_raw_account_states(&self.pool, &self.network, query).await;
} }
/// Inspects one data-free random-access RAW account-state window with exact logical counts.
pub async fn inspect_raw_account_states(
&self,
query: &ksp_store_api::RawAccountStateInspectionQuery,
) -> std::result::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>, crate::PostgresBackendError> {
return crate::inspect_raw_account_states(&self.pool, &self.network, query).await;
}
/// Reads one persisted RAW account observation by producer-owned idempotence key. /// Reads one persisted RAW account observation by producer-owned idempotence key.
pub async fn get_raw_account_observation( pub async fn get_raw_account_observation(
&self, &self,
@@ -523,6 +531,18 @@ impl ksp_store_api::RawAccountStateRead for PostgresBackend {
} }
} }
impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend {
fn inspect_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateInspectionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawInspectionPage<ksp_store_api::RawAccountStateSummary>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::inspect_raw_account_states(self, query).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawAccountStateWrite for PostgresBackend { impl ksp_store_api::RawAccountStateWrite for PostgresBackend {
fn persist_raw_account_acquisition<'a>( fn persist_raw_account_acquisition<'a>(
&'a self, &'a self,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs // file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 26 // version: 27
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -184,6 +184,7 @@ fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_trait_impls_ou
assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}"); assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}");
} }
for implementation in [ for implementation in [
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
@@ -218,6 +219,7 @@ fn pre_006_raw_account_additional_observation_is_reference_guarded_cancellation_
assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}"); assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}");
} }
for implementation in [ for implementation in [
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
@@ -374,11 +376,36 @@ fn pre_007_raw_account_pagination_is_keyset_cursor_bound_and_policy_free() {
assert!(cursor.contains(required), "missing pre.007 account cursor/binding contract: {required}"); assert!(cursor.contains(required), "missing pre.007 account cursor/binding contract: {required}");
} }
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)")); assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000"] { let ascending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_ASC_SQL"));
let ascending = match ascending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account ascending keyset SQL"),
};
let descending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_DESC_SQL"));
let descending = match descending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account descending keyset SQL"),
};
let by_pubkey_ascending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL"));
let by_pubkey_ascending = match by_pubkey_ascending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account pubkey ascending keyset SQL"),
};
let by_pubkey_descending = raw.lines().find(|line| return line.starts_with("const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL"));
let by_pubkey_descending = match by_pubkey_descending {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account pubkey descending keyset SQL"),
};
for statement in [ascending, descending, by_pubkey_ascending, by_pubkey_descending] {
assert!(!statement.contains(" OFFSET "), "pre.007 canonical account keyset SQL must remain OFFSET-free");
}
assert!(!cursor.contains(" OFFSET "), "pre.007 account cursor contains forbidden OFFSET material");
for forbidden in ["limit.min(", "clamp(", "500", "1000"] {
assert!(!raw.contains(forbidden), "pre.007 account source contains forbidden pagination/policy material: {forbidden}"); assert!(!raw.contains(forbidden), "pre.007 account source contains forbidden pagination/policy material: {forbidden}");
assert!(!cursor.contains(forbidden), "pre.007 account cursor contains forbidden pagination/policy material: {forbidden}"); assert!(!cursor.contains(forbidden), "pre.007 account cursor contains forbidden pagination/policy material: {forbidden}");
} }
for forbidden in [ for forbidden in [
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
@@ -422,11 +449,12 @@ fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compactio
} }
#[test] #[test]
fn v0_3_8_pre_004_backend_trait_implementations_cover_exact_eleven_raw_capabilities_in_runtime_bridge() { fn v0_3_8_pre_005_backend_trait_implementations_cover_exact_twelve_raw_capabilities_in_runtime_bridge() {
let runtime = include_str!("../src/runtime.rs"); let runtime = include_str!("../src/runtime.rs");
for implementation in [ for implementation in [
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionInspectionRead for PostgresBackend", "impl ksp_store_api::RawTransactionInspectionRead for PostgresBackend",
@@ -439,6 +467,6 @@ fn v0_3_8_pre_004_backend_trait_implementations_cover_exact_eleven_raw_capabilit
] { ] {
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL RAW capability inventory: {implementation}"); assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL RAW capability inventory: {implementation}");
} }
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 11); assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 12);
return; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs // file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
// version: 19 // version: 20
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -250,6 +250,9 @@ fn pre_009_live_raw_account_proof_is_opt_in_isolated_secret_safe_and_cross_famil
"prove_additional_observations", "prove_additional_observations",
"ReferenceNotFound", "ReferenceNotFound",
"prove_pagination_and_cursors", "prove_pagination_and_cursors",
"prove_account_inspection",
"inspect_raw_account_states",
"RawAccountStateInspectionQuery",
"KSPT", "KSPT",
"prove_cross_family_coexistence", "prove_cross_family_coexistence",
"persist_raw_transaction_acquisition", "persist_raw_transaction_acquisition",
@@ -299,11 +302,12 @@ fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
} }
#[test] #[test]
fn v0_3_8_pre_004_raw_capability_implementation_inventory_is_exactly_eleven() { fn v0_3_8_pre_005_raw_capability_implementation_inventory_is_exactly_twelve() {
let runtime = include_str!("../src/runtime.rs"); let runtime = include_str!("../src/runtime.rs");
let capability_impls = [ let capability_impls = [
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend", "impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend", "impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
"impl ksp_store_api::RawAccountStateInspectionRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend", "impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend", "impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionInspectionRead for PostgresBackend", "impl ksp_store_api::RawTransactionInspectionRead for PostgresBackend",
@@ -317,7 +321,7 @@ fn v0_3_8_pre_004_raw_capability_implementation_inventory_is_exactly_eleven() {
for implementation in capability_impls { for implementation in capability_impls {
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL capability implementation inventory: {implementation}"); assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL capability implementation inventory: {implementation}");
} }
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 11); assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 12);
let migration = include_str!("../src/migration.rs"); let migration = include_str!("../src/migration.rs");
assert!(migration.contains("raw_account_state")); assert!(migration.contains("raw_account_state"));
assert!(migration.contains("crate::V002_RESOURCES")); assert!(migration.contains("crate::V002_RESOURCES"));
@@ -466,3 +470,38 @@ fn pre_010_v002_schema_inventory_is_exact_family_local_and_query_justified() {
} }
return; return;
} }
#[test]
fn v0_3_8_pre_005_raw_account_keyset_sql_remains_offset_free_while_inspection_is_random_access() {
let source = include_str!("../src/raw_account.rs");
for name in [
"const LIST_ACCOUNT_STATES_ASC_SQL",
"const LIST_ACCOUNT_STATES_DESC_SQL",
"const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL",
"const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL",
] {
let statement = source.lines().find(|line| return line.starts_with(name));
let statement = match statement {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing canonical account keyset SQL: {name}"),
};
assert!(!statement.contains(" OFFSET "));
}
for name in ["const INSPECT_ACCOUNT_STATES_ASC_SQL", "const INSPECT_ACCOUNT_STATES_DESC_SQL"] {
let statement = source.lines().find(|line| return line.starts_with(name));
let statement = match statement {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("missing account inspection SQL: {name}"),
};
for required in ["COUNT(*)", "LEFT JOIN LATERAL", "OCTET_LENGTH(account_row.data)", "LIMIT $4 OFFSET $5"] {
assert!(statement.contains(required), "missing account inspection SQL contract: {required}");
}
assert!(!statement.contains("account_row.data,"));
assert!(!statement.contains("SELECT account_row.data"));
assert!(statement.contains("ORDER BY account_row.slot"));
assert!(statement.contains("account_row.pubkey"));
assert!(statement.contains("account_row.state_hash"));
}
assert_eq!(source.matches(" OFFSET ").count(), 2);
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs // file: crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
// version: 1 // version: 2
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -68,7 +68,10 @@ enum LiveAccountPersistResult {
#[test] #[test]
#[ignore = "opt-in real PostgreSQL RawAccountState proof; reads one dedicated URI from stdin"] #[ignore = "opt-in real PostgreSQL RawAccountState proof; reads one dedicated URI from stdin"]
fn pre_009_real_postgres_raw_account_vertical_slice_is_atomic_concurrent_and_recoverable() { fn pre_009_real_postgres_raw_account_vertical_slice_is_atomic_concurrent_and_recoverable() {
eprintln!("KSP Store RawAccountState live proof: reading one dedicated URI from stdin without echoing it from the test."); eprintln!(
"KSP Store RawAccountState live proof: waiting for one dedicated PostgreSQL URI line on stdin; the scenario does not start until Enter is pressed."
);
eprintln!("KSP Store RawAccountState live proof: the test itself never echoes the URI.");
let uri_result = read_uri_from_stdin(); let uri_result = read_uri_from_stdin();
let uri = match uri_result { let uri = match uri_result {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -179,6 +182,7 @@ async fn run_raw_account_scenario(admin: &mut tokio_postgres::Client, uri: &str,
prove_atomic_idempotence_and_conflicts(&backend).await, prove_atomic_idempotence_and_conflicts(&backend).await,
prove_additional_observations(&backend).await, prove_additional_observations(&backend).await,
prove_pagination_and_cursors(&backend).await, prove_pagination_and_cursors(&backend).await,
prove_account_inspection(&backend).await,
prove_cross_family_coexistence(&backend, admin).await, prove_cross_family_coexistence(&backend, admin).await,
] { ] {
if let std::result::Result::Err(error) = proof { if let std::result::Result::Err(error) = proof {
@@ -561,6 +565,80 @@ async fn prove_pagination_and_cursors(backend: &ksp_store_postgres_lib::Postgres
return std::result::Result::Ok(()); return std::result::Result::Ok(());
} }
async fn prove_account_inspection(backend: &ksp_store_postgres_lib::PostgresBackend) -> std::result::Result<(), LiveFailure> {
let filtered_query = match account_inspection_query(
std::option::Option::Some(ksp_store_api::Pubkey::new_from_array([50; 32])),
200,
202,
ksp_store_api::RawSortDirection::Ascending,
1,
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filtered = match backend.inspect_raw_account_states(&filtered_query).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("inspection_filtered_query")),
};
if filtered.filtered_items() != 3 || filtered.total_items() < 5 || filtered.items().len() != 2 {
return std::result::Result::Err(LiveFailure::new("inspection_filtered_counts"));
}
let first = &filtered.items()[0];
let second = &filtered.items()[1];
if first.reference().slot() != 200
|| first.reference().pubkey() != &ksp_store_api::Pubkey::new_from_array([50; 32])
|| first.reference().state_hash() != ksp_store_api::RawContentHash::new([51; 32])
|| first.data_length_bytes() != 2
|| second.reference().slot() != 201
|| second.reference().state_hash() != ksp_store_api::RawContentHash::new([52; 32])
|| second.data_length_bytes() != 2
{
return std::result::Result::Err(LiveFailure::new("inspection_filtered_order"));
}
let descending_query = match account_inspection_query(std::option::Option::None, 200, 202, ksp_store_api::RawSortDirection::Descending, 1, 2) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let descending = match backend.inspect_raw_account_states(&descending_query).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("inspection_descending_query")),
};
if descending.filtered_items() != 5 || descending.items().len() != 2 {
return std::result::Result::Err(LiveFailure::new("inspection_descending_counts"));
}
let descending_first = &descending.items()[0];
let descending_second = &descending.items()[1];
if descending_first.reference().slot() != 201
|| descending_first.reference().pubkey() != &ksp_store_api::Pubkey::new_from_array([51; 32])
|| descending_first.reference().state_hash() != ksp_store_api::RawContentHash::new([53; 32])
|| descending_second.reference().slot() != 201
|| descending_second.reference().pubkey() != &ksp_store_api::Pubkey::new_from_array([50; 32])
|| descending_second.reference().state_hash() != ksp_store_api::RawContentHash::new([52; 32])
{
return std::result::Result::Err(LiveFailure::new("inspection_descending_order"));
}
let deep_query = match account_inspection_query(
std::option::Option::Some(ksp_store_api::Pubkey::new_from_array([50; 32])),
200,
202,
ksp_store_api::RawSortDirection::Ascending,
10_000,
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let deep = match backend.inspect_raw_account_states(&deep_query).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("inspection_deep_query")),
};
if deep.filtered_items() != 3 || deep.total_items() != filtered.total_items() || !deep.items().is_empty() {
return std::result::Result::Err(LiveFailure::new("inspection_deep_page"));
}
return std::result::Result::Ok(());
}
async fn prove_cross_family_coexistence( async fn prove_cross_family_coexistence(
backend: &ksp_store_postgres_lib::PostgresBackend, backend: &ksp_store_postgres_lib::PostgresBackend,
admin: &tokio_postgres::Client, admin: &tokio_postgres::Client,
@@ -850,6 +928,30 @@ fn account_query(
return std::result::Result::Ok(ksp_store_api::RawAccountStateQuery::new(network, pubkey, slots, direction, page)); return std::result::Result::Ok(ksp_store_api::RawAccountStateQuery::new(network, pubkey, slots, direction, page));
} }
fn account_inspection_query(
pubkey: std::option::Option<ksp_store_api::Pubkey>,
start: u64,
end: u64,
direction: ksp_store_api::RawSortDirection,
offset: u64,
limit: u64,
) -> std::result::Result<ksp_store_api::RawAccountStateInspectionQuery, LiveFailure> {
let network = match network("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slots = match ksp_store_api::RawSlotRange::new(std::option::Option::Some(start), std::option::Option::Some(end)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("inspection_model_slot_range")),
};
let limit = match ksp_store_api::RawPageLimit::new(limit) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("inspection_model_page_limit")),
};
let page = ksp_store_api::RawInspectionPageRequest::new(offset, limit);
return std::result::Result::Ok(ksp_store_api::RawAccountStateInspectionQuery::new(network, pubkey, slots, direction, page));
}
fn account_reference(pubkey_seed: u8, slot: u64, hash_seed: u8) -> std::result::Result<ksp_store_api::RawAccountStateReference, LiveFailure> { fn account_reference(pubkey_seed: u8, slot: u64, hash_seed: u8) -> std::result::Result<ksp_store_api::RawAccountStateReference, LiveFailure> {
let network = match network("devnet") { let network = match network("devnet") {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/public_api.rs // file: crates/ksp-store-postgres-lib/tests/public_api.rs
// version: 15 // version: 16
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -117,9 +117,11 @@ fn pre_005_raw_write_bridge_uses_only_backend_independent_models_and_outcomes()
} }
#[test] #[test]
fn v0_3_8_pre_004_raw_list_and_inspection_bridges_use_only_backend_independent_models() { fn v0_3_8_pre_005_raw_list_and_inspection_bridges_use_only_backend_independent_models() {
let _list = ksp_store_postgres_lib::PostgresBackend::list_raw_transactions; let _account_list = ksp_store_postgres_lib::PostgresBackend::list_raw_account_states;
let _inspection = ksp_store_postgres_lib::PostgresBackend::inspect_raw_transactions; let _account_inspection = ksp_store_postgres_lib::PostgresBackend::inspect_raw_account_states;
let _transaction_list = ksp_store_postgres_lib::PostgresBackend::list_raw_transactions;
let _transaction_inspection = ksp_store_postgres_lib::PostgresBackend::inspect_raw_transactions;
return; return;
} }
@@ -133,6 +135,7 @@ fn assert_raw_capabilities<T>()
where where
T: ksp_store_api::RawAccountObservationRead T: ksp_store_api::RawAccountObservationRead
+ ksp_store_api::RawAccountObservationWrite + ksp_store_api::RawAccountObservationWrite
+ ksp_store_api::RawAccountStateInspectionRead
+ ksp_store_api::RawAccountStateRead + ksp_store_api::RawAccountStateRead
+ ksp_store_api::RawAccountStateWrite + ksp_store_api::RawAccountStateWrite
+ ksp_store_api::RawTransactionInspectionRead + ksp_store_api::RawTransactionInspectionRead
@@ -148,7 +151,7 @@ where
} }
#[test] #[test]
fn v0_3_8_pre_004_postgres_backend_implements_transaction_inspection_as_capability_11_of_11() { fn v0_3_8_pre_005_postgres_backend_implements_both_inspection_capabilities_as_12_of_12() {
assert_raw_capabilities::<ksp_store_postgres_lib::PostgresBackend>(); assert_raw_capabilities::<ksp_store_postgres_lib::PostgresBackend>();
return; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/raw_account.rs // file: crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
// version: 5 // version: 6
fn network() -> ksp_store_api::RawNetworkId { fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") { return match ksp_store_api::RawNetworkId::new("devnet") {
@@ -67,6 +67,95 @@ fn observation_row() -> super::RawAccountObservationDbRow {
}; };
} }
return super::RawAccountInspectionDbRow {
data_length_bytes: std::option::Option::Some(data_length_bytes),
executable: std::option::Option::Some(true),
filtered_count_text: "3".to_owned(),
lamports_text: std::option::Option::Some(u64::MAX.to_string()),
owner: std::option::Option::Some(vec![2; 32]),
page_present: true,
pubkey: std::option::Option::Some(vec![1; 32]),
rent_epoch_text: std::option::Option::Some(u64::MAX.to_string()),
slot_text: std::option::Option::Some(u64::MAX.to_string()),
state_hash: std::option::Option::Some(vec![3; 32]),
total_count_text: "4".to_owned(),
};
}
#[test]
fn v0_3_8_pre_005_inspection_window_rejects_unrepresentable_limit_and_offset_before_io() {
let normal_limit = match ksp_store_api::RawPageLimit::new(100) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account inspection page limit rejected: {error:?}"),
};
let normal = super::raw_account_inspection_sql_window(ksp_store_api::RawInspectionPageRequest::new(25, normal_limit));
assert_eq!(normal, std::result::Result::Ok((100, 25)));
let huge_limit = match ksp_store_api::RawPageLimit::new(u64::MAX) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("API unexpectedly rejected backend-physical account limit fixture: {error:?}"),
};
let limit_error = super::raw_account_inspection_sql_window(ksp_store_api::RawInspectionPageRequest::new(0, huge_limit));
assert_eq!(
limit_error.err().map(|value| return (value.kind(), value.phase())),
std::option::Option::Some((crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_inspection_limit")),
);
let offset_error = super::raw_account_inspection_sql_window(ksp_store_api::RawInspectionPageRequest::new(u64::MAX, normal_limit));
assert_eq!(
offset_error.err().map(|value| return (value.kind(), value.phase())),
std::option::Option::Some((crate::PostgresBackendErrorKind::QueryInvalid, "raw_account_inspection_offset")),
);
return;
}
#[test]
fn v0_3_8_pre_005_inspection_summary_is_data_free_and_preserves_complete_u64_domain() {
let network = network();
let decoded = super::decode_raw_account_inspection_summary(&network, inspection_row(4));
let summary = match decoded {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account inspection summary rejected: {error:?}"),
};
assert_eq!(summary.reference().network().as_str(), "devnet");
assert_eq!(summary.reference().pubkey(), &ksp_store_api::Pubkey::new_from_array([1; 32]));
assert_eq!(summary.reference().slot(), u64::MAX);
assert_eq!(summary.reference().state_hash(), ksp_store_api::RawContentHash::new([3; 32]));
assert_eq!(summary.lamports(), u64::MAX);
assert_eq!(summary.owner(), &ksp_store_api::Pubkey::new_from_array([2; 32]));
assert!(summary.executable());
assert_eq!(summary.rent_epoch(), u64::MAX);
assert_eq!(summary.data_length_bytes(), 4);
return;
}
#[test]
fn v0_3_8_pre_005_inspection_summary_rejects_hostile_width_decimal_and_data_length() {
let network = network();
let mut bad_owner = inspection_row(4);
bad_owner.owner = std::option::Option::Some(vec![2; 31]);
assert_eq!(
super::decode_raw_account_inspection_summary(&network, bad_owner).err().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
);
let mut bad_slot = inspection_row(4);
bad_slot.slot_text = std::option::Option::Some("18446744073709551616-HOSTILE".to_owned());
let error = match super::decode_raw_account_inspection_summary(&network, bad_slot) {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => panic!("hostile account inspection slot unexpectedly decoded"),
};
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::DataInvalid);
assert!(!std::format!("{error:?}").contains("HOSTILE"));
let maximum = match i64::try_from(ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => panic!("account data bound must fit PostgreSQL BIGINT in test fixture"),
};
for length in [-1_i64, maximum.saturating_add(1)] {
let result = super::decode_raw_account_inspection_summary(&network, inspection_row(length));
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
}
return;
}
#[test] #[test]
fn pre_004_account_state_row_round_trips_complete_u64_domain_and_bytes() { fn pre_004_account_state_row_round_trips_complete_u64_domain_and_bytes() {
let network = network(); let network = network();

192
deltas/0.3.8/pre.005.md Normal file
View File

@@ -0,0 +1,192 @@
<!-- file: deltas/0.3.8/pre.005.md -->
<!-- version: 1 -->
# Delta `0.3.8-pre.005` — inspection PostgreSQL RawAccountState et dispatch Store
## Base requise
Base directe attendue :
```text
0.3.8-pre.004-fix.001
workspace.package.version = 0.3.8-pre.4.fix.1
```
Le gate opérateur de cette base est entièrement vert et le live proof RawTransaction a réellement passé sur PostgreSQL 17.
La livraison est :
```text
0.3.8-pre.005
workspace.package.version = 0.3.8-pre.5
commit = v0.3.8-pre.005
tag = aucun
```
## Objectif
Compléter le second vertical slice physique de l'API d'inspection backend-neutral :
```text
RawAccountStateInspectionRead
ksp-store-postgres-lib
-> ksp-store-lib::Store
```
Les deux familles RAW disposent alors de leur capability d'inspection random-access. Aucun branchement UI/DataTables, aucune migration et aucune capability destructive Account ne sont ajoutés.
## SQL d'inspection Account
Deux statements privés existent, un par direction canonique. Chaque appel exécute une seule instruction SQL comprenant :
```text
filtered_count
conditional total_count
LATERAL page
ORDER BY slot, pubkey, state_hash
LIMIT/OFFSET
```
`filtered_items` applique le filtre exact `pubkey` optionnel et le range slot inclusif. `total_items` reste le nombre exact de lignes `ksp_raw_account_states`. Lorsqu'aucun filtre n'est actif, le count filtré est réutilisé comme total.
Counts et page proviennent donc du même snapshot de statement PostgreSQL.
## Aucun transfert de data bytes
Le statement ne sélectionne jamais :
```text
account_row.data
```
Il projette uniquement :
```text
OCTET_LENGTH(account_row.data)::BIGINT AS data_length_bytes
```
Le summary reconstruit uniquement l'identité canonique, lamports, owner, executable, rent epoch et longueur des données. Aucun N+1 n'est introduit.
## Pagination et limites physiques
Le chemin inspection convertit avant I/O :
```text
limit -> i64 PostgreSQL
offset -> i64 PostgreSQL
```
Un limit non représentable renvoie `PageLimitUnsupported`; un offset non représentable renvoie `QueryInvalid`.
Les quatre statements Account historiques de pagination cursor/keyset restent OFFSET-free :
```text
LIST_ACCOUNT_STATES_ASC_SQL
LIST_ACCOUNT_STATES_DESC_SQL
LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL
LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL
```
L'OFFSET reste donc strictement local à l'inspection random-access.
## Façade Store et inventaire
`PostgresBackend` et `Store` implémentent désormais `RawAccountStateInspectionRead`, avec guard network côté façade avant dispatch.
L'inventaire RAW passe de 11 à 12 capabilities sur les deux couches :
```text
11 historiques incluant RawTransactionInspectionRead
+ RawAccountStateInspectionRead
= 12
```
Les inventaires Store/PostgreSQL restent exactement symétriques.
## Tests et live proof
Le delta ajoute ou renforce les preuves suivantes :
- conversion hostile limit/offset avant I/O ;
- summary data-free avec domaine `u64` complet ;
- rejet widths/décimaux/data length hostiles sans echo ;
- séparation explicite keyset OFFSET-free / inspection avec OFFSET ;
- SQL counted, single-statement, `OCTET_LENGTH(data)` et aucun data bytes ;
- inventaires exacts 12/12 ;
- live proof Account opt-in : filtre pubkey, counts, ordre asc/desc, offset, data length et page vide profonde.
## Fichiers ajoutés
```text
deltas/0.3.8/pre.005.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-lib/src/lib.rs
crates/ksp-store-lib/src/store.rs
crates/ksp-store-lib/tests/dependency_boundary.rs
crates/ksp-store-lib/tests/hardening_completeness.rs
crates/ksp-store-lib/tests/public_api.rs
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/raw_account.rs
crates/ksp-store-postgres-lib/src/runtime.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
crates/ksp-store-postgres-lib/tests/public_api.rs
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
docs/plans/029-V0_3_8_STORE_DESK_PLAN.md
docs/validation/025-V0_3_8_STORE_DESK.md
```
## Fichiers supprimés
```text
aucun
```
## Validations d'assemblage
Contrôles exécutés avant emballage :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (314 table(s), 698 file(s))
pre.005 structural contract audit: clean
workspace.package.version: 0.3.8-pre.5
Store RAW capability implementations: 12
PostgreSQL RAW capability implementations: 12
RawAccount keyset OFFSET occurrences: 0
RawAccount inspection OFFSET occurrences: 2
delta scope: 1 ajout / 16 modifications / 0 suppression
```
Le sandbox d'assemblage ne dispose pas de `cargo`/`rustfmt`; aucun gate Cargo de `pre.005` n'est donc revendiqué localement.
## Gate requis
```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
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-store-lib
cargo check -p ksp-store-lib --no-default-features
cargo test -p ksp-store-api
```
Live proof opt-in, uniquement sur base PostgreSQL dédiée vide :
```bash
read -rsp 'Dedicated PostgreSQL URI: ' KSP_LIVE_PG_URI
printf '\n'
printf '%s\n' "$KSP_LIVE_PG_URI" | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture
unset KSP_LIVE_PG_URI
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md --> <!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Plan v0.3.8 — Store Desk V1 RAW # Plan v0.3.8 — Store Desk V1 RAW
@@ -572,7 +572,7 @@ Implémenter summary query, counts exacts, offset/limit checked et trait Tx dans
### pre.005 — inspection PostgreSQL RawAccountState ### pre.005 — inspection PostgreSQL RawAccountState
Même vertical slice pour Account : pubkey/slot filters, ordre canonique, data length sans data bytes, counts exacts, offset/limit checked, façade et canaris cross-family. Même vertical slice pour Account : filtre exact `pubkey`, range slot inclusif, ordre canonique `(slot, pubkey, state_hash)`, `data_length_bytes` via `OCTET_LENGTH(data)` sans sélectionner `data`, counts exacts et page `LIMIT/OFFSET` dans une seule instruction SQL par direction. `RawAccountStateInspectionRead` est implémenté sur PostgreSQL puis dispatché par `Store`, ce qui porte l'inventaire symétrique à 12 capabilities RAW. Les quatre statements cursor/keyset Account restent strictement OFFSET-free et ne sont jamais réécrits. Le live proof opt-in couvre filtre pubkey, counts, asc/desc, offset et page vide profonde.
### pre.006 — composite Config + lifecycle Store + Overview ### pre.006 — composite Config + lifecycle Store + Overview

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md --> <!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
<!-- version: 7 --> <!-- version: 8 -->
# Validation v0.3.8 — Store Desk V1 RAW # Validation v0.3.8 — Store Desk V1 RAW
@@ -358,4 +358,49 @@ unset KSP_LIVE_PG_URI
``` ```
Aucun SQL, contrat Store, runtime PostgreSQL ou comportement d'inspection de production n'est modifié par ce fix. Aucun SQL, contrat Store, runtime PostgreSQL ou comportement d'inspection de production n'est modifié par ce fix.
## 20. Gate opérateur `pre.004-fix.001` et live proof PostgreSQL
Le rejeu opérateur sur `0.3.8-pre.4.fix.1` est entièrement vert :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (314 table(s), 697 file(s))
cargo check --workspace: PASS
cargo clippy --workspace --all-targets: PASS
cargo test -p ksp-store-postgres-lib: PASS
cargo test -p ksp-store-lib: PASS
cargo check -p ksp-store-lib --no-default-features: PASS
cargo test -p ksp-store-api: PASS
```
Le live proof `postgres_raw_transaction_live` a ensuite été exécuté sur une base PostgreSQL dédiée vide. Il atteint réellement PostgreSQL 17 et passe :
```text
KSP Store RawTransaction live proof: server major 17
test pre_009_real_postgres_raw_transaction_vertical_slice_is_atomic_concurrent_and_recoverable ... ok
```
`pre.004` est donc fermé sans réserve et `pre.005` peut ouvrir uniquement le vertical slice `RawAccountStateInspectionRead`.
## 21. `pre.005` — inspection PostgreSQL RawAccountState + façade Store
- [X] `workspace.package.version = 0.3.8-pre.5` ;
- [X] `PostgresBackend` implémente `RawAccountStateInspectionRead` ;
- [X] `Store` implémente le même trait avec guard network avant dispatch ;
- [X] les inventaires RAW Store/PostgreSQL passent de 11 à 12 et restent exactement symétriques ;
- [X] chaque direction d'inspection Account utilise une seule instruction SQL regroupant `filtered_count`, `total_count` conditionnel et page LATERAL ;
- [X] `filtered_items` applique le filtre exact `pubkey` optionnel et le range slot inclusif ;
- [X] `total_items` compte toute la famille `ksp_raw_account_states` et réutilise `filtered_count` lorsqu'aucun filtre n'est actif ;
- [X] l'ordre inspection est canonique `(slot, pubkey, state_hash)` en ascendant ou descendant ;
- [X] la page random-access utilise `LIMIT/OFFSET` uniquement dans le chemin inspection ;
- [X] limit/offset non représentables par PostgreSQL sont rejetés avant acquisition du pool ;
- [X] les quatre statements cursor/keyset Account historiques restent sans `OFFSET` ;
- [X] le SQL d'inspection ne sélectionne jamais `account_row.data` et projette seulement `OCTET_LENGTH(account_row.data)` ;
- [X] `RawAccountStateSummary` est reconstruit avec reference, lamports, owner, executable, rent epoch et data length uniquement ;
- [X] longueurs, widths et décimaux hostiles sont rejetés sans écho de valeur ;
- [X] une page vide obtenue par offset profond conserve les counts exacts ;
- [X] le live proof Account opt-in couvre filtre pubkey, counts, ordre asc/desc, offset, data length et page vide profonde ;
- [X] aucune migration, table, index, capability destructive Account, UI Desk ou protocole DataTables n'est ajouté.