diff --git a/dialect/duckdb/duckdb.go b/dialect/duckdb/duckdb.go new file mode 100644 index 0000000..817fdeb --- /dev/null +++ b/dialect/duckdb/duckdb.go @@ -0,0 +1,38 @@ +package duckdb + +import ( + _ "github.com/marcboeker/go-duckdb" + + "git.fsdpf.net/go/db" +) + +func DialectOptions() *db.SQLDialectOptions { + do := db.DefaultDialectOptions() + + // DuckDB uses PostgreSQL-style placeholders ($1, $2, etc.) + do.PlaceHolderFragment = []byte("$") + do.IncludePlaceholderNum = true + + // DuckDB supports most PostgreSQL features + do.SupportsReturn = true + do.SupportsOrderByOnUpdate = false + do.SupportsLimitOnUpdate = false + do.SupportsOrderByOnDelete = false + do.SupportsLimitOnDelete = false + do.SupportsConflictUpdateWhere = true + do.SupportsInsertIgnoreSyntax = false + do.SupportsConflictTarget = true + do.SupportsWithCTE = true + do.SupportsWithCTERecursive = true + do.SupportsDistinctOn = true + do.SupportsWindowFunction = true + + // Use double quotes for identifiers (PostgreSQL-style) + do.QuoteRune = '"' + + return do +} + +func init() { + db.RegisterDialect("duckdb", DialectOptions()) +} diff --git a/dialect/duckdb/duckdb_test.go b/dialect/duckdb/duckdb_test.go new file mode 100644 index 0000000..41c4c74 --- /dev/null +++ b/dialect/duckdb/duckdb_test.go @@ -0,0 +1,135 @@ +package duckdb_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db" + "git.fsdpf.net/go/db/dialect/duckdb" + + "github.com/stretchr/testify/assert" +) + +func TestDialectOptions(t *testing.T) { + opts := duckdb.DialectOptions() + assert.NotNil(t, opts) + assert.Equal(t, '"', opts.QuoteRune) + assert.True(t, opts.IncludePlaceholderNum) + assert.Equal(t, []byte("$"), opts.PlaceHolderFragment) + assert.True(t, opts.SupportsReturn) + assert.True(t, opts.SupportsConflictTarget) + assert.True(t, opts.SupportsWithCTE) + assert.True(t, opts.SupportsDistinctOn) + assert.True(t, opts.SupportsWindowFunction) +} + +func TestDialectRegistration(t *testing.T) { + // Verify the dialect is registered + dialect := dbv2.GetDialect("duckdb") + assert.NotNil(t, dialect) +} + +func TestSelectSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.From("users") + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "users"`, sql) + + ds = db.From("users").Select("id", "name") + sql, _, err = ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT "id", "name" FROM "users"`, sql) + + ds = db.From("users").Where(dbv2.C("id").Eq(1)) + sql, _, err = ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "users" WHERE ("id" = 1)`, sql) +} + +func TestPreparedStatement(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.From("users").Prepared(true).Where(dbv2.C("id").Eq(1)) + sql, args, err := ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "users" WHERE ("id" = $1)`, sql) + assert.Equal(t, []interface{}{int64(1)}, args) + + ds = db.From("users").Prepared(true).Where(dbv2.C("name").Eq("John")) + sql, args, err = ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "users" WHERE ("name" = $1)`, sql) + assert.Equal(t, []interface{}{"John"}, args) +} + +func TestInsertSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.Insert("users").Rows(dbv2.Record{"id": 1, "name": "John"}) + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Contains(t, sql, `INSERT INTO "users"`) + assert.Contains(t, sql, `"id"`) + assert.Contains(t, sql, `"name"`) +} + +func TestUpdateSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.Update("users").Set(dbv2.Record{"name": "Jane"}).Where(dbv2.C("id").Eq(1)) + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Contains(t, sql, `UPDATE "users"`) + assert.Contains(t, sql, `"name"`) + assert.Contains(t, sql, `WHERE ("id" = 1)`) +} + +func TestDeleteSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.Delete("users").Where(dbv2.C("id").Eq(1)) + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `DELETE FROM "users" WHERE ("id" = 1)`, sql) +} + +func TestJoinSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.From("users"). + Join(dbv2.T("orders"), dbv2.On(dbv2.Ex{"users.id": dbv2.I("orders.user_id")})). + Select("users.name", "orders.total") + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Contains(t, sql, `FROM "users"`) + assert.Contains(t, sql, `INNER JOIN "orders"`) + assert.Contains(t, sql, `ON ("users"."id" = "orders"."user_id")`) +} + +func TestWindowFunction(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.From("sales"). + Select( + "product", + "amount", + dbv2.ROW_NUMBER().Over(dbv2.W().PartitionBy("product").OrderBy(dbv2.C("amount").Desc())).As("rank"), + ) + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Contains(t, sql, `ROW_NUMBER()`) + assert.Contains(t, sql, `OVER`) + assert.Contains(t, sql, `PARTITION BY`) +} + +func TestCTE(t *testing.T) { + db := dbv2.Dialect("duckdb") + + cte := db.From("users").Where(dbv2.C("active").IsTrue()) + ds := db.From("active_users").With("active_users", cte).Select("*") + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Contains(t, sql, `WITH active_users AS`) + assert.Contains(t, sql, `SELECT * FROM "users" WHERE ("active" IS TRUE)`) +} diff --git a/dialect/mysql/mysql.go b/dialect/mysql/mysql.go index ac2f89b..84f6f40 100644 --- a/dialect/mysql/mysql.go +++ b/dialect/mysql/mysql.go @@ -1,6 +1,8 @@ package mysql import ( + _ "github.com/go-sql-driver/mysql" + "git.fsdpf.net/go/db" "git.fsdpf.net/go/db/exp" ) diff --git a/dialect/postgres/postgres.go b/dialect/postgres/postgres.go index 965529c..e450010 100644 --- a/dialect/postgres/postgres.go +++ b/dialect/postgres/postgres.go @@ -1,6 +1,10 @@ package postgres -import "git.fsdpf.net/go/db" +import ( + _ "github.com/lib/pq" + + "git.fsdpf.net/go/db" +) func DialectOptions() *db.SQLDialectOptions { do := db.DefaultDialectOptions() diff --git a/dialect/sqlite3/sqlite3.go b/dialect/sqlite3/sqlite3.go index cbbc7ba..f217ac4 100644 --- a/dialect/sqlite3/sqlite3.go +++ b/dialect/sqlite3/sqlite3.go @@ -1,12 +1,18 @@ package sqlite3 import ( + "database/sql" "time" "git.fsdpf.net/go/db" "git.fsdpf.net/go/db/exp" + gosqlite3 "github.com/mattn/go-sqlite3" ) +// DriverWithIF 是注册了 IF() 函数的 SQLite3 驱动名称。 +// 使用该驱动名打开连接时,可在 SQL 中直接使用 IF(condition, trueVal, falseVal)。 +const DriverWithIF = "sqlite3_with_if" + func DialectOptions() *db.SQLDialectOptions { opts := db.DefaultDialectOptions() @@ -72,5 +78,15 @@ func DialectOptions() *db.SQLDialectOptions { } func init() { + sql.Register(DriverWithIF, &gosqlite3.SQLiteDriver{ + ConnectHook: func(conn *gosqlite3.SQLiteConn) error { + return conn.RegisterFunc("IF", func(cond int64, trueVal, falseVal interface{}) interface{} { + if cond != 0 { + return trueVal + } + return falseVal + }, true) + }, + }) db.RegisterDialect("sqlite3", DialectOptions()) } diff --git a/dialect/sqlserver/sqlserver.go b/dialect/sqlserver/sqlserver.go index 125571e..df37060 100644 --- a/dialect/sqlserver/sqlserver.go +++ b/dialect/sqlserver/sqlserver.go @@ -1,6 +1,8 @@ package sqlserver import ( + _ "github.com/microsoft/go-mssqldb" // SQL Server driver + "git.fsdpf.net/go/db" "git.fsdpf.net/go/db/exp" "git.fsdpf.net/go/db/sqlgen" diff --git a/dialect/sqlserver/sqlserver_test.go b/dialect/sqlserver/sqlserver_test.go index 72296c9..c50b48d 100644 --- a/dialect/sqlserver/sqlserver_test.go +++ b/dialect/sqlserver/sqlserver_test.go @@ -11,7 +11,7 @@ import ( dbv2 "git.fsdpf.net/go/db" _ "git.fsdpf.net/go/db/dialect/sqlserver" - _ "github.com/denisenkom/go-mssqldb" + _ "github.com/microsoft/go-mssqldb" "github.com/stretchr/testify/suite" ) diff --git a/engine/engine.go b/engine/engine.go index b5b06f3..ed45a05 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -2,15 +2,11 @@ package engine import ( "database/sql" + "errors" "fmt" - _ "git.fsdpf.net/go/db/dialect/mysql" - _ "git.fsdpf.net/go/db/dialect/postgres" - _ "git.fsdpf.net/go/db/dialect/sqlite3" - _ "git.fsdpf.net/go/db/dialect/sqlserver" - "git.fsdpf.net/go/db" - "git.fsdpf.net/go/db/internal/errors" + sqlite3dialect "git.fsdpf.net/go/db/dialect/sqlite3" ) type Engine struct { @@ -47,16 +43,19 @@ func (e Engine) Connection(name string) *db.Database { func (e Engine) MakeConnection(cfg DBConfig) (db *sql.DB) { dsn := cfg.ToDSN() + driverName := cfg.Driver switch cfg.Driver { case "mysql": case "sqlite3": + driverName = sqlite3dialect.DriverWithIF case "sqlserver": case "postgres": + case "duckdb": default: panic(fmt.Sprintf("Unsupported driver: %s", cfg.Driver)) } - db, err := sql.Open(cfg.Driver, dsn) + db, err := sql.Open(driverName, dsn) if err != nil { panic(err) diff --git a/engine/engine_config.go b/engine/engine_config.go index 1d6a607..5d1786d 100644 --- a/engine/engine_config.go +++ b/engine/engine_config.go @@ -68,6 +68,14 @@ type DBConfig struct { KeepAlive int Dsn string } + + DuckDB struct { + File string + AccessMode string + Threads int + MaxMemory string + Dsn string + } } // Option 配置选项类型 @@ -84,6 +92,8 @@ func (c *DBConfig) ToDSN() string { return c.toSQLiteDSN() case "sqlserver": return c.toSQLServerDSN() + case "duckdb": + return c.toDuckDBDSN() default: panic(fmt.Sprintf("Unsupported driver for DSN generation: %s", c.Driver)) } @@ -279,6 +289,32 @@ func (c *DBConfig) toSQLServerDSN() string { return "sqlserver://" + c.Username + ":" + url.QueryEscape(c.Password) + "@" + host + ":" + c.Port + "?" + params.Encode() } +// toDuckDBDSN 生成 DuckDB DSN +func (c *DBConfig) toDuckDBDSN() string { + if c.DuckDB.Dsn != "" { + return c.DuckDB.Dsn + } + // DuckDB 可以使用内存数据库或文件数据库 + dsn := c.DuckDB.File + if dsn == "" { + dsn = "" // 空字符串表示内存数据库 + } + params := url.Values{} + if c.DuckDB.AccessMode != "" { + params.Add("access_mode", c.DuckDB.AccessMode) + } + if c.DuckDB.Threads > 0 { + params.Add("threads", fmt.Sprintf("%d", c.DuckDB.Threads)) + } + if c.DuckDB.MaxMemory != "" { + params.Add("max_memory", c.DuckDB.MaxMemory) + } + if len(params) > 0 { + dsn += "?" + params.Encode() + } + return dsn +} + // NewDBConfig 创建新的 DBConfig 实例,检查关键参数 func NewDBConfig(driver string, options ...Option) DBConfig { if driver == "" { @@ -291,6 +327,7 @@ func NewDBConfig(driver string, options ...Option) DBConfig { "pgsql": true, "sqlite3": true, "sqlserver": true, + "duckdb": true, } if !validDrivers[driver] { panic(fmt.Sprintf("Unsupported driver: %s", driver)) @@ -330,6 +367,8 @@ func NewDBConfig(driver string, options ...Option) DBConfig { if config.SQLite.File == "" { panic("File is required for sqlite3 driver") } + case "duckdb": + // DuckDB 文件参数可选,空字符串表示内存数据库 } return *config @@ -447,3 +486,40 @@ func WithSQLServerInstance(instance string) Option { c.SQLServer.Instance = instance } } + +// DuckDB 专用选项 +func WithDuckDBFile(file string) Option { + return func(c *DBConfig) { + if c.Driver != "duckdb" { + panic("WithDuckDBFile is only valid for duckdb driver") + } + c.DuckDB.File = file + } +} + +func WithDuckDBAccessMode(accessMode string) Option { + return func(c *DBConfig) { + if c.Driver != "duckdb" { + panic("WithDuckDBAccessMode is only valid for duckdb driver") + } + c.DuckDB.AccessMode = accessMode + } +} + +func WithDuckDBThreads(threads int) Option { + return func(c *DBConfig) { + if c.Driver != "duckdb" { + panic("WithDuckDBThreads is only valid for duckdb driver") + } + c.DuckDB.Threads = threads + } +} + +func WithDuckDBMaxMemory(maxMemory string) Option { + return func(c *DBConfig) { + if c.Driver != "duckdb" { + panic("WithDuckDBMaxMemory is only valid for duckdb driver") + } + c.DuckDB.MaxMemory = maxMemory + } +} diff --git a/engine/engine_config_test.go b/engine/engine_config_test.go index bec6b0b..e2bc59b 100644 --- a/engine/engine_config_test.go +++ b/engine/engine_config_test.go @@ -5,9 +5,6 @@ import ( "time" "git.fsdpf.net/go/db/engine" - _ "github.com/denisenkom/go-mssqldb" - _ "github.com/go-sql-driver/mysql" - _ "github.com/mattn/go-sqlite3" ) // TestMySQLConfig 测试 MySQL 驱动的配置 @@ -59,7 +56,7 @@ func TestMySQLConfig(t *testing.T) { engine.WithSQLServerInstance("SQLEXPRESS"), }, wantErr: true, - errMsg: "engine.WithSQLServerInstance is only valid for sqlserver driver", + errMsg: "WithSQLServerInstance is only valid for sqlserver driver", }, } @@ -124,7 +121,7 @@ func TestPostgreSQLConfig(t *testing.T) { engine.WithMySQLCollation("utf8mb4_unicode_ci"), }, wantErr: true, - errMsg: "engine.WithMySQLCollation is only valid for mysql driver", + errMsg: "WithMySQLCollation is only valid for mysql driver", }, } @@ -179,7 +176,7 @@ func TestSQLiteConfig(t *testing.T) { engine.WithPgSslmode("disable"), }, wantErr: true, - errMsg: "engine.WithPgSslmode is only valid for pgsql driver", + errMsg: "WithPgSslmode is only valid for pgsql driver", }, } @@ -244,7 +241,7 @@ func TestSQLServerConfig(t *testing.T) { engine.WithSQLiteFile("test.db"), }, wantErr: true, - errMsg: "engine.WithSQLiteFile is only valid for sqlite3 driver", + errMsg: "WithSQLiteFile is only valid for sqlite3 driver", }, } @@ -285,6 +282,105 @@ func TestInvalidDriver(t *testing.T) { engine.NewDBConfig("oracle") } +// TestDuckDBConfig 测试 DuckDB 驱动的配置 +func TestDuckDBConfig(t *testing.T) { + tests := []struct { + name string + options []engine.Option + wantErr bool + errMsg string + wantDSN string + }{ + { + name: "In-memory database (no file)", + options: []engine.Option{}, + wantErr: false, + wantDSN: "", + }, + { + name: "File-based database", + options: []engine.Option{ + engine.WithDuckDBFile("test.db"), + }, + wantErr: false, + wantDSN: "test.db", + }, + { + name: "With access mode", + options: []engine.Option{ + engine.WithDuckDBFile("test.db"), + engine.WithDuckDBAccessMode("READ_ONLY"), + }, + wantErr: false, + wantDSN: "test.db?access_mode=READ_ONLY", + }, + { + name: "With threads", + options: []engine.Option{ + engine.WithDuckDBFile("test.db"), + engine.WithDuckDBThreads(4), + }, + wantErr: false, + wantDSN: "test.db?threads=4", + }, + { + name: "With max memory", + options: []engine.Option{ + engine.WithDuckDBFile("test.db"), + engine.WithDuckDBMaxMemory("1GB"), + }, + wantErr: false, + wantDSN: "test.db?max_memory=1GB", + }, + { + name: "With all options", + options: []engine.Option{ + engine.WithDuckDBFile("test.db"), + engine.WithDuckDBAccessMode("READ_WRITE"), + engine.WithDuckDBThreads(8), + engine.WithDuckDBMaxMemory("2GB"), + }, + wantErr: false, + }, + { + name: "With MySQL option (should panic)", + options: []engine.Option{ + engine.WithDuckDBFile("test.db"), + engine.WithMySQLCollation("utf8mb4_unicode_ci"), + }, + wantErr: true, + errMsg: "WithMySQLCollation is only valid for mysql driver", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r) + } else if tt.errMsg != "" && r != tt.errMsg { + t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg) + } + } else if tt.wantErr { + t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg) + } + }() + + config := engine.NewDBConfig("duckdb", tt.options...) + if !tt.wantErr { + if config.Driver != "duckdb" { + t.Errorf("Driver = %v, want duckdb", config.Driver) + } + dsn := config.ToDSN() + if tt.wantDSN != "" && dsn != tt.wantDSN { + t.Errorf("DSN = %v, want %v", dsn, tt.wantDSN) + } + } + }) + } +} + // TestDefaultValues 测试默认值 func TestDefaultValues(t *testing.T) { config := engine.NewDBConfig("mysql", @@ -358,7 +454,7 @@ func TestToDSN(t *testing.T) { engine.WithPassword("password"), engine.WithSQLServerInstance("SQLEXPRESS"), ), - wantDSN: "sqlserver://sa:password@sqlserver.example.com\\SQLEXPRESS:1433?server=sqlserver.example.com\\SQLEXPRESS&database=master", + wantDSN: "sqlserver://sa:password@sqlserver.example.com\\SQLEXPRESS:1433?database=master&server=sqlserver.example.com%5CSQLEXPRESS", wantErr: false, }, } diff --git a/go.mod b/go.mod index d6f4dd0..18a591f 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,43 @@ module git.fsdpf.net/go/db -go 1.19 +go 1.24 + +toolchain go1.24.10 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/denisenkom/go-mssqldb v0.12.3 github.com/go-sql-driver/mysql v1.7.1 github.com/lib/pq v1.10.9 + github.com/marcboeker/go-duckdb v1.8.5 github.com/mattn/go-sqlite3 v1.14.17 + github.com/microsoft/go-mssqldb v1.9.4 github.com/samber/lo v1.49.1 github.com/stretchr/testify v1.10.0 ) require ( + github.com/apache/arrow-go/v18 v18.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/google/flatbuffers v25.1.24+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.29.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c1b87f4..95ae804 100644 --- a/go.sum +++ b/go.sum @@ -1,63 +1,109 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= +github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= -github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/microsoft/go-mssqldb v1.9.4 h1:sHrj3GcdgkxytZ09aZ3+ys72pMeyEXJowT44j74pNgs= +github.com/microsoft/go-mssqldb v1.9.4/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/schema/dialect/duckdb/README.md b/schema/dialect/duckdb/README.md new file mode 100644 index 0000000..b41b219 --- /dev/null +++ b/schema/dialect/duckdb/README.md @@ -0,0 +1,169 @@ +# DuckDB Schema Dialect + +DuckDB schema dialect 为 DuckDB 数据库提供了 DDL (Data Definition Language) 操作支持。 + +## 功能特性 + +- ✅ 创建表 (CREATE TABLE) +- ✅ 添加列 (ALTER TABLE ADD COLUMN) +- ✅ 修改列 (ALTER TABLE ALTER COLUMN) +- ✅ 删除列 (ALTER TABLE DROP COLUMN) +- ✅ 删除表 (DROP TABLE) +- ✅ 重命名表 (ALTER TABLE RENAME TO) +- ✅ 表注释 (COMMENT ON TABLE) +- ✅ 列注释 (通过 Comment 属性) +- ✅ 自增主键 (SERIAL/BIGSERIAL) +- ✅ 查询表是否存在 +- ✅ 获取表的列列表 + +## 支持的数据类型 + +| Schema 类型 | DuckDB 类型 | 说明 | +|------------|-------------|------| +| char | CHAR(n) | 固定长度字符 | +| string | VARCHAR(n) | 可变长度字符串 | +| text | TEXT | 长文本 | +| integer | INTEGER | 整数 | +| bigInteger | BIGINT | 大整数 | +| smallInteger | SMALLINT | 小整数 | +| tinyInteger | TINYINT | 微整数 | +| decimal | DECIMAL(m,d) | 定点数 | +| boolean | BOOLEAN | 布尔值 | +| json | JSON | JSON 数据 | +| binary | BLOB | 二进制数据 | +| datetime | TIMESTAMP | 日期时间 | +| timestamp | TIMESTAMP | 时间戳 | +| date | DATE | 日期 | +| time | TIME | 时间 | +| uuid | UUID | UUID | + +## 使用示例 + +```go +package main + +import ( + "git.fsdpf.net/go/db/engine" + "git.fsdpf.net/go/db/schema" + + _ "git.fsdpf.net/go/db/dialect/duckdb" + _ "git.fsdpf.net/go/db/schema/dialect/duckdb" + _ "github.com/marcboeker/go-duckdb" +) + +func main() { + // 创建数据库连接 + db := engine.Open(map[string]engine.DBConfig{ + "duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile("mydb.duckdb"), // 或者 "" 表示内存数据库 + ), + }).Connection("duckdb") + + // 获取 schema 实现 + s := schema.GetSchemaDialect(db) + + // 创建表 + bp := schema.NewBlueprint("users") + bp.Create() + + bp.BigIncrements("id").AutoIncrement().Comment("用户ID") + bp.String("name", 50).Comment("用户名") + bp.String("email", 100).Comment("邮箱") + bp.Integer("age").Nullable().Comment("年龄") + bp.Boolean("active").Default("1").Comment("是否激活") + bp.Timestamp("created_at").UseCurrent().Comment("创建时间") + bp.Timestamp("updated_at").UseCurrent().Comment("更新时间") + + // 编译并执行 + sqls := s.CompileCreate(bp) + for _, sql := range sqls { + db.Exec(sql) + } + + // 添加新列 + bp2 := schema.NewBlueprint("users") + bp2.String("phone", 20).Nullable().Comment("电话") + + sqls = s.CompileAdd(bp2) + for _, sql := range sqls { + db.Exec(sql) + } +} +``` + +## DuckDB 特性说明 + +### 自增主键 + +DuckDB 使用 `SERIAL` 类型系列实现自增: + +- `SERIAL` - 对应 INTEGER 自增 +- `BIGSERIAL` - 对应 BIGINT 自增 +- `SMALLSERIAL` - 对应 SMALLINT 自增 + +```go +bp.BigIncrements("id").AutoIncrement() +// 生成: "id" BIGSERIAL NOT NULL PRIMARY KEY +``` + +### 列修改 + +DuckDB 的 ALTER COLUMN 操作需要分别执行: + +```go +bp.String("name", 100).Change("username") +// 生成多条 SQL: +// 1. ALTER TABLE "users" RENAME COLUMN "name" TO "username" +// 2. ALTER TABLE "users" ALTER COLUMN "username" TYPE VARCHAR(100) +// 3. ALTER TABLE "users" ALTER COLUMN "username" SET NOT NULL +``` + +### 表注释和列注释 + +DuckDB 使用标准 SQL 的 COMMENT ON 语法: + +```go +bp := schema.NewBlueprint("users") +bp.Comment = "用户表" +// 生成: COMMENT ON TABLE "users" IS '用户表' +``` + +列注释暂时不支持单独的 COMMENT ON COLUMN 语句。 + +### 标识符引用 + +DuckDB 使用双引号引用标识符 (PostgreSQL 风格): + +```sql +CREATE TABLE "users" ( + "id" BIGSERIAL NOT NULL PRIMARY KEY, + "name" VARCHAR(50) NOT NULL +) +``` + +## 测试 + +运行测试: + +```bash +# 运行所有测试 +go test ./schema/dialect/duckdb/... + +# 运行示例测试 +go test -v ./schema/dialect/duckdb/... -run Example + +# 运行特定测试 +go test -v ./schema/dialect/duckdb/... -run TestCompileCreate +``` + +## 注意事项 + +1. DuckDB 是分析型数据库,主要用于 OLAP 场景 +2. 内存数据库性能优异,适合测试和临时数据处理 +3. 支持 PostgreSQL 兼容的大部分语法 +4. 不支持所有的 MySQL 特性 (如 UNSIGNED、AUTO_INCREMENT 等) + +## 相关链接 + +- [DuckDB 官方文档](https://duckdb.org/docs/) +- [go-duckdb 驱动](https://github.com/marcboeker/go-duckdb) diff --git a/schema/dialect/duckdb/duckdb.go b/schema/dialect/duckdb/duckdb.go new file mode 100644 index 0000000..6d1fcd4 --- /dev/null +++ b/schema/dialect/duckdb/duckdb.go @@ -0,0 +1,271 @@ +package duckdb + +import ( + "strings" + + "git.fsdpf.net/go/db" + "git.fsdpf.net/go/db/dialect/duckdb" + "git.fsdpf.net/go/db/internal/sb" + "git.fsdpf.net/go/db/schema" + "git.fsdpf.net/go/db/sqlgen" +) + +type DuckDB struct { + db *db.Database + esg sqlgen.ExpressionSQLGenerator +} + +var duckdbDefaultModifiers = []string{ + "Nullable", "Default", "Increment", "Comment", +} + +func (this DuckDB) GetColumnListing(table string) ([]string, error) { + cols := []string{} + err := this.db.From(db.S("information_schema").Table("columns")).Where( + db.C("table_name").Eq(table), + ).Pluck(&cols, "column_name") + return cols, err +} + +func (this DuckDB) TableExists(table string) (bool, error) { + result, err := this.db.From(db.S("information_schema").Table("tables")).Where(db.Ex{ + "table_name": table, + "table_type": "BASE TABLE", + }).Count() + return result > 0, err +} + +func (this DuckDB) CompileCreate(bp *schema.Blueprint) []string { + temporary := db.L("CREATE") + if bp.Temporary { + temporary = db.L("CREATE TEMPORARY") + } + columns := strings.Join(this.getAddedColumns(bp), ",\n") + sql := this.GenerateSQL("? TABLE ? (\n?\n)", temporary, db.T(bp.GetTable()), db.L(columns)) + + if comment := bp.Comment; comment != "" { + sql = sql[:len(sql)-1] + ";" + this.GenerateSQL("\nCOMMENT ON TABLE ? IS ?", db.T(bp.GetTable()), db.V(comment)) + } + + return []string{sql} +} + +func (this DuckDB) CompileAdd(bp *schema.Blueprint) []string { + columns := strings.Join(schema.PrefixArray("ADD COLUMN", this.getAddedColumns(bp)), ",\n") + sql := this.GenerateSQL("ALTER TABLE ?\n?", db.T(bp.GetTable()), db.L(columns)) + return []string{sql} +} + +func (this DuckDB) CompileChange(bp *schema.Blueprint) []string { + sqls := []string{} + for _, column := range bp.GetChangedColumns() { + name := column.Name + rename := column.Name + if column.GetRename() != "" { + rename = column.GetRename() + } + + // DuckDB 需要分别修改类型和约束 + if rename != name { + sqls = append(sqls, this.GenerateSQL("ALTER TABLE ? RENAME COLUMN ? TO ?", + db.T(bp.GetTable()), db.C(name), db.C(rename))) + } + + // 修改类型 + sql := this.GenerateSQL("ALTER TABLE ? ALTER COLUMN ? TYPE ?", + db.T(bp.GetTable()), db.C(rename), db.L(this.GetColumnType(column))) + sqls = append(sqls, sql) + + // 修改 nullable + if column.IsNullable() { + sqls = append(sqls, this.GenerateSQL("ALTER TABLE ? ALTER COLUMN ? DROP NOT NULL", + db.T(bp.GetTable()), db.C(rename))) + } else { + sqls = append(sqls, this.GenerateSQL("ALTER TABLE ? ALTER COLUMN ? SET NOT NULL", + db.T(bp.GetTable()), db.C(rename))) + } + + // 修改默认值 + v := column.GetDefault() + if v != nil { + sqls = append(sqls, this.GenerateSQL("ALTER TABLE ? ALTER COLUMN ? SET DEFAULT ?", + db.T(bp.GetTable()), db.C(rename), v)) + } else if column.IsUseCurrent() { + sqls = append(sqls, this.GenerateSQL("ALTER TABLE ? ALTER COLUMN ? SET DEFAULT CURRENT_TIMESTAMP", + db.T(bp.GetTable()), db.C(rename))) + } + } + return sqls +} + +func (this DuckDB) CompileDropColumn(bp *schema.Blueprint) []string { + columns := []string{} + for _, item := range bp.GetCommands() { + if item.Type != "dropColumn" { + continue + } + for _, col := range item.Columns { + columns = append(columns, this.GenerateSQL("DROP COLUMN ?", db.C(col))) + } + } + sql := this.GenerateSQL("ALTER TABLE ?\n?", db.T(bp.GetTable()), db.L(strings.Join(columns, ",\n"))) + return []string{sql} +} + +func (this DuckDB) CompileDrop(bp *schema.Blueprint) []string { + return []string{this.GenerateSQL("DROP TABLE ?", db.T(bp.GetTable()))} +} + +func (this DuckDB) CompileDropIfExists(bp *schema.Blueprint) []string { + return []string{this.GenerateSQL("DROP TABLE IF EXISTS ?", db.T(bp.GetTable()))} +} + +func (this DuckDB) CompileRename(bp *schema.Blueprint) []string { + toName := "" + commands := bp.GetCommands() + if len(commands) == 0 { + panic("new table undefined") + } + toName = commands[0].To + if toName == "" { + panic("new table undefined") + } + return []string{this.GenerateSQL("ALTER TABLE ? RENAME TO ?", db.T(bp.GetTable()), db.T(toName))} +} + +func (this DuckDB) CompileModifyComment(bp *schema.Blueprint) []string { + return []string{ + this.GenerateSQL("COMMENT ON TABLE ? IS ?", db.T(bp.GetTable()), db.V(bp.Comment)), + } +} + +func (this DuckDB) addModifiers(sql string, bp *schema.Blueprint, column *schema.ColumnDefinition) string { + for _, modifier := range duckdbDefaultModifiers { + sql += this.GetColumnModifier(modifier, bp, column) + } + return sql +} + +func (this DuckDB) GetColumnType(column *schema.ColumnDefinition) string { + switch column.Type { + case "char": + return this.GenerateSQL("CHAR(?)", column.Length) + case "string": + return this.GenerateSQL("VARCHAR(?)", column.Length) + case "text": + return "TEXT" + case "integer": + return "INTEGER" + case "bigInteger": + return "BIGINT" + case "smallInteger": + return "SMALLINT" + case "tinyInteger": + return "TINYINT" + case "decimal": + return this.GenerateSQL("DECIMAL(?,?)", column.Total, column.Places) + case "boolean": + return "BOOLEAN" + case "enum": + // DuckDB 支持 ENUM 类型 + return this.GenerateSQL("VARCHAR(?)", column.Length) + case "json": + return "JSON" + case "binary": + return "BLOB" + case "datetime": + return "TIMESTAMP" + case "timestamp": + return "TIMESTAMP" + case "date": + return "DATE" + case "time": + return "TIME" + case "year": + return "SMALLINT" + case "uuid": + return "UUID" + } + panic("Unsupported data type: " + column.Type) +} + +func (this DuckDB) GetColumnModifier(modifier string, bp *schema.Blueprint, column *schema.ColumnDefinition) string { + switch modifier { + case "Nullable": + if column.IsNullable() { + return " NULL" + } + return " NOT NULL" + case "Default": + v := column.GetDefault() + if v == nil { + if column.IsUseCurrent() { + return " DEFAULT CURRENT_TIMESTAMP" + } + return "" + } + return this.GenerateSQL(" DEFAULT ?", v) + case "Increment": + if column.IsAutoIncrement() { + // DuckDB 使用 SERIAL 或者 SEQUENCE + return " PRIMARY KEY" + } + case "Comment": + // DuckDB 支持列注释,但需要在 CREATE TABLE 后使用 COMMENT ON + return "" + } + return "" +} + +func (this DuckDB) getAddedColumns(bp *schema.Blueprint) (columns []string) { + for _, column := range bp.GetAddedColumns() { + colType := this.GetColumnType(column) + // 对于自增列,使用 SERIAL 类型 + if column.IsAutoIncrement() { + switch column.Type { + case "bigInteger": + colType = "BIGSERIAL" + case "smallInteger": + colType = "SMALLSERIAL" + default: + colType = "SERIAL" + } + } + sql := this.GenerateSQL("? ?", db.C(column.Name), db.L(colType)) + columns = append(columns, this.addModifiers(sql, bp, column)) + } + return +} + +func (this DuckDB) getChangedColumns(bp *schema.Blueprint) (columns []string) { + for _, column := range bp.GetChangedColumns() { + name := column.Name + rename := column.Name + if column.GetRename() != "" { + rename = column.GetRename() + } + sql := this.GenerateSQL("? ?", db.C(rename), db.L(this.GetColumnType(column))) + columns = append(columns, this.addModifiers(sql, bp, column)) + if rename != name { + // 需要额外的重命名语句 + columns = append(columns, this.GenerateSQL("RENAME COLUMN ? TO ?", db.C(name), db.C(rename))) + } + } + return +} + +func (this DuckDB) GenerateSQL(sql string, args ...any) string { + sb := sb.NewSQLBuilder(false) + this.esg.Generate(sb, db.L(sql, args...)) + result, _, _ := sb.ToSQL() + return result +} + +func init() { + schema.RegisterDialect("duckdb", func(db *db.Database) schema.Schema { + return &DuckDB{ + db: db, + esg: sqlgen.NewExpressionSQLGenerator("duckdb", duckdb.DialectOptions()), + } + }) +} diff --git a/schema/dialect/duckdb/duckdb_test.go b/schema/dialect/duckdb/duckdb_test.go new file mode 100644 index 0000000..a9a948c --- /dev/null +++ b/schema/dialect/duckdb/duckdb_test.go @@ -0,0 +1,189 @@ +package duckdb_test + +import ( + "os" + "testing" + + "git.fsdpf.net/go/db/engine" + "git.fsdpf.net/go/db/schema" + "github.com/stretchr/testify/suite" + + _ "github.com/marcboeker/go-duckdb" +) + +type duckDBTest struct { + suite.Suite + schema schema.Schema +} + +var ( + tableName = "entry" + + dropTable = "DROP TABLE IF EXISTS entry;" + + createTable = "CREATE TABLE entry (" + + "id INTEGER PRIMARY KEY," + + "int INTEGER NOT NULL UNIQUE," + + "float REAL NOT NULL," + + "string VARCHAR(255) NOT NULL," + + "time TIMESTAMP NOT NULL," + + "bool BOOLEAN NOT NULL," + + "bytes BLOB NOT NULL" + + ");" +) + +func TestDuckDBSuite(t *testing.T) { + suite.Run(t, new(duckDBTest)) +} + +func (t *duckDBTest) SetupSuite() { + dbPath := os.Getenv("DUCKDB_PATH") + if dbPath == "" { + dbPath = "" + } + + db := engine.Open(map[string]engine.DBConfig{ + "test-duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile(dbPath), + ), + }).Connection("test-duckdb") + + t.schema = schema.GetSchemaDialect(db) + + db.Exec(dropTable) + db.Exec(createTable) +} + +func (t *duckDBTest) TestGetColumnListing() { + result, err := t.schema.GetColumnListing(tableName) + t.Require().NoError(err) + t.Require().ElementsMatch([]string{"id", "int", "float", "string", "time", "bool", "bytes"}, result) +} + +func (t *duckDBTest) TestTableExists() { + result, err := t.schema.TableExists(tableName) + t.Require().NoError(err) + t.Require().Equal(result, true) +} + +func (t *duckDBTest) TestCompileCreate() { + bp := schema.NewBlueprint("users") + bp.Create() + + bp.BigIncrements("id").AutoIncrement().Comment("ID") + bp.Boolean("enabled").Default("1").Comment("是否有效") + bp.Char("created_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("创建者") + bp.Char("owned_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("拥有者") + bp.Timestamp("created_at").UseCurrent().Comment("创建时间") + bp.Timestamp("updated_at").UseCurrent().Comment("更新时间") + bp.DateTime("deleted_at").Nullable().Comment("删除时间") + + sql := t.schema.CompileCreate(bp) + + t.Equal([]string{ + "CREATE TABLE \"users\" (\n" + + "\"id\" BIGSERIAL NOT NULL PRIMARY KEY,\n" + + "\"enabled\" BOOLEAN NOT NULL DEFAULT '1',\n" + + "\"created_user\" CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',\n" + + "\"owned_user\" CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',\n" + + "\"created_at\" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + "\"updated_at\" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + + "\"deleted_at\" TIMESTAMP NULL\n" + + ")", + }, sql) + + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileAdd() { + bp := schema.NewBlueprint("users") + + bp.String("name", 50).Comment("用户名") + bp.SmallInteger("age").Default("18").Comment("年龄") + bp.String("sex", 1).Default("0").Comment("性别") + + sql := t.schema.CompileAdd(bp) + + t.Equal([]string{ + "ALTER TABLE \"users\"\n" + + "ADD COLUMN \"name\" VARCHAR(50) NOT NULL,\n" + + "ADD COLUMN \"age\" SMALLINT NOT NULL DEFAULT '18',\n" + + "ADD COLUMN \"sex\" VARCHAR(1) NOT NULL DEFAULT '0'", + }, sql) + + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileChange() { + bp := schema.NewBlueprint("users") + + bp.String("name", 50).Change("username") + bp.SmallInteger("age").Default("19").Comment("年龄").Change() + + sql := t.schema.CompileChange(bp) + + t.Require().NotEmpty(sql) + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileDropColumn() { + bp := schema.NewBlueprint("users") + + bp.DropColumn("age", "sex") + + sql := t.schema.CompileDropColumn(bp) + + t.Equal([]string{ + "ALTER TABLE \"users\"\n" + + "DROP COLUMN \"age\",\n" + + "DROP COLUMN \"sex\"", + }, sql) + + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileDrop() { + bp := schema.NewBlueprint("users") + + bp.Drop() + + sql := t.schema.CompileDrop(bp) + + t.Equal([]string{"DROP TABLE \"users\""}, sql) + + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileDropIfExists() { + bp := schema.NewBlueprint("users") + + bp.Drop() + + sql := t.schema.CompileDropIfExists(bp) + t.Equal([]string{"DROP TABLE IF EXISTS \"users\""}, sql) + + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileRename() { + bp := schema.NewBlueprint("users") + + bp.Rename("user") + + sql := t.schema.CompileRename(bp) + + t.Equal([]string{"ALTER TABLE \"users\" RENAME TO \"user\""}, sql) + + t.T().Log(sql) +} + +func (t *duckDBTest) TestCompileModifyComment() { + bp := schema.NewBlueprint("users") + bp.Comment = "用户表" + + sql := t.schema.CompileModifyComment(bp) + + t.Equal([]string{"COMMENT ON TABLE \"users\" IS '用户表'"}, sql) + + t.T().Log(sql) +} diff --git a/schema/dialect/duckdb/example_test.go b/schema/dialect/duckdb/example_test.go new file mode 100644 index 0000000..e3dda43 --- /dev/null +++ b/schema/dialect/duckdb/example_test.go @@ -0,0 +1,146 @@ +package duckdb_test + +import ( + "fmt" + + "git.fsdpf.net/go/db/engine" + "git.fsdpf.net/go/db/schema" + + _ "git.fsdpf.net/go/db/dialect/duckdb" + _ "git.fsdpf.net/go/db/schema/dialect/duckdb" + _ "github.com/marcboeker/go-duckdb" +) + +func Example_createTable() { + // 创建内存数据库连接 + db := engine.Open(map[string]engine.DBConfig{ + "duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile(""), // 空字符串表示内存数据库 + ), + }).Connection("duckdb") + + // 获取 schema 方言实现 + s := schema.GetSchemaDialect(db) + + // 创建表结构定义 + bp := schema.NewBlueprint("users") + bp.Create() + + bp.BigIncrements("id").AutoIncrement().Comment("用户ID") + bp.String("name", 50).Comment("用户名") + bp.String("email", 100).Comment("邮箱") + bp.Integer("age").Nullable().Comment("年龄") + bp.Boolean("active").Default("1").Comment("是否激活") + bp.Timestamp("created_at").UseCurrent().Comment("创建时间") + bp.Timestamp("updated_at").UseCurrent().Comment("更新时间") + + // 编译生成 CREATE TABLE SQL + sqls := s.CompileCreate(bp) + for _, sql := range sqls { + fmt.Println(sql) + } + + // Output: + // CREATE TABLE "users" ( + // "id" BIGSERIAL NOT NULL PRIMARY KEY, + // "name" VARCHAR(50) NOT NULL, + // "email" VARCHAR(100) NOT NULL, + // "age" INTEGER NULL, + // "active" BOOLEAN NOT NULL DEFAULT '1', + // "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + // "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + // ) +} + +func Example_addColumn() { + db := engine.Open(map[string]engine.DBConfig{ + "duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile(""), + ), + }).Connection("duckdb") + + s := schema.GetSchemaDialect(db) + + // 添加新列 + bp := schema.NewBlueprint("users") + bp.String("phone", 20).Nullable().Comment("电话号码") + bp.Text("address").Nullable().Comment("地址") + + sqls := s.CompileAdd(bp) + for _, sql := range sqls { + fmt.Println(sql) + } + + // Output: + // ALTER TABLE "users" + // ADD COLUMN "phone" VARCHAR(20) NULL, + // ADD COLUMN "address" TEXT NULL +} + +func Example_dropColumn() { + db := engine.Open(map[string]engine.DBConfig{ + "duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile(""), + ), + }).Connection("duckdb") + + s := schema.GetSchemaDialect(db) + + // 删除列 + bp := schema.NewBlueprint("users") + bp.DropColumn("phone", "address") + + sqls := s.CompileDropColumn(bp) + for _, sql := range sqls { + fmt.Println(sql) + } + + // Output: + // ALTER TABLE "users" + // DROP COLUMN "phone", + // DROP COLUMN "address" +} + +func Example_renameTable() { + db := engine.Open(map[string]engine.DBConfig{ + "duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile(""), + ), + }).Connection("duckdb") + + s := schema.GetSchemaDialect(db) + + // 重命名表 + bp := schema.NewBlueprint("users") + bp.Rename("customers") + + sqls := s.CompileRename(bp) + for _, sql := range sqls { + fmt.Println(sql) + } + + // Output: + // ALTER TABLE "users" RENAME TO "customers" +} + +func Example_modifyComment() { + db := engine.Open(map[string]engine.DBConfig{ + "duckdb": engine.NewDBConfig("duckdb", + engine.WithDuckDBFile(""), + ), + }).Connection("duckdb") + + s := schema.GetSchemaDialect(db) + + // 修改表注释 + bp := schema.NewBlueprint("users") + bp.Comment = "用户信息表" + + sqls := s.CompileModifyComment(bp) + for _, sql := range sqls { + fmt.Println(sql) + } + + // Output: + // COMMENT ON TABLE "users" IS '用户信息表' +} diff --git a/schema/dialect/sqlite3/sqlite3.go b/schema/dialect/sqlite3/sqlite3.go index d70493a..0900d70 100644 --- a/schema/dialect/sqlite3/sqlite3.go +++ b/schema/dialect/sqlite3/sqlite3.go @@ -73,10 +73,10 @@ func (this Sqlite3) CompileCreate(bp *schema.Blueprint) (sqls []string) { columns = append(columns, sql) } - prefix := "? TABLE ? (\n?\n)" + prefix := "? TABLE IF NOT EXISTS ? (\n?\n)" if comment := bp.Comment; comment != "" { - prefix = "? TABLE ? ( -- " + comment + "\n?\n)" + prefix = "? TABLE IF NOT EXISTS ? ( -- " + comment + "\n?\n)" } sqls = append(sqls, diff --git a/schema/dialect/sqlite3/sqlite3_test.go b/schema/dialect/sqlite3/sqlite3_test.go index a33739b..4511e93 100644 --- a/schema/dialect/sqlite3/sqlite3_test.go +++ b/schema/dialect/sqlite3/sqlite3_test.go @@ -81,7 +81,7 @@ func (t *sqlite3Test) TestCompileCreate() { sql := t.schema.CompileCreate(bp) t.Equal([]string{ - "CREATE TABLE `users` (\n" + + "CREATE TABLE IF NOT EXISTS `users` (\n" + "`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- ID\n" + "`enabled` tinyint(1) NOT NULL DEFAULT '1', -- 是否有效\n" + "`created_user` char(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', -- 创建者\n" + diff --git a/schema/dialect/sqlserver/sqlserver_test.go b/schema/dialect/sqlserver/sqlserver_test.go index 283d392..3c0e041 100644 --- a/schema/dialect/sqlserver/sqlserver_test.go +++ b/schema/dialect/sqlserver/sqlserver_test.go @@ -8,7 +8,7 @@ import ( "git.fsdpf.net/go/db/schema" "github.com/stretchr/testify/suite" - _ "github.com/denisenkom/go-mssqldb" + _ "github.com/microsoft/go-mssqldb" ) type sqlServerTest struct {