feat(agent): migrate Oracle to JDBC agent, add install progress bar and toast

- Migrate Oracle from rust-oracle to JDBC agent (ojdbc11)
- Delete oracle_driver.rs (567 lines) and rust-oracle dependency
- Add streaming download progress bar for driver/JRE installation
- Add toast notifications for install/uninstall success/failure
- Add "update available" badge in driver manager
This commit is contained in:
t8y2 2026-05-13 09:20:06 +08:00
parent b012ae32f9
commit c50323cd6c
14 changed files with 175 additions and 972 deletions

132
Cargo.lock generated
View File

@ -1573,7 +1573,6 @@ dependencies = [
"redis",
"reqwest 0.12.28",
"russh",
"rust-oracle",
"rust_decimal",
"rustls 0.23.40",
"serde",
@ -1615,7 +1614,6 @@ dependencies = [
"reqwest 0.12.28",
"russh",
"rust-gaussdb",
"rust-oracle",
"rust_decimal",
"rustls 0.23.40",
"serde",
@ -2937,17 +2935,6 @@ 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.38.0"
@ -3092,7 +3079,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"socket2",
"system-configuration",
"tokio",
"tower-service",
@ -3348,7 +3335,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
dependencies = [
"socket2 0.6.3",
"socket2",
"widestring",
"windows-registry",
"windows-result 0.4.1",
@ -4043,7 +4030,7 @@ dependencies = [
"serde_with",
"sha1 0.10.6",
"sha2 0.10.9",
"socket2 0.6.3",
"socket2",
"stringprep",
"strsim",
"take_mut",
@ -4865,21 +4852,6 @@ dependencies = [
"spki 0.8.0-rc.4",
]
[[package]]
name = "pkcs5"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
dependencies = [
"aes 0.8.4",
"cbc 0.1.2",
"der 0.7.10",
"pbkdf2 0.12.2",
"scrypt 0.11.0",
"sha2 0.10.9",
"spki 0.7.3",
]
[[package]]
name = "pkcs5"
version = "0.8.0-rc.13"
@ -4892,7 +4864,7 @@ dependencies = [
"der 0.8.0",
"pbkdf2 0.13.0",
"rand_core 0.10.1",
"scrypt 0.12.0",
"scrypt",
"sha2 0.11.0",
"spki 0.8.0-rc.4",
]
@ -4904,8 +4876,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der 0.7.10",
"pkcs5 0.7.1",
"rand_core 0.6.4",
"spki 0.7.3",
]
@ -4916,7 +4886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577"
dependencies = [
"der 0.8.0",
"pkcs5 0.8.0-rc.13",
"pkcs5",
"rand_core 0.10.1",
"spki 0.8.0-rc.4",
]
@ -5242,7 +5212,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.40",
"socket2 0.6.3",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
@ -5279,7 +5249,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
@ -5411,7 +5381,7 @@ dependencies = [
"rustls-native-certs 0.8.3",
"ryu",
"sha1_smol",
"socket2 0.6.3",
"socket2",
"tokio",
"tokio-rustls 0.26.4",
"tokio-util",
@ -5769,7 +5739,7 @@ dependencies = [
"pbkdf2 0.12.2",
"pbkdf2 0.13.0",
"pkcs1 0.8.0-rc.4",
"pkcs5 0.8.0-rc.13",
"pkcs5",
"pkcs8 0.11.0-rc.11",
"polyval 0.7.1",
"rand 0.10.1",
@ -5777,8 +5747,8 @@ dependencies = [
"rsa 0.10.0-rc.16",
"russh-cryptovec",
"russh-util",
"salsa20 0.11.0",
"scrypt 0.12.0",
"salsa20",
"scrypt",
"sec1",
"sha1 0.10.6",
"sha1 0.11.0",
@ -5846,39 +5816,6 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "rust-oracle"
version = "0.1.6"
source = "git+https://github.com/t8y2/rust-oracle?branch=main#72ceb94752c4c16daa4f449fc6d7a0b16ba9fd09"
dependencies = [
"aes 0.8.4",
"async-trait",
"bytes",
"cbc 0.1.2",
"chrono",
"hex",
"hmac 0.12.1",
"hostname",
"indexmap 2.14.0",
"md-5 0.10.6",
"pbkdf2 0.12.2",
"pkcs8 0.10.2",
"rand 0.8.6",
"rustls 0.23.40",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
"serde_json",
"sha1 0.10.6",
"sha2 0.10.9",
"socket2 0.5.10",
"thiserror 1.0.69",
"tokio",
"tokio-rustls 0.26.4",
"tracing",
"webpki-roots 0.26.11",
]
[[package]]
name = "rust_decimal"
version = "1.42.0"
@ -6003,7 +5940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe 0.1.6",
"rustls-pemfile 1.0.4",
"rustls-pemfile",
"schannel",
"security-framework 2.11.1",
]
@ -6029,15 +5966,6 @@ dependencies = [
"base64 0.21.7",
]
[[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"
@ -6109,15 +6037,6 @@ 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 0.4.4",
]
[[package]]
name = "salsa20"
version = "0.11.0"
@ -6203,17 +6122,6 @@ 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 0.12.2",
"salsa20 0.10.2",
"sha2 0.10.9",
]
[[package]]
name = "scrypt"
version = "0.12.0"
@ -6222,7 +6130,7 @@ checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0"
dependencies = [
"cfg-if",
"pbkdf2 0.13.0",
"salsa20 0.11.0",
"salsa20",
"sha2 0.11.0",
]
@ -6702,16 +6610,6 @@ dependencies = [
"serde",
]
[[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.3"
@ -7742,7 +7640,7 @@ dependencies = [
"pretty-hex",
"rust_decimal",
"rustls-native-certs 0.6.3",
"rustls-pemfile 1.0.4",
"rustls-pemfile",
"thiserror 1.0.69",
"tokio-rustls 0.24.1",
"tokio-util",
@ -7829,7 +7727,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.3",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]

View File

@ -26,7 +26,6 @@ tiberius = { version = "0.12.3", default-features = false, features = ["tds73",
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "socks"] }
futures = "0.3"
mongodb = "3.2.5"
rust-oracle = { git = "https://github.com/t8y2/rust-oracle", branch = "main" }
russh = "0.60"
portpicker = "0.1.1"
csv = "1"

View File

@ -121,6 +121,7 @@ impl AgentManager {
DatabaseType::Kingbase => Some("kingbase"),
DatabaseType::Vastbase => Some("vastbase"),
DatabaseType::Goldendb => Some("goldendb"),
DatabaseType::Oracle => Some("oracle"),
_ => None,
}
}

View File

@ -1,6 +1,5 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::RwLock;
@ -43,7 +42,6 @@ pub enum PoolKind {
MongoDb(mongodb::Client),
ClickHouse(db::clickhouse_driver::ChClient),
SqlServer(Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
Oracle(Arc<OraclePool>),
Elasticsearch(db::elasticsearch_driver::EsClient),
Agent(Arc<tokio::sync::Mutex<db::agent_driver::AgentDriverClient>>),
Gaussdb(Arc<tokio::sync::Mutex<db::gaussdb_driver::GaussdbClient>>),
@ -51,46 +49,6 @@ pub enum PoolKind {
ExternalDriver { driver_id: String, config: ConnectionConfig, session: Arc<PluginDriverSession> },
}
pub struct OraclePool {
clients: Vec<Arc<tokio::sync::Mutex<db::oracle_driver::OracleClient>>>,
next: AtomicUsize,
}
impl OraclePool {
pub fn new(clients: Vec<db::oracle_driver::OracleClient>) -> Self {
Self {
clients: clients.into_iter().map(|client| Arc::new(tokio::sync::Mutex::new(client))).collect(),
next: AtomicUsize::new(0),
}
}
pub fn client(&self) -> Arc<tokio::sync::Mutex<db::oracle_driver::OracleClient>> {
let index = self.next.fetch_add(1, Ordering::Relaxed) % self.clients.len();
self.clients[index].clone()
}
pub fn primary(&self) -> Arc<tokio::sync::Mutex<db::oracle_driver::OracleClient>> {
self.clients[0].clone()
}
}
async fn connect_oracle_pool(
host: &str,
port: u16,
service: &str,
user: &str,
pass: &str,
sysdba: bool,
) -> Result<OraclePool, String> {
let (first, second, third) = tokio::try_join!(
db::oracle_driver::connect(host, port, service, user, pass, sysdba),
db::oracle_driver::connect(host, port, service, user, pass, sysdba),
db::oracle_driver::connect(host, port, service, user, pass, sysdba),
)?;
let clients = vec![first, second, third];
Ok(OraclePool::new(clients))
}
pub struct AppState {
pub connections: RwLock<HashMap<String, PoolKind>>,
pub configs: RwLock<HashMap<String, ConnectionConfig>>,
@ -187,20 +145,7 @@ impl AppState {
let conns = self.connections.read().await;
if conns.contains_key(&pool_key) {
if let Some(PoolKind::Oracle(pool)) = conns.get(&pool_key) {
let client = pool.primary();
let conn = client.lock().await;
if conn.is_closed() {
drop(conn);
drop(conns);
log::info!("[oracle] connection closed, reconnecting...");
self.connections.write().await.remove(&pool_key);
} else {
return Ok(pool_key);
}
} else {
return Ok(pool_key);
}
return Ok(pool_key);
} else {
drop(conns);
}
@ -259,25 +204,17 @@ impl AppState {
.await?;
PoolKind::SqlServer(Arc::new(tokio::sync::Mutex::new(client)))
}
DatabaseType::Oracle => {
let pool = connect_oracle_pool(
&host,
port,
db_config.database.as_deref().unwrap_or("ORCL"),
&db_config.username,
&db_config.password,
db_config.sysdba,
)
.await?;
PoolKind::Oracle(Arc::new(pool))
}
DatabaseType::Elasticsearch => {
let client =
db::elasticsearch_driver::EsClient::new(&url, Some(&db_config.username), Some(&db_config.password));
db::elasticsearch_driver::test_connection(&client).await?;
PoolKind::Elasticsearch(client)
}
DatabaseType::Dameng | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb => {
DatabaseType::Dameng
| DatabaseType::Kingbase
| DatabaseType::Vastbase
| DatabaseType::Goldendb
| DatabaseType::Oracle => {
let mut client = self.agent_manager.spawn(&db_config.db_type).await?;
client
.call::<serde_json::Value>(
@ -465,7 +402,11 @@ pub async fn probe_connection_endpoint(config: &ConnectionConfig, host: &str, po
DatabaseType::Sqlite | DatabaseType::DuckDb => Ok(()),
DatabaseType::MongoDb if config.connection_string.as_deref().is_some_and(|value| !value.is_empty()) => Ok(()),
DatabaseType::Jdbc => Ok(()),
DatabaseType::Dameng | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb => Ok(()),
DatabaseType::Dameng
| DatabaseType::Kingbase
| DatabaseType::Vastbase
| DatabaseType::Goldendb
| DatabaseType::Oracle => Ok(()),
_ => db::probe_tcp_endpoint(&format!("{:?}", config.db_type), host, port).await,
}
}

View File

@ -7,7 +7,6 @@ pub mod gaussdb_driver;
pub mod mongo_driver;
pub mod mysql;
pub mod ob_oracle;
pub mod oracle_driver;
pub mod postgres;
pub mod proxy_tunnel;
pub mod redis_driver;

View File

@ -1,567 +0,0 @@
use log;
use rust_oracle::{Config, Connection};
use std::time::Instant;
use super::{connection_timeout, CONNECTION_TIMEOUT_SECS};
use crate::sql::starts_with_executable_sql_keyword;
use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
pub type OracleClient = Connection;
const ORACLE_QUERY_LIMIT: usize = crate::query::MAX_ROWS + 1;
fn quote_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
pub async fn connect(
host: &str,
port: u16,
service: &str,
user: &str,
pass: &str,
sysdba: bool,
) -> Result<OracleClient, String> {
let config = Config::new(host, port, service, user, pass).with_statement_cache_size(0).sysdba_flag(sysdba);
let conn = tokio::time::timeout(connection_timeout(), Connection::connect_with_config(config))
.await
.map_err(|_| format!("Oracle connection timed out ({CONNECTION_TIMEOUT_SECS}s)"))?
.map_err(|e| format!("Oracle connection failed: {e}"))?;
Ok(conn)
}
fn value_to_json(val: &rust_oracle::Value) -> serde_json::Value {
match val {
rust_oracle::Value::Null => serde_json::Value::Null,
rust_oracle::Value::String(s) => serde_json::Value::String(s.clone()),
rust_oracle::Value::Integer(n) => serde_json::Value::Number((*n).into()),
rust_oracle::Value::Float(f) => {
serde_json::Number::from_f64(*f).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)
}
rust_oracle::Value::Date(d) => serde_json::Value::String(format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
d.year, d.month, d.day, d.hour, d.minute, d.second
)),
rust_oracle::Value::Timestamp(ts) => {
let base = format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second
);
let value = if ts.microsecond > 0 { format!("{base}.{:06}", ts.microsecond) } else { base };
serde_json::Value::String(value)
}
rust_oracle::Value::Boolean(b) => serde_json::Value::Bool(*b),
rust_oracle::Value::Json(v) => v.clone(),
_ => serde_json::Value::String(format!("{val:?}")),
}
}
pub async fn list_databases(conn: &OracleClient) -> Result<Vec<DatabaseInfo>, String> {
let result = conn
.query(
"WITH schema_names AS ( \
SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS owner FROM DUAL \
UNION \
SELECT DISTINCT owner FROM all_tables \
UNION \
SELECT DISTINCT owner FROM all_views \
) \
SELECT owner \
FROM schema_names \
WHERE owner IS NOT NULL \
AND owner NOT IN (\
'SYS','SYSTEM','SYSMAN','DBSNMP','SYSBACKUP','SYSDG','SYSKM','OUTLN',\
'AUDSYS','LBACSYS','DVF','DVSYS','APPQOSSYS','CTXSYS','MDSYS','MDDATA',\
'ORDSYS','ORDDATA','ORDPLUGINS','XDB','ANONYMOUS','DIP','EXFSYS',\
'GSMADMIN_INTERNAL','GSMCATUSER','GSMUSER','OJVMSYS','OLAPSYS',\
'ORACLE_OCM','SI_INFORMTN_SCHEMA','WMSYS','XS$NULL','DBSFWUSER',\
'REMOTE_SCHEDULER_AGENT','PDBADMIN','DGPDB_INT','OPS$ORACLE',\
'GGSYS','FLOWS_FILES','APEX_PUBLIC_USER'\
) \
AND owner NOT LIKE 'APEX_%' \
AND owner NOT LIKE 'FLOWS_%' \
AND owner NOT LIKE '%$%' \
ORDER BY CASE \
WHEN owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') THEN 0 \
WHEN owner = SYS_CONTEXT('USERENV', 'SESSION_USER') THEN 1 \
ELSE 2 \
END, owner",
&[],
)
.await
.map_err(|e| {
log::error!("[oracle] list_databases failed: {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(conn: &OracleClient) -> Result<Vec<String>, String> {
let dbs = list_databases(conn).await?;
Ok(dbs.into_iter().map(|d| d.name).collect())
}
pub async fn list_tables(conn: &OracleClient, schema: &str) -> Result<Vec<TableInfo>, String> {
let s = quote_literal(schema);
let sql = format!(
"SELECT o.OBJECT_NAME, \
CASE o.OBJECT_TYPE WHEN 'VIEW' THEN 'VIEW' ELSE 'TABLE' END AS TABLE_TYPE, \
c.COMMENTS \
FROM ALL_OBJECTS o \
LEFT JOIN ALL_TAB_COMMENTS c ON c.OWNER = o.OWNER AND c.TABLE_NAME = o.OBJECT_NAME \
WHERE o.OWNER = {s} AND o.OBJECT_TYPE IN ('TABLE','VIEW') \
ORDER BY o.OBJECT_NAME"
);
log::debug!("[oracle] list_tables: schema={schema}, sql={sql}");
let result = conn.query(&sql, &[]).await.map_err(|e| {
log::error!("[oracle] list_tables failed: {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(),
comment: row.get_string(2).filter(|s| !s.is_empty()).map(|s| s.to_string()),
})
.collect())
}
pub async fn list_objects(conn: &OracleClient, schema: &str) -> Result<Vec<crate::types::ObjectInfo>, String> {
let s = quote_literal(schema);
let sql = format!(
"SELECT o.OBJECT_NAME, \
CASE o.OBJECT_TYPE \
WHEN 'TABLE' THEN 'TABLE' \
WHEN 'VIEW' THEN 'VIEW' \
WHEN 'PROCEDURE' THEN 'PROCEDURE' \
WHEN 'FUNCTION' THEN 'FUNCTION' \
ELSE o.OBJECT_TYPE \
END AS OBJECT_TYPE, \
c.COMMENTS \
FROM ALL_OBJECTS o \
LEFT JOIN ALL_TAB_COMMENTS c ON c.OWNER = o.OWNER AND c.TABLE_NAME = o.OBJECT_NAME \
WHERE o.OWNER = {s} \
AND o.OBJECT_TYPE IN ('TABLE','VIEW','PROCEDURE','FUNCTION') \
AND o.OBJECT_NAME NOT LIKE 'BIN$%' \
ORDER BY CASE o.OBJECT_TYPE \
WHEN 'TABLE' THEN 0 \
WHEN 'VIEW' THEN 1 \
WHEN 'PROCEDURE' THEN 2 \
WHEN 'FUNCTION' THEN 3 \
ELSE 4 \
END, o.OBJECT_NAME"
);
let result = conn.query(&sql, &[]).await.map_err(|e| e.to_string())?;
Ok(result
.rows
.iter()
.map(|row| crate::types::ObjectInfo {
name: row.get_string(0).unwrap_or("").to_string(),
object_type: row.get_string(1).unwrap_or("TABLE").to_string(),
schema: Some(schema.to_string()),
comment: row.get_string(2).filter(|s| !s.is_empty()).map(|s| s.to_string()),
})
.collect())
}
pub async fn get_columns(conn: &OracleClient, schema: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
log::debug!("[oracle] get_columns: schema={schema}, table={table}");
let s = quote_literal(schema);
let t = quote_literal(table);
let col_result = conn
.query(
&format!(
"SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_PRECISION, c.DATA_SCALE, c.DATA_LENGTH, \
c.CHAR_LENGTH, cc.COMMENTS, CASE WHEN pk.COLUMN_NAME IS NULL THEN 0 ELSE 1 END AS IS_PK \
FROM ALL_TAB_COLUMNS c \
LEFT JOIN ALL_COL_COMMENTS cc ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME \
LEFT JOIN ( \
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} \
) pk ON pk.COLUMN_NAME = c.COLUMN_NAME \
WHERE c.OWNER = {s} AND c.TABLE_NAME = {t} \
ORDER BY c.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();
let base = row.get_string(1).unwrap_or("").to_string();
let data_len = row.get_i64(5).map(|v| v as i32);
let char_len = row.get_i64(6).map(|v| v as i32);
let num_prec = row.get_i64(3).map(|v| v as i32);
let num_scale = row.get_i64(4).map(|v| v as i32);
let data_type = match base.to_uppercase().as_str() {
"VARCHAR2" | "NVARCHAR2" | "CHAR" | "NCHAR" => {
let len = char_len.or(data_len);
match len {
Some(n) => format!("{base}({n})"),
None => base,
}
}
"NUMBER" => match (num_prec, num_scale) {
(Some(p), Some(s)) if s > 0 => format!("NUMBER({p},{s})"),
(Some(p), _) if p > 0 => format!("NUMBER({p})"),
_ => "NUMBER".to_string(),
},
"RAW" => match data_len {
Some(n) => format!("RAW({n})"),
None => "RAW".to_string(),
},
_ => base,
};
ColumnInfo {
is_primary_key: row.get_i64(8).unwrap_or(0) == 1,
name,
data_type,
is_nullable: row.get_string(2).unwrap_or("N") == "Y",
column_default: None,
extra: None,
comment: row.get_string(7).filter(|s| !s.is_empty()).map(|s| s.to_string()),
numeric_precision: num_prec,
numeric_scale: num_scale,
character_maximum_length: char_len,
}
})
.collect())
}
pub async fn get_table_comment(conn: &OracleClient, schema: &str, table: &str) -> Result<Option<String>, String> {
let s = quote_literal(schema);
let t = quote_literal(table);
let result = conn
.query(&format!("SELECT COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = {s} AND TABLE_NAME = {t}"), &[])
.await
.map_err(|e| e.to_string())?;
Ok(result.rows.first().and_then(|row| row.get_string(0)).filter(|s| !s.is_empty()).map(|s| s.to_string()))
}
pub async fn list_indexes(conn: &OracleClient, 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, \
i.INDEX_TYPE \
FROM ALL_INDEXES i \
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER 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, i.INDEX_TYPE \
ORDER BY i.INDEX_NAME",
s = schema.replace('\'', "''"), t = table.replace('\'', "''")
);
let result = 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(',').filter(|s| !s.is_empty()).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,
filter: None,
index_type: row.get_string(4).map(|s| s.to_string()),
included_columns: None,
comment: None,
}
})
.collect())
}
pub async fn list_foreign_keys(conn: &OracleClient, 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 = 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(conn: &OracleClient, 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 = 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_with_schema(conn: &OracleClient, schema: &str, sql: &str) -> Result<QueryResult, String> {
let set_schema = format!("ALTER SESSION SET CURRENT_SCHEMA = \"{}\"", schema);
log::info!("[oracle][set-schema:start] schema={schema}");
conn.execute(&set_schema, &[]).await.map_err(|e| {
log::error!("[oracle] set current_schema failed: {e}");
e.to_string()
})?;
log::info!("[oracle][set-schema:done] schema={schema}");
execute_query(conn, sql).await
}
pub async fn execute_query(conn: &OracleClient, sql: &str) -> Result<QueryResult, String> {
let start = Instant::now();
let sql = sql.trim().trim_end_matches(';');
let explicit_limit = explicit_select_row_limit(sql);
log::info!("[oracle][execute:start] explicit_limit={:?} sql={}", explicit_limit, sql);
// Rewrite FETCH FIRST N ROWS ONLY → ROWNUM for Oracle 11g compatibility.
let sql = rewrite_fetch_first(sql);
log::info!("[oracle][execute:rewritten] sql={}", sql.as_ref());
if starts_with_executable_sql_keyword(sql.as_ref(), &["SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN"]) {
let capped_sql = cap_select_rows(sql.as_ref());
let query_limit = explicit_limit.unwrap_or(ORACLE_QUERY_LIMIT).min(ORACLE_QUERY_LIMIT);
log::info!(
"[oracle][query_with_limit:start] query_limit={} fetch_size=500 sql={}",
query_limit,
capped_sql.as_ref()
);
let result = conn.query_with_limit(capped_sql.as_ref(), &[], query_limit, 500).await.map_err(|e| {
log::error!("[oracle] execute_query SELECT failed: {e}");
e.to_string()
})?;
log::info!(
"[oracle][query_with_limit:done] column_count={} row_count={} has_more_rows={} elapsed_ms={}",
result.columns.len(),
result.rows.len(),
result.has_more_rows,
start.elapsed().as_millis()
);
let columns: Vec<String> = result.columns.iter().map(|c| c.name.clone()).collect();
let mut 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();
let truncated = rows.len() > crate::query::MAX_ROWS || result.has_more_rows;
if rows.len() > crate::query::MAX_ROWS {
rows.truncate(crate::query::MAX_ROWS);
}
log::info!(
"[oracle][execute:done] column_count={} row_count={} truncated={} elapsed_ms={}",
columns.len(),
rows.len(),
truncated,
start.elapsed().as_millis()
);
Ok(QueryResult { columns, rows, affected_rows: 0, execution_time_ms: start.elapsed().as_millis(), truncated })
} else {
log::info!("[oracle][execute-non-select:start] sql={}", sql.as_ref());
match conn.execute(sql.as_ref(), &[]).await {
Ok(result) => {
let _ = conn.commit().await;
log::info!(
"[oracle][execute-non-select:done] affected_rows={} elapsed_ms={}",
result.rows_affected,
start.elapsed().as_millis()
);
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();
log::error!("[oracle][execute-non-select:error] {msg}");
if msg.contains("Server rejected") || msg.contains("closed the connection") {
Err(format!("Operation failed (connection closed). Original driver error: {msg}"))
} else {
Err(msg)
}
}
}
}
}
fn cap_select_rows(sql: &str) -> std::borrow::Cow<'_, str> {
if !starts_with_executable_sql_keyword(sql, &["SELECT", "WITH"]) || has_for_update_clause(sql) {
return std::borrow::Cow::Borrowed(sql);
}
std::borrow::Cow::Owned(format!("SELECT * FROM ({sql}) WHERE ROWNUM <= {ORACLE_QUERY_LIMIT}"))
}
fn has_for_update_clause(sql: &str) -> bool {
sql.to_uppercase().contains(" FOR UPDATE")
}
fn explicit_select_row_limit(sql: &str) -> Option<usize> {
fetch_first_row_limit(sql).or_else(|| rownum_row_limit(sql))
}
fn fetch_first_row_limit(sql: &str) -> Option<usize> {
let upper = sql.to_uppercase();
let fetch_pos = upper.find("FETCH FIRST").or_else(|| upper.find("FETCH NEXT"))?;
let after_fetch = &upper[fetch_pos..];
let end = after_fetch.find("ROWS ONLY")?;
let keyword_len = if after_fetch.starts_with("FETCH FIRST") { 11 } else { 10 };
sql[fetch_pos + keyword_len..fetch_pos + end].trim().parse::<usize>().ok()
}
fn rownum_row_limit(sql: &str) -> Option<usize> {
let upper = sql.to_uppercase();
let mut rest = upper.as_str();
let mut best: Option<usize> = None;
while let Some(pos) = rest.find("ROWNUM") {
rest = &rest[pos + "ROWNUM".len()..];
let trimmed = rest.trim_start();
let value_start = if let Some(after) = trimmed.strip_prefix("<=") {
after.trim_start()
} else if let Some(after) = trimmed.strip_prefix('<') {
if let Some(n) = parse_leading_usize(after.trim_start()) {
let exclusive = n.saturating_sub(1);
best = Some(best.map_or(exclusive, |current| current.min(exclusive)));
}
continue;
} else {
continue;
};
if let Some(n) = parse_leading_usize(value_start) {
best = Some(best.map_or(n, |current| current.min(n)));
}
}
best
}
fn parse_leading_usize(value: &str) -> Option<usize> {
let digits: String = value.chars().take_while(|ch| ch.is_ascii_digit()).collect();
if digits.is_empty() {
return None;
}
digits.parse().ok()
}
fn rewrite_fetch_first(sql: &str) -> std::borrow::Cow<'_, str> {
let upper = sql.to_uppercase();
// Match: ... [OFFSET M ROWS] FETCH FIRST|NEXT N ROWS ONLY
let fetch_pos = upper.find("FETCH FIRST").or_else(|| upper.find("FETCH NEXT"));
let Some(fpos) = fetch_pos else { return std::borrow::Cow::Borrowed(sql) };
let after_fetch = &upper[fpos..];
let Some(end) = after_fetch.find("ROWS ONLY") else { return std::borrow::Cow::Borrowed(sql) };
let keyword_len = if after_fetch.starts_with("FETCH FIRST") { 11 } else { 10 };
let between = sql[fpos + keyword_len..fpos + end].trim();
let Ok(n) = between.parse::<u64>() else { return std::borrow::Cow::Borrowed(sql) };
// Check for OFFSET M ROWS before FETCH
let mut base = &sql[..fpos];
let base_upper = base.to_uppercase();
if let Some(opos) = base_upper.rfind("OFFSET ") {
let after_offset = base_upper[opos + 7..].trim();
if let Some(rpos) = after_offset.find(" ROWS") {
let offset_str = after_offset[..rpos].trim();
if let Ok(offset) = offset_str.parse::<u64>() {
let inner = sql[..opos].trim_end();
return std::borrow::Cow::Owned(format!(
"SELECT * FROM (SELECT a.*, ROWNUM rn__ FROM ({inner}) a WHERE ROWNUM <= {}) WHERE rn__ > {offset}",
offset + n
));
}
}
base = sql[..opos].trim_end();
} else {
base = base.trim_end();
}
std::borrow::Cow::Owned(format!("SELECT * FROM ({base}) WHERE ROWNUM <= {n}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cap_select_rows_wraps_selects() {
let sql = "SELECT * FROM users ORDER BY id";
assert_eq!(cap_select_rows(sql), format!("SELECT * FROM ({sql}) WHERE ROWNUM <= {ORACLE_QUERY_LIMIT}"));
}
#[test]
fn cap_select_rows_wraps_ctes() {
let sql = "WITH recent AS (SELECT * FROM users) SELECT * FROM recent";
assert_eq!(cap_select_rows(sql), format!("SELECT * FROM ({sql}) WHERE ROWNUM <= {ORACLE_QUERY_LIMIT}"));
}
#[test]
fn cap_select_rows_keeps_for_update_queries() {
let sql = "SELECT * FROM users FOR UPDATE";
assert_eq!(cap_select_rows(sql), sql);
}
#[test]
fn rewrite_fetch_first_to_rownum() {
assert_eq!(
rewrite_fetch_first("SELECT * FROM users FETCH FIRST 20 ROWS ONLY"),
"SELECT * FROM (SELECT * FROM users) WHERE ROWNUM <= 20"
);
}
#[test]
fn explicit_select_row_limit_reads_fetch_first() {
assert_eq!(explicit_select_row_limit("SELECT * FROM users FETCH FIRST 100 ROWS ONLY"), Some(100));
assert_eq!(explicit_select_row_limit("SELECT * FROM users OFFSET 20 ROWS FETCH NEXT 50 ROWS ONLY"), Some(50));
}
#[test]
fn explicit_select_row_limit_reads_rownum() {
assert_eq!(explicit_select_row_limit("SELECT * FROM users WHERE ROWNUM <= 100"), Some(100));
assert_eq!(explicit_select_row_limit("SELECT * FROM users WHERE ROWNUM < 101"), Some(100));
assert_eq!(
explicit_select_row_limit("SELECT * FROM (SELECT * FROM users WHERE ROWNUM <= 500) WHERE ROWNUM <= 100"),
Some(100)
);
}
}

View File

@ -201,28 +201,6 @@ pub async fn do_execute(
};
wait_for_query(cancel_token, db::sqlserver::execute_query(&mut client, sql)).await.map(truncate_result)
}
PoolKind::Oracle(pool) => {
let client = pool.client();
let schema = schema.map(|s| s.to_string());
drop(connections);
log::info!("[query][oracle:lock:start] schema={:?} sql={}", schema, sql);
let client = match cancel_token.as_ref() {
Some(token) => tokio::select! {
biased;
_ = token.cancelled() => return Err(canceled_error()),
guard = client.lock() => guard,
},
None => client.lock().await,
};
log::info!("[query][oracle:lock:done] schema={:?}", schema);
if let Some(schema) = schema {
wait_for_query(cancel_token, db::oracle_driver::execute_query_with_schema(&*client, &schema, sql))
.await
.map(truncate_result)
} else {
wait_for_query(cancel_token, db::oracle_driver::execute_query(&*client, sql)).await.map(truncate_result)
}
}
PoolKind::Elasticsearch(client) => {
let client = client.clone();
let sql = sql.to_string();
@ -531,7 +509,6 @@ pub async fn execute_statements_in_transaction(
PoolKind::DuckDb(_)
| PoolKind::Redis(_)
| PoolKind::MongoDb(_)
| PoolKind::Oracle(_)
| PoolKind::Elasticsearch(_)
| PoolKind::ExternalTabular(_)
| PoolKind::ExternalDriver { .. } => TxPath::None,

View File

@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::connection::{AppState, MysqlMode, OraclePool, PoolKind};
use crate::connection::{AppState, MysqlMode, PoolKind};
use crate::db;
pub fn duckdb_query_tables(con: &duckdb::Connection) -> Result<Vec<db::TableInfo>, String> {
@ -102,13 +102,6 @@ pub fn extract_clickhouse(
}
}
pub fn extract_oracle(connections: &HashMap<String, PoolKind>, key: &str) -> Option<Arc<OraclePool>> {
match connections.get(key)? {
PoolKind::Oracle(pool) => Some(pool.clone()),
_ => None,
}
}
pub fn extract_agent(
connections: &HashMap<String, PoolKind>,
key: &str,
@ -152,12 +145,6 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_databases(&*client).await;
}
if let Some(client) = extract_agent(&connections, connection_id) {
drop(connections);
let mut client = client.lock().await;
@ -206,12 +193,6 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_schemas(&*client).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -278,12 +259,6 @@ pub async fn list_tables_core(
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_tables(&*client, schema).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -346,12 +321,6 @@ pub async fn list_objects_core(
let mut client = client.lock().await;
return db::sqlserver::list_objects(&mut client, schema).await;
}
if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_objects(&*client, schema).await;
}
}
Ok(list_tables_core(state, connection_id, database, schema)
@ -418,12 +387,6 @@ pub async fn get_columns_core(
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::get_columns(&*client, schema, table).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -469,12 +432,6 @@ pub async fn list_indexes_core(
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_indexes(&*client, schema, table).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -520,12 +477,6 @@ pub async fn list_foreign_keys_core(
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_foreign_keys(&*client, schema, table).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -571,12 +522,6 @@ pub async fn list_triggers_core(
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 client = pool.client();
let client = client.lock().await;
return db::oracle_driver::list_triggers(&*client, schema, table).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -648,12 +593,6 @@ pub async fn get_table_ddl_core(
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 client = pool.client();
let client = client.lock().await;
return build_oracle_ddl(&*client, schema, table).await;
}
if let Some(client) = extract_agent(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -808,14 +747,6 @@ pub async fn get_object_source_core(
db::sqlserver::execute_query(&mut client, &sqlserver_object_source_sql(schema, name, &object_type))
.await?,
)?
} else if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let client = pool.client();
let client = client.lock().await;
first_string_cell(
db::oracle_driver::execute_query(&*client, &oracle_object_source_sql(schema, name, &object_type))
.await?,
)?
} else if let Some(client) = extract_gaussdb(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
@ -1034,71 +965,6 @@ pub async fn build_sqlserver_ddl(
Ok(ddl)
}
pub async fn build_oracle_ddl(
client: &db::oracle_driver::OracleClient,
schema: &str,
table: &str,
) -> Result<String, String> {
let columns = db::oracle_driver::get_columns(client, schema, table).await?;
let indexes = db::oracle_driver::list_indexes(client, schema, table).await?;
let fkeys = db::oracle_driver::list_foreign_keys(client, schema, table).await?;
let table_comment = db::oracle_driver::get_table_comment(client, 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");
if let Some(comment) = table_comment.as_deref().filter(|comment| !comment.trim().is_empty()) {
ddl.push_str(&format!("\nCOMMENT ON TABLE \"{schema}\".\"{table}\" IS '{}';", comment.replace('\'', "''")));
}
for col in &columns {
if let Some(comment) = col.comment.as_deref().filter(|comment| !comment.trim().is_empty()) {
ddl.push_str(&format!(
"\nCOMMENT ON COLUMN \"{schema}\".\"{table}\".\"{}\" IS '{}';",
col.name,
comment.replace('\'', "''")
));
}
}
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)
}
pub async fn build_gaussdb_ddl(
client: &mut db::gaussdb_driver::GaussdbClient,
schema: &str,

View File

@ -501,12 +501,6 @@ pub async fn execute_on_pool(state: &AppState, pool_key: &str, sql: &str) -> Res
let mut client = client.lock().await;
db::sqlserver::execute_query(&mut client, sql).await
}
PoolKind::Oracle(pool) => {
let client = pool.client();
drop(connections);
let client = client.lock().await;
db::oracle_driver::execute_query(&*client, sql).await
}
PoolKind::DuckDb(con) => {
let con = con.clone();
let sql = sql.to_string();
@ -641,15 +635,6 @@ pub async fn get_columns_for_transfer(
let mut client = client.lock().await;
return db::sqlserver::get_columns(&mut client, &schema, &table).await;
}
if let Some(PoolKind::Oracle(pool)) = connections.get(pool_key) {
let client = pool.client();
let schema = schema.to_string();
let table = table.to_string();
drop(connections);
let client = client.lock().await;
return db::oracle_driver::get_columns(&*client, &schema, &table).await;
}
let pool = connections.get(pool_key).ok_or("Pool not found")?;
let schema = schema.to_string();
let table = table.to_string();

View File

@ -42,7 +42,6 @@ tokio-util = { version = "0.7", features = ["compat"] }
reqwest = { version = "0.12", features = ["json", "stream"] }
futures = "0.3"
mongodb = "3.2.5"
rust-oracle = { git = "https://github.com/t8y2/rust-oracle", branch = "main" }
percent-encoding = "2"
russh = "0.60"
csv = "1.4.0"

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use tauri::State;
use tauri::{Emitter, State};
use dbx_core::agent_manager::{AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver};
use dbx_core::connection::AppState;
@ -43,23 +43,46 @@ pub async fn list_installed_agents(state: State<'_, Arc<AppState>>) -> Result<Ve
}
#[tauri::command]
pub async fn install_agent(state: State<'_, Arc<AppState>>, db_type: String) -> Result<(), String> {
pub async fn install_agent(
app: tauri::AppHandle,
state: State<'_, Arc<AppState>>,
db_type: String,
) -> Result<(), String> {
let am = &state.agent_manager;
let registry = fetch_registry().await?;
let needs_jre = !am.is_jre_installed();
if !am.is_jre_installed() {
if needs_jre {
let platform = AgentManager::current_platform();
let jre_info =
registry.jre.platforms.get(platform).ok_or_else(|| format!("No JRE available for platform: {platform}"))?;
let jre_archive = am.base_dir().join("jre-download.tar.gz");
download_with_proxy(&jre_info.url, &jre_archive).await?;
let _ = app.emit(
"agent-install-progress",
serde_json::json!({
"step": "jre", "downloaded": 0u64, "total": jre_info.size,
}),
);
download_with_progress(&app, "jre", &jre_info.url, &jre_archive, jre_info.size).await?;
let _ = app.emit(
"agent-install-progress",
serde_json::json!({
"step": "jre-extract", "downloaded": 0u64, "total": 0u64,
}),
);
extract_archive(&jre_archive, &am.base_dir().join("jre"))?;
std::fs::remove_file(&jre_archive).ok();
}
let driver = registry.drivers.get(&db_type).ok_or_else(|| format!("Unknown driver type: {db_type}"))?;
let jar_path = am.driver_jar_path(&db_type);
download_with_proxy(&driver.jar.url, &jar_path).await?;
let _ = app.emit(
"agent-install-progress",
serde_json::json!({
"step": "driver", "downloaded": 0u64, "total": driver.jar.size,
}),
);
download_with_progress(&app, "driver", &driver.jar.url, &jar_path, driver.jar.size).await?;
let mut local_state = am.load_state();
local_state.jre_version = Some(registry.jre.version.clone());
@ -68,6 +91,7 @@ pub async fn install_agent(state: State<'_, Arc<AppState>>, db_type: String) ->
InstalledDriver { version: driver.version.clone(), installed_at: chrono::Utc::now().to_rfc3339() },
);
am.save_state(&local_state)?;
let _ = app.emit("agent-install-progress", serde_json::json!({ "step": "done" }));
Ok(())
}
@ -94,7 +118,7 @@ pub async fn check_jre_installed(state: State<'_, Arc<AppState>>) -> Result<bool
}
#[tauri::command]
pub async fn reinstall_jre(state: State<'_, Arc<AppState>>) -> Result<(), String> {
pub async fn reinstall_jre(app: tauri::AppHandle, state: State<'_, Arc<AppState>>) -> Result<(), String> {
let am = &state.agent_manager;
let jre_dir = am.base_dir().join("jre");
if jre_dir.exists() {
@ -105,12 +129,13 @@ pub async fn reinstall_jre(state: State<'_, Arc<AppState>>) -> Result<(), String
let jre_info =
registry.jre.platforms.get(platform).ok_or_else(|| format!("No JRE available for platform: {platform}"))?;
let jre_archive = am.base_dir().join("jre-download.tar.gz");
download_with_proxy(&jre_info.url, &jre_archive).await?;
download_with_progress(&app, "jre", &jre_info.url, &jre_archive, jre_info.size).await?;
extract_archive(&jre_archive, &jre_dir)?;
std::fs::remove_file(&jre_archive).ok();
let mut local_state = am.load_state();
local_state.jre_version = Some(registry.jre.version.clone());
am.save_state(&local_state)?;
let _ = app.emit("agent-install-progress", serde_json::json!({ "step": "done" }));
Ok(())
}
@ -140,7 +165,13 @@ async fn fetch_registry() -> Result<AgentRegistry, String> {
Err(format!("Failed to fetch agent registry: {last_err}"))
}
async fn download_with_proxy(url: &str, dest: &std::path::Path) -> Result<(), String> {
async fn download_with_progress(
app: &tauri::AppHandle,
step: &str,
url: &str,
dest: &std::path::Path,
total_size: u64,
) -> Result<(), String> {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
@ -160,8 +191,18 @@ async fn download_with_proxy(url: &str, dest: &std::path::Path) -> Result<(), St
.and_then(|r| r.error_for_status())
{
Ok(resp) => {
let bytes = resp.bytes().await.map_err(|e| format!("Download read failed: {e}"))?;
std::fs::write(dest, &bytes).map_err(|e| format!("Failed to write file: {e}"))?;
let content_length = resp.content_length().unwrap_or(total_size);
let mut file = std::fs::File::create(dest).map_err(|e| format!("Failed to create file: {e}"))?;
let mut downloaded: u64 = 0;
let mut bytes = resp;
while let Some(chunk) = bytes.chunk().await.map_err(|e| format!("Download stream error: {e}"))? {
std::io::Write::write_all(&mut file, &chunk).map_err(|e| format!("Failed to write chunk: {e}"))?;
downloaded += chunk.len() as u64;
let _ = app.emit(
"agent-install-progress",
serde_json::json!({ "step": step, "downloaded": downloaded, "total": content_length }),
);
}
return Ok(());
}
Err(e) => {

View File

@ -3,7 +3,7 @@ use tauri::State;
pub use dbx_core::connection::{
connection_url_for_endpoint, expand_tilde, metadata_connection_config, probe_connection_endpoint,
redacted_connection_url_for_endpoint, AppState, MysqlMode, OraclePool, PoolKind,
redacted_connection_url_for_endpoint, AppState, MysqlMode, PoolKind,
};
use dbx_core::db;
use dbx_core::models::connection::{rewrite_jdbc_url_host, ConnectionConfig, DatabaseType};
@ -97,22 +97,16 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
.await
.map(|_| "Connection successful".to_string())
}
DatabaseType::Oracle => db::oracle_driver::connect(
&host,
port,
config.database.as_deref().unwrap_or("ORCL"),
&config.username,
&config.password,
config.sysdba,
)
.await
.map(|_| "Connection successful".to_string()),
DatabaseType::Elasticsearch => {
let client =
db::elasticsearch_driver::EsClient::new(&url, Some(&config.username), Some(&config.password));
db::elasticsearch_driver::test_connection(&client).await.map(|_| "Connection successful".to_string())
}
DatabaseType::Dameng | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb => {
DatabaseType::Dameng
| DatabaseType::Kingbase
| DatabaseType::Vastbase
| DatabaseType::Goldendb
| DatabaseType::Oracle => {
state
.agent_manager
.call_daemon::<serde_json::Value>(
@ -210,25 +204,17 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
.await?;
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
}
DatabaseType::Oracle => {
let client = db::oracle_driver::connect(
&host,
port,
db_config.database.as_deref().unwrap_or("ORCL"),
&db_config.username,
&db_config.password,
db_config.sysdba,
)
.await?;
PoolKind::Oracle(std::sync::Arc::new(OraclePool::new(vec![client])))
}
DatabaseType::Elasticsearch => {
let client =
db::elasticsearch_driver::EsClient::new(&url, Some(&db_config.username), Some(&db_config.password));
db::elasticsearch_driver::test_connection(&client).await?;
PoolKind::Elasticsearch(client)
}
DatabaseType::Dameng | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb => {
DatabaseType::Dameng
| DatabaseType::Kingbase
| DatabaseType::Vastbase
| DatabaseType::Goldendb
| DatabaseType::Oracle => {
let mut client = state.agent_manager.spawn(&db_config.db_type).await?;
client
.call::<serde_json::Value>(
@ -280,7 +266,6 @@ pub async fn disconnect_db(state: State<'_, Arc<AppState>>, connection_id: Strin
PoolKind::MongoDb(_) => {}
PoolKind::ClickHouse(_) => {}
PoolKind::SqlServer(_) => {}
PoolKind::Oracle(_) => {}
PoolKind::Elasticsearch(_) => {}
PoolKind::Agent(_) => {}
PoolKind::Gaussdb(_) => {}

View File

@ -1,8 +1,13 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, onMounted, onUnmounted, computed } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useToast } from "@/composables/useToast";
const { toast } = useToast();
interface AgentDriverInfo {
db_type: string;
@ -14,13 +19,51 @@ interface AgentDriverInfo {
update_available: boolean;
}
interface InstallProgress {
step: string;
downloaded?: number;
total?: number;
}
const drivers = ref<AgentDriverInfo[]>([]);
const jreInstalled = ref(false);
const installing = ref<string | null>(null);
const reinstallingJre = ref(false);
const progress = ref<InstallProgress | null>(null);
let unlisten: UnlistenFn | null = null;
onMounted(async () => {
await refresh();
unlisten = await listen<InstallProgress>("agent-install-progress", (event) => {
if (event.payload.step === "done") {
progress.value = null;
} else {
progress.value = event.payload;
}
});
});
onUnmounted(() => {
unlisten?.();
});
const progressText = computed(() => {
const p = progress.value;
if (!p) return "";
if (p.step === "jre-extract") return "解压 JRE...";
const label = p.step === "jre" ? "下载 JRE" : "下载驱动";
if (!p.total) return `${label}...`;
const pct = Math.round(((p.downloaded ?? 0) / p.total) * 100);
const dl = formatSize(p.downloaded ?? 0);
const total = formatSize(p.total);
return `${label} ${dl} / ${total} (${pct}%)`;
});
const progressPercent = computed(() => {
const p = progress.value;
if (!p || !p.total) return 0;
return Math.round(((p.downloaded ?? 0) / p.total) * 100);
});
async function refresh() {
@ -29,40 +72,50 @@ async function refresh() {
}
async function installDriver(dbType: string) {
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
installing.value = dbType;
progress.value = null;
try {
await invoke("install_agent", { dbType });
await refresh();
toast(`${label} 驱动安装成功`);
} catch (e: any) {
alert(e);
toast(`${label} 驱动安装失败: ${e}`);
} finally {
installing.value = null;
progress.value = null;
}
}
async function uninstallDriver(dbType: string) {
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
try {
await invoke("uninstall_agent", { dbType });
await refresh();
toast(`${label} 驱动已卸载`);
} catch (e: any) {
alert(e);
toast(`${label} 驱动卸载失败: ${e}`);
}
}
async function reinstallJre() {
reinstallingJre.value = true;
progress.value = null;
try {
await invoke("reinstall_jre");
await refresh();
toast("JRE 重新安装成功");
} catch (e: any) {
alert(e);
toast(`JRE 重新安装失败: ${e}`);
} finally {
reinstallingJre.value = false;
progress.value = null;
}
}
function formatSize(bytes: number): string {
if (!bytes) return "";
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
</script>
@ -96,6 +149,17 @@ function formatSize(bytes: number): string {
</div>
</div>
<!-- Progress bar -->
<div v-if="progress" class="space-y-1.5">
<div class="text-xs text-muted-foreground">{{ progressText }}</div>
<div class="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary transition-all duration-200"
:style="{ width: `${progressPercent}%` }"
/>
</div>
</div>
<div class="rounded-md border">
<div v-if="drivers.length === 0" class="p-4 text-sm text-muted-foreground">加载中...</div>
<div v-else class="divide-y">
@ -106,6 +170,7 @@ function formatSize(bytes: number): string {
<span v-if="driver.installed">v{{ driver.installed_version }}</span>
<span v-if="driver.installed && formatSize(driver.size)"> · </span>
<span v-if="formatSize(driver.size)">{{ formatSize(driver.size) }}</span>
<span v-if="driver.update_available" class="ml-1.5 text-amber-500">有更新</span>
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
@ -113,7 +178,21 @@ function formatSize(bytes: number): string {
<Button v-if="driver.installed" variant="ghost" size="sm" @click="uninstallDriver(driver.db_type)">
卸载
</Button>
<Button v-else size="sm" :disabled="installing !== null" @click="installDriver(driver.db_type)">
<Button
v-if="driver.installed && driver.update_available"
size="sm"
variant="outline"
:disabled="installing !== null"
@click="installDriver(driver.db_type)"
>
{{ installing === driver.db_type ? "更新中..." : "更新" }}
</Button>
<Button
v-if="!driver.installed"
size="sm"
:disabled="installing !== null"
@click="installDriver(driver.db_type)"
>
{{ installing === driver.db_type ? "安装中..." : "安装" }}
</Button>
</div>

View File

@ -96,7 +96,7 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>(["postgres", "redshift",
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb"]);
export const AGENT_DRIVER_TYPES = new Set<DatabaseType>(["dameng", "kingbase", "vastbase", "goldendb"]);
export const AGENT_DRIVER_TYPES = new Set<DatabaseType>(["dameng", "kingbase", "vastbase", "goldendb", "oracle"]);
export const TRANSFER_SQL_TYPES = new Set<DatabaseType>([
"mysql",