0.1.0-pre.004

This commit is contained in:
2026-07-23 18:25:10 +02:00
parent 0da75c1311
commit 149d4c6ef6
85 changed files with 25696 additions and 227 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
// file: kb-store/src/postgres/query.rs
// version: 1
//! PostgreSQL query modules.
mod core_extraction_queries;
mod core_queries;
mod decode_pipeline_queries;
mod health_queries;
mod raw_queries;
mod replay_candidate_queries;
mod table_diagnostics_queries;
pub(in crate::postgres) use crate::postgres::query::core_extraction_queries::is_core_extraction_current;
pub(in crate::postgres) use crate::postgres::query::core_extraction_queries::list_raw_transactions_for_core_extraction;
pub(in crate::postgres) use crate::postgres::query::core_extraction_queries::mark_core_extraction_failed;
pub(in crate::postgres) use crate::postgres::query::core_extraction_queries::persist_core_extraction;
pub(in crate::postgres) use crate::postgres::query::core_queries::apply_core_store_schema;
pub(in crate::postgres) use crate::postgres::query::core_queries::insert_core_account_keys;
pub(in crate::postgres) use crate::postgres::query::core_queries::insert_core_balance_changes;
pub(in crate::postgres) use crate::postgres::query::core_queries::insert_core_inner_instructions;
pub(in crate::postgres) use crate::postgres::query::core_queries::insert_core_instructions;
pub(in crate::postgres) use crate::postgres::query::core_queries::insert_core_logs;
pub(in crate::postgres) use crate::postgres::query::core_queries::insert_core_transaction;
pub(in crate::postgres) use crate::postgres::query::core_queries::list_core_instruction_replay_inputs;
pub(in crate::postgres) use crate::postgres::query::core_queries::list_core_instructions_for_replay;
pub(in crate::postgres) use crate::postgres::query::core_queries::update_core_instruction_lifecycle;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::apply_decode_store_schema;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::is_decode_current;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::list_decode_coverage_summary;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::list_decode_inputs;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::list_materialized_events;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::mark_decode_failed;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::persist_decode_coverage_declarations;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::persist_decode_result;
pub(in crate::postgres) use crate::postgres::query::decode_pipeline_queries::persist_materialization_result;
pub(in crate::postgres) use crate::postgres::query::health_queries::load_current_schema;
pub(in crate::postgres) use crate::postgres::query::health_queries::load_latest_migration_version;
pub(in crate::postgres) use crate::postgres::query::health_queries::load_migration_table_name;
pub(in crate::postgres) use crate::postgres::query::health_queries::load_server_version;
pub(in crate::postgres) use crate::postgres::query::health_queries::run_health_check;
pub(in crate::postgres) use crate::postgres::query::raw_queries::apply_raw_store_schema;
pub(in crate::postgres) use crate::postgres::query::raw_queries::has_raw_transaction_signature;
pub(in crate::postgres) use crate::postgres::query::raw_queries::has_transaction_observation_key;
pub(in crate::postgres) use crate::postgres::query::raw_queries::insert_raw_transaction;
pub(in crate::postgres) use crate::postgres::query::raw_queries::insert_transaction_observation;
pub(in crate::postgres) use crate::postgres::query::raw_queries::update_raw_payload_lifecycle;
pub(in crate::postgres) use crate::postgres::query::replay_candidate_queries::list_replay_entity_summaries;
pub(in crate::postgres) use crate::postgres::query::replay_candidate_queries::list_replay_program_summaries;
pub(in crate::postgres) use crate::postgres::query::replay_candidate_queries::list_replay_transaction_candidates;
pub(in crate::postgres) use crate::postgres::query::table_diagnostics_queries::load_table_statistics;
pub(in crate::postgres) use crate::postgres::query::table_diagnostics_queries::table_exists;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
// file: kb-store/src/postgres/query/health_queries.rs
// version: 1
//! PostgreSQL health and diagnostic SQL queries.
pub(in crate::postgres) async fn run_health_check(pool: &sqlx::PgPool) -> kb_core::Result<()> {
let query_result = sqlx::query_scalar::<sqlx::Postgres, i32>("SELECT 1").fetch_one(pool).await;
return match query_result {
std::result::Result::Ok(_value) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres healthcheck failed: {error}"
))),
};
}
pub(in crate::postgres) async fn load_current_schema(
pool: &sqlx::PgPool,
) -> kb_core::Result<std::string::String> {
let query_result =
sqlx::query_scalar::<sqlx::Postgres, std::string::String>("SELECT current_schema()")
.fetch_one(pool)
.await;
return match query_result {
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres current schema query failed: {error}"
))),
};
}
pub(in crate::postgres) async fn load_server_version(
pool: &sqlx::PgPool,
) -> kb_core::Result<std::string::String> {
let query_result =
sqlx::query_scalar::<sqlx::Postgres, std::string::String>("SELECT version()")
.fetch_one(pool)
.await;
return match query_result {
std::result::Result::Ok(version) => std::result::Result::Ok(version),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres version query failed: {error}"
))),
};
}
pub(in crate::postgres) async fn load_migration_table_name(
pool: &sqlx::PgPool,
) -> kb_core::Result<std::option::Option<std::string::String>> {
let query_result =
sqlx::query_scalar::<sqlx::Postgres, std::option::Option<std::string::String>>(
"SELECT to_regclass('_sqlx_migrations')::text",
)
.fetch_one(pool)
.await;
return match query_result {
std::result::Result::Ok(table_name) => std::result::Result::Ok(table_name),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres migration table query failed: {error}"
))),
};
}
pub(in crate::postgres) async fn load_latest_migration_version(
pool: &sqlx::PgPool,
) -> kb_core::Result<std::option::Option<std::string::String>> {
let query_result = sqlx::query_scalar::<sqlx::Postgres, std::option::Option<std::string::String>>(
"SELECT version::text FROM _sqlx_migrations WHERE success = true ORDER BY version DESC LIMIT 1",
)
.fetch_optional(pool)
.await;
return match query_result {
std::result::Result::Ok(version) => std::result::Result::Ok(version.flatten()),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres latest migration query failed: {error}"
))),
};
}

View File

