- 新增 dialect/duckdb 方言,支持 DuckDB SQL 语法 - 新增 schema/dialect/duckdb DDL 操作支持 - dialect/sqlite3: 注册 sqlite3_with_if 驱动,连接时自动注册 IF() 函数 - engine: MakeConnection 对 sqlite3 自动使用带 IF 支持的驱动 - engine: 新增 DBConfig DuckDB 配置项及相关 Option 函数 - 统一各方言测试引用路径
483 lines
12 KiB
Go
483 lines
12 KiB
Go
package engine_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"git.fsdpf.net/go/db/engine"
|
|
)
|
|
|
|
// TestMySQLConfig 测试 MySQL 驱动的配置
|
|
func TestMySQLConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options []engine.Option
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Valid Config",
|
|
options: []engine.Option{
|
|
engine.WithHost("localhost"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("root"),
|
|
engine.WithPassword("password"),
|
|
engine.WithMySQLCollation("utf8mb4_unicode_ci"),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Missing Host",
|
|
options: []engine.Option{
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("root"),
|
|
engine.WithPassword("password"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "Host, ReadHosts, or WriteHosts is required for mysql driver",
|
|
},
|
|
{
|
|
name: "Missing Database",
|
|
options: []engine.Option{
|
|
engine.WithHost("localhost"),
|
|
engine.WithUsername("root"),
|
|
engine.WithPassword("password"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "Database is required for mysql driver",
|
|
},
|
|
{
|
|
name: "engine.engine.With SQL Server engine.Option",
|
|
options: []engine.Option{
|
|
engine.WithHost("localhost"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("root"),
|
|
engine.WithPassword("password"),
|
|
engine.WithSQLServerInstance("SQLEXPRESS"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "WithSQLServerInstance is only valid for sqlserver 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("mysql", tt.options...)
|
|
if !tt.wantErr && (config.Driver != "mysql" || config.MySQL.Collation != "utf8mb4_unicode_ci") {
|
|
t.Errorf("MySQL config not set correctly: %+v", config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPostgreSQLConfig 测试 PostgreSQL 驱动的配置
|
|
func TestPostgreSQLConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options []engine.Option
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Valid Config",
|
|
options: []engine.Option{
|
|
engine.WithHost("localhost"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("postgres"),
|
|
engine.WithPassword("password"),
|
|
engine.WithPgSslmode("disable"),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Missing Username",
|
|
options: []engine.Option{
|
|
engine.WithHost("localhost"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithPassword("password"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "Username is required for pgsql driver",
|
|
},
|
|
{
|
|
name: "engine.engine.With MySQL engine.Option",
|
|
options: []engine.Option{
|
|
engine.WithHost("localhost"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("postgres"),
|
|
engine.WithPassword("password"),
|
|
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("pgsql", tt.options...)
|
|
if !tt.wantErr && (config.Driver != "pgsql" || config.PostgreSQL.Sslmode != "disable") {
|
|
t.Errorf("PostgreSQL config not set correctly: %+v", config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSQLiteConfig 测试 SQLite 驱动的配置
|
|
func TestSQLiteConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options []engine.Option
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Valid Config",
|
|
options: []engine.Option{
|
|
engine.WithSQLiteFile("test.db"),
|
|
engine.WithSQLiteJournal("WAL"),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Missing File",
|
|
options: []engine.Option{},
|
|
wantErr: true,
|
|
errMsg: "File is required for sqlite3 driver",
|
|
},
|
|
{
|
|
name: "engine.engine.With PostgreSQL engine.Option",
|
|
options: []engine.Option{
|
|
engine.WithSQLiteFile("test.db"),
|
|
engine.WithPgSslmode("disable"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "WithPgSslmode is only valid for pgsql 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("sqlite3", tt.options...)
|
|
if !tt.wantErr && (config.Driver != "sqlite3" || config.SQLite.File != "test.db") {
|
|
t.Errorf("SQLite config not set correctly: %+v", config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSQLServerConfig 测试 SQL Server 驱动的配置
|
|
func TestSQLServerConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options []engine.Option
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Valid Config",
|
|
options: []engine.Option{
|
|
engine.WithHost("sqlserver.example.com"),
|
|
engine.WithDatabase("master"),
|
|
engine.WithUsername("sa"),
|
|
engine.WithPassword("password"),
|
|
engine.WithSQLServerInstance("SQLEXPRESS"),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Missing Password",
|
|
options: []engine.Option{
|
|
engine.WithHost("sqlserver.example.com"),
|
|
engine.WithDatabase("master"),
|
|
engine.WithUsername("sa"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "Password is required for sqlserver driver",
|
|
},
|
|
{
|
|
name: "engine.engine.With SQLite engine.Option",
|
|
options: []engine.Option{
|
|
engine.WithHost("sqlserver.example.com"),
|
|
engine.WithDatabase("master"),
|
|
engine.WithUsername("sa"),
|
|
engine.WithPassword("password"),
|
|
engine.WithSQLiteFile("test.db"),
|
|
},
|
|
wantErr: true,
|
|
errMsg: "WithSQLiteFile is only valid for sqlite3 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("sqlserver", tt.options...)
|
|
if !tt.wantErr && (config.Driver != "sqlserver" || config.SQLServer.Instance != "SQLEXPRESS") {
|
|
t.Errorf("SQL Server config not set correctly: %+v", config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInvalidDriver 测试不支持的驱动
|
|
func TestInvalidDriver(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if r != "Unsupported driver: oracle" {
|
|
t.Errorf("engine.NewDBConfig() panic = %v, want 'Unsupported driver: oracle'", r)
|
|
}
|
|
} else {
|
|
t.Errorf("engine.NewDBConfig() did not panic for invalid driver")
|
|
}
|
|
}()
|
|
|
|
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",
|
|
engine.WithHost("localhost"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("root"),
|
|
engine.WithPassword("password"),
|
|
)
|
|
|
|
if config.Charset != "utf8mb4" {
|
|
t.Errorf("Default Charset = %v, want utf8mb4", config.Charset)
|
|
}
|
|
if config.ConnMaxLifetime != 2*time.Hour {
|
|
t.Errorf("Default ConnMaxLifetime = %v, want 2h", config.ConnMaxLifetime)
|
|
}
|
|
if config.MaxOpenConns != 100 {
|
|
t.Errorf("Default MaxOpenConns = %v, want 100", config.MaxOpenConns)
|
|
}
|
|
}
|
|
|
|
func TestToDSN(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config engine.DBConfig
|
|
wantDSN string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "MySQL DSN",
|
|
config: engine.NewDBConfig("mysql",
|
|
engine.WithHost("localhost"),
|
|
engine.WithPort("3306"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("root"),
|
|
engine.WithPassword("password"),
|
|
engine.WithMySQLCollation("utf8mb4_unicode_ci"),
|
|
),
|
|
wantDSN: "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&collation=utf8mb4_unicode_ci",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "PostgreSQL DSN",
|
|
config: engine.NewDBConfig("pgsql",
|
|
engine.WithHost("localhost"),
|
|
engine.WithPort("5432"),
|
|
engine.WithDatabase("testdb"),
|
|
engine.WithUsername("postgres"),
|
|
engine.WithPassword("password"),
|
|
engine.WithPgSslmode("disable"),
|
|
),
|
|
wantDSN: "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "SQLite DSN",
|
|
config: engine.NewDBConfig("sqlite3",
|
|
engine.WithSQLiteFile("test.db"),
|
|
engine.WithSQLiteJournal("WAL"),
|
|
),
|
|
wantDSN: "test.db?_journal=WAL",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "SQL Server DSN",
|
|
config: engine.NewDBConfig("sqlserver",
|
|
engine.WithHost("sqlserver.example.com"),
|
|
engine.WithPort("1433"),
|
|
engine.WithDatabase("master"),
|
|
engine.WithUsername("sa"),
|
|
engine.WithPassword("password"),
|
|
engine.WithSQLServerInstance("SQLEXPRESS"),
|
|
),
|
|
wantDSN: "sqlserver://sa:password@sqlserver.example.com\\SQLEXPRESS:1433?database=master&server=sqlserver.example.com%5CSQLEXPRESS",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if !tt.wantErr {
|
|
t.Errorf("ToDSN() panicked unexpectedly: %v", r)
|
|
} else if tt.errMsg != "" && r != tt.errMsg {
|
|
t.Errorf("ToDSN() panic = %v, want %v", r, tt.errMsg)
|
|
}
|
|
} else if tt.wantErr {
|
|
t.Errorf("ToDSN() did not panic, expected panic with: %v", tt.errMsg)
|
|
}
|
|
}()
|
|
|
|
dsn := tt.config.ToDSN()
|
|
if !tt.wantErr && dsn != tt.wantDSN {
|
|
t.Errorf("ToDSN() = %v, want %v", dsn, tt.wantDSN)
|
|
}
|
|
})
|
|
}
|
|
}
|