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:
+254
@@ -0,0 +1,254 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// ---- 测试辅助类型 ----
|
||||
|
||||
// testValuer 实现 driver.Valuer,用于测试 convertValue 的解包逻辑
|
||||
type testValuer struct {
|
||||
value driver.Value
|
||||
err error
|
||||
}
|
||||
|
||||
func (v testValuer) Value() (driver.Value, error) {
|
||||
return v.value, v.err
|
||||
}
|
||||
|
||||
// softDeleteModel 含软删除字段,用于构造 schema
|
||||
type softDeleteModel struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
|
||||
// plainModel 无软删除字段
|
||||
type plainModel struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string
|
||||
}
|
||||
|
||||
// createDataModel 用于 validateCreateData 测试
|
||||
type createDataModel struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
func parseTestSchema(t *testing.T, model interface{}) *schema.Schema {
|
||||
t.Helper()
|
||||
sch, err := schema.Parse(model, &sync.Map{}, schema.NamingStrategy{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse schema: %v", err)
|
||||
}
|
||||
return sch
|
||||
}
|
||||
|
||||
// ---- TestConvertValue ----
|
||||
|
||||
func TestConvertValue(t *testing.T) {
|
||||
now := time.Now()
|
||||
longString := strings.Repeat("a", 4001)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value interface{}
|
||||
want interface{}
|
||||
}{
|
||||
{"bool true to 1", true, 1},
|
||||
{"bool false to 0", false, 0},
|
||||
{"plain string unchanged", "hello", "hello"},
|
||||
{"long string (over 4000) unchanged", longString, longString},
|
||||
{"nil unchanged", nil, nil},
|
||||
{"time.Time unchanged", now, now},
|
||||
{"driver.Valuer unwrapped", testValuer{value: int64(42)}, int64(42)},
|
||||
{"driver.Valuer with string", testValuer{value: "val"}, "val"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := convertValue(tt.value, nil)
|
||||
if tt.want == nil {
|
||||
if got != nil {
|
||||
t.Errorf("convertValue(%v) = %v, want nil", tt.value, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("convertValue(%v) = %v (%T), want %v (%T)", tt.value, got, got, tt.want, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertValueValuerError(t *testing.T) {
|
||||
v := testValuer{err: errors.New("valuing failed")}
|
||||
got := convertValue(v, nil)
|
||||
// 出错时返回原始值
|
||||
if _, ok := got.(testValuer); !ok {
|
||||
t.Errorf("expected original value returned on Valuer error, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TestConvertFromOracleToField ----
|
||||
|
||||
func TestConvertFromOracleToField(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value interface{}
|
||||
want interface{}
|
||||
}{
|
||||
{"nil unchanged", nil, nil},
|
||||
{"plain int unchanged", 42, 42},
|
||||
{"plain string unchanged", "abc", "abc"},
|
||||
{"NullTime valid", sql.NullTime{Time: now, Valid: true}, now},
|
||||
{"NullTime invalid", sql.NullTime{Valid: false}, nil},
|
||||
{"NullInt64 valid", sql.NullInt64{Int64: 99, Valid: true}, int64(99)},
|
||||
{"NullInt64 invalid", sql.NullInt64{Valid: false}, nil},
|
||||
{"NullFloat64 valid", sql.NullFloat64{Float64: 3.14, Valid: true}, 3.14},
|
||||
{"NullFloat64 invalid", sql.NullFloat64{Valid: false}, nil},
|
||||
{"NullBool valid", sql.NullBool{Bool: true, Valid: true}, true},
|
||||
{"NullBool invalid", sql.NullBool{Valid: false}, nil},
|
||||
{"NullString valid", sql.NullString{String: "x", Valid: true}, "x"},
|
||||
{"NullString invalid", sql.NullString{Valid: false}, nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := convertFromOracleToField(tt.value, nil)
|
||||
if tt.want == nil {
|
||||
if got != nil {
|
||||
t.Errorf("convertFromOracleToField(%v) = %v, want nil", tt.value, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("convertFromOracleToField(%v) = %v (%T), want %v (%T)", tt.value, got, got, tt.want, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TestBuildOracleDefault ----
|
||||
|
||||
func TestBuildOracleDefault(t *testing.T) {
|
||||
// 12c 及以上版本号(NEXTVAL 默认值走 DEFAULT 子句)
|
||||
const dbVer12c = "12.1.0.2.0"
|
||||
// 11g 版本号(NEXTVAL 默认值不生成 DEFAULT 子句)
|
||||
const dbVer11g = "11.2.0.4.0"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dbVer string
|
||||
value string
|
||||
expected string
|
||||
}{
|
||||
{"empty string", dbVer12c, "", ""},
|
||||
{"NULL keyword", dbVer12c, "NULL", "DEFAULT NULL"},
|
||||
{"null lowercase", dbVer12c, "null", "DEFAULT NULL"},
|
||||
{"CURRENT_TIMESTAMP", dbVer12c, "CURRENT_TIMESTAMP", "DEFAULT CURRENT_TIMESTAMP"},
|
||||
{"now()", dbVer12c, "now()", "DEFAULT CURRENT_TIMESTAMP"},
|
||||
{"SYSDATE", dbVer12c, "SYSDATE", "DEFAULT SYSDATE"},
|
||||
{"sysdate lowercase", dbVer12c, "sysdate", "DEFAULT SYSDATE"},
|
||||
{"TRUE", dbVer12c, "TRUE", "DEFAULT 1"},
|
||||
{"false lowercase", dbVer12c, "false", "DEFAULT 0"},
|
||||
// 12c+ 原生支持 DEFAULT <seq>.NEXTVAL,行为不变
|
||||
{"sequence nextval 12c", dbVer12c, "SEQ_MY.NEXTVAL", "DEFAULT SEQ_MY.NEXTVAL"},
|
||||
// 11g 的 DEFAULT 子句不支持引用序列 NEXTVAL(ORA-00984),返回空串,
|
||||
// 由建表流程创建 BEFORE INSERT 触发器实现等价语义
|
||||
{"sequence nextval 11g", dbVer11g, "SEQ_MY.NEXTVAL", ""},
|
||||
{"sequence nextval 11g lowercase", dbVer11g, "seq_my.nextval", ""},
|
||||
{"date format", dbVer12c, "2006-01-02", "DEFAULT TO_DATE('2006-01-02', 'YYYY-MM-DD')"},
|
||||
{"timestamp format", dbVer12c, "2006-01-02 15:04:05", "DEFAULT TO_DATE('2006-01-02 15:04:05', 'YYYY-MM-DD HH24:MI:SS')"},
|
||||
{"plain string", dbVer12c, "hello", "DEFAULT 'hello'"},
|
||||
{"plain string with spaces", dbVer12c, " hello ", "DEFAULT ' hello '"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := buildOracleDefault(tt.dbVer, tt.value, nil)
|
||||
if got != tt.expected {
|
||||
t.Errorf("buildOracleDefault(%q, %q) = %q, want %q", tt.dbVer, tt.value, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TestCheckMissingWhereConditions ----
|
||||
|
||||
func TestCheckMissingWhereConditions(t *testing.T) {
|
||||
softSch := parseTestSchema(t, &softDeleteModel{})
|
||||
plainSch := parseTestSchema(t, &plainModel{})
|
||||
|
||||
softDeleteCond := clause.Eq{Column: clause.Column{Name: "deleted_at"}, Value: nil}
|
||||
normalCond := clause.Eq{Column: "age", Value: 25}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
conditions []clause.Expression
|
||||
schema *schema.Schema
|
||||
want bool
|
||||
}{
|
||||
{"empty conditions", nil, softSch, true},
|
||||
{"empty slice", []clause.Expression{}, plainSch, true},
|
||||
{"only soft delete condition", []clause.Expression{softDeleteCond}, softSch, true},
|
||||
{"soft delete + normal condition", []clause.Expression{softDeleteCond, normalCond}, softSch, false},
|
||||
{"only normal condition", []clause.Expression{normalCond}, softSch, false},
|
||||
{"soft delete condition on model without deleted_at", []clause.Expression{softDeleteCond}, plainSch, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := checkMissingWhereConditions(tt.conditions, tt.schema)
|
||||
if got != tt.want {
|
||||
t.Errorf("checkMissingWhereConditions(%v) = %v, want %v", tt.conditions, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TestValidateCreateData ----
|
||||
|
||||
func TestValidateCreateData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{"nil data", nil, "create data cannot be nil"},
|
||||
{"nil pointer", (*createDataModel)(nil), "create data pointer cannot be nil"},
|
||||
{"empty slice", []createDataModel{}, "create data slice cannot be empty"},
|
||||
{"valid struct", createDataModel{ID: 1}, ""},
|
||||
{"valid non-empty slice", []createDataModel{{ID: 1}}, ""},
|
||||
{"valid pointer to struct", &createDataModel{ID: 2}, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateCreateData(tt.data)
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Errorf("validateCreateData(%v) = %v, want nil error", tt.data, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("validateCreateData(%v) = nil, want error containing %q", tt.data, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Errorf("validateCreateData(%v) error = %q, want containing %q", tt.data, err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user