diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java deleted file mode 100644 index 2654bfe..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 io.github.malonetalk.entity.Datasource; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@AllArgsConstructor -public class SchemaReader { - - private final DynamicDataSourceManager dynamicDataSourceManager; - - public List getTableSchema(Datasource datasource, String tableName) { - javax.sql.DataSource ds = dynamicDataSourceManager.getOrCreateDataSource(datasource); - - try (Connection conn = ds.getConnection()) { - Set primaryKeys = getPrimaryKeys(conn, tableName); - return getColumns(conn, tableName, primaryKeys); - } catch (SQLException e) { - log.error("Failed to read schema for table {}: {}", tableName, e.getMessage(), e); - throw new SchemaReadException( - "Failed to read schema for table " + tableName + ": " + e.getMessage(), e); - } - } - - private Set getPrimaryKeys(Connection conn, String tableName) throws SQLException { - Set pkColumns = new HashSet<>(); - DatabaseMetaData metaData = conn.getMetaData(); - - try (ResultSet rs = - metaData.getPrimaryKeys(conn.getCatalog(), conn.getSchema(), tableName)) { - while (rs.next()) { - pkColumns.add(rs.getString("COLUMN_NAME")); - } - } - - return pkColumns; - } - - private List getColumns(Connection conn, String tableName, Set primaryKeys) - throws SQLException { - List columns = new ArrayList<>(); - DatabaseMetaData metaData = conn.getMetaData(); - - try (ResultSet rs = - metaData.getColumns(conn.getCatalog(), conn.getSchema(), tableName, null)) { - while (rs.next()) { - String columnName = rs.getString("COLUMN_NAME"); - String typeName = rs.getString("TYPE_NAME"); - int columnSize = rs.getInt("COLUMN_SIZE"); - String nullableStr = rs.getString("IS_NULLABLE"); - boolean nullable = "YES".equalsIgnoreCase(nullableStr); - String defaultValue = rs.getString("COLUMN_DEF"); - String remarks = rs.getString("REMARKS"); - boolean isPk = primaryKeys.contains(columnName); - - columns.add( - new ColumnInfo( - columnName, - typeName, - columnSize, - nullable, - defaultValue, - isPk, - remarks)); - } - } - - return columns; - } - - public static class SchemaReadException extends RuntimeException { - public SchemaReadException(String message, Throwable cause) { - super(message, cause); - } - } -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java index c7396bf..fcbabb0 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java @@ -19,12 +19,12 @@ import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; -import io.github.malonetalk.agent.datasource.QueryResult; -import io.github.malonetalk.agent.datasource.SqlExecutor; -import io.github.malonetalk.agent.datasource.SqlExecutor.SqlExecutionException; -import io.github.malonetalk.agent.datasource.SqlExecutor.SqlSecurityException; +import io.github.malonetalk.common.QueryResult; import io.github.malonetalk.entity.Datasource; import io.github.malonetalk.enums.Status; +import io.github.malonetalk.infrastructure.SqlExecutor; +import io.github.malonetalk.infrastructure.SqlExecutor.SqlExecutionException; +import io.github.malonetalk.infrastructure.SqlExecutor.SqlSecurityException; import io.github.malonetalk.service.DatasourceService; import java.util.List; import lombok.AllArgsConstructor; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java index d7c7032..85f1580 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java @@ -19,24 +19,20 @@ import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; -import io.github.malonetalk.agent.datasource.ColumnInfo; -import io.github.malonetalk.agent.datasource.SchemaReader; -import io.github.malonetalk.agent.datasource.SchemaReader.SchemaReadException; -import io.github.malonetalk.entity.Datasource; -import io.github.malonetalk.enums.Status; -import io.github.malonetalk.service.DatasourceService; -import java.util.List; -import lombok.AllArgsConstructor; +import io.github.malonetalk.common.ToolResult; +import io.github.malonetalk.dto.PageResponse; +import io.github.malonetalk.dto.semantic.TableSchemaSemanticPrompt; +import io.github.malonetalk.service.semantic.SemanticMergeService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component -@AllArgsConstructor +@RequiredArgsConstructor public class GetTableSchemaTool implements MarkAgentTool { - private final DatasourceService dataSourceService; - private final SchemaReader schemaReader; + private final SemanticMergeService semanticMergeService; @Tool( name = "get_table_schema", @@ -45,43 +41,30 @@ public class GetTableSchemaTool implements MarkAgentTool { + " type, whether it is primary key, whether it allows null, default value" + " and column comments. This tool should be called to understand the table" + " structure before generating SQL.") - public String getTableSchema( + public ToolResult getTableSchema( @ToolParam(name = "table_name", description = "The table name to query schema for") - String tableName) { - List activeDataSources = - dataSourceService.findByStatus(Status.ACTIVE.getCode()); - - if (activeDataSources.isEmpty()) { - return "No active datasource available, cannot get table schema."; - } - - if (activeDataSources.size() > 1) { - log.warn( - "Found {} active data sources, using the first one.", activeDataSources.size()); - } - - Datasource datasource = activeDataSources.get(0); - + String tableName, + @ToolParam( + name = "column_page", + description = "Optional column page number, default is 1") + Integer columnPage, + @ToolParam( + name = "column_page_size", + description = + "Optional column page size, default is 20, maximum is 100") + Integer columnPageSize) { try { - List columns = schemaReader.getTableSchema(datasource, tableName); - return formatSchema(tableName, columns); - } catch (SchemaReadException e) { - return "Failed to get table schema: " + e.getMessage(); + int resolvedPage = PageResponse.resolvePage(columnPage); + int resolvedPageSize = PageResponse.resolvePageSize(columnPageSize); + return ToolResult.success( + semanticMergeService.getTableSchema(tableName, resolvedPage, resolvedPageSize)); + } catch (IllegalStateException e) { + return ToolResult.error("Data source parsing failed", e.getMessage()); + } catch (IllegalArgumentException e) { + return ToolResult.error("Invalid arguments", e.getMessage()); + } catch (RuntimeException e) { + log.error("Failed to get schema for table {}: {}", tableName, e.getMessage(), e); + return ToolResult.error("Failed to retrieve schema", e.getMessage()); } } - - private String formatSchema(String tableName, List columns) { - if (columns.isEmpty()) { - return "Table " + tableName + " does not exist or has no column information."; - } - - StringBuilder sb = new StringBuilder(); - sb.append("Schema of table ").append(tableName).append(":\n"); - - for (ColumnInfo col : columns) { - sb.append(" - ").append(col.toString()).append("\n"); - } - - return sb.toString(); - } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java index c98eb91..dba4b7a 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java @@ -18,42 +18,42 @@ package io.github.malonetalk.agent.tools; import io.agentscope.core.tool.Tool; -import io.github.malonetalk.entity.Datasource; -import io.github.malonetalk.entity.TableInfo; -import io.github.malonetalk.enums.Status; -import io.github.malonetalk.service.DatasourceService; -import io.github.malonetalk.service.semantic.table.TableSemanticService; -import java.util.Collections; -import java.util.List; -import lombok.AllArgsConstructor; +import io.agentscope.core.tool.ToolParam; +import io.github.malonetalk.agent.tools.response.TablePromptResponse; +import io.github.malonetalk.common.ToolResult; +import io.github.malonetalk.dto.PageResponse; +import io.github.malonetalk.service.semantic.SemanticMergeService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component -@AllArgsConstructor +@RequiredArgsConstructor public class GetTablesTool implements MarkAgentTool { - private final DatasourceService dataSourceService; - private final TableSemanticService tableSemanticService; + private final SemanticMergeService semanticMergeService; @Tool(name = "get_tables", description = "获取数据库中的表信息,包括表名和表描述") - public List getTables() { - List activeDataSources = - dataSourceService.findByStatus(Status.ACTIVE.getCode()); - - if (activeDataSources.isEmpty()) { - return Collections.emptyList(); - } - - if (activeDataSources.size() > 1) { - log.warn( - "Found {} active data sources, using the first one. This may cause data" - + " inconsistency.", - activeDataSources.size()); + public ToolResult> getTables( + @ToolParam(name = "page", description = "Optional page number, default is 1") + Integer page, + @ToolParam( + name = "page_size", + description = "Optional page size, default is 20, maximum is 100") + Integer pageSize) { + try { + int resolvedPage = PageResponse.resolvePage(page); + int resolvedPageSize = PageResponse.resolvePageSize(pageSize); + return ToolResult.success( + semanticMergeService.getVisibleTablePromptPage(resolvedPage, resolvedPageSize)); + } catch (IllegalStateException e) { + return ToolResult.error("Data source parsing failed", e.getMessage()); + } catch (IllegalArgumentException e) { + return ToolResult.error("Invalid arguments", e.getMessage()); + } catch (RuntimeException e) { + log.error("Failed to retrieve visible tables: {}", e.getMessage(), e); + return ToolResult.error("Failed to retrieve tables", e.getMessage()); } - - Datasource dataSource = activeDataSources.get(0); - return tableSemanticService.listTableInfosByDatasourceId(dataSource.getId()); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/ColumnPromptResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/ColumnPromptResponse.java new file mode 100644 index 0000000..42dcd86 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/ColumnPromptResponse.java @@ -0,0 +1,26 @@ +/* + * 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.tools.response; + +public record ColumnPromptResponse( + String name, + String type, + Boolean primaryKey, + Boolean nullable, + String defaultValue, + String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/TablePromptResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/TablePromptResponse.java new file mode 100644 index 0000000..863317c --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/TablePromptResponse.java @@ -0,0 +1,23 @@ +/* + * 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.tools.response; + +import java.util.List; + +public record TablePromptResponse( + String name, String domain, String description, List relations) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/TableRelationResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/TableRelationResponse.java new file mode 100644 index 0000000..1da72c7 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/response/TableRelationResponse.java @@ -0,0 +1,29 @@ +/* + * 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.tools.response; + +import java.util.List; + +public record TableRelationResponse( + String relationType, + String source, + String sourceTableName, + List sourceColumnNames, + String targetTableName, + List targetColumnNames, + String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/QueryResult.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/QueryResult.java similarity index 98% rename from data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/QueryResult.java rename to data-agent-backend/src/main/java/io/github/malonetalk/common/QueryResult.java index e63d13b..520396d 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/QueryResult.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/QueryResult.java @@ -15,7 +15,7 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.agent.datasource; +package io.github.malonetalk.common; import java.util.ArrayList; import java.util.LinkedHashMap; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/common/ToolError.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/ToolError.java new file mode 100644 index 0000000..8514999 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/ToolError.java @@ -0,0 +1,20 @@ +/* + * 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.common; + +public record ToolError(String code, String message) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/common/ToolResult.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/ToolResult.java new file mode 100644 index 0000000..91edc3b --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/ToolResult.java @@ -0,0 +1,29 @@ +/* + * 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.common; + +public record ToolResult(boolean success, T data, ToolError error) { + + public static ToolResult success(T data) { + return new ToolResult<>(true, data, null); + } + + public static ToolResult error(String code, String message) { + return new ToolResult<>(false, null, new ToolError(code, message)); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ColumnSemanticController.java similarity index 97% rename from data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java rename to data-agent-backend/src/main/java/io/github/malonetalk/controller/ColumnSemanticController.java index d6bb3e3..4b8e1db 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableColumnSemanticController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ColumnSemanticController.java @@ -18,7 +18,7 @@ package io.github.malonetalk.controller; import io.github.malonetalk.common.Result; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.BatchResetColumnSemanticRequest; import io.github.malonetalk.dto.semantic.ColumnSemanticPageQuery; import io.github.malonetalk.dto.semantic.ColumnSemanticResponse; @@ -41,7 +41,7 @@ @RestController @RequestMapping("/api/semantic/tables/columns/{tableName}") @RequiredArgsConstructor -public class TableColumnSemanticController { +public class ColumnSemanticController { private final ColumnSemanticService columnSemanticService; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/RelationSemanticController.java similarity index 74% rename from data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java rename to data-agent-backend/src/main/java/io/github/malonetalk/controller/RelationSemanticController.java index bd3ffab..2273ed4 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableRelationSemanticController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/RelationSemanticController.java @@ -18,10 +18,12 @@ package io.github.malonetalk.controller; import io.github.malonetalk.common.Result; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.BatchDeleteLogicalTableRelationRequest; import io.github.malonetalk.dto.semantic.BindLogicalTableRelationRequest; import io.github.malonetalk.dto.semantic.LogicalTableRelationResponse; +import io.github.malonetalk.dto.semantic.RelationCandidateColumnResponse; +import io.github.malonetalk.dto.semantic.RelationCandidateTableResponse; import io.github.malonetalk.dto.semantic.RelationSemanticPageQuery; import io.github.malonetalk.dto.semantic.UpdateLogicalTableRelationEnabledRequest; import io.github.malonetalk.dto.semantic.UpdateLogicalTableRelationRequest; @@ -42,13 +44,13 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/semantic/tables/relations/{tableName}") +@RequestMapping("/api/semantic/tables") @RequiredArgsConstructor -public class TableRelationSemanticController { +public class RelationSemanticController { private final RelationSemanticService relationSemanticService; - @GetMapping + @GetMapping("/{tableName}/relations") public Result> listByTable( @PathVariable @NotBlank String tableName, @Valid RelationSemanticPageQuery query) { return Result.success( @@ -63,21 +65,21 @@ public Result> listByTable( query.sortOrder()))); } - @PostMapping + @PostMapping("/{tableName}/relations") public Result create( @PathVariable @NotBlank String tableName, @Valid @RequestBody BindLogicalTableRelationRequest request) { return Result.success(relationSemanticService.createRelationSemantic(tableName, request)); } - @PutMapping + @PutMapping("/{tableName}/relations") public Result update( @PathVariable @NotBlank String tableName, @Valid @RequestBody UpdateLogicalTableRelationRequest request) { return Result.success(relationSemanticService.updateRelationSemantic(tableName, request)); } - @PutMapping("/enabled") + @PutMapping("/{tableName}/relations/enabled") public Result updateEnabled( @PathVariable @NotBlank String tableName, @Valid @RequestBody UpdateLogicalTableRelationEnabledRequest request) { @@ -85,7 +87,7 @@ public Result updateEnabled( relationSemanticService.updateRelationSemanticEnabled(tableName, request)); } - @DeleteMapping("/{relationId}") + @DeleteMapping("/{tableName}/relations/{relationId}") public Result delete( @PathVariable @NotBlank String tableName, @PathVariable @NotNull @Min(1) Integer relationId, @@ -95,7 +97,7 @@ public Result delete( datasourceId, tableName, relationId)); } - @DeleteMapping("/batch") + @DeleteMapping("/{tableName}/relations/batch") public Result deleteBatch( @PathVariable @NotBlank String tableName, @Valid @RequestBody BatchDeleteLogicalTableRelationRequest request) { @@ -103,4 +105,25 @@ public Result deleteBatch( relationSemanticService.deleteRelationSemantics( request.datasourceId(), tableName, request.relationIds())); } + + @GetMapping("/relations/candidate/tables") + public Result> candidateTables( + @Valid RelationSemanticPageQuery query) { + return Result.success(relationSemanticService.getCandidateTablePage(query)); + } + + @GetMapping("/{tableName}/relations/candidate/columns") + public Result> candidateColumns( + @PathVariable @NotBlank String tableName, @Valid RelationSemanticPageQuery query) { + return Result.success( + relationSemanticService.getCandidateColumnPage( + new RelationSemanticPageQuery( + query.datasourceId(), + tableName, + query.page(), + query.pageSize(), + query.keyword(), + query.enabled(), + query.sortOrder()))); + } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java index 14dcb26..a2e1e53 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/TableSemanticController.java @@ -18,7 +18,7 @@ package io.github.malonetalk.controller; import io.github.malonetalk.common.Result; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.BatchResetTableSemanticRequest; import io.github.malonetalk.dto.semantic.TableSemanticPageQuery; import io.github.malonetalk.dto.semantic.TableSemanticResponse; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/ColumnSemanticConverter.java b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/ColumnSemanticConverter.java new file mode 100644 index 0000000..3bd182b --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/ColumnSemanticConverter.java @@ -0,0 +1,37 @@ +/* + * 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.convertor; + +import io.github.malonetalk.dto.semantic.ColumnSemanticResponse; +import io.github.malonetalk.entity.ColumnSemantic; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface ColumnSemanticConverter { + + @Mapping(target = "physicalColumnDescription", expression = "java(null)") + @Mapping(target = "typeName", expression = "java(null)") + @Mapping(target = "primaryKey", expression = "java(null)") + @Mapping(target = "hasPhysicalColumn", constant = "true") + @Mapping( + target = "effective", + expression = "java(Boolean.TRUE.equals(columnSemantic.getIsVisible()))") + @Mapping(target = "invalidReason", expression = "java(null)") + ColumnSemanticResponse toResponse(ColumnSemantic columnSemantic); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/LogicalTableRelationConverter.java b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/LogicalTableRelationConverter.java new file mode 100644 index 0000000..c690c8d --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/LogicalTableRelationConverter.java @@ -0,0 +1,61 @@ +/* + * 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.convertor; + +import io.github.malonetalk.dto.semantic.LogicalTableRelationResponse; +import io.github.malonetalk.entity.LogicalTableRelation; +import io.github.malonetalk.service.semantic.relation.LogicalTableRelationHelper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LogicalTableRelationConverter { + + private final LogicalTableRelationHelper logicalTableRelationHelper; + + public LogicalTableRelationResponse toResponse(LogicalTableRelation relation) { + List sourceColumns = + logicalTableRelationHelper.fromJson( + relation.getSourceColumnNamesJson(), "sourceColumnNames"); + List targetColumns = + logicalTableRelationHelper.fromJson( + relation.getTargetColumnNamesJson(), "targetColumnNames"); + return new LogicalTableRelationResponse( + relation.getId(), + logicalTableRelationHelper.buildRelationKey( + relation.getSourceTableName(), + sourceColumns, + relation.getTargetTableName(), + targetColumns), + relation.getDatasourceId(), + LogicalTableRelationHelper.RELATION_SOURCE_LOGICAL, + relation.getSourceTableName(), + sourceColumns, + relation.getTargetTableName(), + targetColumns, + relation.getRelationType(), + relation.getDescription(), + relation.getIsEnabled(), + Boolean.TRUE.equals(relation.getIsEnabled()), + null, + relation.getCreateTime(), + relation.getUpdateTime()); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/TableSemanticConverter.java b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/TableSemanticConverter.java new file mode 100644 index 0000000..aa8c58d --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/TableSemanticConverter.java @@ -0,0 +1,35 @@ +/* + * 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.convertor; + +import io.github.malonetalk.dto.semantic.TableSemanticResponse; +import io.github.malonetalk.entity.TableSemantic; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface TableSemanticConverter { + + @Mapping(target = "physicalTableDescription", expression = "java(null)") + @Mapping(target = "hasPhysicalTable", constant = "true") + @Mapping( + target = "effective", + expression = "java(Boolean.TRUE.equals(tableSemantic.getIsVisible()))") + @Mapping(target = "invalidReason", expression = "java(null)") + TableSemanticResponse toResponse(TableSemantic tableSemantic); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/pagination/PageResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/PageResponse.java similarity index 98% rename from data-agent-backend/src/main/java/io/github/malonetalk/dto/pagination/PageResponse.java rename to data-agent-backend/src/main/java/io/github/malonetalk/dto/PageResponse.java index e8ff6f8..113304b 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/pagination/PageResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/PageResponse.java @@ -15,7 +15,7 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.dto.pagination; +package io.github.malonetalk.dto; import java.util.Collections; import java.util.List; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/RelationCandidateColumnResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/RelationCandidateColumnResponse.java new file mode 100644 index 0000000..a201b1e --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/RelationCandidateColumnResponse.java @@ -0,0 +1,21 @@ +/* + * 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.dto.semantic; + +public record RelationCandidateColumnResponse( + String columnName, String description, String typeName, Boolean primaryKey) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/RelationCandidateTableResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/RelationCandidateTableResponse.java new file mode 100644 index 0000000..56f355f --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/RelationCandidateTableResponse.java @@ -0,0 +1,20 @@ +/* + * 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.dto.semantic; + +public record RelationCandidateTableResponse(String tableName, String domain, String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/TableSchemaSemanticPrompt.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/TableSchemaSemanticPrompt.java new file mode 100644 index 0000000..cd4ff63 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/TableSchemaSemanticPrompt.java @@ -0,0 +1,27 @@ +/* + * 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.dto.semantic; + +import io.github.malonetalk.agent.tools.response.ColumnPromptResponse; +import io.github.malonetalk.dto.PageResponse; + +public record TableSchemaSemanticPrompt( + String name, + String domain, + String description, + PageResponse columns) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/ColumnInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/Column.java similarity index 95% rename from data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/ColumnInfo.java rename to data-agent-backend/src/main/java/io/github/malonetalk/entity/Column.java index 142a8c0..20cd617 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/ColumnInfo.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/Column.java @@ -15,9 +15,9 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.agent.datasource; +package io.github.malonetalk.entity; -public record ColumnInfo( +public record Column( String columnName, String typeName, int columnSize, diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnSemantic.java similarity index 97% rename from data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java rename to data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnSemantic.java index 45bb532..811028f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnSemantic.java @@ -21,7 +21,7 @@ import lombok.Data; @Data -public class ColumnInfo { +public class ColumnSemantic { private Integer id; private Integer datasourceId; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/PhysicalTable.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/PhysicalTable.java new file mode 100644 index 0000000..60e4846 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/PhysicalTable.java @@ -0,0 +1,28 @@ +/* + * 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.entity; + +import lombok.Data; + +@Data +public class PhysicalTable { + + private String tableName; + private String tableDescription; + private Integer datasourceId; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableRelationInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableRelationInfo.java new file mode 100644 index 0000000..9e1a90f --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableRelationInfo.java @@ -0,0 +1,28 @@ +/* + * 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.entity; + +import java.util.List; + +public record TableRelationInfo( + String sourceTableName, + List sourceColumnNames, + String targetTableName, + List targetColumnNames, + String relationType, + String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableSemantic.java similarity index 97% rename from data-agent-backend/src/main/java/io/github/malonetalk/entity/TableInfo.java rename to data-agent-backend/src/main/java/io/github/malonetalk/entity/TableSemantic.java index cb2c16f..6a094de 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableInfo.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/TableSemantic.java @@ -21,7 +21,7 @@ import lombok.Data; @Data -public class TableInfo { +public class TableSemantic { private Integer id; private String tableName; 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/enums/DataSourceType.java similarity index 97% rename from data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DataSourceType.java rename to data-agent-backend/src/main/java/io/github/malonetalk/enums/DataSourceType.java index 0e48e00..e599f42 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/enums/DataSourceType.java @@ -15,7 +15,7 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.agent.datasource; +package io.github.malonetalk.enums; import java.util.Arrays; import java.util.Optional; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/exception/GlobalExceptionHandler.java b/data-agent-backend/src/main/java/io/github/malonetalk/exception/GlobalExceptionHandler.java index df0c536..e0cc323 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/exception/GlobalExceptionHandler.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/exception/GlobalExceptionHandler.java @@ -39,4 +39,11 @@ public Result handleBadRequest(Exception exception) { String message = exception.getMessage() == null ? "Bad request" : exception.getMessage(); return Result.error(400, message); } + + @ExceptionHandler(SemanticSchemaException.class) + public Result handleSemanticSchema(SemanticSchemaException exception) { + String message = + exception.getMessage() == null ? "Semantic schema error" : exception.getMessage(); + return Result.error(400, message); + } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/exception/SemanticSchemaException.java b/data-agent-backend/src/main/java/io/github/malonetalk/exception/SemanticSchemaException.java new file mode 100644 index 0000000..67c6cad --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/exception/SemanticSchemaException.java @@ -0,0 +1,29 @@ +/* + * 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.exception; + +public class SemanticSchemaException extends RuntimeException { + + public SemanticSchemaException(String message) { + super(message); + } + + public SemanticSchemaException(String message, Throwable cause) { + super(message, cause); + } +} 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/infrastructure/DynamicDataSourceManager.java similarity index 97% rename from data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/DynamicDataSourceManager.java rename to data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/DynamicDataSourceManager.java index 137150c..10579b2 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/infrastructure/DynamicDataSourceManager.java @@ -15,11 +15,12 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.agent.datasource; +package io.github.malonetalk.infrastructure; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.enums.DataSourceType; import jakarta.annotation.PreDestroy; import java.util.concurrent.ConcurrentHashMap; import javax.sql.DataSource; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/SchemaReader.java b/data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/SchemaReader.java new file mode 100644 index 0000000..aec6f19 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/SchemaReader.java @@ -0,0 +1,246 @@ +/* + * 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.infrastructure; + +import io.github.malonetalk.entity.Column; +import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.entity.PhysicalTable; +import io.github.malonetalk.entity.TableRelationInfo; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@AllArgsConstructor +public class SchemaReader { + + private final DynamicDataSourceManager dynamicDataSourceManager; + + public List getTableSchema(Datasource datasource, String tableName) { + javax.sql.DataSource ds = dynamicDataSourceManager.getOrCreateDataSource(datasource); + + try (Connection conn = ds.getConnection()) { + Set primaryKeys = getPrimaryKeys(conn, tableName); + return getColumns(conn, tableName, primaryKeys); + } catch (SQLException e) { + log.error("Failed to read schema for table {}: {}", tableName, e.getMessage(), e); + throw new SchemaReadException( + "Failed to read schema for table " + tableName + ": " + e.getMessage(), e); + } + } + + public List getTables(Datasource datasource) { + javax.sql.DataSource ds = dynamicDataSourceManager.getOrCreateDataSource(datasource); + try (Connection conn = ds.getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + List tables = new ArrayList<>(); + try (ResultSet rs = + metaData.getTables( + conn.getCatalog(), conn.getSchema(), null, new String[] {"TABLE"})) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + if (tableName == null || tableName.isBlank()) { + continue; + } + PhysicalTable tableInfo = new PhysicalTable(); + tableInfo.setTableName(tableName); + tableInfo.setTableDescription(normalizeBlankToNull(rs.getString("REMARKS"))); + tableInfo.setDatasourceId(datasource.getId()); + tables.add(tableInfo); + } + } + return tables; + } catch (SQLException e) { + log.error( + "Failed to read tables for datasource {}: {}", + datasource.getId(), + e.getMessage(), + e); + throw new SchemaReadException( + "Failed to read tables for datasource " + + datasource.getId() + + ": " + + e.getMessage(), + e); + } + } + + public List getImportedRelations(Datasource datasource, String tableName) { + javax.sql.DataSource ds = dynamicDataSourceManager.getOrCreateDataSource(datasource); + try (Connection conn = ds.getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + Map relationMap = new LinkedHashMap<>(); + try (ResultSet rs = + metaData.getImportedKeys(conn.getCatalog(), conn.getSchema(), tableName)) { + while (rs.next()) { + String sourceTable = rs.getString("FKTABLE_NAME"); + String sourceColumn = rs.getString("FKCOLUMN_NAME"); + String targetTable = rs.getString("PKTABLE_NAME"); + String targetColumn = rs.getString("PKCOLUMN_NAME"); + if (anyBlank(sourceTable, sourceColumn, targetTable, targetColumn)) { + continue; + } + String key = + buildRelationKey( + sourceTable, + sourceColumn, + targetTable, + targetColumn, + normalizeBlankToNull(rs.getString("FK_NAME"))); + TableRelationInfo existing = relationMap.get(key); + if (existing != null) { + List mergedSource = + mergeList(existing.sourceColumnNames(), sourceColumn); + List mergedTarget = + mergeList(existing.targetColumnNames(), targetColumn); + relationMap.put( + key, + new TableRelationInfo( + sourceTable, + mergedSource, + targetTable, + mergedTarget, + "foreign_key", + null)); + } else { + relationMap.put( + key, + new TableRelationInfo( + sourceTable, + List.of(sourceColumn), + targetTable, + List.of(targetColumn), + "foreign_key", + null)); + } + } + } + return List.copyOf(relationMap.values()); + } catch (SQLException e) { + log.error( + "Failed to read imported relations for table {} in datasource {}: {}", + tableName, + datasource.getId(), + e.getMessage(), + e); + throw new SchemaReadException( + "Failed to read imported relations for table " + + tableName + + ": " + + e.getMessage(), + e); + } + } + + private Set getPrimaryKeys(Connection conn, String tableName) throws SQLException { + Set pkColumns = new HashSet<>(); + DatabaseMetaData metaData = conn.getMetaData(); + + try (ResultSet rs = + metaData.getPrimaryKeys(conn.getCatalog(), conn.getSchema(), tableName)) { + while (rs.next()) { + pkColumns.add(rs.getString("COLUMN_NAME")); + } + } + + return pkColumns; + } + + private List getColumns(Connection conn, String tableName, Set primaryKeys) + throws SQLException { + List columns = new ArrayList<>(); + DatabaseMetaData metaData = conn.getMetaData(); + + try (ResultSet rs = + metaData.getColumns(conn.getCatalog(), conn.getSchema(), tableName, null)) { + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + String typeName = rs.getString("TYPE_NAME"); + int columnSize = rs.getInt("COLUMN_SIZE"); + String nullableStr = rs.getString("IS_NULLABLE"); + boolean nullable = "YES".equalsIgnoreCase(nullableStr); + String defaultValue = rs.getString("COLUMN_DEF"); + String remarks = rs.getString("REMARKS"); + boolean isPk = primaryKeys.contains(columnName); + + columns.add( + new Column( + columnName, + typeName, + columnSize, + nullable, + defaultValue, + isPk, + remarks)); + } + } + + return columns; + } + + private static String normalizeBlankToNull(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.trim(); + } + + private static boolean anyBlank(String... values) { + for (String value : values) { + if (value == null || value.isBlank()) { + return true; + } + } + return false; + } + + private static String buildRelationKey( + String sourceTable, + String sourceColumn, + String targetTable, + String targetColumn, + String foreignKeyName) { + if (foreignKeyName != null) { + return sourceTable + "|" + targetTable + "|" + foreignKeyName; + } + return sourceTable + "|" + sourceColumn + "|" + targetTable + "|" + targetColumn; + } + + private static List mergeList(List existing, String newValue) { + List merged = new ArrayList<>(existing); + merged.add(newValue); + return merged; + } + + public static class SchemaReadException extends RuntimeException { + public SchemaReadException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SqlExecutor.java b/data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/SqlExecutor.java similarity index 97% rename from data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SqlExecutor.java rename to data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/SqlExecutor.java index 855204e..89e762d 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SqlExecutor.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/infrastructure/SqlExecutor.java @@ -15,8 +15,9 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.agent.datasource; +package io.github.malonetalk.infrastructure; +import io.github.malonetalk.common.QueryResult; import io.github.malonetalk.entity.Datasource; import java.sql.Connection; import java.sql.PreparedStatement; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ColumnSemanticInfoMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ColumnSemanticMapper.java similarity index 70% rename from data-agent-backend/src/main/java/io/github/malonetalk/mapper/ColumnSemanticInfoMapper.java rename to data-agent-backend/src/main/java/io/github/malonetalk/mapper/ColumnSemanticMapper.java index 470e761..9e62522 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ColumnSemanticInfoMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ColumnSemanticMapper.java @@ -18,31 +18,35 @@ package io.github.malonetalk.mapper; import io.github.malonetalk.dto.semantic.ColumnSemanticPageQuery; -import io.github.malonetalk.entity.ColumnInfo; +import io.github.malonetalk.entity.ColumnSemantic; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper -public interface ColumnSemanticInfoMapper { +public interface ColumnSemanticMapper { - List selectByDatasourceId(@Param("datasourceId") Integer datasourceId); + List selectByDatasourceId(@Param("datasourceId") Integer datasourceId); - List selectByDatasourceIdAndTableName( + List selectByDatasourceIdAndTableName( @Param("datasourceId") Integer datasourceId, @Param("tableName") String tableName); - List selectPageByDatasourceIdAndTableName( + List selectPageByDatasourceIdAndTableName( @Param("query") ColumnSemanticPageQuery query, @Param("sortDescending") boolean sortDescending); - ColumnInfo selectByDatasourceIdAndTableNameAndColumnName( + List selectVisiblePageByDatasourceIdAndTableName( + @Param("query") ColumnSemanticPageQuery query, + @Param("sortDescending") boolean sortDescending); + + ColumnSemantic selectByDatasourceIdAndTableNameAndColumnName( @Param("datasourceId") Integer datasourceId, @Param("tableName") String tableName, @Param("columnName") String columnName); - int insert(ColumnInfo columnInfo); + int insert(ColumnSemantic columnSemantic); - int update(ColumnInfo columnInfo); + int update(ColumnSemantic columnSemantic); int deleteByDatasourceId(@Param("datasourceId") Integer datasourceId); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableSemanticMapper.java similarity index 70% rename from data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java rename to data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableSemanticMapper.java index 9c41458..7b32aea 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableSemanticMapper.java @@ -18,27 +18,31 @@ package io.github.malonetalk.mapper; import io.github.malonetalk.dto.semantic.TableSemanticPageQuery; -import io.github.malonetalk.entity.TableInfo; +import io.github.malonetalk.entity.TableSemantic; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper -public interface TableInfoMapper { +public interface TableSemanticMapper { - int insert(TableInfo tableInfo); + int insert(TableSemantic tableSemantic); - int update(TableInfo tableInfo); + int update(TableSemantic tableSemantic); int deleteByDatasourceIdAndIds( @Param("datasourceId") Integer datasourceId, @Param("ids") List ids); - List selectByDatasourceId(@Param("datasourceId") Integer datasourceId); + List selectByDatasourceId(@Param("datasourceId") Integer datasourceId); - List selectPageByDatasourceId( + List selectPageByDatasourceId( @Param("query") TableSemanticPageQuery query, @Param("sortDescending") boolean sortDescending); - TableInfo selectByDatasourceIdAndTableName( + List selectVisiblePageByDatasourceId( + @Param("query") TableSemanticPageQuery query, + @Param("sortDescending") boolean sortDescending); + + TableSemantic selectByDatasourceIdAndTableName( @Param("datasourceId") Integer datasourceId, @Param("tableName") String tableName); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java index 03e1b02..8fe3b6d 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java @@ -37,4 +37,6 @@ public interface DatasourceService { List findByType(String type); boolean updateStatus(Integer id, String status); + + Datasource requireActiveDatasource(); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java index b3b6b87..ca75a63 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java @@ -18,12 +18,15 @@ package io.github.malonetalk.service; import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.enums.Status; import io.github.malonetalk.mapper.DatasourceMapper; import java.time.LocalDateTime; import java.util.List; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +@Slf4j @Service @AllArgsConstructor public class DatasourceServiceImpl implements DatasourceService { @@ -72,4 +75,20 @@ public List findByType(String type) { public boolean updateStatus(Integer id, String status) { return dataSourceMapper.updateStatus(id, status) > 0; } + + @Override + public Datasource requireActiveDatasource() { + List activeDataSources = + dataSourceMapper.selectByStatus(Status.ACTIVE.getCode()); + if (activeDataSources.isEmpty()) { + throw new IllegalStateException("没有活跃的数据源,请先激活一个数据源。"); + } + if (activeDataSources.size() > 1) { + List activeIds = activeDataSources.stream().map(Datasource::getId).toList(); + String message = "存在多个活跃数据源 " + activeIds + ",请仅保留一个活跃数据源。"; + log.error("发现 {} 个活跃数据源,拒绝隐式选择。activeIds={}", activeDataSources.size(), activeIds); + throw new IllegalStateException(message); + } + return activeDataSources.get(0); + } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java new file mode 100644 index 0000000..90080e0 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java @@ -0,0 +1,319 @@ +/* + * 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.service.semantic; + +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import io.github.malonetalk.agent.tools.response.ColumnPromptResponse; +import io.github.malonetalk.agent.tools.response.TablePromptResponse; +import io.github.malonetalk.agent.tools.response.TableRelationResponse; +import io.github.malonetalk.dto.PageResponse; +import io.github.malonetalk.dto.semantic.ColumnSemanticPageQuery; +import io.github.malonetalk.dto.semantic.TableSchemaSemanticPrompt; +import io.github.malonetalk.dto.semantic.TableSemanticPageQuery; +import io.github.malonetalk.entity.Column; +import io.github.malonetalk.entity.ColumnSemantic; +import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.entity.LogicalTableRelation; +import io.github.malonetalk.entity.TableRelationInfo; +import io.github.malonetalk.entity.TableSemantic; +import io.github.malonetalk.infrastructure.SchemaReader; +import io.github.malonetalk.mapper.ColumnSemanticMapper; +import io.github.malonetalk.mapper.LogicalTableRelationMapper; +import io.github.malonetalk.mapper.TableSemanticMapper; +import io.github.malonetalk.service.DatasourceService; +import io.github.malonetalk.service.semantic.relation.LogicalTableRelationHelper; +import io.github.malonetalk.utils.SemanticUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SemanticMergeService { + + private final DatasourceService datasourceService; + private final SchemaReader schemaReader; + private final TableSemanticMapper tableSemanticMapper; + private final ColumnSemanticMapper columnSemanticMapper; + private final LogicalTableRelationMapper logicalTableRelationMapper; + private final LogicalTableRelationHelper logicalTableRelationHelper; + + public PageResponse getVisibleTablePromptPage(int page, int pageSize) { + Datasource datasource = datasourceService.requireActiveDatasource(); + PageHelper.startPage(page, pageSize); + Page pageResult = + (Page) + tableSemanticMapper.selectVisiblePageByDatasourceId( + new TableSemanticPageQuery( + datasource.getId(), page, pageSize, null, "asc"), + false); + + Map> logicalRelationsBySource = + buildLogicalRelationsBySource(datasource.getId()); + + List items = + pageResult.stream() + .map( + t -> + new TablePromptResponse( + t.getTableName(), + SemanticUtils.normalizeBlankToNull(t.getDomain()), + SemanticUtils.normalizeBlankToNull( + t.getTableDescription()), + resolveVisibleRelations( + t.getTableName(), + logicalRelationsBySource, + datasource))) + .toList(); + + return PageResponse.of(items, pageResult.getTotal(), page, pageSize); + } + + public TableSchemaSemanticPrompt getTableSchema(String tableName, int page, int pageSize) { + Datasource datasource = datasourceService.requireActiveDatasource(); + String normalizedTableName = SemanticUtils.requireName(tableName, "tableName"); + + TableSemantic semanticTable = + tableSemanticMapper.selectByDatasourceIdAndTableName( + datasource.getId(), normalizedTableName); + if (semanticTable == null || !Boolean.TRUE.equals(semanticTable.getIsVisible())) { + throw new IllegalArgumentException("表 " + normalizedTableName + " 不存在或不可见。"); + } + + Map physicalByKey = new HashMap<>(); + try { + for (Column col : schemaReader.getTableSchema(datasource, normalizedTableName)) { + physicalByKey.put(col.columnName().toLowerCase(Locale.ROOT), col); + } + } catch (SchemaReader.SchemaReadException e) { + throw new IllegalArgumentException( + "无法读取表 " + normalizedTableName + " 的 Schema: " + e.getMessage(), e); + } + if (physicalByKey.isEmpty()) { + throw new IllegalArgumentException("表 " + normalizedTableName + " 不存在或没有可读列。"); + } + + PageHelper.startPage(page, pageSize); + Page pageResult = + (Page) + columnSemanticMapper.selectVisiblePageByDatasourceIdAndTableName( + new ColumnSemanticPageQuery( + datasource.getId(), + normalizedTableName, + page, + pageSize, + null, + "asc"), + false); + + List items = + pageResult.stream().map(c -> mapColumnPrompt(c, physicalByKey)).toList(); + + String description = + SemanticUtils.normalizeBlankToNull(semanticTable.getTableDescription()); + String domain = SemanticUtils.normalizeBlankToNull(semanticTable.getDomain()); + + PageResponse columnPage = + PageResponse.of(items, pageResult.getTotal(), page, pageSize); + return new TableSchemaSemanticPrompt(normalizedTableName, domain, description, columnPage); + } + + private List resolveVisibleRelations( + String sourceTableName, + Map> logicalRelationsBySource, + Datasource datasource) { + LinkedHashMap merged = new LinkedHashMap<>(); + + // 先添加物理 FK 关系 + List physicalRelations = + schemaReader.getImportedRelations(datasource, sourceTableName); + for (TableRelationInfo phys : physicalRelations) { + if (!areEndpointsVisible(datasource.getId(), phys)) { + continue; + } + String key = + buildRelationMergeKey( + phys.sourceTableName(), + phys.sourceColumnNames(), + phys.targetTableName(), + phys.targetColumnNames()); + merged.put( + key, + new TableRelationResponse( + phys.relationType(), + "physical", + phys.sourceTableName(), + phys.sourceColumnNames(), + phys.targetTableName(), + phys.targetColumnNames(), + phys.description())); + } + + // 逻辑关系覆盖物理关系 + List logicalRelations = + logicalRelationsBySource.getOrDefault( + sourceTableName.toLowerCase(Locale.ROOT), Collections.emptyList()); + for (LogicalTableRelation relation : logicalRelations) { + if (!Boolean.TRUE.equals(relation.getIsEnabled())) { + continue; + } + List sourceColumns; + List targetColumns; + try { + sourceColumns = + logicalTableRelationHelper.fromJson( + relation.getSourceColumnNamesJson(), "sourceColumnNames"); + targetColumns = + logicalTableRelationHelper.fromJson( + relation.getTargetColumnNamesJson(), "targetColumnNames"); + } catch (IllegalArgumentException e) { + log.warn("跳过无效的逻辑关系 id={}: {}", relation.getId(), e.getMessage()); + continue; + } + String key = + buildRelationMergeKey( + relation.getSourceTableName(), + sourceColumns, + relation.getTargetTableName(), + targetColumns); + merged.put( + key, + new TableRelationResponse( + relation.getRelationType(), + LogicalTableRelationHelper.RELATION_SOURCE_LOGICAL, + relation.getSourceTableName(), + sourceColumns, + relation.getTargetTableName(), + targetColumns, + relation.getDescription())); + } + + return List.copyOf(merged.values()); + } + + private boolean areEndpointsVisible(Integer datasourceId, TableRelationInfo relation) { + TableSemantic sourceTable = + tableSemanticMapper.selectByDatasourceIdAndTableName( + datasourceId, relation.sourceTableName()); + if (sourceTable != null && !Boolean.TRUE.equals(sourceTable.getIsVisible())) { + return false; + } + TableSemantic targetTable = + tableSemanticMapper.selectByDatasourceIdAndTableName( + datasourceId, relation.targetTableName()); + if (targetTable != null && !Boolean.TRUE.equals(targetTable.getIsVisible())) { + return false; + } + for (String colName : relation.sourceColumnNames()) { + ColumnSemantic col = + columnSemanticMapper.selectByDatasourceIdAndTableNameAndColumnName( + datasourceId, relation.sourceTableName(), colName); + if (col != null && !Boolean.TRUE.equals(col.getIsVisible())) { + return false; + } + } + for (String colName : relation.targetColumnNames()) { + ColumnSemantic col = + columnSemanticMapper.selectByDatasourceIdAndTableNameAndColumnName( + datasourceId, relation.targetTableName(), colName); + if (col != null && !Boolean.TRUE.equals(col.getIsVisible())) { + return false; + } + } + return true; + } + + private String buildRelationMergeKey( + String sourceTable, + List sourceColumns, + String targetTable, + List targetColumns) { + String sourceColSig = + sourceColumns.stream() + .map(c -> c.toLowerCase(Locale.ROOT)) + .sorted() + .reduce((a, b) -> a + "|" + b) + .orElse(""); + String targetColSig = + targetColumns.stream() + .map(c -> c.toLowerCase(Locale.ROOT)) + .sorted() + .reduce((a, b) -> a + "|" + b) + .orElse(""); + return sourceTable.toLowerCase(Locale.ROOT) + + "|" + + sourceColSig + + "|" + + targetTable.toLowerCase(Locale.ROOT) + + "|" + + targetColSig; + } + + private ColumnPromptResponse mapColumnPrompt( + ColumnSemantic semanticColumn, Map physicalByKey) { + Column physicalColumn = + physicalByKey.get(semanticColumn.getColumnName().toLowerCase(Locale.ROOT)); + String description = + SemanticUtils.normalizeBlankToNull(semanticColumn.getColumnDescription()); + if (description == null && physicalColumn != null) { + description = SemanticUtils.normalizeBlankToNull(physicalColumn.remarks()); + } + String typeText = ""; + boolean primaryKey = false; + boolean nullable = true; + String defaultValue = null; + if (physicalColumn != null) { + StringBuilder typeBuilder = new StringBuilder(physicalColumn.typeName()); + if (physicalColumn.columnSize() > 0) { + typeBuilder.append("(").append(physicalColumn.columnSize()).append(")"); + } + typeText = typeBuilder.toString(); + primaryKey = physicalColumn.primaryKey(); + nullable = physicalColumn.nullable(); + defaultValue = SemanticUtils.normalizeBlankToNull(physicalColumn.defaultValue()); + } + return new ColumnPromptResponse( + semanticColumn.getColumnName(), + typeText, + primaryKey, + nullable, + defaultValue, + description); + } + + private Map> buildLogicalRelationsBySource( + Integer datasourceId) { + List allRelations = + logicalTableRelationMapper.selectByDatasourceId(datasourceId); + Map> result = new HashMap<>(); + for (LogicalTableRelation relation : allRelations) { + String key = relation.getSourceTableName().toLowerCase(Locale.ROOT); + result.computeIfAbsent(key, k -> new ArrayList<>()).add(relation); + } + return result; + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticService.java index d014abe..cd08abd 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticService.java @@ -17,7 +17,7 @@ */ package io.github.malonetalk.service.semantic.column; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.ColumnSemanticPageQuery; import io.github.malonetalk.dto.semantic.ColumnSemanticResponse; import io.github.malonetalk.dto.semantic.ColumnSemanticUpdateRequest; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticServiceImpl.java index 295f1e2..5e1eb12 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/column/ColumnSemanticServiceImpl.java @@ -20,12 +20,13 @@ import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.github.malonetalk.common.SemanticConstants; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.convertor.ColumnSemanticConverter; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.ColumnSemanticPageQuery; import io.github.malonetalk.dto.semantic.ColumnSemanticResponse; import io.github.malonetalk.dto.semantic.ColumnSemanticUpdateRequest; -import io.github.malonetalk.entity.ColumnInfo; -import io.github.malonetalk.mapper.ColumnSemanticInfoMapper; +import io.github.malonetalk.entity.ColumnSemantic; +import io.github.malonetalk.mapper.ColumnSemanticMapper; import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.utils.SemanticUtils; import java.time.LocalDateTime; @@ -41,7 +42,8 @@ public class ColumnSemanticServiceImpl implements ColumnSemanticService { private final DatasourceService datasourceService; - private final ColumnSemanticInfoMapper columnSemanticInfoMapper; + private final ColumnSemanticMapper columnSemanticMapper; + private final ColumnSemanticConverter columnSemanticConverter; @Override public PageResponse getColumnPage(ColumnSemanticPageQuery query) { @@ -56,9 +58,9 @@ public PageResponse getColumnPage(ColumnSemanticPageQuer boolean sortDescending = SemanticConstants.SORT_ORDER_DESC.equalsIgnoreCase(query.sortOrder()); PageHelper.startPage(pageNumber, pageSize); - Page page = - (Page) - columnSemanticInfoMapper.selectPageByDatasourceIdAndTableName( + Page page = + (Page) + columnSemanticMapper.selectPageByDatasourceIdAndTableName( new ColumnSemanticPageQuery( query.datasourceId(), normalizedTableName, @@ -67,7 +69,8 @@ public PageResponse getColumnPage(ColumnSemanticPageQuer SemanticUtils.normalizeBlankToNull(query.keyword()), query.sortOrder()), sortDescending); - List responses = page.stream().map(this::mapResponse).toList(); + List responses = + page.stream().map(columnSemanticConverter::toResponse).toList(); return PageResponse.of(responses, page.getTotal(), pageNumber, pageSize); } @@ -76,20 +79,20 @@ public void updateColumnSemantic(String tableName, ColumnSemanticUpdateRequest r requireDatasource(request.datasourceId()); String normalizedTableName = SemanticUtils.requireName(tableName, "tableName"); String normalizedColumnName = SemanticUtils.requireName(request.columnName(), "columnName"); - ColumnInfo existing = - columnSemanticInfoMapper.selectByDatasourceIdAndTableNameAndColumnName( + ColumnSemantic existing = + columnSemanticMapper.selectByDatasourceIdAndTableNameAndColumnName( request.datasourceId(), normalizedTableName, normalizedColumnName); if (existing == null) { - ColumnInfo columnInfo = new ColumnInfo(); - columnInfo.setDatasourceId(request.datasourceId()); - columnInfo.setTableName(normalizedTableName); - columnInfo.setColumnName(normalizedColumnName); - columnInfo.setColumnDescription( + ColumnSemantic columnSemantic = new ColumnSemantic(); + columnSemantic.setDatasourceId(request.datasourceId()); + columnSemantic.setTableName(normalizedTableName); + columnSemantic.setColumnName(normalizedColumnName); + columnSemantic.setColumnDescription( SemanticUtils.normalizeBlankToNull(request.columnDescription())); - columnInfo.setIsVisible(request.isVisible()); - columnInfo.setCreateTime(LocalDateTime.now()); - columnInfo.setUpdateTime(LocalDateTime.now()); - columnSemanticInfoMapper.insert(columnInfo); + columnSemantic.setIsVisible(request.isVisible()); + columnSemantic.setCreateTime(LocalDateTime.now()); + columnSemantic.setUpdateTime(LocalDateTime.now()); + columnSemanticMapper.insert(columnSemantic); return; } existing.setTableName(normalizedTableName); @@ -98,20 +101,19 @@ public void updateColumnSemantic(String tableName, ColumnSemanticUpdateRequest r SemanticUtils.normalizeBlankToNull(request.columnDescription())); existing.setIsVisible(request.isVisible()); existing.setUpdateTime(LocalDateTime.now()); - columnSemanticInfoMapper.update(existing); + columnSemanticMapper.update(existing); } @Override public void resetColumnSemantic(Integer datasourceId, String tableName, String columnName) { requireDatasource(datasourceId); - ColumnInfo existing = - columnSemanticInfoMapper.selectByDatasourceIdAndTableNameAndColumnName( + ColumnSemantic existing = + columnSemanticMapper.selectByDatasourceIdAndTableNameAndColumnName( datasourceId, tableName, columnName); if (existing == null) { throw new IllegalArgumentException("Column semantic metadata does not exist."); } - columnSemanticInfoMapper.deleteByDatasourceIdAndIds( - datasourceId, List.of(existing.getId())); + columnSemanticMapper.deleteByDatasourceIdAndIds(datasourceId, List.of(existing.getId())); } @Override @@ -131,14 +133,14 @@ public int resetColumnSemantics( columnName, "columnName"))) .collect(Collectors.toCollection(java.util.LinkedHashSet::new)); List matchedIds = - columnSemanticInfoMapper + columnSemanticMapper .selectByDatasourceIdAndTableName(datasourceId, normalizedTableName) .stream() .filter( column -> normalizedColumnNames.contains( normalizeKey(column.getColumnName()))) - .map(ColumnInfo::getId) + .map(ColumnSemantic::getId) .distinct() .toList(); if (matchedIds.isEmpty()) { @@ -150,7 +152,7 @@ public int resetColumnSemantics( + normalizedTableName + "."); } - return columnSemanticInfoMapper.deleteByDatasourceIdAndIds(datasourceId, matchedIds); + return columnSemanticMapper.deleteByDatasourceIdAndIds(datasourceId, matchedIds); } private void requireDatasource(Integer datasourceId) { @@ -160,21 +162,6 @@ private void requireDatasource(Integer datasourceId) { } } - private ColumnSemanticResponse mapResponse(ColumnInfo columnInfo) { - return new ColumnSemanticResponse( - columnInfo.getId(), - columnInfo.getColumnName(), - null, - columnInfo.getColumnDescription(), - null, - null, - columnInfo.getIsVisible(), - true, - Boolean.TRUE.equals(columnInfo.getIsVisible()), - null, - columnInfo.getUpdateTime()); - } - private String normalizeKey(String value) { return value.trim().toLowerCase(Locale.ROOT); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticService.java index bafecff..96c3916 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticService.java @@ -17,9 +17,11 @@ */ package io.github.malonetalk.service.semantic.relation; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.BindLogicalTableRelationRequest; import io.github.malonetalk.dto.semantic.LogicalTableRelationResponse; +import io.github.malonetalk.dto.semantic.RelationCandidateColumnResponse; +import io.github.malonetalk.dto.semantic.RelationCandidateTableResponse; import io.github.malonetalk.dto.semantic.RelationSemanticPageQuery; import io.github.malonetalk.dto.semantic.UpdateLogicalTableRelationEnabledRequest; import io.github.malonetalk.dto.semantic.UpdateLogicalTableRelationRequest; @@ -41,4 +43,10 @@ boolean updateRelationSemanticEnabled( boolean deleteRelationSemantic(Integer datasourceId, String tableName, Integer relationId); int deleteRelationSemantics(Integer datasourceId, String tableName, List relationIds); + + PageResponse getCandidateTablePage( + RelationSemanticPageQuery query); + + PageResponse getCandidateColumnPage( + RelationSemanticPageQuery query); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticServiceImpl.java index 4ebaf41..28f7273 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/relation/RelationSemanticServiceImpl.java @@ -20,22 +20,40 @@ import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.github.malonetalk.common.SemanticConstants; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.convertor.LogicalTableRelationConverter; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.BindLogicalTableRelationRequest; +import io.github.malonetalk.dto.semantic.ColumnSemanticPageQuery; import io.github.malonetalk.dto.semantic.LogicalTableRelationResponse; +import io.github.malonetalk.dto.semantic.RelationCandidateColumnResponse; +import io.github.malonetalk.dto.semantic.RelationCandidateTableResponse; import io.github.malonetalk.dto.semantic.RelationSemanticPageQuery; +import io.github.malonetalk.dto.semantic.TableSemanticPageQuery; import io.github.malonetalk.dto.semantic.UpdateLogicalTableRelationEnabledRequest; import io.github.malonetalk.dto.semantic.UpdateLogicalTableRelationRequest; +import io.github.malonetalk.entity.Column; +import io.github.malonetalk.entity.ColumnSemantic; +import io.github.malonetalk.entity.Datasource; import io.github.malonetalk.entity.LogicalTableRelation; +import io.github.malonetalk.entity.TableSemantic; +import io.github.malonetalk.exception.SemanticSchemaException; +import io.github.malonetalk.infrastructure.SchemaReader; +import io.github.malonetalk.mapper.ColumnSemanticMapper; import io.github.malonetalk.mapper.LogicalTableRelationMapper; +import io.github.malonetalk.mapper.TableSemanticMapper; import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.utils.SemanticUtils; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class RelationSemanticServiceImpl implements RelationSemanticService { @@ -43,6 +61,10 @@ public class RelationSemanticServiceImpl implements RelationSemanticService { private final DatasourceService datasourceService; private final LogicalTableRelationMapper logicalTableRelationMapper; private final LogicalTableRelationHelper logicalTableRelationHelper; + private final LogicalTableRelationConverter logicalTableRelationConverter; + private final TableSemanticMapper tableSemanticMapper; + private final ColumnSemanticMapper columnSemanticMapper; + private final SchemaReader schemaReader; @Override public PageResponse getRelationPage( @@ -71,7 +93,8 @@ public PageResponse getRelationPage( if (page.getTotal() == 0L) { return PageResponse.empty(pageNumber, pageSize); } - List items = page.stream().map(this::mapResponse).toList(); + List items = + page.stream().map(logicalTableRelationConverter::toResponse).toList(); return PageResponse.of(items, page.getTotal(), pageNumber, pageSize); } @@ -80,6 +103,12 @@ public PageResponse getRelationPage( public LogicalTableRelationResponse createRelationSemantic( String tableName, BindLogicalTableRelationRequest request) { requireDatasource(request.datasourceId()); + validateEndpoints( + request.datasourceId(), + tableName, + request.sourceColumnNames(), + request.targetTableName(), + request.targetColumnNames()); LogicalTableRelation relation = buildRelation(request.datasourceId(), tableName, request); ensureUniqueSourceKey( request.datasourceId(), @@ -87,7 +116,7 @@ public LogicalTableRelationResponse createRelationSemantic( relation.getSourceColumnSignature(), null); logicalTableRelationMapper.insert(relation); - return mapResponse(relation); + return logicalTableRelationConverter.toResponse(relation); } @Override @@ -95,6 +124,12 @@ public LogicalTableRelationResponse createRelationSemantic( public LogicalTableRelationResponse updateRelationSemantic( String tableName, UpdateLogicalTableRelationRequest request) { requireDatasource(request.datasourceId()); + validateEndpoints( + request.datasourceId(), + tableName, + request.sourceColumnNames(), + request.targetTableName(), + request.targetColumnNames()); LogicalTableRelation existing = requireRelation(request.datasourceId(), tableName, request.relationId()); applyRelationUpdate(existing, tableName, request); @@ -105,7 +140,7 @@ public LogicalTableRelationResponse updateRelationSemantic( existing.getId()); existing.setUpdateTime(LocalDateTime.now()); logicalTableRelationMapper.update(existing); - return mapResponse(existing); + return logicalTableRelationConverter.toResponse(existing); } @Override @@ -166,6 +201,108 @@ public int deleteRelationSemantics( datasourceId, normalizedTableName, relationIds); } + @Override + public PageResponse getCandidateTablePage( + RelationSemanticPageQuery query) { + requireDatasource(query.datasourceId()); + int pageNumber = PageResponse.resolvePage(query.page()); + int pageSize = PageResponse.resolvePageSize(query.pageSize()); + SemanticUtils.validateSortOrder(query.sortOrder()); + boolean sortDescending = + SemanticConstants.SORT_ORDER_DESC.equalsIgnoreCase(query.sortOrder()); + PageHelper.startPage(pageNumber, pageSize); + Page page = + (Page) + tableSemanticMapper.selectVisiblePageByDatasourceId( + new TableSemanticPageQuery( + query.datasourceId(), + pageNumber, + pageSize, + SemanticUtils.normalizeBlankToNull(query.keyword()), + query.sortOrder()), + sortDescending); + List items = + page.stream() + .map( + t -> + new RelationCandidateTableResponse( + t.getTableName(), + SemanticUtils.normalizeBlankToNull(t.getDomain()), + SemanticUtils.normalizeBlankToNull( + t.getTableDescription()))) + .toList(); + return PageResponse.of(items, page.getTotal(), pageNumber, pageSize); + } + + @Override + public PageResponse getCandidateColumnPage( + RelationSemanticPageQuery query) { + requireDatasource(query.datasourceId()); + String normalizedTableName = + logicalTableRelationHelper.normalizeTableName(query.tableName(), "tableName"); + TableSemantic table = + tableSemanticMapper.selectByDatasourceIdAndTableName( + query.datasourceId(), normalizedTableName); + if (table == null || !Boolean.TRUE.equals(table.getIsVisible())) { + throw new IllegalArgumentException("表 " + normalizedTableName + " 不存在或不可见。"); + } + int pageNumber = PageResponse.resolvePage(query.page()); + int pageSize = PageResponse.resolvePageSize(query.pageSize()); + SemanticUtils.validateSortOrder(query.sortOrder()); + boolean sortDescending = + SemanticConstants.SORT_ORDER_DESC.equalsIgnoreCase(query.sortOrder()); + PageHelper.startPage(pageNumber, pageSize); + Page page = + (Page) + columnSemanticMapper.selectVisiblePageByDatasourceIdAndTableName( + new ColumnSemanticPageQuery( + query.datasourceId(), + normalizedTableName, + pageNumber, + pageSize, + SemanticUtils.normalizeBlankToNull(query.keyword()), + query.sortOrder()), + sortDescending); + + // 物理列信息补充 + Map physicalByKey = new HashMap<>(); + try { + for (Column col : + schemaReader.getTableSchema( + datasourceService.findById(query.datasourceId()), + normalizedTableName)) { + physicalByKey.put(col.columnName().toLowerCase(Locale.ROOT), col); + } + } catch (SchemaReader.SchemaReadException e) { + log.warn("无法读取表 {} 的物理列信息: {}", normalizedTableName, e.getMessage()); + } + + List items = + page.stream() + .map( + c -> { + Column phys = + physicalByKey.get( + c.getColumnName().toLowerCase(Locale.ROOT)); + return new RelationCandidateColumnResponse( + c.getColumnName(), + SemanticUtils.normalizeBlankToNull( + c.getColumnDescription()), + phys != null ? buildTypeText(phys) : null, + phys != null ? phys.primaryKey() : null); + }) + .toList(); + return PageResponse.of(items, page.getTotal(), pageNumber, pageSize); + } + + private static String buildTypeText(Column physicalColumn) { + StringBuilder sb = new StringBuilder(physicalColumn.typeName()); + if (physicalColumn.columnSize() > 0) { + sb.append("(").append(physicalColumn.columnSize()).append(")"); + } + return sb.toString(); + } + private void requireDatasource(Integer datasourceId) { SemanticUtils.requireDatasourceId(datasourceId); if (datasourceService.findById(datasourceId) == null) { @@ -173,6 +310,114 @@ private void requireDatasource(Integer datasourceId) { } } + private void validateEndpoints( + Integer datasourceId, + String sourceTableName, + List sourceColumnNames, + String targetTableName, + List targetColumnNames) { + Datasource datasource = datasourceService.findById(datasourceId); + if (datasource == null) { + throw new IllegalArgumentException("Datasource does not exist: " + datasourceId); + } + String normalizedSourceTableName = + logicalTableRelationHelper.normalizeTableName(sourceTableName, "sourceTableName"); + String normalizedTargetTableName = + logicalTableRelationHelper.normalizeTableName(targetTableName, "targetTableName"); + List normalizedSourceColumnNames = + logicalTableRelationHelper.normalizeColumnNames( + sourceColumnNames, "sourceColumnNames"); + List normalizedTargetColumnNames = + logicalTableRelationHelper.normalizeColumnNames( + targetColumnNames, "targetColumnNames"); + TableSemantic sourceTable = + tableSemanticMapper.selectByDatasourceIdAndTableName( + datasourceId, normalizedSourceTableName); + if (sourceTable == null || !Boolean.TRUE.equals(sourceTable.getIsVisible())) { + throw new IllegalArgumentException("源表 " + normalizedSourceTableName + " 不存在或不可见。"); + } + TableSemantic targetTable = + tableSemanticMapper.selectByDatasourceIdAndTableName( + datasourceId, normalizedTargetTableName); + if (targetTable == null || !Boolean.TRUE.equals(targetTable.getIsVisible())) { + throw new IllegalArgumentException("目标表 " + normalizedTargetTableName + " 不存在或不可见。"); + } + for (String columnName : normalizedSourceColumnNames) { + ColumnSemantic col = + columnSemanticMapper.selectByDatasourceIdAndTableNameAndColumnName( + datasourceId, normalizedSourceTableName, columnName); + if (col == null || !Boolean.TRUE.equals(col.getIsVisible())) { + throw new IllegalArgumentException( + "源列 " + normalizedSourceTableName + "." + columnName + " 不存在或不可见。"); + } + } + for (String columnName : normalizedTargetColumnNames) { + ColumnSemantic col = + columnSemanticMapper.selectByDatasourceIdAndTableNameAndColumnName( + datasourceId, normalizedTargetTableName, columnName); + if (col == null || !Boolean.TRUE.equals(col.getIsVisible())) { + throw new IllegalArgumentException( + "目标列 " + normalizedTargetTableName + "." + columnName + " 不存在或不可见。"); + } + } + + validatePhysicalSchema( + datasource, + normalizedSourceTableName, + normalizedSourceColumnNames, + normalizedTargetTableName, + normalizedTargetColumnNames); + } + + private void validatePhysicalSchema( + Datasource datasource, + String sourceTableName, + List sourceColumnNames, + String targetTableName, + List targetColumnNames) { + Map sourceSchema = loadSchema(datasource, sourceTableName); + Map targetSchema = loadSchema(datasource, targetTableName); + if (sourceSchema.isEmpty()) { + throw new SemanticSchemaException( + "Physical source table does not exist: " + sourceTableName); + } + if (targetSchema.isEmpty()) { + throw new SemanticSchemaException( + "Physical target table does not exist: " + targetTableName); + } + for (String columnName : sourceColumnNames) { + if (!sourceSchema.containsKey(columnName.toLowerCase(Locale.ROOT))) { + throw new IllegalArgumentException( + "Physical source column does not exist: " + + sourceTableName + + "." + + columnName); + } + } + for (String columnName : targetColumnNames) { + if (!targetSchema.containsKey(columnName.toLowerCase(Locale.ROOT))) { + throw new IllegalArgumentException( + "Physical target column does not exist: " + + targetTableName + + "." + + columnName); + } + } + } + + private Map loadSchema(Datasource datasource, String tableName) { + Map physicalByKey = new HashMap<>(); + try { + for (Column column : schemaReader.getTableSchema(datasource, tableName)) { + physicalByKey.put(column.columnName().toLowerCase(Locale.ROOT), column); + } + } catch (SchemaReader.SchemaReadException e) { + throw new SemanticSchemaException( + "Failed to read physical schema for table " + tableName, e); + } + return physicalByKey; + } + private LogicalTableRelation buildRelation( Integer datasourceId, String tableName, BindLogicalTableRelationRequest request) { LogicalTableRelation relation = new LogicalTableRelation(); @@ -254,33 +499,4 @@ private LogicalTableRelation requireRelation( } return relation; } - - private LogicalTableRelationResponse mapResponse(LogicalTableRelation relation) { - List sourceColumns = - logicalTableRelationHelper.fromJson( - relation.getSourceColumnNamesJson(), "sourceColumnNames"); - List targetColumns = - logicalTableRelationHelper.fromJson( - relation.getTargetColumnNamesJson(), "targetColumnNames"); - return new LogicalTableRelationResponse( - relation.getId(), - logicalTableRelationHelper.buildRelationKey( - relation.getSourceTableName(), - sourceColumns, - relation.getTargetTableName(), - targetColumns), - relation.getDatasourceId(), - LogicalTableRelationHelper.RELATION_SOURCE_LOGICAL, - relation.getSourceTableName(), - sourceColumns, - relation.getTargetTableName(), - targetColumns, - relation.getRelationType(), - relation.getDescription(), - relation.getIsEnabled(), - relation.getIsEnabled(), - null, - relation.getCreateTime(), - relation.getUpdateTime()); - } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java index 83e8717..997d7d6 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java @@ -17,11 +17,10 @@ */ package io.github.malonetalk.service.semantic.table; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.TableSemanticPageQuery; import io.github.malonetalk.dto.semantic.TableSemanticResponse; import io.github.malonetalk.dto.semantic.TableSemanticUpdateRequest; -import io.github.malonetalk.entity.TableInfo; import java.util.List; public interface TableSemanticService { @@ -30,8 +29,6 @@ public interface TableSemanticService { List listAvailableDomains(Integer datasourceId); - List listTableInfosByDatasourceId(Integer datasourceId); - void updateTableSemantic(TableSemanticUpdateRequest request); void resetTableSemantic(Integer datasourceId, String tableName); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java index e1223e3..57dea23 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java @@ -20,12 +20,13 @@ import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.github.malonetalk.common.SemanticConstants; -import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.convertor.TableSemanticConverter; +import io.github.malonetalk.dto.PageResponse; import io.github.malonetalk.dto.semantic.TableSemanticPageQuery; import io.github.malonetalk.dto.semantic.TableSemanticResponse; import io.github.malonetalk.dto.semantic.TableSemanticUpdateRequest; -import io.github.malonetalk.entity.TableInfo; -import io.github.malonetalk.mapper.TableInfoMapper; +import io.github.malonetalk.entity.TableSemantic; +import io.github.malonetalk.mapper.TableSemanticMapper; import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.utils.SemanticUtils; import java.time.LocalDateTime; @@ -41,7 +42,8 @@ public class TableSemanticServiceImpl implements TableSemanticService { private final DatasourceService datasourceService; - private final TableInfoMapper tableInfoMapper; + private final TableSemanticMapper tableSemanticMapper; + private final TableSemanticConverter tableSemanticConverter; @Override public PageResponse getTablePage(TableSemanticPageQuery query) { @@ -55,9 +57,9 @@ public PageResponse getTablePage(TableSemanticPageQuery q boolean sortDescending = SemanticConstants.SORT_ORDER_DESC.equalsIgnoreCase(query.sortOrder()); PageHelper.startPage(pageNumber, pageSize); - Page page = - (Page) - tableInfoMapper.selectPageByDatasourceId( + Page page = + (Page) + tableSemanticMapper.selectPageByDatasourceId( new TableSemanticPageQuery( query.datasourceId(), pageNumber, @@ -65,7 +67,8 @@ public PageResponse getTablePage(TableSemanticPageQuery q SemanticUtils.normalizeBlankToNull(query.keyword()), query.sortOrder()), sortDescending); - List responses = page.stream().map(this::mapResponse).toList(); + List responses = + page.stream().map(tableSemanticConverter::toResponse).toList(); long total = page.getTotal(); return PageResponse.of(responses, total, pageNumber, pageSize); } @@ -76,8 +79,8 @@ public List listAvailableDomains(Integer datasourceId) { if (datasourceService.findById(datasourceId) == null) { return List.of(); } - return tableInfoMapper.selectByDatasourceId(datasourceId).stream() - .map(TableInfo::getDomain) + return tableSemanticMapper.selectByDatasourceId(datasourceId).stream() + .map(TableSemantic::getDomain) .filter(domain -> domain != null && !domain.isBlank()) .map(String::trim) .distinct() @@ -85,33 +88,24 @@ public List listAvailableDomains(Integer datasourceId) { .toList(); } - @Override - public List listTableInfosByDatasourceId(Integer datasourceId) { - SemanticUtils.requireDatasourceId(datasourceId); - if (datasourceService.findById(datasourceId) == null) { - return List.of(); - } - return tableInfoMapper.selectByDatasourceId(datasourceId); - } - @Override public void updateTableSemantic(TableSemanticUpdateRequest request) { requireDatasource(request.datasourceId()); String normalizedTableName = SemanticUtils.requireName(request.tableName(), "tableName"); - TableInfo existing = - tableInfoMapper.selectByDatasourceIdAndTableName( + TableSemantic existing = + tableSemanticMapper.selectByDatasourceIdAndTableName( request.datasourceId(), normalizedTableName); if (existing == null) { - TableInfo tableInfo = new TableInfo(); - tableInfo.setDatasourceId(request.datasourceId()); - tableInfo.setTableName(normalizedTableName); - tableInfo.setTableDescription( + TableSemantic tableSemantic = new TableSemantic(); + tableSemantic.setDatasourceId(request.datasourceId()); + tableSemantic.setTableName(normalizedTableName); + tableSemantic.setTableDescription( SemanticUtils.normalizeBlankToNull(request.tableDescription())); - tableInfo.setDomain(SemanticUtils.normalizeBlankToNull(request.domain())); - tableInfo.setIsVisible(request.isVisible()); - tableInfo.setCreateTime(LocalDateTime.now()); - tableInfo.setUpdateTime(LocalDateTime.now()); - tableInfoMapper.insert(tableInfo); + tableSemantic.setDomain(SemanticUtils.normalizeBlankToNull(request.domain())); + tableSemantic.setIsVisible(request.isVisible()); + tableSemantic.setCreateTime(LocalDateTime.now()); + tableSemantic.setUpdateTime(LocalDateTime.now()); + tableSemanticMapper.insert(tableSemantic); return; } existing.setTableName(normalizedTableName); @@ -120,19 +114,20 @@ public void updateTableSemantic(TableSemanticUpdateRequest request) { existing.setDomain(SemanticUtils.normalizeBlankToNull(request.domain())); existing.setIsVisible(request.isVisible()); existing.setUpdateTime(LocalDateTime.now()); - tableInfoMapper.update(existing); + tableSemanticMapper.update(existing); } @Override public void resetTableSemantic(Integer datasourceId, String tableName) { requireDatasource(datasourceId); String normalizedTableName = SemanticUtils.requireName(tableName, "tableName"); - TableInfo existing = - tableInfoMapper.selectByDatasourceIdAndTableName(datasourceId, normalizedTableName); + TableSemantic existing = + tableSemanticMapper.selectByDatasourceIdAndTableName( + datasourceId, normalizedTableName); if (existing == null) { throw new IllegalArgumentException("Table semantic metadata does not exist."); } - tableInfoMapper.deleteByDatasourceIdAndIds(datasourceId, List.of(existing.getId())); + tableSemanticMapper.deleteByDatasourceIdAndIds(datasourceId, List.of(existing.getId())); } @Override @@ -146,12 +141,12 @@ public int resetTableSemantics(Integer datasourceId, List tableNames) { .map(this::normalizeName) .collect(Collectors.toCollection(java.util.LinkedHashSet::new)); List matchedIds = - tableInfoMapper.selectByDatasourceId(datasourceId).stream() + tableSemanticMapper.selectByDatasourceId(datasourceId).stream() .filter( table -> normalizedNames.contains( normalizeName(table.getTableName()))) - .map(TableInfo::getId) + .map(TableSemantic::getId) .distinct() .toList(); if (matchedIds.isEmpty()) { @@ -163,7 +158,7 @@ public int resetTableSemantics(Integer datasourceId, List tableNames) { + datasourceId + "."); } - return tableInfoMapper.deleteByDatasourceIdAndIds(datasourceId, matchedIds); + return tableSemanticMapper.deleteByDatasourceIdAndIds(datasourceId, matchedIds); } private void requireDatasource(Integer datasourceId) { @@ -173,20 +168,6 @@ private void requireDatasource(Integer datasourceId) { } } - private TableSemanticResponse mapResponse(TableInfo tableInfo) { - return new TableSemanticResponse( - tableInfo.getId(), - tableInfo.getTableName(), - tableInfo.getDomain(), - null, - tableInfo.getTableDescription(), - tableInfo.getIsVisible(), - true, - Boolean.TRUE.equals(tableInfo.getIsVisible()), - null, - tableInfo.getUpdateTime()); - } - private String normalizeName(String value) { return SemanticUtils.requireName(value, "tableName").toLowerCase(Locale.ROOT); } diff --git a/data-agent-backend/src/main/resources/application.properties b/data-agent-backend/src/main/resources/application.properties index 50ae6d6..02d19fb 100644 --- a/data-agent-backend/src/main/resources/application.properties +++ b/data-agent-backend/src/main/resources/application.properties @@ -16,7 +16,7 @@ spring.datasource.hikari.connection-timeout=20000 # MyBatis Configuration mybatis.mapper-locations=classpath:mapper/*.xml -mybatis.type-aliases-package=com.malone.entity +mybatis.type-aliases-package=io.github.malonetalk.entity mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl diff --git a/data-agent-backend/src/main/resources/mapper/ColumnSemanticInfoMapper.xml b/data-agent-backend/src/main/resources/mapper/ColumnSemanticMapper.xml similarity index 80% rename from data-agent-backend/src/main/resources/mapper/ColumnSemanticInfoMapper.xml rename to data-agent-backend/src/main/resources/mapper/ColumnSemanticMapper.xml index eb3cf09..e2337f3 100644 --- a/data-agent-backend/src/main/resources/mapper/ColumnSemanticInfoMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/ColumnSemanticMapper.xml @@ -1,8 +1,8 @@ - + - + @@ -43,6 +43,24 @@ + + - + INSERT INTO column_info ( datasource_id, table_name, column_name, column_description, is_visible, create_time, update_time @@ -62,7 +80,7 @@ ) - + UPDATE column_info table_name = #{tableName}, diff --git a/data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml b/data-agent-backend/src/main/resources/mapper/TableSemanticMapper.xml similarity index 79% rename from data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml rename to data-agent-backend/src/main/resources/mapper/TableSemanticMapper.xml index c308e2f..cb376ef 100644 --- a/data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/TableSemanticMapper.xml @@ -1,8 +1,8 @@ - + - + @@ -35,6 +35,23 @@ + + - + INSERT INTO table_info ( table_name, table_description, domain, datasource_id, is_visible, create_time, update_time @@ -53,7 +70,7 @@ ) - + UPDATE table_info table_name = #{tableName},