// 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, max_connections: u32, connect_timeout_ms: u64, auto_initialize_schema: bool, ) -> ks_core::Result { 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 { 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, max_connections: u32, connect_timeout_ms: u64, auto_initialize_schema: bool, option_count: u32, ) -> ks_core::Result { 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 { 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 { 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 { 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 { 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 { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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 { 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(""); } return std::string::String::from(dsn); } fn query_suffix(had_query: bool) -> std::string::String { if had_query { return std::string::String::from("?"); } 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?"); } #[test] fn mask_postgres_dsn_masks_plain_password_dsn() { let masked = super::mask_postgres_dsn("host=localhost password=secret dbname=khadhroony"); assert_eq!(masked, ""); } }