🐛 fix(tdengine): 修复低版本驱动连接与表元数据兼容问题

- 修复 TDengine 历史驱动源码构建未按所选版本切换依赖的问题
- 为 DESCRIBE 与 SHOW CREATE 增加旧版本语法降级,避免表详情加载报错
- 为表概览补充 TDengine 专用查询分支,避免误查 information_schema
- 补充 TDengine 兼容性与驱动构建回归测试

Refs #531
This commit is contained in:
Syngnat
2026-06-03 21:33:15 +08:00
parent c6f6f76489
commit 23ac30086f
8 changed files with 345 additions and 18 deletions

View File

@@ -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")

View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 == "" {