驱动接入(全部本机真实例验收): - 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 同步
140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
package db
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newExportConn(t *testing.T) ConnReq {
|
|
return newSQLiteFile(t,
|
|
`CREATE TABLE t_export (id INTEGER PRIMARY KEY, name TEXT, note TEXT)`,
|
|
`INSERT INTO t_export (id, name, note) VALUES (1, 'alice', 'hi, "q"')`,
|
|
`INSERT INTO t_export (id, name, note) VALUES (2, 'bo''b', NULL)`,
|
|
`INSERT INTO t_export (id, name, note) VALUES (3, 'carol', 'line1`+"\n"+`line2')`,
|
|
)
|
|
}
|
|
|
|
// R2-B7:csv 输出形状(表头 + csv 引号转义由 encoding/csv 负责)。
|
|
func TestExportCSV(t *testing.T) {
|
|
c := newExportConn(t)
|
|
c.SQL = `SELECT id, name, note FROM t_export ORDER BY id`
|
|
c.Format = "csv"
|
|
var buf bytes.Buffer
|
|
if err := ExportQuery(context.Background(), c, &buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.HasPrefix(out, "id,name,note\n") {
|
|
t.Fatalf("csv 表头错误: %q", out)
|
|
}
|
|
// 含逗号/引号的字段整体加引号、内部引号 doubling
|
|
if !strings.Contains(out, `"hi, ""q"""`) {
|
|
t.Fatalf("csv 转义错误: %q", out)
|
|
}
|
|
// 单引号 doubling 是 SQL 字面量层;csv 层原样输出 'bo''b' 存好的 bo'b
|
|
if !strings.Contains(out, "2,bo'b,") {
|
|
t.Fatalf("csv 内容错误: %q", out)
|
|
}
|
|
// 多行字面量会使单条记录跨物理行,用 csv.Reader 按记录解析
|
|
recs, err := csv.NewReader(strings.NewReader(out)).ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("csv 输出应可解析: %v", err)
|
|
}
|
|
if len(recs) != 4 { // 表头 + 3 行
|
|
t.Fatalf("应为表头+3 行: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestExportJSON(t *testing.T) {
|
|
c := newExportConn(t)
|
|
c.SQL = `SELECT id, name, note FROM t_export ORDER BY id`
|
|
c.Format = "json"
|
|
var buf bytes.Buffer
|
|
if err := ExportQuery(context.Background(), c, &buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var rows []map[string]interface{}
|
|
if err := json.Unmarshal(buf.Bytes(), &rows); err != nil {
|
|
t.Fatalf("json 输出应可解析: %v\n%s", err, buf.String())
|
|
}
|
|
if len(rows) != 3 {
|
|
t.Fatalf("应为 3 行: %v", rows)
|
|
}
|
|
if rows[0]["name"] != "alice" || rows[1]["note"] != nil {
|
|
t.Fatalf("行值错误: %v", rows)
|
|
}
|
|
}
|
|
|
|
func TestExportInsert(t *testing.T) {
|
|
c := newExportConn(t)
|
|
c.SQL = `SELECT id, name, note FROM t_export ORDER BY id`
|
|
c.Format = "insert"
|
|
c.Table = "t_export"
|
|
var buf bytes.Buffer
|
|
if err := ExportQuery(context.Background(), c, &buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, `INSERT INTO "t_export" ("id", "name", "note") VALUES (1, 'alice', 'hi, "q"');`) {
|
|
t.Fatalf("insert 首行错误: %q", out)
|
|
}
|
|
// 字符串单引号 doubling;NULL 原样
|
|
if !strings.Contains(out, `VALUES (2, 'bo''b', NULL);`) {
|
|
t.Fatalf("insert 字面量错误: %q", out)
|
|
}
|
|
// 多行字面量内换行原样保留
|
|
if !strings.Contains(out, "line1\nline2") {
|
|
t.Fatalf("insert 换行字面量错误: %q", out)
|
|
}
|
|
}
|
|
|
|
// limit 生效(含默认上限收敛)。
|
|
func TestExportLimit(t *testing.T) {
|
|
c := newExportConn(t)
|
|
c.SQL = `SELECT id FROM t_export ORDER BY id`
|
|
c.Format = "csv"
|
|
c.Limit = 2
|
|
var buf bytes.Buffer
|
|
if err := ExportQuery(context.Background(), c, &buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
|
if len(rows) != 3 { // 表头 + 2 行
|
|
t.Fatalf("limit=2 应只导 2 行: %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestExportAllowedQuery(t *testing.T) {
|
|
for _, ok := range []string{
|
|
"SELECT * FROM t", "WITH x AS (SELECT 1) SELECT * FROM x",
|
|
"SHOW TABLES", "DESCRIBE t", "EXPLAIN SELECT 1",
|
|
"-- c\nSELECT 1",
|
|
} {
|
|
if !ExportAllowedQuery(ok) {
|
|
t.Fatalf("应允许只读: %q", ok)
|
|
}
|
|
}
|
|
for _, bad := range []string{
|
|
"DELETE FROM t", "INSERT INTO t VALUES (1)", "DROP TABLE t",
|
|
"UPDATE t SET a=1", "", " ",
|
|
} {
|
|
if ExportAllowedQuery(bad) {
|
|
t.Fatalf("不应允许: %q", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExportBadFormat(t *testing.T) {
|
|
c := newExportConn(t)
|
|
c.SQL = "SELECT 1"
|
|
c.Format = "xml"
|
|
var buf bytes.Buffer
|
|
if err := ExportQuery(context.Background(), c, &buf); err == nil {
|
|
t.Fatal("未知格式应报错")
|
|
}
|
|
}
|