From 23ac30086fcf81e7768e75efdae24d48c68c86bf Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 3 Jun 2026 21:33:15 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(tdengine):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BD=8E=E7=89=88=E6=9C=AC=E9=A9=B1=E5=8A=A8=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E4=B8=8E=E8=A1=A8=E5=85=83=E6=95=B0=E6=8D=AE=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 TDengine 历史驱动源码构建未按所选版本切换依赖的问题 - 为 DESCRIBE 与 SHOW CREATE 增加旧版本语法降级,避免表详情加载报错 - 为表概览补充 TDengine 专用查询分支,避免误查 information_schema - 补充 TDengine 兼容性与驱动构建回归测试 Refs #531 --- frontend/package.json.md5 | 2 +- frontend/src/components/TableOverview.tsx | 4 +- go.mod | 5 +- go.sum | 6 +- internal/app/methods_driver.go | 101 +++++++++++++++++- .../app/methods_driver_tdengine_build_test.go | 75 +++++++++++++ internal/db/tdengine_applychanges_test.go | 72 +++++++++++++ internal/db/tdengine_impl.go | 98 +++++++++++++++-- 8 files changed, 345 insertions(+), 18 deletions(-) create mode 100644 internal/app/methods_driver_tdengine_build_test.go diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index bed8925..7396e24 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -0295a42fd931778d85157816d79d29e5 \ No newline at end of file +d0464f9da25e9356e61652e638c99ffe \ No newline at end of file diff --git a/frontend/src/components/TableOverview.tsx b/frontend/src/components/TableOverview.tsx index 9aa5d0d..6a5cc68 100644 --- a/frontend/src/components/TableOverview.tsx +++ b/frontend/src/components/TableOverview.tsx @@ -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[]): 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']), diff --git a/go.mod b/go.mod index 40ed829..ea49a54 100644 --- a/go.mod +++ b/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 ( diff --git a/go.sum b/go.sum index db802b4..b15bbc0 100644 --- a/go.sum +++ b/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= diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index d6e80a3..b1a9115 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -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\s*%s\s+)v[^\s]+(?P\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") diff --git a/internal/app/methods_driver_tdengine_build_test.go b/internal/app/methods_driver_tdengine_build_test.go new file mode 100644 index 0000000..917bd9d --- /dev/null +++ b/internal/app/methods_driver_tdengine_build_test.go @@ -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) + } +} diff --git a/internal/db/tdengine_applychanges_test.go b/internal/db/tdengine_applychanges_test.go index 7292d35..e2ab794 100644 --- a/internal/db/tdengine_applychanges_test.go +++ b/internal/db/tdengine_applychanges_test.go @@ -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) + } +} diff --git a/internal/db/tdengine_impl.go b/internal/db/tdengine_impl.go index 785d307..b43d249 100644 --- a/internal/db/tdengine_impl.go +++ b/internal/db/tdengine_impl.go @@ -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 == "" {