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:
2026-08-08 10:36:57 +08:00
parent 1f6c8a18a3
commit 9154afab6b
35 changed files with 4956 additions and 37 deletions
+254
View File
@@ -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 子句不支持引用序列 NEXTVALORA-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)
}
})
}
}