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:
+6
-7
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user