Files
oracle/oracle_test.go
charlie 9154afab6b feat: Oracle 驱动完整化——回调体系、版本感知、驱动抽象层与测试套件
- 新增驱动抽象层 driver_adapter(go-ora/godror 双驱动切换)
- 新增 Update/Delete/Query 回调,完善 RETURNING INTO 子句
- 修复 create.go 事务悬挂行 BUG、批量 RowsAffected、主键 WHERE 注入
- 版本感知体系:分页/自增/序列默认值/BOOLEAN/32k VARCHAR2 分级能力判定
- 11g 序列+触发器自增、ON UPDATE 触发器、默认值智能转换
- 升级 go-ora v2.8.19 → v2.9.0
- 单元测试+模块测试+集成测试共 96 个,真实 Oracle 11g 全绿
- 测试 DSN 密码移除,改为 ORACLE_DSN 环境变量注入
2026-08-08 10:36:57 +08:00

750 lines
21 KiB
Go

package oracle
import (
"reflect"
"strings"
"testing"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
)
// ---- 测试辅助 ----
// limitModel 带主键的模型,用于 RewriteLimit/RewriteLimit11 测试
type limitModel struct {
ID uint `gorm:"primaryKey"`
Name string
}
func newTestDialector(dbVer string, defaultStringSize uint) *Dialector {
return &Dialector{Config: &Config{DBVer: dbVer, DefaultStringSize: defaultStringSize}}
}
func newTestStatement(d *Dialector) *gorm.Statement {
db := &gorm.DB{Config: &gorm.Config{Dialector: d}}
return &gorm.Statement{DB: db}
}
// testField 构造一个最小可用的 schema.Field
func testField(dataType schema.DataType) *schema.Field {
return &schema.Field{
DataType: dataType,
FieldType: reflect.TypeOf(""),
TagSettings: map[string]string{},
}
}
// limitClause 构造 LIMIT 子句
func limitClause(offset, limit int) clause.Clause {
expr := clause.Limit{Offset: offset}
if limit > 0 {
v := limit
expr.Limit = &v
}
return clause.Clause{Name: "LIMIT", Expression: expr}
}
// ---- TestDataTypeOf ----
func TestDataTypeOf(t *testing.T) {
d12 := newTestDialector("12.1.0.2.0", 1024)
tests := []struct {
name string
dialector *Dialector
mutate func(*schema.Field)
want string
}{
{
name: "bool maps to NUMBER(1)",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Bool },
want: "NUMBER(1)",
},
{
name: "int size 0 maps to SMALLINT (size <= 8 rule)",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Int },
want: "SMALLINT",
},
{
name: "int size 8 maps to SMALLINT",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Int; f.Size = 8 },
want: "SMALLINT",
},
{
name: "int size 64 maps to INTEGER",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Int; f.Size = 64 },
want: "INTEGER",
},
{
name: "uint size 0 maps to SMALLINT (size <= 8 rule)",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Uint },
want: "SMALLINT",
},
{
name: "uint size 8 maps to SMALLINT",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Uint; f.Size = 8 },
want: "SMALLINT",
},
{
name: "uint size 64 maps to INTEGER",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Uint; f.Size = 64 },
want: "INTEGER",
},
{
name: "int autoincrement on 12c maps to identity",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Int; f.AutoIncrement = true; f.Size = 64 },
want: "INTEGER GENERATED BY DEFAULT AS IDENTITY",
},
{
name: "int autoincrement on 11g stays INTEGER",
dialector: newTestDialector("11.2.0.4.0", 1024),
mutate: func(f *schema.Field) { f.DataType = schema.Int; f.AutoIncrement = true; f.Size = 64 },
want: "INTEGER",
},
{
name: "float maps to FLOAT",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Float },
want: "FLOAT",
},
{
name: "float with AUTOINCREMENT tag maps to identity",
dialector: d12,
mutate: func(f *schema.Field) {
f.DataType = schema.Float
f.TagSettings["AUTOINCREMENT"] = "true"
},
want: "FLOAT GENERATED BY DEFAULT AS IDENTITY",
},
{
name: "string size 100 maps to VARCHAR2(100)",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.String; f.Size = 100 },
want: "VARCHAR2(100)",
},
{
name: "string without size uses default string size",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.String },
want: "VARCHAR2(1024)",
},
{
name: "string size 2000 maps to CLOB",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.String; f.Size = 2000 },
want: "CLOB",
},
{
name: "string size 4096 on 12c maps to VARCHAR2(4096) via 32k support",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.String; f.Size = 4096 },
want: "VARCHAR2(4096)",
},
{
name: "string size 4096 on 11g maps to CLOB",
dialector: newTestDialector("11.2.0.4.0", 1024),
mutate: func(f *schema.Field) { f.DataType = schema.String; f.Size = 4096 },
want: "CLOB",
},
{
name: "string size 5000 on 12c maps to VARCHAR2(5000) via 32k support",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.String; f.Size = 5000 },
want: "VARCHAR2(5000)",
},
{
name: "string size 5000 on 11g maps to CLOB",
dialector: newTestDialector("11.2.0.4.0", 1024),
mutate: func(f *schema.Field) { f.DataType = schema.String; f.Size = 5000 },
want: "CLOB",
},
{
name: "bool on 21c maps to native BOOLEAN",
dialector: newTestDialector("21.0.0.0.0", 1024),
mutate: func(f *schema.Field) { f.DataType = schema.Bool },
want: "BOOLEAN",
},
{
name: "bool on 23ai maps to native BOOLEAN",
dialector: newTestDialector("23.0.0.0.0", 1024),
mutate: func(f *schema.Field) { f.DataType = schema.Bool },
want: "BOOLEAN",
},
{
name: "bool on 11g maps to NUMBER(1)",
dialector: newTestDialector("11.2.0.4.0", 1024),
mutate: func(f *schema.Field) { f.DataType = schema.Bool },
want: "NUMBER(1)",
},
{
name: "primary key without size and without default size maps to VARCHAR2(191)",
dialector: newTestDialector("12.1.0.2.0", 0),
mutate: func(f *schema.Field) { f.DataType = schema.String; f.PrimaryKey = true },
want: "VARCHAR2(191)",
},
{
name: "unique field without size and without default size maps to VARCHAR2(191)",
dialector: newTestDialector("12.1.0.2.0", 0),
mutate: func(f *schema.Field) {
f.DataType = schema.String
f.TagSettings["UNIQUE"] = "unique"
},
want: "VARCHAR2(191)",
},
{
name: "time maps to TIMESTAMP WITH TIME ZONE",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Time },
want: "TIMESTAMP WITH TIME ZONE",
},
{
name: "time with precision 6",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Time; f.Precision = 6 },
want: "TIMESTAMP(6) WITH TIME ZONE",
},
{
name: "bytes maps to BLOB",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.Bytes },
want: "BLOB",
},
{
name: "text data type maps to CLOB",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.DataType("text") },
want: "CLOB",
},
{
name: "VARCHAR2 data type with size",
dialector: d12,
mutate: func(f *schema.Field) { f.DataType = schema.DataType("VARCHAR2"); f.Size = 50 },
want: "VARCHAR2(50)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := testField("")
tt.mutate(f)
got := tt.dialector.DataTypeOf(f)
if got != tt.want {
t.Errorf("DataTypeOf() = %q, want %q", got, tt.want)
}
})
}
}
func TestDataTypeOfRemovesRestrictTag(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
f := testField(schema.Int)
f.TagSettings["RESTRICT"] = "true"
d.DataTypeOf(f)
if _, ok := f.TagSettings["RESTRICT"]; ok {
t.Error("expected RESTRICT to be removed from TagSettings")
}
}
func TestDataTypeOfPanicsOnEmptyType(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
f := testField("")
defer func() {
if r := recover(); r == nil {
t.Error("expected panic for empty DataType")
}
}()
d.DataTypeOf(f)
}
// ---- TestBindVarTo ----
func TestBindVarTo(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
stmt := newTestStatement(d)
var buf strings.Builder
// GORM 的 AddVar 会先 append 再调用 BindVarTo,因此绑定位置从 1 开始
stmt.Vars = append(stmt.Vars, "a")
d.BindVarTo(&buf, stmt, "a")
stmt.Vars = append(stmt.Vars, "b")
d.BindVarTo(&buf, stmt, "b")
stmt.Vars = append(stmt.Vars, "c")
d.BindVarTo(&buf, stmt, "c")
if want := ":1:2:3"; buf.String() != want {
t.Errorf("BindVarTo output = %q, want %q", buf.String(), want)
}
}
func TestBindVarToEmptyVars(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
stmt := newTestStatement(d)
var buf strings.Builder
d.BindVarTo(&buf, stmt, "a")
if want := ":0"; buf.String() != want {
t.Errorf("BindVarTo output = %q, want %q", buf.String(), want)
}
}
// ---- TestQuoteTo ----
func TestQuoteTo(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
tests := []struct {
name string
value string
want string
}{
{"plain identifier", "USER_NAME", "USER_NAME"},
{"lowercase identifier", "user_name", "user_name"},
{"empty string", "", ""},
// 注意:SELECT 不在 reserved.go 的 ReservedWordsList 中,不会被加引号
{"SELECT is not in reserved list", "SELECT", "SELECT"},
{"reserved word FROM", "FROM", `"FROM"`},
{"reserved word WHERE", "WHERE", `"WHERE"`},
{"reserved word ORDER", "ORDER", `"ORDER"`},
{"reserved word SET", "SET", `"SET"`},
{"reserved word VALUES", "VALUES", `"VALUES"`},
{"reserved word UPDATE", "UPDATE", `"UPDATE"`},
{"reserved word CLOB", "CLOB", `"CLOB"`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf strings.Builder
d.QuoteTo(&buf, tt.value)
if got := buf.String(); got != tt.want {
t.Errorf("QuoteTo(%q) = %q, want %q", tt.value, got, tt.want)
}
})
}
}
func TestQuoteToSkipQuoteIdentifiers(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
d.SkipQuoteIdentifiers = true
var buf strings.Builder
d.QuoteTo(&buf, "SELECT")
d.QuoteTo(&buf, "FROM")
if want := "SELECTFROM"; buf.String() != want {
t.Errorf("QuoteTo with SkipQuoteIdentifiers = %q, want %q", buf.String(), want)
}
}
// ---- TestRewriteLimit ----
func TestRewriteLimit(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
t.Run("adds order by primary key when no order by", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.Clauses = map[string]clause.Clause{}
stmt.Schema = parseTestSchema(t, &limitModel{})
d.RewriteLimit(limitClause(10, 5), stmt)
got := stmt.SQL.String()
for _, want := range []string{"ORDER BY id", "OFFSET 10 ROWS", "FETCH NEXT 5 ROWS ONLY"} {
if !strings.Contains(got, want) {
t.Errorf("RewriteLimit output %q missing %q", got, want)
}
}
})
t.Run("keeps existing order by", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.Clauses = map[string]clause.Clause{
"ORDER BY": {Name: "ORDER BY", Expression: clause.OrderBy{
Columns: []clause.OrderByColumn{{Column: clause.Column{Name: "NAME"}, Desc: true}},
}},
}
d.RewriteLimit(limitClause(10, 5), stmt)
got := stmt.SQL.String()
if strings.HasPrefix(got, "ORDER BY") {
t.Errorf("RewriteLimit should not add ORDER BY when already present, got %q", got)
}
if !strings.Contains(got, "OFFSET 10 ROWS") || !strings.Contains(got, "FETCH NEXT 5 ROWS ONLY") {
t.Errorf("RewriteLimit output %q missing offset/fetch", got)
}
})
t.Run("uses DUAL subquery when schema is nil", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.Clauses = map[string]clause.Clause{}
d.RewriteLimit(limitClause(0, 3), stmt)
got := stmt.SQL.String()
if !strings.Contains(got, "ORDER BY (SELECT NULL FROM DUAL)") {
t.Errorf("RewriteLimit output %q missing DUAL subquery", got)
}
})
t.Run("offset only", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.Clauses = map[string]clause.Clause{}
d.RewriteLimit(limitClause(5, 0), stmt)
got := stmt.SQL.String()
if !strings.Contains(got, "OFFSET 5 ROWS") {
t.Errorf("RewriteLimit output %q missing offset", got)
}
if strings.Contains(got, "FETCH NEXT") {
t.Errorf("RewriteLimit output %q should not contain FETCH NEXT", got)
}
})
}
func TestRewriteLimitIgnoresNonLimitExpression(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
stmt := newTestStatement(d)
stmt.Clauses = map[string]clause.Clause{}
d.RewriteLimit(clause.Clause{Name: "LIMIT", Expression: clause.Expr{SQL: "1"}}, stmt)
if got := stmt.SQL.String(); got != "" {
t.Errorf("RewriteLimit should write nothing for non-Limit expression, got %q", got)
}
}
// ---- TestRewriteLimit11 ----
func TestRewriteLimit11(t *testing.T) {
d := newTestDialector("11.2.0.4.0", 1024)
t.Run("limit and offset", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.SQL.WriteString("SELECT * FROM TEST_USERS")
d.RewriteLimit11(limitClause(10, 5), stmt)
got := stmt.SQL.String()
for _, want := range []string{
"ROW_NUMBER() OVER (ORDER BY NULL) AS ROW_NUM",
"FROM (SELECT * FROM TEST_USERS) T",
"ROW_NUM BETWEEN 11 AND 15",
} {
if !strings.Contains(got, want) {
t.Errorf("RewriteLimit11 output %q missing %q", got, want)
}
}
})
t.Run("limit only uses ROWNUM", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.SQL.WriteString("SELECT * FROM TEST_USERS")
d.RewriteLimit11(limitClause(0, 5), stmt)
got := stmt.SQL.String()
if want := "SELECT * FROM (SELECT * FROM TEST_USERS) WHERE ROWNUM <= 5"; got != want {
t.Errorf("RewriteLimit11 output = %q, want %q", got, want)
}
})
t.Run("offset only", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.SQL.WriteString("SELECT * FROM TEST_USERS")
d.RewriteLimit11(limitClause(10, 0), stmt)
got := stmt.SQL.String()
if !strings.Contains(got, "ROW_NUM > 11") {
t.Errorf("RewriteLimit11 output %q missing ROW_NUM > 11", got)
}
})
t.Run("respects existing order by columns", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.SQL.WriteString("SELECT * FROM TEST_USERS")
stmt.Clauses = map[string]clause.Clause{
"ORDER BY": {Name: "ORDER BY", Expression: clause.OrderBy{
Columns: []clause.OrderByColumn{{Column: clause.Column{Name: "NAME"}, Desc: true}},
}},
}
d.RewriteLimit11(limitClause(10, 5), stmt)
got := stmt.SQL.String()
if !strings.Contains(got, "ORDER BY NAME DESC") {
t.Errorf("RewriteLimit11 output %q missing ORDER BY NAME DESC", got)
}
})
t.Run("no-op when no limit and no offset", func(t *testing.T) {
stmt := newTestStatement(d)
stmt.SQL.WriteString("SELECT * FROM TEST_USERS")
d.RewriteLimit11(limitClause(0, 0), stmt)
if want := "SELECT * FROM TEST_USERS"; stmt.SQL.String() != want {
t.Errorf("RewriteLimit11 output = %q, want %q", stmt.SQL.String(), want)
}
})
}
// ---- TestClauseBuilders ----
func TestClauseBuilders(t *testing.T) {
t.Run("Oracle 11g uses RewriteLimit11", func(t *testing.T) {
d := newTestDialector("11.2.0.4.0", 1024)
builders := d.ClauseBuilders()
builder, ok := builders["LIMIT"]
if !ok {
t.Fatal("expected LIMIT clause builder to be registered")
}
stmt := newTestStatement(d)
stmt.SQL.WriteString("SELECT * FROM TEST_USERS")
builder(limitClause(0, 5), stmt)
if got := stmt.SQL.String(); !strings.Contains(got, "ROWNUM") {
t.Errorf("expected ROWNUM-based rewrite for 11g, got %q", got)
}
})
t.Run("Oracle 12c uses RewriteLimit", func(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
builders := d.ClauseBuilders()
builder, ok := builders["LIMIT"]
if !ok {
t.Fatal("expected LIMIT clause builder to be registered")
}
stmt := newTestStatement(d)
builder(limitClause(0, 5), stmt)
if got := stmt.SQL.String(); !strings.Contains(got, "FETCH NEXT 5 ROWS ONLY") {
t.Errorf("expected FETCH-based rewrite for 12c, got %q", got)
}
})
t.Run("Oracle 19c uses RewriteLimit", func(t *testing.T) {
d := newTestDialector("19.0.0.0", 1024)
builders := d.ClauseBuilders()
stmt := newTestStatement(d)
builders["LIMIT"](limitClause(0, 5), stmt)
if got := stmt.SQL.String(); !strings.Contains(got, "FETCH NEXT 5 ROWS ONLY") {
t.Errorf("expected FETCH-based rewrite for 19c, got %q", got)
}
})
}
// ---- TestVersionCapabilities ----
func TestVersionCapabilities(t *testing.T) {
tests := []struct {
name string
dbVer string
wantMajor int
wantIdentity bool
wantFetchOffset bool
wantNativeBoolean bool
wantExtendedString bool
wantVector bool
wantIsOracle11g bool
}{
{"11g", "11.2.0.4.0", 11, false, false, false, false, false, true},
{"10g", "10.2.0.1.0", 10, false, false, false, false, false, true},
{"12c", "12.1.0.2.0", 12, true, true, false, true, false, false},
{"18c", "18.0.0.0.0", 18, true, true, false, true, false, false},
{"19c", "19.0.0.0.0", 19, true, true, false, true, false, false},
{"21c", "21.0.0.0.0", 21, true, true, true, true, false, false},
{"23ai", "23.0.0.0.0", 23, true, true, true, true, true, false},
{"empty", "", 0, false, false, false, false, false, true},
{"invalid", "invalid", 0, false, false, false, false, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := oracleMajor(tt.dbVer); got != tt.wantMajor {
t.Errorf("oracleMajor(%q) = %d, want %d", tt.dbVer, got, tt.wantMajor)
}
if got := supportsIdentity(tt.dbVer); got != tt.wantIdentity {
t.Errorf("supportsIdentity(%q) = %v, want %v", tt.dbVer, got, tt.wantIdentity)
}
if got := supportsFetchOffset(tt.dbVer); got != tt.wantFetchOffset {
t.Errorf("supportsFetchOffset(%q) = %v, want %v", tt.dbVer, got, tt.wantFetchOffset)
}
if got := supportsNativeBoolean(tt.dbVer); got != tt.wantNativeBoolean {
t.Errorf("supportsNativeBoolean(%q) = %v, want %v", tt.dbVer, got, tt.wantNativeBoolean)
}
if got := supportsExtendedString(tt.dbVer); got != tt.wantExtendedString {
t.Errorf("supportsExtendedString(%q) = %v, want %v", tt.dbVer, got, tt.wantExtendedString)
}
if got := supportsVector(tt.dbVer); got != tt.wantVector {
t.Errorf("supportsVector(%q) = %v, want %v", tt.dbVer, got, tt.wantVector)
}
if got := isOracle11g(tt.dbVer); got != tt.wantIsOracle11g {
t.Errorf("isOracle11g(%q) = %v, want %v", tt.dbVer, got, tt.wantIsOracle11g)
}
})
}
}
// ---- TestIsOracle11g ----
func TestIsOracle11g(t *testing.T) {
tests := []struct {
dbVer string
want bool
}{
{"11.2.0.4.0", true},
{"10.2.0.1.0", true},
{"12.1.0.2.0", false},
{"19.0.0.0", false},
{"21.0.0.0", false},
// 空或非法版本无法判定,委托 supportsIdentity 后视为不支持 IDENTITY(即 11g 保守路径)
{"", true},
{"invalid", true},
}
for _, tt := range tests {
t.Run(tt.dbVer, func(t *testing.T) {
if got := isOracle11g(tt.dbVer); got != tt.want {
t.Errorf("isOracle11g(%q) = %v, want %v", tt.dbVer, got, tt.want)
}
})
}
}
// ---- 简单访问器 ----
func TestDefaultValueOf(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
expr := d.DefaultValueOf(nil)
e, ok := expr.(clause.Expr)
if !ok {
t.Fatalf("DefaultValueOf() returned %T, want clause.Expr", expr)
}
if e.SQL != "VALUES (DEFAULT)" {
t.Errorf("DefaultValueOf() = %q, want %q", e.SQL, "VALUES (DEFAULT)")
}
}
func TestDummyTableName(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
if got := d.DummyTableName(); got != "DUAL" {
t.Errorf("DummyTableName() = %q, want %q", got, "DUAL")
}
}
func TestName(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
if got := d.Name(); got != "oracle" {
t.Errorf("Name() = %q, want %q", got, "oracle")
}
}
func TestGetAdapter(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
adapter := d.GetAdapter()
if adapter == nil {
t.Fatal("GetAdapter() returned nil adapter")
}
// 再次调用应返回相同的驱动类型
_ = d.GetAdapter()
}
// ---- TestOpen / TestNew / TestMigrator ----
func TestOpen(t *testing.T) {
dsn := "oracle://user:pass@host:1521/db?SSL=false"
dl := Open(dsn)
d, ok := dl.(*Dialector)
if !ok {
t.Fatalf("Open() returned %T, want *Dialector", dl)
}
if d.Config == nil || d.Config.DSN != dsn {
t.Errorf("Open() did not store DSN, got %+v", d.Config)
}
}
func TestNew(t *testing.T) {
cfg := Config{DBVer: "12.1.0.2.0", DefaultStringSize: 512, SkipQuoteIdentifiers: true}
dl := New(cfg)
d, ok := dl.(*Dialector)
if !ok {
t.Fatalf("New() returned %T, want *Dialector", dl)
}
if d.DBVer != cfg.DBVer || d.DefaultStringSize != cfg.DefaultStringSize || !d.SkipQuoteIdentifiers {
t.Errorf("New() did not preserve config, got %+v", d.Config)
}
}
func TestMigrator(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
db := &gorm.DB{Config: &gorm.Config{Dialector: d}}
m := d.Migrator(db)
if m == nil {
t.Fatal("Migrator() returned nil")
}
}
// ---- TestExplain ----
func TestExplain(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
got := d.Explain("SELECT * FROM USERS WHERE id = :1 AND active = :2 AND name = :3", 5, true, "joe")
want := "SELECT * FROM USERS WHERE id = 5 AND active = 1 AND name = 'joe'"
if got != want {
t.Errorf("Explain() = %q, want %q", got, want)
}
}
func TestExplainBoolConversion(t *testing.T) {
d := newTestDialector("12.1.0.2.0", 1024)
got := d.Explain("WHERE a = :1 AND b = :2", true, false)
want := "WHERE a = 1 AND b = 0"
if got != want {
t.Errorf("Explain() = %q, want %q", got, want)
}
}
// ---- SavePoint / RollbackTo(依赖真实 DB,跳过) ----
func TestSavePoint(t *testing.T) {
t.Skip("SavePoint 需要真实的 *gorm.DB 连接,跳过")
}
func TestRollbackTo(t *testing.T) {
t.Skip("RollbackTo 需要真实的 *gorm.DB 连接,跳过")
}