mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
🐛 fix(tdengine): 修复低版本驱动连接与表元数据兼容问题
- 修复 TDengine 历史驱动源码构建未按所选版本切换依赖的问题 - 为 DESCRIBE 与 SHOW CREATE 增加旧版本语法降级,避免表详情加载报错 - 为表概览补充 TDengine 专用查询分支,避免误查 information_schema - 补充 TDengine 兼容性与驱动构建回归测试 Refs #531
This commit is contained in:
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
@@ -188,6 +188,8 @@ ORDER BY s.name, t.name`;
|
||||
}
|
||||
case 'clickhouse':
|
||||
return `SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows, total_bytes AS data_length, 0 AS index_length FROM system.tables WHERE database = '${escapeLiteral(dbName)}' AND engine NOT IN ('View', 'MaterializedView') ORDER BY name`;
|
||||
case 'tdengine':
|
||||
return `SHOW TABLES FROM \`${dbName.replace(/`/g, '``')}\``;
|
||||
case 'dm':
|
||||
case 'oracle': {
|
||||
const owner = (schemaName || dbName).toUpperCase();
|
||||
@@ -217,7 +219,7 @@ const parseTableStats = (dialect: string, rows: Record<string, any>[]): TableSta
|
||||
};
|
||||
|
||||
return {
|
||||
name: strVal(['Name', 'table_name', 'tablename', 'TABLE_NAME']),
|
||||
name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME']),
|
||||
comment: strVal(['Comment', 'table_comment', 'TABLE_COMMENT', 'comments']),
|
||||
rows: numVal(['Rows', 'table_rows', 'TABLE_ROWS', 'num_rows', 'reltuples', 'total_rows']),
|
||||
dataSize: numVal(['Data_length', 'data_length', 'DATA_LENGTH', 'total_bytes']),
|
||||
|
||||
5
go.mod
5
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
|
||||
github.com/caretdev/go-irisnative v0.2.1
|
||||
github.com/duckdb/duckdb-go/v2 v2.5.5
|
||||
github.com/elastic/go-elasticsearch/v8 v8.19.6
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/highgo/pq-sm3 v0.0.0
|
||||
@@ -30,14 +31,10 @@ require (
|
||||
|
||||
require (
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 // indirect
|
||||
github.com/elastic/go-elasticsearch/v8 v8.19.6 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
6
go.sum
6
go.sum
@@ -38,7 +38,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -136,7 +135,6 @@ github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxh
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -195,7 +193,6 @@ github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -213,7 +210,6 @@ github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
@@ -291,6 +287,8 @@ go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
stdRuntime "runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -3656,6 +3657,15 @@ func buildOptionalDriverAgentFromSource(definition driverDefinition, executableP
|
||||
if rootErr != nil {
|
||||
return "", rootErr
|
||||
}
|
||||
buildArgs := []string{"build", "-tags", tagName, "-trimpath", "-ldflags", "-s -w"}
|
||||
cleanupModOverride := func() {}
|
||||
if modOverride, modErr := prepareOptionalDriverBuildModOverride(projectRoot, driverType, selectedVersion); modErr != nil {
|
||||
return "", modErr
|
||||
} else if modOverride != nil {
|
||||
buildArgs = append(buildArgs, "-modfile", modOverride.modFile)
|
||||
cleanupModOverride = modOverride.cleanup
|
||||
}
|
||||
defer cleanupModOverride()
|
||||
env := append([]string{}, os.Environ()...)
|
||||
env = withEnvValue(env, "GOTOOLCHAIN", "auto")
|
||||
var duckDBLibDir string
|
||||
@@ -3679,7 +3689,8 @@ func buildOptionalDriverAgentFromSource(definition driverDefinition, executableP
|
||||
env = withEnvValue(env, "CGO_LDFLAGS", fmt.Sprintf("-L\"%s\" -lduckdb", filepath.ToSlash(duckDBLibDir)))
|
||||
env = prependPathEnv(env, duckDBLibDir)
|
||||
}
|
||||
cmd := exec.Command(goPath, "build", "-tags", tagName, "-trimpath", "-ldflags", "-s -w", "-o", executablePath, "./cmd/optional-driver-agent")
|
||||
buildArgs = append(buildArgs, "-o", executablePath, "./cmd/optional-driver-agent")
|
||||
cmd := exec.Command(goPath, buildArgs...)
|
||||
cmd.Dir = projectRoot
|
||||
cmd.Env = env
|
||||
output, buildErr := cmd.CombinedOutput()
|
||||
@@ -3701,6 +3712,94 @@ func buildOptionalDriverAgentFromSource(definition driverDefinition, executableP
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
type optionalDriverBuildModOverride struct {
|
||||
modFile string
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
func prepareOptionalDriverBuildModOverride(projectRoot string, driverType string, selectedVersion string) (*optionalDriverBuildModOverride, error) {
|
||||
modulePath := strings.TrimSpace(driverGoModulePathMap[normalizeDriverType(driverType)])
|
||||
versionText := normalizeVersion(strings.TrimSpace(selectedVersion))
|
||||
if strings.EqualFold(normalizeDriverType(driverType), "tdengine") && modulePath != "" && versionText != "" {
|
||||
return buildVersionedDriverModOverride(projectRoot, modulePath, versionText)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func buildVersionedDriverModOverride(projectRoot string, modulePath string, version string) (*optionalDriverBuildModOverride, error) {
|
||||
goModPath := filepath.Join(projectRoot, "go.mod")
|
||||
goSumPath := filepath.Join(projectRoot, "go.sum")
|
||||
modBytes, err := os.ReadFile(goModPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 go.mod 失败:%w", err)
|
||||
}
|
||||
|
||||
replaced, changed, err := rewriteRequiredModuleVersion(modBytes, modulePath, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !changed {
|
||||
return nil, fmt.Errorf("未在 go.mod 中找到驱动依赖:%s", modulePath)
|
||||
}
|
||||
|
||||
workDir, err := os.MkdirTemp("", "gonavi-driver-mod-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建驱动构建临时目录失败:%w", err)
|
||||
}
|
||||
cleanup := func() {
|
||||
_ = os.RemoveAll(workDir)
|
||||
}
|
||||
|
||||
modFile := filepath.Join(workDir, "go.mod")
|
||||
sumFile := filepath.Join(workDir, "go.sum")
|
||||
if err := os.WriteFile(modFile, replaced, 0o644); err != nil {
|
||||
cleanup()
|
||||
return nil, fmt.Errorf("写入临时 go.mod 失败:%w", err)
|
||||
}
|
||||
if sumBytes, readErr := os.ReadFile(goSumPath); readErr == nil {
|
||||
if writeErr := os.WriteFile(sumFile, sumBytes, 0o644); writeErr != nil {
|
||||
cleanup()
|
||||
return nil, fmt.Errorf("写入临时 go.sum 失败:%w", writeErr)
|
||||
}
|
||||
}
|
||||
|
||||
return &optionalDriverBuildModOverride{
|
||||
modFile: modFile,
|
||||
cleanup: cleanup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rewriteRequiredModuleVersion(goMod []byte, modulePath string, version string) ([]byte, bool, error) {
|
||||
trimmedModule := strings.TrimSpace(modulePath)
|
||||
trimmedVersion := normalizeVersion(strings.TrimSpace(version))
|
||||
if trimmedModule == "" || trimmedVersion == "" {
|
||||
return nil, false, fmt.Errorf("驱动模块或版本为空")
|
||||
}
|
||||
|
||||
pattern := fmt.Sprintf(`(?m)^(?P<prefix>\s*%s\s+)v[^\s]+(?P<suffix>\s*(//.*)?)$`, regexp.QuoteMeta(trimmedModule))
|
||||
re := regexp.MustCompile(pattern)
|
||||
changed := false
|
||||
replaced := re.ReplaceAllFunc(goMod, func(line []byte) []byte {
|
||||
match := re.FindSubmatch(line)
|
||||
if len(match) == 0 {
|
||||
return line
|
||||
}
|
||||
changed = true
|
||||
text := string(line)
|
||||
submatches := re.FindStringSubmatch(text)
|
||||
if len(submatches) == 0 {
|
||||
return line
|
||||
}
|
||||
prefix := submatches[1]
|
||||
suffix := ""
|
||||
if len(submatches) > 2 {
|
||||
suffix = submatches[2]
|
||||
}
|
||||
return []byte(prefix + "v" + trimmedVersion + suffix)
|
||||
})
|
||||
return replaced, changed, nil
|
||||
}
|
||||
|
||||
func resolveMongoDriverMajorFromVersion(version string) int {
|
||||
trimmed := strings.TrimSpace(version)
|
||||
trimmed = strings.TrimPrefix(trimmed, "v")
|
||||
|
||||
75
internal/app/methods_driver_tdengine_build_test.go
Normal file
75
internal/app/methods_driver_tdengine_build_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRewriteRequiredModuleVersionUpdatesTDengineDriver(t *testing.T) {
|
||||
input := []byte(`module example
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/taosdata/driver-go/v3 v3.7.8
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
)
|
||||
`)
|
||||
|
||||
got, changed, err := rewriteRequiredModuleVersion(input, "github.com/taosdata/driver-go/v3", "3.3.1")
|
||||
if err != nil {
|
||||
t.Fatalf("rewriteRequiredModuleVersion returned error: %v", err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatal("expected TDengine module version to be rewritten")
|
||||
}
|
||||
text := string(got)
|
||||
if !strings.Contains(text, "github.com/taosdata/driver-go/v3 v3.3.1") {
|
||||
t.Fatalf("expected rewritten go.mod to contain TDengine 3.3.1, got:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "github.com/go-sql-driver/mysql v1.9.3") {
|
||||
t.Fatalf("expected unrelated dependencies to remain unchanged, got:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareOptionalDriverBuildModOverrideCreatesVersionedModFileForTDengine(t *testing.T) {
|
||||
projectRoot := t.TempDir()
|
||||
goMod := `module example
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/taosdata/driver-go/v3 v3.7.8
|
||||
)
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(projectRoot, "go.mod"), []byte(goMod), 0o644); err != nil {
|
||||
t.Fatalf("write go.mod: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(projectRoot, "go.sum"), []byte("placeholder"), 0o644); err != nil {
|
||||
t.Fatalf("write go.sum: %v", err)
|
||||
}
|
||||
|
||||
override, err := prepareOptionalDriverBuildModOverride(projectRoot, "tdengine", "3.3.1")
|
||||
if err != nil {
|
||||
t.Fatalf("prepareOptionalDriverBuildModOverride returned error: %v", err)
|
||||
}
|
||||
if override == nil {
|
||||
t.Fatal("expected TDengine versioned build to create a mod override")
|
||||
}
|
||||
|
||||
modBytes, err := os.ReadFile(override.modFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read override mod file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(modBytes), "github.com/taosdata/driver-go/v3 v3.3.1") {
|
||||
t.Fatalf("override mod file did not pin TDengine 3.3.1:\n%s", string(modBytes))
|
||||
}
|
||||
|
||||
overrideDir := filepath.Dir(override.modFile)
|
||||
override.cleanup()
|
||||
if _, statErr := os.Stat(overrideDir); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected cleanup to remove override dir, statErr=%v", statErr)
|
||||
}
|
||||
}
|
||||
@@ -251,3 +251,75 @@ func TestTDengineGetTablesIncludesSuperTables(t *testing.T) {
|
||||
t.Fatalf("unexpected tables: got=%v want=%v", tables, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTDengineGetColumnsFallsBackToLegacyDescribeSyntax(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openTDengineRecordingDB(t)
|
||||
state.mu.Lock()
|
||||
state.queryResults["DESCRIBE `metrics`.`meters`"] = tdengineQueryResult{
|
||||
err: fmt.Errorf("[0x2600] syntax error near '`metrics`.`meters`'"),
|
||||
}
|
||||
state.queryResults["DESCRIBE metrics.meters"] = tdengineQueryResult{
|
||||
columns: []string{"Field", "Type", "Note", "Null"},
|
||||
rows: [][]driver.Value{
|
||||
{"ts", "TIMESTAMP", "", "NO"},
|
||||
{"value", "DOUBLE", "", "YES"},
|
||||
},
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
td := &TDengineDB{conn: dbConn}
|
||||
columns, err := td.GetColumns("metrics", "meters")
|
||||
if err != nil {
|
||||
t.Fatalf("GetColumns returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(columns) != 2 {
|
||||
t.Fatalf("expected 2 columns, got %d", len(columns))
|
||||
}
|
||||
queries := state.snapshotQueries()
|
||||
wantQueries := []string{"DESCRIBE `metrics`.`meters`", "DESCRIBE metrics.meters"}
|
||||
if !reflect.DeepEqual(queries, wantQueries) {
|
||||
t.Fatalf("unexpected query sequence: got=%v want=%v", queries, wantQueries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTDengineGetCreateStatementFallsBackToLegacySyntax(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openTDengineRecordingDB(t)
|
||||
state.mu.Lock()
|
||||
state.queryResults["SHOW CREATE TABLE `metrics`.`meters`"] = tdengineQueryResult{
|
||||
err: fmt.Errorf("[0x2600] syntax error near '`metrics`.`meters`'"),
|
||||
}
|
||||
state.queryResults["SHOW CREATE STABLE `metrics`.`meters`"] = tdengineQueryResult{
|
||||
err: fmt.Errorf("[0x2600] syntax error near '`metrics`.`meters`'"),
|
||||
}
|
||||
state.queryResults["SHOW CREATE TABLE metrics.meters"] = tdengineQueryResult{
|
||||
columns: []string{"SQL"},
|
||||
rows: [][]driver.Value{
|
||||
{"CREATE TABLE metrics.meters (ts TIMESTAMP, value DOUBLE)"},
|
||||
},
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
td := &TDengineDB{conn: dbConn}
|
||||
ddl, err := td.GetCreateStatement("metrics", "meters")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCreateStatement returned error: %v", err)
|
||||
}
|
||||
if ddl != "CREATE TABLE metrics.meters (ts TIMESTAMP, value DOUBLE)" {
|
||||
t.Fatalf("unexpected DDL: %q", ddl)
|
||||
}
|
||||
|
||||
queries := state.snapshotQueries()
|
||||
wantQueries := []string{
|
||||
"SHOW CREATE TABLE `metrics`.`meters`",
|
||||
"SHOW CREATE STABLE `metrics`.`meters`",
|
||||
"SHOW CREATE TABLE metrics.meters",
|
||||
}
|
||||
if !reflect.DeepEqual(queries, wantQueries) {
|
||||
t.Fatalf("unexpected query sequence: got=%v want=%v", queries, wantQueries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -267,11 +268,7 @@ func (t *TDengineDB) GetTables(dbName string) ([]string, error) {
|
||||
}
|
||||
|
||||
func (t *TDengineDB) GetCreateStatement(dbName, tableName string) (string, error) {
|
||||
qualified := quoteTDengineTable(dbName, tableName)
|
||||
queries := []string{
|
||||
fmt.Sprintf("SHOW CREATE TABLE %s", qualified),
|
||||
fmt.Sprintf("SHOW CREATE STABLE %s", qualified),
|
||||
}
|
||||
queries := tdengineCreateStatementQueries(dbName, tableName)
|
||||
|
||||
var lastErr error
|
||||
for _, query := range queries {
|
||||
@@ -308,9 +305,25 @@ func (t *TDengineDB) GetCreateStatement(dbName, tableName string) (string, error
|
||||
}
|
||||
|
||||
func (t *TDengineDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
|
||||
query := fmt.Sprintf("DESCRIBE %s", quoteTDengineTable(dbName, tableName))
|
||||
data, _, err := t.Query(query)
|
||||
var (
|
||||
data []map[string]interface{}
|
||||
err error
|
||||
lastErr error
|
||||
)
|
||||
for _, query := range tdengineDescribeQueries(dbName, tableName) {
|
||||
data, _, err = t.Query(query)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
lastErr = err
|
||||
if !isTDengineSyntaxCompatibilityError(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -502,6 +515,77 @@ func escapeBacktickIdent(ident string) string {
|
||||
return strings.ReplaceAll(strings.TrimSpace(ident), "`", "``")
|
||||
}
|
||||
|
||||
func tdengineDescribeQueries(dbName, tableName string) []string {
|
||||
qualified := quoteTDengineTable(dbName, tableName)
|
||||
legacyQualified := quoteTDengineTableLegacy(dbName, tableName)
|
||||
queries := []string{fmt.Sprintf("DESCRIBE %s", qualified)}
|
||||
if legacyQualified != qualified {
|
||||
queries = append(queries, fmt.Sprintf("DESCRIBE %s", legacyQualified))
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
||||
func tdengineCreateStatementQueries(dbName, tableName string) []string {
|
||||
queries := make([]string, 0, 4)
|
||||
appendQualifiedQueries := func(qualified string) {
|
||||
if strings.TrimSpace(qualified) == "" {
|
||||
return
|
||||
}
|
||||
queries = append(queries,
|
||||
fmt.Sprintf("SHOW CREATE TABLE %s", qualified),
|
||||
fmt.Sprintf("SHOW CREATE STABLE %s", qualified),
|
||||
)
|
||||
}
|
||||
qualified := quoteTDengineTable(dbName, tableName)
|
||||
appendQualifiedQueries(qualified)
|
||||
legacyQualified := quoteTDengineTableLegacy(dbName, tableName)
|
||||
if legacyQualified != qualified {
|
||||
appendQualifiedQueries(legacyQualified)
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
||||
func quoteTDengineTableLegacy(dbName, tableName string) string {
|
||||
table := strings.TrimSpace(tableName)
|
||||
if table == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(table, ".") {
|
||||
return strings.Join(splitTDengineIdentifierParts(table), ".")
|
||||
}
|
||||
db := strings.TrimSpace(dbName)
|
||||
if db == "" {
|
||||
return table
|
||||
}
|
||||
return db + "." + table
|
||||
}
|
||||
|
||||
func splitTDengineIdentifierParts(path string) []string {
|
||||
parts := strings.Split(strings.TrimSpace(path), ".")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.Trim(strings.TrimSpace(part), "`")
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isTDengineSyntaxCompatibilityError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
text := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(text, "syntax error near") ||
|
||||
strings.Contains(text, "[0x2600]") ||
|
||||
errors.Is(err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func quoteTDengineTable(dbName, tableName string) string {
|
||||
t := escapeBacktickIdent(tableName)
|
||||
if t == "" {
|
||||
|
||||
Reference in New Issue
Block a user