Files
oracle/common_test.go
T
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

255 lines
7.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
})
}
}