@@ -0,0 +1,506 @@
// file: kb-store/src/postgres/query/raw_queries.rs
// version: 2
//! PostgreSQL canonical transaction and acquisition observation SQL queries.
pub(in crate::postgres) async fn apply_raw_store_schema(
pool: &sqlx::PgPool,
) -> kb_core::Result<()> {
let validation_result = crate::validate_raw_store_table_names();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let transaction_result = pool.begin().await;
let mut transaction = match transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres canonical raw store schema transaction failed: {error}"
)));
},
};
let lock_result = sqlx::query("SELECT pg_advisory_xact_lock($1)")
.bind(crate::STORE_SCHEMA_ADVISORY_LOCK_ID)
.execute(&mut *transaction)
.await;
if let std::result::Result::Err(error) = lock_result {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres canonical raw store schema advisory lock failed: {error}"
)));
}
for statement in crate::raw_store_schema_statements() {
let execution_result = sqlx::query(statement).execute(&mut *transaction).await;
if let std::result::Result::Err(error) = execution_result {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres canonical raw store schema statement failed: {error}"
)));
}
}
let commit_result = transaction.commit().await;
if let std::result::Result::Err(error) = commit_result {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres canonical raw store schema commit failed: {error}"
)));
}
return std::result::Result::Ok(());
}
pub(in crate::postgres) async fn has_raw_transaction_signature(
pool: &sqlx::PgPool,
signature: &str,
) -> kb_core::Result<bool> {
let validation_result = crate::postgres::query::raw_queries::validate_required_text(
signature,
"canonical raw transaction signature must not be empty",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let query_result = sqlx::query_scalar::<sqlx::Postgres, bool>(
"SELECT EXISTS(SELECT 1 FROM kb_sol_raw_transactions WHERE signature = $1)",
)
.bind(signature)
.fetch_one(pool)
.await;
return match query_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres canonical transaction signature lookup failed: {error}"
))),
};
}
pub(in crate::postgres) async fn has_transaction_observation_key(
pool: &sqlx::PgPool,
observation_key: &str,
) -> kb_core::Result<bool> {
let validation_result = crate::postgres::query::raw_queries::validate_required_text(
observation_key,
"transaction observation key must not be empty",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let query_result = sqlx::query_scalar::<sqlx::Postgres, bool>(
"SELECT EXISTS(SELECT 1 FROM kb_sol_obs_transaction_observations WHERE observation_key = $1)",
)
.bind(observation_key)
.fetch_one(pool)
.await;
return match query_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres transaction observation lookup failed: {error}"
))),
};
}
pub(in crate::postgres) async fn insert_raw_transaction(
pool: &sqlx::PgPool,
input: &crate::RawTransactionInsert,
) -> kb_core::Result<crate::InsertOutcome> {
let slot_result = crate::postgres::query::raw_queries::sql_slot_from_u64(input.slot);
let slot = match slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let version_result = i32::try_from(input.canonical_format_version);
let canonical_format_version = match version_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"canonical transaction format version does not fit into SQL INTEGER: {error}"
)));
},
};
let query_result = sqlx::query_scalar::<sqlx::Postgres, i64>(
"INSERT INTO kb_sol_raw_transactions (signature, slot, canonical_json, canonical_json_hash, canonical_format_version, retention_state, processing_state) VALUES ($1, $2, $3, $4, $5, 'full', 'received') ON CONFLICT (signature) DO NOTHING RETURNING id",
)
.bind(input.signature.as_str())
.bind(slot)
.bind(&input.canonical_json)
.bind(input.canonical_json_hash.as_deref())
.bind(canonical_format_version)
.fetch_optional(pool)
.await;
return match query_result {
std::result::Result::Ok(std::option::Option::Some(_id)) => {
std::result::Result::Ok(crate::InsertOutcome::new(1, 0, 0))
},
std::result::Result::Ok(std::option::Option::None) => {
std::result::Result::Ok(crate::InsertOutcome::new(0, 0, 1))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres canonical transaction insert failed: {error}"
))),
};
}
pub(in crate::postgres) async fn insert_transaction_observation(
pool: &sqlx::PgPool,
input: &crate::TransactionObservationInsert,
) -> kb_core::Result<crate::InsertOutcome> {
let slot_result = crate::postgres::query::raw_queries::optional_sql_slot_from_u64(input.slot);
let slot = match slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payload_size_result =
crate::postgres::query::raw_queries::optional_sql_bigint_from_u64(input.payload_size_bytes);
let payload_size_bytes = match payload_size_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let origin =
crate::postgres::query::raw_queries::transaction_observation_origin_to_sql(input.origin);
let status =
crate::postgres::query::raw_queries::transaction_observation_status_to_sql(input.status);
let query_result = sqlx::query_scalar::<sqlx::Postgres, i64>(
"INSERT INTO kb_sol_obs_transaction_observations (raw_transaction_id, observation_key, signature, slot, provider, endpoint_code, protocol, acquisition_method, origin, commitment, capture_session_id, filter_code, detected_at, received_at, normalized_at, payload_size_bytes, source_payload_hash, status, error_code, error_message) VALUES (COALESCE($1, (SELECT id FROM kb_sol_raw_transactions WHERE signature = $3 LIMIT 1)), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) ON CONFLICT (observation_key) DO NOTHING RETURNING id",
)
.bind(input.raw_transaction_id)
.bind(input.observation_key.as_str())
.bind(input.signature.as_deref())
.bind(slot)
.bind(input.provider.as_str())
.bind(input.endpoint_code.as_deref())
.bind(input.protocol.as_str())
.bind(input.acquisition_method.as_str())
.bind(origin)
.bind(input.commitment.as_deref())
.bind(input.capture_session_id.as_deref())
.bind(input.filter_code.as_deref())
.bind(input.detected_at.as_ref())
.bind(input.received_at)
.bind(input.normalized_at.as_ref())
.bind(payload_size_bytes)
.bind(input.source_payload_hash.as_deref())
.bind(status)
.bind(input.error_code.as_deref())
.bind(input.error_message.as_deref())
.fetch_optional(pool)
.await;
return match query_result {
std::result::Result::Ok(std::option::Option::Some(_id)) => {
std::result::Result::Ok(crate::InsertOutcome::new(1, 0, 0))
},
std::result::Result::Ok(std::option::Option::None) => {
std::result::Result::Ok(crate::InsertOutcome::new(0, 0, 1))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres transaction observation insert failed: {error}"
))),
};
}
pub(in crate::postgres) async fn update_raw_payload_lifecycle(
pool: &sqlx::PgPool,
mark: &crate::RawPayloadLifecycleMark,
) -> kb_core::Result<crate::InsertOutcome> {
if mark.raw_table_name != crate::RAW_TRANSACTIONS_TABLE_NAME {
return std::result::Result::Err(kb_core::Error::db(
"raw lifecycle table name is not supported by the PostgreSQL canonical raw store",
));
}
return crate::postgres::query::raw_queries::update_raw_transaction_lifecycle(pool, mark).await;
}
fn raw_retention_state_to_sql(state: crate::RawPayloadRetentionState) -> &'static str {
return match state {
crate::RawPayloadRetentionState::Full => "full",
crate::RawPayloadRetentionState::Compacted => "compacted",
crate::RawPayloadRetentionState::Archived => "archived",
crate::RawPayloadRetentionState::Purged => "purged",
};
}
fn raw_processing_state_to_sql(state: crate::RawPayloadProcessingState) -> &'static str {
return match state {
crate::RawPayloadProcessingState::Received => "received",
crate::RawPayloadProcessingState::CoreExtracted => "core_extracted",
crate::RawPayloadProcessingState::Decoded => "decoded",
crate::RawPayloadProcessingState::Materialized => "materialized",
crate::RawPayloadProcessingState::Failed => "failed",
};
}
fn transaction_observation_origin_to_sql(
origin: crate::TransactionObservationOrigin,
) -> &'static str {
return match origin {
crate::TransactionObservationOrigin::Live => "live",
crate::TransactionObservationOrigin::Backfill => "backfill",
crate::TransactionObservationOrigin::Replay => "replay",
crate::TransactionObservationOrigin::Repair => "repair",
crate::TransactionObservationOrigin::Migration => "migration",
};
}
fn transaction_observation_status_to_sql(
status: crate::TransactionObservationStatus,
) -> &'static str {
return match status {
crate::TransactionObservationStatus::Detected => "detected",
crate::TransactionObservationStatus::Received => "received",
crate::TransactionObservationStatus::Normalized => "normalized",
crate::TransactionObservationStatus::Persisted => "persisted",
crate::TransactionObservationStatus::Failed => "failed",
crate::TransactionObservationStatus::Missing => "missing",
};
}
fn sql_slot_from_u64(slot: u64) -> kb_core::Result<i64> {
let conversion_result = i64::try_from(slot);
return match conversion_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"Solana slot does not fit into PostgreSQL BIGINT: {error}"
))),
};
}
fn optional_sql_slot_from_u64(
slot: std::option::Option<u64>,
) -> kb_core::Result<std::option::Option<i64>> {
return crate::postgres::query::raw_queries::optional_sql_bigint_from_u64(slot);
}
fn optional_sql_bigint_from_u64(
value: std::option::Option<u64>,
) -> kb_core::Result<std::option::Option<i64>> {
return match value {
std::option::Option::Some(raw_value) => {
let conversion_result = i64::try_from(raw_value);
match conversion_result {
std::result::Result::Ok(converted) => {
std::result::Result::Ok(std::option::Option::Some(converted))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(
format!("unsigned value does not fit into PostgreSQL BIGINT: {error}"),
)),
}
},
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
};
}
fn validate_required_text(value: &str, message: &str) -> kb_core::Result<()> {
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(message));
}
return std::result::Result::Ok(());
}
async fn update_raw_transaction_lifecycle(
pool: &sqlx::PgPool,
mark: &crate::RawPayloadLifecycleMark,
) -> kb_core::Result<crate::InsertOutcome> {
let retention_state =
crate::postgres::query::raw_queries::raw_retention_state_to_sql(mark.retention_state);
let processing_state =
crate::postgres::query::raw_queries::raw_processing_state_to_sql(mark.processing_state);
let query_result = sqlx::query(
"UPDATE kb_sol_raw_transactions SET retention_state = $1, processing_state = $2, lifecycle_reason = $3, updated_at = NOW() WHERE signature = $4",
)
.bind(retention_state)
.bind(processing_state)
.bind(mark.reason.as_deref())
.bind(mark.raw_row_key.as_str())
.execute(pool)
.await;
return crate::postgres::query::raw_queries::outcome_from_update_result(
query_result,
"postgres canonical raw transaction lifecycle update failed",
);
}
fn outcome_from_update_result(
query_result: std::result::Result<sqlx::postgres::PgQueryResult, sqlx::Error>,
error_prefix: &str,
) -> kb_core::Result<crate::InsertOutcome> {
return match query_result {
std::result::Result::Ok(result) => {
let rows_affected = result.rows_affected();
if rows_affected == 0 {
return std::result::Result::Ok(crate::InsertOutcome::new(0, 0, 1));
}
std::result::Result::Ok(crate::InsertOutcome::new(0, rows_affected, 0))
},
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::db(format!("{error_prefix}: {error}")))
},
};
}
#[cfg(test)]
mod tests {
#[test]
fn retention_state_serializes_to_lower_snake_case() {
let value = crate::postgres::query::raw_queries::raw_retention_state_to_sql(
crate::RawPayloadRetentionState::Compacted,
);
assert_eq!(value, "compacted");
}
#[test]
fn processing_state_serializes_to_lower_snake_case() {
let value = crate::postgres::query::raw_queries::raw_processing_state_to_sql(
crate::RawPayloadProcessingState::CoreExtracted,
);
assert_eq!(value, "core_extracted");
}
#[test]
fn observation_origin_serializes_to_lower_snake_case() {
let value = crate::postgres::query::raw_queries::transaction_observation_origin_to_sql(
crate::TransactionObservationOrigin::Backfill,
);
assert_eq!(value, "backfill");
}
#[test]
fn observation_status_serializes_to_lower_snake_case() {
let value = crate::postgres::query::raw_queries::transaction_observation_status_to_sql(
crate::TransactionObservationStatus::Normalized,
);
assert_eq!(value, "normalized");
}
#[test]
fn sql_slot_rejects_values_above_bigint() {
let result = crate::postgres::query::raw_queries::sql_slot_from_u64(u64::MAX);
assert!(result.is_err());
}
#[tokio::test]
async fn optional_postgres_canonical_store_roundtrip_from_env() {
let database_url = match std::env::var("KB_POSTGRES_TEST_URL") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => return,
};
let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await;
let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false);
let options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected options error: {error}"),
};
let store_result = crate::PostgresStore::connect(options).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected connect error: {error}"),
};
let schema_result = store.initialize_raw_store_schema().await;
if let std::result::Result::Err(error) = schema_result {
panic!("unexpected schema error: {error}");
}
let signature = test_signature();
let raw_input_result = crate::RawTransactionInsert::new(
signature.clone(),
1,
serde_json::json!({"source": "test"}),
1,
);
let raw_input = match raw_input_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected raw input error: {error}"),
};
let first_result =
crate::RawTransactionStore::insert_raw_transaction(&store, &raw_input).await;
let first_outcome = match first_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected first insert error: {error}"),
};
assert_eq!(first_outcome.inserted_count, 1);
let duplicate_result =
crate::RawTransactionStore::insert_raw_transaction(&store, &raw_input).await;
let duplicate_outcome = match duplicate_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected duplicate insert error: {error}"),
};
assert_eq!(duplicate_outcome.skipped_count, 1);
let has_result = crate::RawTransactionStore::has_raw_transaction_signature(
&store,
&kb_lib::Signature(signature.clone()),
)
.await;
let has_signature = match has_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected signature lookup error: {error}"),
};
assert!(has_signature);
let observation_key = std::string::String::from("test:http:") + signature.as_str();
let observation_result = crate::TransactionObservationInsert::new(
observation_key.clone(),
"test_provider",
"solana_http",
"getTransaction",
crate::TransactionObservationOrigin::Backfill,
chrono::Utc::now(),
);
let observation = match observation_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected observation error: {error}"),
};
let identity_result =
observation.with_transaction_identity(signature.clone(), std::option::Option::Some(1));
let observation_with_identity = match identity_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected identity error: {error}"),
};
let observation_insert_result = crate::RawTransactionStore::insert_transaction_observation(
&store,
&observation_with_identity,
)
.await;
let observation_outcome = match observation_insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("unexpected observation insert error: {error}")
},
};
assert_eq!(observation_outcome.inserted_count, 1);
let observation_lookup_result =
crate::RawTransactionStore::has_transaction_observation_key(
&store,
observation_key.as_str(),
)
.await;
let has_observation = match observation_lookup_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("unexpected observation lookup error: {error}")
},
};
assert!(has_observation);
let mark_result = crate::RawPayloadLifecycleMark::new(
crate::RAW_TRANSACTIONS_TABLE_NAME,
signature,
crate::RawPayloadRetentionState::Full,
crate::RawPayloadProcessingState::CoreExtracted,
std::option::Option::Some(std::string::String::from("test extraction")),
);
let mark = match mark_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected mark error: {error}"),
};
let mark_outcome_result =
crate::RawTransactionStore::mark_raw_payload_lifecycle(&store, &mark).await;
let mark_outcome = match mark_outcome_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected lifecycle update error: {error}"),
};
assert_eq!(mark_outcome.updated_count, 1);
return;
}
fn test_signature() -> std::string::String {
let now_result = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
let duration = match now_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => {
return std::string::String::from("test_signature_fallback");
},
};
return format!("test_signature_{}", duration.as_nanos());
}
}

