diff --git a/README.md b/README.md index 9dae78e3..3016dbe3 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,8 @@ SqlKit supports **70+ databases** across four adapter strategies: |----------|-----------| | **Native** (Rust) | PostgreSQL, MySQL, SQL Server, SQLite | | **PG-wire compat** | CockroachDB, Redshift, YugabyteDB, TimescaleDB, QuestDB, Vastbase, YashanDB, KingbaseES, GaussDB, HighGo, UXDB, OpenGauss, GBase8c, Greenplum, EnterpriseDB, CrateDB, Materialize, AlloyDB, CloudSQLPG, FujitsuPG | -| **MySQL-wire compat** | MariaDB, TiDB, OceanBase, TDSQL, PolarDB, DM8, Doris, SelectDB, StarRocks, Databend, GoldenDB, ManticoreSearch, SingleStore, CloudSQLMySQL | -| **JDBC bridge** | Oracle, DuckDB, Firebird, DB2, H2, Snowflake, TDengine, Derby, Hive, Databricks, Hana, Teradata, Vertica, Exasol, BigQuery, Informix, Kylin, Cassandra, Iris, Access, DM8Oracle, XuguDB, GBase8a | +| **MySQL-wire compat** | MariaDB, TiDB, OceanBase, TDSQL, PolarDB, Doris, SelectDB, StarRocks, Databend, GoldenDB, ManticoreSearch, SingleStore, CloudSQLMySQL | +| **JDBC bridge** | Oracle, DuckDB, Firebird, DB2, H2, Snowflake, TDengine, Derby, Hive, Databricks, Hana, Teradata, Vertica, Exasol, BigQuery, Informix, Kylin, Cassandra, Iris, Access, Dameng, XuguDB, GBase8a | | **HTTP bridge** | ClickHouse, Trino, Presto, RQLite, Turso | ### Product-Grade Editor diff --git a/README_zh.md b/README_zh.md index 20f2261c..57850e1e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -107,8 +107,8 @@ SqlKit 支持 **70+ 种数据库**,通过四种适配策略覆盖: |------|--------| | **原生** (Rust) | PostgreSQL、MySQL、SQL Server、SQLite | | **PG 协议兼容** | CockroachDB、Redshift、YugabyteDB、TimescaleDB、QuestDB、Vastbase、YashanDB、KingbaseES、GaussDB、HighGo、UXDB、OpenGauss、GBase8c、Greenplum、EnterpriseDB、CrateDB、Materialize、AlloyDB、CloudSQLPG、FujitsuPG | -| **MySQL 协议兼容** | MariaDB、TiDB、OceanBase、TDSQL、PolarDB、DM8、Doris、SelectDB、StarRocks、Databend、GoldenDB、ManticoreSearch、SingleStore、CloudSQLMySQL | -| **JDBC 桥接** | Oracle、DuckDB、Firebird、DB2、H2、Snowflake、TDengine、Derby、Hive、Databricks、Hana、Teradata、Vertica、Exasol、BigQuery、Informix、Kylin、Cassandra、Iris、Access、DM8Oracle、XuguDB、GBase8a | +| **MySQL 协议兼容** | MariaDB、TiDB、OceanBase、TDSQL、PolarDB、Doris、SelectDB、StarRocks、Databend、GoldenDB、ManticoreSearch、SingleStore、CloudSQLMySQL | +| **JDBC 桥接** | Oracle、DuckDB、Firebird、DB2、H2、Snowflake、TDengine、Derby、Hive、Databricks、Hana、Teradata、Vertica、Exasol、BigQuery、Informix、Kylin、Cassandra、Iris、Access、Dameng、XuguDB、GBase8a | | **HTTP 桥接** | ClickHouse、Trino、Presto、RQLite、Turso | ### 专业级编辑器 diff --git a/jdbc-bridge/src/main/java/sqlkit/bridge/MetadataProvider.java b/jdbc-bridge/src/main/java/sqlkit/bridge/MetadataProvider.java index b7d604c6..0c75f342 100644 --- a/jdbc-bridge/src/main/java/sqlkit/bridge/MetadataProvider.java +++ b/jdbc-bridge/src/main/java/sqlkit/bridge/MetadataProvider.java @@ -10,14 +10,57 @@ public class MetadataProvider { /** * List all databases (catalogs) on the server. + * + * For Oracle, getCatalogs() returns empty because Oracle doesn't use + * JDBC catalogs in the traditional sense. Fall back to querying the + * current container/PDB name via SYS_CONTEXT, then try listing all + * PDBs if connected to a CDB. */ public static List listDatabases(Connection conn) throws Exception { List databases = new ArrayList<>(); try (ResultSet rs = conn.getMetaData().getCatalogs()) { while (rs.next()) { - databases.add(rs.getString("TABLE_CAT")); + String cat = rs.getString("TABLE_CAT"); + if (cat != null && !cat.isEmpty()) { + databases.add(cat); + } } } + + // Oracle fallback: getCatalogs() returns empty for Oracle JDBC. + // Try SYS_CONTEXT first (works in any Oracle container), + // then v$pdbs (only works in CDB$ROOT). + if (databases.isEmpty()) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT SYS_CONTEXT('USERENV', 'CON_NAME') FROM DUAL")) { + if (rs.next()) { + String conName = rs.getString(1); + if (conName != null && !conName.isEmpty()) { + databases.add(conName); + } + } + } catch (SQLException e) { + // Fall through to v$pdbs + } + } + + if (databases.isEmpty()) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT name FROM v$pdbs")) { + while (rs.next()) { + String name = rs.getString(1); + if (name != null && !name.isEmpty()) { + databases.add(name); + } + } + } catch (SQLException e) { + // Driver does not support Oracle-specific queries; + // leave databases empty (frontend will fall back to + // the configured connection database). + } + } + return databases; } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7cd2b23d..a8fcf68b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1483,7 +1483,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "data-studio-agent" version = "0.1.1" -source = "git+https://github.com/geek-fun/data-studio-agent.git?tag=v0.1.1#4e82283a77d3627dbce6ecc8aeb7832ec0ad3d72" +source = "git+https://github.com/geek-fun/data-studio-agent.git?tag=v0.1.2#bd34c5a061ea3162b2b8d351234c05ebceae57fa" dependencies = [ "async-openai", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0c5f060c..edfcfe86 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -86,7 +86,7 @@ http = "1" log = "0.4" futures = "0.3" rand = "0.8" -data-studio-agent = { git = "https://github.com/geek-fun/data-studio-agent.git", tag = "v0.1.1" } +data-studio-agent = { git = "https://github.com/geek-fun/data-studio-agent.git", tag = "v0.1.2" } # Archive extraction (JRE downloads) flate2 = "1.0" diff --git a/src-tauri/src/capabilities/sql.rs b/src-tauri/src/capabilities/sql.rs index 3b1d5f43..c159a686 100644 --- a/src-tauri/src/capabilities/sql.rs +++ b/src-tauri/src/capabilities/sql.rs @@ -96,7 +96,36 @@ async fn execute_on_adapter(adapter: &ActiveConnection, sql: &str) -> Result todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::HttpSql(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::Rqlite(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::Turso(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), } } @@ -202,7 +231,36 @@ impl CapabilityHandler for ListDatabasesHandler { .list_databases() .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&dbs).map_err(|e| e.to_string()) } @@ -238,7 +296,36 @@ impl CapabilityHandler for ListSchemasHandler { .list_schemas(database) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&schemas).map_err(|e| e.to_string()) } @@ -280,7 +367,36 @@ impl CapabilityHandler for ListTablesHandler { .list_tables(database, schema) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&tables).map_err(|e| e.to_string()) } @@ -323,10 +439,48 @@ impl CapabilityHandler for GetSchemaHandler { .list_tables(database, schema) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, }; + const MAX_SCHEMA_TABLES: usize = 30; + let tables: Vec<_> = tables.into_iter().take(MAX_SCHEMA_TABLES).collect(); + let mut schema_lines: Vec = Vec::new(); + if tables.len() >= MAX_SCHEMA_TABLES { + schema_lines.push(format!( + "-- Showing first {} tables. Specify a schema filter for complete results.\n", + MAX_SCHEMA_TABLES + )); + } for table in &tables { let cols = match &adapter { ActiveConnection::Postgres(a) => a @@ -353,7 +507,36 @@ impl CapabilityHandler for GetSchemaHandler { .list_columns(database, schema, &table.name) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, }; let schema_name = table.schema.as_deref().unwrap_or("public"); @@ -425,7 +608,36 @@ impl CapabilityHandler for DescribeTableHandler { .list_columns(database, schema, table) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&cols).map_err(|e| e.to_string()) } @@ -504,7 +716,7 @@ pub fn register_sql_tools(reg: &mut CapabilityRegistry) { reg.register(Capability { name: "sqlkit__list_tables", - description: "List all tables in a database schema.", + description: "List all tables in a database schema. Returns table names, types, and row counts — fast and lightweight. Use this to check if tables exist or browse available objects. For full column details, use sqlkit__describe_table or sqlkit__get_schema.", handler: Arc::new(ListTablesHandler), input_schema: json!({"type": "object", "properties": { "connection_id": connection_id_schema(), @@ -518,7 +730,7 @@ pub fn register_sql_tools(reg: &mut CapabilityRegistry) { reg.register(Capability { name: "sqlkit__get_schema", - description: "Get the full database schema (all tables and columns) as DDL-like text. Use this before writing queries to understand the structure.", + description: "Get the full database schema (all tables and all columns) as DDL-like text. SLOW on databases with many objects. Prefer sqlkit__list_tables for browsing and sqlkit__describe_table for single-table details.", handler: Arc::new(GetSchemaHandler), input_schema: json!({"type": "object", "properties": { "connection_id": connection_id_schema(), diff --git a/src-tauri/src/capabilities/sqlkit.rs b/src-tauri/src/capabilities/sqlkit.rs index 13abd52c..23f35ae3 100644 --- a/src-tauri/src/capabilities/sqlkit.rs +++ b/src-tauri/src/capabilities/sqlkit.rs @@ -19,13 +19,15 @@ pub(crate) struct TauriStoreReader; impl ConnectionStoreReader for TauriStoreReader { fn get_connections(&self) -> Result { - let app = crate::APP_HANDLE - .get() - .ok_or_else(|| "AppHandle not initialized — app may still be starting".to_string())?; - - let store = app - .store(".store.dat") - .map_err(|e| format!("Failed to open store: {}", e))?; + let app = match crate::APP_HANDLE.get() { + Some(handle) => handle, + None => return Ok(Value::Array(vec![])), + }; + + let store = match app.store(".store.dat") { + Ok(s) => s, + Err(_) => return Ok(Value::Array(vec![])), + }; Ok(store.get("connections").unwrap_or(Value::Array(vec![]))) } diff --git a/src-tauri/src/commands/helpers.rs b/src-tauri/src/commands/helpers.rs index b0516dd7..09d76d73 100644 --- a/src-tauri/src/commands/helpers.rs +++ b/src-tauri/src/commands/helpers.rs @@ -170,8 +170,7 @@ fn db_type_to_enum(db_type: &str) -> Result Ok(DatabaseType::OceanBase), "tdsql" => Ok(DatabaseType::TDSQL), "polardb" => Ok(DatabaseType::PolarDB), - "dm8" | "dm" => Ok(DatabaseType::DM8), - "dm8_oracle" | "dm8oracle" => Ok(DatabaseType::DM8Oracle), + "dameng" | "dm" | "dm8" | "dm8_oracle" | "dm8oracle" => Ok(DatabaseType::Dameng), "kingbasees" | "kingbase" => Ok(DatabaseType::KingbaseES), "gaussdb" | "gauss" => Ok(DatabaseType::GaussDB), "highgo" => Ok(DatabaseType::HighGo), diff --git a/src-tauri/src/commands/jdbc.rs b/src-tauri/src/commands/jdbc.rs index 5d74c32d..966da013 100644 --- a/src-tauri/src/commands/jdbc.rs +++ b/src-tauri/src/commands/jdbc.rs @@ -139,7 +139,7 @@ fn parse_db_type(s: &str) -> Result { "h2" => Ok(DatabaseType::H2), "derby" => Ok(DatabaseType::Derby), "snowflake" => Ok(DatabaseType::Snowflake), - "dm8_oracle" | "dm8oracle" => Ok(DatabaseType::DM8Oracle), + "dameng" | "dm8" | "dm" | "dm8_oracle" | "dm8oracle" => Ok(DatabaseType::Dameng), "xugudb" | "xugu" => Ok(DatabaseType::XuguDB), "gbase8a" | "gbase_8a" => Ok(DatabaseType::GBase8a), "hive" => Ok(DatabaseType::Hive), diff --git a/src-tauri/src/database/config.rs b/src-tauri/src/database/config.rs index ed5aa0c2..a1584442 100644 --- a/src-tauri/src/database/config.rs +++ b/src-tauri/src/database/config.rs @@ -84,8 +84,6 @@ pub enum DatabaseType { TDSQL, /// 阿里云 PolarDB (MySQL mode) — MySQL wire protocol. PolarDB, - /// 达梦 DM8 (MySQL mode, secondary) — MySQL wire protocol alias. - DM8, /// Apache Doris — MySQL wire protocol. Doris, /// SelectDB (Doris fork) — MySQL wire protocol. @@ -139,8 +137,8 @@ pub enum DatabaseType { Iris, /// Microsoft Access — JDBC bridge. Access, - /// 达梦 DM8 (Oracle mode, primary) — JDBC bridge. - DM8Oracle, + /// 达梦 Dameng DM8 — JDBC bridge. + Dameng, /// 虚谷 XuguDB — JDBC bridge. XuguDB, /// 南大通用 GBase 8a — JDBC bridge. diff --git a/src-tauri/src/database/jdbc_bridge/adapter.rs b/src-tauri/src/database/jdbc_bridge/adapter.rs index 0dc7dd39..4a456f44 100644 --- a/src-tauri/src/database/jdbc_bridge/adapter.rs +++ b/src-tauri/src/database/jdbc_bridge/adapter.rs @@ -74,8 +74,13 @@ impl JdbcBridgeAdapter { launcher: &Arc>, req: JdbcRequest, ) -> DbResult { - let mut guard = launcher.lock().await; - let resp = guard.send_request(&req)?; + let launcher = launcher.clone(); + let resp = tokio::task::spawn_blocking(move || { + let mut guard = launcher.blocking_lock(); + guard.send_request(&req) + }) + .await + .map_err(|e| DbError::Connection(format!("JDBC bridge task panicked: {}", e)))??; Ok(resp.result.unwrap_or(serde_json::Value::Null)) } @@ -162,18 +167,65 @@ impl DatabaseAdapter for JdbcBridgeAdapter { async fn execute_query(&self, query: &str) -> DbResult { let launcher = self.launcher()?; - let data = Self::send_request( - &launcher, - JdbcRequest::new( - JdbcMethod::ExecuteQuery, - serde_json::json!({ - "conn_id": self.conn_id, - "sql": query, - }), - ), - ) - .await?; - Self::parse_query_result(data) + let statements = split_sql_statements(query); + if statements.is_empty() { + return Ok(QueryResult { + columns: Vec::new(), + column_types: Vec::new(), + rows: Vec::new(), + rows_affected: Some(0), + execution_time_ms: None, + truncated: false, + }); + } + // Execute each statement individually. Many JDBC drivers (notably + // Dameng) reject multiple DDL/DML statements sent as a single string, + // so splitting is required. + // + // Result aggregation: for SELECT statements we want to show the last + // result (the user is interested in the final query output). For DML + // statements (no rows, has rows_affected) we accumulate the total so + // that multi-statement INSERTs and UPDATEs report correct counts. + // + // Note: statements are NOT wrapped in a transaction — each executes + // in autocommit mode. If statement N fails, statements 1..N-1 are + // already committed. This matches the behavior of executing each + // statement individually in the editor. + let mut result = QueryResult { + columns: Vec::new(), + column_types: Vec::new(), + rows: Vec::new(), + rows_affected: Some(0), + execution_time_ms: None, + truncated: false, + }; + for stmt in &statements { + let data = Self::send_request( + &launcher, + JdbcRequest::new( + JdbcMethod::ExecuteQuery, + serde_json::json!({ + "conn_id": self.conn_id, + "sql": stmt, + }), + ), + ) + .await?; + let stmt_result = Self::parse_query_result(data)?; + let has_rows = !stmt_result.columns.is_empty() || !stmt_result.rows.is_empty(); + if has_rows { + result.columns = stmt_result.columns; + result.column_types = stmt_result.column_types; + result.rows = stmt_result.rows; + result.rows_affected = stmt_result.rows_affected; + } else if let Some(affected) = stmt_result.rows_affected { + let total = result.rows_affected.unwrap_or(0) + affected; + result.rows_affected = Some(total); + } + result.execution_time_ms = stmt_result.execution_time_ms; + result.truncated = result.truncated || stmt_result.truncated; + } + Ok(result) } async fn list_databases(&self) -> DbResult> { @@ -338,3 +390,174 @@ impl DatabaseAdapter for JdbcBridgeAdapter { &self.config } } + +/// Split a SQL string into individual statements separated by `;`. +/// +/// Handles: +/// - Single-quoted string literals (`'...'`) +/// - Double-quoted identifiers (`"..."`) +/// - `--` single-line comments +/// - `/* ... */` block comments +/// - Trailing semicolons and whitespace +/// - Empty statements are skipped +fn split_sql_statements(sql: &str) -> Vec { + let mut statements: Vec = Vec::new(); + let mut current = String::new(); + let chars: Vec = sql.chars().collect(); + let mut i = 0; + + while i < chars.len() { + let c = chars[i]; + + // Single-quoted string literal + if c == '\'' { + current.push(c); + i += 1; + while i < chars.len() { + current.push(chars[i]); + if chars[i] == '\'' { + // Check for escaped single quote '' + if i + 1 < chars.len() && chars[i + 1] == '\'' { + i += 1; + current.push(chars[i]); + } else { + break; + } + } + i += 1; + } + } + // Double-quoted identifier + else if c == '"' { + current.push(c); + i += 1; + while i < chars.len() { + current.push(chars[i]); + if chars[i] == '"' { + // Check for escaped double quote "" + if i + 1 < chars.len() && chars[i + 1] == '"' { + i += 1; + current.push(chars[i]); + } else { + break; + } + } + i += 1; + } + } + // Single-line comment + else if c == '-' && i + 1 < chars.len() && chars[i + 1] == '-' { + i += 2; + while i < chars.len() && chars[i] != '\n' { + i += 1; + } + } + // Block comment + else if c == '/' && i + 1 < chars.len() && chars[i + 1] == '*' { + i += 2; + while i + 1 < chars.len() { + if chars[i] == '*' && chars[i + 1] == '/' { + i += 2; + break; + } + i += 1; + } + } + // Statement separator + else if c == ';' { + let trimmed = current.trim().to_string(); + if !trimmed.is_empty() { + statements.push(trimmed); + } + current.clear(); + } else { + current.push(c); + } + + i += 1; + } + + // Last statement (after final semicolon or no trailing semicolon) + let trimmed = current.trim().to_string(); + if !trimmed.is_empty() { + statements.push(trimmed); + } + + statements +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_single_statement() { + let stmts = split_sql_statements("SELECT * FROM users"); + assert_eq!(stmts, vec!["SELECT * FROM users"]); + } + + #[test] + fn test_split_with_trailing_semicolon() { + let stmts = split_sql_statements("SELECT * FROM users;"); + assert_eq!(stmts, vec!["SELECT * FROM users"]); + } + + #[test] + fn test_split_multi_statement() { + let stmts = split_sql_statements( + "CREATE TABLE t (id INT); INSERT INTO t VALUES (1); SELECT * FROM t", + ); + assert_eq!( + stmts, + vec![ + "CREATE TABLE t (id INT)", + "INSERT INTO t VALUES (1)", + "SELECT * FROM t", + ] + ); + } + + #[test] + fn test_split_comment_statement() { + let stmts = split_sql_statements( + "CREATE TABLE t (id INT);\nCOMMENT ON TABLE t IS 'hello';", + ); + assert_eq!( + stmts, + vec![ + "CREATE TABLE t (id INT)", + "COMMENT ON TABLE t IS 'hello'", + ] + ); + } + + #[test] + fn test_split_with_semicolon_in_string() { + let stmts = split_sql_statements("SELECT 'hello;world' AS x"); + assert_eq!(stmts, vec!["SELECT 'hello;world' AS x"]); + } + + #[test] + fn test_skip_empty_statements() { + let stmts = split_sql_statements(";;SELECT 1;;;"); + assert_eq!(stmts, vec!["SELECT 1"]); + } + + #[test] + fn test_split_complex_ddl() { + let sql = "CREATE TABLE SYSDBA.CLASSES (\n class_id INT PRIMARY KEY\n);\nCOMMENT ON TABLE SYSDBA.CLASSES IS '班级信息表';\nCOMMENT ON COLUMN SYSDBA.CLASSES.class_id IS '班级ID';"; + let stmts = split_sql_statements(sql); + assert_eq!(stmts.len(), 3); + assert!(stmts[0].starts_with("CREATE TABLE")); + assert!(stmts[1].starts_with("COMMENT ON TABLE")); + assert!(stmts[2].starts_with("COMMENT ON COLUMN")); + } + + #[test] + fn test_split_dollar_sign_is_not_comment() { + let stmts = split_sql_statements("SELECT * FROM t WHERE x = 1; -- comment\nSELECT 2"); + assert_eq!(stmts.len(), 2); + assert_eq!(stmts[0], "SELECT * FROM t WHERE x = 1"); + assert_eq!(stmts[1], "SELECT 2"); + } +} diff --git a/src-tauri/src/database/jdbc_bridge/drivers.toml b/src-tauri/src/database/jdbc_bridge/drivers.toml index 7d1437df..ddb84795 100644 --- a/src-tauri/src/database/jdbc_bridge/drivers.toml +++ b/src-tauri/src/database/jdbc_bridge/drivers.toml @@ -143,13 +143,13 @@ version_error_signatures = [ "Incompatible driver version", ] -# ── 达梦 DM8 (Oracle mode) ── +# ── 达梦 Dameng DM8 (JDBC bridge) ── -[databases.dm8_oracle] -name = "达梦 DM8" +[databases.dameng] +name = "达梦 Dameng" class_name = "dm.jdbc.driver.DmDriver" maven_group = "com.dameng" -maven_artifact = "DmJdbcDriver" +maven_artifact = "DmJdbcDriver8" jdbc_url_template = "jdbc:dm://{host}:{port}" default_port = 5236 min_jre_version = "11" diff --git a/src-tauri/src/database/jdbc_bridge/fallback.rs b/src-tauri/src/database/jdbc_bridge/fallback.rs index 3549134a..c420d6be 100644 --- a/src-tauri/src/database/jdbc_bridge/fallback.rs +++ b/src-tauri/src/database/jdbc_bridge/fallback.rs @@ -351,7 +351,7 @@ fn db_type_from_config(config: &DatabaseDriverConfig) -> DatabaseType { "H2 Database" => DatabaseType::H2, "Apache Derby" => DatabaseType::Derby, "Snowflake" => DatabaseType::Snowflake, - "达梦 DM8" => DatabaseType::DM8Oracle, + "达梦 Dameng" => DatabaseType::Dameng, "虚谷 XuguDB" => DatabaseType::XuguDB, "GBase 8a" => DatabaseType::GBase8a, "Apache Hive" => DatabaseType::Hive, diff --git a/src-tauri/src/database/jdbc_bridge/registry.rs b/src-tauri/src/database/jdbc_bridge/registry.rs index bca0cfdd..46196e12 100644 --- a/src-tauri/src/database/jdbc_bridge/registry.rs +++ b/src-tauri/src/database/jdbc_bridge/registry.rs @@ -175,7 +175,7 @@ fn db_type_to_registry_key(db: DatabaseType) -> Option<&'static str> { DatabaseType::Derby => Some("derby"), DatabaseType::Snowflake => Some("snowflake"), DatabaseType::TDengine => Some("tdengine"), - DatabaseType::DM8Oracle => Some("dm8_oracle"), + DatabaseType::Dameng => Some("dameng"), DatabaseType::XuguDB => Some("xugudb"), DatabaseType::GBase8a => Some("gbase8a"), DatabaseType::Hive => Some("hive"), @@ -321,12 +321,12 @@ mod tests { #[test] fn test_build_jdbc_url_no_database_placeholder() { - // DM8 template has no {database} — should remain unchanged + // Dameng DM8 template has no {database} — should remain unchanged let config = DatabaseDriverConfig { - name: "DM8".into(), + name: "达梦 Dameng".into(), class_name: "dm.jdbc.driver.DmDriver".into(), maven_group: "com.dameng".into(), - maven_artifact: "DmJdbcDriver".into(), + maven_artifact: "DmJdbcDriver8".into(), jdbc_url_template: "jdbc:dm://{host}:{port}".into(), jdbc_url_template_service: None, default_port: Some(5236), diff --git a/src-tauri/src/database/strategy.rs b/src-tauri/src/database/strategy.rs index 847435a1..68631f1f 100644 --- a/src-tauri/src/database/strategy.rs +++ b/src-tauri/src/database/strategy.rs @@ -17,7 +17,6 @@ pub enum CoreDatabaseType { DB2, H2, Snowflake, - DM8Oracle, Trino, Presto, } @@ -54,7 +53,7 @@ pub fn resolve_effective_type(db: DatabaseType) -> ConnectionStrategy { // Native MySQL adapter MySQL => ConnectionStrategy::Native(CoreDatabaseType::MySQL), // MySQL wire protocol compat - MariaDB | TiDB | OceanBase | TDSQL | PolarDB | DM8 | Doris | SelectDB | StarRocks + MariaDB | TiDB | OceanBase | TDSQL | PolarDB | Doris | SelectDB | StarRocks | Databend | GoldenDB | ManticoreSearch | SingleStoreMemSQL | CloudSQLMySQL => { ConnectionStrategy::Native(CoreDatabaseType::MySQL) @@ -73,7 +72,7 @@ pub fn resolve_effective_type(db: DatabaseType) -> ConnectionStrategy { H2 => ConnectionStrategy::JdbcBridge, Snowflake => ConnectionStrategy::JdbcBridge, TDengine => ConnectionStrategy::JdbcBridge, - DM8Oracle => ConnectionStrategy::JdbcBridge, + Dameng => ConnectionStrategy::JdbcBridge, XuguDB => ConnectionStrategy::JdbcBridge, GBase8a => ConnectionStrategy::JdbcBridge, Derby => ConnectionStrategy::JdbcBridge, @@ -124,7 +123,7 @@ pub fn default_port(db: DatabaseType) -> Option { | AlloyDB | CloudSQLPG | FujitsuPG => Some(5432), QuestDB => Some(8812), YashanDB => Some(1688), - MySQL | MariaDB | TiDB | OceanBase | TDSQL | PolarDB | DM8 | GoldenDB + MySQL | MariaDB | TiDB | OceanBase | TDSQL | PolarDB | GoldenDB | SingleStoreMemSQL | CloudSQLMySQL => Some(3306), Doris | SelectDB | StarRocks => Some(9030), Databend => Some(3307), @@ -139,7 +138,7 @@ pub fn default_port(db: DatabaseType) -> Option { H2 => Some(9092), Snowflake => Some(443), TDengine => Some(6030), - DM8Oracle => Some(5236), + Dameng => Some(5236), XuguDB => Some(5138), GBase8a => Some(5258), Derby => Some(1527), @@ -257,7 +256,7 @@ mod tests { DatabaseType::H2, DatabaseType::Snowflake, DatabaseType::TDengine, - DatabaseType::DM8Oracle, + DatabaseType::Dameng, DatabaseType::XuguDB, DatabaseType::GBase8a, DatabaseType::Derby, @@ -305,7 +304,7 @@ mod tests { assert_eq!(default_port(DatabaseType::PostgreSQL), Some(5432)); assert_eq!(default_port(DatabaseType::MySQL), Some(3306)); assert_eq!(default_port(DatabaseType::SqlServer), Some(1433)); - assert_eq!(default_port(DatabaseType::DM8Oracle), Some(5236)); + assert_eq!(default_port(DatabaseType::Dameng), Some(5236)); assert_eq!(default_port(DatabaseType::Oracle), Some(1521)); assert_eq!(default_port(DatabaseType::Doris), Some(9030)); assert_eq!(default_port(DatabaseType::SelectDB), Some(9030)); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 77770ca2..0f67eeb7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -80,7 +80,6 @@ pub fn run() { use crate::connection::guardian::ConnectionGuardian; use state::AppState; - let app_state = Arc::new(AppState::new()); let store = commands::store::Store::new(); tauri::Builder::default() @@ -91,7 +90,7 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_window_state::Builder::default().build()) - .manage(app_state.clone()) + .manage(AppState::new()) .manage(store.clone()) .setup(move |app| { let handle = app.handle().clone(); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index c4f44a03..c022374f 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -117,8 +117,7 @@ impl ServerConfig { "h2" => Ok(DatabaseType::H2), "snowflake" => Ok(DatabaseType::Snowflake), "tdengine" | "td" => Ok(DatabaseType::TDengine), - "dm8" | "dm" => Ok(DatabaseType::DM8), - "dm8_oracle" => Ok(DatabaseType::DM8Oracle), + "dameng" | "dm" | "dm8" | "dm8_oracle" => Ok(DatabaseType::Dameng), "trino" => Ok(DatabaseType::Trino), "presto" => Ok(DatabaseType::Presto), "rqlite" => Ok(DatabaseType::RQLite), diff --git a/src/assets/images/database-icons/dm8oracle-logo.svg b/src/assets/images/database-icons/dm8oracle-logo.svg deleted file mode 100644 index 569aa6e7..00000000 --- a/src/assets/images/database-icons/dm8oracle-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/components/agent-message-bubble.vue b/src/components/agent-message-bubble.vue index 70cd8e03..706b1590 100644 --- a/src/components/agent-message-bubble.vue +++ b/src/components/agent-message-bubble.vue @@ -87,11 +87,13 @@ const activeToolName = computed(() => { ) }) -function resultStatus(tc: AgentToolCall): 'success' | 'error' | 'denied' { +function resultStatus(tc: AgentToolCall): 'success' | 'error' | 'denied' | 'executing' { if (tc.status === 'denied') return 'denied' if (tc.status === 'error') return 'error' + if (tc.status === 'executing') + return 'executing' return 'success' } @@ -360,7 +362,10 @@ function toolVerb(toolName: string, tc: AgentToolCall): string {