feat: add Oracle database support

This commit is contained in:
t8y2 2026-04-30 04:28:33 +08:00
parent 5dfe340551
commit 2e971ce152
13 changed files with 617 additions and 19 deletions

195
src-tauri/Cargo.lock generated
View File

@ -8,6 +8,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.7.8"
@ -353,6 +364,28 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "base64"
version = "0.21.7"
@ -422,6 +455,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
@ -633,6 +675,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.2.61"
@ -698,6 +749,25 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "combine"
version = "4.6.7"
@ -1048,6 +1118,7 @@ dependencies = [
"duckdb",
"log",
"mongodb",
"oracle-rs",
"portpicker",
"redis",
"reqwest 0.12.28",
@ -1593,6 +1664,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
@ -2172,6 +2249,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hostname"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
dependencies = [
"cfg-if",
"libc",
"windows-link 0.2.1",
]
[[package]]
name = "html5ever"
version = "0.29.1"
@ -2267,7 +2355,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
"webpki-roots 1.0.7",
]
[[package]]
@ -2492,6 +2580,16 @@ dependencies = [
"cfb",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "ipconfig"
version = "0.3.4"
@ -3112,7 +3210,7 @@ dependencies = [
"tokio-util",
"typed-builder",
"uuid",
"webpki-roots",
"webpki-roots 1.0.7",
]
[[package]]
@ -3505,6 +3603,39 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "oracle-rs"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c9ca364b441f92b717658c62e85207158b30285deb01811ccd923017f61612e"
dependencies = [
"aes",
"async-trait",
"bytes",
"cbc",
"chrono",
"hex",
"hmac",
"hostname",
"indexmap 2.14.0",
"md-5",
"pbkdf2",
"pkcs8",
"rand 0.8.6",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"sha1",
"sha2",
"thiserror 1.0.69",
"tokio",
"tokio-rustls",
"tracing",
"webpki-roots 0.26.11",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
@ -3582,6 +3713,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
@ -3803,6 +3935,21 @@ dependencies = [
"spki",
]
[[package]]
name = "pkcs5"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
dependencies = [
"aes",
"cbc",
"der",
"pbkdf2",
"scrypt",
"sha2",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -3810,6 +3957,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"pkcs5",
"rand_core 0.6.4",
"spki",
]
@ -4359,7 +4508,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"webpki-roots 1.0.7",
]
[[package]]
@ -4563,6 +4712,7 @@ version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
@ -4572,6 +4722,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
@ -4588,6 +4747,7 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@ -4605,6 +4765,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -4680,6 +4849,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "seahash"
version = "4.1.0"
@ -6853,6 +7033,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.7",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"

View File

@ -42,3 +42,4 @@ tiberius = { version = "0.12.3", features = ["tds73", "chrono"] }
tokio-util = { version = "0.7", features = ["compat"] }
reqwest = { version = "0.12", features = ["json"] }
mongodb = "3.2.5"
oracle-rs = "0.1"

View File

@ -16,6 +16,7 @@ pub enum PoolKind {
MongoDb(mongodb::Client),
ClickHouse(db::clickhouse_driver::ChClient),
SqlServer(std::sync::Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
Oracle(std::sync::Arc<tokio::sync::Mutex<db::oracle_driver::OraclePool>>),
}
pub struct AppState {
@ -38,20 +39,24 @@ impl AppState {
connection_id: &str,
database: Option<&str>,
) -> Result<String, String> {
let is_embedded = {
let db_type = {
let configs = self.configs.lock().await;
configs.get(connection_id)
.map(|c| c.db_type == DatabaseType::Sqlite || c.db_type == DatabaseType::DuckDb)
.unwrap_or(false)
configs.get(connection_id).map(|c| c.db_type.clone())
};
let is_embedded = matches!(db_type, Some(DatabaseType::Sqlite) | Some(DatabaseType::DuckDb));
if is_embedded {
return Ok(connection_id.to_string());
}
let pool_key = match database {
Some(db) => format!("{connection_id}:{db}"),
None => connection_id.to_string(),
let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle));
let pool_key = if is_single_conn {
connection_id.to_string()
} else {
match database {
Some(db) => format!("{connection_id}:{db}"),
None => connection_id.to_string(),
}
};
let conns = self.connections.lock().await;
@ -69,7 +74,9 @@ impl AppState {
let mut db_config = config.clone();
if let Some(db) = database {
db_config.database = Some(db.to_string());
if db_config.db_type != DatabaseType::Oracle {
db_config.database = Some(db.to_string());
}
}
let url = db_config.connection_url();
@ -102,6 +109,14 @@ impl AppState {
).await?;
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
}
DatabaseType::Oracle => {
let pool = db::oracle_driver::OraclePool::connect(
&db_config.host, db_config.port,
db_config.database.as_deref().unwrap_or("ORCL"),
&db_config.username, &db_config.password,
).await?;
PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(pool)))
}
};
self.connections.lock().await.insert(pool_key.clone(), pool);
@ -113,9 +128,19 @@ impl AppState {
connection_id: &str,
database: Option<&str>,
) -> Result<String, String> {
let pool_key = match database {
Some(db) => format!("{connection_id}:{db}"),
None => connection_id.to_string(),
let is_single_conn = {
let configs = self.configs.lock().await;
configs.get(connection_id)
.map(|c| c.db_type == DatabaseType::Oracle)
.unwrap_or(false)
};
let pool_key = if is_single_conn {
connection_id.to_string()
} else {
match database {
Some(db) => format!("{connection_id}:{db}"),
None => connection_id.to_string(),
}
};
self.connections.lock().await.remove(&pool_key);
self.get_or_create_pool(connection_id, database).await
@ -196,6 +221,14 @@ pub async fn test_connection(config: ConnectionConfig) -> Result<String, String>
).await?;
Ok("Connection successful".to_string())
}
DatabaseType::Oracle => {
let _pool = db::oracle_driver::OraclePool::connect(
&config.host, config.port,
config.database.as_deref().unwrap_or("ORCL"),
&config.username, &config.password,
).await?;
Ok("Connection successful".to_string())
}
}
}
@ -245,6 +278,14 @@ pub async fn connect_db(
config.database.as_deref(),
).await?;
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client))) }
DatabaseType::Oracle => {
let pool = db::oracle_driver::OraclePool::connect(
&config.host, config.port,
config.database.as_deref().unwrap_or("ORCL"),
&config.username, &config.password,
).await?;
PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(pool)))
}
};
state.connections.lock().await.insert(id.clone(), pool);
@ -275,6 +316,7 @@ pub async fn disconnect_db(
PoolKind::MongoDb(_) => {},
PoolKind::ClickHouse(_) => {},
PoolKind::SqlServer(_) => {},
PoolKind::Oracle(_) => {},
}
}
}

View File

@ -132,6 +132,15 @@ async fn do_execute(
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
.map(truncate_result)
}
PoolKind::Oracle(pool) => {
let pool = pool.clone();
drop(connections);
let mut pool = pool.lock().await;
timeout(QUERY_TIMEOUT, db::oracle_driver::execute_query(&mut pool, sql))
.await
.map_err(|_| format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs()))?
.map(truncate_result)
}
PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()),
PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()),
}

View File

@ -75,6 +75,13 @@ fn extract_clickhouse(connections: &std::collections::HashMap<String, PoolKind>,
}
}
fn extract_oracle(connections: &std::collections::HashMap<String, PoolKind>, key: &str) -> Option<std::sync::Arc<tokio::sync::Mutex<db::oracle_driver::OraclePool>>> {
match connections.get(key)? {
PoolKind::Oracle(pool) => Some(pool.clone()),
_ => None,
}
}
#[tauri::command]
pub async fn list_databases(
state: State<'_, Arc<AppState>>,
@ -91,6 +98,11 @@ pub async fn list_databases(
let mut client = client.lock().await;
return db::sqlserver::list_databases(&mut client).await;
}
if let Some(pool) = extract_oracle(&connections, &connection_id) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::list_databases(&pool).await;
}
}
let connections = state.connections.lock().await;
@ -120,6 +132,11 @@ pub async fn list_schemas(
let mut client = client.lock().await;
return db::sqlserver::list_schemas(&mut client).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::list_schemas(&pool).await;
}
}
let connections = state.connections.lock().await;
@ -156,6 +173,11 @@ pub async fn list_tables(
let mut client = client.lock().await;
return db::sqlserver::list_tables(&mut client, &schema).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::list_tables(&pool, &schema).await;
}
}
let connections = state.connections.lock().await;
@ -195,6 +217,11 @@ pub async fn get_columns(
let mut client = client.lock().await;
return db::sqlserver::get_columns(&mut client, &schema, &table).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::get_columns(&pool, &schema, &table).await;
}
}
let connections = state.connections.lock().await;
@ -225,6 +252,11 @@ pub async fn list_indexes(
let mut client = client.lock().await;
return db::sqlserver::list_indexes(&mut client, &schema, &table).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::list_indexes(&pool, &schema, &table).await;
}
}
let connections = state.connections.lock().await;
@ -255,6 +287,11 @@ pub async fn list_foreign_keys(
let mut client = client.lock().await;
return db::sqlserver::list_foreign_keys(&mut client, &schema, &table).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::list_foreign_keys(&pool, &schema, &table).await;
}
}
let connections = state.connections.lock().await;
@ -285,6 +322,11 @@ pub async fn list_triggers(
let mut client = client.lock().await;
return db::sqlserver::list_triggers(&mut client, &schema, &table).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return db::oracle_driver::list_triggers(&pool, &schema, &table).await;
}
}
let connections = state.connections.lock().await;
@ -336,6 +378,11 @@ pub async fn get_table_ddl(
let mut client = client.lock().await;
return build_sqlserver_ddl(&mut client, &schema, &table).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let pool = pool.lock().await;
return build_oracle_ddl(&pool, &schema, &table).await;
}
}
let connections = state.connections.lock().await;
@ -429,3 +476,35 @@ async fn build_sqlserver_ddl(client: &mut db::sqlserver::SqlServerClient, schema
}
Ok(ddl)
}
async fn build_oracle_ddl(pool: &db::oracle_driver::OraclePool, schema: &str, table: &str) -> Result<String, String> {
let columns = db::oracle_driver::get_columns(pool, schema, table).await?;
let indexes = db::oracle_driver::list_indexes(pool, schema, table).await?;
let fkeys = db::oracle_driver::list_foreign_keys(pool, schema, table).await?;
let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n");
let col_lines: Vec<String> = columns.iter().map(|c| {
let mut line = format!(" \"{}\" {}", c.name, c.data_type);
if !c.is_nullable { line.push_str(" NOT NULL"); }
if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); }
line
}).collect();
ddl.push_str(&col_lines.join(",\n"));
let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect();
if !pks.is_empty() {
ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("\"{k}\"")).collect::<Vec<_>>().join(", ")));
}
for fk in &fkeys {
ddl.push_str(&format!(",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", fk.name, fk.column, fk.ref_table, fk.ref_column));
}
ddl.push_str("\n);\n");
for idx in &indexes {
if idx.is_primary { continue; }
let unique = if idx.is_unique { "UNIQUE " } else { "" };
let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::<Vec<_>>().join(", ");
ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name));
}
Ok(ddl)
}

