313 lines
12 KiB
Rust
313 lines
12 KiB
Rust
// 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<i64>,
|
|
/// Minimum slot when available.
|
|
#[ts(type = "number | null")]
|
|
pub(crate) min_slot: std::option::Option<i64>,
|
|
/// Maximum slot when available.
|
|
#[ts(type = "number | null")]
|
|
pub(crate) max_slot: std::option::Option<i64>,
|
|
/// Latest insertion timestamp rendered by PostgreSQL when available.
|
|
pub(crate) latest_created_at: std::option::Option<std::string::String>,
|
|
}
|
|
|
|
/// 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<ks_store::PostgresStoreOptions> {
|
|
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<ks_store::PostgresStore, std::string::String> {
|
|
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<crate::DemoSqlTableSnapshot> {
|
|
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<T: std::fmt::Debug>(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<bool> {
|
|
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,
|
|
);
|
|
}
|