feat: add DM (Dameng) database support via ODBC
Connect to DM8 databases using the odbc-api crate with unixODBC. Includes schema browsing, query execution, DDL generation, and full frontend integration with download link to DM ODBC driver.
This commit is contained in:
parent
02813f4b9e
commit
4bf4f3b77f
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
- name: Install system dependencies (Linux)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev unixodbc-dev
|
||||
|
||||
- name: Cargo fmt check
|
||||
run: cargo fmt --check
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ jobs:
|
|||
shared-key: release-${{ matrix.target }}-${{ steps.deps-hash.outputs.hash }}
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install system dependencies (macOS)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
run: brew install unixodbc
|
||||
|
||||
- name: Install Apple certificate (macOS)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
env:
|
||||
|
|
@ -79,7 +83,7 @@ jobs:
|
|||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev unixodbc-dev
|
||||
|
||||
- name: Setup Tauri signing key
|
||||
shell: bash
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,6 +18,7 @@ ARG TARGETARCH
|
|||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential cmake pkg-config perl python3-pip gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu \
|
||||
unixodbc-dev libodbc2:amd64 libodbc2:arm64 \
|
||||
&& pip3 install --break-system-packages ziglang \
|
||||
&& cargo install cargo-zigbuild \
|
||||
&& rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu \
|
||||
|
|
@ -59,7 +60,7 @@ RUN case "$TARGETARCH" in \
|
|||
# Stage 3: Final image
|
||||
FROM debian:bookworm-slim
|
||||
ARG TARGETPLATFORM
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 unixodbc && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=backend /out/${TARGETPLATFORM}/dbx-web /usr/local/bin/
|
||||
COPY --from=frontend /app/dist /app/static
|
||||
ENV DBX_STATIC_DIR=/app/static
|
||||
|
|
|
|||
|
|
@ -27,3 +27,4 @@ russh = "0.60"
|
|||
portpicker = "0.1.1"
|
||||
csv = "1"
|
||||
calamine = "0.30.1"
|
||||
odbc-api = "25"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub enum PoolKind {
|
|||
SqlServer(Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
|
||||
Oracle(Arc<tokio::sync::Mutex<db::oracle_driver::OracleClient>>),
|
||||
Elasticsearch(db::elasticsearch_driver::EsClient),
|
||||
Dameng(Arc<std::sync::Mutex<db::dm_driver::DmClient>>),
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
|
|
@ -60,7 +61,7 @@ impl AppState {
|
|||
return Ok(connection_id.to_string());
|
||||
}
|
||||
|
||||
let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle));
|
||||
let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle) | Some(DatabaseType::Dameng));
|
||||
let pool_key = if is_single_conn {
|
||||
connection_id.to_string()
|
||||
} else {
|
||||
|
|
@ -95,7 +96,7 @@ impl AppState {
|
|||
|
||||
let mut db_config = config.clone();
|
||||
if let Some(db) = database {
|
||||
if db_config.db_type != DatabaseType::Oracle {
|
||||
if db_config.db_type != DatabaseType::Oracle && db_config.db_type != DatabaseType::Dameng {
|
||||
db_config.database = Some(db.to_string());
|
||||
}
|
||||
}
|
||||
|
|
@ -162,6 +163,17 @@ impl AppState {
|
|||
db::elasticsearch_driver::test_connection(&client).await?;
|
||||
PoolKind::Elasticsearch(client)
|
||||
}
|
||||
DatabaseType::Dameng => {
|
||||
let client = db::dm_driver::connect(
|
||||
&host,
|
||||
port,
|
||||
db_config.database.as_deref().unwrap_or(""),
|
||||
&db_config.username,
|
||||
&db_config.password,
|
||||
)
|
||||
.await?;
|
||||
PoolKind::Dameng(Arc::new(std::sync::Mutex::new(client)))
|
||||
}
|
||||
};
|
||||
|
||||
self.connections.lock().await.insert(pool_key.clone(), pool);
|
||||
|
|
@ -205,7 +217,11 @@ impl AppState {
|
|||
let configs = self.configs.lock().await;
|
||||
configs
|
||||
.get(connection_id)
|
||||
.map(|c| c.db_type == DatabaseType::Oracle || c.db_type == DatabaseType::Elasticsearch)
|
||||
.map(|c| {
|
||||
c.db_type == DatabaseType::Oracle
|
||||
|| c.db_type == DatabaseType::Elasticsearch
|
||||
|| c.db_type == DatabaseType::Dameng
|
||||
})
|
||||
.unwrap_or(false)
|
||||
};
|
||||
let pool_key = if is_single_conn {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,334 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use odbc_api::{buffers::TextRowSet, ConnectionOptions, Cursor, Environment, ResultSetMetadata};
|
||||
|
||||
use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo};
|
||||
|
||||
use super::CONNECTION_TIMEOUT_SECS;
|
||||
|
||||
static ENV: std::sync::LazyLock<Environment> =
|
||||
std::sync::LazyLock::new(|| Environment::new().expect("Failed to initialize ODBC environment"));
|
||||
|
||||
pub struct DmClient {
|
||||
conn: odbc_api::Connection<'static>,
|
||||
}
|
||||
|
||||
unsafe impl Send for DmClient {}
|
||||
|
||||
impl DmClient {
|
||||
pub fn query_rows(&self, sql: &str) -> Result<Vec<Vec<String>>, String> {
|
||||
match self.conn.execute(sql, (), None).map_err(|e| e.to_string())? {
|
||||
Some(cursor) => read_cursor(cursor),
|
||||
None => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_single_column(&self, sql: &str) -> Result<Vec<String>, String> {
|
||||
Ok(self.query_rows(sql)?.into_iter().filter_map(|r| r.into_iter().next()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cursor(cursor: impl Cursor) -> Result<Vec<Vec<String>>, String> {
|
||||
let mut cursor = cursor;
|
||||
let col_count = cursor.num_result_cols().map_err(|e| e.to_string())? as u16;
|
||||
let buffer = TextRowSet::for_cursor(1000, &mut cursor, Some(8192)).map_err(|e| e.to_string())?;
|
||||
let mut row_cursor = cursor.bind_buffer(buffer).map_err(|e| e.to_string())?;
|
||||
let mut rows = Vec::new();
|
||||
while let Some(batch) = row_cursor.fetch().map_err(|e| e.to_string())? {
|
||||
for row_idx in 0..batch.num_rows() {
|
||||
let vals: Vec<String> = (0..col_count as usize)
|
||||
.map(|col| {
|
||||
batch.at(col, row_idx).and_then(|bytes| std::str::from_utf8(bytes).ok()).unwrap_or("").to_string()
|
||||
})
|
||||
.collect();
|
||||
rows.push(vals);
|
||||
}
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn connect(host: &str, port: u16, database: &str, user: &str, pass: &str) -> Result<DmClient, String> {
|
||||
let conn_str = format!(
|
||||
"Driver={{DM8 ODBC DRIVER}};Server={host};TCP_PORT={port};DATABASE={db};UID={user};PWD={pass}",
|
||||
host = host,
|
||||
port = port,
|
||||
db = database,
|
||||
user = user,
|
||||
pass = pass,
|
||||
);
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
ENV.connect_with_connection_string(&conn_str, ConnectionOptions::default())
|
||||
.map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("Data source name not found") || msg.contains("Driver") {
|
||||
format!(
|
||||
"DM8 ODBC driver not found. Please install the DM8 ODBC driver \
|
||||
and register it in odbcinst.ini (Linux/macOS) or the ODBC Data Source Administrator (Windows). \
|
||||
Original error: {msg}"
|
||||
)
|
||||
} else {
|
||||
format!("DM connection failed: {msg}")
|
||||
}
|
||||
})
|
||||
.map(|conn| DmClient { conn })
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| format!("DM connection timed out ({CONNECTION_TIMEOUT_SECS}s)"))?
|
||||
.map_err(|e| format!("DM connection task failed: {e}"))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn list_databases(client: &DmClient) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let rows = client.query_single_column("SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME")?;
|
||||
Ok(rows.into_iter().map(|name| DatabaseInfo { name }).collect())
|
||||
}
|
||||
|
||||
pub fn list_schemas(client: &DmClient) -> Result<Vec<String>, String> {
|
||||
client.query_single_column("SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME")
|
||||
}
|
||||
|
||||
pub fn list_tables(client: &DmClient, schema: &str) -> Result<Vec<TableInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
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"
|
||||
);
|
||||
let rows = client.query_rows(&sql)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TableInfo {
|
||||
name: r.first().cloned().unwrap_or_default(),
|
||||
table_type: r.get(1).cloned().unwrap_or_else(|| "TABLE".to_string()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_columns(client: &DmClient, 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 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}'"
|
||||
))?;
|
||||
let pk_names: std::collections::HashSet<String> = pk_rows.into_iter().collect();
|
||||
|
||||
let col_rows = client.query_rows(&format!(
|
||||
"SELECT COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_PRECISION, DATA_SCALE, DATA_LENGTH, CHAR_LENGTH \
|
||||
FROM ALL_TAB_COLUMNS \
|
||||
WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \
|
||||
ORDER BY COLUMN_ID"
|
||||
))?;
|
||||
|
||||
Ok(col_rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let name = r.first().cloned().unwrap_or_default();
|
||||
let base = r.get(1).cloned().unwrap_or_default();
|
||||
let num_prec = r.get(3).and_then(|v| v.parse::<i32>().ok());
|
||||
let num_scale = r.get(4).and_then(|v| v.parse::<i32>().ok());
|
||||
let data_len = r.get(5).and_then(|v| v.parse::<i32>().ok());
|
||||
let char_len = r.get(6).and_then(|v| v.parse::<i32>().ok());
|
||||
let data_type = match base.to_uppercase().as_str() {
|
||||
"VARCHAR2" | "NVARCHAR2" | "VARCHAR" | "CHAR" | "NCHAR" => {
|
||||
let len = char_len.or(data_len);
|
||||
match len {
|
||||
Some(n) => format!("{base}({n})"),
|
||||
None => base,
|
||||
}
|
||||
}
|
||||
"NUMBER" | "NUMERIC" | "DECIMAL" => match (num_prec, num_scale) {
|
||||
(Some(p), Some(s)) if s > 0 => format!("{base}({p},{s})"),
|
||||
(Some(p), _) if p > 0 => format!("{base}({p})"),
|
||||
_ => base,
|
||||
},
|
||||
"RAW" => match data_len {
|
||||
Some(n) => format!("RAW({n})"),
|
||||
None => "RAW".to_string(),
|
||||
},
|
||||
_ => base,
|
||||
};
|
||||
ColumnInfo {
|
||||
is_primary_key: pk_names.contains(&name),
|
||||
name,
|
||||
data_type,
|
||||
is_nullable: r.get(2).map(|v| v == "Y").unwrap_or(false),
|
||||
column_default: None,
|
||||
extra: None,
|
||||
comment: None,
|
||||
numeric_precision: num_prec,
|
||||
numeric_scale: num_scale,
|
||||
character_maximum_length: char_len,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn list_indexes(client: &DmClient, schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
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"
|
||||
);
|
||||
let rows = client.query_rows(&sql)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let cols_str = r.get(1).cloned().unwrap_or_default();
|
||||
IndexInfo {
|
||||
name: r.first().cloned().unwrap_or_default(),
|
||||
columns: cols_str.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(),
|
||||
is_unique: r.get(2).map(|v| v == "UNIQUE").unwrap_or(false),
|
||||
is_primary: r.get(3).map(|v| v == "1").unwrap_or(false),
|
||||
filter: None,
|
||||
index_type: r.get(4).cloned(),
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn list_foreign_keys(client: &DmClient, schema: &str, table: &str) -> Result<Vec<ForeignKeyInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
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"
|
||||
);
|
||||
let rows = client.query_rows(&sql)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| ForeignKeyInfo {
|
||||
name: r.first().cloned().unwrap_or_default(),
|
||||
column: r.get(1).cloned().unwrap_or_default(),
|
||||
ref_table: r.get(2).cloned().unwrap_or_default(),
|
||||
ref_column: r.get(3).cloned().unwrap_or_default(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn list_triggers(client: &DmClient, schema: &str, table: &str) -> Result<Vec<TriggerInfo>, String> {
|
||||
let s = schema.replace('\'', "''");
|
||||
let t = table.replace('\'', "''");
|
||||
let sql = format!(
|
||||
"SELECT TRIGGER_NAME, TRIGGERING_EVENT, TRIGGER_TYPE \
|
||||
FROM ALL_TRIGGERS \
|
||||
WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \
|
||||
ORDER BY TRIGGER_NAME"
|
||||
);
|
||||
let rows = client.query_rows(&sql)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TriggerInfo {
|
||||
name: r.first().cloned().unwrap_or_default(),
|
||||
event: r.get(1).cloned().unwrap_or_default(),
|
||||
timing: r.get(2).cloned().unwrap_or_default(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn execute_query_sync(client: &DmClient, 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")
|
||||
{
|
||||
match client.conn.execute(sql, (), None).map_err(|e| e.to_string())? {
|
||||
Some(mut cursor) => {
|
||||
let col_count = cursor.num_result_cols().map_err(|e| e.to_string())? as u16;
|
||||
let columns: Vec<String> = (1..=col_count)
|
||||
.map(|i| cursor.col_name(i).map_err(|e| e.to_string()).unwrap_or_else(|_| format!("col{i}")))
|
||||
.collect();
|
||||
|
||||
let buffer = TextRowSet::for_cursor(1000, &mut cursor, Some(8192)).map_err(|e| e.to_string())?;
|
||||
let mut row_cursor = cursor.bind_buffer(buffer).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut rows = Vec::new();
|
||||
while let Some(batch) = row_cursor.fetch().map_err(|e| e.to_string())? {
|
||||
for row_idx in 0..batch.num_rows() {
|
||||
let vals: Vec<serde_json::Value> = (0..col_count as usize)
|
||||
.map(|col| {
|
||||
batch
|
||||
.at(col, row_idx)
|
||||
.and_then(|bytes| std::str::from_utf8(bytes).ok())
|
||||
.map(|s| serde_json::Value::String(s.to_string()))
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
})
|
||||
.collect();
|
||||
rows.push(vals);
|
||||
if rows.len() >= crate::query::MAX_ROWS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if rows.len() >= crate::query::MAX_ROWS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let truncated = rows.len() >= crate::query::MAX_ROWS;
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated,
|
||||
})
|
||||
}
|
||||
None => Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
match client.conn.execute(sql, (), None) {
|
||||
Ok(Some(_cursor)) => Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
}),
|
||||
Ok(None) => Ok(QueryResult {
|
||||
columns: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
}),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod clickhouse_driver;
|
||||
pub mod dm_driver;
|
||||
pub mod duckdb_driver;
|
||||
pub mod elasticsearch_driver;
|
||||
pub mod file_validator;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ pub enum DatabaseType {
|
|||
#[serde(rename = "starrocks")]
|
||||
StarRocks,
|
||||
Redshift,
|
||||
Dameng,
|
||||
}
|
||||
|
||||
impl ConnectionConfig {
|
||||
|
|
@ -128,6 +129,7 @@ impl ConnectionConfig {
|
|||
}
|
||||
DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"),
|
||||
DatabaseType::Elasticsearch => format!("http://{host}:{port}"),
|
||||
DatabaseType::Dameng => format!("dm://{host}:{port}{db_part}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,6 +188,9 @@ impl ConnectionConfig {
|
|||
format!("oracle://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Elasticsearch => format!("http://{host}:{port}"),
|
||||
DatabaseType::Dameng => {
|
||||
format!("dm://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,20 @@ pub async fn do_execute(
|
|||
PoolKind::Elasticsearch(_) => Err("Use document browser for Elasticsearch".to_string()),
|
||||
PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()),
|
||||
PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()),
|
||||
PoolKind::Dameng(client) => {
|
||||
let client = client.clone();
|
||||
let sql = sql.to_string();
|
||||
drop(connections);
|
||||
wait_for_query(cancel_token, async move {
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
db::dm_driver::execute_query_sync(&client, &sql)
|
||||
});
|
||||
task.await.map_err(|e| e.to_string())?
|
||||
})
|
||||
.await
|
||||
.map(truncate_result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,16 @@ pub fn extract_oracle(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn extract_dameng(
|
||||
connections: &HashMap<String, PoolKind>,
|
||||
key: &str,
|
||||
) -> Option<Arc<std::sync::Mutex<db::dm_driver::DmClient>>> {
|
||||
match connections.get(key)? {
|
||||
PoolKind::Dameng(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.lock().await;
|
||||
|
|
@ -117,6 +127,11 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::list_databases(&*client).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, connection_id) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::list_databases(&client);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -146,6 +161,11 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::list_schemas(&*client).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::list_schemas(&client);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -186,6 +206,11 @@ pub async fn list_tables_core(
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::list_tables(&*client, schema).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::list_tables(&client, schema);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -229,6 +254,11 @@ pub async fn get_columns_core(
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::get_columns(&*client, schema, table).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::get_columns(&client, schema, table);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -263,6 +293,11 @@ pub async fn list_indexes_core(
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::list_indexes(&*client, schema, table).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::list_indexes(&client, schema, table);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -297,6 +332,11 @@ pub async fn list_foreign_keys_core(
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::list_foreign_keys(&*client, schema, table).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::list_foreign_keys(&client, schema, table);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -331,6 +371,11 @@ pub async fn list_triggers_core(
|
|||
let client = client.lock().await;
|
||||
return db::oracle_driver::list_triggers(&*client, schema, table).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return db::dm_driver::list_triggers(&client, schema, table);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -391,6 +436,11 @@ pub async fn get_table_ddl_core(
|
|||
let client = client.lock().await;
|
||||
return build_oracle_ddl(&*client, schema, table).await;
|
||||
}
|
||||
if let Some(client) = extract_dameng(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = client.lock().map_err(|e| e.to_string())?;
|
||||
return build_dameng_ddl(&client, schema, table);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = state.connections.lock().await;
|
||||
|
|
@ -598,3 +648,50 @@ pub async fn build_oracle_ddl(
|
|||
}
|
||||
Ok(ddl)
|
||||
}
|
||||
|
||||
pub fn build_dameng_ddl(client: &db::dm_driver::DmClient, schema: &str, table: &str) -> Result<String, String> {
|
||||
let columns = db::dm_driver::get_columns(client, schema, table)?;
|
||||
let indexes = db::dm_driver::list_indexes(client, schema, table)?;
|
||||
let fkeys = db::dm_driver::list_foreign_keys(client, schema, table)?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,15 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
|
|||
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 => db::dm_driver::connect(
|
||||
&host,
|
||||
port,
|
||||
config.database.as_deref().unwrap_or(""),
|
||||
&config.username,
|
||||
&config.password,
|
||||
)
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -179,6 +188,17 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
|
|||
db::elasticsearch_driver::test_connection(&client).await?;
|
||||
PoolKind::Elasticsearch(client)
|
||||
}
|
||||
DatabaseType::Dameng => {
|
||||
let client = db::dm_driver::connect(
|
||||
&host,
|
||||
port,
|
||||
config.database.as_deref().unwrap_or(""),
|
||||
&config.username,
|
||||
&config.password,
|
||||
)
|
||||
.await?;
|
||||
PoolKind::Dameng(std::sync::Arc::new(std::sync::Mutex::new(client)))
|
||||
}
|
||||
};
|
||||
|
||||
state.connections.lock().await.insert(id.clone(), pool);
|
||||
|
|
@ -205,6 +225,7 @@ pub async fn disconnect_db(state: State<'_, Arc<AppState>>, connection_id: Strin
|
|||
PoolKind::SqlServer(_) => {}
|
||||
PoolKind::Oracle(_) => {}
|
||||
PoolKind::Elasticsearch(_) => {}
|
||||
PoolKind::Dameng(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ const driverProfiles: Record<
|
|||
label: "CockroachDB",
|
||||
icon: "cockroachdb",
|
||||
},
|
||||
dm: { type: "postgres", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" },
|
||||
dm: { type: "dameng", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" },
|
||||
tdengine: { type: "mysql", port: 6030, user: "root", label: "TDengine", icon: "tdengine" },
|
||||
custom_mysql: {
|
||||
type: "mysql",
|
||||
|
|
@ -322,6 +322,7 @@ const dbOptions = [
|
|||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "elasticsearch", label: "Elasticsearch" },
|
||||
{ value: "mariadb", label: "MariaDB" },
|
||||
{ value: "dm", label: "DM (Dameng)" },
|
||||
];
|
||||
|
||||
const mysqlCompat = [
|
||||
|
|
@ -340,7 +341,6 @@ const pgCompat = [
|
|||
{ value: "gaussdb", label: "GaussDB" },
|
||||
{ value: "kingbase", label: "KingBase" },
|
||||
{ value: "vastbase", label: "Vastbase" },
|
||||
{ value: "dm", label: "DM (Dameng)" },
|
||||
{ value: "redshift", label: "Redshift" },
|
||||
{ value: "cockroachdb", label: "CockroachDB" },
|
||||
{ value: "custom_postgres", label: "Custom" },
|
||||
|
|
@ -844,6 +844,13 @@ async function browseDbFilePath() {
|
|||
<span />
|
||||
<p class="col-span-3 text-xs text-muted-foreground">
|
||||
{{ t("connection.dmCompatHint") }}
|
||||
<a
|
||||
href="https://eco.dameng.com/download/"
|
||||
target="_blank"
|
||||
class="underline text-primary hover:text-primary/80"
|
||||
>
|
||||
{{ t("connection.dmDownload") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ const props = defineProps<{
|
|||
focusTableName?: string;
|
||||
}>();
|
||||
|
||||
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"];
|
||||
const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift"];
|
||||
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"];
|
||||
const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift", "dameng"];
|
||||
const CARD_WIDTH = 270;
|
||||
const COLUMN_ROW_HEIGHT = 24;
|
||||
const CARD_HEADER_HEIGHT = 44;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ async function resolveSchema(connectionId: string, database: string): Promise<st
|
|||
config?.db_type === "postgres" ||
|
||||
config?.db_type === "sqlserver" ||
|
||||
config?.db_type === "oracle" ||
|
||||
config?.db_type === "redshift";
|
||||
config?.db_type === "redshift" ||
|
||||
config?.db_type === "dameng";
|
||||
if (needsSchema) {
|
||||
const schemas = await api.listSchemas(connectionId, database);
|
||||
return schemas.includes("public") ? "public" : (schemas[0] ?? "");
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ type SearchTableTask = {
|
|||
table: TableInfo;
|
||||
};
|
||||
|
||||
const SCHEMA_AWARE_TYPES = new Set<DatabaseType>(["postgres", "sqlserver", "oracle", "redshift"]);
|
||||
const SCHEMA_AWARE_TYPES = new Set<DatabaseType>(["postgres", "sqlserver", "oracle", "redshift", "dameng"]);
|
||||
const SYSTEM_SCHEMAS = new Set([
|
||||
"information_schema",
|
||||
"pg_catalog",
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const sqlFileUnsupportedTypes = new Set(["redis", "mongodb", "elasticsearch"]);
|
||||
const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]);
|
||||
const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"]);
|
||||
const databaseSearchSupportedTypes = new Set([
|
||||
"mysql",
|
||||
"postgres",
|
||||
|
|
@ -97,6 +97,7 @@ const databaseSearchSupportedTypes = new Set([
|
|||
"redshift",
|
||||
"duckdb",
|
||||
"clickhouse",
|
||||
"dameng",
|
||||
]);
|
||||
const tableImportSupportedTypes = new Set([
|
||||
"mysql",
|
||||
|
|
@ -109,9 +110,18 @@ const tableImportSupportedTypes = new Set([
|
|||
"doris",
|
||||
"starrocks",
|
||||
"redshift",
|
||||
"dameng",
|
||||
]);
|
||||
const tableStructureSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver"]);
|
||||
const fieldLineageSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]);
|
||||
const fieldLineageSupportedTypes = new Set([
|
||||
"mysql",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"sqlserver",
|
||||
"oracle",
|
||||
"redshift",
|
||||
"dameng",
|
||||
]);
|
||||
const isExportingDatabase = ref(false);
|
||||
|
||||
function currentDatabaseType(): DatabaseType | undefined {
|
||||
|
|
@ -123,7 +133,7 @@ function quoteIdent(name: string): string {
|
|||
}
|
||||
|
||||
function isSchemaAwareDbType(dbType?: DatabaseType): boolean {
|
||||
return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver";
|
||||
return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver" || dbType === "dameng";
|
||||
}
|
||||
|
||||
function qualifiedTableName(tableName: string, schema?: string): string {
|
||||
|
|
@ -277,7 +287,11 @@ async function openData() {
|
|||
if (!config) throw new Error("Connection config not found");
|
||||
|
||||
const qualifiedName =
|
||||
(config.db_type === "postgres" || config.db_type === "oracle" || config.db_type === "sqlserver") && node.schema
|
||||
(config.db_type === "postgres" ||
|
||||
config.db_type === "oracle" ||
|
||||
config.db_type === "sqlserver" ||
|
||||
config.db_type === "dameng") &&
|
||||
node.schema
|
||||
? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}`
|
||||
: quoteIdent(node.label);
|
||||
|
||||
|
|
@ -286,7 +300,7 @@ async function openData() {
|
|||
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(", ")}` : "";
|
||||
let sql: string;
|
||||
if (config.db_type === "oracle") {
|
||||
if (config.db_type === "oracle" || config.db_type === "dameng") {
|
||||
sql = `SELECT * FROM ${qualifiedName}${order} FETCH FIRST 100 ROWS ONLY`;
|
||||
} else if (config.db_type === "sqlserver") {
|
||||
sql = `SELECT TOP 100 * FROM ${qualifiedName}${order}`;
|
||||
|
|
@ -462,7 +476,7 @@ async function confirmDuplicateStructure() {
|
|||
sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`;
|
||||
} else if (dbType === "sqlserver") {
|
||||
sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`;
|
||||
} else if (dbType === "oracle") {
|
||||
} else if (dbType === "oracle" || dbType === "dameng") {
|
||||
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 1=0`;
|
||||
} else {
|
||||
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 0;`;
|
||||
|
|
@ -653,7 +667,11 @@ async function exportData(format: "csv" | "json" | "sql") {
|
|||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const qualifiedName =
|
||||
(config.db_type === "postgres" || config.db_type === "oracle" || config.db_type === "sqlserver") && node.schema
|
||||
(config.db_type === "postgres" ||
|
||||
config.db_type === "oracle" ||
|
||||
config.db_type === "sqlserver" ||
|
||||
config.db_type === "dameng") &&
|
||||
node.schema
|
||||
? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}`
|
||||
: quoteIdent(node.label);
|
||||
const result = await api.executeQuery(node.connectionId, node.database, `SELECT * FROM ${qualifiedName}`);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,16 @@ const props = defineProps<{
|
|||
|
||||
const store = useConnectionStore();
|
||||
|
||||
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "clickhouse", "duckdb"];
|
||||
const SQL_TYPES: DatabaseType[] = [
|
||||
"mysql",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"sqlserver",
|
||||
"oracle",
|
||||
"clickhouse",
|
||||
"duckdb",
|
||||
"dameng",
|
||||
];
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => SQL_TYPES.includes(c.db_type)));
|
||||
|
||||
|
|
@ -122,7 +131,10 @@ async function loadTables() {
|
|||
try {
|
||||
const config = store.getConfig(sourceConnectionId.value);
|
||||
const needsSchema =
|
||||
config?.db_type === "postgres" || config?.db_type === "sqlserver" || config?.db_type === "oracle";
|
||||
config?.db_type === "postgres" ||
|
||||
config?.db_type === "sqlserver" ||
|
||||
config?.db_type === "oracle" ||
|
||||
config?.db_type === "dameng";
|
||||
if (needsSchema) {
|
||||
const schemas = await api.listSchemas(sourceConnectionId.value, sourceDatabase.value);
|
||||
sourceSchema.value = schemas.includes("public") ? "public" : (schemas[0] ?? "");
|
||||
|
|
@ -210,7 +222,10 @@ async function startTransfer() {
|
|||
// Auto-detect target schema
|
||||
const targetConfig = store.getConfig(targetConnectionId.value);
|
||||
const targetNeedsSchema =
|
||||
targetConfig?.db_type === "postgres" || targetConfig?.db_type === "sqlserver" || targetConfig?.db_type === "oracle";
|
||||
targetConfig?.db_type === "postgres" ||
|
||||
targetConfig?.db_type === "sqlserver" ||
|
||||
targetConfig?.db_type === "oracle" ||
|
||||
targetConfig?.db_type === "dameng";
|
||||
if (targetNeedsSchema && !targetSchema.value) {
|
||||
try {
|
||||
const schemas = await api.listSchemas(targetConnectionId.value, targetDatabase.value);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export function useSchemaOptions() {
|
|||
|
||||
function isSchemaAware(connectionId: string): boolean {
|
||||
const dbType = connectionStore.getConfig(connectionId)?.db_type;
|
||||
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle";
|
||||
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng";
|
||||
}
|
||||
|
||||
async function loadSchemaOptions(connectionId: string, database: string) {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ export default {
|
|||
sshKeyPassphrasePlaceholder: "Leave empty if key is not encrypted",
|
||||
sshKeyPathBrowse: "Browse",
|
||||
sshExposeLan: "Expose tunnel to LAN",
|
||||
dmCompatHint: "Requires DM PG compatibility mode (set COMPATIBLE_MODE=7 in dm.ini and restart)",
|
||||
dmCompatHint: "Requires DM8 ODBC driver installed on your system.",
|
||||
dmDownload: "Download from Dameng",
|
||||
mongoLegacyHint:
|
||||
"For older MongoDB servers, enter authSource=admin&authMechanism=SCRAM-SHA-1 here; add directConnection=true only when needed for direct standalone connections.",
|
||||
compatible: "Compatible",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ export default {
|
|||
sshKeyPassphrasePlaceholder: "密钥未加密则留空",
|
||||
sshKeyPathBrowse: "浏览",
|
||||
sshExposeLan: "允许局域网访问隧道",
|
||||
dmCompatHint: "需要开启达梦 PG 兼容模式(dm.ini 中设置 COMPATIBLE_MODE=7 并重启服务)",
|
||||
dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。",
|
||||
dmDownload: "前往达梦官网下载",
|
||||
mongoLegacyHint:
|
||||
"连接旧版 MongoDB 时,可在此填写 authSource=admin&authMechanism=SCRAM-SHA-1;直连单节点时可按需追加 directConnection=true。",
|
||||
compatible: "兼容",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,13 @@ export function qualifiedTableName(
|
|||
options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName">,
|
||||
): string {
|
||||
const { databaseType, schema, tableName } = options;
|
||||
if ((databaseType === "postgres" || databaseType === "oracle" || databaseType === "sqlserver") && schema) {
|
||||
if (
|
||||
(databaseType === "postgres" ||
|
||||
databaseType === "oracle" ||
|
||||
databaseType === "sqlserver" ||
|
||||
databaseType === "dameng") &&
|
||||
schema
|
||||
) {
|
||||
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;
|
||||
}
|
||||
return quoteTableIdentifier(databaseType, tableName);
|
||||
|
|
@ -47,7 +53,7 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
|
|||
const orderBy = options.orderBy ?? defaultOrderBy;
|
||||
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
|
||||
|
||||
if (databaseType === "oracle") {
|
||||
if (databaseType === "oracle" || databaseType === "dameng") {
|
||||
const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : "";
|
||||
return `SELECT * FROM ${table}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
doris: "Doris",
|
||||
starrocks: "StarRocks",
|
||||
redshift: "Redshift",
|
||||
dameng: "DM (Dameng)",
|
||||
};
|
||||
return {
|
||||
...config,
|
||||
|
|
@ -652,7 +653,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
function isSchemaAwareDatabase(connectionId: string): boolean {
|
||||
const dbType = getConfig(connectionId)?.db_type;
|
||||
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle";
|
||||
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng";
|
||||
}
|
||||
|
||||
async function listCompletionTables(connectionId: string, database: string): Promise<SqlCompletionTable[]> {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ export type DatabaseType =
|
|||
| "elasticsearch"
|
||||
| "doris"
|
||||
| "starrocks"
|
||||
| "redshift";
|
||||
| "redshift"
|
||||
| "dameng";
|
||||
|
||||
export interface ConnectionConfig {
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue