feat: 新增 DuckDB 方言支持及 SQLite3 自动注册 IF 函数

- 新增 dialect/duckdb 方言,支持 DuckDB SQL 语法
- 新增 schema/dialect/duckdb DDL 操作支持
- dialect/sqlite3: 注册 sqlite3_with_if 驱动,连接时自动注册 IF() 函数
- engine: MakeConnection 对 sqlite3 自动使用带 IF 支持的驱动
- engine: 新增 DBConfig DuckDB 配置项及相关 Option 函数
- 统一各方言测试引用路径
This commit is contained in:
2026-04-16 10:14:46 +08:00
parent c4590cf592
commit 2221ca0ddb
19 changed files with 1270 additions and 62 deletions
+38
View File
@@ -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())
}
+135
View File
@@ -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)`)
}
+2
View File
@@ -1,6 +1,8 @@
package mysql
import (
_ "github.com/go-sql-driver/mysql"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exp"
)
+5 -1
View File
@@ -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()
+16
View File
@@ -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())
}
+2
View File
@@ -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"
+1 -1
View File
@@ -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"
)
+6 -7
View File
@@ -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)
+76
View File
@@ -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
}
}
+104 -8
View File
@@ -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,
},
}
+23 -4
View File
@@ -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
)
+83 -37
View File
@@ -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=
+169
View File
@@ -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)
+271
View File
@@ -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()),
}
})
}
+189
View File
@@ -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)
}
+146
View File
@@ -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 '用户信息表'
}
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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" +
+1 -1
View File
@@ -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 {