mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-06 18:41:42 +08:00
🐛 fix(tdengine): 修复低版本驱动连接与表元数据兼容问题
- 修复 TDengine 历史驱动源码构建未按所选版本切换依赖的问题 - 为 DESCRIBE 与 SHOW CREATE 增加旧版本语法降级,避免表详情加载报错 - 为表概览补充 TDengine 专用查询分支,避免误查 information_schema - 补充 TDengine 兼容性与驱动构建回归测试 Refs #531
This commit is contained in:
@@ -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