From f1290360b86995eb91dcf504a67023352bb02883 Mon Sep 17 00:00:00 2001 From: matheuscamposmt Date: Sat, 1 Nov 2025 15:51:39 -0300 Subject: [PATCH 1/7] feat: add SSL/TLS support dependencies - Add postgres-native-tls for secure database connections - Add native-tls for cross-platform SSL support - Enable SSL connections for PostgreSQL and Redshift --- src-tauri/Cargo.lock | 209 +++++++++++++++++++++++++++++++++++++------ src-tauri/Cargo.toml | 2 + 2 files changed, 185 insertions(+), 26 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 061ac62..15cb707 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -526,6 +526,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -549,9 +559,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.4", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -562,7 +572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.9.4", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -987,6 +997,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -994,7 +1013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1008,6 +1027,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1571,7 +1596,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -2111,6 +2136,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2476,6 +2518,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2623,6 +2709,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -2727,6 +2823,15 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2796,11 +2901,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "postgres-native-tls" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac73153d92e4bde922bd6f1dfba7f1ab8132266c031153b55e20a1521cd36d49" +dependencies = [ + "native-tls", + "tokio", + "tokio-native-tls", + "tokio-postgres", +] + [[package]] name = "postgres-protocol" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" +checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" dependencies = [ "base64 0.22.1", "byteorder", @@ -2816,9 +2933,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" +checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" dependencies = [ "bytes", "fallible-iterator", @@ -3181,6 +3298,8 @@ version = "0.1.0" dependencies = [ "bincode", "chrono", + "native-tls", + "postgres-native-tls", "serde", "serde_json", "sled", @@ -3244,6 +3363,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3301,6 +3429,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3600,16 +3751,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -3629,7 +3770,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "objc2 0.5.2", @@ -3802,7 +3943,7 @@ checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" dependencies = [ "bitflags 2.9.4", "block2 0.6.1", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -4289,15 +4430,25 @@ dependencies = [ "mio", "pin-project-lite", "slab", - "socket2 0.6.0", + "socket2", "windows-sys 0.59.0", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-postgres" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" +checksum = "2b40d66d9b2cfe04b628173409368e58247e8eddbbd3b0e6c6ba1d09f20f6c9e" dependencies = [ "async-trait", "byteorder", @@ -4308,12 +4459,12 @@ dependencies = [ "log", "parking_lot 0.12.4", "percent-encoding", - "phf 0.11.3", + "phf 0.13.1", "pin-project-lite", "postgres-protocol", "postgres-types", "rand 0.9.2", - "socket2 0.5.10", + "socket2", "tokio", "tokio-util", "whoami", @@ -4708,6 +4859,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0f5a2ea..1bd22de 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,8 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = "1.47.1" tokio-postgres = "0.7.13" +postgres-native-tls = "0.5" +native-tls = "0.2" chrono = "0.4.42" sled = "0.34.7" tracing = "0.1.41" From f18718991f2acafd0cf432b7b43eaa8c8a8bf906 Mon Sep 17 00:00:00 2001 From: matheuscamposmt Date: Sat, 1 Nov 2025 15:51:43 -0300 Subject: [PATCH 2/7] refactor: implement modular database driver architecture - Create DatabaseDriver interface for extensibility - Implement factory pattern for driver management - Add DriverFactory to manage multiple database types - Add driver configuration with default ports - Make it easy to add new database drivers in the future --- src-tauri/src/common/enums.rs | 3 ++ src/lib/database-driver.ts | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/lib/database-driver.ts diff --git a/src-tauri/src/common/enums.rs b/src-tauri/src/common/enums.rs index 7b706df..f010179 100644 --- a/src-tauri/src/common/enums.rs +++ b/src-tauri/src/common/enums.rs @@ -6,12 +6,14 @@ use serde::{Deserialize, Serialize}; pub enum Drivers { #[default] PGSQL, + REDSHIFT, } impl Display for Drivers { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Drivers::PGSQL => write!(f, "PGSQL"), + Drivers::REDSHIFT => write!(f, "REDSHIFT"), } } } @@ -20,6 +22,7 @@ impl AsRef for Drivers { fn as_ref(&self) -> &str { match self { Drivers::PGSQL => "PGSQL", + Drivers::REDSHIFT => "REDSHIFT", } } } diff --git a/src/lib/database-driver.ts b/src/lib/database-driver.ts new file mode 100644 index 0000000..af19ec6 --- /dev/null +++ b/src/lib/database-driver.ts @@ -0,0 +1,95 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { ProjectConnectionStatus, TableInfo, QueryResult } from "@/tauri"; + +export type DriverType = "PGSQL" | "REDSHIFT"; + +export interface DatabaseDriver { + connect(projectId: string, key: [string, string, string, string, string, string]): Promise; + loadSchemas(projectId: string): Promise; + loadTables(projectId: string, schema: string): Promise; + loadColumns(projectId: string, schema: string, table: string): Promise; + runQuery(projectId: string, sql: string): Promise; +} + +class PostgreSQLDriver implements DatabaseDriver { + async connect(projectId: string, key: [string, string, string, string, string, string]): Promise { + return await invoke("pgsql_connector", { project_id: projectId, key }); + } + + async loadSchemas(projectId: string): Promise { + return await invoke("pgsql_load_schemas", { project_id: projectId }); + } + + async loadTables(projectId: string, schema: string): Promise { + return await invoke("pgsql_load_tables", { project_id: projectId, schema }); + } + + async loadColumns(projectId: string, schema: string, table: string): Promise { + return await invoke("pgsql_load_columns", { project_id: projectId, schema, table }); + } + + async runQuery(projectId: string, sql: string): Promise { + return await invoke("pgsql_run_query", { project_id: projectId, sql }); + } +} + +class RedshiftDriver implements DatabaseDriver { + async connect(projectId: string, key: [string, string, string, string, string, string]): Promise { + return await invoke("redshift_connector", { project_id: projectId, key }); + } + + async loadSchemas(projectId: string): Promise { + return await invoke("redshift_load_schemas", { project_id: projectId }); + } + + async loadTables(projectId: string, schema: string): Promise { + return await invoke("redshift_load_tables", { project_id: projectId, schema }); + } + + async loadColumns(projectId: string, schema: string, table: string): Promise { + return await invoke("redshift_load_columns", { project_id: projectId, schema, table }); + } + + async runQuery(projectId: string, sql: string): Promise { + return await invoke("redshift_run_query", { project_id: projectId, sql }); + } +} + +// Factory pattern para criar drivers +export class DriverFactory { + private static drivers: Map = new Map([ + ["PGSQL", new PostgreSQLDriver()], + ["REDSHIFT", new RedshiftDriver()], + ]); + + static getDriver(driverType: DriverType): DatabaseDriver { + const driver = this.drivers.get(driverType); + if (!driver) { + throw new Error(`Driver ${driverType} not found`); + } + return driver; + } + + static getSupportedDrivers(): DriverType[] { + return Array.from(this.drivers.keys()); + } +} + +// Configuração de cada driver +export interface DriverConfig { + name: string; + defaultPort: string; + icon?: string; +} + +export const DRIVER_CONFIGS: Record = { + PGSQL: { + name: "PostgreSQL", + defaultPort: "5432", + }, + REDSHIFT: { + name: "Amazon Redshift", + defaultPort: "5439", + }, +}; + From 457cb28b6afe5394f33eecd1de6a13076c3a8553 Mon Sep 17 00:00:00 2001 From: matheuscamposmt Date: Sat, 1 Nov 2025 15:51:46 -0300 Subject: [PATCH 3/7] feat: add Amazon Redshift database driver - Implement Redshift connector with SSL support - Add schema, table, and column loading for Redshift - Use information_schema for better permission compatibility - Support Redshift-specific SQL queries - Register Redshift commands in Tauri handler --- src-tauri/src/drivers/mod.rs | 1 + src-tauri/src/drivers/redshift.rs | 270 ++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 5 + 3 files changed, 276 insertions(+) create mode 100644 src-tauri/src/drivers/redshift.rs diff --git a/src-tauri/src/drivers/mod.rs b/src-tauri/src/drivers/mod.rs index c23eeb7..23a16fa 100644 --- a/src-tauri/src/drivers/mod.rs +++ b/src-tauri/src/drivers/mod.rs @@ -1 +1,2 @@ pub mod pgsql; +pub mod redshift; diff --git a/src-tauri/src/drivers/redshift.rs b/src-tauri/src/drivers/redshift.rs new file mode 100644 index 0000000..6e093eb --- /dev/null +++ b/src-tauri/src/drivers/redshift.rs @@ -0,0 +1,270 @@ +use std::{sync::Arc, time::Instant}; + +use crate::common::{ + enums::{PostgresqlError, ProjectConnectionStatus}, + pgsql::{PgsqlLoadColumns, PgsqlLoadSchemas, PgsqlLoadTables}, +}; +use tauri::{AppHandle, Manager, Result, State}; +use tokio::{sync::Mutex, time as tokio_time}; +use tokio_postgres::Config; +use postgres_native_tls::MakeTlsConnector; +use native_tls::TlsConnector; + +use crate::{utils::reflective_get, AppState}; + +#[tauri::command(rename_all = "snake_case")] +pub async fn redshift_connector( + project_id: &str, + key: Option<[&str; 6]>, + app: AppHandle, +) -> Result { + let app_state = app.state::(); + let mut clients = app_state.client.lock().await; + tracing::info!("Redshift connection attempt: {:?}", key); + + // check if connection already exists + if clients.as_ref().unwrap().contains_key(project_id) { + tracing::info!("Redshift connection already exists!"); + return Ok(ProjectConnectionStatus::Connected); + } + + let (user, password, database, host, port_str, use_ssl) = match key { + Some(key) => ( + key[0].to_string(), + key[1].to_string(), + key[2].to_string(), + key[3].to_string(), + key[4].to_string(), + key[5] == "true", + ), + None => { + let projects_db = app_state.project_db.lock().await; + let projects_db = projects_db.as_ref().unwrap(); + let project_details = projects_db.get(project_id).unwrap(); + let project_details = match project_details { + Some(bytes) => bincode::deserialize::>(&bytes).unwrap(), + _ => Vec::new(), + }; + ( + project_details[1].clone(), + project_details[2].clone(), + project_details[3].clone(), + project_details[4].clone(), + project_details[5].clone(), + project_details.get(6).map(|s| s == "true").unwrap_or(false), + ) + } + }; + + let port: u16 = port_str.parse::().unwrap_or(5439); // Redshift default port + let mut cfg = Config::new(); + cfg.user(&user); + cfg.password(password); + cfg.dbname(&database); + cfg.host(&host); + cfg.port(port); + + tracing::info!("Redshift SSL enabled: {}", use_ssl); + + // Redshift always uses SSL in production, but allow NoTls for testing + let tls_connector = TlsConnector::builder() + .danger_accept_invalid_certs(true) // Accept self-signed certs + .build() + .unwrap(); + let tls = MakeTlsConnector::new(tls_connector); + + let connection = tokio_time::timeout(tokio_time::Duration::from_secs(10), cfg.connect(tls)) + .await + .map_err(|_| PostgresqlError::ConnectionTimeout); + + if let Err(e) = connection { + tracing::error!("Redshift connection timeout error: {:?}", e); + return Ok(ProjectConnectionStatus::Failed); + } + + let connection = connection.unwrap(); + if let Err(e) = connection { + tracing::error!("Redshift connection error: {:?}", e); + return Ok(ProjectConnectionStatus::Failed); + } + + let is_connection_error = Arc::new(Mutex::new(false)); + let (client, connection) = connection.unwrap(); + tracing::info!("Redshift connection established!"); + + // check if connection has some error + tokio::spawn({ + let is_connection_error = Arc::clone(&is_connection_error); + async move { + if let Err(e) = connection.await { + tracing::info!("Redshift connection error: {:?}", e); + *is_connection_error.lock().await = true; + } + } + }); + + if *is_connection_error.lock().await { + tracing::error!("Redshift connection error!"); + return Ok(ProjectConnectionStatus::Failed); + } + + let clients = clients.as_mut().unwrap(); + clients.insert(project_id.to_string(), client); + + Ok(ProjectConnectionStatus::Connected) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn redshift_load_schemas( + project_id: &str, + app_state: State<'_, AppState>, +) -> Result { + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + + // Redshift uses similar schema query but with some differences + let query = tokio_time::timeout( + tokio_time::Duration::from_secs(10), + client.query( + r#" + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_internal') + ORDER BY schema_name; + "#, + &[], + ), + ) + .await + .map_err(|_| PostgresqlError::QueryTimeout); + + if query.is_err() { + tracing::error!("Redshift schema query timeout error!"); + return Err(tauri::Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + PostgresqlError::QueryTimeout, + ))); + } + + let query = query.unwrap(); + if query.is_err() { + tracing::error!("Redshift schema query error!"); + return Err(tauri::Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + PostgresqlError::QueryError, + ))); + } + + let qeury = query.unwrap(); + let schemas = qeury.iter().map(|r| r.get(0)).collect::>(); + tracing::info!("Redshift schemas: {:?}", schemas); + Ok(schemas) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn redshift_load_tables( + project_id: &str, + schema: &str, + app_state: State<'_, AppState>, +) -> Result { + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + + // Use information_schema which is more universally accessible + let query = client + .query( + r#"--sql + SELECT + table_name, + '-' AS size + FROM information_schema.tables + WHERE table_schema = $1 + AND table_type = 'BASE TABLE' + ORDER BY table_name; + "#, + &[&schema], + ) + .await; + + + if let Err(e) = query { + tracing::error!("Redshift load tables error: {:?}", e); + return Err(tauri::Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to load tables: {:?}", e), + ))); + } + + let rows = query.unwrap(); + let tables = rows + .iter() + .map(|r| (r.get(0), r.get(1))) + .collect::>(); + Ok(tables) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn redshift_load_columns( + project_id: &str, + schema: &str, + table: &str, + app_state: State<'_, AppState>, +) -> Result { + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + let rows = client + .query( + r#"--sql + SELECT column_name + FROM information_schema.columns + WHERE table_schema = $1 AND table_name = $2 + ORDER BY ordinal_position; + "#, + &[&schema, &table], + ) + .await + .unwrap(); + let cols = rows + .iter() + .map(|r| r.get::<_, String>(0)) + .collect::>(); + Ok(cols) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn redshift_run_query( + project_id: &str, + sql: &str, + app_state: State<'_, AppState>, +) -> Result<(Vec, Vec>, f32)> { + let start = Instant::now(); + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + let rows = client.query(sql, &[]).await.unwrap(); + + if rows.is_empty() { + return Ok((Vec::new(), Vec::new(), 0.0f32)); + } + + let columns = rows + .first() + .unwrap() + .columns() + .iter() + .map(|c| c.name().to_string()) + .collect::>(); + let rows = rows + .iter() + .map(|row| { + let mut row_values = Vec::new(); + for i in 0..row.len() { + let value = reflective_get(row, i); + row_values.push(value); + } + row_values + }) + .collect::>>(); + let elasped = start.elapsed().as_millis() as f32; + Ok((columns, rows, elasped)) +} + diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c09f546..63c68a7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -80,6 +80,11 @@ fn main() { drivers::pgsql::pgsql_load_tables, drivers::pgsql::pgsql_load_columns, drivers::pgsql::pgsql_run_query, + drivers::redshift::redshift_connector, + drivers::redshift::redshift_load_schemas, + drivers::redshift::redshift_load_tables, + drivers::redshift::redshift_load_columns, + drivers::redshift::redshift_run_query, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); From 3cbb1363565fb9be79b2da8c9ce31df2bf50869b Mon Sep 17 00:00:00 2001 From: matheuscamposmt Date: Sat, 1 Nov 2025 15:51:50 -0300 Subject: [PATCH 4/7] feat: add multi-database driver support to UI - Update connection modal with database type selector - Add driver selection dropdown (PostgreSQL, Redshift) - Auto-adjust default ports based on selected driver - Update connection logic to use driver factory - Extend connection configuration to include SSL option - Refactor frontend to dynamically use appropriate driver --- src-tauri/src/drivers/pgsql.rs | 6 +- src/App.tsx | 104 ++++++++++++++++++---------- src/components/connection-modal.tsx | 35 +++++++++- src/tauri.ts | 43 +++++++++++- 4 files changed, 148 insertions(+), 40 deletions(-) diff --git a/src-tauri/src/drivers/pgsql.rs b/src-tauri/src/drivers/pgsql.rs index 8d69751..c62d8e9 100644 --- a/src-tauri/src/drivers/pgsql.rs +++ b/src-tauri/src/drivers/pgsql.rs @@ -13,7 +13,7 @@ use crate::{utils::reflective_get, AppState}; #[tauri::command(rename_all = "snake_case")] pub async fn pgsql_connector( project_id: &str, - key: Option<[&str; 5]>, + key: Option<[&str; 6]>, app: AppHandle, ) -> Result { let app_state = app.state::(); @@ -25,13 +25,14 @@ pub async fn pgsql_connector( return Ok(ProjectConnectionStatus::Connected); } - let (user, password, database, host, port_str) = match key { + let (user, password, database, host, port_str, use_ssl) = match key { Some(key) => ( key[0].to_string(), key[1].to_string(), key[2].to_string(), key[3].to_string(), key[4].to_string(), + key[5] == "true", ), None => { let projects_db = app_state.project_db.lock().await; @@ -47,6 +48,7 @@ pub async fn pgsql_connector( project_details[3].clone(), project_details[4].clone(), project_details[5].clone(), + project_details.get(6).map(|s| s == "true").unwrap_or(false), ) } }; diff --git a/src/App.tsx b/src/App.tsx index aac2b69..491cdfe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback, useEffect } from "react"; import type * as Monaco from "monaco-editor"; import { useKeyPressEvent } from "react-use"; -import { Database, Play, Save, Settings, ChevronRight, ChevronDown, Server, Table, Plus, X, CheckCircle2, Clock } from "lucide-react"; +import { Database, Play, Save, Settings, ChevronRight, ChevronDown, Server, Table, Plus, X, CheckCircle2, Clock, Moon, Sun } from "lucide-react"; import Editor from "@monaco-editor/react"; import { Button } from "@/components/ui/button"; import { ResizeHandle } from "@/components/resize-handle"; @@ -13,15 +13,11 @@ import { getProjects, insertProject, insertQuery, - pgsqlConnector, - pgsqlLoadColumns, - pgsqlLoadSchemas, - pgsqlLoadTables, - pgsqlRunQuery, ProjectConnectionStatus, ProjectMap, TableInfo, } from "@/tauri"; +import { DriverFactory, type DriverType } from "@/lib/database-driver"; type Tab = { id: number; @@ -216,6 +212,7 @@ export default function App() { const [connectionModalOpen, setConnectionModalOpen] = useState(false); const [viewMode, setViewMode] = useState<"grid" | "record">("grid"); const [selectedRow, setSelectedRow] = useState(0); + const [theme, setTheme] = useState<"light" | "dark">("light"); const [projects, setProjects] = useState({}); const [status, setStatus] = useState>({}); @@ -243,6 +240,14 @@ export default function App() { useEffect(() => { columnsRef.current = columns; }, [columns]); useEffect(() => { activeProjectRef.current = activeProject; }, [activeProject]); + // Helper to get driver for a project + const getProjectDriver = useCallback((projectId: string) => { + const d = projectsRef.current[projectId]; + if (!d) return null; + const driverType = d[0] as DriverType; + return DriverFactory.getDriver(driverType); + }, []); + function registerContextAwareCompletions(monaco: typeof Monaco) { type TableRef = { schema?: string; table: string }; function stripQuotes(s: string) { return s.replaceAll('"', ""); } @@ -261,12 +266,15 @@ export default function App() { async function resolveTableRef(projectId: string, ref: TableRef): Promise<{ schema: string; table: string } | null> { if (ref.schema) return { schema: ref.schema, table: ref.table }; const projSchemas = schemasRef.current[projectId] || []; + const driver = getProjectDriver(projectId); + if (!driver) return { schema: "public", table: ref.table }; + for (const schema of projSchemas) { const key = `${projectId}::${schema}`; let t = tablesRef.current[key]; if (!t) { try { - t = await pgsqlLoadTables(projectId, schema); + t = await driver.loadTables(projectId, schema); tablesRef.current[key] = t; setTables((prev) => ({ ...prev, [key]: t! })); } catch {} @@ -280,6 +288,8 @@ export default function App() { provideCompletionItems: async (model, position) => { const projectId = activeProjectRef.current; if (!projectId) return { suggestions: [] }; + const driver = getProjectDriver(projectId); + if (!driver) return { suggestions: [] }; const textUntilPosition = model.getValueInRange({ startLineNumber: 1, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, @@ -315,7 +325,7 @@ export default function App() { let cols = columnsRef.current[colKey]; if (!cols) { try { - cols = await pgsqlLoadColumns(projectId, resolved.schema, resolved.table); + cols = await driver.loadColumns(projectId, resolved.schema, resolved.table); columnsRef.current[colKey] = cols; setColumns((prev) => ({ ...prev, [colKey]: cols! })); } catch {} @@ -330,7 +340,7 @@ export default function App() { let t = tablesRef.current[key]; if (!t) { try { - t = await pgsqlLoadTables(projectId, schema); + t = await driver.loadTables(projectId, schema); tablesRef.current[key] = t; setTables((prev) => ({ ...prev, [key]: t! })); } catch {} @@ -347,7 +357,7 @@ export default function App() { let cols = columnsRef.current[colKey]; if (!cols) { try { - cols = await pgsqlLoadColumns(projectId, left, right); + cols = await driver.loadColumns(projectId, left, right); columnsRef.current[colKey] = cols; setColumns((prev) => ({ ...prev, [colKey]: cols! })); } catch {} @@ -364,7 +374,7 @@ export default function App() { let t = tablesRef.current[key]; if (!t) { try { - t = await pgsqlLoadTables(projectId, schema); + t = await driver.loadTables(projectId, schema); tablesRef.current[key] = t; setTables((prev) => ({ ...prev, [key]: t! })); } catch {} @@ -415,12 +425,15 @@ export default function App() { const d = projects[project_id]; if (!d) return; setStatus((s) => ({ ...s, [project_id]: ProjectConnectionStatus.Connecting })); - const key: [string, string, string, string, string] = [d[1], d[2], d[3], d[4], d[5]]; + const driverType = d[0] as DriverType; + const useSsl = d[6] === "true"; // 7th element is SSL flag + const key: [string, string, string, string, string, string] = [d[1], d[2], d[3], d[4], d[5], useSsl ? "true" : "false"]; try { - const st = await pgsqlConnector(project_id, key); + const driver = DriverFactory.getDriver(driverType); + const st = await driver.connect(project_id, key); setStatus((s) => ({ ...s, [project_id]: st })); if (st === ProjectConnectionStatus.Connected) { - const sc = await pgsqlLoadSchemas(project_id); + const sc = await driver.loadSchemas(project_id); setSchemas((prev) => ({ ...prev, [project_id]: sc })); } } catch { @@ -436,9 +449,13 @@ export default function App() { const onLoadTables = useCallback(async (project_id: string, schema: string) => { const key = `${project_id}::${schema}`; if (tables[key]) return; - const rows = await pgsqlLoadTables(project_id, schema); + const d = projects[project_id]; + if (!d) return; + const driverType = d[0] as DriverType; + const driver = DriverFactory.getDriver(driverType); + const rows = await driver.loadTables(project_id, schema); setTables((t) => ({ ...t, [key]: rows })); - }, [tables]); + }, [tables, projects]); const onOpenDefaultTableQuery = useCallback((project_id: string, schema: string, table: string) => { const sql = `SELECT * FROM "${schema}"."${table}" LIMIT 100;`; @@ -448,14 +465,18 @@ export default function App() { const runQuery = useCallback(async () => { if (!activeProject || !activeTab) return; - const [cols, rows, time] = await pgsqlRunQuery(activeProject, activeTab.editorValue); + const d = projects[activeProject]; + if (!d) return; + const driverType = d[0] as DriverType; + const driver = DriverFactory.getDriver(driverType); + const [cols, rows, time] = await driver.runQuery(activeProject, activeTab.editorValue); setTabs((ts) => { const copy = ts.slice(); copy[selectedTab] = { ...copy[selectedTab], result: { columns: cols, rows, time } }; return copy; }); setSelectedRow(0); - }, [activeProject, activeTab, selectedTab]); + }, [activeProject, activeTab, selectedTab, projects]); const saveQuery = useCallback(async (title: string) => { if (!activeProject) return; @@ -478,19 +499,31 @@ export default function App() { }; const handleSaveConnection = useCallback(async (connection: ConnectionConfig) => { - const details = ["PGSQL", connection.username, connection.password, connection.database, connection.host, connection.port]; + const details = [connection.driver, connection.username, connection.password, connection.database, connection.host, connection.port, connection.ssl ? "true" : "false"]; await insertProject(connection.name, details); await reloadProjects(); }, [reloadProjects]); + const toggleTheme = () => { + setTheme(prev => prev === "light" ? "dark" : "light"); + }; + + useEffect(() => { + if (theme === "dark") { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [theme]); + return (
{/* Top Bar */}
- - PostgresGUI + + PostgresGUI
{activeProject && activeProjectDetails ? ( <> @@ -503,9 +536,9 @@ export default function App() { status[activeProject] === ProjectConnectionStatus.Failed && "bg-destructive", !status[activeProject] && "bg-destructive" )} /> - {activeProject} + {activeProject} • - {activeProjectDetails[4]}:{activeProjectDetails[5]} + {activeProjectDetails[4]}:{activeProjectDetails[5]}
) : null} @@ -522,8 +555,8 @@ export default function App() { }} disabled={!activeProject} > - - Save + + Save
-
@@ -578,7 +611,7 @@ export default function App() { : "bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground" )} > - {tabs.length > 1 && ( @@ -592,26 +625,26 @@ export default function App() { }} className="opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100" > - + )}
))} {/* SQL Editor */}
-
+
+
Loading editor...
} @@ -624,7 +657,6 @@ export default function App() { lineNumbers: "on", quickSuggestions: { other: true, comments: false, strings: true }, suggestOnTriggerCharacters: true, - theme: "vs-dark", }} value={activeTab?.editorValue} onChange={(v) => { diff --git a/src/components/connection-modal.tsx b/src/components/connection-modal.tsx index 48f4a21..fe12ce3 100644 --- a/src/components/connection-modal.tsx +++ b/src/components/connection-modal.tsx @@ -7,6 +7,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" +import { DriverFactory, DRIVER_CONFIGS, type DriverType } from "@/lib/database-driver" interface ConnectionModalProps { open: boolean @@ -17,6 +18,7 @@ interface ConnectionModalProps { export interface ConnectionConfig { id: string name: string + driver: DriverType host: string port: string database: string @@ -28,6 +30,7 @@ export interface ConnectionConfig { export function ConnectionModal({ open, onOpenChange, onSave }: ConnectionModalProps) { const [formData, setFormData] = useState>({ name: "", + driver: "PGSQL", host: "localhost", port: "5432", database: "", @@ -47,6 +50,7 @@ export function ConnectionModal({ open, onOpenChange, onSave }: ConnectionModalP // Reset form setFormData({ name: "", + driver: "PGSQL", host: "localhost", port: "5432", database: "", @@ -56,16 +60,45 @@ export function ConnectionModal({ open, onOpenChange, onSave }: ConnectionModalP }) } + const handleDriverChange = (driver: DriverType) => { + const config = DRIVER_CONFIGS[driver]; + setFormData({ + ...formData, + driver, + port: config.defaultPort, + }) + } + + const supportedDrivers = DriverFactory.getSupportedDrivers(); + return ( New Connection - Add a new PostgreSQL database connection + Add a new database connection
+
+ + +
+