View File

@ -1,6 +1,7 @@
pub mod clickhouse_driver;
pub mod mongo_driver;
pub mod mysql;
pub mod oracle_driver;
pub mod postgres;
pub mod redis_driver;
pub mod sqlite;

View File

@ -0,0 +1,250 @@
use oracle_rs::{Config, Connection};
use std::time::Instant;
use super::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
pub struct OraclePool {
pub conn: Connection,
host: String,
port: u16,
service: String,
user: String,
pass: String,
}
impl OraclePool {
pub async fn connect(host: &str, port: u16, service: &str, user: &str, pass: &str) -> Result<Self, String> {
let config = Config::new(host, port, service, user, pass);
let conn = Connection::connect_with_config(config)
.await
.map_err(|e| format!("Oracle connection failed: {e}"))?;
Ok(Self {
conn,
host: host.to_string(),
port,
service: service.to_string(),
user: user.to_string(),
pass: pass.to_string(),
})
}
pub async fn reconnect(&mut self) -> Result<(), String> {
let config = Config::new(&self.host, self.port, &self.service, &self.user, &self.pass);
self.conn = Connection::connect_with_config(config)
.await
.map_err(|e| format!("Oracle reconnect failed: {e}"))?;
Ok(())
}
}
fn value_to_json(val: &oracle_rs::Value) -> serde_json::Value {
match val {
oracle_rs::Value::Null => serde_json::Value::Null,
oracle_rs::Value::String(s) => serde_json::Value::String(s.clone()),
oracle_rs::Value::Integer(n) => serde_json::Value::Number((*n).into()),
oracle_rs::Value::Float(f) => serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
oracle_rs::Value::Boolean(b) => serde_json::Value::Bool(*b),
oracle_rs::Value::Json(v) => v.clone(),
_ => serde_json::Value::String(format!("{val:?}")),
}
}
pub async fn list_databases(pool: &OraclePool) -> Result<Vec<DatabaseInfo>, String> {
let result = pool.conn.query(
"SELECT username FROM all_users ORDER BY username",
&[],
).await.map_err(|e| e.to_string())?;
Ok(result.rows.iter().map(|row| {
DatabaseInfo {
name: row.get_string(0).unwrap_or("").to_string(),
}
}).collect())
}
pub async fn list_schemas(pool: &OraclePool) -> Result<Vec<String>, String> {
let result = pool.conn.query(
"SELECT username FROM all_users ORDER BY username",
&[],
).await.map_err(|e| e.to_string())?;
Ok(result.rows.iter().map(|row| {
row.get_string(0).unwrap_or("").to_string()
}).collect())
}
pub async fn list_tables(pool: &OraclePool, schema: &str) -> Result<Vec<TableInfo>, String> {
let sql = format!(
"SELECT table_name, 'TABLE' AS table_type FROM all_tables WHERE owner = '{s}' \
UNION ALL \
SELECT view_name, 'VIEW' FROM all_views WHERE owner = '{s}' \
ORDER BY 1",
s = schema.replace('\'', "''")
);
let result = pool.conn.query(&sql, &[]).await.map_err(|e| e.to_string())?;
Ok(result.rows.iter().map(|row| {
TableInfo {
name: row.get_string(0).unwrap_or("").to_string(),
table_type: row.get_string(1).unwrap_or("TABLE").to_string(),
}
}).collect())
}
pub async fn get_columns(pool: &OraclePool, schema: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
let s = schema.replace('\'', "''");
let t = table.replace('\'', "''");
let pk_result = pool.conn.query(
&format!(
"SELECT cols.COLUMN_NAME FROM ALL_CONS_COLUMNS cols \
JOIN ALL_CONSTRAINTS cons ON cols.CONSTRAINT_NAME = cons.CONSTRAINT_NAME AND cols.OWNER = cons.OWNER \
WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.OWNER = '{s}' AND cons.TABLE_NAME = '{t}'"
),
&[],
).await.map_err(|e| e.to_string())?;
let pk_names: std::collections::HashSet<String> = pk_result.rows.iter()
.filter_map(|row| row.get_string(0).map(|s| s.to_string()))
.collect();
let col_result = pool.conn.query(
&format!(
"SELECT COLUMN_NAME, DATA_TYPE, NULLABLE \
FROM ALL_TAB_COLUMNS \
WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \
ORDER BY COLUMN_ID"
),
&[],
).await.map_err(|e| e.to_string())?;
Ok(col_result.rows.iter().map(|row| {
let name = row.get_string(0).unwrap_or("").to_string();
ColumnInfo {
is_primary_key: pk_names.contains(&name),
name,
data_type: row.get_string(1).unwrap_or("").to_string(),
is_nullable: row.get_string(2).unwrap_or("N") == "Y",
column_default: None,
extra: None,
}
}).collect())
}
pub async fn list_indexes(pool: &OraclePool, schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
let sql = format!(
"SELECT i.INDEX_NAME, \
LISTAGG(ic.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) AS columns, \
i.UNIQUENESS, \
CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK \
FROM ALL_INDEXES i \
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.TABLE_OWNER = ic.TABLE_OWNER \
LEFT JOIN ALL_CONSTRAINTS c ON i.INDEX_NAME = c.INDEX_NAME AND i.TABLE_OWNER = c.OWNER \
AND c.CONSTRAINT_TYPE = 'P' \
WHERE i.TABLE_OWNER = '{s}' AND i.TABLE_NAME = '{t}' \
GROUP BY i.INDEX_NAME, i.UNIQUENESS, c.CONSTRAINT_TYPE \
ORDER BY i.INDEX_NAME",
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
);
let result = pool.conn.query(&sql, &[]).await.map_err(|e| e.to_string())?;
Ok(result.rows.iter().map(|row| {
let cols_str = row.get_string(1).unwrap_or("");
IndexInfo {
name: row.get_string(0).unwrap_or("").to_string(),
columns: cols_str.split(',').map(|s| s.to_string()).collect(),
is_unique: row.get_string(2).unwrap_or("") == "UNIQUE",
is_primary: row.get_i64(3).unwrap_or(0) == 1,
}
}).collect())
}
pub async fn list_foreign_keys(pool: &OraclePool, schema: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
let sql = format!(
"SELECT c.CONSTRAINT_NAME, cc.COLUMN_NAME, rc.TABLE_NAME, rcc.COLUMN_NAME \
FROM ALL_CONSTRAINTS c \
JOIN ALL_CONS_COLUMNS cc ON c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME AND c.OWNER = cc.OWNER \
JOIN ALL_CONSTRAINTS rc ON c.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND c.R_OWNER = rc.OWNER \
JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER \
WHERE c.CONSTRAINT_TYPE = 'R' AND c.OWNER = '{s}' AND c.TABLE_NAME = '{t}' \
ORDER BY c.CONSTRAINT_NAME",
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
);
let result = pool.conn.query(&sql, &[]).await.map_err(|e| e.to_string())?;
Ok(result.rows.iter().map(|row| {
ForeignKeyInfo {
name: row.get_string(0).unwrap_or("").to_string(),
column: row.get_string(1).unwrap_or("").to_string(),
ref_table: row.get_string(2).unwrap_or("").to_string(),
ref_column: row.get_string(3).unwrap_or("").to_string(),
}
}).collect())
}
pub async fn list_triggers(pool: &OraclePool, schema: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
let sql = format!(
"SELECT TRIGGER_NAME, TRIGGERING_EVENT, TRIGGER_TYPE \
FROM ALL_TRIGGERS \
WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \
ORDER BY TRIGGER_NAME",
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
);
let result = pool.conn.query(&sql, &[]).await.map_err(|e| e.to_string())?;
Ok(result.rows.iter().map(|row| {
TriggerInfo {
name: row.get_string(0).unwrap_or("").to_string(),
event: row.get_string(1).unwrap_or("").to_string(),
timing: row.get_string(2).unwrap_or("").to_string(),
}
}).collect())
}
pub async fn execute_query(pool: &mut OraclePool, sql: &str) -> Result<QueryResult, String> {
let start = Instant::now();
let sql = sql.trim().trim_end_matches(';');
let trimmed = sql.to_uppercase();
if trimmed.starts_with("SELECT")
|| trimmed.starts_with("WITH")
|| trimmed.starts_with("SHOW")
|| trimmed.starts_with("DESCRIBE")
|| trimmed.starts_with("EXPLAIN")
{
let result = pool.conn.query(sql, &[]).await.map_err(|e| e.to_string())?;
let columns: Vec<String> = result.columns.iter().map(|c| c.name.clone()).collect();
let rows: Vec<Vec<serde_json::Value>> = result.rows.iter().map(|row| {
(0..columns.len()).map(|i| {
row.get(i)
.map(|v| value_to_json(v))
.unwrap_or(serde_json::Value::Null)
}).collect()
}).collect();
Ok(QueryResult {
columns,
rows,
affected_rows: 0,
execution_time_ms: start.elapsed().as_millis(),
truncated: false,
})
} else {
match pool.conn.execute(sql, &[]).await {
Ok(result) => {
pool.conn.commit().await.map_err(|e| e.to_string())?;
Ok(QueryResult {
columns: vec![],
rows: vec![],
affected_rows: result.rows_affected,
execution_time_ms: start.elapsed().as_millis(),
truncated: false,
})
}
Err(e) => {
let msg = e.to_string();
if msg.contains("Server rejected") || msg.contains("closed the connection") {
let _ = pool.reconnect().await;
Err("Operation failed — possibly a constraint violation (foreign key, unique, or check constraint).".to_string())
} else {
Err(msg)
}
}
}
}
}

