Files
opsxdb/apps/server/internal/db/search.go
T
logan a9510235bb
CI / Go server (push) Canceled after 0s
CI / Web + core packages (push) Canceled after 0s
CI / Desktop (Tauri) cargo check (push) Canceled after 0s
feat: 18 引擎驱动全家桶 + 驱动管理/离线包 + 设置中心 + 工作台交互打磨
驱动接入(全部本机真实例验收):
- Elasticsearch 原生 SQL API、InfluxDB 原生 HTTP+Flux(官方 JDBC 付费许可,实测弃用)
- ZooKeeper/MongoDB JVM agent(连接时自动下载官方驱动 jar,sha256 钉版本到 ~/.opsxdb/drivers/)
- 达梦 JDBC agent 桥就绪(待厂商 jar+实例);TiDB 真实例验收
- 协议族代码审查放行 PolarDB/TDSQL/OceanBase/Kingbase/GaussDB/Oracle
- 驱动能力模型:按驱动开关对象树分组/右键菜单/行编辑,NoSQL 树拍平(对象直挂库节点)

驱动管理:
- 驱动管理弹框(状态/预装/离线包导入/JDK 检测)+ /api/drivers/* 三接口
- opsxdb-driver 驱动商店仓库(清单/fetch.sh/pack.sh 离线包/import.sh)
- 连接时懒加载:DMG 零 Java 产物,首连自动下载(实测 ZK 九件套 1.35s),离线包自动回退

UI/交互:
- 设置中心(⚙):常规/执行/结果/驱动/快捷键/关于,左导航+右面板(对标 opsxshell)
- 实例级展开箭头(行首 SVG chevron,替代字体回退失真的 Unicode 三角)
- 树内嵌套渲染修复:对象树挂在激活连接下;工具栏窄窗口不断行(按钮 nowrap+整颗换行)
- 侧栏连接行精简(图标+连接名)、SQLite SSH 守卫、Oracle 死参数清理

质量:
- Go 协议层/离线回退单测、E2E 门控测试(ES/ZK/Influx/Mongo/TiDB/JDBC 桥)
- 驱动注册表单一事实来源 connection-types.json,README/BACKLOG/AGENTS 同步
2026-09-22 01:07:46 +08:00

132 lines
4.2 KiB
Go

package db
import (
"context"
"database/sql"
"strings"
)
// ObjectHit 对象搜索结果条目。
type ObjectHit struct {
Schema string `json:"schema"`
Name string `json:"name"`
Type string `json:"type"` // table | view
}
// SearchObjects 按关键词模糊搜索表/视图,最多返回 50 条。redis 返回空。
func SearchObjects(ctx context.Context, c ConnReq) ([]ObjectHit, error) {
ctx = reqCtx(ctx)
if isRedis(c.Driver) {
return []ObjectHit{}, nil
}
var out []ObjectHit
err := withDB(c, func(db *sql.DB) error {
var err error
switch {
case isMySQLFamily(c.Driver):
out, err = searchObjectsMySQL(ctx, db, c.Database, c.Keyword)
case isPostgresFamily(c.Driver):
out, err = searchObjectsPostgres(ctx, db, c.Keyword)
case isMSSQL(c.Driver):
out, err = searchObjectsMSSQL(ctx, db, c.Keyword)
case driverKey(c.Driver) == "oracle":
out, err = searchObjectsOracle(ctx, db, c.Database, c.User, c.Keyword)
case driverKey(c.Driver) == "sqlite":
out, err = searchObjectsSQLite(ctx, db, c.Keyword)
default:
err = unsupportedDriverErr(c)
}
return err
})
if err != nil {
return nil, err
}
if out == nil {
out = []ObjectHit{}
}
return out, nil
}
func likePattern(keyword string) string {
return "%" + keyword + "%"
}
// hitType information_schema 的 table_type 含 VIEW 即为视图。
func hitType(tableType string) string {
if strings.Contains(strings.ToUpper(tableType), "VIEW") {
return "view"
}
return "table"
}
func searchObjectsMySQL(ctx context.Context, dbh *sql.DB, database, keyword string) ([]ObjectHit, error) {
// database 为空时用当前库 DATABASE()
q := `SELECT table_schema, table_name, table_type FROM information_schema.TABLES
WHERE table_schema = DATABASE() AND table_name LIKE ? LIMIT 50`
args := []interface{}{likePattern(keyword)}
if strings.TrimSpace(database) != "" {
q = `SELECT table_schema, table_name, table_type FROM information_schema.TABLES
WHERE table_schema = ? AND table_name LIKE ? LIMIT 50`
args = []interface{}{database, likePattern(keyword)}
}
return scanObjectHits(ctx, dbh, q, args, "mysql")
}
func searchObjectsPostgres(ctx context.Context, dbh *sql.DB, keyword string) ([]ObjectHit, error) {
return scanObjectHits(ctx, dbh,
`SELECT table_schema, table_name, table_type FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog','information_schema') AND table_name LIKE $1 LIMIT 50`,
[]interface{}{likePattern(keyword)}, "pg")
}
func searchObjectsMSSQL(ctx context.Context, dbh *sql.DB, keyword string) ([]ObjectHit, error) {
// MSSQL 无 LIMIT,用 TOP 50
return scanObjectHits(ctx, dbh,
`SELECT TOP 50 TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES
WHERE TABLE_NAME LIKE @p1`,
[]interface{}{likePattern(keyword)}, "mssql")
}
func searchObjectsOracle(ctx context.Context, dbh *sql.DB, database, user, keyword string) ([]ObjectHit, error) {
owner := strings.ToUpper(strings.TrimSpace(database))
if owner == "" {
owner = strings.ToUpper(strings.TrimSpace(user))
}
return scanObjectHits(ctx, dbh,
`SELECT owner, table_name, 'table' FROM all_tables WHERE owner = :1 AND table_name LIKE :2
UNION ALL
SELECT owner, view_name, 'view' FROM all_views WHERE owner = :3 AND view_name LIKE :4
FETCH FIRST 50 ROWS ONLY`,
[]interface{}{owner, likePattern(keyword), owner, likePattern(keyword)}, "oracle")
}
func searchObjectsSQLite(ctx context.Context, dbh *sql.DB, keyword string) ([]ObjectHit, error) {
return scanObjectHits(ctx, dbh,
`SELECT '', name, type FROM sqlite_master WHERE name LIKE ? AND type IN ('table','view') LIMIT 50`,
[]interface{}{likePattern(keyword)}, "sqlite")
}
func scanObjectHits(ctx context.Context, dbh *sql.DB, q string, args []interface{}, dialect string) ([]ObjectHit, error) {
rows, err := dbh.QueryContext(ctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := []ObjectHit{}
for rows.Next() {
var schema, name, typ string
if err := rows.Scan(&schema, &name, &typ); err != nil {
return nil, err
}
t := "table"
switch dialect {
case "sqlite":
t = typ // sqlite_master.type 直接是 table/view
default:
t = hitType(typ)
}
out = append(out, ObjectHit{Schema: schema, Name: name, Type: t})
}
return out, rows.Err()
}