Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
885b33a
feat: add sync service in semantic
mengnankkkk Jul 4, 2026
5413dee
fix: add bug fix
mengnankkkk Jul 8, 2026
7f8e028
fix: ci
mengnankkkk Jul 8, 2026
a21a621
fix: add remove
mengnankkkk Jul 9, 2026
138b90b
fix: add now in mapper
mengnankkkk Jul 11, 2026
0fbeb37
fix: add turn
mengnankkkk Jul 11, 2026
5638aef
fix: add sync builder
mengnankkkk Jul 11, 2026
02e4c37
feat: replace with public class
mengnankkkk Jul 11, 2026
cee9f2c
fix: add sync fix
mengnankkkk Jul 11, 2026
23b4c59
Merge branch 'main' into feature/add_sync_datasource
VLSMB Jul 11, 2026
d83b921
fix: fix CR
mengnankkkk Jul 16, 2026
482da21
fix: add batch query
mengnankkkk Jul 16, 2026
5a9bcbe
refactor: use Lombok @Builder for SyncTableResult
lzq986 Jul 18, 2026
9511773
Merge branch 'main' into feature/add_sync_datasource
mengnankkkk Jul 18, 2026
164696a
Merge remote-tracking branch 'origin/feature/add_sync_datasource' int…
lzq986 Jul 18, 2026
6403e9f
refactor: require contextual semantic missing-field errors
mengnankkkk Jul 18, 2026
ab6e42f
fix: guard against null/empty tableNames in SchemaReader
lzq986 Jul 18, 2026
6ec4b51
Merge remote-tracking branch 'origin/feature/add_sync_datasource' int…
lzq986 Jul 18, 2026
b7f1408
refactor: remove SemanticUtils.objectKey
mengnankkkk Jul 18, 2026
f8a1113
Merge branch 'feature/add_sync_datasource' of github.com:MaloneTalk/D…
mengnankkkk Jul 18, 2026
5f73b54
fix: shorten refresh physical status transactions
mengnankkkk Jul 18, 2026
fafef09
refactor: make SemanticAvailabilityHelper fail-fast on null
lzq986 Jul 18, 2026
c4f625d
docs: clarify semantics of ColumnSemanticResponse.effective
lzq986 Jul 18, 2026
80f682b
refactor: use Lombok @Builder for RelationWorkspace responses
lzq986 Jul 18, 2026
9190e40
refactor: simplify sync diff returns and use positive availability pr…
lzq986 Jul 18, 2026
654e38e
refactor: rename tableMarkedMissing to tableMarkedAsMissing
lzq986 Jul 18, 2026
baec28b
refactor: extract enums, remove dead code, fix config
VLSMB Jul 18, 2026
e74cabb
perf: batch sync physical schema metadata
mengnankkkk Jul 19, 2026
e978835
fix: replace gettables
mengnankkkk Jul 19, 2026
ae1caf0
refactor: simplify semantic schema sync implementation
mengnankkkk Jul 19, 2026
8309e5f
fix: fix ci
VLSMB Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@

**Don't write boilerplate that can be generated.** Use Lombok (`@Data`, `@Builder`, `@RequiredArgsConstructor`), MapStruct for mappers, and IDE generation for equals/hashCode. If it's mechanical, don't type it by hand. **For records with many fields, prefer `@Builder` over positional `new` — the named setter style improves readability at every call site.** A 6-argument `new Xxx(a, b, c, d, e, f)` forces readers to count positions; `.name(a).type(b)...` does not.

**Prefer imports over fully-qualified names for JDK and framework classes.** Import the class at the top and use its short name in code — `Objects::nonNull`, not `java.util.Objects::nonNull`. The exception is when a project class shares the same simple name as a JDK or framework class you need to use. In that case, qualify the JDK/framework class with its full package path everywhere — even if the conflicting project class isn't imported in the current file. For example, use `javax.sql.DataSource` fully-qualified because `io.github.malonetalk.entity.Datasource` exists in the project and the two names are easily confused.

**Write code for the next reader, not for the compiler.** The compiler can parse anything. A human shouldn't have to. Choose names that reveal intent, structure code in small logical steps, and prefer clarity over cleverness. If a line makes you pause — it will make someone else pause too.
2 changes: 1 addition & 1 deletion data-agent-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<agentscope.version>1.0.11</agentscope.version>
<spotless.version>3.4.0</spotless.version>
<google-java-format.version>1.28.0</google-java-format.version>
<mybatis-springboot-starter.version>4.0.0</mybatis-springboot-starter.version>
<mybatis-springboot-starter.version>4.0.1</mybatis-springboot-starter.version>
<pagehelper-spring-boot.version>2.1.1</pagehelper-spring-boot.version>
<lombok.version>1.18.42</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -63,6 +67,36 @@ public List<PhysicalColumnInfo> getTableSchema(Datasource datasource, String tab
}
}

