// file: kb-app-demo-desktop/src/demo_sql_common.rs // version: 13 //! Shared SQL demo helpers and serializable payloads. use ts_rs::TS; // rust-rules: derive-import /// UI-safe table diagnostics shown by SQL demo windows. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot.ts" )] pub(crate) struct DemoSqlTableSnapshot { /// Table name. pub(crate) table_name: std::string::String, /// Logical Solana domain encoded in the table name. pub(crate) domain: std::string::String, /// Human-readable table role. pub(crate) role: std::string::String, /// Whether the table currently exists. pub(crate) exists: bool, /// Row count when the table exists. #[ts(type = "number | null")] pub(crate) row_count: std::option::Option, /// Minimum slot when available. #[ts(type = "number | null")] pub(crate) min_slot: std::option::Option, /// Maximum slot when available. #[ts(type = "number | null")] pub(crate) max_slot: std::option::Option, /// Latest insertion timestamp rendered by PostgreSQL when available. pub(crate) latest_created_at: std::option::Option, } /// Builds PostgreSQL options from the active profile without coupling ks-store to ks-config. pub(crate) fn postgres_store_options_from_profile( profile: &ks_config::ProfileConfig, auto_initialize_schema: bool, ) -> ks_core::Result { return ks_store::PostgresStoreOptions::new( profile.database.postgres.url.clone(), profile.database.postgres.max_connections, profile.database.postgres.connect_timeout_ms, auto_initialize_schema, ); } /// Connects to PostgreSQL without reapplying schemas initialized at application startup. pub(crate) async fn connect_postgres_store( profile: &ks_config::ProfileConfig, ) -> std::result::Result { if !profile.database.enabled { return std::result::Result::Err(std::string::String::from( "database configuration is disabled in the active profile", )); } let options_result = postgres_store_options_from_profile(profile, false); let mut options = match options_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; options.auto_initialize_schema = false; let store_result = ks_store::PostgresStore::connect(options).await; return match store_result { std::result::Result::Ok(store) => std::result::Result::Ok(store), std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), }; } /// Converts store table diagnostics to the UI payload shape. pub(crate) fn table_snapshot_from_pg( value: &ks_store::PostgresTableDiagnostics, ) -> crate::DemoSqlTableSnapshot { let row_count = match &value.statistics { std::option::Option::Some(statistics) => std::option::Option::Some(statistics.row_count), std::option::Option::None => std::option::Option::None, }; let min_slot = match &value.statistics { std::option::Option::Some(statistics) => statistics.min_slot, std::option::Option::None => std::option::Option::None, }; let max_slot = match &value.statistics { std::option::Option::Some(statistics) => statistics.max_slot, std::option::Option::None => std::option::Option::None, }; let latest_created_at = match &value.statistics { std::option::Option::Some(statistics) => statistics.latest_created_at.clone(), std::option::Option::None => std::option::Option::None, }; return crate::DemoSqlTableSnapshot { table_name: value.table_name.clone(), domain: value.domain.clone(), role: value.role.clone(), exists: value.exists, row_count, min_slot, max_slot, latest_created_at, }; } /// Converts many table diagnostics to UI payloads. pub(crate) fn table_snapshots_from_pg( values: &[ks_store::PostgresTableDiagnostics], ) -> std::vec::Vec { let mut output = std::vec::Vec::new(); for value in values { output.push(crate::table_snapshot_from_pg(value)); } return output; } /// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup. pub(crate) async fn initialize_postgres_schema_for_startup( state: &crate::AppState, splash_window: &tauri::WebviewWindow, ) { let profile = state.active_profile(); if !profile.database.enabled { emit_sql_startup_splash( splash_window, "SQL store disabled; schema initialization skipped.", "info", false, ); tracing::debug!(target: crate::TRACING_TARGET, "database is disabled; skip SQL schema initialization"); return; } if profile.database.backend != "postgres" { emit_sql_startup_splash( splash_window, "SQL backend is not PostgreSQL; schema initialization skipped.", "info", false, ); tracing::debug!(target: crate::TRACING_TARGET, backend = profile.database.backend.as_str(), "database backend is not postgres; skip SQL schema initialization"); return; } let options_result = postgres_store_options_from_profile( profile, profile.database.postgres.auto_initialize_schema, ); let mut options = match options_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let error_message = error.to_string(); emit_sql_startup_error(splash_window, error_message.as_str()); tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot build PostgreSQL store options"); return; }, }; let initialize_schema = options.auto_initialize_schema; let masked_dsn = options.masked_dsn(); options.auto_initialize_schema = false; emit_sql_startup_splash( splash_window, "Connexion PostgreSQL et vérification des tables raw/core/decode...", "info", false, ); if !initialize_schema { emit_sql_startup_splash( splash_window, "Auto-initialisation désactivée : vérification sans création de table.", "info", true, ); } tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization"); let store_result = ks_store::PostgresStore::connect(options).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let error_message = error.to_string(); emit_sql_startup_error(splash_window, error_message.as_str()); tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL connection failed during startup schema initialization"); return; }, }; let before_result = store.known_table_diagnostics().await; let before_tables = match before_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let error_message = error.to_string(); emit_sql_startup_error(splash_window, error_message.as_str()); tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics before schema initialization"); return; }, }; if initialize_schema { let initialize_result = store.initialize_store_schema().await; if let std::result::Result::Err(error) = initialize_result { let error_message = error.to_string(); emit_sql_startup_error(splash_window, error_message.as_str()); tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed"); return; } } let after_result = store.known_table_diagnostics().await; let after_tables = match after_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let error_message = error.to_string(); emit_sql_startup_error(splash_window, error_message.as_str()); tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics after schema initialization"); return; }, }; emit_sql_startup_table_report(splash_window, before_tables.as_slice(), after_tables.as_slice()); } /// Formats debug enums for UI display. pub(crate) fn debug_status(value: T) -> std::string::String { return format!("{value:?}"); } fn emit_sql_startup_table_report( splash_window: &tauri::WebviewWindow, before_tables: &[ks_store::PostgresTableDiagnostics], after_tables: &[ks_store::PostgresTableDiagnostics], ) { let mut created_count = 0_u32; let mut existing_count = 0_u32; let mut missing_count = 0_u32; for table in after_tables { let before_exists = table_exists_in(before_tables, table.table_name.as_str()); if table.exists && before_exists == std::option::Option::Some(false) { created_count += 1; emit_sql_startup_splash( splash_window, format!("Created table {}", table.table_name).as_str(), "success", true, ); tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table created"); } else if table.exists { existing_count += 1; emit_sql_startup_splash( splash_window, format!("Table {} already exists", table.table_name).as_str(), "info", true, ); tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table already exists"); } else { missing_count += 1; emit_sql_startup_splash( splash_window, format!("Table {} is missing after initialization", table.table_name).as_str(), "warning", true, ); tracing::error!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table missing after initialization"); } } emit_sql_startup_splash( splash_window, format!( "PostgreSQL schema ready: {created_count} created, {existing_count} already present, {missing_count} missing." ) .as_str(), schema_report_status(missing_count), false, ); tracing::debug!(target: crate::TRACING_TARGET, created = created_count, existing = existing_count, missing = missing_count, "PostgreSQL schema initialization completed"); } fn table_exists_in( tables: &[ks_store::PostgresTableDiagnostics], table_name: &str, ) -> std::option::Option { for table in tables { if table.table_name.as_str() == table_name { return std::option::Option::Some(table.exists); } } return std::option::Option::None; } fn schema_report_status(missing_count: u32) -> &'static str { if missing_count == 0 { return "success"; } return "warning"; } fn emit_sql_startup_error(splash_window: &tauri::WebviewWindow, message: &str) { emit_sql_startup_splash( splash_window, format!("PostgreSQL schema initialization error: {message}").as_str(), "danger", false, ); } fn emit_sql_startup_splash( splash_window: &tauri::WebviewWindow, message: &str, status: &str, log_only: bool, ) { let order = if log_only { "add_log" } else { "add_msg" }; crate::emit_splash_order( splash_window, order, std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None, ); }