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"
)