chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
package com.xspaceagi.compose.infra.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* 自定义字段定义
*/
@Schema(description = "自定义字段定义")
@TableName(value = "custom_field_definition")
@Getter
@Setter
@Builder
public class CustomFieldDefinition {
/**
* 主键ID
*/
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 是否为系统字段,1:系统字段;-1:非系统字段
*/
@Schema(description = "是否为系统字段,1:系统字段;-1:非系统字段")
private Integer systemFieldFlag;
/**
* 租户ID
*/
@Schema(description = "租户ID")
@TableField(value = "_tenant_id")
private Long tenantId;
/**
* 所属空间ID
*/
@Schema(description = "所属空间ID")
private Long spaceId;
/**
* 关联的表ID
*/
@Schema(description = "关联的表ID")
private Long tableId;
/**
* 字段名
*/
@Schema(description = "字段名")
private String fieldName;
/**
* 字段描述
*/
@Schema(description = "字段描述")
private String fieldDescription;
/**
* 字段类型1:String;2:Integer;3:Number;4:Boolean;5:Date
*/
@Schema(description = "字段类型1:String;2:Integer;3:Number;4:Boolean;5:Date")
private Integer fieldType;
/**
* 是否可为空1-可空 -1-非空
*/
@Schema(description = "是否可为空1-可空 -1-非空")
private Integer nullableFlag;
/**
* 默认值
*/
@Schema(description = "默认值")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String defaultValue;
/**
* 是否唯一1-唯一 -1-非唯一
*/
@Schema(description = "是否唯一1-唯一 -1-非唯一")
private Integer uniqueFlag;
/**
* 是否启用1-启用 -1-禁用
*/
@Schema(description = "是否启用1-启用 -1-禁用")
private Integer enabledFlag;
/**
* 字段顺序
*/
@Schema(description = "字段顺序")
private Integer sortIndex;
/**
* 创建时间
*/
@Schema(description = "创建时间")
private LocalDateTime created;
/**
* 字符串字段长度,可空,比如字符串,可以指定长度使用
*/
@Schema(description = "字符串字段长度,可空,比如字符串,可以指定长度使用")
private Integer fieldStrLen;
/**
* 创建人id
*/
@Schema(description = "创建人id")
private Long creatorId;
/**
* 创建人
*/
@Schema(description = "创建人")
private String creatorName;
/**
* 更新时间
*/
@Schema(description = "更新时间")
private LocalDateTime modified;
/**
* 最后修改人id
*/
@Schema(description = "最后修改人id")
private Long modifiedId;
/**
* 最后修改人
*/
@Schema(description = "最后修改人")
private String modifiedName;
/**
* 逻辑标记,1:有效;-1:无效
*/
@Schema(description = "逻辑标记,1:有效;-1:无效")
private Integer yn;
}

View File

@@ -0,0 +1,120 @@
package com.xspaceagi.compose.infra.dao.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 自定义数据表定义
*/
@Schema(description = "自定义数据表定义")
@TableName(value = "custom_table_definition")
@Getter
@Setter
public class CustomTableDefinition {
/**
* 主键ID
*/
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 租户ID
*/
@Schema(description = "租户ID")
@TableField(value = "_tenant_id")
private Long tenantId;
/**
* 所属空间ID
*/
@Schema(description = "所属空间ID")
private Long spaceId;
/**
* 图标图片地址
*/
@Schema(description = "图标图片地址")
private String icon;
/**
* 表名
*/
@Schema(description = "表名")
private String tableName;
/**
* 表描述
*/
@Schema(description = "表描述")
private String tableDescription;
/**
* Doris数据库名
*/
@Schema(description = "Doris数据库名")
private String dorisDatabase;
/**
* Doris表名
*/
@Schema(description = "Doris表名")
private String dorisTable;
/**
* 状态1-启用 -1-禁用
*/
@Schema(description = "状态1-启用 -1-禁用")
private Integer status;
/**
* 创建时间
*/
@Schema(description = "创建时间")
private LocalDateTime created;
/**
* 创建人id
*/
@Schema(description = "创建人id")
private Long creatorId;
/**
* 创建人
*/
@Schema(description = "创建人")
private String creatorName;
/**
* 更新时间
*/
@Schema(description = "更新时间")
private LocalDateTime modified;
/**
* 最后修改人id
*/
@Schema(description = "最后修改人id")
private Long modifiedId;
/**
* 最后修改人
*/
@Schema(description = "最后修改人")
private String modifiedName;
/**
* 逻辑标记,1:有效;-1:无效
*/
@Schema(description = "逻辑标记,1:有效;-1:无效")
private Integer yn;
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.compose.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition;
public interface CustomFieldDefinitionMapper extends BaseMapper<CustomFieldDefinition> {
}

View File

@@ -0,0 +1,37 @@
package com.xspaceagi.compose.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
public interface CustomTableDefinitionMapper extends BaseMapper<CustomTableDefinition> {
/**
* 总条数查询
*
* @param queryMap 筛选条件
* @return 总条数
*/
Long queryTotal(@Param("queryMap") Map<String, Object> queryMap);
/**
* 列表查询
*
* @param queryMap 筛选条件
* @param orderColumns 排序
* @param startIndex 索引开始位置
* @param pageSize 业务大小
* @return 列表
*/
List<CustomTableDefinition> queryList(@Param("queryMap") Map<String, Object> queryMap,
@Param("orderColumns") List<OrderItem> orderColumns,
@Param("startIndex") Long startIndex, @Param("pageSize") Long pageSize);
}

View File

@@ -0,0 +1,96 @@
package com.xspaceagi.compose.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition;
import java.util.List;
/**
* 数据表的字段定义
*/
public interface CustomFieldDefinitionService extends IService<CustomFieldDefinition> {
/**
* 根据ID集合查询列表
*
* @param ids id集合
* @return 列表
*/
List<CustomFieldDefinition> queryListByIds(List<Long> ids);
/**
* 根据主键查询 单条记录
*
* @param id id
* @return 单条记录
*/
CustomFieldDefinition queryOneInfoById(Long id);
/**
* 根据表id查询记录数
*
* @param tableId 表id
* @return 记录数
*/
Long queryCountByTableId(Long tableId);
/**
* 更新
*
* @param entity entity
* @return id
*/
Long updateInfo(CustomFieldDefinition entity);
/**
* 新增
*/
Long addInfo(CustomFieldDefinition entity);
/**
* 批量新增
*
* @param entityList 新增列表
* @param userContext 用户上下文
*/
void batchAddInfo(List<CustomFieldDefinition> entityList);
/**
* 批量更新
*
* @param entityList 更新列表
* @param userContext 用户上下文
*/
void batchUpdateInfo(List<CustomFieldDefinition> entityList);
/**
* 删除根据主键id
*
* @param id id
*/
void deleteById(Long id);
/**
* 根据表id删除
*
* @param tableId 表id
*/
void deleteByTableId(Long tableId);
/**
* 根据表id查询列表
*
* @param tableId 表id
* @return 列表
*/
List<CustomFieldDefinition> queryListByTableId(Long tableId);
/**
* 根据表id集合查询列表
*
* @param tableIds 表id集合
* @return 列表
*/
List<CustomFieldDefinition> queryListByTableIds(List<Long> tableIds);
}

View File

@@ -0,0 +1,86 @@
package com.xspaceagi.compose.infra.dao.service;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition;
import com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition;
import java.util.List;
import java.util.Map;
/**
* 自定义数据表定义服务
*/
public interface CustomTableDefinitionService extends IService<CustomTableDefinition> {
/**
* 根据ID集合查询列表
*
* @param ids id集合
* @return 列表
*/
List<CustomTableDefinition> queryListByIds(List<Long> ids);
/**
* 根据主键查询 单条记录
*
* @param id id
* @return 单条记录
*/
CustomTableDefinition queryOneInfoById(Long id);
/**
* 更新
*
* @param entity entity
* @return id
*/
Long updateInfo(CustomTableDefinition entity);
/**
* 新增
*/
Long addInfo(CustomTableDefinition entity);
/**
* 删除根据主键id
*
* @param id id
*/
void deleteById(Long id);
/**
* 分页查询
*
* @param queryMap 查询条件
* @param orderColumns 排序条件
* @param startIndex 开始索引
* @param pageSize 页大小
* @return 分页数据
*/
List<CustomTableDefinition> pageQuery(Map<String, Object> queryMap, List<OrderItem> orderColumns, Long startIndex, Long pageSize);
/**
* 查询总数
*
* @param queryMap 查询条件
* @return 总数
*/
Long queryTotal(Map<String, Object> queryMap);
/**
* 根据条件查询表定义列表
*
* @param condition 查询条件
* @return 表定义列表
*/
List<CustomTableDefinition> queryListByCondition(CustomTableDefinition condition);
/**
* 根据空间ID查询表定义列表
*
* @param spaceId 空间ID
* @return 表定义列表
*/
List<CustomTableDefinition> queryListBySpaceId(Long spaceId);
}

View File

@@ -0,0 +1,119 @@
package com.xspaceagi.compose.infra.dao.service.impl;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition;
import com.xspaceagi.compose.infra.dao.mapper.CustomFieldDefinitionMapper;
import com.xspaceagi.compose.infra.dao.service.CustomFieldDefinitionService;
import com.xspaceagi.system.spec.enums.YnEnum;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.ComposeException;
@Service
public class CustomFieldDefinitionServiceImpl extends ServiceImpl<CustomFieldDefinitionMapper, CustomFieldDefinition>
implements CustomFieldDefinitionService {
@Autowired
private CustomFieldDefinitionMapper customFieldDefinitionMapper;
@Override
public List<CustomFieldDefinition> queryListByIds(List<Long> ids) {
LambdaQueryWrapper<CustomFieldDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomFieldDefinition::getYn, YnEnum.Y.getKey())
.in(CustomFieldDefinition::getId, ids);
return this.list(queryWrapper);
}
@Override
public CustomFieldDefinition queryOneInfoById(Long id) {
LambdaQueryWrapper<CustomFieldDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomFieldDefinition::getYn, YnEnum.Y.getKey())
.eq(CustomFieldDefinition::getId, id);
return this.getOne(queryWrapper);
}
@Override
public Long updateInfo(CustomFieldDefinition entity) {
var updateObj = this.getById(entity.getId());
if (Objects.isNull(updateObj)) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
entity.setCreated(null);
entity.setModified(null);
this.updateById(entity);
return entity.getId();
}
@Override
public Long addInfo(CustomFieldDefinition entity) {
entity.setId(null);
entity.setCreated(null);
entity.setModified(null);
this.save(entity);
return entity.getId();
}
@Override
public void deleteById(Long id) {
var existObj = this.getById(id);
if (Objects.isNull(existObj)) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
this.removeById(id);
}
@Override
public List<CustomFieldDefinition> queryListByTableId(Long tableId) {
LambdaQueryWrapper<CustomFieldDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomFieldDefinition::getYn, YnEnum.Y.getKey())
.eq(CustomFieldDefinition::getTableId, tableId);
return this.list(queryWrapper);
}
@Override
public List<CustomFieldDefinition> queryListByTableIds(List<Long> tableIds) {
LambdaQueryWrapper<CustomFieldDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomFieldDefinition::getYn, YnEnum.Y.getKey())
.in(CustomFieldDefinition::getTableId, tableIds);
return this.list(queryWrapper);
}
@Override
public void batchAddInfo(List<CustomFieldDefinition> entityList) {
entityList.forEach(entity -> {
entity.setId(null);
entity.setCreated(null);
entity.setModified(null);
});
this.saveBatch(entityList);
}
@Override
public void batchUpdateInfo(List<CustomFieldDefinition> entityList) {
entityList.forEach(entity -> {
entity.setModified(null);
});
this.updateBatchById(entityList);
}
@Override
public void deleteByTableId(Long tableId) {
LambdaQueryWrapper<CustomFieldDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomFieldDefinition::getTableId, tableId);
this.remove(queryWrapper);
}
@Override
public Long queryCountByTableId(Long tableId) {
LambdaQueryWrapper<CustomFieldDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomFieldDefinition::getTableId, tableId);
return this.count(queryWrapper);
}
}

