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 环境变量注入
This commit is contained in:
+749
@@ -0,0 +1,749 @@
|
||||
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 连接,跳过")
|
||||
}
|
||||
Reference in New Issue
Block a user