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 连接,跳过") }