View File

@@ -0,0 +1,131 @@
package com.xspaceagi.compose.infra.dao.service.impl;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition;
import com.xspaceagi.compose.infra.dao.mapper.CustomTableDefinitionMapper;
import com.xspaceagi.compose.infra.dao.service.CustomTableDefinitionService;
import com.xspaceagi.system.spec.enums.YnEnum;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.ComposeException;
@Service
public class CustomTableDefinitionServiceImpl extends ServiceImpl<CustomTableDefinitionMapper, CustomTableDefinition>
implements CustomTableDefinitionService {
@Autowired
private CustomTableDefinitionMapper customTableDefinitionMapper;
@Override
public List<CustomTableDefinition> queryListByIds(List<Long> ids) {
LambdaQueryWrapper<CustomTableDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomTableDefinition::getYn, YnEnum.Y.getKey())
.in(CustomTableDefinition::getId, ids);
return this.list(queryWrapper);
}
@Override
public CustomTableDefinition queryOneInfoById(Long id) {
LambdaQueryWrapper<CustomTableDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomTableDefinition::getYn, YnEnum.Y.getKey())
.eq(CustomTableDefinition::getId, id);
return this.getOne(queryWrapper);
}
@Override
public Long updateInfo(CustomTableDefinition entity) {
var updateObj = this.getById(entity.getId());
if (Objects.isNull(updateObj)) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
entity.setCreated(null);
entity.setModified(null);
this.updateById(entity);
return entity.getId();
}
@Override
public Long addInfo(CustomTableDefinition entity) {
entity.setId(null);
entity.setCreated(null);
entity.setModified(null);
this.save(entity);
return entity.getId();
}
@Override
public void deleteById(Long id) {
var existObj = this.getById(id);
if (Objects.isNull(existObj)) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
this.removeById(id);
}
@Override
public List<CustomTableDefinition> pageQuery(Map<String, Object> queryMap, List<OrderItem> orderColumns, Long startIndex, Long pageSize) {
return customTableDefinitionMapper.queryList(queryMap, orderColumns, startIndex, pageSize);
}
@Override
public Long queryTotal(Map<String, Object> queryMap) {
return customTableDefinitionMapper.queryTotal(queryMap);
}
@Override
public List<CustomTableDefinition> queryListByCondition(CustomTableDefinition condition) {
LambdaQueryWrapper<CustomTableDefinition> wrapper = new LambdaQueryWrapper<>();
// 添加租户ID条件必填
wrapper.eq(CustomTableDefinition::getTenantId, condition.getTenantId());
// 添加空间ID条件可选
if (condition.getSpaceId() != null) {
wrapper.eq(CustomTableDefinition::getSpaceId, condition.getSpaceId());
}
// 添加表名模糊查询条件(可选)
if (StringUtils.hasText(condition.getTableName())) {
wrapper.like(CustomTableDefinition::getTableName, condition.getTableName());
}
// 添加表描述模糊查询条件(可选)
if (StringUtils.hasText(condition.getTableDescription())) {
wrapper.like(CustomTableDefinition::getTableDescription, condition.getTableDescription());
}
// 添加状态条件(可选)
if (condition.getStatus() != null) {
wrapper.eq(CustomTableDefinition::getStatus, condition.getStatus());
} else {
// 默认只查询启用状态的表定义
wrapper.eq(CustomTableDefinition::getStatus, 1);
}
// 按创建时间降序排序
wrapper.orderByDesc(CustomTableDefinition::getId);
return this.list(wrapper);
}
@Override
public List<CustomTableDefinition> queryListBySpaceId(Long spaceId) {
if (spaceId == null) {
return List.of();
}
LambdaQueryWrapper<CustomTableDefinition> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustomTableDefinition::getSpaceId, spaceId);
wrapper.eq(CustomTableDefinition::getYn, YnEnum.Y.getKey());
return this.list(wrapper);
}
}

View File

@@ -0,0 +1,856 @@
package com.xspaceagi.compose.infra.respository;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.dynamic.datasource.tx.DsPropagation;
import com.xspaceagi.compose.domain.repository.ICustomDorisTableRepository;
import com.xspaceagi.compose.domain.util.SqlParserUtil;
import com.xspaceagi.compose.domain.util.SqlParserUtil.SqlType;
import com.xspaceagi.compose.domain.util.TableDbWrapperUtil;
import com.xspaceagi.compose.sdk.enums.TableFieldTypeEnum;
import com.xspaceagi.compose.sdk.vo.data.ExecuteRawResultVo;
import com.xspaceagi.compose.sdk.vo.doris.DorisTableDefinitionVo;
import com.xspaceagi.compose.sdk.vo.doris.DorisTableFieldVo;
import com.xspaceagi.compose.sdk.vo.doris.DorisTableIndexVo;
import com.xspaceagi.compose.spec.constants.DorisConfigContants;
import com.xspaceagi.compose.spec.utils.ComposeExceptionUtils;
import com.xspaceagi.compose.spec.utils.DDLSqlParseUtil;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.ComposeException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
@Slf4j
@Repository
@DS("doris")
public class CustomDorisTableRepository implements ICustomDorisTableRepository {
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private TableDbWrapperUtil tableDbWrapperUtil;
@Lazy
@Resource
private CustomDorisTableRepository self;
@DS("doris")
@Override
public boolean hasData(String database, String table) {
String sql = "SELECT COUNT(*) FROM `" + database + "`.`" + table + "` LIMIT 1";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
return count != null && count > 0;
}
@DSTransactional(propagation = DsPropagation.NOT_SUPPORTED)
@Override
public boolean tableExists(String database, String table) {
String sql = tableDbWrapperUtil.getTableExistsSql(database, table);
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
return !result.isEmpty();
}
@Override
public void executeCreateTable(String createTableSql) {
jdbcTemplate.execute(createTableSql);
}
@Override
public void createUniqueIndex(String database, String table, String fieldName) {
String sql = tableDbWrapperUtil.buildCreateUniqueIndexSql(database, table, fieldName);
log.info("Create unique index, sql={}", sql);
jdbcTemplate.execute(sql);
}
@Override
public void dropTable(String database, String table) {
String sql = "DROP TABLE IF EXISTS `" + database + "`.`" + table + "`";
jdbcTemplate.execute(sql);
}
@Override
public void truncateTable(String database, String table) {
String sql = "TRUNCATE TABLE `" + database + "`.`" + table + "`";
jdbcTemplate.execute(sql);
}
@Override
public boolean isFieldValueUnique(String database, String table, String fieldName, String fieldValue) {
String sql = "SELECT COUNT(*) FROM `" + database + "`.`" + table + "` WHERE `" + fieldName + "` = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, fieldValue);
return count != null && count.equals(0);
}
@Override
public void executeAlterTable(String database, String table, String alterSql) {
try {
log.debug("Execute Doris ALTER SQL: {}", alterSql);
jdbcTemplate.execute(alterSql);
} catch (Exception e) {
log.error("Doris schema change error, database: {}, table: {}, sql: {}", database, table, alterSql, e);
var errorMessage = ComposeExceptionUtils.getRootErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, errorMessage);
}
}
@Override
public int deleteTableDataById(String database, String table, Long id) {
try {
String sql = "DELETE FROM `" + database + "`.`" + table + "` WHERE id = ?";
return jdbcTemplate.update(sql, id);
} catch (Exception e) {
log.error("Doris delete row error, database: {}, table: {}, id: {}", database, table, id, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "删除数据失败");
}
}
@Override
public DorisTableDefinitionVo getTableDefinition(String database, String table) {
try {
// 1. 获取表的建表 DDL
String createTableDdl = getCreateTableDdl(database, table);
log.info("Table DDL: database={}, table={}, ddl={}", database, table, createTableDdl);
// 2. 获取表的字段信息
String descTableSql = "DESC `" + database + "`.`" + table + "`";
List<Map<String, Object>> fieldsResult = jdbcTemplate.queryForList(descTableSql);
// 3. 获取表的索引信息
String showIndexSql = "SHOW INDEX FROM `" + database + "`.`" + table + "`";
List<Map<String, Object>> indexesResult = jdbcTemplate.queryForList(showIndexSql);
// 4. 构建返回对象
DorisTableDefinitionVo definition = new DorisTableDefinitionVo();
definition.setDatabase(database);
definition.setTable(table);
definition.setCreateTableDdl(createTableDdl);
// 调用新的工具类方法进行解析
DDLSqlParseUtil.parseCreateTableDdl(createTableDdl, definition);
// 构建字段列表
List<DorisTableFieldVo> fields = new ArrayList<>();
for (Map<String, Object> field : fieldsResult) {
var fieldType = (String) field.get("Type");
// 根据Doris/MySQL 类型获取枚举值
var fieldTypeEnum = TableFieldTypeEnum.tryGetByDorisType(fieldType);
var fieldTypeInteger = Optional.ofNullable(fieldTypeEnum).map(TableFieldTypeEnum::getCode).orElse(null);
DorisTableFieldVo fieldVo = DorisTableFieldVo.builder()
.fieldName((String) field.get("Field"))
.fieldType(fieldTypeInteger)
.fieldTypeDesc(fieldType)
.nullable("YES".equals(field.get("Null")))
.defaultValue((String) field.get("Default"))
.comment((String) field.get("Comment"))
.build();
fields.add(fieldVo);
}
definition.setFields(fields);
// 构建索引列表
Map<String, DorisTableIndexVo> indexMap = new HashMap<>();
for (Map<String, Object> index : indexesResult) {
String indexName = (String) index.get("Key_name");
String column = (String) index.get("Column_name");
String indexType = (String) index.get("Index_type");
String indexComment = (String) index.get("Index_comment");
DorisTableIndexVo indexVo = indexMap.computeIfAbsent(indexName, k -> {
DorisTableIndexVo vo = new DorisTableIndexVo();
vo.setIndexName(indexName);
vo.setIndexType(indexType);
vo.setComment(indexComment);
vo.setIndexColumns(column);
return vo;
});
// 如果已存在,则追加列名
if (!column.equals(indexVo.getIndexColumns())) {
indexVo.setIndexColumns(indexVo.getIndexColumns() + "," + column);
}
}
definition.setIndexes(new ArrayList<>(indexMap.values()));
return definition;
} catch (Exception e) {
log.error("Get Doris table definition error, database: {}, table: {}", database, table, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "获取表定义失败");
}
}
@Override
public int insertRow(String database, String table, Map<String, Object> data) {
try {
if (data == null || data.isEmpty()) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "插入数据不能为空");
}
StringBuilder sql = new StringBuilder("INSERT INTO `").append(database).append("`.`")
.append(table).append("` (");
StringBuilder values = new StringBuilder(") VALUES (");
List<Object> params = new ArrayList<>();
for (Map.Entry<String, Object> entry : data.entrySet()) {
sql.append("`").append(entry.getKey()).append("`,");
values.append("?,");
params.add(entry.getValue());
}
// 移除最后的逗号
sql.setLength(sql.length() - 1);
values.setLength(values.length() - 1);
sql.append(values).append(")");
return jdbcTemplate.update(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("Doris insert error, database: {}, table: {}, data: {}", database, table, data, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "插入数据失败");
}
}
@Override
public int batchInsert(String database, String table, List<Map<String, Object>> dataList) {
try {
if (CollectionUtils.isEmpty(dataList)) {
return 0;
}
// 获取所有字段名
Set<String> allFields = new HashSet<>();
for (Map<String, Object> data : dataList) {
allFields.addAll(data.keySet());
}
StringBuilder sql = new StringBuilder("INSERT INTO `").append(database).append("`.`")
.append(table).append("` (");
// 构建字段列表
for (String field : allFields) {
sql.append("`").append(field).append("`,");
}
sql.setLength(sql.length() - 1);
sql.append(") VALUES ");
// 构建值占位符
String valuePlaceholder = "(" + String.join(",", Collections.nCopies(allFields.size(), "?")) + ")";
List<String> valuePlaceholders = Collections.nCopies(dataList.size(), valuePlaceholder);
sql.append(String.join(",", valuePlaceholders));
// 准备参数
List<Object> params = new ArrayList<>();
for (Map<String, Object> data : dataList) {
for (String field : allFields) {
params.add(data.get(field));
}
}
return jdbcTemplate.update(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("Doris batch insert error, database: {}, table: {}, dataSize: {}",
database, table, dataList.size(), e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "批量插入数据失败");
}
}
@Override
public int updateRow(String database, String table, Long id, Map<String, Object> data) {
try {
if (data == null || data.isEmpty()) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "更新数据不能为空");
}
StringBuilder sql = new StringBuilder("UPDATE `").append(database).append("`.`")
.append(table).append("` SET ");
List<Object> params = new ArrayList<>();
for (Map.Entry<String, Object> entry : data.entrySet()) {
sql.append("`").append(entry.getKey()).append("` = ?,");
params.add(entry.getValue());
}
sql.setLength(sql.length() - 1);
sql.append(" WHERE id = ?");
params.add(id);
return jdbcTemplate.update(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("Doris update error, database: {}, table: {}, id: {}, data: {}",
database, table, id, data, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "更新数据失败");
}
}
@Override
public int batchUpdate(String database, String table, List<Map<String, Object>> dataList) {
try {
if (CollectionUtils.isEmpty(dataList)) {
return 0;
}
// 使用 CASE WHEN 语法进行批量更新
StringBuilder sql = new StringBuilder("UPDATE `").append(database).append("`.`")
.append(table).append("` SET ");
// 获取所有需要更新的字段排除id
Set<String> updateFields = new HashSet<>();
List<Object> params = new ArrayList<>();
for (Map<String, Object> data : dataList) {
updateFields.addAll(data.keySet());
}
updateFields.remove("id");
// 构建每个字段的 CASE WHEN 语句
boolean first = true;
for (String field : updateFields) {
if (!first) {
sql.append(", ");
}
first = false;
sql.append("`").append(field).append("` = CASE id ");
for (Map<String, Object> data : dataList) {
if (data.containsKey(field)) {
sql.append("WHEN ? THEN ? ");
params.add(data.get("id"));
params.add(data.get(field));
}
}
sql.append("ELSE `").append(field).append("` END");
}
// 添加 WHERE 条件
sql.append(" WHERE id IN (");
sql.append(String.join(",", Collections.nCopies(dataList.size(), "?")));
sql.append(")");
// 添加 id 参数
for (Map<String, Object> data : dataList) {
params.add(data.get("id"));
}
return jdbcTemplate.update(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("Doris batch update error, database: {}, table: {}, dataSize: {}",
database, table, dataList.size(), e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "批量更新数据失败");
}
}
@Override
public List<Map<String, Object>> queryByConditions(String database, String table,
Map<String, Object> conditions, String orderBy, Integer offset, Integer limit) {
try {
StringBuilder sql = new StringBuilder("SELECT * FROM `").append(database).append("`.`")
.append(table).append("`");
List<Object> params = new ArrayList<>();
// 添加查询条件
if (conditions != null && !conditions.isEmpty()) {
sql.append(" WHERE ");
boolean first = true;
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
if (!first) {
sql.append(" AND ");
}
first = false;
sql.append("`").append(entry.getKey()).append("` = ?");
params.add(entry.getValue());
}
}
// 添加排序
if (StringUtils.hasText(orderBy)) {
sql.append(" ORDER BY ").append(orderBy);
}
// 添加分页
if (offset != null && limit != null) {
sql.append(" LIMIT ?, ?");
params.add(offset);
params.add(limit);
}
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("Doris query error, database: {}, table: {}, conditions: {}",
database, table, conditions, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "查询数据失败");
}
}
@Override
public long countByConditions(String database, String table, Map<String, Object> conditions) {
try {
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM `").append(database)
.append("`.`").append(table).append("`");
List<Object> params = new ArrayList<>();
// 添加查询条件
if (conditions != null && !conditions.isEmpty()) {
sql.append(" WHERE ");
boolean first = true;
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
if (!first) {
sql.append(" AND ");
}
first = false;
sql.append("`").append(entry.getKey()).append("` = ?");
params.add(entry.getValue());
}
}
return jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray());
} catch (Exception e) {
log.error("Doris count error, database: {}, table: {}, conditions: {}",
database, table, conditions, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "统计数据失败");
}
}
@Override
public int deleteByConditions(String database, String table, Map<String, Object> conditions) {
try {
if (conditions == null || conditions.isEmpty()) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "删除条件不能为空");
}
StringBuilder sql = new StringBuilder("DELETE FROM `").append(database).append("`.`")
.append(table).append("` WHERE ");
List<Object> params = new ArrayList<>();
boolean first = true;
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
if (!first) {
sql.append(" AND ");
}
first = false;
sql.append("`").append(entry.getKey()).append("` = ?");
params.add(entry.getValue());
}
return jdbcTemplate.update(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("Doris delete error, database: {}, table: {}, conditions: {}",
database, table, conditions, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "删除数据失败");
}
}
@Override
public int batchDelete(String database, String table, List<Long> ids) {
try {
if (CollectionUtils.isEmpty(ids)) {
return 0;
}
String sql = "DELETE FROM `" + database + "`.`" + table + "` WHERE id IN (" +
String.join(",", Collections.nCopies(ids.size(), "?")) + ")";
return jdbcTemplate.update(sql, ids.toArray());
} catch (Exception e) {
log.error("Doris batch delete error, database: {}, table: {}, ids: {}",
database, table, ids, e);
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "批量删除数据失败");
}
}
@Override
public List<Map<String, Object>> queryAllData(String database, String table,
Map<String, Object> conditions, String orderBy, Integer limit) {
try {
// 如果 limit 无效,设置默认最大值 DorisConfigContants.EXPORT_EXCEL_DATA_MAX_ROWS
int effectiveLimit = (limit != null && limit > 0) ? limit : DorisConfigContants.EXPORT_EXCEL_DATA_MAX_ROWS;
// 调用现有带分页的方法offset 设为 0
return queryByConditions(database, table, conditions, orderBy, 0, effectiveLimit);
} catch (Exception e) {
// 异常处理和日志记录已在 queryByConditions 中完成,这里可以不再重复记录
// 但仍然需要向上抛出或包装异常
log.error("Doris query all error (limit={}), database: {}, table: {}, conditions: {}",
limit, database, table, conditions, e);
throw ComposeException.build(BizExceptionCodeEnum.composeQueryAllDataFailed);
}
}
@Override
public ExecuteRawResultVo executeRawQuery(String sql, Object... params) {
try {
if (!StringUtils.hasText(sql)) {
throw ComposeException.build(BizExceptionCodeEnum.composeSqlEmpty);
}
SqlType sqlType;
try {
sqlType = SqlParserUtil.getSqlType(sql);
} catch (JSQLParserException e) {
log.error("SQL parse failed: {}", sql, e);
var errorMessage = ComposeExceptionUtils.getRootErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, errorMessage);
}
switch (sqlType) {
case SELECT: {
log.debug("Raw Doris query SQL: {}, params: {}", sql, params);
var result = jdbcTemplate.queryForList(sql, params);
return ExecuteRawResultVo.builder()
.data(result)
.rowNum((long) result.size())
.build();
}
case UPDATE: {
log.debug("Raw Doris update SQL: {}, params: {}", sql, params);
int affectedRows = jdbcTemplate.update(sql, params);
return ExecuteRawResultVo.builder()
.rowNum((long) affectedRows)
.build();
}
case INSERT: {
log.debug("Raw Doris insert SQL: {}, params: {}", sql, params);
KeyHolder keyHolder = new GeneratedKeyHolder();
try {
int affectedRows = jdbcTemplate.update(connection -> {
var ps = connection.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS);
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
return ps;
}, keyHolder);
// 判断是否为批量插入通过检查keyHolder中的key数量
List<Map<String, Object>> keyList = keyHolder.getKeyList();
if (keyList != null && keyList.size() > 1) {
// 批量插入情况获取所有生成的主键ID
log.info("Batch insert, affected: {}, generated keys: {}", affectedRows, keyList.size());
// 提取所有生成的主键ID
List<Long> generatedIds = new ArrayList<>();
for (Map<String, Object> keyMap : keyList) {
// 通常主键列名为"GENERATED_KEY"或数据库特定的名称
Object keyObj = keyMap.get("GENERATED_KEY");
if (keyObj != null) {
if (keyObj instanceof Number) {
generatedIds.add(((Number) keyObj).longValue());
} else {
try {
generatedIds.add(Long.parseLong(keyObj.toString()));
} catch (NumberFormatException e) {
log.warn("Cannot parse generated PK: {}", keyObj);
}
}
}
}
// 返回第一个主键ID作为rowId并在data中返回所有主键ID
Long firstId = generatedIds.isEmpty() ? null : generatedIds.get(0);
Map<String, Object> resultData = new HashMap<>();
resultData.put("rowIds", generatedIds);
return ExecuteRawResultVo.builder()
.rowNum((long) affectedRows)
.rowId(firstId)
.data(Collections.singletonList(resultData))
.build();
} else {
// 单条插入情况,获取生成的主键
Number key = keyHolder.getKey();
if (key == null) {
log.warn("Auto-increment PK not returned after insert, SQL: {}", sql);
}
var rowId = key != null ? key.longValue() : null;
return ExecuteRawResultVo.builder()
.rowNum((long) affectedRows)
.rowId(rowId)
.build();
}
} catch (DuplicateKeyException e) {
// 因为唯一key重复,不抛异常,避免阻塞工作流任务;返回主键id是-1;
log.error("Unique key conflict on insert, SQL: {}", sql, e);
return ExecuteRawResultVo.builder()
.data(List.of())
.rowNum(0L)
.rowId(-1L)
.build();
}
}
case DELETE: {
log.debug("Raw Doris delete SQL: {}, params: {}", sql, params);
int affectedRows = jdbcTemplate.update(sql, params);
return ExecuteRawResultVo.builder()
.rowNum((long) affectedRows)
.build();
}
case DDL:
default:
log.error("DDL raw SQL not allowed: {}", sql);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlOnlyDmlAllowed);
}
} catch (BadSqlGrammarException e) {
log.error("DML SQL execution error, SQL: {}", sql, e);
var errorMessage = ComposeExceptionUtils.getUserFriendlyErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, errorMessage);
} catch (ComposeException ce) {
log.error("DML SQL execution error, SQL: {}", sql, ce);
throw ce;
} catch (Exception e) {
log.error("Raw Doris query SQL execution error, SQL: {}", sql, e);
var errorMessage = ComposeExceptionUtils.getUserFriendlyErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, errorMessage);
}
}
@DSTransactional(propagation = DsPropagation.NOT_SUPPORTED)
@Override
public String getCreateTableDdl(String database, String table) {
// 获取表的建表DDL语句
// 检查表是否存在
var existFlag = tableExists(database, table);
if (!existFlag) {
log.error("Table not found, database: {}, table: {}", database, table);
throw ComposeException.build(BizExceptionCodeEnum.composeTableDefinitionNotFound);
}
// 1. 获取表的基本信息
String showCreateTableSql = tableDbWrapperUtil.getShowCreateTableDdl(database, table);
Map<String, Object> createTableResult = jdbcTemplate.queryForMap(showCreateTableSql);
// 尝试不同的列名格式MySQL使用空格Doris使用下划线
String createTableDdl = (String) createTableResult.get("Create Table"); // MySQL格式
if (createTableDdl == null) {
createTableDdl = (String) createTableResult.get("Create_table"); // Doris格式
}
if (createTableDdl == null) {
throw ComposeException.build(BizExceptionCodeEnum.composeCannotGetCreateTableDdl);
}
return createTableDdl;
}
@Override
public int executeRawAdminSql(String sql) {
try {
if (!StringUtils.hasText(sql)) {
throw ComposeException.build(BizExceptionCodeEnum.composeSqlEmpty); // SQL不能为空
}
// 不再检查是否为 SELECT
log.warn("About to run raw admin/DML/DDL SQL (high risk): {}", sql);
int affectedRows = jdbcTemplate.update(sql);
log.info("Raw admin/DML/DDL SQL OK: {}, affected: {}", sql, affectedRows);
return affectedRows;
} catch (BadSqlGrammarException e) {
log.error("Raw admin/DML/DDL SQL error, SQL: {}", sql, e);
var errorMessage = ComposeExceptionUtils.getUserFriendlyErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, errorMessage);
} catch (ComposeException ce) {
// 直接抛出业务异常
log.error("Raw admin/DML/DDL SQL execution error, SQL: {}", sql, ce);
throw ce;
} catch (Exception e) {
log.error("Raw admin/DML/DDL SQL execution error, SQL: {}", sql, e);
// 使用用户友好的错误信息,将数据库技术错误转换为易于理解的提示
var errorMessage = ComposeExceptionUtils.getUserFriendlyErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeGenericMessage, errorMessage);
}
}
@Override
public Long getTableTotal(String database, String table) {
String sql = "SELECT COUNT(*) FROM " + database + "." + table;
return jdbcTemplate.queryForObject(sql, Long.class);
}
@Override
public boolean queryDorisTableRowDataById(String database, String table, Long id) {
String sql = "SELECT COUNT(*) FROM " + database + "." + table + " WHERE id = " + id;
return jdbcTemplate.queryForObject(sql, Long.class) > 0;
}
@Override
public void clearBusinessData(String database, String table) {
String sql = "TRUNCATE TABLE " + database + "." + table;
jdbcTemplate.update(sql);
}
@Override
public boolean existTableData(String database, String table) {
// 检查表是否存在
var existFlag = tableExists(database, table);
if (!existFlag) {
log.error("Table not found, database: {}, table: {}", database, table);
return false;
}
String sql = "SELECT * FROM " + database + "." + table + " LIMIT 1";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
return !result.isEmpty();
}
@Override
public void executeRawDDL(String sql) {
try {
// 只允许 DDL 类型
SqlType sqlType = SqlParserUtil.getSqlType(sql);
if (sqlType != SqlType.DDL) {
log.error("DDL only, sql={}", sql);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlOnlyDdl, sql);
}
jdbcTemplate.execute(sql);
} catch (ComposeException e) {
throw e;
} catch (JSQLParserException e) {
log.error("DDL execution error, sql={}", sql, e);
var errorMessage = ComposeExceptionUtils.getRootErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlParseFailed, errorMessage);
}
}
@Override
public Long countRawQuery(String sql) {
try {
return jdbcTemplate.queryForObject(sql, Long.class);
} catch (Exception e) {
log.error("Raw Doris query SQL execution error, SQL: {}", sql, e);
var errorMessage = ComposeExceptionUtils.getRootErrorMessage(e);
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, errorMessage);
}
}
/**
* 获取根错误信息
*
* @param e 异常
* @return 根错误信息
*/
private String getRootErrorMessage(Throwable e) {
Throwable cause = e;
while (cause.getCause() != null) {
cause = cause.getCause();
}
return cause.getMessage();
}
@Override
public void executeRawAdminSqls(List<String> sqls) {
// 直接调用FAIL_FAST策略保持原有行为
this.self.executeFailFast(sqls);
}
@Override
public void executeRawAdminSqlsWithStrategy(List<String> sqls,
ICustomDorisTableRepository.BatchExecuteStrategy strategy) {
if (CollectionUtils.isEmpty(sqls)) {
return;
}
log.info("Batch run {} SQL statements, strategy: {}", sqls.size(), strategy);
switch (strategy) {
case FAIL_FAST:
this.self.executeFailFast(sqls);
break;
case CONTINUE_ON_ERROR:
this.self.executeContinueOnError(sqls);
break;
case ATOMIC_BATCH:
this.self.executeAtomicBatch(sqls);
break;
default:
throw new IllegalArgumentException("Unsupported execution strategy: " + strategy);
}
}
/**
* 策略1遇到错误立即失败原有行为
*/
@DSTransactional(rollbackFor = Exception.class)
public void executeFailFast(List<String> sqls) {
// 将多条SQL语句合并成一条使用分号分隔
String batchSql = String.join(";", sqls);
batchSql = batchSql + ";";
this.self.executeRawAdminSql(batchSql);
log.info("Batch SQL done (FAIL_FAST)");
}
/**
* 策略2遇到错误继续执行其他SQL无事务保护
*/
@DSTransactional(propagation = DsPropagation.NOT_SUPPORTED) // 不使用事务
public void executeContinueOnError(List<String> sqls) {
int successCount = 0;
List<String> errorSqls = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
for (int i = 0; i < sqls.size(); i++) {
String sql = sqls.get(i);
try {
jdbcTemplate.update(sql);
successCount++;
log.debug("SQL[{}] OK: {}", i + 1, sql);
} catch (Exception e) {
String errorMsg = String.format("SQL[%d]执行失败: %s, 错误: %s",
i + 1, sql, e.getMessage());
log.error(errorMsg, e);
errorSqls.add(sql);
errorMessages.add(errorMsg);
}
}
log.info("Batch SQL done (CONTINUE_ON_ERROR): ok {}, failed {}",
successCount, errorSqls.size());
// 如果有失败的SQL抛出汇总异常
if (!errorSqls.isEmpty()) {
String summaryError = String.format("批量执行SQL部分失败: 成功%d条失败%d条。失败详情: %s",
successCount, errorSqls.size(),
String.join("; ", errorMessages));
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed, summaryError);
}
}
/**
* 策略3原子性批处理逐个执行但保持事务完整性
*/
@DSTransactional(rollbackFor = Exception.class)
public void executeAtomicBatch(List<String> sqls) {
try {
for (int i = 0; i < sqls.size(); i++) {
String sql = sqls.get(i);
try {
int affectedRows = jdbcTemplate.update(sql);
log.debug("SQL[{}] OK, affected {}: {}", i + 1, affectedRows, sql);
} catch (Exception e) {
String errorMsg = String.format("SQL[%d]执行失败: %s", i + 1, sql);
log.error(errorMsg, e);
// 在事务中抛出异常,会导致整个事务回滚
throw ComposeException.build(BizExceptionCodeEnum.composeSqlExecuteFailed,
errorMsg + ",错误: " + e.getMessage());
}
}
log.info("Batch SQL done (ATOMIC_BATCH): {} statements", sqls.size());
} catch (Exception e) {
log.error("Batch SQL failed, rolling back all", e);
throw e;
}
}
}

View File

@@ -0,0 +1,119 @@
package com.xspaceagi.compose.infra.respository;
import com.xspaceagi.compose.domain.model.CustomFieldDefinitionModel;
import com.xspaceagi.compose.domain.repository.ICustomFieldDefinitionRepository;
import com.xspaceagi.compose.infra.dao.mapper.CustomFieldDefinitionMapper;
import com.xspaceagi.compose.infra.dao.service.CustomFieldDefinitionService;
import com.xspaceagi.compose.infra.translator.CustomFieldDefinitionTranslator;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.ComposeException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Repository
public class CustomFieldDefinitionRepository implements ICustomFieldDefinitionRepository {
@Resource
private CustomFieldDefinitionService customFieldDefinitionService;
@Resource
private CustomFieldDefinitionMapper customFieldDefinitionMapper;
@Resource
private CustomFieldDefinitionTranslator customFieldDefinitionTranslator;
@Override
public List<CustomFieldDefinitionModel> queryListByIds(List<Long> ids) {
return customFieldDefinitionService.queryListByIds(ids)
.stream()
.map(customFieldDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
}
@Override
public CustomFieldDefinitionModel queryOneInfoById(Long id) {
return customFieldDefinitionTranslator.convertToModel(
customFieldDefinitionService.queryOneInfoById(id));
}
@Override
public List<CustomFieldDefinitionModel> queryListByTableId(Long tableId) {
return customFieldDefinitionService.queryListByTableId(tableId)
.stream()
.map(customFieldDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
}
@Override
public List<CustomFieldDefinitionModel> queryListByTableIds(List<Long> tableIds) {
return customFieldDefinitionService.queryListByTableIds(tableIds)
.stream()
.map(customFieldDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
}
@Override
public void batchAddInfo(List<CustomFieldDefinitionModel> modelList, UserContext userContext) {
modelList.forEach(entity -> {
entity.setCreated(null);
entity.setModified(null);
entity.setModifiedId(userContext.getUserId());
entity.setModifiedName(userContext.getUserName());
});
var entityList = modelList.stream()
.map(customFieldDefinitionTranslator::convertToEntity)
.collect(Collectors.toList());
customFieldDefinitionService.batchAddInfo(entityList);
}
@Override
public void batchUpdateInfo(List<CustomFieldDefinitionModel> modelList, UserContext userContext) {
modelList.forEach(entity -> {
entity.setCreated(null);
entity.setModified(null);
entity.setModifiedId(userContext.getUserId());
entity.setModifiedName(userContext.getUserName());
});
var entityList = modelList.stream()
.map(customFieldDefinitionTranslator::convertToEntity)
.collect(Collectors.toList());
customFieldDefinitionService.batchUpdateInfo(entityList);
}
@Override
public void deleteById(Long id, UserContext userContext) {
var existOne = customFieldDefinitionService.queryOneInfoById(id);
if (Objects.isNull(existOne)) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
customFieldDefinitionService.deleteById(id);
}
@Override
public void deleteByTableId(Long tableId) {
var existCount = customFieldDefinitionService.queryCountByTableId(tableId);
if (existCount == 0) {
log.info("No columns to drop, tableId={}", tableId);
} else {
customFieldDefinitionService.deleteByTableId(tableId);
}
}
}

View File

@@ -0,0 +1,389 @@
package com.xspaceagi.compose.infra.respository;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xspaceagi.compose.domain.dto.ColumnDefinitionResult;
import com.xspaceagi.compose.sdk.vo.data.FrontColumnDefineVo;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.ComposeException;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.xspaceagi.compose.spec.constants.DorisConfigContants;
import com.xspaceagi.compose.domain.dto.CustomEmptyTableVo;
import com.xspaceagi.compose.domain.model.CustomFieldDefinitionModel;
import com.xspaceagi.compose.domain.model.CustomTableDefinitionModel;
import com.xspaceagi.compose.domain.repository.ICustomFieldDefinitionRepository;
import com.xspaceagi.compose.domain.repository.ICustomTableDefinitionRepository;
import com.xspaceagi.compose.domain.util.TableDbWrapperUtil;
import com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition;
import com.xspaceagi.compose.infra.dao.mapper.CustomTableDefinitionMapper;
import com.xspaceagi.compose.infra.dao.service.CustomFieldDefinitionService;
import com.xspaceagi.compose.infra.dao.service.CustomTableDefinitionService;
import com.xspaceagi.compose.infra.translator.CustomFieldDefinitionTranslator;
import com.xspaceagi.compose.infra.translator.CustomTableDefinitionTranslator;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.YnEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Repository
public class CustomTableDefinitionRepository implements ICustomTableDefinitionRepository {
@Resource
private CustomFieldDefinitionService customFieldDefinitionService;
@Resource
private CustomTableDefinitionService customTableDefinitionService;
@Resource
private CustomTableDefinitionMapper customTableDefinitionMapper;
@Resource
private CustomTableDefinitionTranslator customTableDefinitionTranslator;
@Resource
private CustomFieldDefinitionTranslator customFieldDefinitionTranslator;
@Resource
private ICustomFieldDefinitionRepository customFieldDefinitionRepository;
@Resource
private TableDbWrapperUtil tableDbWrapperUtil;
@Lazy
@Resource
private ICustomTableDefinitionRepository self;
@Override
public List<CustomTableDefinitionModel> queryListByIds(List<Long> ids) {
List<CustomTableDefinitionModel> tableList = customTableDefinitionService.queryListByIds(ids)
.stream()
.map(customTableDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
return fillFieldList(tableList);
}
@Override
public CustomTableDefinitionModel queryOneInfoById(Long id) {
var entity = customTableDefinitionService.queryOneInfoById(id);
CustomTableDefinitionModel tableModel = customTableDefinitionTranslator.convertToModel(entity);
if (tableModel == null) {
return null;
}
return fillFieldList(Collections.singletonList(tableModel)).stream()
.findFirst()
.orElseThrow();
}
@DSTransactional(rollbackFor = Exception.class)
@Override
public Long updateInfo(CustomTableDefinitionModel entity, UserContext userContext) {
entity.setModified(null);
entity.setModifiedId(userContext.getUserId());
entity.setModifiedName(userContext.getUserName());
// 通常更新操作不需要返回完整的字段列表,如果需要则在此处调用 fillFieldList
var id = customTableDefinitionService.updateInfo(
customTableDefinitionTranslator.convertToEntity(entity));
// 批量修改 字段定义
return id;
}
@DSTransactional(rollbackFor = Exception.class)
@Override
public Long addInfo(CustomTableDefinitionModel entity) {
// 通常新增操作不需要返回完整的字段列表,如果需要则在此处调用 fillFieldList
entity.setDorisTable("");
var id = customTableDefinitionService.addInfo(
customTableDefinitionTranslator.convertToEntity(entity));
// 更新doris表名
var updateEntity = new CustomTableDefinition();
updateEntity.setId(id);
var dorisTableName = DorisConfigContants.obtainTableName(id);
updateEntity.setDorisTable(dorisTableName);
this.customTableDefinitionService.updateById(updateEntity);
return id;
}
@Override
public void deleteById(Long id) {
customTableDefinitionService.deleteById(id);
}
@Override
public List<CustomTableDefinitionModel> pageQuery(Map<String, Object> queryMap, List<OrderItem> orderColumns,
Long startIndex, Long pageSize) {
List<CustomTableDefinitionModel> tableList = customTableDefinitionService
.pageQuery(queryMap, orderColumns, startIndex, pageSize)
.stream()
.map(customTableDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
return fillFieldList(tableList);
}
@Override
public Long queryTotal(Map<String, Object> queryMap) {
return customTableDefinitionService.queryTotal(queryMap);
}
/**
* 为表定义模型列表填充字段列表信息
*
* @param tableList 表定义模型列表
* @return 填充字段列表后的表定义模型列表
*/
private List<CustomTableDefinitionModel> fillFieldList(List<CustomTableDefinitionModel> tableList) {
if (CollectionUtils.isEmpty(tableList)) {
return tableList;
}
// 提取表 ID
List<Long> tableIds = tableList.stream().map(CustomTableDefinitionModel::getId).collect(Collectors.toList());
// 查询表字段
List<CustomFieldDefinitionModel> fieldList = customFieldDefinitionRepository.queryListByTableIds(tableIds);
// 按 TableId 分组
Map<Long, List<CustomFieldDefinitionModel>> tableFieldMap = fieldList.stream()
.collect(Collectors.groupingBy(CustomFieldDefinitionModel::getTableId));
// 将表字段添加到表模型中,且 字段需要根据 sortIndex排序,sortIndex越小越靠前
tableList.forEach(table -> {
List<CustomFieldDefinitionModel> fields = tableFieldMap.getOrDefault(table.getId(),
Collections.emptyList());
fields.sort(Comparator.comparing(CustomFieldDefinitionModel::getSortIndex,
Comparator.nullsLast(Comparator.naturalOrder())));
table.setFieldList(fields);
});
return tableList;
}
@DSTransactional(rollbackFor = Exception.class)
@Override
public Long addEmptyTableInfo(CustomEmptyTableVo vo, UserContext userContext) {
var entity = new CustomTableDefinition();
entity.setTenantId(userContext.getTenantId());
entity.setSpaceId(vo.getSpaceId());
entity.setTableName(vo.getTableName());
entity.setTableDescription(vo.getTableDescription());
entity.setStatus(YnEnum.Y.getKey());
entity.setCreated(LocalDateTime.now());
entity.setCreatorId(userContext.getUserId());
entity.setCreatorName(userContext.getUserName());
entity.setDorisDatabase(DorisConfigContants.DEFAULT_DORIS_DATABASE);
var id = customTableDefinitionService.addInfo(entity);
// 更新doris表名
var updateEntity = new CustomTableDefinition();
updateEntity.setId(id);
var dorisTableName = DorisConfigContants.obtainTableName(id);
updateEntity.setDorisTable(dorisTableName);
this.customTableDefinitionService.updateById(updateEntity);
return id;
}
@Override
public List<CustomTableDefinitionModel> queryListByCondition(CustomTableDefinitionModel condition) {
log.debug("Query table definitions by condition: {}", condition);
// 将领域模型转换为实体对象
CustomTableDefinition queryCondition = customTableDefinitionTranslator.convertToEntity(condition);
// 调用 service 层查询
List<CustomTableDefinition> entityList = customTableDefinitionService.queryListByCondition(queryCondition);
// 转换为领域模型
if (CollectionUtils.isEmpty(entityList)) {
return Collections.emptyList();
}
List<CustomTableDefinitionModel> tableList = entityList.stream()
.map(customTableDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
// 填充字段列表信息
return fillFieldList(tableList);
}
@Override
public ColumnDefinitionResult getColumnDefinitions(Long tableId) {
// 1. 查询表定义
CustomTableDefinitionModel tableModel = this.self.queryOneInfoById(tableId);
if (tableModel == null) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "表定义不存在: " + tableId);
}
if (tableModel.getDorisDatabase() == null || tableModel.getDorisTable() == null) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "表定义缺少Doris库名或表名: " + tableId);
}
// 2. 查询字段定义
List<CustomFieldDefinitionModel> fieldModels = tableModel.getFieldList();
// 按 sortIndex 排序 (假设 CustomFieldDefinitionModel 有 sortIndex 字段)
fieldModels.sort(Comparator
.comparing(CustomFieldDefinitionModel::getSortIndex, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(CustomFieldDefinitionModel::getId)); // 添加ID作为次要排序保证稳定性
// 3. 转换为 ExcelColumnDefineVo 并提取有序字段名
List<FrontColumnDefineVo> columnDefines = new ArrayList<>();
List<String> orderedFieldNames = new ArrayList<>();
List<CustomFieldDefinitionModel> fields = new ArrayList<>();
for (CustomFieldDefinitionModel field : fieldModels) {
// 只包含启用的字段
if (field.getEnabledFlag() != null && field.getEnabledFlag() == 1) {
FrontColumnDefineVo vo = FrontColumnDefineVo.builder()
.fieldName(field.getFieldName())
.fieldDescription(field.getFieldDescription())
.fieldType(field.getFieldType())
.nullableFlag(field.getNullableFlag() != null && field.getNullableFlag() == 1)
.uniqueFlag(field.getUniqueFlag() != null && field.getUniqueFlag() == 1)
.enabledFlag(true) // 既然已经筛选了启用的,这里直接设为 true
.defaultValue(field.getDefaultValue())
.sortIndex(field.getSortIndex())
.build();
columnDefines.add(vo);
orderedFieldNames.add(field.getFieldName());
fields.add(field);
}
}
if (columnDefines.isEmpty()) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound, "表没有定义任何启用的字段: " + tableId);
}
return new ColumnDefinitionResult(tableModel, columnDefines, orderedFieldNames, fields);
}
@Override
public Long updateTableName(CustomTableDefinitionModel entity, UserContext userContext) {
// 1. 查询表定义
CustomTableDefinitionModel tableModel = this.self.queryOneInfoById(entity.getId());
if (tableModel == null) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
// 2. 更新表名称
var updateEntity = new CustomTableDefinition();
updateEntity.setId(entity.getId());
updateEntity.setTableName(entity.getTableName());
updateEntity.setTableDescription(entity.getTableDescription());
updateEntity.setIcon(entity.getIcon());
updateEntity.setModified(LocalDateTime.now());
updateEntity.setModifiedId(userContext.getUserId());
updateEntity.setModifiedName(userContext.getUserName());
this.customTableDefinitionService.updateById(updateEntity);
return entity.getId();
}
@DSTransactional(rollbackFor = Exception.class)
@Override
public Long copyTableDefinition(Long tableId, UserContext userContext) {
// 1. 查询表定义
CustomTableDefinitionModel tableModel = this.self.queryOneInfoById(tableId);
if (tableModel == null) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
// 获取字段定义
List<CustomFieldDefinitionModel> fieldModels = tableModel.getFieldList();
// 2. 创建新表
String newTableName = tableModel.getTableName() + "_copy";
tableModel.setTableName(newTableName);
tableModel.setId(null);
// 3. 新增新表定义
Long newTableId = this.addInfo(tableModel);
// 4. 去掉主键id,设置绑定的新 newTableId,然后入库
fieldModels.forEach(item -> {
item.setId(null);
item.setTableId(newTableId);
});
var fieldEntities = fieldModels.stream()
.map(customFieldDefinitionTranslator::convertToEntity)
.collect(Collectors.toList());
this.customFieldDefinitionService.batchAddInfo(fieldEntities);
// 5. 更新doris表名
var updateEntity = new CustomTableDefinition();
updateEntity.setId(newTableId);
var dorisTableName = DorisConfigContants.obtainTableName(newTableId);
updateEntity.setDorisTable(dorisTableName);
this.customTableDefinitionService.updateById(updateEntity);
return newTableId;
}
@Override
public void updateTableLastModified(Long tableId, UserContext userContext) {
//根据id查询,看数据是否存在
var existEntity = this.customTableDefinitionService.queryOneInfoById(tableId);
if (existEntity == null) {
throw ComposeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
var updateEntity = new CustomTableDefinition();
updateEntity.setId(tableId);
updateEntity.setModified(LocalDateTime.now());
updateEntity.setModifiedId(userContext.getUserId());
updateEntity.setModifiedName(userContext.getUserName());
this.customTableDefinitionService.updateById(updateEntity);
}
@Override
public List<CustomTableDefinitionModel> queryListBySpaceId(Long spaceId) {
List<CustomTableDefinition> entityList = customTableDefinitionService.queryListBySpaceId(spaceId);
if (CollectionUtils.isEmpty(entityList)) {
return Collections.emptyList();
}
var data = entityList.stream()
.map(customTableDefinitionTranslator::convertToModel)
.collect(Collectors.toList());
return fillFieldList(data);
}
@Override
public Long countTotalTables(Long userId) {
if (userId == null) {
return customTableDefinitionService.count();
}
LambdaQueryWrapper<CustomTableDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomTableDefinition::getCreatorId, userId);
return customTableDefinitionService.count(queryWrapper);
}
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.compose.infra.translator;
import com.xspaceagi.compose.domain.model.CustomFieldDefinitionModel;
import com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface CustomFieldDefinitionTranslator extends ICommonTranslator<CustomFieldDefinitionModel, CustomFieldDefinition> {
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.compose.infra.translator;
import com.xspaceagi.compose.domain.model.CustomTableDefinitionModel;
import com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface CustomTableDefinitionTranslator extends ICommonTranslator<CustomTableDefinitionModel, CustomTableDefinition> {
}

View File

@@ -0,0 +1,74 @@
package com.xspaceagi.compose.infra.translator.impl;
import com.xspaceagi.compose.domain.model.CustomFieldDefinitionModel;
import com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition;
import com.xspaceagi.compose.infra.translator.CustomFieldDefinitionTranslator;
import com.xspaceagi.system.spec.enums.YnEnum;
import org.springframework.stereotype.Service;
@Service
public class CustomFieldDefinitionTranslatorImpl implements CustomFieldDefinitionTranslator {
@Override
public CustomFieldDefinitionModel convertToModel(CustomFieldDefinition entity) {
if (entity == null) {
return null;
}
CustomFieldDefinitionModel customFieldDefinitionModel = CustomFieldDefinitionModel.builder()
.id(entity.getId())
.systemFieldFlag(entity.getSystemFieldFlag())
.tenantId(entity.getTenantId())
.spaceId(entity.getSpaceId())
.tableId(entity.getTableId())
.fieldName(entity.getFieldName())
.fieldDescription(entity.getFieldDescription())
.fieldType(entity.getFieldType())
.nullableFlag(entity.getNullableFlag())
.defaultValue(entity.getDefaultValue())
.uniqueFlag(entity.getUniqueFlag())
.enabledFlag(entity.getEnabledFlag())
.sortIndex(entity.getSortIndex())
.created(entity.getCreated())
.fieldStrLen(entity.getFieldStrLen())
.creatorId(entity.getCreatorId())
.creatorName(entity.getCreatorName())
.modified(entity.getModified())
.modifiedId(entity.getModifiedId())
.modifiedName(entity.getModifiedName())
.build();
return customFieldDefinitionModel;
}
@Override
public CustomFieldDefinition convertToEntity(CustomFieldDefinitionModel model) {
if (model == null) {
return null;
}
CustomFieldDefinition customFieldDefinition = CustomFieldDefinition.builder()
.id(model.getId())
.systemFieldFlag(model.getSystemFieldFlag())
.tenantId(model.getTenantId())
.spaceId(model.getSpaceId())
.tableId(model.getTableId())
.fieldName(model.getFieldName())
.fieldDescription(model.getFieldDescription())
.fieldType(model.getFieldType())
.nullableFlag(model.getNullableFlag())
.defaultValue(model.getDefaultValue())
.uniqueFlag(model.getUniqueFlag())
.enabledFlag(model.getEnabledFlag())
.sortIndex(model.getSortIndex())
.created(model.getCreated())
.fieldStrLen(model.getFieldStrLen())
.creatorId(model.getCreatorId())
.creatorName(model.getCreatorName())
.modified(model.getModified())
.modifiedId(model.getModifiedId())
.modifiedName(model.getModifiedName())
.yn(YnEnum.Y.getKey())
.build();
return customFieldDefinition;
}
}

View File

@@ -0,0 +1,62 @@
package com.xspaceagi.compose.infra.translator.impl;
import com.xspaceagi.compose.domain.model.CustomTableDefinitionModel;
import com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition;
import com.xspaceagi.compose.infra.translator.CustomTableDefinitionTranslator;
import com.xspaceagi.system.spec.enums.YnEnum;
import org.springframework.stereotype.Service;
@Service
public class CustomTableDefinitionTranslatorImpl implements CustomTableDefinitionTranslator {
@Override
public CustomTableDefinitionModel convertToModel(CustomTableDefinition entity) {
if (entity == null) {
return null;
}
CustomTableDefinitionModel customTableDefinitionModel = new CustomTableDefinitionModel();
customTableDefinitionModel.setId(entity.getId());
customTableDefinitionModel.setTenantId(entity.getTenantId());
customTableDefinitionModel.setSpaceId(entity.getSpaceId());
customTableDefinitionModel.setIcon(entity.getIcon());
customTableDefinitionModel.setTableName(entity.getTableName());
customTableDefinitionModel.setTableDescription(entity.getTableDescription());
customTableDefinitionModel.setDorisDatabase(entity.getDorisDatabase());
customTableDefinitionModel.setDorisTable(entity.getDorisTable());
customTableDefinitionModel.setStatus(entity.getStatus());
customTableDefinitionModel.setCreated(entity.getCreated());
customTableDefinitionModel.setCreatorId(entity.getCreatorId());
customTableDefinitionModel.setCreatorName(entity.getCreatorName());
customTableDefinitionModel.setModified(entity.getModified());
customTableDefinitionModel.setModifiedId(entity.getModifiedId());
customTableDefinitionModel.setModifiedName(entity.getModifiedName());
return customTableDefinitionModel;
}
@Override
public CustomTableDefinition convertToEntity(CustomTableDefinitionModel model) {
if (model == null) {
return null;
}
CustomTableDefinition customTableDefinition = new CustomTableDefinition();
customTableDefinition.setId(model.getId());
customTableDefinition.setTenantId(model.getTenantId());
customTableDefinition.setSpaceId(model.getSpaceId());
customTableDefinition.setIcon(model.getIcon());
customTableDefinition.setTableName(model.getTableName());
customTableDefinition.setTableDescription(model.getTableDescription());
customTableDefinition.setDorisDatabase(model.getDorisDatabase());
customTableDefinition.setDorisTable(model.getDorisTable());
customTableDefinition.setStatus(model.getStatus());
customTableDefinition.setCreated(model.getCreated());
customTableDefinition.setCreatorId(model.getCreatorId());
customTableDefinition.setCreatorName(model.getCreatorName());
customTableDefinition.setModified(model.getModified());
customTableDefinition.setModifiedId(model.getModifiedId());
customTableDefinition.setModifiedName(model.getModifiedName());
customTableDefinition.setYn(YnEnum.Y.getKey());
return customTableDefinition;
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xspaceagi.compose.infra.dao.mapper.CustomFieldDefinitionMapper">
<resultMap id="BaseResultMap" type="com.xspaceagi.compose.infra.dao.entity.CustomFieldDefinition">
</resultMap>
</mapper>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xspaceagi.compose.infra.dao.mapper.CustomTableDefinitionMapper">
<resultMap id="BaseResultMap" type="com.xspaceagi.compose.infra.dao.entity.CustomTableDefinition">
</resultMap>
<!--后端业务逻辑,公共查询逻辑1-->
<sql id="font_base_where">
<if test="queryMap.spaceId != null">
and space_id = #{queryMap.spaceId}
</if>
<if test="queryMap.status != null">
and status = #{queryMap.status}
</if>
<if test="queryMap.tableName != null">
and table_name like concat('%',#{queryMap.tableName},'%')
</if>
<if test="queryMap.authSpaceIds != null and queryMap.authSpaceIds.size > 0">
and space_id in
<foreach collection="queryMap.authSpaceIds" item="spaceId" separator="," open="(" close=")">
#{spaceId}
</foreach>
</if>
<if test="queryMap.userId != null">
and creator_id = #{queryMap.userId}
</if>
<include refid="SysCommonSql.base_where"></include>
and yn = 1
</sql>
<select id="queryTotal" resultType="java.lang.Long">
select count(*)
from custom_table_definition
<where>
<include refid="font_base_where"></include>
</where>
</select>
<select id="queryList" resultMap="BaseResultMap">
select
*
from custom_table_definition
<where>
<include refid="font_base_where"></include>
</where>
<choose>
<when test="orderColumns != null and orderColumns.size > 0">
<foreach collection="orderColumns" item="entity" index="index" separator="," open="order by " close="">
<if test="entity != null">
${entity.column}
<if test="!entity.asc">
desc
</if>
</if>
</foreach>
</when>
<otherwise>
order by created desc
</otherwise>
</choose>
limit #{startIndex}, #{pageSize}
</select>
</mapper>

View File

@@ -0,0 +1,123 @@
package com.xspaceagi.compose.infra.respository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.jdbc.core.JdbcTemplate;
import com.xspaceagi.compose.sdk.vo.doris.DorisTableDefinitionVo;
@ExtendWith(MockitoExtension.class)
class CustomDorisTableRepositoryTest {
@Mock
private JdbcTemplate jdbcTemplate;
// 如果 CustomDorisTableRepository 依赖 self 进行 getCreateTableDdl 调用,
// 可能需要 @Spy 或其他方式处理,但这里我们假设 getCreateTableDdl 直接被 getTableDefinition 调用
// 或者我们将测试逻辑放在 parseCreateTableDdl 使其可见 (不推荐修改源码可见性)
// 这里选择通过 mock getTableDefinition 的依赖来测试其内部调用
@InjectMocks
private CustomDorisTableRepository customDorisTableRepository;
private final String MOCK_DB = "test_db";
private final String MOCK_TABLE = "agent_component_config";
private String mockDorisCreateTableDdl;
@BeforeEach
void setUp() {
// 模拟的 Doris SHOW CREATE TABLE 输出
mockDorisCreateTableDdl = "CREATE TABLE `" + MOCK_DB + "`.`" + MOCK_TABLE + "` (\n" +
" `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',\n" +
" `_tenant_id` BIGINT NULL DEFAULT '1' COMMENT '商户ID',\n" +
" `name` VARCHAR(64) NULL COMMENT '节点名称',\n" +
" `icon` VARCHAR(255) NULL COMMENT '组件图标',\n" +
" `description` TEXT NULL COMMENT '组件描述',\n" +
" `agent_id` BIGINT NULL COMMENT 'AgentID',\n" +
" `type` VARCHAR(64) NOT NULL COMMENT '组件类型',\n" +
" `target_id` BIGINT NULL COMMENT '关联的组件ID',\n" +
" `bind_config` STRING NULL COMMENT '组件绑定配置 (Using STRING for JSON in Doris)',\n" +
" `exception_out` TINYINT NULL DEFAULT '0' COMMENT '异常是否抛出,中断主要流程',\n" +
" `fallback_msg` TEXT NULL COMMENT '异常时兜底内容',\n" +
" `modified` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Modified Time',\n" +
" `created` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Created Time'\n" +
") ENGINE=OLAP\n" +
"DUPLICATE KEY(`id`, `_tenant_id`)\n" +
"COMMENT '智能体组件配置 - Doris Version'\n" +
"DISTRIBUTED BY HASH(`id`, `type`) BUCKETS 16\n" +
"PROPERTIES (\n" +
" \"replication_num\" = \"1\",\n" +
" \"in_memory\" = \"false\",\n" +
" \"storage_format\" = \"V2\",\n" +
" \"function_column.sequence_type\" = \"auto_increment\"\n" +
");";
}
@Test
void getTableDefinition_ShouldParseDorisDdlCorrectly() {
// --- Arrange ---
// 1. Mock SHOW CREATE TABLE result
when(jdbcTemplate.queryForMap(eq("SHOW CREATE TABLE `" + MOCK_DB + "`.`" + MOCK_TABLE + "`")))
.thenReturn(Map.of("Create Table", mockDorisCreateTableDdl));
// 2. Mock DESC table result (can be simplified if only testing parsing)
List<Map<String, Object>> fieldsResult = new ArrayList<>(); // Populate if needed
when(jdbcTemplate.queryForList(eq("DESC `" + MOCK_DB + "`.`" + MOCK_TABLE + "`")))
.thenReturn(fieldsResult);
// 3. Mock SHOW INDEX result (can be simplified if only testing parsing)
List<Map<String, Object>> indexesResult = new ArrayList<>(); // Populate if needed
when(jdbcTemplate.queryForList(eq("SHOW INDEX FROM `" + MOCK_DB + "`.`" + MOCK_TABLE + "`")))
.thenReturn(indexesResult);
// --- Act ---
DorisTableDefinitionVo actualDefinition = customDorisTableRepository.getTableDefinition(MOCK_DB, MOCK_TABLE);
// --- Assert ---
assertNotNull(actualDefinition);
// Verify fields parsed by parseCreateTableDdl
assertEquals("智能体组件配置 - Doris Version", actualDefinition.getComment());
assertEquals("OLAP", actualDefinition.getEngine());
assertEquals(16, actualDefinition.getBuckets());
assertEquals(1, actualDefinition.getReplicationNum()); // Parsed from properties
assertEquals(Arrays.asList("id", "type"), actualDefinition.getDistributedKeys());
assertEquals(Arrays.asList("id", "_tenant_id"), actualDefinition.getDuplicateKeys());
// Verify properties map
assertNotNull(actualDefinition.getProperties());
assertEquals(4, actualDefinition.getProperties().size());
assertEquals("1", actualDefinition.getProperties().get("replication_num"));
assertEquals("false", actualDefinition.getProperties().get("in_memory"));
assertEquals("V2", actualDefinition.getProperties().get("storage_format"));
assertEquals("auto_increment", actualDefinition.getProperties().get("function_column.sequence_type"));
// Verify the original DDL is stored
assertEquals(mockDorisCreateTableDdl, actualDefinition.getCreateTableDdl());
// Verify basic info
assertEquals(MOCK_DB, actualDefinition.getDatabase());
assertEquals(MOCK_TABLE, actualDefinition.getTable());
// Fields and Indexes list can be asserted if mock data was provided for them
// assertNotNull(actualDefinition.getFields());
// assertNotNull(actualDefinition.getIndexes());
}
// TODO: Add more tests for edge cases, e.g., missing optional clauses in DDL,
// different property formats, different key definitions.
}