驱动接入(全部本机真实例验收): - 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 同步
132 lines
4.2 KiB
Go
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()
|
|
}
|