Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ tiberius = { version = "0.12", default-features = false, features = [
"rust_decimal"
] }
tokio-util = { version = "0.7", features = [ "compat" ] }
rusqlite = { version = "0.32", features = [
"bundled",
"chrono"
] }
rusqlite = { version = "0.32", default-features = false, features = [ "chrono" ] }
# C library bundling is selected by cargo features:
# default → rusqlite/bundled (SQLite)
# sqlcipher → rusqlite/bundled-sqlcipher (SQLCipher, encrypted SQLite)
uuid = { version = "1", features = [ "v4" ] }
tauri-plugin-store = "2"
tauri-plugin-dialog = "2"
Expand Down Expand Up @@ -109,5 +109,8 @@ mockall = "0.13"
tempfile = "3"

[features]
default = []
default = [ "rusqlite/bundled" ]
ohos = []
# Use SQLCipher (encrypted SQLite) instead of plain SQLite.
# Build with: cargo build --no-default-features --features sqlcipher
sqlcipher = [ "rusqlite/bundled-sqlcipher" ]
5 changes: 4 additions & 1 deletion src-tauri/src/commands/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ fn db_type_to_enum(db_type: &str) -> Result<crate::database::DatabaseType, Strin
"mysql" => Ok(DatabaseType::MySQL),
"sqlserver" | "mssql" => Ok(DatabaseType::SqlServer),
"sqlite" => Ok(DatabaseType::SQLite),
"sqlcipher" => Ok(DatabaseType::SQLCipher),
"duckdb" | "duck_db" | "duck" => Ok(DatabaseType::DuckDb),
"clickhouse" => Ok(DatabaseType::ClickHouse),
"firebird" => Ok(DatabaseType::Firebird),
Expand Down Expand Up @@ -218,7 +219,9 @@ fn db_type_to_enum(db_type: &str) -> Result<crate::database::DatabaseType, Strin
fn is_file_based_db(db_type: &crate::database::DatabaseType) -> bool {
matches!(
db_type,
crate::database::DatabaseType::SQLite | crate::database::DatabaseType::DuckDb
crate::database::DatabaseType::SQLite
| crate::database::DatabaseType::SQLCipher
| crate::database::DatabaseType::DuckDb
)
}

Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/database/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub enum DatabaseType {
SqlServer,
/// SQLite.
SQLite,
/// SQLCipher — encrypted SQLite variant (same adapter, requires encryption key).
SQLCipher,
/// DuckDB — JDBC bridge.
DuckDb,
/// ClickHouse (HTTP protocol).
Expand Down
41 changes: 37 additions & 4 deletions src-tauri/src/database/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,23 @@ const MEMORY_DB: &str = ":memory:";
///
/// SQLite has limited concurrency support compared to client-server databases.
/// This pool manages connections with proper synchronization for thread-safety.
/// For SQLCipher, an encryption key is stored and applied via `PRAGMA key` on
/// each newly created connection.
pub struct SQLitePool {
connections: Arc<Mutex<Vec<Arc<Mutex<Connection>>>>>,
max_connections: usize,
db_path: Option<PathBuf>,
encryption_key: Option<String>,
}

impl SQLitePool {
/// Create a new SQLite connection pool.
fn new(max_connections: usize, db_path: Option<PathBuf>) -> Self {
fn new(max_connections: usize, db_path: Option<PathBuf>, encryption_key: Option<String>) -> Self {
Self {
connections: Arc::new(Mutex::new(Vec::new())),
max_connections,
db_path,
encryption_key,
}
}

Expand Down Expand Up @@ -110,8 +114,22 @@ impl SQLitePool {
})?
};

// Apply SQLCipher encryption key if provided
//
// SQLCipher uses `PRAGMA key` to set the passphrase. When compiled with
// the `sqlcipher` cargo feature, rusqlite links against SQLCipher instead
// of SQLite, and this pragma activates encryption. Without the feature
// (default SQLite build), the pragma is silently ignored.
if let Some(ref key) = self.encryption_key {
conn.execute(&format!("PRAGMA key = '{}'", key.replace('\'', "''")), [])
.map_err(|e| {
DbError::Configuration(format!("Failed to set SQLCipher encryption key: {}", e))
})?;
}

// Enable Write-Ahead Logging (WAL) mode for better concurrency
// WAL mode allows multiple readers and one writer to proceed concurrently
// Note: SQLCipher with WAL requires the same key to be set on each connection.
if self.db_path.is_some() {
conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(()))
.map_err(|e| DbError::Configuration(format!("Failed to enable WAL mode: {}", e)))?;
Expand Down Expand Up @@ -201,12 +219,14 @@ impl ConnectionPool for SQLitePool {

/// SQLite database adapter.
///
/// Supports both file-based and in-memory SQLite databases with proper
/// thread-safety through connection pooling and Write-Ahead Logging (WAL) mode.
/// Supports both file-based and in-memory SQLite databases (and SQLCipher
/// encrypted databases) with proper thread-safety through connection pooling
/// and Write-Ahead Logging (WAL) mode.
pub struct SQLiteAdapter {
config: ConnectionConfig,
pool: Option<Arc<SQLitePool>>,
db_path: Option<PathBuf>,
encryption_key: Option<String>,
}

impl SQLiteAdapter {
Expand All @@ -215,6 +235,8 @@ impl SQLiteAdapter {
/// The database path can be specified in the config.database field:
/// - For file-based databases: Use a file path (e.g., "/path/to/db.sqlite")
/// - For in-memory databases: Use ":memory:" or leave database as None
///
/// For SQLCipher, the encryption passphrase can be provided via `config.password`.
pub fn new(config: ConnectionConfig) -> Self {
let db_path = config.database.as_ref().and_then(|db| {
if db == MEMORY_DB {
Expand All @@ -224,10 +246,17 @@ impl SQLiteAdapter {
}
});

let encryption_key = if config.db_type == crate::database::config::DatabaseType::SQLCipher {
config.password.clone()
} else {
None
};

Self {
config,
pool: None,
db_path,
encryption_key,
}
}

Expand Down Expand Up @@ -422,7 +451,11 @@ impl DatabaseAdapter for SQLiteAdapter {
self.config.pool_config.max_connections as usize
};

let pool = SQLitePool::new(max_connections, self.db_path.clone());
let pool = SQLitePool::new(
max_connections,
self.db_path.clone(),
self.encryption_key.clone(),
);

// Test the connection by creating one
let conn = pool.get_conn().await?;
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/database/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub fn resolve_effective_type(db: DatabaseType) -> ConnectionStrategy {

// Other native adapters
SqlServer => ConnectionStrategy::Native(CoreDatabaseType::SqlServer),
SQLite => ConnectionStrategy::Native(CoreDatabaseType::SQLite),
SQLite | SQLCipher => ConnectionStrategy::Native(CoreDatabaseType::SQLite),
ClickHouse => ConnectionStrategy::Native(CoreDatabaseType::ClickHouse),

// JDBC bridge (Java subprocess)
Expand Down Expand Up @@ -131,7 +131,7 @@ pub fn default_port(db: DatabaseType) -> Option<u16> {
Databend => Some(3307),
ManticoreSearch => Some(9306),
SqlServer => Some(1433),
SQLite => None,
SQLite | SQLCipher => None,
DuckDb => None,
ClickHouse => Some(8123),
Firebird => Some(3050),
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ impl ServerConfig {
"mysql" => Ok(DatabaseType::MySQL),
"sqlserver" | "mssql" => Ok(DatabaseType::SqlServer),
"sqlite" => Ok(DatabaseType::SQLite),
"sqlcipher" => Ok(DatabaseType::SQLCipher),
"duckdb" | "duck" => Ok(DatabaseType::DuckDb),
"clickhouse" => Ok(DatabaseType::ClickHouse),
"firebird" => Ok(DatabaseType::Firebird),
Expand Down Expand Up @@ -209,8 +210,8 @@ impl ServerConfig {
}

let db_lower = self.db_type.to_lowercase();
// DuckDB is file-based, uses JDBC bridge with jdbc:duckdb:{filepath}
if db_lower == "sqlite" || db_lower == "duckdb" || db_lower == "duck" {
// SQLite, SQLCipher, and DuckDB are file-based
if db_lower == "sqlite" || db_lower == "sqlcipher" || db_lower == "duckdb" || db_lower == "duck" {
config = config.with_database(&self.host);
} else if let Some(ref database) = self.database {
config = config.with_database(database);
Expand Down
4 changes: 3 additions & 1 deletion src-tauri/src/transfer/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ pub fn generate_ddl_for_engine(
match engine {
DatabaseType::PostgreSQL => generate_postgres_ddl(schema, table, columns, options),
DatabaseType::MySQL => generate_mysql_ddl(schema, table, columns, options),
DatabaseType::SQLite => generate_sqlite_ddl(schema, table, columns, options),
DatabaseType::SQLite | DatabaseType::SQLCipher => {
generate_sqlite_ddl(schema, table, columns, options)
}
DatabaseType::SqlServer => generate_sqlserver_ddl(schema, table, columns, options),
_ => generate_generic_ddl(schema, table, columns, options),
}
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/transfer/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ fn map_type_to_engine(source_type: &str, target_engine: DatabaseType) -> String
"bytea" => "BLOB".to_string(),
_ => source_type.to_string(),
},
DatabaseType::SQLite => match source_lower.as_str() {
DatabaseType::SQLite | DatabaseType::SQLCipher => match source_lower.as_str() {
"int" | "integer" | "bigint" | "smallint" | "tinyint" => "INTEGER".to_string(),
"varchar" | "char" | "text" => "TEXT".to_string(),
"datetime" | "timestamp" | "date" => "TEXT".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/assets/images/database-icons/sqlcipher-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading