mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-15 20:37:52 +08:00
* 🐛 fix(data-viewer): 修复ClickHouse尾部分页异常并增强DuckDB复杂类型兼容 - DataViewer 新增 ClickHouse 反向分页策略,修复最后页与倒数页查询失败 - DuckDB 查询失败时按列类型生成安全 SELECT,复杂类型转 VARCHAR 重试 - 分页状态统一使用 currentPage 回填,避免页码与总数推导不一致 - 增强查询异常日志与重试路径,降低大表场景卡顿与误报 * ✨ feat(frontend-driver): 驱动管理支持快速搜索并优化信息展示 - 新增搜索框,支持按 DuckDB/ClickHouse 等关键字快速定位驱动 - 显示“匹配 x / y”统计与无结果提示 - 优化头部区域排版,提升透明/暗色场景下的视觉对齐 * 🔧 fix(connection-modal): 修复多数据源URI导入解析并校正Oracle服务名校验 - 新增单主机URI解析映射,兼容 postgres/postgresql、sqlserver、redis、tdengine、dameng(dm)、kingbase、highgo、vastbase、clickhouse、oracle - 抽取 parseSingleHostUri 复用逻辑,统一 host/port/user/password/database 回填行为 - Oracle 连接新增服务名必填校验,移除“服务名为空回退用户名”的隐式逻辑 - 连接弹窗补充 Oracle 服务名输入项与 URI 示例 * 🐛 fix(query-export): 修复查询结果导出卡住并统一按数据源能力控制导出路径 - 查询结果页导出增加稳定兜底,异常时确保 loading 关闭避免持续转圈 - DataGrid 导出逻辑按数据源能力分流,优先走后端 ExportQuery 并保留结果集导出降级 - QueryEditor 传递结果导出 SQL,保证查询结果导出范围与当前结果一致 - 后端补充 ExportData/ExportQuery 关键日志,提升导出链路可观测性 * 🐛 fix(precision): 修复查询链路与分页统计的大整数精度丢失 - 代理响应数据解码改为 UseNumber,避免默认 float64 吞精度 - 统一归一化 json.Number 与超界整数,超出 JS 安全范围转字符串 - 修复 DataViewer 总数解析,超大值不再误转 Number 参与分页 - refs #142 * 🐛 fix(driver-manager): 修复驱动管理网络告警重复并强化代理引导 - 新增下载链路域名探测,区分“GitHub可达但驱动下载链路不可达” - 网络不可达场景仅保留红色强提醒,移除重复二级告警 - 强提醒增加“打开全局代理设置”入口,优先引导使用 GoNavi 全局代理 - 统一网络检测与目录说明提示图标尺寸,修复加载期视觉不一致 - refs #141 * ♻️ refactor(frontend-interaction): 统一标签拖拽与暗色主题交互实现 - 重构Tab拖拽排序实现,统一为可配置拖拽引擎 - 规范拖拽与点击事件边界,提升交互一致性 - 统一多组件暗色透明样式策略,减少硬编码色值 - 提升Redis/表格/连接面板在透明模式下的观感一致性 - refs #144 * ♻️ refactor(update-state): 重构在线更新状态流并按版本统一进度展示 - 重构更新检查与下载状态同步流程,减少前后端状态分叉 - 进度展示严格绑定 latestVersion,避免跨版本状态串用 - 优化 about 打开场景的静默检查状态回填逻辑 - 统一下载弹窗关闭/后台隐藏行为 - 保持现有安装流程并补齐目录打开能力 * 🎨 style(sidebar-log): 将SQL执行日志入口调整为悬浮胶囊样式 - 移除侧栏底部整条日志入口容器 - 新增悬浮按钮阴影/边框/透明背景并适配明暗主题 - 为树区域预留底部空间避免入口遮挡内容 * ✨ feat(redis-cluster): 支持集群模式逻辑多库隔离与 0-15 库切换 - 前端恢复 Redis 集群场景下 db0-db15 的数据库选择与展示 - 后端新增集群逻辑库命名空间前缀映射,统一 key/pattern 读写隔离 - 覆盖扫描、读取、写入、删除、重命名等核心操作的键映射规则 - 集群命令通道支持 SELECT 逻辑切库与 FLUSHDB 逻辑库清空 - refs #145 * ✨ feat(DataGrid): 大数据表虚拟滚动性能优化及UI一致性修复 - 启用动态虚拟滚动(数据量≥500行自动切换),解决万行数据表卡顿问题 - 虚拟模式下EditableCell改用div渲染,CSS选择器从元素级改为类级适配虚拟DOM - 修复虚拟模式双水平滚动条:样式化rc-virtual-list内置滚动条为胶囊外观,禁用自定义外部滚动条 - 为rc-virtual-list水平滚动条添加鼠标滚轮支持(MutationObserver + marginLeft驱动) - 修复白色主题透明模式下列名悬浮Tooltip对比度不足的问题 - 新增白色主题全局滚动条样式适配透明模式(App.css) - App.tsx主题token与组件样式优化 - refs #147 * 🔧 chore(app): 清理 App.tsx 类型告警并收敛前端壳层实现 - 清除未使用代码和冗余状态 - 替换弃用 API 以消除 IDE 提示 - 显式处理浮动 Promise 避免告警 - 保持现有更新检查和代理设置行为不变 * 🔧 fix(ci): 修复 Windows AMD64 下 DuckDB 驱动构建链路 - 将 DuckDB 工具链准备切换为优先使用 MSYS2 - 增加 gcc 和 g++ 存在性校验与版本验证 - 在 MSYS2 异常时回退 Chocolatey 安装 MinGW - 保持 Windows ARM64 跳过 DuckDB 构建与平台支持一致 * 🔧 fix(ci): 修复 Windows AMD64 下 DuckDB 驱动构建链路 - 将 DuckDB 工具链准备切换为优先使用 MSYS2 - 增加 gcc 和 g++ 存在性校验与版本验证 - 在 MSYS2 异常时回退 Chocolatey 安装 MinGW - 保持 Windows ARM64 跳过 DuckDB 构建与平台支持一致 * 🔧 fix(ci): 修复 Windows AMD64 下 DuckDB 驱动构建工具链 - 将 DuckDB 编译链从 MINGW64 切换为 MSYS2 UCRT64 - 修正 Windows AMD64 的 gcc 和 g++ 探测路径 - 增加 DuckDB 编译器版本校验步骤 * 📝 docs(contributing): 补充中英文贡献指南并统一 README 入口 - 新增英文版 CONTRIBUTING.md 作为正式贡献文档 - 新增中文版 CONTRIBUTING.zh-CN.md 作为中文贡献说明 - 调整 README 和 README.zh-CN 的贡献入口指向对应语言文档 * - feat(connection,metadata,kingbase): 增强多数据源连接能力并修复金仓/达梦/Oracle/ClickHouse兼容性问题 (#188) (#190) * feat(http-tunnel): 支持独立 HTTP 隧道连接并覆盖多数据源 refs #168 * fix(kingbase-data-grid): 修复金仓打开表卡顿并降低对象渲染开销 refs #178 * fix(kingbase-transaction): 修复金仓事务提交重复引号导致语法错误 refs #176 * fix(driver-agent): 修复老版本 Win10 升级后金仓驱动代理启动失败 refs #177 * chore(ci): 新增手动触发的 macOS 测试构建工作流 * chore(ci): 允许测试工作流在当前分支自动触发 * fix(query-editor): 修复 SQL 编辑中光标随机跳到末尾 refs #185 * feat(data-sync): 增加差异 SQL 预览能力便于审核 refs #174 * fix(clickhouse-connect): 自动识别并回退 HTTP/Native 协议连接 refs #181 * fix(oracle-metadata): 修复视图与函数加载按 schema 过滤异常 refs #155 * fix(dameng-databases): 修复显示全部库时数据库列表不完整 refs #154 * fix(connection,db-list): 统一处理空列表返回并修复达梦连接测试报错 refs #157 Co-authored-by: 辣条 <69459608+tianqijiuyun-latiao@users.noreply.github.com> * ✨ feat(release-notes): 支持自动生成 Release 更新说明并区分配置文件命名 * 🔁 chore(sync): 回灌 main 到 dev (#192) * - feat(connection,metadata,kingbase): 增强多数据源连接能力并修复金仓/达梦/Oracle/ClickHouse兼容性问题 (#188) * feat(http-tunnel): 支持独立 HTTP 隧道连接并覆盖多数据源 refs #168 * fix(kingbase-data-grid): 修复金仓打开表卡顿并降低对象渲染开销 refs #178 * fix(kingbase-transaction): 修复金仓事务提交重复引号导致语法错误 refs #176 * fix(driver-agent): 修复老版本 Win10 升级后金仓驱动代理启动失败 refs #177 * chore(ci): 新增手动触发的 macOS 测试构建工作流 * chore(ci): 允许测试工作流在当前分支自动触发 * fix(query-editor): 修复 SQL 编辑中光标随机跳到末尾 refs #185 * feat(data-sync): 增加差异 SQL 预览能力便于审核 refs #174 * fix(clickhouse-connect): 自动识别并回退 HTTP/Native 协议连接 refs #181 * fix(oracle-metadata): 修复视图与函数加载按 schema 过滤异常 refs #155 * fix(dameng-databases): 修复显示全部库时数据库列表不完整 refs #154 * fix(connection,db-list): 统一处理空列表返回并修复达梦连接测试报错 refs #157 * Release/0.5.3 (#191) --------- Co-authored-by: 辣条 <69459608+tianqijiuyun-latiao@users.noreply.github.com> Co-authored-by: Syngnat <92659908+Syngnat@users.noreply.github.com> * 🐛 fix(branch-sync): 修复 main 回灌 dev 时 mergeable 异步计算导致漏开自动合并 - 增加 mergeable 状态轮询,避免新建同步 PR 后立即返回 UNKNOWN - 在合并状态未稳定时输出中文告警与执行摘要 - 保持冲突分支、待计算分支与自动合并分支的处理路径清晰 * 🔁 chore(sync): 回灌 main 到 dev (#195) * - feat(connection,metadata,kingbase): 增强多数据源连接能力并修复金仓/达梦/Oracle/ClickHouse兼容性问题 (#188) * feat(http-tunnel): 支持独立 HTTP 隧道连接并覆盖多数据源 refs #168 * fix(kingbase-data-grid): 修复金仓打开表卡顿并降低对象渲染开销 refs #178 * fix(kingbase-transaction): 修复金仓事务提交重复引号导致语法错误 refs #176 * fix(driver-agent): 修复老版本 Win10 升级后金仓驱动代理启动失败 refs #177 * chore(ci): 新增手动触发的 macOS 测试构建工作流 * chore(ci): 允许测试工作流在当前分支自动触发 * fix(query-editor): 修复 SQL 编辑中光标随机跳到末尾 refs #185 * feat(data-sync): 增加差异 SQL 预览能力便于审核 refs #174 * fix(clickhouse-connect): 自动识别并回退 HTTP/Native 协议连接 refs #181 * fix(oracle-metadata): 修复视图与函数加载按 schema 过滤异常 refs #155 * fix(dameng-databases): 修复显示全部库时数据库列表不完整 refs #154 * fix(connection,db-list): 统一处理空列表返回并修复达梦连接测试报错 refs #157 * Release/0.5.3 (#191) * - chore(ci): 新增全平台测试包手动构建工作流 tianqijiuyun-latiao 今天 下午4:26 (#194) * feat(http-tunnel): 支持独立 HTTP 隧道连接并覆盖多数据源 refs #168 * fix(kingbase-data-grid): 修复金仓打开表卡顿并降低对象渲染开销 refs #178 * fix(kingbase-transaction): 修复金仓事务提交重复引号导致语法错误 refs #176 * fix(driver-agent): 修复老版本 Win10 升级后金仓驱动代理启动失败 refs #177 * chore(ci): 新增手动触发的 macOS 测试构建工作流 * chore(ci): 允许测试工作流在当前分支自动触发 * fix(query-editor): 修复 SQL 编辑中光标随机跳到末尾 refs #185 * feat(data-sync): 增加差异 SQL 预览能力便于审核 refs #174 * fix(clickhouse-connect): 自动识别并回退 HTTP/Native 协议连接 refs #181 * fix(oracle-metadata): 修复视图与函数加载按 schema 过滤异常 refs #155 * fix(dameng-databases): 修复显示全部库时数据库列表不完整 refs #154 * fix(connection,db-list): 统一处理空列表返回并修复达梦连接测试报错 refs #157 * fix(kingbase): 补齐主键识别并优化宽表卡顿 refs #176 refs #178 * fix(query-execution): 支持带前置注释的读查询结果识别 * chore(ci): 新增全平台测试包手动构建工作流 --------- Co-authored-by: 辣条 <69459608+tianqijiuyun-latiao@users.noreply.github.com> Co-authored-by: Syngnat <92659908+Syngnat@users.noreply.github.com> * ♻️ refactor(frontend-sync): 优化桌面交互细节并移除 main 回灌 dev 自动化 - 优化新建连接、主题设置、侧边栏工具区与 SQL 日志的界面表现 - 调整分页、筛选、透明模式与弹窗样式,统一整体交互层次 - 收口外观参数生效逻辑并补齐多组件适配 - 删除 sync-main-to-dev 工作流并同步维护者手动回灌说明 * feat: 统一筛选条件逻辑按钮宽度 (#201) * 🐛 fix(oracle-query): 修复 Oracle 表数据分页 SQL 兼容问题 refs #196 (#202) * ✨ feat(datasource): 支持 DuckDB Parquet 文件模式并优化弹窗打开链路 - 统一 DuckDB 文件库与 Parquet 文件接入能力 - 补充 URI、文件选择、只读挂载与连接缓存键处理 - 去掉数据源卡片点击前的同步驱动查询,修复打开卡顿 * ✨ feat(datasource): 支持 DuckDB Parquet 文件模式并优化弹窗打开链路 - 统一 DuckDB 文件库与 Parquet 文件接入能力 - 补充 URI、文件选择、只读挂载与连接缓存键处理 - 去掉数据源卡片点击前的同步驱动查询,修复打开卡顿 - refs #166 * 🐛 fix(dameng): 修复达梦连接成功后数据库列表为空问题 - 调整达梦数据库列表获取策略,优先回退查询当前 schema 与当前用户 - 保留可见用户与 owner 聚合逻辑,兼容低权限账号场景 - 补充前端空列表提示与后端单元测试,降低排查成本 - close #203 * ✨ feat(data-sync): 扩展跨库迁移链路并优化数据同步交互 - 统一同库同步与跨库迁移入口,补充模式区分与风险提示 - 扩展 ClickHouse 与 PG-like 双向迁移,并新增 PG-like、ClickHouse、TDengine 到 MongoDB 的迁移路由 - 完善 TDengine 目标端建表规划、回归测试与需求追踪文档 - refs #51 --------- Co-authored-by: Syngnat <yangguofeng919@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: 辣条 <69459608+tianqijiuyun-latiao@users.noreply.github.com> Co-authored-by: TSS <266256496+Zencok@users.noreply.github.com>
1039 lines
36 KiB
Go
1039 lines
36 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
"GoNavi-Wails/internal/db"
|
||
"GoNavi-Wails/internal/logger"
|
||
"GoNavi-Wails/internal/utils"
|
||
)
|
||
|
||
// Generic DB Methods
|
||
|
||
func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResult {
|
||
// 连接测试需要强制 ping,避免缓存命中但连接已失效时误判成功。
|
||
_, err := a.getDatabaseForcePing(config)
|
||
if err != nil {
|
||
logger.Error(err, "DBConnect 连接失败:%s", formatConnSummary(config))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
logger.Infof("DBConnect 连接成功:%s", formatConnSummary(config))
|
||
return connection.QueryResult{Success: true, Message: "连接成功"}
|
||
}
|
||
|
||
func (a *App) TestConnection(config connection.ConnectionConfig) connection.QueryResult {
|
||
_, err := a.getDatabaseForcePing(config)
|
||
if err != nil {
|
||
logger.Error(err, "TestConnection 连接测试失败:%s", formatConnSummary(config))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
logger.Infof("TestConnection 连接测试成功:%s", formatConnSummary(config))
|
||
return connection.QueryResult{Success: true, Message: "连接成功"}
|
||
}
|
||
|
||
func (a *App) MongoDiscoverMembers(config connection.ConnectionConfig) connection.QueryResult {
|
||
config.Type = "mongodb"
|
||
|
||
dbInst, err := a.getDatabaseForcePing(config)
|
||
if err != nil {
|
||
logger.Error(err, "MongoDiscoverMembers 获取连接失败:%s", formatConnSummary(config))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
discoverable, ok := dbInst.(interface {
|
||
DiscoverMembers() (string, []connection.MongoMemberInfo, error)
|
||
})
|
||
if !ok {
|
||
return connection.QueryResult{Success: false, Message: "当前 MongoDB 驱动不支持成员发现"}
|
||
}
|
||
|
||
replicaSet, members, err := discoverable.DiscoverMembers()
|
||
if err != nil {
|
||
logger.Error(err, "MongoDiscoverMembers 执行失败:%s", formatConnSummary(config))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
data := map[string]interface{}{
|
||
"replicaSet": replicaSet,
|
||
"members": members,
|
||
}
|
||
|
||
logger.Infof("MongoDiscoverMembers 成功:%s 成员数=%d 副本集=%s", formatConnSummary(config), len(members), replicaSet)
|
||
return connection.QueryResult{
|
||
Success: true,
|
||
Message: fmt.Sprintf("发现 %d 个成员", len(members)),
|
||
Data: data,
|
||
}
|
||
}
|
||
|
||
func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string) connection.QueryResult {
|
||
runConfig := config
|
||
runConfig.Database = ""
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
escapedDbName := strings.ReplaceAll(dbName, "`", "``")
|
||
query := fmt.Sprintf("CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", escapedDbName)
|
||
dbType := strings.ToLower(strings.TrimSpace(runConfig.Type))
|
||
if dbType == "postgres" || dbType == "kingbase" || dbType == "highgo" || dbType == "vastbase" {
|
||
escapedDbName = strings.ReplaceAll(dbName, `"`, `""`)
|
||
query = fmt.Sprintf("CREATE DATABASE \"%s\"", escapedDbName)
|
||
} else if dbType == "tdengine" {
|
||
query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName))
|
||
} else if dbType == "clickhouse" {
|
||
query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName))
|
||
} else if dbType == "mariadb" || dbType == "diros" {
|
||
// MariaDB uses same syntax as MySQL
|
||
} else if dbType == "sphinx" {
|
||
return connection.QueryResult{Success: false, Message: "Sphinx 暂不支持创建数据库"}
|
||
}
|
||
|
||
_, err = dbInst.Exec(query)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Message: "Database created successfully"}
|
||
}
|
||
|
||
func resolveDDLDBType(config connection.ConnectionConfig) string {
|
||
dbType := strings.ToLower(strings.TrimSpace(config.Type))
|
||
if dbType != "custom" {
|
||
return dbType
|
||
}
|
||
|
||
driver := strings.ToLower(strings.TrimSpace(config.Driver))
|
||
switch driver {
|
||
case "postgresql", "postgres", "pg", "pq", "pgx":
|
||
return "postgres"
|
||
case "dm", "dameng", "dm8":
|
||
return "dameng"
|
||
case "sqlite3", "sqlite":
|
||
return "sqlite"
|
||
case "sphinxql":
|
||
return "sphinx"
|
||
case "diros", "doris":
|
||
return "diros"
|
||
case "kingbase", "kingbase8", "kingbasees", "kingbasev8":
|
||
return "kingbase"
|
||
case "highgo":
|
||
return "highgo"
|
||
case "vastbase":
|
||
return "vastbase"
|
||
}
|
||
|
||
switch {
|
||
case strings.Contains(driver, "postgres"):
|
||
return "postgres"
|
||
case strings.Contains(driver, "kingbase"):
|
||
return "kingbase"
|
||
case strings.Contains(driver, "highgo"):
|
||
return "highgo"
|
||
case strings.Contains(driver, "vastbase"):
|
||
return "vastbase"
|
||
case strings.Contains(driver, "sqlite"):
|
||
return "sqlite"
|
||
case strings.Contains(driver, "sphinx"):
|
||
return "sphinx"
|
||
case strings.Contains(driver, "diros"), strings.Contains(driver, "doris"):
|
||
return "diros"
|
||
default:
|
||
return driver
|
||
}
|
||
}
|
||
|
||
func normalizeSchemaAndTableByType(dbType string, dbName string, tableName string) (string, string) {
|
||
rawTable := strings.TrimSpace(tableName)
|
||
rawDB := strings.TrimSpace(dbName)
|
||
if rawTable == "" {
|
||
return rawDB, rawTable
|
||
}
|
||
|
||
if parts := strings.SplitN(rawTable, ".", 2); len(parts) == 2 {
|
||
schema := strings.TrimSpace(parts[0])
|
||
table := strings.TrimSpace(parts[1])
|
||
if schema != "" && table != "" {
|
||
return schema, table
|
||
}
|
||
}
|
||
|
||
switch dbType {
|
||
case "postgres", "kingbase", "highgo", "vastbase":
|
||
return "public", rawTable
|
||
default:
|
||
return rawDB, rawTable
|
||
}
|
||
}
|
||
|
||
func quoteTableIdentByType(dbType string, schema string, table string) string {
|
||
s := strings.TrimSpace(schema)
|
||
t := strings.TrimSpace(table)
|
||
if s == "" {
|
||
return quoteIdentByType(dbType, t)
|
||
}
|
||
return fmt.Sprintf("%s.%s", quoteIdentByType(dbType, s), quoteIdentByType(dbType, t))
|
||
}
|
||
|
||
func buildRunConfigForDDL(config connection.ConnectionConfig, dbType string, dbName string) connection.ConnectionConfig {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
if strings.EqualFold(strings.TrimSpace(config.Type), "custom") {
|
||
// custom 连接的 dbName 语义依赖 driver,尽量在常见驱动上对齐内置类型行为。
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "vastbase", "dameng", "clickhouse":
|
||
if strings.TrimSpace(dbName) != "" {
|
||
runConfig.Database = strings.TrimSpace(dbName)
|
||
}
|
||
}
|
||
}
|
||
return runConfig
|
||
}
|
||
|
||
func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string, newName string) connection.QueryResult {
|
||
oldName = strings.TrimSpace(oldName)
|
||
newName = strings.TrimSpace(newName)
|
||
if oldName == "" || newName == "" {
|
||
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
|
||
}
|
||
if strings.EqualFold(oldName, newName) {
|
||
return connection.QueryResult{Success: false, Message: "新旧数据库名称不能相同"}
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx":
|
||
return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/Doris/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"}
|
||
case "postgres", "kingbase", "highgo", "vastbase":
|
||
if strings.EqualFold(strings.TrimSpace(config.Database), oldName) {
|
||
return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"}
|
||
}
|
||
runConfig := config
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
sql := fmt.Sprintf("ALTER DATABASE %s RENAME TO %s", quoteIdentByType(dbType, oldName), quoteIdentByType(dbType, newName))
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "数据库重命名成功"}
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名数据库", dbType)}
|
||
}
|
||
}
|
||
|
||
func (a *App) DropDatabase(config connection.ConnectionConfig, dbName string) connection.QueryResult {
|
||
dbName = strings.TrimSpace(dbName)
|
||
if dbName == "" {
|
||
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
var (
|
||
runConfig connection.ConnectionConfig
|
||
sql string
|
||
)
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "tdengine", "clickhouse":
|
||
runConfig = config
|
||
runConfig.Database = ""
|
||
sql = fmt.Sprintf("DROP DATABASE %s", quoteIdentByType(dbType, dbName))
|
||
case "postgres", "kingbase", "highgo", "vastbase":
|
||
if strings.EqualFold(strings.TrimSpace(config.Database), dbName) {
|
||
return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再删除"}
|
||
}
|
||
runConfig = config
|
||
sql = fmt.Sprintf("DROP DATABASE %s", quoteIdentByType(dbType, dbName))
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除数据库", dbType)}
|
||
}
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "数据库删除成功"}
|
||
}
|
||
|
||
func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, oldTableName string, newTableName string) connection.QueryResult {
|
||
oldTableName = strings.TrimSpace(oldTableName)
|
||
newTableName = strings.TrimSpace(newTableName)
|
||
if oldTableName == "" || newTableName == "" {
|
||
return connection.QueryResult{Success: false, Message: "表名不能为空"}
|
||
}
|
||
if strings.EqualFold(oldTableName, newTableName) {
|
||
return connection.QueryResult{Success: false, Message: "新旧表名不能相同"}
|
||
}
|
||
if strings.Contains(newTableName, ".") {
|
||
return connection.QueryResult{Success: false, Message: "新表名不能包含 schema 或数据库前缀"}
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "clickhouse":
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名表", dbType)}
|
||
}
|
||
|
||
schemaName, pureOldTableName := normalizeSchemaAndTableByType(dbType, dbName, oldTableName)
|
||
if pureOldTableName == "" {
|
||
return connection.QueryResult{Success: false, Message: "旧表名不能为空"}
|
||
}
|
||
oldQualifiedTable := quoteTableIdentByType(dbType, schemaName, pureOldTableName)
|
||
newTableQuoted := quoteIdentByType(dbType, newTableName)
|
||
|
||
var sql string
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "clickhouse":
|
||
newQualifiedTable := quoteTableIdentByType(dbType, schemaName, newTableName)
|
||
sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualifiedTable, newQualifiedTable)
|
||
case "sqlserver":
|
||
// SQL Server 使用 sp_rename,参数为 'schema.oldname', 'newname'
|
||
oldFullName := schemaName + "." + pureOldTableName
|
||
escapedOld := strings.ReplaceAll(oldFullName, "'", "''")
|
||
escapedNew := strings.ReplaceAll(newTableName, "'", "''")
|
||
sql = fmt.Sprintf("EXEC sp_rename '%s', '%s'", escapedOld, escapedNew)
|
||
default:
|
||
sql = fmt.Sprintf("ALTER TABLE %s RENAME TO %s", oldQualifiedTable, newTableQuoted)
|
||
}
|
||
|
||
runConfig := buildRunConfigForDDL(config, dbType, dbName)
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "表重命名成功"}
|
||
}
|
||
|
||
func (a *App) DropTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
tableName = strings.TrimSpace(tableName)
|
||
if tableName == "" {
|
||
return connection.QueryResult{Success: false, Message: "表名不能为空"}
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "tdengine", "clickhouse":
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除表", dbType)}
|
||
}
|
||
|
||
schemaName, pureTableName := normalizeSchemaAndTableByType(dbType, dbName, tableName)
|
||
if pureTableName == "" {
|
||
return connection.QueryResult{Success: false, Message: "表名不能为空"}
|
||
}
|
||
qualifiedTable := quoteTableIdentByType(dbType, schemaName, pureTableName)
|
||
sql := fmt.Sprintf("DROP TABLE %s", qualifiedTable)
|
||
|
||
runConfig := buildRunConfigForDDL(config, dbType, dbName)
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "表删除成功"}
|
||
}
|
||
|
||
func (a *App) MySQLConnect(config connection.ConnectionConfig) connection.QueryResult {
|
||
config.Type = "mysql"
|
||
return a.DBConnect(config)
|
||
}
|
||
|
||
func (a *App) MySQLQuery(config connection.ConnectionConfig, dbName string, query string) connection.QueryResult {
|
||
config.Type = "mysql"
|
||
return a.DBQuery(config, dbName, query)
|
||
}
|
||
|
||
func (a *App) MySQLGetDatabases(config connection.ConnectionConfig) connection.QueryResult {
|
||
config.Type = "mysql"
|
||
return a.DBGetDatabases(config)
|
||
}
|
||
|
||
func (a *App) MySQLGetTables(config connection.ConnectionConfig, dbName string) connection.QueryResult {
|
||
config.Type = "mysql"
|
||
return a.DBGetTables(config, dbName)
|
||
}
|
||
|
||
func (a *App) MySQLShowCreateTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
config.Type = "mysql"
|
||
return a.DBShowCreateTable(config, dbName, tableName)
|
||
}
|
||
|
||
func (a *App) DBQuery(config connection.ConnectionConfig, dbName string, query string) connection.QueryResult {
|
||
return a.DBQueryWithCancel(config, dbName, query, "")
|
||
}
|
||
|
||
func (a *App) DBQueryWithCancel(config connection.ConnectionConfig, dbName string, query string, queryID string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
// Generate query ID if not provided
|
||
if queryID == "" {
|
||
queryID = generateQueryID()
|
||
}
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBQuery 获取连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||
}
|
||
|
||
query = sanitizeSQLForPgLike(runConfig.Type, query)
|
||
timeoutSeconds := runConfig.Timeout
|
||
if timeoutSeconds <= 0 {
|
||
timeoutSeconds = 30
|
||
}
|
||
ctx, cancel := utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second)
|
||
defer cancel()
|
||
|
||
// Store cancel function for potential manual cancellation
|
||
a.queryMu.Lock()
|
||
a.runningQueries[queryID] = queryContext{
|
||
cancel: cancel,
|
||
started: time.Now(),
|
||
}
|
||
a.queryMu.Unlock()
|
||
|
||
// Ensure query is removed from tracking when done
|
||
defer func() {
|
||
a.queryMu.Lock()
|
||
delete(a.runningQueries, queryID)
|
||
a.queryMu.Unlock()
|
||
}()
|
||
|
||
isReadQuery := isReadOnlySQLQuery(runConfig.Type, query)
|
||
|
||
runReadQuery := func(inst db.Database) ([]map[string]interface{}, []string, error) {
|
||
if q, ok := inst.(interface {
|
||
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
|
||
}); ok {
|
||
return q.QueryContext(ctx, query)
|
||
}
|
||
return inst.Query(query)
|
||
}
|
||
|
||
runExecQuery := func(inst db.Database) (int64, error) {
|
||
if e, ok := inst.(interface {
|
||
ExecContext(context.Context, string) (int64, error)
|
||
}); ok {
|
||
return e.ExecContext(ctx, query)
|
||
}
|
||
return inst.Exec(query)
|
||
}
|
||
|
||
if isReadQuery {
|
||
data, columns, err := runReadQuery(dbInst)
|
||
if err != nil && shouldRefreshCachedConnection(err) {
|
||
if a.invalidateCachedDatabase(runConfig, err) {
|
||
retryInst, retryErr := a.getDatabaseForcePing(runConfig)
|
||
if retryErr != nil {
|
||
logger.Error(retryErr, "DBQuery 重建连接失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
|
||
return connection.QueryResult{Success: false, Message: retryErr.Error()}
|
||
}
|
||
data, columns, err = runReadQuery(retryInst)
|
||
}
|
||
}
|
||
if err != nil {
|
||
logger.Error(err, "DBQuery 查询失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
|
||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: data, Fields: columns, QueryID: queryID}
|
||
} else {
|
||
affected, err := runExecQuery(dbInst)
|
||
if err != nil && shouldRefreshCachedConnection(err) {
|
||
if a.invalidateCachedDatabase(runConfig, err) {
|
||
retryInst, retryErr := a.getDatabaseForcePing(runConfig)
|
||
if retryErr != nil {
|
||
logger.Error(retryErr, "DBQuery 重建连接失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
|
||
return connection.QueryResult{Success: false, Message: retryErr.Error()}
|
||
}
|
||
affected, err = runExecQuery(retryInst)
|
||
}
|
||
}
|
||
if err != nil {
|
||
logger.Error(err, "DBQuery 执行失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
|
||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: map[string]int64{"affectedRows": affected}, QueryID: queryID}
|
||
}
|
||
}
|
||
|
||
func (a *App) DBQueryIsolated(config connection.ConnectionConfig, dbName string, query string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
dbInst, err := a.openDatabaseIsolated(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBQueryIsolated 获取连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
defer func() {
|
||
if closeErr := dbInst.Close(); closeErr != nil {
|
||
logger.Error(closeErr, "DBQueryIsolated 关闭临时连接失败:%s", formatConnSummary(runConfig))
|
||
}
|
||
}()
|
||
|
||
query = sanitizeSQLForPgLike(runConfig.Type, query)
|
||
timeoutSeconds := runConfig.Timeout
|
||
if timeoutSeconds <= 0 {
|
||
timeoutSeconds = 30
|
||
}
|
||
ctx, cancel := utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second)
|
||
defer cancel()
|
||
|
||
isReadQuery := isReadOnlySQLQuery(runConfig.Type, query)
|
||
|
||
if isReadQuery {
|
||
var data []map[string]interface{}
|
||
var columns []string
|
||
if q, ok := dbInst.(interface {
|
||
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
|
||
}); ok {
|
||
data, columns, err = q.QueryContext(ctx, query)
|
||
} else {
|
||
data, columns, err = dbInst.Query(query)
|
||
}
|
||
if err != nil {
|
||
logger.Error(err, "DBQueryIsolated 查询失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: data, Fields: columns}
|
||
}
|
||
|
||
var affected int64
|
||
if e, ok := dbInst.(interface {
|
||
ExecContext(context.Context, string) (int64, error)
|
||
}); ok {
|
||
affected, err = e.ExecContext(ctx, query)
|
||
} else {
|
||
affected, err = dbInst.Exec(query)
|
||
}
|
||
if err != nil {
|
||
logger.Error(err, "DBQueryIsolated 执行失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: map[string]int64{"affectedRows": affected}}
|
||
}
|
||
|
||
func sqlSnippet(query string) string {
|
||
q := strings.TrimSpace(query)
|
||
const max = 200
|
||
if len(q) <= max {
|
||
return q
|
||
}
|
||
return q[:max] + "..."
|
||
}
|
||
|
||
func ensureNonNilSlice[T any](items []T) []T {
|
||
if items == nil {
|
||
return make([]T, 0)
|
||
}
|
||
return items
|
||
}
|
||
|
||
func (a *App) DBGetDatabases(config connection.ConnectionConfig) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, "")
|
||
if strings.EqualFold(strings.TrimSpace(runConfig.Type), "redis") {
|
||
runConfig.Type = "redis"
|
||
client, err := a.getRedisClient(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBGetDatabases 获取 Redis 连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
dbs, err := client.GetDatabases()
|
||
if err != nil {
|
||
logger.Error(err, "DBGetDatabases 获取 Redis 库列表失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
resData := make([]map[string]string, 0, len(dbs))
|
||
for _, item := range dbs {
|
||
resData = append(resData, map[string]string{"Database": strconv.Itoa(item.Index)})
|
||
}
|
||
return connection.QueryResult{Success: true, Data: resData}
|
||
}
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBGetDatabases 获取连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
dbs, err := dbInst.GetDatabases()
|
||
if err != nil && shouldRefreshCachedConnection(err) {
|
||
if a.invalidateCachedDatabase(runConfig, err) {
|
||
retryInst, retryErr := a.getDatabaseForcePing(runConfig)
|
||
if retryErr != nil {
|
||
logger.Error(retryErr, "DBGetDatabases 重建连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: retryErr.Error()}
|
||
}
|
||
dbs, err = retryInst.GetDatabases()
|
||
}
|
||
}
|
||
if err != nil {
|
||
logger.Error(err, "DBGetDatabases 获取数据库列表失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
resData := make([]map[string]string, 0, len(dbs))
|
||
for _, name := range dbs {
|
||
resData = append(resData, map[string]string{"Database": name})
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: resData}
|
||
}
|
||
|
||
func (a *App) DBGetTables(config connection.ConnectionConfig, dbName string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
if strings.EqualFold(strings.TrimSpace(runConfig.Type), "redis") {
|
||
runConfig.Type = "redis"
|
||
client, err := a.getRedisClient(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBGetTables 获取 Redis 连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
cursor := uint64(0)
|
||
tables := make([]string, 0, 128)
|
||
seen := make(map[string]struct{}, 128)
|
||
for {
|
||
result, err := client.ScanKeys("*", cursor, 1000)
|
||
if err != nil {
|
||
logger.Error(err, "DBGetTables 扫描 Redis Key 失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
for _, item := range result.Keys {
|
||
key := strings.TrimSpace(item.Key)
|
||
if key == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[key]; ok {
|
||
continue
|
||
}
|
||
seen[key] = struct{}{}
|
||
tables = append(tables, key)
|
||
}
|
||
if strings.TrimSpace(result.Cursor) == "" || strings.TrimSpace(result.Cursor) == "0" {
|
||
break
|
||
}
|
||
next, err := strconv.ParseUint(strings.TrimSpace(result.Cursor), 10, 64)
|
||
if err != nil || next == cursor {
|
||
break
|
||
}
|
||
cursor = next
|
||
}
|
||
resData := make([]map[string]string, 0, len(tables))
|
||
for _, name := range tables {
|
||
resData = append(resData, map[string]string{"Table": name})
|
||
}
|
||
return connection.QueryResult{Success: true, Data: resData}
|
||
}
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBGetTables 获取连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
tables, err := dbInst.GetTables(dbName)
|
||
if err != nil && shouldRefreshCachedConnection(err) {
|
||
if a.invalidateCachedDatabase(runConfig, err) {
|
||
retryInst, retryErr := a.getDatabaseForcePing(runConfig)
|
||
if retryErr != nil {
|
||
logger.Error(retryErr, "DBGetTables 重建连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: retryErr.Error()}
|
||
}
|
||
tables, err = retryInst.GetTables(dbName)
|
||
}
|
||
}
|
||
if err != nil {
|
||
logger.Error(err, "DBGetTables 获取表列表失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
resData := make([]map[string]string, 0, len(tables))
|
||
for _, name := range tables {
|
||
resData = append(resData, map[string]string{"Table": name})
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: resData}
|
||
}
|
||
|
||
func (a *App) DBShowCreateTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
dbType := resolveDDLDBType(config)
|
||
runConfig := buildRunConfigForDDL(config, dbType, dbName)
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
logger.Error(err, "DBShowCreateTable 获取连接失败:%s", formatConnSummary(runConfig))
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
sqlStr, err := resolveCreateStatementWithFallback(dbInst, config, dbName, tableName)
|
||
if err != nil {
|
||
logger.Error(err, "DBShowCreateTable 获取建表语句失败:%s 表=%s", formatConnSummary(runConfig), tableName)
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: sqlStr}
|
||
}
|
||
|
||
func resolveCreateStatementWithFallback(dbInst db.Database, config connection.ConnectionConfig, dbName string, tableName string) (string, error) {
|
||
dbType := resolveDDLDBType(config)
|
||
schemaName, pureTableName := normalizeSchemaAndTableByType(dbType, dbName, tableName)
|
||
if pureTableName == "" {
|
||
return "", fmt.Errorf("表名不能为空")
|
||
}
|
||
|
||
sqlStr, sourceErr := dbInst.GetCreateStatement(schemaName, pureTableName)
|
||
if sourceErr == nil && !shouldFallbackCreateStatement(dbType, sqlStr) {
|
||
return sqlStr, nil
|
||
}
|
||
|
||
if !supportsCreateStatementFallback(dbType) {
|
||
if sourceErr != nil {
|
||
return "", sourceErr
|
||
}
|
||
return sqlStr, nil
|
||
}
|
||
|
||
columns, colErr := dbInst.GetColumns(schemaName, pureTableName)
|
||
if colErr != nil {
|
||
if sourceErr != nil {
|
||
return "", sourceErr
|
||
}
|
||
return "", colErr
|
||
}
|
||
|
||
fallbackDDL, buildErr := buildFallbackCreateStatement(dbType, schemaName, pureTableName, columns)
|
||
if buildErr != nil {
|
||
if sourceErr != nil {
|
||
return "", sourceErr
|
||
}
|
||
return "", buildErr
|
||
}
|
||
return fallbackDDL, nil
|
||
}
|
||
|
||
func supportsCreateStatementFallback(dbType string) bool {
|
||
switch dbType {
|
||
case "postgres", "kingbase", "highgo", "vastbase":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func shouldFallbackCreateStatement(dbType string, ddl string) bool {
|
||
if !supportsCreateStatementFallback(dbType) {
|
||
return false
|
||
}
|
||
|
||
trimmed := strings.TrimSpace(ddl)
|
||
if trimmed == "" {
|
||
return true
|
||
}
|
||
if hasCreateTableHead(trimmed) {
|
||
return false
|
||
}
|
||
|
||
lower := strings.ToLower(trimmed)
|
||
if strings.Contains(lower, "not fully supported") ||
|
||
strings.Contains(lower, "not directly supported") ||
|
||
strings.Contains(lower, "not supported") {
|
||
return true
|
||
}
|
||
return true
|
||
}
|
||
|
||
func hasCreateTableHead(sqlText string) bool {
|
||
lines := strings.Split(sqlText, "\n")
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
continue
|
||
}
|
||
if strings.HasPrefix(line, "--") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") {
|
||
continue
|
||
}
|
||
return strings.HasPrefix(strings.ToLower(line), "create table")
|
||
}
|
||
return false
|
||
}
|
||
|
||
func buildFallbackCreateStatement(dbType string, schemaName string, tableName string, columns []connection.ColumnDefinition) (string, error) {
|
||
table := strings.TrimSpace(tableName)
|
||
if table == "" {
|
||
return "", fmt.Errorf("表名不能为空")
|
||
}
|
||
if len(columns) == 0 {
|
||
return "", fmt.Errorf("未获取到字段定义,无法生成建表语句")
|
||
}
|
||
|
||
qualifiedTable := quoteTableIdentByType(dbType, schemaName, table)
|
||
columnLines := make([]string, 0, len(columns)+1)
|
||
primaryKeys := make([]string, 0, 2)
|
||
|
||
for _, col := range columns {
|
||
colNameRaw := strings.TrimSpace(col.Name)
|
||
if colNameRaw == "" {
|
||
continue
|
||
}
|
||
colType := strings.TrimSpace(col.Type)
|
||
if colType == "" {
|
||
colType = "text"
|
||
}
|
||
|
||
colName := quoteIdentByType(dbType, colNameRaw)
|
||
defParts := []string{fmt.Sprintf("%s %s", colName, colType)}
|
||
|
||
if strings.EqualFold(strings.TrimSpace(col.Nullable), "NO") {
|
||
defParts = append(defParts, "NOT NULL")
|
||
}
|
||
if col.Default != nil {
|
||
defVal := strings.TrimSpace(*col.Default)
|
||
if defVal != "" {
|
||
defParts = append(defParts, "DEFAULT "+defVal)
|
||
}
|
||
}
|
||
|
||
columnLines = append(columnLines, " "+strings.Join(defParts, " "))
|
||
if strings.EqualFold(strings.TrimSpace(col.Key), "PRI") {
|
||
primaryKeys = append(primaryKeys, colName)
|
||
}
|
||
}
|
||
|
||
if len(columnLines) == 0 {
|
||
return "", fmt.Errorf("字段定义为空,无法生成建表语句")
|
||
}
|
||
if len(primaryKeys) > 0 {
|
||
columnLines = append(columnLines, " PRIMARY KEY ("+strings.Join(primaryKeys, ", ")+")")
|
||
}
|
||
|
||
ddl := strings.Builder{}
|
||
ddl.WriteString("CREATE TABLE ")
|
||
ddl.WriteString(qualifiedTable)
|
||
ddl.WriteString(" (\n")
|
||
ddl.WriteString(strings.Join(columnLines, ",\n"))
|
||
ddl.WriteString("\n);")
|
||
return ddl.String(), nil
|
||
}
|
||
|
||
func (a *App) DBGetColumns(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
|
||
columns, err := dbInst.GetColumns(schemaName, pureTableName)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: ensureNonNilSlice(columns)}
|
||
}
|
||
|
||
func (a *App) DBGetIndexes(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
|
||
indexes, err := dbInst.GetIndexes(schemaName, pureTableName)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: ensureNonNilSlice(indexes)}
|
||
}
|
||
|
||
func (a *App) DBGetForeignKeys(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
|
||
fks, err := dbInst.GetForeignKeys(schemaName, pureTableName)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: ensureNonNilSlice(fks)}
|
||
}
|
||
|
||
func (a *App) DBGetTriggers(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
|
||
triggers, err := dbInst.GetTriggers(schemaName, pureTableName)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: ensureNonNilSlice(triggers)}
|
||
}
|
||
|
||
func (a *App) DropView(config connection.ConnectionConfig, dbName string, viewName string) connection.QueryResult {
|
||
viewName = strings.TrimSpace(viewName)
|
||
if viewName == "" {
|
||
return connection.QueryResult{Success: false, Message: "视图名称不能为空"}
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "clickhouse":
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除视图", dbType)}
|
||
}
|
||
|
||
schemaName, pureViewName := normalizeSchemaAndTableByType(dbType, dbName, viewName)
|
||
if pureViewName == "" {
|
||
return connection.QueryResult{Success: false, Message: "视图名称不能为空"}
|
||
}
|
||
qualifiedView := quoteTableIdentByType(dbType, schemaName, pureViewName)
|
||
sql := fmt.Sprintf("DROP VIEW %s", qualifiedView)
|
||
|
||
runConfig := buildRunConfigForDDL(config, dbType, dbName)
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "视图删除成功"}
|
||
}
|
||
|
||
func (a *App) DropFunction(config connection.ConnectionConfig, dbName string, routineName string, routineType string) connection.QueryResult {
|
||
routineName = strings.TrimSpace(routineName)
|
||
routineType = strings.TrimSpace(strings.ToUpper(routineType))
|
||
if routineName == "" {
|
||
return connection.QueryResult{Success: false, Message: "函数/存储过程名称不能为空"}
|
||
}
|
||
if routineType != "FUNCTION" && routineType != "PROCEDURE" {
|
||
routineType = "FUNCTION"
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "duckdb":
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除函数/存储过程", dbType)}
|
||
}
|
||
if dbType == "duckdb" && routineType == "PROCEDURE" {
|
||
return connection.QueryResult{Success: false, Message: "DuckDB 暂不支持存储过程"}
|
||
}
|
||
|
||
schemaName, pureName := normalizeSchemaAndTableByType(dbType, dbName, routineName)
|
||
if pureName == "" {
|
||
return connection.QueryResult{Success: false, Message: "函数/存储过程名称不能为空"}
|
||
}
|
||
qualifiedName := quoteTableIdentByType(dbType, schemaName, pureName)
|
||
sql := fmt.Sprintf("DROP %s %s", routineType, qualifiedName)
|
||
|
||
runConfig := buildRunConfigForDDL(config, dbType, dbName)
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
label := "函数"
|
||
if routineType == "PROCEDURE" {
|
||
label = "存储过程"
|
||
}
|
||
return connection.QueryResult{Success: true, Message: fmt.Sprintf("%s删除成功", label)}
|
||
}
|
||
|
||
func (a *App) RenameView(config connection.ConnectionConfig, dbName string, oldName string, newName string) connection.QueryResult {
|
||
oldName = strings.TrimSpace(oldName)
|
||
newName = strings.TrimSpace(newName)
|
||
if oldName == "" || newName == "" {
|
||
return connection.QueryResult{Success: false, Message: "视图名称不能为空"}
|
||
}
|
||
if strings.EqualFold(oldName, newName) {
|
||
return connection.QueryResult{Success: false, Message: "新旧视图名称不能相同"}
|
||
}
|
||
if strings.Contains(newName, ".") {
|
||
return connection.QueryResult{Success: false, Message: "新视图名不能包含 schema 或数据库前缀"}
|
||
}
|
||
|
||
dbType := resolveDDLDBType(config)
|
||
schemaName, pureOldName := normalizeSchemaAndTableByType(dbType, dbName, oldName)
|
||
if pureOldName == "" {
|
||
return connection.QueryResult{Success: false, Message: "旧视图名不能为空"}
|
||
}
|
||
oldQualified := quoteTableIdentByType(dbType, schemaName, pureOldName)
|
||
newQuoted := quoteIdentByType(dbType, newName)
|
||
|
||
var sql string
|
||
switch dbType {
|
||
case "mysql", "mariadb", "diros", "sphinx", "clickhouse":
|
||
newQualified := quoteTableIdentByType(dbType, schemaName, newName)
|
||
sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualified, newQualified)
|
||
case "postgres", "kingbase", "highgo", "vastbase":
|
||
sql = fmt.Sprintf("ALTER VIEW %s RENAME TO %s", oldQualified, newQuoted)
|
||
case "sqlserver":
|
||
oldFullName := schemaName + "." + pureOldName
|
||
escapedOld := strings.ReplaceAll(oldFullName, "'", "''")
|
||
escapedNew := strings.ReplaceAll(newName, "'", "''")
|
||
sql = fmt.Sprintf("EXEC sp_rename '%s', '%s'", escapedOld, escapedNew)
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名视图", dbType)}
|
||
}
|
||
|
||
runConfig := buildRunConfigForDDL(config, dbType, dbName)
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if _, err := dbInst.Exec(sql); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "视图重命名成功"}
|
||
}
|
||
|
||
func (a *App) DBGetAllColumns(config connection.ConnectionConfig, dbName string) connection.QueryResult {
|
||
runConfig := normalizeRunConfig(config, dbName)
|
||
|
||
dbInst, err := a.getDatabase(runConfig)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
cols, err := dbInst.GetAllColumns(dbName)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
|
||
return connection.QueryResult{Success: true, Data: ensureNonNilSlice(cols)}
|
||
}
|