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
+84
View File
@@ -0,0 +1,84 @@
package tests
import (
"testing"
"time"
)
func TestCreateSingle(t *testing.T) {
// 先创建表
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
user := User{
Name: "Test User",
Email: "test@example.com",
Age: 25,
Active: true,
}
result := DB.Create(&user)
if result.Error != nil {
t.Fatalf("failed to create user: %v", result.Error)
}
if user.ID == 0 {
t.Error("expected user ID to be set after create")
}
t.Logf("Created user with ID: %d", user.ID)
}
func TestCreateBatch(t *testing.T) {
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
users := []User{
{Name: "User 1", Email: "user1@example.com", Age: 20},
{Name: "User 2", Email: "user2@example.com", Age: 22},
{Name: "User 3", Email: "user3@example.com", Age: 24},
}
result := DB.Create(&users)
if result.Error != nil {
t.Fatalf("failed to create users: %v", result.Error)
}
if result.RowsAffected != 3 {
t.Errorf("expected 3 rows affected, got %d", result.RowsAffected)
}
for i, user := range users {
if user.ID == 0 {
t.Errorf("user %d: expected ID to be set", i)
}
}
}
func TestCreateWithTimestamp(t *testing.T) {
if err := DB.AutoMigrate(&Product{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_PRODUCTS")
product := Product{
Name: "Test Product",
Price: 99.99,
Stock: 100,
Description: "This is a test product with long description",
CreatedAt: time.Now(),
}
result := DB.Create(&product)
if result.Error != nil {
t.Fatalf("failed to create product: %v", result.Error)
}
if product.ID == 0 {
t.Error("expected product ID to be set")
}
}
+70
View File
@@ -0,0 +1,70 @@
package tests
import (
"testing"
)
func TestDeleteSingle(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
user := User{Name: "Delete Test", Email: "delete@example.com", Age: 40}
DB.Create(&user)
// 删除
result := DB.Delete(&user)
if result.Error != nil {
t.Fatalf("failed to delete: %v", result.Error)
}
// 验证软删除(如果有 deleted_at)
var deleted User
result = DB.First(&deleted, user.ID)
if result.Error == nil {
t.Error("expected user to be soft deleted")
}
}
func TestDeleteWithoutWhere(t *testing.T) {
// 确保表存在
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 测试无 WHERE 条件的删除应该失败
result := DB.Delete(&User{})
if result.Error == nil {
t.Error("expected error for delete without WHERE condition")
}
t.Logf("Got expected error: %v", result.Error)
}
func TestHardDelete(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
user := User{Name: "Hard Delete", Email: "hard@example.com", Age: 50}
DB.Create(&user)
// 硬删除
result := DB.Unscoped().Delete(&user)
if result.Error != nil {
t.Fatalf("failed to hard delete: %v", result.Error)
}
// 验证完全删除
var deleted User
result = DB.Unscoped().First(&deleted, user.ID)
if result.Error == nil {
t.Error("expected user to be completely deleted")
}
}
+92
View File
@@ -0,0 +1,92 @@
package tests
import (
"testing"
"time"
"gorm.io/gorm"
)
// UserWithHook 带 Hook 的测试模型
type UserWithHook struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"size:100"`
Email string `gorm:"size:200"`
HookLog string `gorm:"size:500"` // 记录 Hook 执行
CreatedAt time.Time
UpdatedAt time.Time
}
func (UserWithHook) TableName() string {
return "TEST_USERS_HOOK"
}
func (u *UserWithHook) BeforeCreate(tx *gorm.DB) error {
u.HookLog += "BeforeCreate;"
return nil
}
func (u *UserWithHook) AfterCreate(tx *gorm.DB) error {
u.HookLog += "AfterCreate;"
return nil
}
func (u *UserWithHook) BeforeUpdate(tx *gorm.DB) error {
u.HookLog += "BeforeUpdate;"
return nil
}
func (u *UserWithHook) AfterUpdate(tx *gorm.DB) error {
u.HookLog += "AfterUpdate;"
return nil
}
func (u *UserWithHook) BeforeDelete(tx *gorm.DB) error {
u.HookLog += "BeforeDelete;"
return nil
}
func (u *UserWithHook) AfterDelete(tx *gorm.DB) error {
u.HookLog += "AfterDelete;"
return nil
}
func TestHooks(t *testing.T) {
// 创建表
if err := DB.AutoMigrate(&UserWithHook{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS_HOOK")
// 测试 Create Hook
user := UserWithHook{Name: "Hook Test", Email: "hook@example.com"}
result := DB.Create(&user)
if result.Error != nil {
t.Fatalf("failed to create: %v", result.Error)
}
if user.HookLog == "" {
t.Error("expected hooks to be called")
}
t.Logf("Hook log after create: %s", user.HookLog)
// 测试 Update Hook
user.HookLog = "" // 清空
if err := DB.Model(&user).Update("name", "Updated Name").Error; err != nil {
t.Fatalf("failed to update: %v", err)
}
t.Logf("Hook log after update: %s", user.HookLog)
if user.HookLog == "" {
t.Error("expected update hooks to be called")
}
// 测试 Delete Hook
user.HookLog = "" // 清空
if err := DB.Delete(&user).Error; err != nil {
t.Fatalf("failed to delete: %v", err)
}
t.Logf("Hook log after delete: %s", user.HookLog)
if user.HookLog == "" {
t.Error("expected delete hooks to be called")
}
}
+62
View File
@@ -0,0 +1,62 @@
package tests
import (
"log"
"os"
"testing"
"gorm.io/gorm"
oracle "git.charlienet.top/go/oracle"
)
var DB *gorm.DB
func TestMain(m *testing.M) {
// 使用提供的 DSN(可通过 ORACLE_DSN 环境变量覆盖)
dsn := os.Getenv("ORACLE_DSN")
if dsn == "" {
// 注:go-ora v2.9.0 起 CONNECTION TIMEOUT 语义从"socket 读超时"变为"连接建立超时"
// 读超时改用 SOCKET TIMEOUT 指定。两者均设 90s 以保留原有的读超时保护语义。
// 安全:此处为占位符,真实 DSN 请通过 ORACLE_DSN 环境变量提供,避免凭据入库。
dsn = "oracle://user:password@host:1521/service?SSL=false&CONNECTION TIMEOUT=90&SOCKET TIMEOUT=90&LANGUAGE=SIMPLIFIED+CHINESE&TERRITORY=CHINA"
}
var err error
DB, err = gorm.Open(oracle.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect database: %v", err)
}
log.Println("successfully connected to database")
// 清理测试表
cleanup()
// 运行测试
code := m.Run()
// 最终清理
cleanup()
os.Exit(code)
}
// cleanup 删除所有测试表(忽略错误,因为表可能不存在)
func cleanup() {
_ = DB.Migrator().DropTable(&User{}, &Product{}, &Order{}, &UserWithHook{}, &SeqDefaultViaDriverModel{}, &BigStringModel{})
// TEST_SEQ_DEFAULT 表通过原生 SQL 创建(无 autoIncrement),DropTable 无法识别,
// 因此用原生 SQL 清理表与序列
DB.Exec("DROP TABLE TEST_SEQ_DEFAULT")
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEFAULT")
// 序列默认值测试的独立序列(TEST_SEQ_DEF 表可通过 DropTable 清理并级联删除触发器)
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEF_CODE")
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEF")
}
// clearTable 清空指定测试表,保证测试之间的数据隔离
func clearTable(t *testing.T, table string) {
t.Helper()
if err := DB.Exec("DELETE FROM " + table).Error; err != nil {
t.Fatalf("failed to clear table %s: %v", table, err)
}
}
+113
View File
@@ -0,0 +1,113 @@
package tests
import (
"strings"
"testing"
)
func TestAutoMigrate(t *testing.T) {
// 测试创建表
err := DB.AutoMigrate(&User{}, &Product{}, &Order{})
if err != nil {
t.Fatalf("failed to auto migrate: %v", err)
}
// 验证表存在
if !DB.Migrator().HasTable(&User{}) {
t.Error("expected User table to exist")
}
if !DB.Migrator().HasTable(&Product{}) {
t.Error("expected Product table to exist")
}
if !DB.Migrator().HasTable(&Order{}) {
t.Error("expected Order table to exist")
}
}
func TestAddColumn(t *testing.T) {
// 先创建表
DB.AutoMigrate(&User{})
// 添加列(需要定义新模型)
type UserWithPhone struct {
User
Phone string `gorm:"size:20"`
}
err := DB.AutoMigrate(&UserWithPhone{})
if err != nil {
t.Fatalf("failed to add column: %v", err)
}
// 验证列存在
if !DB.Migrator().HasColumn(&UserWithPhone{}, "phone") {
t.Error("expected phone column to exist")
}
}
func TestDropTable(t *testing.T) {
// 确保表存在
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
err := DB.Migrator().DropTable(&User{})
if err != nil {
t.Fatalf("failed to drop table: %v", err)
}
if DB.Migrator().HasTable(&User{}) {
t.Error("expected User table to be dropped")
}
}
// BigStringModel 验证 11g 下 size>4000 的 string 字段映射为 CLOB 列
type BigStringModel struct {
ID uint `gorm:"column:id;primaryKey"`
Big string `gorm:"column:big;size:5000"`
}
func (BigStringModel) TableName() string {
return "TEST_BIG_STRING"
}
// TestBigStringMapsToCLOBOn11g 验证 11g 下 size>4000 的 string 字段:
// DataTypeOf 的 32k VARCHAR2 特性(12c+ 才支持)在 11g 不触发,
// 字段应建为 CLOB,且能正常插入超过 4000 字节的长文本。
func TestBigStringMapsToCLOBOn11g(t *testing.T) {
if err := DB.AutoMigrate(&BigStringModel{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
defer func() {
DB.Migrator().DropTable(&BigStringModel{})
}()
// 验证列类型为 CLOB(Oracle 数据字典列名大写存储)
var dataType string
if err := DB.Raw("SELECT DATA_TYPE FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?",
"TEST_BIG_STRING", "BIG").Scan(&dataType).Error; err != nil {
t.Fatalf("failed to query column type: %v", err)
}
if dataType != "CLOB" {
t.Errorf("expected column type CLOB on 11g, got %q", dataType)
}
// 插入超过 4000 字节的长文本,验证 CLOB 列可容纳
longText := strings.Repeat("a", 5000)
bm := BigStringModel{Big: longText}
if err := DB.Create(&bm).Error; err != nil {
t.Fatalf("failed to create: %v", err)
}
if bm.ID == 0 {
t.Error("expected ID to be set")
}
// 回读验证内容完整
var got string
if err := DB.Raw("SELECT BIG FROM TEST_BIG_STRING WHERE id = ?", bm.ID).Scan(&got).Error; err != nil {
t.Fatalf("failed to query back: %v", err)
}
if got != longText {
t.Errorf("text roundtrip mismatch: got len=%d, want len=%d", len(got), len(longText))
}
}
+80
View File
@@ -0,0 +1,80 @@
package tests
import (
"time"
"gorm.io/gorm"
)
// User 基本测试模型
type User struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:200;uniqueIndex"`
Age int `gorm:"default:0"`
Active bool // 不使用 default,以便显式设置 false 时能正确存储
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (User) TableName() string {
return "TEST_USERS"
}
// Product 测试数值类型
type Product struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement"`
Name string `gorm:"size:200;not null"`
Price float64 `gorm:"precision:10;scale:2"`
Stock int `gorm:"default:0"`
Description string `gorm:"type:CLOB"`
CreatedAt time.Time
}
func (Product) TableName() string {
return "TEST_PRODUCTS"
}
// Order 测试关联关系
type Order struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement"`
UserID uint `gorm:"not null;index"`
User User `gorm:"foreignKey:UserID"`
Total float64 `gorm:"precision:12;scale:2"`
Status string `gorm:"size:20;default:'pending'"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (Order) TableName() string {
return "TEST_ORDERS"
}
// SeqDefaultModel 显式使用序列默认值的模型(主键不用 autoIncrement
// 而是通过列的 DEFAULT 值使用序列 SEQ_TEST_SEQ_DEFAULT.NEXTVAL
type SeqDefaultModel struct {
ID uint `gorm:"column:id;primaryKey"`
Name string `gorm:"size:100"`
}
func (SeqDefaultModel) TableName() string {
return "TEST_SEQ_DEFAULT"
}
// SeqDefaultViaDriverModel 通过驱动 AutoMigrate 建表,验证 11g 下序列默认值的
// 触发器路径(模型使用 gorm:"default:(SEQ_TEST_SEQ_DEF_CODE.NEXTVAL)")。
//
// Code 用 int 字段并给默认值加括号:GORM 对含括号的默认值会跳过 ParseInt 解析
// schema/field.go:231),从而 DefaultValueInterface 保持 nil,字段进入
// FieldsWithDefaultDBValue —— INSERT 时 GORM 会省略该列,触发 BEFORE INSERT 触发器回填序列值。
// 若不加括号,GORM 会把 "SEQ_...NEXTVAL" 当作整数解析失败,schema.Parse 直接报错。
type SeqDefaultViaDriverModel struct {
ID uint `gorm:"column:id;primaryKey"`
Code int `gorm:"column:code;default:(SEQ_TEST_SEQ_DEF_CODE.NEXTVAL)"`
Name string `gorm:"size:100"`
}
func (SeqDefaultViaDriverModel) TableName() string {
return "TEST_SEQ_DEF"
}
+84
View File
@@ -0,0 +1,84 @@
package tests
import (
"testing"
)
func TestQuerySingle(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
user := User{Name: "Query Test", Email: "query@example.com", Age: 35}
DB.Create(&user)
// 查询
var found User
result := DB.First(&found, user.ID)
if result.Error != nil {
t.Fatalf("failed to query: %v", result.Error)
}
if found.Name != "Query Test" {
t.Errorf("expected name 'Query Test', got '%s'", found.Name)
}
}
func TestQueryWithConditions(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
users := []User{
{Name: "Condition 1", Email: "cond1@example.com", Age: 20, Active: true},
{Name: "Condition 2", Email: "cond2@example.com", Age: 25, Active: true},
{Name: "Condition 3", Email: "cond3@example.com", Age: 30, Active: false},
}
DB.Create(&users)
// 条件查询
var results []User
result := DB.Where("active = ? AND age > ?", true, 22).Find(&results)
if result.Error != nil {
t.Fatalf("failed to query: %v", result.Error)
}
if len(results) != 1 {
t.Errorf("expected 1 result, got %d", len(results))
}
}
func TestQueryWithLimit(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
for i := 0; i < 10; i++ {
user := User{
Name: "Limit Test",
Email: "limit" + string(rune('a'+i)) + "@example.com",
Age: i,
}
DB.Create(&user)
}
// 分页查询
var results []User
result := DB.Limit(5).Offset(2).Find(&results)
if result.Error != nil {
t.Fatalf("failed to query: %v", result.Error)
}
if len(results) != 5 {
t.Errorf("expected 5 results, got %d", len(results))
}
}
+230
View File
@@ -0,0 +1,230 @@
package tests
import (
"testing"
)
// TestSequenceObjectExists 验证 11g 自增依赖的序列对象存在
// (Oracle 对象名默认大写存储,序列命名规则见 migrator.go 的 sequenceName
func TestSequenceObjectExists(t *testing.T) {
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
// 验证序列对象存在(11g 自增依赖序列)
var count int64
if err := DB.Raw("SELECT COUNT(*) FROM USER_SEQUENCES WHERE SEQUENCE_NAME = ?", "SEQ_TEST_USERS").Scan(&count).Error; err != nil {
t.Fatalf("failed to query sequence: %v", err)
}
if count == 0 {
t.Error("expected sequence SEQ_TEST_USERS to exist (auto increment support)")
}
}
// TestTriggerObjectExists 验证 11g 自增依赖的触发器对象存在
// (触发器命名规则见 migrator.go 的 triggerName
func TestTriggerObjectExists(t *testing.T) {
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
// 验证触发器对象存在(11g 自增依赖触发器)
var count int64
if err := DB.Raw("SELECT COUNT(*) FROM USER_TRIGGERS WHERE TRIGGER_NAME = ?", "TRG_TEST_USERS").Scan(&count).Error; err != nil {
t.Fatalf("failed to query trigger: %v", err)
}
if count == 0 {
t.Error("expected trigger TRG_TEST_USERS to exist (auto increment support)")
}
}
// TestAutoIncrementViaSequence 连续插入两条,验证 ID 递增(证明序列工作)
func TestAutoIncrementViaSequence(t *testing.T) {
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 连续插入两条,验证 ID 递增(证明序列工作)
u1 := User{Name: "Seq 1", Email: "seq1@example.com", Age: 1}
u2 := User{Name: "Seq 2", Email: "seq2@example.com", Age: 2}
if err := DB.Create(&u1).Error; err != nil {
t.Fatalf("failed to create u1: %v", err)
}
if err := DB.Create(&u2).Error; err != nil {
t.Fatalf("failed to create u2: %v", err)
}
if u1.ID == 0 {
t.Error("expected u1.ID to be set")
}
if u2.ID != u1.ID+1 {
t.Errorf("expected u2.ID = u1.ID+1, got u1.ID=%d, u2.ID=%d", u1.ID, u2.ID)
}
}
// TestExplicitSequenceDefaultValue 验证:插入时不给主键 id,主键由序列自动生成。
//
// ⚠️ 重要发现:Oracle 11g 的 CREATE TABLE 的 DEFAULT 子句不允许引用序列的
// NEXTVALORA-00984: 列在此处不允许,报错位置指向 NEXTVAL),该能力 12c 才引入。
// 因此任务原始 SQL "id NUMBER(19) DEFAULT SEQ_TEST_SEQ_DEFAULT.NEXTVAL" 无法在 11g 执行。
// 本测试改用 11g 的标准做法——BEFORE INSERT 触发器(与 go-ora migrator 给 autoIncrement
// 表生成的触发器机制相同)在 id 为 NULL 时从序列取值,验证语义与"DEFAULT 序列"等价:
// 插入时不提供 id,主键由序列自动生成并严格递增。
//
// 驱动行为实测(GORM v1.31.2 + go-ora v2.9.0,真实 11g 库):
// - GORM 会把非 autoIncrement 的 int/uint 主键自动视为自增(schema.go:337-348
// 设 AutoIncrement=true、HasDefaultValue=true,并加入 FieldsWithDefaultDBValue)。
// - 因此 DB.Create 不会把 id 列显式写入 INSERT,而是使用 RETURNING 回填;
// 由于表上有 BEFORE INSERT 触发器从序列取值,RETURNING 返回的即序列生成值。
// - 实测 DB.Create 后 m1.ID 被回填为序列值(START WITH 100 → 100),非 0。
func TestExplicitSequenceDefaultValue(t *testing.T) {
// 先删除可能残留的对象(忽略错误:对象可能不存在)
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEFAULT")
DB.Migrator().DropTable(&SeqDefaultModel{})
// 创建序列
if err := DB.Exec("CREATE SEQUENCE SEQ_TEST_SEQ_DEFAULT START WITH 100 INCREMENT BY 1 NOCACHE").Error; err != nil {
t.Fatalf("failed to create sequence: %v", err)
}
// 测试结束清理序列和表(DROP TABLE 会级联删除其上的触发器)
defer func() {
DB.Exec("DROP TABLE TEST_SEQ_DEFAULT")
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEFAULT")
}()
// 建表(11g 的 DEFAULT 子句不支持引用序列,改用普通列 + 触发器)
if err := DB.Exec(`CREATE TABLE TEST_SEQ_DEFAULT (
id NUMBER(19) NOT NULL PRIMARY KEY,
name VARCHAR2(100)
)`).Error; err != nil {
t.Fatalf("failed to create table: %v", err)
}
// 创建 BEFORE INSERT 触发器:id 为 NULL 时从序列取值
triggerSQL := `CREATE OR REPLACE TRIGGER TRG_TEST_SEQ_DEFAULT
BEFORE INSERT ON TEST_SEQ_DEFAULT
FOR EACH ROW
BEGIN
IF :NEW.id IS NULL THEN
SELECT SEQ_TEST_SEQ_DEFAULT.NEXTVAL INTO :NEW.id FROM DUAL;
END IF;
END;`
if err := DB.Exec(triggerSQL).Error; err != nil {
t.Fatalf("failed to create trigger: %v", err)
}
// 插入时不给 id(依赖触发器从序列默认取值)
m1 := SeqDefaultModel{Name: "Default Seq 1"}
if err := DB.Create(&m1).Error; err != nil {
t.Fatalf("failed to create m1: %v", err)
}
if m1.ID == 0 {
t.Error("expected m1.ID to be set from sequence")
}
t.Logf("m1.ID 回填=%d", m1.ID)
// 用数据库查询交叉验证实际存储的 ID 来自序列
var id1 uint
if err := DB.Raw("SELECT id FROM TEST_SEQ_DEFAULT WHERE name = ?", "Default Seq 1").Scan(&id1).Error; err != nil {
t.Fatalf("failed to query id1: %v", err)
}
if id1 == 0 {
t.Error("expected database id1 to be set from sequence")
}
if id1 != m1.ID {
t.Errorf("database id1=%d != m1.ID=%d", id1, m1.ID)
}
t.Logf("数据库实际 id=%d(序列 START WITH 100", id1)
// 第二次插入验证序列递增
m2 := SeqDefaultModel{Name: "Default Seq 2"}
if err := DB.Create(&m2).Error; err != nil {
t.Fatalf("failed to create m2: %v", err)
}
if m2.ID != m1.ID+1 {
t.Errorf("expected m2.ID = m1.ID+1, got m1.ID=%d, m2.ID=%d", m1.ID, m2.ID)
}
// 数据库端二次确认
var id2 uint
if err := DB.Raw("SELECT id FROM TEST_SEQ_DEFAULT WHERE name = ?", "Default Seq 2").Scan(&id2).Error; err != nil {
t.Fatalf("failed to query id2: %v", err)
}
if id2 != id1+1 {
t.Errorf("expected database id2 = id1+1, got id1=%d, id2=%d", id1, id2)
}
t.Logf("m2.ID 回填=%d, 数据库实际 id=%d", m2.ID, id2)
}
// TestSequenceDefaultViaAutoMigrate 验证 11g 下通过驱动 AutoMigrate 建表时的序列默认值触发器路径。
//
// 模型使用 gorm:"default:SEQ_TEST_SEQ_DEF_CODE.NEXTVAL"
// - 11g 的 CREATE TABLE 的 DEFAULT 子句不允许引用序列 NEXTVALORA-00984),
// 驱动重写的 FullDataTypeOf 会跳过 DEFAULT 子句,建表后由 CreateTable 流程创建
// BEFORE INSERT 触发器 SEQDEF_TRG_TEST_SEQ_DEF_CODE 实现等价语义;
// - 12c+ 则不创建触发器,直接生成 DEFAULT SEQ_TEST_SEQ_DEF_CODE.NEXTVAL。
func TestSequenceDefaultViaAutoMigrate(t *testing.T) {
// 先清理可能残留的对象(忽略错误:对象可能不存在)
DB.Migrator().DropTable(&SeqDefaultViaDriverModel{})
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEF_CODE")
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEF")
DB.Exec("DROP TRIGGER SEQDEF_TRG_TEST_SEQ_DEF_CODE")
// 手动创建序列:DEFAULT 引用的序列不会自动创建(autoIncrement 只负责创建
// ID 用的 SEQ_TEST_SEQ_DEF),测试中先 DROP 再 CREATE,测试后清理。
if err := DB.Exec("CREATE SEQUENCE SEQ_TEST_SEQ_DEF_CODE START WITH 100 INCREMENT BY 1 NOCACHE").Error; err != nil {
t.Fatalf("failed to create sequence: %v", err)
}
// 测试结束清理表(DROP TABLE 会级联删除其上触发器)和序列
defer func() {
DB.Exec("DROP TABLE TEST_SEQ_DEF")
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEF_CODE")
DB.Exec("DROP SEQUENCE SEQ_TEST_SEQ_DEF")
}()
// 通过驱动 AutoMigrate 建表(11g 下不会生成 DEFAULT <seq>.NEXTVAL 子句)
if err := DB.AutoMigrate(&SeqDefaultViaDriverModel{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
// 验证序列默认值触发器已创建(命名 SEQDEF_TRG_<table>_<column>
// 避免与 autoIncrement 的 TRG_TEST_SEQ_DEF 冲突)
var trigCount int64
if err := DB.Raw("SELECT COUNT(*) FROM USER_TRIGGERS WHERE TRIGGER_NAME = ?", "SEQDEF_TRG_TEST_SEQ_DEF_CODE").Scan(&trigCount).Error; err != nil {
t.Fatalf("failed to query trigger: %v", err)
}
if trigCount == 0 {
t.Error("expected sequence default trigger SEQDEF_TRG_TEST_SEQ_DEF_CODE to exist")
}
// 插入时不给 Code(字段在 FieldsWithDefaultDBValue 中,GORM 省略该列),
// 触发器应从序列回填,RETURNING 将序列值回填到 m1.Code
m1 := SeqDefaultViaDriverModel{Name: "Via AutoMigrate 1"}
if err := DB.Create(&m1).Error; err != nil {
t.Fatalf("failed to create m1: %v", err)
}
if m1.Code == 0 {
t.Error("expected m1.Code to be set by sequence default trigger")
}
t.Logf("m1.Code 回填=%d(序列 START WITH 100", m1.Code)
// 查询数据库交叉验证 code 由触发器回填
var code1 int
if err := DB.Raw("SELECT code FROM TEST_SEQ_DEF WHERE name = ?", "Via AutoMigrate 1").Scan(&code1).Error; err != nil {
t.Fatalf("failed to query code1: %v", err)
}
if code1 != m1.Code {
t.Errorf("database code1=%d != m1.Code=%d", code1, m1.Code)
}
// 再次插入验证序列递增
m2 := SeqDefaultViaDriverModel{Name: "Via AutoMigrate 2"}
if err := DB.Create(&m2).Error; err != nil {
t.Fatalf("failed to create m2: %v", err)
}
if m2.Code != m1.Code+1 {
t.Errorf("expected m2.Code = m1.Code+1, got m1.Code=%d, m2.Code=%d", m1.Code, m2.Code)
}
t.Logf("m2.Code 回填=%d", m2.Code)
}
+37
View File
@@ -0,0 +1,37 @@
package tests
import (
"testing"
)
// TestSoftDeleteSetsDeletedAt 精确验证软删除后 DELETED_AT 确实被写入数据库
func TestSoftDeleteSetsDeletedAt(t *testing.T) {
// 确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
user := User{Name: "Soft Delete Exact", Email: "soft_exact@example.com", Age: 40}
if err := DB.Create(&user).Error; err != nil {
t.Fatalf("failed to create: %v", err)
}
// 软删除
if err := DB.Delete(&user).Error; err != nil {
t.Fatalf("failed to soft delete: %v", err)
}
// 直接验证 DELETED_AT 被写入:用 Unscoped 查询(绕过软删除过滤)
var deleted User
if err := DB.Unscoped().First(&deleted, user.ID).Error; err != nil {
t.Fatalf("failed to query unscoped: %v", err)
}
if !deleted.DeletedAt.Valid {
t.Errorf("expected DeletedAt to be valid (set), got invalid")
}
if deleted.DeletedAt.Time.IsZero() {
t.Errorf("expected DeletedAt to have a timestamp, got zero")
}
t.Logf("DeletedAt set to: %v", deleted.DeletedAt.Time)
}
+74
View File
@@ -0,0 +1,74 @@
package tests
import (
"testing"
)
func TestUpdateSingle(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
user := User{Name: "Update Test", Email: "update@example.com", Age: 30}
DB.Create(&user)
// 更新
result := DB.Model(&user).Update("age", 31)
if result.Error != nil {
t.Fatalf("failed to update: %v", result.Error)
}
if result.RowsAffected != 1 {
t.Errorf("expected 1 row affected, got %d", result.RowsAffected)
}
// 验证更新
var updated User
DB.First(&updated, user.ID)
if updated.Age != 31 {
t.Errorf("expected age 31, got %d", updated.Age)
}
}
func TestUpdateMultiple(t *testing.T) {
// 先确保表存在并清空
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 创建测试数据
users := []User{
{Name: "Multi Update 1", Email: "multi1@example.com", Age: 25},
{Name: "Multi Update 2", Email: "multi2@example.com", Age: 25},
}
DB.Create(&users)
// 批量更新
result := DB.Model(&User{}).Where("age = ?", 25).Update("age", 26)
if result.Error != nil {
t.Fatalf("failed to update: %v", result.Error)
}
if result.RowsAffected < 2 {
t.Errorf("expected at least 2 rows affected, got %d", result.RowsAffected)
}
}
func TestUpdateWithoutWhere(t *testing.T) {
// 确保表存在
if err := DB.AutoMigrate(&User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
clearTable(t, "TEST_USERS")
// 测试无 WHERE 条件的更新应该失败
result := DB.Model(&User{}).Update("age", 99)
if result.Error == nil {
t.Error("expected error for update without WHERE condition")
}
t.Logf("Got expected error: %v", result.Error)
}