View File

@@ -0,0 +1,474 @@
// file: kb-store/src/postgres/query/replay_candidate_queries.rs
// version: 2
//! Read-only PostgreSQL queries for replay candidate discovery.
#[derive(sqlx::FromRow)]
struct ReplayTransactionCandidateRow {
signature: std::string::String,
slot: i64,
raw_processing_state: std::string::String,
retention_state: std::string::String,
has_core_transaction: bool,
transaction_failed: std::option::Option<bool>,
ledger_status: std::string::String,
processor_version: std::option::Option<std::string::String>,
attempt_count: i32,
outer_instruction_count: i64,
inner_instruction_count: i64,
outer_program_count: i64,
inner_program_count: i64,
updated_at: std::string::String,
}
#[derive(sqlx::FromRow)]
struct ReplayProgramSummaryRow {
program_id: std::string::String,
transaction_count: i64,
outer_instruction_count: i64,
inner_instruction_count: i64,
log_count: i64,
min_slot: i64,
max_slot: i64,
}
#[derive(sqlx::FromRow)]
struct ReplayEntitySummaryRow {
entity_kind: std::string::String,
entity_value: std::string::String,
transaction_count: i64,
occurrence_count: i64,
min_slot: i64,
max_slot: i64,
}
pub(in crate::postgres) async fn list_replay_transaction_candidates(
pool: &sqlx::PgPool,
filter: &crate::PostgresReplayTransactionFilter,
) -> kb_core::Result<std::vec::Vec<crate::PostgresReplayTransactionCandidate>> {
let min_slot_result =
crate::postgres::query::replay_candidate_queries::optional_sql_bigint(filter.min_slot);
let min_slot = match min_slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_slot_result =
crate::postgres::query::replay_candidate_queries::optional_sql_bigint(filter.max_slot);
let max_slot = match max_slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entity_kind = match filter.entity_kind {
std::option::Option::Some(value) => std::option::Option::Some(value.as_sql()),
std::option::Option::None => std::option::Option::None,
};
let sql = if filter.newest_first {
crate::postgres::query::replay_candidate_queries::transaction_candidate_sql_desc()
} else {
crate::postgres::query::replay_candidate_queries::transaction_candidate_sql_asc()
};
let query_result = sqlx::query_as::<
sqlx::Postgres,
crate::postgres::query::replay_candidate_queries::ReplayTransactionCandidateRow,
>(sql)
.bind(filter.signature_contains.as_deref())
.bind(min_slot)
.bind(max_slot)
.bind(filter.raw_processing_state.as_deref())
.bind(filter.ledger_status.as_deref())
.bind(filter.program_id.as_deref())
.bind(filter.program_scope.as_sql())
.bind(entity_kind)
.bind(filter.entity_value.as_deref())
.bind(i64::from(filter.limit))
.fetch_all(pool)
.await;
let rows = match query_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres replay transaction candidate query failed: {error}"
)));
},
};
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(crate::PostgresReplayTransactionCandidate {
signature: row.signature,
slot: row.slot,
raw_processing_state: row.raw_processing_state,
retention_state: row.retention_state,
has_core_transaction: row.has_core_transaction,
transaction_failed: row.transaction_failed,
ledger_status: row.ledger_status,
processor_version: row.processor_version,
attempt_count: row.attempt_count,
outer_instruction_count: row.outer_instruction_count,
inner_instruction_count: row.inner_instruction_count,
outer_program_count: row.outer_program_count,
inner_program_count: row.inner_program_count,
updated_at: row.updated_at,
});
}
return std::result::Result::Ok(output);
}
pub(in crate::postgres) async fn list_replay_program_summaries(
pool: &sqlx::PgPool,
filter: &crate::PostgresReplayProgramFilter,
) -> kb_core::Result<std::vec::Vec<crate::PostgresReplayProgramSummary>> {
let query_result = sqlx::query_as::<sqlx::Postgres, crate::postgres::query::replay_candidate_queries::ReplayProgramSummaryRow>(
r#"WITH occurrences AS (
SELECT program_id, signature, slot, 'outer'::TEXT AS scope FROM kb_sol_core_instructions
UNION ALL
SELECT program_id, signature, slot, 'inner'::TEXT AS scope FROM kb_sol_core_inner_instructions
UNION ALL
SELECT program_id, signature, slot, 'logs'::TEXT AS scope FROM kb_sol_core_logs WHERE program_id IS NOT NULL
)
SELECT program_id,
COUNT(DISTINCT signature)::BIGINT AS transaction_count,
COUNT(*) FILTER (WHERE scope = 'outer')::BIGINT AS outer_instruction_count,
COUNT(*) FILTER (WHERE scope = 'inner')::BIGINT AS inner_instruction_count,
COUNT(*) FILTER (WHERE scope = 'logs')::BIGINT AS log_count,
MIN(slot)::BIGINT AS min_slot,
MAX(slot)::BIGINT AS max_slot
FROM occurrences
WHERE ($1::TEXT IS NULL OR program_id ILIKE '%' || $1 || '%')
GROUP BY program_id
ORDER BY transaction_count DESC, program_id ASC
LIMIT $2"#,
)
.bind(filter.program_id_contains.as_deref())
.bind(i64::from(filter.limit))
.fetch_all(pool)
.await;
let rows = match query_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres replay program summary query failed: {error}"
)));
},
};
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(crate::PostgresReplayProgramSummary {
program_id: row.program_id,
transaction_count: row.transaction_count,
outer_instruction_count: row.outer_instruction_count,
inner_instruction_count: row.inner_instruction_count,
log_count: row.log_count,
min_slot: row.min_slot,
max_slot: row.max_slot,
});
}
return std::result::Result::Ok(output);
}
pub(in crate::postgres) async fn list_replay_entity_summaries(
pool: &sqlx::PgPool,
filter: &crate::PostgresReplayEntityFilter,
) -> kb_core::Result<std::vec::Vec<crate::PostgresReplayEntitySummary>> {
let entity_kind = filter.entity_kind.as_sql();
let query_result = sqlx::query_as::<
sqlx::Postgres,
crate::postgres::query::replay_candidate_queries::ReplayEntitySummaryRow,
>(
r#"WITH entities AS (
SELECT 'mint'::TEXT AS entity_kind, mint AS entity_value, signature, slot
FROM kb_sol_core_balance_changes
WHERE mint IS NOT NULL
UNION ALL
SELECT 'owner'::TEXT AS entity_kind, owner AS entity_value, signature, slot
FROM kb_sol_core_balance_changes
WHERE owner IS NOT NULL
UNION ALL
SELECT 'account_key'::TEXT AS entity_kind, account_key AS entity_value, signature, slot
FROM kb_sol_core_account_keys
)
SELECT entity_kind,
entity_value,
COUNT(DISTINCT signature)::BIGINT AS transaction_count,
COUNT(*)::BIGINT AS occurrence_count,
MIN(slot)::BIGINT AS min_slot,
MAX(slot)::BIGINT AS max_slot
FROM entities
WHERE entity_kind = $1
AND ($2::TEXT IS NULL OR entity_value ILIKE '%' || $2 || '%')
GROUP BY entity_kind, entity_value
ORDER BY transaction_count DESC, entity_value ASC
LIMIT $3"#,
)
.bind(entity_kind)
.bind(filter.entity_value_contains.as_deref())
.bind(i64::from(filter.limit))
.fetch_all(pool)
.await;
let rows = match query_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres replay entity summary query failed: {error}"
)));
},
};
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(crate::PostgresReplayEntitySummary {
entity_kind: row.entity_kind,
entity_value: row.entity_value,
transaction_count: row.transaction_count,
occurrence_count: row.occurrence_count,
min_slot: row.min_slot,
max_slot: row.max_slot,
});
}
return std::result::Result::Ok(output);
}
fn transaction_candidate_sql_desc() -> &'static str {
return crate::postgres::query::replay_candidate_queries::transaction_candidate_sql("DESC");
}
fn transaction_candidate_sql_asc() -> &'static str {
return crate::postgres::query::replay_candidate_queries::transaction_candidate_sql("ASC");
}
fn transaction_candidate_sql(order: &str) -> &'static str {
if order == "DESC" {
return r#"SELECT raw.signature,
raw.slot,
raw.processing_state AS raw_processing_state,
raw.retention_state,
(core.id IS NOT NULL) AS has_core_transaction,
core.failed AS transaction_failed,
COALESCE(ledger.status, 'not_started') AS ledger_status,
ledger.processor_version,
COALESCE(ledger.attempt_count, 0)::INTEGER AS attempt_count,
COALESCE(outer_stats.instruction_count, 0)::BIGINT AS outer_instruction_count,
COALESCE(inner_stats.instruction_count, 0)::BIGINT AS inner_instruction_count,
COALESCE(outer_stats.program_count, 0)::BIGINT AS outer_program_count,
COALESCE(inner_stats.program_count, 0)::BIGINT AS inner_program_count,
raw.updated_at::TEXT AS updated_at
FROM kb_sol_raw_transactions raw
LEFT JOIN kb_sol_core_transactions core ON core.signature = raw.signature
LEFT JOIN LATERAL (
SELECT status, processor_version, attempt_count
FROM kb_sol_ops_processing_ledger
WHERE stage = 'core_extraction'
AND processor_name = 'canonical_to_core'
AND input_key = raw.signature
ORDER BY updated_at DESC, id DESC
LIMIT 1
) ledger ON TRUE
LEFT JOIN LATERAL (
SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count
FROM kb_sol_core_instructions
WHERE signature = raw.signature
) outer_stats ON TRUE
LEFT JOIN LATERAL (
SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count
FROM kb_sol_core_inner_instructions
WHERE signature = raw.signature
) inner_stats ON TRUE
WHERE ($1::TEXT IS NULL OR raw.signature ILIKE '%' || $1 || '%')
AND ($2::BIGINT IS NULL OR raw.slot >= $2)
AND ($3::BIGINT IS NULL OR raw.slot <= $3)
AND ($4::TEXT IS NULL OR raw.processing_state = $4)
AND ($5::TEXT IS NULL OR ($5 = 'not_started' AND ledger.status IS NULL) OR ledger.status = $5)
AND ($6::TEXT IS NULL OR
($7 = 'any' AND (
EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6) OR
EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6) OR
EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6)
)) OR
($7 = 'outer' AND EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6)) OR
($7 = 'inner' AND EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6)) OR
($7 = 'logs' AND EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6)))
AND ($8::TEXT IS NULL OR
($8 = 'mint' AND EXISTS (
SELECT 1 FROM kb_sol_core_balance_changes candidate_balance
WHERE candidate_balance.signature = raw.signature
AND candidate_balance.mint = $9
)) OR
($8 = 'owner' AND EXISTS (
SELECT 1 FROM kb_sol_core_balance_changes candidate_balance
WHERE candidate_balance.signature = raw.signature
AND candidate_balance.owner = $9
)) OR
($8 = 'account_key' AND EXISTS (
SELECT 1 FROM kb_sol_core_account_keys candidate_account
WHERE candidate_account.signature = raw.signature
AND candidate_account.account_key = $9
)))
ORDER BY raw.slot DESC, raw.signature DESC
LIMIT $10"#;
}
return r#"SELECT raw.signature,
raw.slot,
raw.processing_state AS raw_processing_state,
raw.retention_state,
(core.id IS NOT NULL) AS has_core_transaction,
core.failed AS transaction_failed,
COALESCE(ledger.status, 'not_started') AS ledger_status,
ledger.processor_version,
COALESCE(ledger.attempt_count, 0)::INTEGER AS attempt_count,
COALESCE(outer_stats.instruction_count, 0)::BIGINT AS outer_instruction_count,
COALESCE(inner_stats.instruction_count, 0)::BIGINT AS inner_instruction_count,
COALESCE(outer_stats.program_count, 0)::BIGINT AS outer_program_count,
COALESCE(inner_stats.program_count, 0)::BIGINT AS inner_program_count,
raw.updated_at::TEXT AS updated_at
FROM kb_sol_raw_transactions raw
LEFT JOIN kb_sol_core_transactions core ON core.signature = raw.signature
LEFT JOIN LATERAL (
SELECT status, processor_version, attempt_count
FROM kb_sol_ops_processing_ledger
WHERE stage = 'core_extraction'
AND processor_name = 'canonical_to_core'
AND input_key = raw.signature
ORDER BY updated_at DESC, id DESC
LIMIT 1
) ledger ON TRUE
LEFT JOIN LATERAL (
SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count
FROM kb_sol_core_instructions
WHERE signature = raw.signature
) outer_stats ON TRUE
LEFT JOIN LATERAL (
SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count
FROM kb_sol_core_inner_instructions
WHERE signature = raw.signature
) inner_stats ON TRUE
WHERE ($1::TEXT IS NULL OR raw.signature ILIKE '%' || $1 || '%')
AND ($2::BIGINT IS NULL OR raw.slot >= $2)
AND ($3::BIGINT IS NULL OR raw.slot <= $3)
AND ($4::TEXT IS NULL OR raw.processing_state = $4)
AND ($5::TEXT IS NULL OR ($5 = 'not_started' AND ledger.status IS NULL) OR ledger.status = $5)
AND ($6::TEXT IS NULL OR
($7 = 'any' AND (
EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6) OR
EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6) OR
EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6)
)) OR
($7 = 'outer' AND EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6)) OR
($7 = 'inner' AND EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6)) OR
($7 = 'logs' AND EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6)))
AND ($8::TEXT IS NULL OR
($8 = 'mint' AND EXISTS (
SELECT 1 FROM kb_sol_core_balance_changes candidate_balance
WHERE candidate_balance.signature = raw.signature
AND candidate_balance.mint = $9
)) OR
($8 = 'owner' AND EXISTS (
SELECT 1 FROM kb_sol_core_balance_changes candidate_balance
WHERE candidate_balance.signature = raw.signature
AND candidate_balance.owner = $9
)) OR
($8 = 'account_key' AND EXISTS (
SELECT 1 FROM kb_sol_core_account_keys candidate_account
WHERE candidate_account.signature = raw.signature
AND candidate_account.account_key = $9
)))
ORDER BY raw.slot ASC, raw.signature ASC
LIMIT $10"#;
}
fn optional_sql_bigint(
value: std::option::Option<u64>,
) -> kb_core::Result<std::option::Option<i64>> {
return match value {
std::option::Option::Some(raw_value) => {
let conversion_result = i64::try_from(raw_value);
match conversion_result {
std::result::Result::Ok(converted) => {
std::result::Result::Ok(std::option::Option::Some(converted))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(
format!("replay candidate slot does not fit into PostgreSQL BIGINT: {error}"),
)),
}
},
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
};
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn optional_postgres_replay_candidate_queries_from_env() {
let database_url = match std::env::var("KB_POSTGRES_TEST_URL") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => return,
};
let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await;
let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false);
let options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected options error: {error}"),
};
let store_result = crate::PostgresStore::connect(options).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected connect error: {error}"),
};
let raw_schema_result = store.initialize_raw_store_schema().await;
if let std::result::Result::Err(error) = raw_schema_result {
panic!("unexpected raw schema error: {error}");
}
let core_schema_result = store.initialize_core_store_schema().await;
if let std::result::Result::Err(error) = core_schema_result {
panic!("unexpected core schema error: {error}");
}
let transaction_filter_result = crate::PostgresReplayTransactionFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
crate::PostgresReplayProgramScope::Any,
std::option::Option::None,
std::option::Option::None,
10,
true,
);
let transaction_filter = match transaction_filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("unexpected transaction filter error: {error}")
},
};
let transaction_result = store.replay_transaction_candidates(&transaction_filter).await;
if let std::result::Result::Err(error) = transaction_result {
panic!("unexpected transaction candidate query error: {error}");
}
let program_filter_result =
crate::PostgresReplayProgramFilter::new(std::option::Option::None, 10);
let program_filter = match program_filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected program filter error: {error}"),
};
let program_result = store.replay_program_summaries(&program_filter).await;
if let std::result::Result::Err(error) = program_result {
panic!("unexpected program summary query error: {error}");
}
for entity_kind in [
crate::PostgresReplayEntityKind::Mint,
crate::PostgresReplayEntityKind::Owner,
crate::PostgresReplayEntityKind::AccountKey,
] {
let entity_filter_result =
crate::PostgresReplayEntityFilter::new(entity_kind, std::option::Option::None, 10);
let entity_filter = match entity_filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("unexpected entity filter error: {error}")
},
};
let entity_result = store.replay_entity_summaries(&entity_filter).await;
if let std::result::Result::Err(error) = entity_result {
panic!("unexpected entity summary query error: {error}");
}
}
}
}

