diff --git a/README.md b/README.md index 2177e66..1e6c522 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ - **自然语言查数**:基于 LLM + ReAct 工具调用,把自然语言转为 SQL 并在目标库执行,全程流式输出。 - **Python 数据分析**:对查询结果自动执行统计分析(相关性、回归、分布检验),补齐 SQL 在复杂统计计算上的短板。 - **多模型可切换**:内置 OpenAI / Ollama / 通义 DashScope / Anthropic 等提供商,换底座模型不影响已沉淀的业务知识。 -- **多数据源(JDBC 抽象)**:数据读取与执行完全基于 JDBC 标准 API,因此支持**任意 JDBC 兼容数据库**——MySQL / PostgreSQL / Oracle 已验证,ClickHouse / SQL Server / 达梦 / OceanBase / SQLite 等只需引入对应驱动即可接入。 +- **多数据源(JDBC 抽象)**:数据读取与执行完全基于 JDBC 标准 API,因此支持**任意 JDBC 兼容数据库**——内置类型已覆盖 MySQL / PostgreSQL / Oracle(已验证)与 ClickHouse / SQL Server / 达梦 / OceanBase / SQLite,其余 JDBC 兼容库扩展枚举即可接入。 + + > **默认仅内置 MySQL 驱动**。使用其他数据库前,需先在 `data-agent-backend/pom.xml` 中添加对应 JDBC 驱动依赖并重新构建后端,否则新增数据源时会提示「未找到数据库驱动」。类型列表与 Maven 坐标见 [docs/configuration.md](docs/configuration.md#4-查询数据源)。 - **语义层(无向量召回)**:以"域(Domain)"组织表,由 LLM 在工具调用时**推理出业务问题所属域、主动选表**,而非向量相似度召回——更精准、更稳定,也无需维护任何 embedding 索引。维度包括逻辑表 / 逻辑列 / 表关系 / 指标口径的业务映射。 - **会话式分析**:SSE 流式回答,会话历史可追溯、可调试。 - **报表生成**:内置报表工具与配套前端报表视图。 @@ -38,7 +40,7 @@ Data Agent 反其道而行——**不引入任何向量检索**。语义层把 - `SchemaReader` 完全基于 JDBC 标准 `DatabaseMetaData` 读取表 / 列 / 主键,不绑定任何数据库方言; - `SqlExecutor` 只使用 `Connection` / `PreparedStatement` / `ResultSet` 执行查询,并叠加 SELECT 校验、自动 `LIMIT` 等安全护栏。 -整条"读取表结构 → 执行查询"的路径都跑在 JDBC 标准 API 上,因此只要目标库**提供 JDBC 驱动**,Data Agent 就能接入。支持范围不局限于 MySQL / PostgreSQL / Oracle——ClickHouse、SQL Server、达梦、OceanBase、SQLite 等任意 JDBC 兼容数据库在理论上都可直接支持,引入对应驱动即可。 +整条"读取表结构 → 执行查询"的路径都跑在 JDBC 标准 API 上,因此只要目标库**提供 JDBC 驱动**,Data Agent 就能接入。目前已内置 MySQL / PostgreSQL / Oracle / ClickHouse / SQL Server / 达梦 / OceanBase / SQLite 八种类型(前端数据源下拉与后端枚举同步支持),其余任意 JDBC 兼容数据库扩展 `DataSourceType` 枚举即可接入。注意:**默认发布包仅内置 MySQL 驱动**,接入其他数据库前请先按 [docs/configuration.md](docs/configuration.md#4-查询数据源) 在 `data-agent-backend/pom.xml` 中引入对应驱动。 ## 🏗️ 架构速览 diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DataSourceType.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DataSourceType.java index 0e48e00..4e50247 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DataSourceType.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DataSourceType.java @@ -25,13 +25,39 @@ @Getter @AllArgsConstructor public enum DataSourceType { - MYSQL("mysql", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://"), - POSTGRESQL("postgresql", "org.postgresql.Driver", "jdbc:postgresql://"), - ORACLE("oracle", "oracle.jdbc.OracleDriver", "jdbc:oracle:thin:@"); + MYSQL("mysql", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://", "com.mysql:mysql-connector-j"), + POSTGRESQL( + "postgresql", + "org.postgresql.Driver", + "jdbc:postgresql://", + "org.postgresql:postgresql"), + ORACLE( + "oracle", + "oracle.jdbc.OracleDriver", + "jdbc:oracle:thin:@", + "com.oracle.database.jdbc:ojdbc11"), + CLICKHOUSE( + "clickhouse", + "com.clickhouse.jdbc.ClickHouseDriver", + "jdbc:clickhouse://", + "com.clickhouse:clickhouse-jdbc"), + SQLSERVER( + "sqlserver", + "com.microsoft.sqlserver.jdbc.SQLServerDriver", + "jdbc:sqlserver://", + "com.microsoft.sqlserver:mssql-jdbc"), + DAMENG("dameng", "dm.jdbc.driver.DmDriver", "jdbc:dm://", "com.dameng:DmJdbcDriver18"), + OCEANBASE( + "oceanbase", + "com.oceanbase.jdbc.Driver", + "jdbc:oceanbase://", + "com.oceanbase:oceanbase-client"), + SQLITE("sqlite", "org.sqlite.JDBC", "jdbc:sqlite:", "org.xerial:sqlite-jdbc"); private final String code; private final String driverClassName; private final String urlPrefix; + private final String mavenCoordinates; public static Optional fromCode(String code) { if (code == null) { @@ -44,9 +70,15 @@ public static Optional fromCode(String code) { public String buildJdbcUrl(String host, int port, String databaseName) { return switch (this) { - case MYSQL -> String.format("%s%s:%d/%s", urlPrefix, host, port, databaseName); - case POSTGRESQL -> String.format("%s%s:%d/%s", urlPrefix, host, port, databaseName); + case MYSQL, POSTGRESQL, CLICKHOUSE, DAMENG, OCEANBASE -> + String.format("%s%s:%d/%s", urlPrefix, host, port, databaseName); case ORACLE -> String.format("%s%s:%d:%s", urlPrefix, host, port, databaseName); + case SQLSERVER -> + String.format("%s%s:%d;databaseName=%s", urlPrefix, host, port, databaseName); + case SQLITE -> + throw new IllegalArgumentException( + "SQLite datasource does not support host/port-based URLs; please" + + " provide a connectionUrl like jdbc:sqlite:/path/to/db"); }; } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManager.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManager.java index b1f16eb..fdb22a9 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManager.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManager.java @@ -58,15 +58,51 @@ private HikariDataSource createDataSource(Datasource datasource) { String jdbcUrl = resolveJdbcUrl(datasource, type); - HikariConfig config = getHikariConfig(datasource, jdbcUrl, type); - - log.info( - "Creating datasource pool for [{}] type={} url={}", - datasource.getName(), - type.getCode(), - jdbcUrl); + // 注意:HikariConfig.setDriverClassName() 会同步加载驱动类,缺驱动时在此即抛异常, + // 因此 config 构建与池创建必须同在一个 try 内,才能被转译为可操作的缺驱动提示。 + try { + HikariConfig config = getHikariConfig(datasource, jdbcUrl, type); + + log.info( + "Creating datasource pool for [{}] type={} url={}", + datasource.getName(), + type.getCode(), + jdbcUrl); + + return new HikariDataSource(config); + } catch (RuntimeException e) { + throw translateInitializationFailure(type, e); + } catch (Error e) { + // 驱动依赖缺失时 JVM 抛的是 NoClassDefFoundError(Error 而非 Exception), + // 且 Error 会绕过 ToolExceptionMapper 的 catch(Exception),必须在此一并转译。 + throw translateInitializationFailure(type, e); + } + } - return new HikariDataSource(config); + /** + * 区分池初始化失败的两类原因:驱动未打包(给出 pom.xml 修复指引)与连接失败(原样抛出, + * 交由全局异常映射处理)。 + */ + private RuntimeException translateInitializationFailure( + DataSourceType type, Throwable exception) { + for (Throwable current = exception; current != null; current = current.getCause()) { + if (current instanceof ClassNotFoundException + || current instanceof NoClassDefFoundError) { + return BusinessException.of( + ErrorCode.JDBC_DRIVER_NOT_FOUND, + String.format( + "未找到数据库驱动 %s(%s 类型)。后端默认仅内置 MySQL 驱动," + + "请在 data-agent-backend/pom.xml 中添加依赖 %s 后重新构建并启动后端。", + type.getDriverClassName(), + type.getCode(), + type.getMavenCoordinates()), + exception); + } + } + if (exception instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new RuntimeException("Unexpected datasource initialization failure", exception); } private static HikariConfig getHikariConfig( diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java index ec5ec1e..399f5e2 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java @@ -62,6 +62,13 @@ public enum ErrorCode { HttpStatus.BAD_REQUEST, "Datasource type is not supported."), + /** 后端未打包对应数据库的 JDBC 驱动,需在 data-agent-backend/pom.xml 引入后重新构建。 */ + JDBC_DRIVER_NOT_FOUND( + "JDBC_DRIVER_NOT_FOUND", + HttpStatus.BAD_REQUEST, + "The JDBC driver for this database is not bundled in the backend. " + + "Please add it to data-agent-backend/pom.xml and rebuild."), + /** 逻辑关系类型非法或不受支持。 */ INVALID_RELATION_TYPE( "INVALID_RELATION_TYPE", HttpStatus.BAD_REQUEST, "Relation type is invalid."), diff --git a/data-agent-backend/src/test/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManagerTest.java b/data-agent-backend/src/test/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManagerTest.java new file mode 100644 index 0000000..ff72ceb --- /dev/null +++ b/data-agent-backend/src/test/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManagerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.agent.datasource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.exception.BusinessException; +import org.junit.jupiter.api.Test; + +/** 池初始化失败的转译逻辑:缺驱动必须给出可操作的 pom.xml 指引,而不是裸 INTERNAL_ERROR。 */ +class DynamicDataSourceManagerTest { + + private final DynamicDataSourceManager manager = new DynamicDataSourceManager(); + + @Test + void missingDriverYieldsActionableBusinessException() { + Datasource ds = new Datasource(); + ds.setId(1); + ds.setName("pg-without-driver"); + ds.setType("postgresql"); + ds.setHost("localhost"); + ds.setPort(5432); + ds.setDatabaseName("db"); + + BusinessException ex = + assertThrows(BusinessException.class, () -> manager.getOrCreateDataSource(ds)); + + assertEquals(ErrorCode.JDBC_DRIVER_NOT_FOUND, ex.getErrorCode()); + assertTrue(ex.getMessage().contains("org.postgresql:postgresql")); + assertTrue(ex.getMessage().contains("pom.xml")); + } +} diff --git a/data-agent-frontend/src/views/data-source/DataSource.vue b/data-agent-frontend/src/views/data-source/DataSource.vue index 0b6e464..978d568 100644 --- a/data-agent-frontend/src/views/data-source/DataSource.vue +++ b/data-agent-frontend/src/views/data-source/DataSource.vue @@ -16,7 +16,7 @@ -->