mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-28 16:09:17 +00:00
feat: add 12 new agent-driven databases, migrate GaussDB to agent
Add H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Apache Kylin, SunDB as new agent-driven database types. Migrate GaussDB from native rust-gaussdb to JDBC agent. Remove rust-gaussdb dependency. Total agent-driven databases: 17.
This commit is contained in:
@@ -30,6 +30,5 @@ russh = "0.60"
|
||||
portpicker = "0.1.1"
|
||||
csv = "1"
|
||||
calamine = "0.30.1"
|
||||
rust-gaussdb = { git = "https://github.com/t8y2/rust-gaussdb", branch = "main" }
|
||||
base64 = "0.22"
|
||||
async-trait = "0.1"
|
||||
|
||||
@@ -122,6 +122,18 @@ impl AgentManager {
|
||||
DatabaseType::Vastbase => Some("vastbase"),
|
||||
DatabaseType::Goldendb => Some("goldendb"),
|
||||
DatabaseType::Oracle => Some("oracle"),
|
||||
DatabaseType::H2 => Some("h2"),
|
||||
DatabaseType::Snowflake => Some("snowflake"),
|
||||
DatabaseType::Trino => Some("trino"),
|
||||
DatabaseType::Hive => Some("hive"),
|
||||
DatabaseType::Db2 => Some("db2"),
|
||||
DatabaseType::Informix => Some("informix"),
|
||||
DatabaseType::Neo4j => Some("neo4j"),
|
||||
DatabaseType::Cassandra => Some("cassandra"),
|
||||
DatabaseType::Bigquery => Some("bigquery"),
|
||||
DatabaseType::Kylin => Some("kylin"),
|
||||
DatabaseType::Sundb => Some("sundb"),
|
||||
DatabaseType::Gaussdb => Some("gaussdb"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ pub enum PoolKind {
|
||||
SqlServer(Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
|
||||
Elasticsearch(db::elasticsearch_driver::EsClient),
|
||||
Agent(Arc<tokio::sync::Mutex<db::agent_driver::AgentDriverClient>>),
|
||||
Gaussdb(Arc<tokio::sync::Mutex<db::gaussdb_driver::GaussdbClient>>),
|
||||
ExternalTabular(Arc<external::ExternalPool>),
|
||||
ExternalDriver { driver_id: String, config: ConnectionConfig, session: Arc<PluginDriverSession> },
|
||||
}
|
||||
@@ -214,7 +213,19 @@ impl AppState {
|
||||
| DatabaseType::Kingbase
|
||||
| DatabaseType::Vastbase
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Oracle => {
|
||||
| DatabaseType::Oracle
|
||||
| DatabaseType::H2
|
||||
| DatabaseType::Snowflake
|
||||
| DatabaseType::Trino
|
||||
| DatabaseType::Hive
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Informix
|
||||
| DatabaseType::Neo4j
|
||||
| DatabaseType::Cassandra
|
||||
| DatabaseType::Bigquery
|
||||
| DatabaseType::Kylin
|
||||
| DatabaseType::Sundb
|
||||
| DatabaseType::Gaussdb => {
|
||||
let mut client = self.agent_manager.spawn(&db_config.db_type).await?;
|
||||
client
|
||||
.call::<serde_json::Value>(
|
||||
@@ -230,17 +241,6 @@ impl AppState {
|
||||
.await?;
|
||||
PoolKind::Agent(Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
DatabaseType::Gaussdb => {
|
||||
let client = db::gaussdb_driver::connect(
|
||||
&host,
|
||||
port,
|
||||
db_config.effective_database().unwrap_or("postgres"),
|
||||
&db_config.username,
|
||||
&db_config.password,
|
||||
)
|
||||
.await?;
|
||||
PoolKind::Gaussdb(Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
DatabaseType::Jdbc => {
|
||||
let mut jdbc_config = db_config.clone();
|
||||
if host != config.host || port != config.port {
|
||||
@@ -406,7 +406,19 @@ pub async fn probe_connection_endpoint(config: &ConnectionConfig, host: &str, po
|
||||
| DatabaseType::Kingbase
|
||||
| DatabaseType::Vastbase
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Oracle => Ok(()),
|
||||
| DatabaseType::Oracle
|
||||
| DatabaseType::H2
|
||||
| DatabaseType::Snowflake
|
||||
| DatabaseType::Trino
|
||||
| DatabaseType::Hive
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Informix
|
||||
| DatabaseType::Neo4j
|
||||
| DatabaseType::Cassandra
|
||||
| DatabaseType::Bigquery
|
||||
| DatabaseType::Kylin
|
||||
| DatabaseType::Sundb
|
||||
| DatabaseType::Gaussdb => Ok(()),
|
||||
_ => db::probe_tcp_endpoint(&format!("{:?}", config.db_type), host, port).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::sql::starts_with_executable_sql_keyword;
|
||||
use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
|
||||
|
||||
use super::CONNECTION_TIMEOUT_SECS;
|
||||
|
||||
pub struct GaussdbClient {
|
||||
client: rust_gaussdb::Client,
|
||||
}
|
||||
|
||||
unsafe impl Send for GaussdbClient {}
|
||||
|
||||
impl GaussdbClient {
|
||||
async fn query_rows(&mut self, sql: &str) -> Result<Vec<Vec<Option<String>>>, String> {
|
||||
let rows = self.client.query(sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let mut result = Vec::new();
|
||||
for row in &rows {
|
||||
let mut vals = Vec::new();
|
||||
for i in 0..row.columns().len() {
|
||||
vals.push(row.try_get::<String>(i));
|
||||
}
|
||||
result.push(vals);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn query_single_column(&mut self, sql: &str) -> Result<Vec<String>, String> {
|
||||
Ok(self.query_rows(sql).await?.into_iter().filter_map(|r| r.into_iter().next().flatten()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(host: &str, port: u16, database: &str, user: &str, pass: &str) -> Result<GaussdbClient, String> {
|
||||
let database = normalize_database(database);
|
||||
let dsn = format!("host={host} port={port} user={user} password={pass} dbname={database}");
|
||||
|
||||
let result = tokio::time::timeout(std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS), async {
|
||||
rust_gaussdb::Client::connect(&dsn).await.map_err(|e| format!("GaussDB connection failed: {e}"))
|
||||
})
|
||||
.await
|
||||
.map_err(|_| format!("GaussDB connection timed out ({CONNECTION_TIMEOUT_SECS}s)"))?;
|
||||
|
||||
result.map(|client| GaussdbClient { client })
|
||||
}
|
||||
|
||||
fn normalize_database(database: &str) -> &str {
|
||||
let database = database.trim();
|
||||
if database.is_empty() {
|
||||
"postgres"
|
||||
} else {
|
||||
database
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_databases(client: &mut GaussdbClient) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let rows = client
|
||||
.query_single_column(
|
||||
"SELECT datname FROM pg_database \
|
||||
WHERE datistemplate = false AND datallowconn = true \
|
||||
ORDER BY datname",
|
||||
)
|
||||
.await?;
|
||||
Ok(rows.into_iter().map(|name| DatabaseInfo { name }).collect())
|
||||
}
|
||||
|
||||
pub async fn list_schemas(client: &mut GaussdbClient) -> Result<Vec<String>, String> {
|
||||
client
|
||||
.query_single_column(
|
||||
"SELECT DISTINCT nspname FROM pg_catalog.pg_namespace n \
|
||||
WHERE nspname NOT LIKE 'pg_%' \
|
||||
AND nspname NOT IN ('information_schema', 'cstore', 'snapshot', 'db4ai', 'dbe_perf', \
|
||||
'dbe_pldebugger', 'dbe_pldeveloper', 'pkg_service', 'pkg_util', 'sqladvisor', 'blockchain') \
|
||||
ORDER BY nspname",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_tables(client: &mut GaussdbClient, schema: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let sql = format!(
|
||||
"SELECT c.relname, CASE c.relkind WHEN 'r' THEN 'TABLE' WHEN 'v' THEN 'VIEW' WHEN 'm' THEN 'VIEW' ELSE 'TABLE' END \
|
||||
FROM pg_catalog.pg_class c \
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \
|
||||
WHERE n.nspname = '{s}' AND c.relkind IN ('r', 'v', 'm') \
|
||||
ORDER BY c.relname"
|
||||
);
|
||||
let rows = client.query_rows(&sql).await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let raw_type = r.get(1).and_then(|v| v.clone()).unwrap_or_default();
|
||||
TableInfo {
|
||||
name: r.first().and_then(|v| v.clone()).unwrap_or_default(),
|
||||
table_type: if raw_type.contains("VIEW") { "VIEW".to_string() } else { "TABLE".to_string() },
|
||||
comment: None,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_columns(client: &mut GaussdbClient, schema: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
|
||||
let pk_rows = client
|
||||
.query_single_column(&format!(
|
||||
"SELECT a.attname FROM pg_catalog.pg_index i \
|
||||
JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) \
|
||||
WHERE i.indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '{t}' \
|
||||
AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{s}')) \
|
||||
AND i.indisprimary"
|
||||
))
|
||||
.await?;
|
||||
let pk_names: std::collections::HashSet<String> = pk_rows.into_iter().collect();
|
||||
|
||||
let col_rows = client
|
||||
.query_rows(&format!(
|
||||
"SELECT a.attname, format_type(a.atttypid, a.atttypmod), \
|
||||
CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END, \
|
||||
pg_catalog.pg_get_expr(d.adbin, d.adrelid), \
|
||||
CASE WHEN t.typname IN ('numeric', 'float4', 'float8') THEN COALESCE(((a.atttypmod - 4) >> 16) & 65535, -1) ELSE NULL END, \
|
||||
CASE WHEN t.typname = 'numeric' THEN COALESCE((a.atttypmod - 4) & 65535, -1) ELSE NULL END, \
|
||||
CASE WHEN a.atttypmod > 0 AND t.typname IN ('varchar', 'bpchar') THEN a.atttypmod - 4 ELSE NULL END \
|
||||
FROM pg_catalog.pg_attribute a \
|
||||
JOIN pg_catalog.pg_type t ON t.oid = a.atttypid \
|
||||
LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum \
|
||||
WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '{t}' \
|
||||
AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{s}')) \
|
||||
AND a.attnum > 0 AND NOT a.attisdropped \
|
||||
ORDER BY a.attnum"
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(col_rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let name = r.first().and_then(|v| v.clone()).unwrap_or_default();
|
||||
let data_type = r.get(1).and_then(|v| v.clone()).unwrap_or_default();
|
||||
let num_prec = r.get(4).and_then(|v| v.as_ref()?.parse::<i32>().ok());
|
||||
let num_scale = r.get(5).and_then(|v| v.as_ref()?.parse::<i32>().ok());
|
||||
let char_len = r.get(6).and_then(|v| v.as_ref()?.parse::<i32>().ok());
|
||||
ColumnInfo {
|
||||
is_primary_key: pk_names.contains(&name),
|
||||
name,
|
||||
data_type,
|
||||
is_nullable: r.get(2).and_then(|v| v.as_ref()).map(|v| v == "YES").unwrap_or(false),
|
||||
column_default: r.get(3).and_then(|v| v.clone()).filter(|v| !v.is_empty()),
|
||||
extra: None,
|
||||
comment: None,
|
||||
numeric_precision: num_prec,
|
||||
numeric_scale: num_scale,
|
||||
character_maximum_length: char_len,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_indexes(client: &mut GaussdbClient, schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
let sql = format!(
|
||||
"SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = '{s}' AND tablename = '{t}' ORDER BY indexname"
|
||||
);
|
||||
let rows = client.query_rows(&sql).await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let name = r.first().and_then(|v| v.clone()).unwrap_or_default();
|
||||
let def = r.get(1).and_then(|v| v.clone()).unwrap_or_default();
|
||||
let is_unique = def.to_uppercase().contains("UNIQUE");
|
||||
let is_primary = def.to_uppercase().contains("PRIMARY");
|
||||
let columns = def
|
||||
.rsplit_once('(')
|
||||
.and_then(|(_, rest)| rest.strip_suffix(')'))
|
||||
.map(|cols| cols.split(',').map(|c| c.trim().trim_matches('"').to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
IndexInfo {
|
||||
name,
|
||||
columns,
|
||||
is_unique,
|
||||
is_primary,
|
||||
filter: None,
|
||||
index_type: None,
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_foreign_keys(
|
||||
client: &mut GaussdbClient,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<ForeignKeyInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
let sql = format!(
|
||||
"SELECT con.conname, a.attname, cl2.relname, a2.attname \
|
||||
FROM pg_catalog.pg_constraint con \
|
||||
JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid \
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace \
|
||||
JOIN pg_catalog.pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) \
|
||||
JOIN pg_catalog.pg_class cl2 ON cl2.oid = con.confrelid \
|
||||
JOIN pg_catalog.pg_attribute a2 ON a2.attrelid = con.confrelid AND a2.attnum = ANY(con.confkey) \
|
||||
WHERE con.contype = 'f' AND n.nspname = '{s}' AND cl.relname = '{t}' \
|
||||
ORDER BY con.conname"
|
||||
);
|
||||
let rows = client.query_rows(&sql).await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| ForeignKeyInfo {
|
||||
name: r.first().and_then(|v| v.clone()).unwrap_or_default(),
|
||||
column: r.get(1).and_then(|v| v.clone()).unwrap_or_default(),
|
||||
ref_table: r.get(2).and_then(|v| v.clone()).unwrap_or_default(),
|
||||
ref_column: r.get(3).and_then(|v| v.clone()).unwrap_or_default(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn list_triggers(client: &mut GaussdbClient, schema: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
let sql = format!(
|
||||
"SELECT t.tgname, \
|
||||
CASE WHEN t.tgtype & 4 = 4 THEN 'INSERT' WHEN t.tgtype & 8 = 8 THEN 'DELETE' \
|
||||
WHEN t.tgtype & 16 = 16 THEN 'UPDATE' ELSE 'UNKNOWN' END, \
|
||||
CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END \
|
||||
FROM pg_catalog.pg_trigger t \
|
||||
JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid \
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \
|
||||
WHERE NOT t.tgisinternal AND n.nspname = '{s}' AND c.relname = '{t}' \
|
||||
ORDER BY t.tgname"
|
||||
);
|
||||
let rows = client.query_rows(&sql).await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TriggerInfo {
|
||||
name: r.first().and_then(|v| v.clone()).unwrap_or_default(),
|
||||
event: r.get(1).and_then(|v| v.clone()).unwrap_or_default(),
|
||||
timing: r.get(2).and_then(|v| v.clone()).unwrap_or_default(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn execute_query(client: &mut GaussdbClient, sql: &str) -> Result<QueryResult, String> {
|
||||
let start = Instant::now();
|
||||
let sql = sql.trim().trim_end_matches(';');
|
||||
|
||||
if starts_with_executable_sql_keyword(sql, &["SELECT", "WITH", "SHOW", "EXPLAIN"]) {
|
||||
let rows = client.client.query(sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let columns: Vec<String> = if let Some(first) = rows.first() {
|
||||
first.columns().iter().map(|c| c.name.clone()).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut result_rows = Vec::new();
|
||||
for row in &rows {
|
||||
let vals: Vec<serde_json::Value> = (0..row.columns().len())
|
||||
.map(|i| row.try_get::<String>(i).map(serde_json::Value::String).unwrap_or(serde_json::Value::Null))
|
||||
.collect();
|
||||
result_rows.push(vals);
|
||||
if result_rows.len() >= crate::query::MAX_ROWS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let truncated = result_rows.len() >= crate::query::MAX_ROWS;
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated,
|
||||
})
|
||||
} else {
|
||||
let affected = client.client.execute(sql).await.map_err(|e| e.to_string())?;
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_database;
|
||||
|
||||
#[test]
|
||||
fn normalize_database_defaults_blank_to_postgres() {
|
||||
assert_eq!(normalize_database(""), "postgres");
|
||||
assert_eq!(normalize_database(" "), "postgres");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_database_keeps_explicit_database() {
|
||||
assert_eq!(normalize_database("app"), "app");
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ pub mod clickhouse_driver;
|
||||
pub mod duckdb_driver;
|
||||
pub mod elasticsearch_driver;
|
||||
pub mod file_validator;
|
||||
pub mod gaussdb_driver;
|
||||
pub mod mongo_driver;
|
||||
pub mod mysql;
|
||||
pub mod ob_oracle;
|
||||
|
||||
@@ -117,6 +117,21 @@ pub enum DatabaseType {
|
||||
Vastbase,
|
||||
Goldendb,
|
||||
Gaussdb,
|
||||
#[serde(rename = "h2")]
|
||||
H2,
|
||||
Snowflake,
|
||||
Trino,
|
||||
Hive,
|
||||
#[serde(rename = "db2")]
|
||||
Db2,
|
||||
Informix,
|
||||
#[serde(rename = "neo4j")]
|
||||
Neo4j,
|
||||
Cassandra,
|
||||
#[serde(rename = "bigquery")]
|
||||
Bigquery,
|
||||
Kylin,
|
||||
Sundb,
|
||||
Jdbc,
|
||||
}
|
||||
|
||||
@@ -216,6 +231,17 @@ impl ConnectionConfig {
|
||||
DatabaseType::Vastbase => format!("vastbase://{host}:{port}{db_part}"),
|
||||
DatabaseType::Goldendb => format!("goldendb://{host}:{port}{db_part}"),
|
||||
DatabaseType::Gaussdb => format!("gaussdb://{host}:{port}{db_part}"),
|
||||
DatabaseType::H2 => format!("h2://{host}:{port}{db_part}"),
|
||||
DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"),
|
||||
DatabaseType::Trino => format!("trino://{host}:{port}{db_part}"),
|
||||
DatabaseType::Hive => format!("hive://{host}:{port}{db_part}"),
|
||||
DatabaseType::Db2 => format!("db2://{host}:{port}{db_part}"),
|
||||
DatabaseType::Informix => format!("informix://{host}:{port}{db_part}"),
|
||||
DatabaseType::Neo4j => format!("neo4j://{host}:{port}{db_part}"),
|
||||
DatabaseType::Cassandra => format!("cassandra://{host}:{port}{db_part}"),
|
||||
DatabaseType::Bigquery => format!("bigquery://{host}/{db_part}"),
|
||||
DatabaseType::Kylin => format!("kylin://{host}:{port}{db_part}"),
|
||||
DatabaseType::Sundb => format!("sundb://{host}:{port}{db_part}"),
|
||||
DatabaseType::Jdbc => "jdbc:<redacted>".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -296,6 +322,39 @@ impl ConnectionConfig {
|
||||
DatabaseType::Gaussdb => {
|
||||
format!("gaussdb://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::H2 => {
|
||||
format!("h2://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Snowflake => {
|
||||
format!("snowflake://{}:{}@{host}/{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Trino => {
|
||||
format!("trino://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Hive => {
|
||||
format!("hive://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Db2 => {
|
||||
format!("db2://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Informix => {
|
||||
format!("informix://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Neo4j => {
|
||||
format!("neo4j://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Cassandra => {
|
||||
format!("cassandra://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Bigquery => {
|
||||
format!("bigquery://{}:{}@{host}/{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Kylin => {
|
||||
format!("kylin://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Sundb => {
|
||||
format!("sundb://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Jdbc => {
|
||||
self.connection_string.as_deref().filter(|value| !value.is_empty()).unwrap_or("jdbc:").to_string()
|
||||
}
|
||||
|
||||
@@ -227,17 +227,6 @@ pub async fn do_execute(
|
||||
.await
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::Gaussdb(client) => {
|
||||
let client = client.clone();
|
||||
let sql = sql.to_string();
|
||||
drop(connections);
|
||||
wait_for_query(cancel_token, async move {
|
||||
let mut client = client.lock().await;
|
||||
db::gaussdb_driver::execute_query(&mut client, &sql).await
|
||||
})
|
||||
.await
|
||||
.map(truncate_result)
|
||||
}
|
||||
PoolKind::ExternalTabular(ext_pool) => {
|
||||
if !starts_with_executable_sql_keyword(sql, &["SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN", "PRAGMA"]) {
|
||||
return Err("External data sources are read-only. Only SELECT queries are supported.".to_string());
|
||||
@@ -476,7 +465,7 @@ pub async fn execute_statements(
|
||||
/// Execute multiple SQL statements within a single transaction.
|
||||
/// For sqlx-based pools (Postgres/MySQL/SQLite), uses the Transaction API to
|
||||
/// guarantee all statements run on the same physical connection.
|
||||
/// For custom drivers (ClickHouse/SqlServer/Agent/Gaussdb), uses explicit
|
||||
/// For custom drivers (ClickHouse/SqlServer/Agent), uses explicit
|
||||
/// BEGIN/COMMIT/ROLLBACK on the already-single-connection client.
|
||||
/// For databases that don't support explicit transactions (Redis, MongoDB, Oracle),
|
||||
/// executes statements sequentially without transaction.
|
||||
@@ -503,9 +492,7 @@ pub async fn execute_statements_in_transaction(
|
||||
PoolKind::Postgres(pg) => TxPath::Pg(pg.clone()),
|
||||
PoolKind::Mysql(mp, _mode) => TxPath::Mysql(mp.clone(), false),
|
||||
PoolKind::Sqlite(sq) => TxPath::Sqlite(sq.clone()),
|
||||
PoolKind::ClickHouse(_) | PoolKind::SqlServer(_) | PoolKind::Agent(_) | PoolKind::Gaussdb(_) => {
|
||||
TxPath::Explicit
|
||||
}
|
||||
PoolKind::ClickHouse(_) | PoolKind::SqlServer(_) | PoolKind::Agent(_) => TxPath::Explicit,
|
||||
PoolKind::DuckDb(_)
|
||||
| PoolKind::Redis(_)
|
||||
| PoolKind::MongoDb(_)
|
||||
|
||||
@@ -112,16 +112,6 @@ pub fn extract_agent(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_gaussdb(
|
||||
connections: &HashMap<String, PoolKind>,
|
||||
key: &str,
|
||||
) -> Option<Arc<tokio::sync::Mutex<db::gaussdb_driver::GaussdbClient>>> {
|
||||
match connections.get(key)? {
|
||||
PoolKind::Gaussdb(client) => Some(client.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Result<Vec<db::DatabaseInfo>, String> {
|
||||
{
|
||||
let connections = state.connections.read().await;
|
||||
@@ -150,11 +140,6 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
|
||||
let mut client = client.lock().await;
|
||||
return client.call("list_databases", serde_json::json!({})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, connection_id) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::list_databases(&mut client).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -198,11 +183,6 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
|
||||
let mut client = client.lock().await;
|
||||
return client.call("list_schemas", serde_json::json!({"database": database})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::list_schemas(&mut client).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -264,11 +244,6 @@ pub async fn list_tables_core(
|
||||
let mut client = client.lock().await;
|
||||
return client.call("list_tables", serde_json::json!({"schema": schema})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::list_tables(&mut client, schema).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -392,11 +367,6 @@ pub async fn get_columns_core(
|
||||
let mut client = client.lock().await;
|
||||
return client.call("get_columns", serde_json::json!({"schema": schema, "table": table})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::get_columns(&mut client, schema, table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -437,11 +407,6 @@ pub async fn list_indexes_core(
|
||||
let mut client = client.lock().await;
|
||||
return client.call("list_indexes", serde_json::json!({"schema": schema, "table": table})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::list_indexes(&mut client, schema, table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -482,11 +447,6 @@ pub async fn list_foreign_keys_core(
|
||||
let mut client = client.lock().await;
|
||||
return client.call("list_foreign_keys", serde_json::json!({"schema": schema, "table": table})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::list_foreign_keys(&mut client, schema, table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -527,11 +487,6 @@ pub async fn list_triggers_core(
|
||||
let mut client = client.lock().await;
|
||||
return client.call("list_triggers", serde_json::json!({"schema": schema, "table": table})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return db::gaussdb_driver::list_triggers(&mut client, schema, table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -598,11 +553,6 @@ pub async fn get_table_ddl_core(
|
||||
let mut client = client.lock().await;
|
||||
return client.call("get_table_ddl", serde_json::json!({"schema": schema, "table": table})).await;
|
||||
}
|
||||
if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return build_gaussdb_ddl(&mut client, schema, table).await;
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.read().await;
|
||||
@@ -747,13 +697,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(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
first_string_cell(
|
||||
db::gaussdb_driver::execute_query(&mut client, &postgres_object_source_sql(schema, name, &object_type))
|
||||
.await?,
|
||||
)?
|
||||
} else {
|
||||
match connections.get(&pool_key).ok_or("Pool not found")? {
|
||||
PoolKind::Mysql(pool, _) => mysql_object_source(pool, name, &object_type).await?,
|
||||
@@ -964,54 +907,3 @@ pub async fn build_sqlserver_ddl(
|
||||
}
|
||||
Ok(ddl)
|
||||
}
|
||||
|
||||
pub async fn build_gaussdb_ddl(
|
||||
client: &mut db::gaussdb_driver::GaussdbClient,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<String, String> {
|
||||
let columns = db::gaussdb_driver::get_columns(client, schema, table).await?;
|
||||
let indexes = db::gaussdb_driver::list_indexes(client, schema, table).await?;
|
||||
let fkeys = db::gaussdb_driver::list_foreign_keys(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");
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user