View File

@@ -0,0 +1,197 @@
// file: kb-store/src/postgres/query/table_diagnostics_queries.rs
// version: 2
//! Read-only PostgreSQL diagnostics for known Solana store tables.
use sqlx::Row; // rust-rules: trait-import
pub(in crate::postgres) async fn table_exists(
pool: &sqlx::PgPool,
table_name: &str,
) -> kb_core::Result<bool> {
let query_result =
sqlx::query_scalar::<sqlx::Postgres, bool>("SELECT to_regclass($1)::text IS NOT NULL")
.bind(table_name)
.fetch_one(pool)
.await;
return match query_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(format!(
"postgres table existence diagnostic failed for '{table_name}': {error}"
))),
};
}
pub(in crate::postgres) async fn load_table_statistics(
pool: &sqlx::PgPool,
table_name: &str,
) -> kb_core::Result<crate::PostgresTableStatistics> {
return match table_name {
crate::RAW_TRANSACTIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_raw_transactions_sql(),
table_name,
)
.await
},
crate::TRANSACTION_OBSERVATIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_obs_transaction_observations_sql(),
table_name,
)
.await
},
crate::CORE_TRANSACTIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_core_transactions_sql(),
table_name,
)
.await
},
crate::CORE_ACCOUNT_KEYS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_core_account_keys_sql(),
table_name,
)
.await
},
crate::CORE_INSTRUCTIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_core_instructions_sql(),
table_name,
)
.await
},
crate::CORE_INNER_INSTRUCTIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_core_inner_instructions_sql(),
table_name,
)
.await
},
crate::CORE_LOGS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_core_logs_sql(),
table_name,
)
.await
},
crate::CORE_BALANCE_CHANGES_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_core_balance_changes_sql(),
table_name,
)
.await
},
crate::PROCESSING_LEDGER_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_ops_processing_ledger_sql(),
table_name,
)
.await
},
crate::DECODE_EVENTS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_decode_events_sql(),
table_name,
)
.await
},
crate::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_decode_coverage_declarations_sql(),
table_name,
)
.await
},
crate::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_decode_coverage_observations_sql(),
table_name,
)
.await
},
crate::MATERIALIZED_EVENTS_TABLE_NAME => {
crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql(
pool,
crate::postgres::migrations::table_stats_kb_sol_mat_events_sql(),
table_name,
)
.await
},
_ => std::result::Result::Err(kb_core::Error::db(format!(
"postgres table statistics are not supported for '{table_name}'"
))),
};
}
async fn load_table_statistics_from_sql(
pool: &sqlx::PgPool,
sql: &'static str,
table_name: &str,
) -> kb_core::Result<crate::PostgresTableStatistics> {
let query_result = sqlx::query(sql).fetch_one(pool).await;
let row = match query_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres table statistics failed for '{table_name}': {error}"
)));
},
};
let row_count_result = row.try_get::<i64, _>("row_count");
let row_count = match row_count_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres table row_count mapping failed for '{table_name}': {error}"
)));
},
};
let min_slot_result = row.try_get::<std::option::Option<i64>, _>("min_slot");
let min_slot = match min_slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres table min_slot mapping failed for '{table_name}': {error}"
)));
},
};
let max_slot_result = row.try_get::<std::option::Option<i64>, _>("max_slot");
let max_slot = match max_slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres table max_slot mapping failed for '{table_name}': {error}"
)));
},
};
let latest_created_at_result =
row.try_get::<std::option::Option<std::string::String>, _>("latest_created_at");
let latest_created_at = match latest_created_at_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::db(format!(
"postgres table latest_created_at mapping failed for '{table_name}': {error}"
)));
},
};
return std::result::Result::Ok(crate::PostgresTableStatistics {
row_count,
min_slot,
max_slot,
latest_created_at,
});
}

