Files
khadhroony-bot3/ks-store/src/postgres/store.rs
2026-08-11 22:22:40 +02:00

584 lines
25 KiB
Rust

// file: ks-store/src/postgres/store.rs
// version: 9
//! PostgreSQL store implementation kept behind the backend-agnostic `Store` facade.
/// PostgreSQL store connection options interpreted only inside `ks-store`.
#[derive(Clone, Eq, PartialEq)]
pub(crate) struct PostgresStoreOptions {
database_url: std::string::String,
max_connections: u32,
connect_timeout_ms: u64,
auto_initialize_schema: bool,
option_count: u32,
}
impl crate::PostgresStoreOptions {
/// Creates validated PostgreSQL store options for crate-internal tests and adapters.
#[cfg(test)]
pub(crate) fn new(
database_url: impl std::convert::Into<std::string::String>,
max_connections: u32,
connect_timeout_ms: u64,
auto_initialize_schema: bool,
) -> ks_core::Result<Self> {
return Self::new_with_option_count(
database_url,
max_connections,
connect_timeout_ms,
auto_initialize_schema,
4,
);
}
/// Interprets the selected opaque backend options supplied to `Store::open`.
pub(crate) fn from_backend_options(options: &serde_json::Value) -> ks_core::Result<Self> {
let object = match options.as_object() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_invalid",
"postgres backend options must be a JSON object",
));
},
};
let database_url = match object.get("url").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value.to_string(),
std::option::Option::None => {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_incomplete",
"postgres backend requires a non-empty connection URL",
));
},
};
let max_connections_u64 =
match object.get("max_connections").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_incomplete",
"postgres backend requires max_connections greater than zero",
));
},
};
let max_connections = match u32::try_from(max_connections_u64) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_invalid",
"postgres max_connections does not fit into u32",
));
},
};
let connect_timeout_ms =
match object.get("connect_timeout_ms").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_incomplete",
"postgres backend requires connect_timeout_ms greater than zero",
));
},
};
let auto_initialize_schema =
match object.get("auto_initialize_schema").and_then(serde_json::Value::as_bool) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_incomplete",
"postgres backend requires auto_initialize_schema",
));
},
};
let option_count = match u32::try_from(object.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => u32::MAX,
};
return Self::new_with_option_count(
database_url,
max_connections,
connect_timeout_ms,
auto_initialize_schema,
option_count,
);
}
fn new_with_option_count(
database_url: impl std::convert::Into<std::string::String>,
max_connections: u32,
connect_timeout_ms: u64,
auto_initialize_schema: bool,
option_count: u32,
) -> ks_core::Result<Self> {
let database_url_value = database_url.into();
if database_url_value.trim().is_empty() {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_incomplete",
"postgres backend requires a non-empty connection URL",
));
}
if max_connections == 0 {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_invalid",
"postgres max_connections must be greater than zero",
));
}
if connect_timeout_ms == 0 {
return std::result::Result::Err(crate::storage_contract_error(
"store_backend_options_invalid",
"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,
option_count,
});
}
/// Returns whether automatic schema initialization is enabled.
pub(crate) fn auto_initialize_schema(&self) -> bool {
return self.auto_initialize_schema;
}
/// Returns a backend-neutral safe configuration summary.
pub(crate) fn configuration_summary(&self) -> crate::StoreConfigurationSummary {
return crate::StoreConfigurationSummary {
enabled: true,
backend_code: "postgres".to_string(),
connection_configured: !self.database_url.trim().is_empty(),
auto_initialize_schema: self.auto_initialize_schema,
backend_option_count: self.option_count,
};
}
/// Returns a DSN masked for diagnostics.
pub(crate) fn masked_connection_descriptor(&self) -> std::string::String {
return mask_postgres_dsn(self.database_url.as_str());
}
}
/// PostgreSQL store handle kept private to `ks-store`.
#[derive(Clone)]
pub(crate) struct PostgresStore {
options: crate::PostgresStoreOptions,
pool: sqlx::PgPool,
}
impl crate::PostgresStore {
/// Connects to PostgreSQL from validated backend options.
pub(crate) async fn connect(options: crate::PostgresStoreOptions) -> ks_core::Result<Self> {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", "open PostgreSQL store connection");
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) => {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", connected = true, "PostgreSQL store connection opened");
std::result::Result::Ok(Self { options, pool })
},
std::result::Result::Err(_error) => {
tracing::error!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", connected = false, "PostgreSQL store connection failed");
std::result::Result::Err(crate::storage_contract_error(
"store_backend_connection_failed",
"postgres connection failed",
))
},
};
}
/// Returns the underlying PostgreSQL pool to backend-private repositories.
pub(crate) fn pool(&self) -> &sqlx::PgPool {
return &self.pool;
}
/// Applies each idempotent store schema once per invocation in dependency order.
pub(crate) async fn initialize_store_schema(&self) -> ks_core::Result<()> {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "schema_initialize", "initialize PostgreSQL store schema");
let raw_result = crate::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::apply_core_store_schema(&self.pool).await;
if let std::result::Result::Err(error) = core_result {
return std::result::Result::Err(error);
}
let result = crate::apply_decode_store_schema(&self.pool).await;
if result.is_ok() {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "schema_initialize", initialized = true, "PostgreSQL store schema initialized");
}
return result;
}
/// Applies the idempotent minimal raw Solana store schema.
#[cfg(test)]
pub(crate) async fn initialize_raw_store_schema(&self) -> ks_core::Result<()> {
return crate::apply_raw_store_schema(&self.pool).await;
}
/// Applies the idempotent minimal Core Solana store schema.
#[cfg(test)]
pub(crate) async fn initialize_core_store_schema(&self) -> ks_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::apply_core_store_schema(&self.pool).await;
}
/// Reads a UI-safe backend descriptor.
pub(crate) async fn backend_descriptor(
&self,
) -> ks_core::Result<crate::StoreBackendDescriptor> {
let schema_result = crate::load_current_schema(&self.pool).await;
return match schema_result {
std::result::Result::Ok(schema) => crate::StoreBackendDescriptor::new(
"postgres",
"PostgreSQL",
std::option::Option::Some(self.options.masked_connection_descriptor()),
std::option::Option::Some(schema),
),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Reads a PostgreSQL health snapshot without exposing backend error details.
pub(crate) async fn health_snapshot(&self) -> ks_core::Result<crate::StoreHealthSnapshot> {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "health_check", "run PostgreSQL store health check");
let health_result = crate::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(
"backend health check succeeded",
)),
),
std::result::Result::Err(_error) => crate::StoreHealthSnapshot::new(
"postgres",
crate::StoreHealthStatus::Unhealthy,
std::option::Option::Some(std::string::String::from("backend health check failed")),
),
};
}
/// Reads a non-destructive migration snapshot.
pub(crate) async fn migration_snapshot(
&self,
) -> ks_core::Result<crate::StoreMigrationSnapshot> {
let migration_table_result = crate::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 migration history table detected; crate-managed schema initialization is active",
)),
))
},
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 backend-neutral diagnostic snapshot.
pub(crate) async fn backend_diagnostics(
&self,
) -> ks_core::Result<crate::StoreBackendDiagnostics> {
let descriptor = match self.backend_descriptor().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let health = match self.health_snapshot().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let migrations = match self.migration_snapshot().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let backend_version = match crate::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::StoreBackendDiagnostics {
descriptor,
health,
migrations,
backend_version,
});
}
/// Lists bounded raw transaction candidates enriched with Core and ledger diagnostics.
pub(crate) async fn replay_transaction_candidates(
&self,
filter: &crate::ReplayTransactionFilter,
) -> ks_core::Result<std::vec::Vec<crate::ReplayTransactionCandidate>> {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_transaction_candidates", "query PostgreSQL replay transaction candidates");
return crate::list_replay_transaction_candidates(&self.pool, filter).await;
}
/// Lists bounded program summaries across top-level, inner and reliably linked logs.
pub(crate) async fn replay_program_summaries(
&self,
filter: &crate::ReplayProgramFilter,
) -> ks_core::Result<std::vec::Vec<crate::ReplayProgramSummary>> {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_program_summaries", "query PostgreSQL replay program summaries");
return crate::list_replay_program_summaries(&self.pool, filter).await;
}
/// Lists bounded mint, owner or account-key summaries from Core facts.
pub(crate) async fn replay_entity_summaries(
&self,
filter: &crate::ReplayEntityFilter,
) -> ks_core::Result<std::vec::Vec<crate::ReplayEntitySummary>> {
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_entity_summaries", "query PostgreSQL replay entity summaries");
return crate::list_replay_entity_summaries(&self.pool, filter).await;
}
/// Reads diagnostics for raw store resources without changing the schema.
pub(crate) async fn raw_resource_diagnostics(
&self,
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
let specs = crate::raw_store_table_diagnostic_specs();
return self.resource_diagnostics(&specs).await;
}
/// Reads diagnostics for Core and processing store resources without changing the schema.
pub(crate) async fn core_resource_diagnostics(
&self,
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
let specs = crate::core_store_table_diagnostic_specs();
return self.resource_diagnostics(&specs).await;
}
/// Reads diagnostics for decode and materialization resources without changing the schema.
pub(crate) async fn decode_resource_diagnostics(
&self,
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
let specs = crate::decode_store_table_diagnostic_specs();
return self.resource_diagnostics(&specs).await;
}
/// Reads diagnostics for every known logical store resource.
pub(crate) async fn known_resource_diagnostics(
&self,
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
let mut diagnostics = std::vec::Vec::new();
let raw_resources = match self.raw_resource_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
diagnostics.extend(raw_resources);
let core_resources = match self.core_resource_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
diagnostics.extend(core_resources);
let decode_resources = match self.decode_resource_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
diagnostics.extend(decode_resources);
return std::result::Result::Ok(diagnostics);
}
async fn resource_diagnostics(
&self,
specs: &[crate::PostgresTableDiagnosticSpec],
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
let mut diagnostics = std::vec::Vec::new();
for spec in specs {
let available = match crate::table_exists(&self.pool, spec.table_name).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let statistics = if available {
let statistics_result =
crate::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::StoreResourceDiagnostics {
resource_code: spec.resource_code.to_string(),
model_code: spec.model_code.to_string(),
role: spec.role.to_string(),
available,
statistics,
});
}
return std::result::Result::Ok(diagnostics);
}
async fn migration_snapshot_from_existing_table(
&self,
) -> ks_core::Result<crate::StoreMigrationSnapshot> {
let version_result = crate::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(
"migration history table detected",
)),
))
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Returns a PostgreSQL connection descriptor masked for logs and diagnostics.
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 = strip_query(trimmed_dsn);
return match queryless.split_once("://") {
std::option::Option::Some((scheme, remainder)) => {
mask_scheme_remainder(scheme, remainder, trimmed_dsn.contains('?'))
},
std::option::Option::None => 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 = 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 backend_options_reject_empty_database_url() {
let result = crate::PostgresStoreOptions::new(" ", 1, 1000, false);
assert!(result.is_err());
}
#[test]
fn backend_options_reject_zero_max_connections() {
let result = crate::PostgresStoreOptions::new("postgres://localhost/db", 0, 1000, false);
assert!(result.is_err());
}
#[test]
fn incomplete_backend_options_do_not_echo_secret_values() {
let options = serde_json::json!({
"url": "postgres://operator:STORE-SECRET-CANARY@localhost/db",
"connect_timeout_ms": 5000,
"auto_initialize_schema": true
});
let error = match crate::PostgresStoreOptions::from_backend_options(&options) {
std::result::Result::Ok(_) => panic!("incomplete backend options must be rejected"),
std::result::Result::Err(error) => error,
};
assert!(!error.to_string().contains("STORE-SECRET-CANARY"));
assert!(!error.to_string().contains("postgres://"));
}
#[test]
fn opaque_backend_options_are_sanitized_in_summary() {
let options = serde_json::json!({
"url": "postgres://operator:STORE-SECRET-CANARY@localhost/db",
"max_connections": 4,
"connect_timeout_ms": 5000,
"auto_initialize_schema": true
});
let parsed = crate::PostgresStoreOptions::from_backend_options(&options);
let value = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("backend options must parse: {error}"),
};
let serialized = match serde_json::to_string(&value.configuration_summary()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("summary must serialize: {error}"),
};
assert!(!serialized.contains("STORE-SECRET-CANARY"));
assert!(!serialized.contains("postgres://"));
}
#[tokio::test]
async fn optional_postgres_healthcheck_from_env() {
let database_url = match std::env::var("KS_SECRET_POSTGRES_TEST_URL") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => return,
};
let _postgres_guard = crate::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);
}
#[test]
fn mask_postgres_dsn_masks_userinfo() {
let masked = super::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 =
super::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 = super::mask_postgres_dsn("host=localhost password=secret dbname=khadhroony");
assert_eq!(masked, "<postgres-dsn-redacted>");
}
}