View File

@ -41,6 +41,8 @@ pub enum DatabaseType {
SqlServer,
#[serde(rename = "mongodb")]
MongoDb,
#[serde(rename = "oracle")]
Oracle,
}
impl ConnectionConfig {
@ -84,6 +86,10 @@ impl ConnectionConfig {
format!("mongodb://{}:{}@{host}:{port}{db_part}", self.username, self.password)
}
}
DatabaseType::Oracle => format!(
"oracle://{}:{}@{host}:{port}{db_part}",
self.username, self.password
),
}
}
}

View File

@ -277,7 +277,7 @@ function quoteIdent(tab: ActiveTab, name: string): string {
function qualifiedTableName(tab: NonNullable<typeof activeTab.value>): string {
const config = connectionStore.getConfig(tab.connectionId);
if (!tab.tableMeta) return "";
if (config?.db_type === "postgres" && tab.tableMeta.schema) {
if ((config?.db_type === "postgres" || config?.db_type === "oracle") && tab.tableMeta.schema) {
return `${quoteIdent(tab, tab.tableMeta.schema)}.${quoteIdent(tab, tab.tableMeta.tableName)}`;
}
return quoteIdent(tab, tab.tableMeta.tableName);
@ -293,9 +293,16 @@ function buildTableSql(
tab: NonNullable<typeof activeTab.value>,
options: { orderBy?: string; limit?: number; offset?: number } = {},
): string {
const config = connectionStore.getConfig(tab.connectionId);
const limit = options.limit ?? 100;
const orderBy = options.orderBy ?? defaultOrderBy(tab);
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
if (config?.db_type === "oracle") {
const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : "";
return `SELECT * FROM ${qualifiedTableName(tab)}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
}
const offset = options.offset ? ` OFFSET ${options.offset}` : "";
return `SELECT * FROM ${qualifiedTableName(tab)}${order} LIMIT ${limit}${offset};`;
}

View File

@ -87,6 +87,7 @@ function onDbTypeChange(val: string) {
mongodb: { type: "mongodb", port: 27017, user: "" },
clickhouse: { type: "clickhouse", port: 8123, user: "default" },
sqlserver: { type: "sqlserver", port: 1433, user: "sa" },
oracle: { type: "oracle", port: 1521, user: "system" },
tidb: { type: "mysql", port: 4000, user: "root" },
oceanbase: { type: "mysql", port: 2881, user: "root" },
goldendb: { type: "mysql", port: 3306, user: "root" },
@ -110,6 +111,7 @@ function onDbTypeChange(val: string) {
const iconTypeMap: Record<string, string> = {
mysql: "mysql", postgres: "postgres", sqlite: "sqlite", redis: "redis",
mongodb: "mongodb", duckdb: "duckdb", clickhouse: "clickhouse", sqlserver: "sqlserver",
oracle: "oracle",
mariadb: "mariadb", tidb: "tidb", oceanbase: "mysql", goldendb: "mysql",
opengauss: "postgres", gaussdb: "postgres", kingbase: "postgres", vastbase: "postgres",
};
@ -123,6 +125,7 @@ const dbOptions = [
{ value: "duckdb", label: "DuckDB" },
{ value: "clickhouse", label: "ClickHouse" },
{ value: "sqlserver", label: "SQL Server" },
{ value: "oracle", label: "Oracle" },
{ value: "mariadb", label: "MariaDB" },
];

File diff suppressed because one or more lines are too long

View File

@ -144,7 +144,7 @@ async function openData() {
if (!(node.type === "table" || node.type === "view") || !node.connectionId || !node.database) return;
await connectionStore.ensureConnected(node.connectionId);
const config = connectionStore.getConfig(node.connectionId);
const qualifiedName = config?.db_type === "postgres" && node.schema
const qualifiedName = (config?.db_type === "postgres" || config?.db_type === "oracle") && node.schema
? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}`
: quoteIdent(node.label);
const tabId = queryStore.createTab(node.connectionId, node.database, node.label, "data");
@ -153,7 +153,9 @@ async function openData() {
const columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
const pks = columns.filter((c) => c.is_primary_key).map((c) => c.name);
const order = pks.length ? ` ORDER BY ${pks.map((pk) => `${quoteIdent(pk)} ASC`).join(", ")}` : "";
const sql = `SELECT * FROM ${qualifiedName}${order} LIMIT 100;`;
const sql = config?.db_type === "oracle"
? `SELECT * FROM ${qualifiedName}${order} FETCH FIRST 100 ROWS ONLY`
: `SELECT * FROM ${qualifiedName}${order} LIMIT 100;`;
queryStore.updateSql(tabId, sql);
queryStore.setTableMeta(tabId, {
schema: node.schema,

View File

@ -1,4 +1,4 @@
export type DatabaseType = "mysql" | "postgres" | "sqlite" | "redis" | "duckdb" | "clickhouse" | "sqlserver" | "mongodb";
export type DatabaseType = "mysql" | "postgres" | "sqlite" | "redis" | "duckdb" | "clickhouse" | "sqlserver" | "mongodb" | "oracle";
export interface ConnectionConfig {
id: string;