View File

@@ -0,0 +1,380 @@
// file: kb-store/src/postgres/replay_candidates.rs
// version: 1
//! Read-only replay candidate filters and PostgreSQL result rows.
/// Maximum number of rows returned by one replay candidate query.
pub const MAX_REPLAY_CANDIDATE_ROWS: u32 = 5_000;
/// Program occurrence scope used while filtering replay candidates.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PostgresReplayProgramScope {
/// Match outer instructions, inner instructions or reliably linked logs.
Any,
/// Match only top-level instructions.
Outer,
/// Match only inner instructions.
Inner,
/// Match only logs with a reliably linked program id.
Logs,
}
impl PostgresReplayProgramScope {
/// Returns the stable SQL code for this scope.
pub fn as_sql(self) -> &'static str {
return match self {
Self::Any => "any",
Self::Outer => "outer",
Self::Inner => "inner",
Self::Logs => "logs",
};
}
}
/// Core entity kind used while filtering replay candidates.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PostgresReplayEntityKind {
/// SPL or Token-2022 mint address.
Mint,
/// Token account owner address.
Owner,
/// Native or token account address.
AccountKey,
}
impl PostgresReplayEntityKind {
/// Returns the stable SQL code for this entity kind.
pub fn as_sql(self) -> &'static str {
return match self {
Self::Mint => "mint",
Self::Owner => "owner",
Self::AccountKey => "account_key",
};
}
}
/// Bounded read-only filter for transaction replay candidates.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostgresReplayTransactionFilter {
/// Optional partial signature search.
pub signature_contains: std::option::Option<std::string::String>,
/// Optional inclusive minimum slot.
pub min_slot: std::option::Option<u64>,
/// Optional inclusive maximum slot.
pub max_slot: std::option::Option<u64>,
/// Optional raw processing state.
pub raw_processing_state: std::option::Option<std::string::String>,
/// Optional latest core extraction ledger status, including `not_started`.
pub ledger_status: std::option::Option<std::string::String>,
/// Optional exact program id.
pub program_id: std::option::Option<std::string::String>,
/// Program occurrence scope.
pub program_scope: crate::PostgresReplayProgramScope,
/// Optional core entity kind.
pub entity_kind: std::option::Option<crate::PostgresReplayEntityKind>,
/// Optional exact entity value.
pub entity_value: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub limit: u32,
/// Orders newest slots first when true.
pub newest_first: bool,
}
impl PostgresReplayTransactionFilter {
/// Creates and validates a bounded transaction candidate filter.
#[allow(clippy::too_many_arguments)]
pub fn new(
signature_contains: std::option::Option<std::string::String>,
min_slot: std::option::Option<u64>,
max_slot: std::option::Option<u64>,
raw_processing_state: std::option::Option<std::string::String>,
ledger_status: std::option::Option<std::string::String>,
program_id: std::option::Option<std::string::String>,
program_scope: crate::PostgresReplayProgramScope,
entity_kind: std::option::Option<crate::PostgresReplayEntityKind>,
entity_value: std::option::Option<std::string::String>,
limit: u32,
newest_first: bool,
) -> kb_core::Result<Self> {
let signature_contains_value = trim_optional_text(signature_contains);
let raw_processing_state_value = trim_optional_text(raw_processing_state);
let ledger_status_value = trim_optional_text(ledger_status);
let program_id_value = trim_optional_text(program_id);
let entity_value_value = trim_optional_text(entity_value);
let slot_result = validate_slot_range(min_slot, max_slot);
if let std::result::Result::Err(error) = slot_result {
return std::result::Result::Err(error);
}
let limit_result = validate_limit(limit);
if let std::result::Result::Err(error) = limit_result {
return std::result::Result::Err(error);
}
let raw_state_result = validate_optional_code(
raw_processing_state_value.as_deref(),
&["received", "core_extracted", "decoded", "materialized", "failed"],
"raw processing state",
);
if let std::result::Result::Err(error) = raw_state_result {
return std::result::Result::Err(error);
}
let ledger_status_result = validate_optional_code(
ledger_status_value.as_deref(),
&["not_started", "running", "succeeded", "failed"],
"ledger status",
);
if let std::result::Result::Err(error) = ledger_status_result {
return std::result::Result::Err(error);
}
if entity_kind.is_some() != entity_value_value.is_some() {
return std::result::Result::Err(kb_core::Error::db(
"replay entity kind and value must either both be present or both be absent",
));
}
return std::result::Result::Ok(Self {
signature_contains: signature_contains_value,
min_slot,
max_slot,
raw_processing_state: raw_processing_state_value,
ledger_status: ledger_status_value,
program_id: program_id_value,
program_scope,
entity_kind,
entity_value: entity_value_value,
limit,
newest_first,
});
}
}
/// One raw transaction candidate enriched with core and ledger diagnostics.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PostgresReplayTransactionCandidate {
/// Canonical transaction signature.
pub signature: std::string::String,
/// Transaction slot.
pub slot: i64,
/// Current raw processing state.
pub raw_processing_state: std::string::String,
/// Current raw retention state.
pub retention_state: std::string::String,
/// Whether a core transaction row exists.
pub has_core_transaction: bool,
/// Core transaction failure flag when a core row exists.
pub transaction_failed: std::option::Option<bool>,
/// Latest core extraction ledger status or `not_started`.
pub ledger_status: std::string::String,
/// Latest core extraction processor version when available.
pub processor_version: std::option::Option<std::string::String>,
/// Latest core extraction attempt count.
pub attempt_count: i32,
/// Number of top-level instructions.
pub outer_instruction_count: i64,
/// Number of inner instructions.
pub inner_instruction_count: i64,
/// Number of distinct top-level programs.
pub outer_program_count: i64,
/// Number of distinct inner programs.
pub inner_program_count: i64,
/// Raw row update timestamp rendered by PostgreSQL.
pub updated_at: std::string::String,
}
/// Bounded read-only filter for program summaries.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostgresReplayProgramFilter {
/// Optional partial program id search.
pub program_id_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub limit: u32,
}
impl PostgresReplayProgramFilter {
/// Creates and validates a bounded program summary filter.
pub fn new(
program_id_contains: std::option::Option<std::string::String>,
limit: u32,
) -> kb_core::Result<Self> {
let limit_result = validate_limit(limit);
if let std::result::Result::Err(error) = limit_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
program_id_contains: trim_optional_text(program_id_contains),
limit,
});
}
}
/// Aggregated program occurrences across outer, inner and linked logs.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PostgresReplayProgramSummary {
/// Program id.
pub program_id: std::string::String,
/// Number of distinct transactions containing the program.
pub transaction_count: i64,
/// Number of top-level instruction occurrences.
pub outer_instruction_count: i64,
/// Number of inner instruction occurrences.
pub inner_instruction_count: i64,
/// Number of reliably linked log occurrences.
pub log_count: i64,
/// Lowest observed slot.
pub min_slot: i64,
/// Highest observed slot.
pub max_slot: i64,
}
/// Bounded read-only filter for core entity summaries.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostgresReplayEntityFilter {
/// Entity kind to aggregate.
pub entity_kind: crate::PostgresReplayEntityKind,
/// Optional partial entity value search.
pub entity_value_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub limit: u32,
}
impl PostgresReplayEntityFilter {
/// Creates and validates a bounded entity summary filter.
pub fn new(
entity_kind: crate::PostgresReplayEntityKind,
entity_value_contains: std::option::Option<std::string::String>,
limit: u32,
) -> kb_core::Result<Self> {
let limit_result = validate_limit(limit);
if let std::result::Result::Err(error) = limit_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
entity_kind,
entity_value_contains: trim_optional_text(entity_value_contains),
limit,
});
}
}
/// Aggregated mint, owner or account-key occurrences from core tables.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PostgresReplayEntitySummary {
/// Stable entity kind code.
pub entity_kind: std::string::String,
/// Mint, owner or account-key address.
pub entity_value: std::string::String,
/// Number of distinct transactions containing the entity.
pub transaction_count: i64,
/// Total number of core-table occurrences.
pub occurrence_count: i64,
/// Lowest observed slot.
pub min_slot: i64,
/// Highest observed slot.
pub max_slot: i64,
}
fn trim_optional_text(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
return match value {
std::option::Option::Some(text) => {
let trimmed = text.trim();
if trimmed.is_empty() {
return std::option::Option::None;
}
std::option::Option::Some(trimmed.to_string())
},
std::option::Option::None => std::option::Option::None,
};
}
fn validate_slot_range(
min_slot: std::option::Option<u64>,
max_slot: std::option::Option<u64>,
) -> kb_core::Result<()> {
if let (std::option::Option::Some(minimum), std::option::Option::Some(maximum)) =
(min_slot, max_slot)
{
if minimum > maximum {
return std::result::Result::Err(kb_core::Error::db(
"replay candidate minimum slot must not exceed maximum slot",
));
}
}
return std::result::Result::Ok(());
}
fn validate_limit(limit: u32) -> kb_core::Result<()> {
if limit == 0 || limit > crate::MAX_REPLAY_CANDIDATE_ROWS {
return std::result::Result::Err(kb_core::Error::db(format!(
"replay candidate limit must be between 1 and {}",
crate::MAX_REPLAY_CANDIDATE_ROWS
)));
}
return std::result::Result::Ok(());
}
fn validate_optional_code(
value: std::option::Option<&str>,
allowed: &[&str],
label: &str,
) -> kb_core::Result<()> {
let selected = match value {
std::option::Option::Some(code) => code,
std::option::Option::None => return std::result::Result::Ok(()),
};
for allowed_code in allowed {
if selected == *allowed_code {
return std::result::Result::Ok(());
}
}
return std::result::Result::Err(kb_core::Error::db(format!(
"unsupported {label}: {selected}"
)));
}
#[cfg(test)]
mod tests {
#[test]
fn transaction_filter_rejects_inverted_slots() {
let result = crate::PostgresReplayTransactionFilter::new(
std::option::Option::None,
std::option::Option::Some(20),
std::option::Option::Some(10),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
crate::PostgresReplayProgramScope::Any,
std::option::Option::None,
std::option::Option::None,
100,
true,
);
assert!(result.is_err());
}
#[test]
fn transaction_filter_requires_complete_entity_pair() {
let result = crate::PostgresReplayTransactionFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
crate::PostgresReplayProgramScope::Any,
std::option::Option::Some(crate::PostgresReplayEntityKind::Mint),
std::option::Option::None,
100,
true,
);
assert!(result.is_err());
}
#[test]
fn program_filter_rejects_limit_above_maximum() {
let result = crate::PostgresReplayProgramFilter::new(
std::option::Option::None,
crate::MAX_REPLAY_CANDIDATE_ROWS + 1,
);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,10 @@
// file: kb-store/src/postgres/repository.rs
// version: 1
//! PostgreSQL repository implementations.
mod core_extraction_repository;
mod core_transaction_repository;
mod decode_pipeline_repository;
mod raw_transaction_repository;
mod store_health_repository;

View File

@@ -0,0 +1,57 @@
// file: kb-store/src/postgres/repository/core_extraction_repository.rs
// version: 1
//! PostgreSQL atomic canonical transaction to core extraction repository.
#[async_trait::async_trait]
impl crate::CoreExtractionStore for crate::PostgresStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_raw_transactions_for_core_extraction(
&self,
filter: &crate::CoreExtractionSelectionFilter,
) -> kb_core::Result<std::vec::Vec<crate::RawTransactionRow>> {
return crate::postgres::query::list_raw_transactions_for_core_extraction(
self.pool(),
filter,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn is_core_extraction_current(
&self,
identity: &crate::ProcessingLedgerIdentity,
) -> kb_core::Result<bool> {
return crate::postgres::query::is_core_extraction_current(self.pool(), identity).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_core_extraction(
&self,
bundle: &crate::CoreExtractionBundle,
force_replay: bool,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::persist_core_extraction(self.pool(), bundle, force_replay)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn mark_core_extraction_failed(
&self,
failure: &crate::CoreExtractionFailure,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::mark_core_extraction_failed(self.pool(), failure).await;
}
}

View File

@@ -0,0 +1,118 @@
// file: kb-store/src/postgres/repository/core_transaction_repository.rs
// version: 1
//! PostgreSQL core Solana repository implementation.
#[async_trait::async_trait]
impl crate::CoreTransactionStore for crate::PostgresStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_core_transaction(
&self,
input: &crate::CoreTransactionInsert,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_core_transaction(self.pool(), input).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_core_account_keys(
&self,
inputs: &[crate::CoreAccountKeyInsert],
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_core_account_keys(self.pool(), inputs).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_core_instructions(
&self,
inputs: &[crate::CoreInstructionInsert],
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_core_instructions(self.pool(), inputs).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_core_inner_instructions(
&self,
inputs: &[crate::CoreInnerInstructionInsert],
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_core_inner_instructions(self.pool(), inputs).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_core_logs(
&self,
inputs: &[crate::CoreLogInsert],
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_core_logs(self.pool(), inputs).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_core_balance_changes(
&self,
inputs: &[crate::CoreBalanceChangeInsert],
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_core_balance_changes(self.pool(), inputs).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_core_instructions_for_replay(
&self,
filter: &crate::CoreInstructionReplayFilter,
page_request: &crate::PageRequest,
) -> kb_core::Result<std::vec::Vec<crate::CoreInstructionRow>> {
return crate::postgres::query::list_core_instructions_for_replay(
self.pool(),
filter,
page_request,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_core_instruction_replay_inputs(
&self,
filter: &crate::CoreInstructionReplayFilter,
page_request: &crate::PageRequest,
) -> kb_core::Result<std::vec::Vec<crate::CoreInstructionReplayInput>> {
return crate::postgres::query::list_core_instruction_replay_inputs(
self.pool(),
filter,
page_request,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn mark_core_instruction_lifecycle(
&self,
mark: &crate::CoreInstructionLifecycleMark,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::update_core_instruction_lifecycle(self.pool(), mark).await;
}
}

View File

@@ -0,0 +1,115 @@
// file: kb-store/src/postgres/repository/decode_pipeline_repository.rs
// version: 1
//! PostgreSQL contextual decode and materialization repository.
#[async_trait::async_trait]
impl crate::DecodePipelineStore for crate::PostgresStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_decode_inputs(
&self,
filter: &crate::DecodeSelectionFilter,
) -> kb_core::Result<std::vec::Vec<crate::CoreInstructionReplayInput>> {
return crate::postgres::query::list_decode_inputs(self.pool(), filter).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn is_decode_current(
&self,
identity: &crate::ProcessingLedgerIdentity,
) -> kb_core::Result<bool> {
return crate::postgres::query::is_decode_current(self.pool(), identity).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_decode_coverage_declarations(
&self,
declarations: &[crate::DecodeCoverageDeclarationInsert],
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::persist_decode_coverage_declarations(
self.pool(),
declarations,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_decode_result(
&self,
bundle: &crate::DecodePersistenceBundle,
force_replay: bool,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::persist_decode_result(self.pool(), bundle, force_replay)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn mark_decode_failed(
&self,
failure: &crate::DecodeFailure,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::mark_decode_failed(self.pool(), failure).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_materialization_result(
&self,
bundle: &crate::MaterializationPersistenceBundle,
force_replay: bool,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::persist_materialization_result(
self.pool(),
bundle,
force_replay,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_decode_coverage_summary(
&self,
processor_name: std::option::Option<&str>,
processor_version: std::option::Option<&str>,
limit: u32,
) -> kb_core::Result<std::vec::Vec<crate::DecodeCoverageSummaryRow>> {
return crate::postgres::query::list_decode_coverage_summary(
self.pool(),
processor_name,
processor_version,
limit,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_materialized_events(
&self,
filter: &crate::MaterializedEventFilter,
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEventQueryRow>> {
return crate::postgres::query::list_materialized_events(self.pool(), filter).await;
}
}

View File

@@ -0,0 +1,70 @@
// file: kb-store/src/postgres/repository/raw_transaction_repository.rs
// version: 1
//! PostgreSQL canonical transaction and acquisition observation repository implementation.
#[async_trait::async_trait]
impl crate::RawTransactionStore for crate::PostgresStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn has_raw_transaction_signature(
&self,
signature: &kb_lib::Signature,
) -> kb_core::Result<bool> {
return crate::postgres::query::has_raw_transaction_signature(
self.pool(),
signature.0.as_str(),
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn has_transaction_observation_key(
&self,
observation_key: &str,
) -> kb_core::Result<bool> {
return crate::postgres::query::has_transaction_observation_key(
self.pool(),
observation_key,
)
.await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_raw_transaction(
&self,
input: &crate::RawTransactionInsert,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_raw_transaction(self.pool(), input).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn insert_transaction_observation(
&self,
input: &crate::TransactionObservationInsert,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::insert_transaction_observation(self.pool(), input).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn mark_raw_payload_lifecycle(
&self,
mark: &crate::RawPayloadLifecycleMark,
) -> kb_core::Result<crate::InsertOutcome> {
return crate::postgres::query::update_raw_payload_lifecycle(self.pool(), mark).await;
}
}

View File

@@ -0,0 +1,31 @@
// file: kb-store/src/postgres/repository/store_health_repository.rs
// version: 1
//! PostgreSQL store health repository implementation.
#[async_trait::async_trait]
impl crate::StoreHealthStore for crate::PostgresStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn backend_descriptor(&self) -> kb_core::Result<crate::StoreBackendDescriptor> {
return crate::PostgresStore::backend_descriptor(self).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn health_snapshot(&self) -> kb_core::Result<crate::StoreHealthSnapshot> {
return crate::PostgresStore::health_snapshot(self).await;
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn migration_snapshot(&self) -> kb_core::Result<crate::StoreMigrationSnapshot> {
return crate::PostgresStore::migration_snapshot(self).await;
}
}

View File

@@ -0,0 +1,512 @@
// file: kb-store/src/postgres/store.rs
// version: 2
//! Store implementation scaffold for the `kb-store` crate.
/// PostgreSQL store connection options.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostgresStoreOptions {
/// Database URL or DSN.
pub database_url: std::string::String,
/// Maximum connection count.
pub max_connections: u32,
/// Connection timeout in milliseconds.
pub connect_timeout_ms: u64,
/// Enables idempotent raw schema initialization at startup.
pub auto_initialize_schema: bool,
}
impl PostgresStoreOptions {
/// Creates validated PostgreSQL store options.
pub fn new(
database_url: impl std::convert::Into<std::string::String>,
max_connections: u32,
connect_timeout_ms: u64,
auto_initialize_schema: bool,
) -> kb_core::Result<Self> {
let database_url_value = database_url.into();
if database_url_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"postgres database url must not be empty",
));
}
if max_connections == 0 {
return std::result::Result::Err(kb_core::Error::db(
"postgres max_connections must be greater than zero",
));
}
if connect_timeout_ms == 0 {
return std::result::Result::Err(kb_core::Error::db(
"postgres connect_timeout_ms must be greater than zero",
));
}
return std::result::Result::Ok(Self {
database_url: database_url_value,
max_connections,
connect_timeout_ms,
auto_initialize_schema,
});
}
/// Returns a DSN masked for diagnostics.
pub fn masked_dsn(&self) -> std::string::String {
return crate::mask_postgres_dsn(self.database_url.as_str());
}
}
/// PostgreSQL diagnostic snapshot.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PostgresBackendDiagnostics {
/// Backend descriptor safe for UI display.
pub descriptor: crate::StoreBackendDescriptor,
/// Backend health snapshot.
pub health: crate::StoreHealthSnapshot,
/// Migration status snapshot.
pub migrations: crate::StoreMigrationSnapshot,
/// Full PostgreSQL server version string when available.
pub server_version: std::option::Option<std::string::String>,
}
/// Read-only statistics for one PostgreSQL table.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PostgresTableStatistics {
/// Number of rows currently stored in the table.
pub row_count: i64,
/// Lowest observed Solana slot when the table contains a slot column and rows.
pub min_slot: std::option::Option<i64>,
/// Highest observed Solana slot when the table contains a slot column and rows.
pub max_slot: std::option::Option<i64>,
/// Latest insertion timestamp rendered by PostgreSQL for UI diagnostics.
pub latest_created_at: std::option::Option<std::string::String>,
}
/// Read-only diagnostics for one expected PostgreSQL table.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PostgresTableDiagnostics {
/// Expected table name.
pub table_name: std::string::String,
/// Logical Solana domain encoded in the table name.
pub domain: std::string::String,
/// Human-readable role of the table.
pub role: std::string::String,
/// Whether the table exists in the current PostgreSQL search path.
pub exists: bool,
/// Table statistics when the table exists.
pub statistics: std::option::Option<crate::PostgresTableStatistics>,
}
/// PostgreSQL store handle.
#[derive(Clone, Debug)]
pub struct PostgresStore {
options: crate::PostgresStoreOptions,
pool: sqlx::PgPool,
}
impl PostgresStore {
/// Connects to PostgreSQL from typed store options.
pub async fn connect(options: crate::PostgresStoreOptions) -> kb_core::Result<Self> {
let pool_options = sqlx::postgres::PgPoolOptions::new()
.max_connections(options.max_connections)
.acquire_timeout(std::time::Duration::from_millis(options.connect_timeout_ms));
let connect_result = pool_options.connect(options.database_url.as_str()).await;
return match connect_result {
std::result::Result::Ok(pool) => {
let store = Self { options, pool };
if store.options.auto_initialize_schema {
let schema_result = store.initialize_store_schema().await;
if let std::result::Result::Err(error) = schema_result {
return std::result::Result::Err(error);
}
}
std::result::Result::Ok(store)
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::db(
format!("postgres connection failed: {error}"),
)),
};
}
/// Creates a store handle from an existing PostgreSQL pool.
pub fn from_pool(options: crate::PostgresStoreOptions, pool: sqlx::PgPool) -> Self {
return Self { options, pool };
}
/// Returns the underlying PostgreSQL pool.
pub fn pool(&self) -> &sqlx::PgPool {
return &self.pool;
}
/// Returns the connection options used to create this store.
pub fn options(&self) -> &crate::PostgresStoreOptions {
return &self.options;
}
/// Applies each idempotent store schema once per invocation in dependency order.
pub async fn initialize_store_schema(&self) -> kb_core::Result<()> {
let raw_result = crate::postgres::query::apply_raw_store_schema(&self.pool).await;
if let std::result::Result::Err(error) = raw_result {
return std::result::Result::Err(error);
}
let core_result = crate::postgres::query::apply_core_store_schema(&self.pool).await;
if let std::result::Result::Err(error) = core_result {
return std::result::Result::Err(error);
}
return crate::postgres::query::apply_decode_store_schema(&self.pool).await;
}
/// Applies the idempotent minimal raw Solana store schema.
pub async fn initialize_raw_store_schema(&self) -> kb_core::Result<()> {
return crate::postgres::query::apply_raw_store_schema(&self.pool).await;
}
/// Applies the idempotent minimal core Solana store schema.
pub async fn initialize_core_store_schema(&self) -> kb_core::Result<()> {
let raw_result = self.initialize_raw_store_schema().await;
if let std::result::Result::Err(error) = raw_result {
return std::result::Result::Err(error);
}
return crate::postgres::query::apply_core_store_schema(&self.pool).await;
}
/// Applies the idempotent common decode and materialization store schema.
pub async fn initialize_decode_store_schema(&self) -> kb_core::Result<()> {
let core_result = self.initialize_core_store_schema().await;
if let std::result::Result::Err(error) = core_result {
return std::result::Result::Err(error);
}
return crate::postgres::query::apply_decode_store_schema(&self.pool).await;
}
/// Reads a UI-safe backend descriptor.
pub async fn backend_descriptor(&self) -> kb_core::Result<crate::StoreBackendDescriptor> {
let schema_result = crate::postgres::query::load_current_schema(&self.pool).await;
return match schema_result {
std::result::Result::Ok(schema) => crate::StoreBackendDescriptor::new(
crate::StoreBackendKind::Postgres,
"postgres",
std::option::Option::Some(self.options.masked_dsn()),
std::option::Option::Some(schema),
),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Reads a PostgreSQL health snapshot.
pub async fn health_snapshot(&self) -> kb_core::Result<crate::StoreHealthSnapshot> {
let health_result = crate::postgres::query::run_health_check(&self.pool).await;
return match health_result {
std::result::Result::Ok(()) => crate::StoreHealthSnapshot::new(
"postgres",
crate::StoreHealthStatus::Healthy,
std::option::Option::Some(std::string::String::from("SELECT 1 succeeded")),
),
std::result::Result::Err(error) => crate::StoreHealthSnapshot::new(
"postgres",
crate::StoreHealthStatus::Unhealthy,
std::option::Option::Some(error.to_string()),
),
};
}
/// Reads a non-destructive migration snapshot.
pub async fn migration_snapshot(&self) -> kb_core::Result<crate::StoreMigrationSnapshot> {
let migration_table_result =
crate::postgres::query::load_migration_table_name(&self.pool).await;
return match migration_table_result {
std::result::Result::Ok(std::option::Option::None) => {
std::result::Result::Ok(crate::StoreMigrationSnapshot::new(
crate::StoreMigrationStatus::NotInitialized,
std::option::Option::None,
std::vec::Vec::new(),
std::option::Option::Some(std::string::String::from(
"no sqlx migration table detected; 0.3.1 canonical acquisition/core schemas use idempotent crate-managed DDL",
)),
))
},
std::result::Result::Ok(std::option::Option::Some(_table_name)) => {
self.migration_snapshot_from_existing_table().await
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Reads a complete PostgreSQL diagnostic snapshot.
pub async fn backend_diagnostics(&self) -> kb_core::Result<crate::PostgresBackendDiagnostics> {
let descriptor_result = self.backend_descriptor().await;
let descriptor = match descriptor_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let health_result = self.health_snapshot().await;
let health = match health_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let migrations_result = self.migration_snapshot().await;
let migrations = match migrations_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let server_version = match crate::postgres::query::load_server_version(&self.pool).await {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_error) => std::option::Option::None,
};
return std::result::Result::Ok(crate::PostgresBackendDiagnostics {
descriptor,
health,
migrations,
server_version,
});
}
/// Lists bounded raw transaction candidates enriched with core and ledger diagnostics.
pub async fn replay_transaction_candidates(
&self,
filter: &crate::PostgresReplayTransactionFilter,
) -> kb_core::Result<std::vec::Vec<crate::PostgresReplayTransactionCandidate>> {
return crate::postgres::query::list_replay_transaction_candidates(&self.pool, filter)
.await;
}
/// Lists bounded program summaries across outer, inner and reliably linked logs.
pub async fn replay_program_summaries(
&self,
filter: &crate::PostgresReplayProgramFilter,
) -> kb_core::Result<std::vec::Vec<crate::PostgresReplayProgramSummary>> {
return crate::postgres::query::list_replay_program_summaries(&self.pool, filter).await;
}
/// Lists bounded mint, owner or account-key summaries from core tables.
pub async fn replay_entity_summaries(
&self,
filter: &crate::PostgresReplayEntityFilter,
) -> kb_core::Result<std::vec::Vec<crate::PostgresReplayEntitySummary>> {
return crate::postgres::query::list_replay_entity_summaries(&self.pool, filter).await;
}
/// Reads diagnostics for raw Solana store tables without changing the schema.
pub async fn raw_table_diagnostics(
&self,
) -> kb_core::Result<std::vec::Vec<crate::PostgresTableDiagnostics>> {
let specs = crate::raw_store_table_diagnostic_specs();
return self.table_diagnostics(&specs).await;
}
/// Reads diagnostics for core Solana store tables without changing the schema.
pub async fn core_table_diagnostics(
&self,
) -> kb_core::Result<std::vec::Vec<crate::PostgresTableDiagnostics>> {
let specs = crate::core_store_table_diagnostic_specs();
return self.table_diagnostics(&specs).await;
}
/// Reads diagnostics for decode and materialization store tables without changing the schema.
pub async fn decode_table_diagnostics(
&self,
) -> kb_core::Result<std::vec::Vec<crate::PostgresTableDiagnostics>> {
let specs = crate::decode_store_table_diagnostic_specs();
return self.table_diagnostics(&specs).await;
}
/// Reads diagnostics for every known raw/core/decode Solana store table.
pub async fn known_table_diagnostics(
&self,
) -> kb_core::Result<std::vec::Vec<crate::PostgresTableDiagnostics>> {
let mut diagnostics = std::vec::Vec::new();
let raw_result = self.raw_table_diagnostics().await;
let raw_tables = match raw_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for table in raw_tables {
diagnostics.push(table);
}
let core_result = self.core_table_diagnostics().await;
let core_tables = match core_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for table in core_tables {
diagnostics.push(table);
}
let decode_result = self.decode_table_diagnostics().await;
let decode_tables = match decode_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for table in decode_tables {
diagnostics.push(table);
}
return std::result::Result::Ok(diagnostics);
}
async fn table_diagnostics(
&self,
specs: &[crate::PostgresTableDiagnosticSpec],
) -> kb_core::Result<std::vec::Vec<crate::PostgresTableDiagnostics>> {
let mut diagnostics = std::vec::Vec::new();
for spec in specs {
let exists_result =
crate::postgres::query::table_exists(&self.pool, spec.table_name).await;
let exists = match exists_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let statistics = if exists {
let statistics_result =
crate::postgres::query::load_table_statistics(&self.pool, spec.table_name)
.await;
match statistics_result {
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
};
diagnostics.push(crate::PostgresTableDiagnostics {
table_name: spec.table_name.to_string(),
domain: spec.domain.to_string(),
role: spec.role.to_string(),
exists,
statistics,
});
}
return std::result::Result::Ok(diagnostics);
}
async fn migration_snapshot_from_existing_table(
&self,
) -> kb_core::Result<crate::StoreMigrationSnapshot> {
let version_result =
crate::postgres::query::load_latest_migration_version(&self.pool).await;
return match version_result {
std::result::Result::Ok(current_version) => {
std::result::Result::Ok(crate::StoreMigrationSnapshot::new(
crate::StoreMigrationStatus::Current,
current_version,
std::vec::Vec::new(),
std::option::Option::Some(std::string::String::from(
"sqlx migration table detected; canonical acquisition/core schema remains idempotent and crate-managed in 0.3.1",
)),
))
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Returns a DSN masked for logs and UI diagnostics.
pub fn mask_postgres_dsn(dsn: &str) -> std::string::String {
let trimmed_dsn = dsn.trim();
if trimmed_dsn.is_empty() {
return std::string::String::from("");
}
let queryless = crate::postgres::store::strip_query(trimmed_dsn);
return match queryless.split_once("://") {
std::option::Option::Some((scheme, remainder)) => {
crate::postgres::store::mask_scheme_remainder(
scheme,
remainder,
trimmed_dsn.contains('?'),
)
},
std::option::Option::None => crate::postgres::store::mask_plain_dsn(queryless.as_str()),
};
}
fn strip_query(dsn: &str) -> std::string::String {
return match dsn.split_once('?') {
std::option::Option::Some((prefix, _query)) => std::string::String::from(prefix),
std::option::Option::None => std::string::String::from(dsn),
};
}
fn mask_scheme_remainder(scheme: &str, remainder: &str, had_query: bool) -> std::string::String {
let suffix = crate::postgres::store::query_suffix(had_query);
return match remainder.rsplit_once('@') {
std::option::Option::Some((_userinfo, host_path)) => {
format!("{scheme}://***:***@{host_path}{suffix}")
},
std::option::Option::None => format!("{scheme}://{remainder}{suffix}"),
};
}
fn mask_plain_dsn(dsn: &str) -> std::string::String {
if dsn.contains("password=") {
return std::string::String::from("<postgres-dsn-redacted>");
}
return std::string::String::from(dsn);
}
fn query_suffix(had_query: bool) -> std::string::String {
if had_query {
return std::string::String::from("?<redacted>");
}
return std::string::String::from("");
}
#[cfg(test)]
mod tests {
#[test]
fn options_reject_empty_database_url() {
let result = crate::PostgresStoreOptions::new(" ", 1, 1000, false);
assert!(result.is_err());
}
#[test]
fn options_reject_zero_max_connections() {
let result = crate::PostgresStoreOptions::new("postgres://localhost/db", 0, 1000, false);
assert!(result.is_err());
}
#[test]
fn options_reject_zero_connect_timeout() {
let result = crate::PostgresStoreOptions::new("postgres://localhost/db", 1, 0, false);
assert!(result.is_err());
}
#[tokio::test]
async fn optional_postgres_healthcheck_from_env() {
let database_url = match std::env::var("KB_POSTGRES_TEST_URL") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => return,
};
let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await;
let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false);
let options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected options error: {error}"),
};
let store_result = crate::PostgresStore::connect(options).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected connect error: {error}"),
};
let health_result = store.health_snapshot().await;
let health = match health_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected health error: {error}"),
};
assert_eq!(health.status, crate::StoreHealthStatus::Healthy);
return;
}
#[test]
fn mask_postgres_dsn_masks_userinfo() {
let masked = crate::mask_postgres_dsn("postgres://user:secret@localhost:5432/db");
assert_eq!(masked, "postgres://***:***@localhost:5432/db");
}
#[test]
fn mask_postgres_dsn_masks_query_string() {
let masked =
crate::mask_postgres_dsn("postgres://localhost/db?sslmode=require&password=secret");
assert_eq!(masked, "postgres://localhost/db?<redacted>");
}
#[test]
fn mask_postgres_dsn_masks_plain_password_dsn() {
let masked = crate::mask_postgres_dsn("host=localhost password=secret dbname=khadhroony");
assert_eq!(masked, "<postgres-dsn-redacted>");
}
}

View File

@@ -0,0 +1,15 @@
// file: kb-store/src/postgres/test_serial.rs
// version: 1
//! Test-only serialization helpers for optional real PostgreSQL tests.
static POSTGRES_TEST_MUTEX: std::sync::OnceLock<std::sync::Arc<tokio::sync::Mutex<()>>> =
std::sync::OnceLock::new();
/// Acquires the process-local guard shared by optional real PostgreSQL tests.
pub(in crate::postgres) async fn postgres_test_guard() -> tokio::sync::OwnedMutexGuard<()> {
let mutex = POSTGRES_TEST_MUTEX
.get_or_init(|| return std::sync::Arc::new(tokio::sync::Mutex::new(())))
.clone();
return mutex.lock_owned().await;
}