public Map<String, Set<String>> getTableColumnNames(
Datasource datasource, Collection<String> tableNames) {
if (tableNames == null || tableNames.isEmpty()) {
return Map.of();
}
Map<String, Set<String>> columnNamesByTable = new LinkedHashMap<>();
for (String tableName : tableNames) {
columnNamesByTable.putIfAbsent(tableName, new LinkedHashSet<>());
}

javax.sql.DataSource ds = dynamicDataSourceManager.getOrCreateDataSource(datasource);

try (Connection conn = ds.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
try (ResultSet rs =
metaData.getColumns(conn.getCatalog(), conn.getSchema(), "%", null)) {
while (rs.next()) {
Set<String> columnNames = columnNamesByTable.get(rs.getString("TABLE_NAME"));
if (columnNames != null) {
columnNames.add(rs.getString("COLUMN_NAME"));
}
}
}
return columnNamesByTable;
} catch (SQLException e) {
log.error("Failed to read column names for tables: {}", e.getMessage(), e);
throw new SchemaReadException("Failed to read column names: " + e.getMessage(), e);
}
}

private List<PhysicalTableInfo> getTables(Connection conn) throws SQLException {
List<PhysicalTableInfo> tables = new ArrayList<>();
DatabaseMetaData metaData = conn.getMetaData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
package io.github.malonetalk.agent.tools;

import io.agentscope.core.tool.Tool;
import io.github.malonetalk.service.DomainService;
import io.github.malonetalk.service.semantic.DomainService;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import io.github.malonetalk.dto.DomainUpdateRequest;
import io.github.malonetalk.dto.pagination.PageResponse;
import io.github.malonetalk.entity.DomainInfo;
import io.github.malonetalk.service.DomainService;
import io.github.malonetalk.service.semantic.DomainService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.controller;

import io.github.malonetalk.common.Result;
import io.github.malonetalk.dto.semantic.RelationWorkspacePageQuery;
import io.github.malonetalk.dto.semantic.RelationWorkspaceResponse;
import io.github.malonetalk.service.semantic.relation.RelationSemanticService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/semantic/tables/relations/workspace")
@RequiredArgsConstructor
public class TableRelationWorkspaceController {

private final RelationSemanticService relationSemanticService;

@GetMapping
public Result<RelationWorkspaceResponse> getWorkspace(@Valid RelationWorkspacePageQuery query) {
return Result.success(relationSemanticService.getRelationWorkspace(query));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.controller;

import io.github.malonetalk.common.Result;
import io.github.malonetalk.dto.pagination.PageResponse;
import io.github.malonetalk.dto.semantic.PhysicalTableCandidatePageQuery;
import io.github.malonetalk.dto.semantic.PhysicalTableCandidateResponse;
import io.github.malonetalk.dto.semantic.RefreshPhysicalStatusRequest;
import io.github.malonetalk.dto.semantic.SyncTableSemanticsRequest;
import io.github.malonetalk.dto.semantic.SyncTableSemanticsResponse;
import io.github.malonetalk.service.semantic.sync.SemanticSyncService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/semantic/tables/sync")
@RequiredArgsConstructor
public class TableSemanticSyncController {

private final SemanticSyncService semanticSyncService;

@GetMapping("/candidates")
public Result<PageResponse<PhysicalTableCandidateResponse>> getPhysicalTableCandidates(
@Valid PhysicalTableCandidatePageQuery query) {
return Result.success(semanticSyncService.getPhysicalTableCandidates(query));
}

@PostMapping
public Result<SyncTableSemanticsResponse> syncTables(
@Valid @RequestBody SyncTableSemanticsRequest request) {
return Result.success(semanticSyncService.syncTables(request));
}

@PostMapping("/physical-status")
public Result<SyncTableSemanticsResponse> refreshPhysicalStatus(
@Valid @RequestBody RefreshPhysicalStatusRequest request) {
return Result.success(semanticSyncService.refreshPhysicalStatus(request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
import io.github.malonetalk.dto.prompt.TableRelationPromptResponse;
import io.github.malonetalk.entity.ColumnInfo;
import io.github.malonetalk.entity.TableInfo;
import io.github.malonetalk.service.semantic.SemanticAvailabilityHelper;
import io.github.malonetalk.service.semantic.enums.UsageLevelEnum;
import io.github.malonetalk.utils.SemanticUtils;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

/** 物理层/语义层 → Agent Prompt DTO 的统一转换器,集中管理所有面向 LLM 的 DTO 映射逻辑。 */
public final class PromptConverter {
Expand All @@ -40,16 +40,19 @@ private PromptConverter() {}
public static ColumnPromptResponse mapColumnPrompt(
PhysicalColumnInfo physicalColumn, Map<String, ColumnInfo> semanticByName) {
ColumnInfo semanticColumn =
semanticByName.get(physicalColumn.columnName().toLowerCase(Locale.ROOT));
if (semanticColumn != null && !Boolean.TRUE.equals(semanticColumn.getIsVisible())) {
semanticByName.get(
SemanticUtils.normalizeObjectName(
physicalColumn.columnName(),
"Missing physical column name for prompt conversion."));
if (!SemanticAvailabilityHelper.isColumnAvailable(
semanticColumn, UsageLevelEnum.AI_PROMPT)) {
return null;
}

String description =
Optional.ofNullable(semanticColumn)
.map(ColumnInfo::getColumnDescription)
.map(SemanticUtils::trimToNull)
.orElseGet(() -> SemanticUtils.trimToNull(physicalColumn.remarks()));
SemanticUtils.firstNonBlank(
semanticColumn == null ? null : semanticColumn.getColumnDescription(),
physicalColumn.remarks());

StringBuilder typeBuilder = new StringBuilder(physicalColumn.typeName());
if (physicalColumn.columnSize() > 0) {
Expand All @@ -72,8 +75,11 @@ public static TablePromptResponse mapTablePrompt(
Map<String, TableInfo> semanticByName,
List<TableRelationPromptResponse> resolvedRelations) {
TableInfo semanticTable =
semanticByName.get(physicalTable.tableName().toLowerCase(Locale.ROOT));
if (semanticTable != null && !Boolean.TRUE.equals(semanticTable.getIsVisible())) {
semanticByName.get(
SemanticUtils.normalizeObjectName(
physicalTable.tableName(),
"Missing physical table name for prompt conversion."));
if (!SemanticAvailabilityHelper.isTableAvailable(semanticTable, UsageLevelEnum.AI_PROMPT)) {
return null;
}

Expand All @@ -93,9 +99,8 @@ private static String resolveDomain(TableInfo semanticTable) {

private static String resolveDescription(
PhysicalTableInfo physicalTable, TableInfo semanticTable) {
return Optional.ofNullable(semanticTable)
.map(TableInfo::getTableDescription)
.map(SemanticUtils::trimToNull)
.orElseGet(() -> SemanticUtils.trimToNull(physicalTable.remarks()));
return SemanticUtils.firstNonBlank(
semanticTable == null ? null : semanticTable.getTableDescription(),
physicalTable.remarks());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
import io.github.malonetalk.common.SemanticConstants;
import io.github.malonetalk.dto.semantic.ColumnSemanticResponse;
import io.github.malonetalk.dto.semantic.LogicalTableRelationResponse;
import io.github.malonetalk.dto.semantic.RelationWorkspaceColumnResponse;
import io.github.malonetalk.dto.semantic.RelationWorkspaceTableResponse;
import io.github.malonetalk.dto.semantic.TableSemanticResponse;
import io.github.malonetalk.entity.ColumnInfo;
import io.github.malonetalk.entity.LogicalTableRelation;
import io.github.malonetalk.entity.TableInfo;
import io.github.malonetalk.enums.LogicalTableRelationType;
import io.github.malonetalk.service.semantic.SemanticAvailabilityHelper;
import io.github.malonetalk.service.semantic.enums.UsageLevelEnum;
import io.github.malonetalk.service.semantic.relation.LogicalTableRelationHelper;
import io.github.malonetalk.utils.SemanticUtils;
import java.util.List;
Expand All @@ -39,25 +43,41 @@ public class SemanticConverter {

public TableSemanticResponse toResponse(TableInfo tableInfo) {
Boolean isVisible = tableInfo.getIsVisible();
boolean hasPhysicalTable = SemanticAvailabilityHelper.hasPhysicalTable(tableInfo);
return TableSemanticResponse.builder()
.id(tableInfo.getId())
.tableName(tableInfo.getTableName())
.domain(SemanticUtils.normalizeDomain(tableInfo.getDomain()))
.physicalTableDescription(
SemanticUtils.trimToNull(tableInfo.getPhysicalTableDescription()))
.tableDescription(SemanticUtils.trimToNull(tableInfo.getTableDescription()))
.isVisible(isVisible)
.hasPhysicalTable(true)
.hasPhysicalTable(hasPhysicalTable)
.invalidReason(
SemanticAvailabilityHelper.tableInvalidReason(
tableInfo, UsageLevelEnum.USER_OPERATION))
.updateTime(tableInfo.getUpdateTime())
.build();
}

public ColumnSemanticResponse toResponse(ColumnInfo columnInfo) {
boolean hasPhysicalColumn = SemanticAvailabilityHelper.hasPhysicalColumn(columnInfo);
return ColumnSemanticResponse.builder()
.id(columnInfo.getId())
.columnName(columnInfo.getColumnName())
.physicalColumnDescription(
SemanticUtils.trimToNull(columnInfo.getPhysicalColumnDescription()))
.columnDescription(columnInfo.getColumnDescription())
.typeName(SemanticUtils.trimToNull(columnInfo.getTypeName()))
.primaryKey(columnInfo.getPrimaryKey())
.isVisible(columnInfo.getIsVisible())
.hasPhysicalColumn(true)
.effective(Boolean.TRUE.equals(columnInfo.getIsVisible()))
.hasPhysicalColumn(hasPhysicalColumn)
.effective(
SemanticAvailabilityHelper.isColumnAvailable(
columnInfo, UsageLevelEnum.USER_OPERATION))
.invalidReason(
SemanticAvailabilityHelper.columnInvalidReason(
columnInfo, UsageLevelEnum.USER_OPERATION))
.updateTime(columnInfo.getUpdateTime())
.build();
}
Expand Down Expand Up @@ -91,4 +111,41 @@ public LogicalTableRelationResponse toResponse(LogicalTableRelation relation) {
.updateTime(relation.getUpdateTime())
.build();
}

public RelationWorkspaceTableResponse toWorkspaceTable(
TableInfo tableInfo, List<ColumnInfo> columns) {
return RelationWorkspaceTableResponse.builder()
.tableName(tableInfo.getTableName())
.domain(SemanticUtils.normalizeDomain(tableInfo.getDomain()))
.description(
SemanticUtils.firstNonBlank(
tableInfo.getTableDescription(),
tableInfo.getPhysicalTableDescription()))
.operable(
SemanticAvailabilityHelper.isTableAvailable(
tableInfo, UsageLevelEnum.USER_OPERATION))
.invalidReason(
SemanticAvailabilityHelper.tableInvalidReason(
tableInfo, UsageLevelEnum.USER_OPERATION))
.columns(columns.stream().map(this::toWorkspaceColumn).toList())
.build();
}

public RelationWorkspaceColumnResponse toWorkspaceColumn(ColumnInfo columnInfo) {
return RelationWorkspaceColumnResponse.builder()
.columnName(columnInfo.getColumnName())
.description(
SemanticUtils.firstNonBlank(
columnInfo.getColumnDescription(),
columnInfo.getPhysicalColumnDescription()))
.typeName(SemanticUtils.trimToNull(columnInfo.getTypeName()))
.primaryKey(columnInfo.getPrimaryKey())
.operable(
SemanticAvailabilityHelper.isColumnAvailable(
columnInfo, UsageLevelEnum.USER_OPERATION))
.invalidReason(
SemanticAvailabilityHelper.columnInvalidReason(
columnInfo, UsageLevelEnum.USER_OPERATION))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public record ColumnSemanticResponse(
Boolean primaryKey,
Boolean isVisible,
Boolean hasPhysicalColumn,
// 最终可用性(推导值,非存储字段):effective = isVisible && hasPhysicalColumn。
// 与 isVisible 含义不同——isVisible 只表示用户是否手动隐藏该列;
// effective 还需物理列依然存在,二者任一为假即为 false。
// 它由 SemanticConverter 调 SemanticAvailabilityHelper.isColumnAvailable(USER_OPERATION) 计算,
// 前端据此渲染“有效/无效”徽标。
Boolean effective,
String invalidReason,
LocalDateTime updateTime) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 <https://www.gnu.org/licenses/>.
* limitations under the License.
*/
package io.github.malonetalk.dto.semantic;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

public record PhysicalTableCandidatePageQuery(
@NotNull(message = "datasourceId 不能为空") Integer datasourceId,
@Min(value = 1, message = "page 不能小于 1") Integer page,
@Min(value = 1, message = "pageSize 不能小于 1") Integer pageSize,
String keyword,
@Pattern(regexp = "^(?i)(asc|desc)$", message = "sortOrder must be asc or desc.")
String sortOrder) {}
Loading
Loading