From 3f1823b82cf3f5b9a4fc05142a19bcafbdb949ae Mon Sep 17 00:00:00 2001 From: yoga_xu <375624193@qq.com> Date: Tue, 14 Apr 2026 13:28:07 +0800 Subject: [PATCH] feat: add TDengine support with transaction handling - Introduced TDengine dialect with support for creating version tables and handling migrations. - Updated `Create` function to select appropriate SQL templates based on transaction support. - Added tests for creating SQL templates specific to TDengine and PostgreSQL, ensuring correct transaction annotations. - Enhanced error handling for transaction support in legacy stores. - Updated README and CLI documentation to include TDengine examples and build tags. --- README.md | 11 +++--- cmd/goose/driver_tdengine.go | 7 ++++ cmd/goose/main.go | 10 ++++++ create.go | 24 ++++++++++++- create_test.go | 40 +++++++++++++++++++++ database/dialects.go | 2 ++ db.go | 4 ++- dialect.go | 3 ++ go.mod | 5 +++ go.sum | 19 ++++++++++ goose_cli_test.go | 2 +- internal/dialects/tdengine.go | 54 +++++++++++++++++++++++++++++ internal/legacystore/legacystore.go | 27 +++++++++++++++ migrate.go | 45 +++++++++++++++++++++++- 14 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 cmd/goose/driver_tdengine.go create mode 100644 internal/dialects/tdengine.go diff --git a/README.md b/README.md index 10094bd12..7e64f4038 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ Manage your **database schema** by creating incremental SQL changes or Go functi #### Features - Works against multiple databases: - - Postgres, MySQL, Spanner, SQLite, YDB, ClickHouse, MSSQL, Vertica, and - more. + - Postgres, MySQL, Spanner, SQLite, YDB, ClickHouse, MSSQL, TDengine, + Vertica, and more. - Supports Go migrations written as plain functions. - Supports [embedded](https://pkg.go.dev/embed/) migrations. - Out-of-order migrations. @@ -39,8 +39,8 @@ Binary too big? Build a lite version by excluding the drivers you don't need: go build -tags='no_postgres no_mysql no_sqlite3 no_ydb' -o goose ./cmd/goose # Available build tags: -# no_clickhouse no_libsql no_mssql no_mysql -# no_postgres no_sqlite3 no_vertica no_ydb +# no_clickhouse no_libsql no_mssql no_mysql +# no_postgres no_sqlite3 no_tdengine no_vertica no_ydb ``` For macOS users `goose` is available as a [Homebrew @@ -81,6 +81,7 @@ Drivers: ydb starrocks turso + tdengine Examples: goose sqlite3 ./foo.db status @@ -98,6 +99,7 @@ Examples: goose clickhouse "tcp://127.0.0.1:9000" status goose ydb "grpcs://localhost:2135/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status goose starrocks "user:password@/dbname?parseTime=true&interpolateParams=true" status + goose tdengine "root:taosdata@ws(localhost:6041)/" status GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql @@ -105,6 +107,7 @@ Examples: GOOSE_DRIVER=mysql GOOSE_DBSTRING="user:password@/dbname" goose status GOOSE_DRIVER=redshift GOOSE_DBSTRING="postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" goose status GOOSE_DRIVER=clickhouse GOOSE_DBSTRING="clickhouse://user:password@qwerty.clickhouse.cloud:9440/dbname?secure=true&skip_verify=false" goose status + GOOSE_DRIVER=tdengine GOOSE_DBSTRING="root:taosdata@ws(localhost:6041)/" goose status Options: diff --git a/cmd/goose/driver_tdengine.go b/cmd/goose/driver_tdengine.go new file mode 100644 index 000000000..e767369b3 --- /dev/null +++ b/cmd/goose/driver_tdengine.go @@ -0,0 +1,7 @@ +//go:build !no_tdengine + +package main + +import ( + _ "github.com/taosdata/driver-go/v3/taosWS" +) diff --git a/cmd/goose/main.go b/cmd/goose/main.go index a5f50eda0..2e1080aa0 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -112,6 +112,13 @@ func main() { } return case "create": + // For create command we may not open a DB connection, so use configured driver (if any) + // to select a dialect-aware SQL template (e.g. NO TRANSACTION for tdengine). + if envConfig.driver != "" { + if err := goose.SetDialect(envConfig.driver); err != nil { + log.Fatalf("goose create: %v", err) + } + } if err := goose.RunContext(ctx, "create", nil, *dir, args[1:]...); err != nil { log.Fatalf("goose run: %v", err) } @@ -271,6 +278,7 @@ Drivers: ydb starrocks turso + tdengine Examples: goose sqlite3 ./foo.db status @@ -287,6 +295,7 @@ Examples: goose clickhouse "tcp://127.0.0.1:9000" status goose ydb "grpcs://localhost:2135/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status goose turso "libsql://dbname.turso.io?authToken=token" status + goose tdengine "root:taosdata@ws(localhost:6041)/" status GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql @@ -295,6 +304,7 @@ Examples: GOOSE_DRIVER=redshift GOOSE_DBSTRING="postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" goose status GOOSE_DRIVER=turso GOOSE_DBSTRING="libsql://dbname.turso.io?authToken=token" goose status GOOSE_DRIVER=clickhouse GOOSE_DBSTRING="clickhouse://user:password@qwerty.clickhouse.cloud:9440/dbname?secure=true&skip_verify=false" goose status + GOOSE_DRIVER=tdengine GOOSE_DBSTRING="root:taosdata@ws(localhost:6041)/" goose status Options: ` diff --git a/create.go b/create.go index a26eae510..d44bc3be5 100644 --- a/create.go +++ b/create.go @@ -8,6 +8,8 @@ import ( "path/filepath" "text/template" "time" + + "github.com/pressly/goose/v3/internal/legacystore" ) type tmplVars struct { @@ -53,7 +55,11 @@ func CreateWithTemplate(db *sql.DB, dir string, tmpl *template.Template, name, m if migrationType == "go" { tmpl = goSQLMigrationTemplate } else { - tmpl = sqlMigrationTemplate + if currentStoreSupportsTx() { + tmpl = sqlMigrationTemplate + } else { + tmpl = sqlNoTxMigrationTemplate + } } } @@ -92,6 +98,22 @@ SELECT 'up SQL query'; SELECT 'down SQL query'; `)) +var sqlNoTxMigrationTemplate = template.Must(template.New("goose.sql-migration-no-tx").Parse(`-- +goose NO TRANSACTION + +-- +goose Up +SELECT 'up SQL query'; + +-- +goose Down +SELECT 'down SQL query'; +`)) + +func currentStoreSupportsTx() bool { + if txSupporter, ok := store.(legacystore.TxSupporter); ok { + return txSupporter.SupportsTx() + } + return true +} + var goSQLMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`package migrations import ( diff --git a/create_test.go b/create_test.go index 34791cc65..226629ad4 100644 --- a/create_test.go +++ b/create_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" "testing" "time" @@ -50,3 +51,42 @@ func TestSequential(t *testing.T) { } } } + +func TestCreateSQLTemplateByTxCapability(t *testing.T) { + t.Cleanup(func() { + _ = SetDialect("postgres") + }) + + createAndRead := func(t *testing.T, dialect, name string) string { + t.Helper() + if err := SetDialect(dialect); err != nil { + t.Fatalf("set dialect: %v", err) + } + dir := t.TempDir() + if err := Create(nil, dir, name, "sql"); err != nil { + t.Fatalf("create migration: %v", err) + } + files, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read dir: %v", err) + } + if len(files) != 1 { + t.Fatalf("expected 1 file, got %d", len(files)) + } + content, err := os.ReadFile(filepath.Join(dir, files[0].Name())) + if err != nil { + t.Fatalf("read migration file: %v", err) + } + return string(content) + } + + tdengineSQL := createAndRead(t, "tdengine", "tdengine_no_tx") + if !strings.Contains(tdengineSQL, "-- +goose NO TRANSACTION") { + t.Fatalf("expected tdengine SQL template to include NO TRANSACTION annotation") + } + + postgresSQL := createAndRead(t, "postgres", "postgres_tx") + if strings.Contains(postgresSQL, "-- +goose NO TRANSACTION") { + t.Fatalf("expected postgres SQL template not to include NO TRANSACTION annotation") + } +} diff --git a/database/dialects.go b/database/dialects.go index f239171db..5aab5a265 100644 --- a/database/dialects.go +++ b/database/dialects.go @@ -24,6 +24,7 @@ const ( DialectSQLite3 Dialect = "sqlite3" DialectSpanner Dialect = "spanner" DialectStarrocks Dialect = "starrocks" + DialectTDengine Dialect = "tdengine" DialectTiDB Dialect = "tidb" DialectTurso Dialect = "turso" DialectYdB Dialect = "ydb" @@ -47,6 +48,7 @@ func NewStore(d Dialect, tableName string) (Store, error) { DialectSQLite3: dialects.NewSqlite3(), DialectSpanner: dialects.NewSpanner(), DialectStarrocks: dialects.NewStarrocks(), + DialectTDengine: dialects.NewTDengine(), DialectTiDB: dialects.NewTidb(), DialectTurso: dialects.NewTurso(), DialectVertica: dialects.NewVertica(), diff --git a/db.go b/db.go index a8606f7c3..207b1376c 100644 --- a/db.go +++ b/db.go @@ -37,10 +37,12 @@ func OpenDBWithDriver(driver string, dbstring string) (*sql.DB, error) { driver = "pgx" case "starrocks": driver = "mysql" + case "tdengine": + driver = "taosWS" } switch driver { - case "postgres", "pgx", "sqlite3", "sqlite", "spanner", "mysql", "sqlserver", "clickhouse", "vertica", "azuresql", "ydb", "libsql", "starrocks": + case "postgres", "pgx", "sqlite3", "sqlite", "spanner", "mysql", "sqlserver", "clickhouse", "vertica", "azuresql", "ydb", "libsql", "starrocks", "taosWS": return sql.Open(driver, dbstring) default: return nil, fmt.Errorf("unsupported driver %s", driver) diff --git a/dialect.go b/dialect.go index b011e858c..fafcd4d01 100644 --- a/dialect.go +++ b/dialect.go @@ -20,6 +20,7 @@ const ( DialectSQLite3 Dialect = database.DialectSQLite3 DialectSpanner Dialect = database.DialectSpanner DialectStarrocks Dialect = database.DialectStarrocks + DialectTDengine Dialect = database.DialectTDengine DialectTiDB Dialect = database.DialectTiDB DialectTurso Dialect = database.DialectTurso DialectYdB Dialect = database.DialectYdB @@ -65,6 +66,8 @@ func SetDialect(s string) error { d = DialectTurso case "starrocks": d = DialectStarrocks + case "tdengine": + d = DialectTDengine default: return fmt.Errorf("%q: unknown dialect", s) } diff --git a/go.mod b/go.mod index 4ce8e8f3c..4e0101e8d 100644 --- a/go.mod +++ b/go.mod @@ -49,13 +49,17 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -66,6 +70,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/taosdata/driver-go/v3 v3.5.6 // indirect github.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect diff --git a/go.sum b/go.sum index f4626ad58..420b8b857 100644 --- a/go.sum +++ b/go.sum @@ -117,11 +117,15 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= @@ -139,6 +143,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -167,6 +173,10 @@ github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= @@ -203,14 +213,23 @@ github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLy github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/taosdata/driver-go/v3 v3.5.6 h1:LDVtMyT3B9p2VREsd5KKM91D4Y7P4kSdh2SQumXi8bk= +github.com/taosdata/driver-go/v3 v3.5.6/go.mod h1:H2vo/At+rOPY1aMzUV9P49SVX7NlXb3LAbKw+MCLrmU= +github.com/taosdata/driver-go/v3 v3.8.0 h1:qY1xFidspISuuZc0TAbeJWMtAdKJOSYQOXfepkUK6fI= +github.com/taosdata/driver-go/v3 v3.8.0/go.mod h1:S6OGOinfR0xxxaMGsvBi9cLkYxEIW1p6qqr8QJATTlg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc h1:lzi/5fg2EfinRlh3v//YyIhnc4tY7BTqazQGwb1ar+0= github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= diff --git a/goose_cli_test.go b/goose_cli_test.go index 1f4185ac5..a97101447 100644 --- a/goose_cli_test.go +++ b/goose_cli_test.go @@ -186,7 +186,7 @@ func buildGooseCLI(t *testing.T, lite bool) gooseBinary { "-ldflags=-s -w -X main.version=" + gooseTestBinaryVersion, } if lite { - args = append(args, "-tags=no_clickhouse no_mssql no_mysql no_vertica no_postgres") + args = append(args, "-tags=no_clickhouse no_mssql no_mysql no_vertica no_postgres no_tdengine") } args = append(args, "./cmd/goose") build := exec.Command("go", args...) diff --git a/internal/dialects/tdengine.go b/internal/dialects/tdengine.go new file mode 100644 index 000000000..40b9bfaa8 --- /dev/null +++ b/internal/dialects/tdengine.go @@ -0,0 +1,54 @@ +package dialects + +import ( + "fmt" + + "github.com/pressly/goose/v3/database/dialect" +) + +// NewTDengine returns a [dialect.Querier] for TDengine dialect. +func NewTDengine() dialect.Querier { + return &tdengine{} +} + +type tdengine struct{} + +var _ dialect.Querier = (*tdengine)(nil) + +func (t *tdengine) SupportsTx() bool { + return false +} + +func (t *tdengine) CreateTable(tableName string) string { + q := `CREATE TABLE IF NOT EXISTS %s ( + version_id TIMESTAMP, + is_applied BOOL, + tstamp TIMESTAMP + )` + return fmt.Sprintf(q, tableName) +} + +func (t *tdengine) InsertVersion(tableName string) string { + q := `INSERT INTO %s VALUES (?, ?, now())` + return fmt.Sprintf(q, tableName) +} + +func (t *tdengine) DeleteVersion(tableName string) string { + q := `DELETE FROM %s WHERE version_id=?` + return fmt.Sprintf(q, tableName) +} + +func (t *tdengine) GetMigrationByVersion(tableName string) string { + q := `SELECT version_id, is_applied FROM %s WHERE version_id=? ORDER BY version_id DESC LIMIT 1` + return fmt.Sprintf(q, tableName) +} + +func (t *tdengine) ListMigrations(tableName string) string { + q := `SELECT CAST(version_id AS BIGINT), is_applied FROM %s ORDER BY version_id DESC` + return fmt.Sprintf(q, tableName) +} + +func (t *tdengine) GetLatestVersion(tableName string) string { + q := `SELECT CAST(MAX(version_id) AS BIGINT) FROM %s` + return fmt.Sprintf(q, tableName) +} diff --git a/internal/legacystore/legacystore.go b/internal/legacystore/legacystore.go index c134a0e0a..bd60cb9da 100644 --- a/internal/legacystore/legacystore.go +++ b/internal/legacystore/legacystore.go @@ -24,6 +24,8 @@ type Store interface { // CreateVersionTable creates the version table within a transaction. // This table is used to store goose migrations. CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error + // CreateVersionTableNoTx creates the version table without a transaction. + CreateVersionTableNoTx(ctx context.Context, db *sql.DB, tableName string) error // InsertVersion inserts a version id into the version table within a transaction. InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error @@ -47,6 +49,13 @@ type Store interface { ListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error) } +// TxSupporter is an optional interface to declare transaction capability. +// +// If a Store does not implement this interface, goose assumes transactions are supported. +type TxSupporter interface { + SupportsTx() bool +} + // NewStore returns a new Store for the given dialect. func NewStore(d database.Dialect) (Store, error) { var querier dialect.Querier @@ -75,6 +84,8 @@ func NewStore(d database.Dialect) (Store, error) { querier = dialects.NewTurso() case database.DialectStarrocks: querier = dialects.NewStarrocks() + case database.DialectTDengine: + querier = dialects.NewTDengine() default: return nil, fmt.Errorf("unknown querier dialect: %v", d) } @@ -103,6 +114,12 @@ func (s *store) CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName st return err } +func (s *store) CreateVersionTableNoTx(ctx context.Context, db *sql.DB, tableName string) error { + q := s.querier.CreateTable(tableName) + _, err := db.ExecContext(ctx, q) + return err +} + func (s *store) InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error { q := s.querier.InsertVersion(tableName) _, err := tx.ExecContext(ctx, q, version, true) @@ -171,3 +188,13 @@ func (s *store) ListMigrations(ctx context.Context, db *sql.DB, tableName string } return migrations, nil } + +// SupportsTx returns whether the current dialect supports transaction operations. +// +// If the dialect does not provide this capability, default to true for backward compatibility. +func (s *store) SupportsTx() bool { + if v, ok := s.querier.(interface{ SupportsTx() bool }); ok { + return v.SupportsTx() + } + return true +} diff --git a/migrate.go b/migrate.go index b60c61ae7..6c8a92701 100644 --- a/migrate.go +++ b/migrate.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/pressly/goose/v3/internal/legacystore" "go.uber.org/multierr" ) @@ -253,19 +254,61 @@ func EnsureDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) { // createVersionTable creates the db version table and inserts the // initial 0 value into it. func createVersionTable(ctx context.Context, db *sql.DB) error { + if !legacyStoreSupportsTx(store) { + return createVersionTableNoTx(ctx, db) + } txn, err := db.BeginTx(ctx, nil) if err != nil { + if isTxUnsupportedErr(err) { + return createVersionTableNoTx(ctx, db) + } return err } if err := store.CreateVersionTable(ctx, txn, TableName()); err != nil { _ = txn.Rollback() + if isTxUnsupportedErr(err) { + return createVersionTableNoTx(ctx, db) + } return err } if err := store.InsertVersion(ctx, txn, TableName(), 0); err != nil { _ = txn.Rollback() + if isTxUnsupportedErr(err) { + return createVersionTableNoTx(ctx, db) + } return err } - return txn.Commit() + if err := txn.Commit(); err != nil { + if isTxUnsupportedErr(err) { + return createVersionTableNoTx(ctx, db) + } + return err + } + return nil +} + +func createVersionTableNoTx(ctx context.Context, db *sql.DB) error { + if err := store.CreateVersionTableNoTx(ctx, db, TableName()); err != nil { + return err + } + return store.InsertVersionNoTx(ctx, db, TableName(), 0) +} + +func legacyStoreSupportsTx(s legacystore.Store) bool { + if v, ok := s.(legacystore.TxSupporter); ok { + return v.SupportsTx() + } + return true +} + +func isTxUnsupportedErr(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "does not support transaction") || + strings.Contains(msg, "not support transaction") || + strings.Contains(msg, "websocket does not support transaction") } // GetDBVersion is an alias for EnsureDBVersion, but returns -1 in error.