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:
@@ -0,0 +1,58 @@
|
||||
package clauses
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// testDialector 是仅用于 SQL 生成测试的最小 Dialector 实现,无需数据库连接。
|
||||
// 它模拟 Oracle 的绑定变量占位符(:N)和不加引号的标识符引用。
|
||||
type testDialector struct{}
|
||||
|
||||
func (testDialector) Name() string { return "oracle" }
|
||||
func (testDialector) Initialize(*gorm.DB) error { return nil }
|
||||
func (testDialector) Migrator(*gorm.DB) gorm.Migrator { return nil }
|
||||
func (testDialector) DataTypeOf(*schema.Field) string { return "" }
|
||||
func (testDialector) DefaultValueOf(*schema.Field) clause.Expression { return nil }
|
||||
|
||||
func (testDialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
|
||||
writer.WriteString(":")
|
||||
writer.WriteString(strconv.Itoa(len(stmt.Vars)))
|
||||
}
|
||||
|
||||
func (testDialector) QuoteTo(writer clause.Writer, str string) {
|
||||
writer.WriteString(str)
|
||||
}
|
||||
|
||||
func (testDialector) Explain(sql string, vars ...interface{}) string { return sql }
|
||||
|
||||
// newStatement 构造一个可以直接作为 clause.Builder 使用的 gorm.Statement。
|
||||
func newStatement(t *testing.T) *gorm.Statement {
|
||||
t.Helper()
|
||||
db := &gorm.DB{
|
||||
Config: &gorm.Config{Dialector: testDialector{}},
|
||||
}
|
||||
return &gorm.Statement{DB: db, Table: "users"}
|
||||
}
|
||||
|
||||
// buildSQL 直接调用子句的 Build 方法生成 SQL。
|
||||
func buildSQL(t *testing.T, expr clause.Expression) string {
|
||||
t.Helper()
|
||||
stmt := newStatement(t)
|
||||
expr.Build(stmt)
|
||||
return stmt.SQL.String()
|
||||
}
|
||||
|
||||
// buildClauseSQL 通过 clause.Clause(模拟 gorm.Statement.Build 的构建流程)
|
||||
// 生成带子句名前缀的完整 SQL。
|
||||
func buildClauseSQL(t *testing.T, name string, expr clause.Expression) string {
|
||||
t.Helper()
|
||||
stmt := newStatement(t)
|
||||
cc := clause.Clause{Name: name, Expression: expr}
|
||||
cc.Build(stmt)
|
||||
return stmt.SQL.String()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package clauses
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func TestMergeName(t *testing.T) {
|
||||
var m Merge
|
||||
if got := m.Name(); got != "MERGE" {
|
||||
t.Errorf("Merge.Name() = %q, want %q", got, "MERGE")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDefaultExcludeName(t *testing.T) {
|
||||
if got := MergeDefaultExcludeName(); got != "exclude" {
|
||||
t.Errorf("MergeDefaultExcludeName() = %q, want %q", got, "exclude")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeBuild(t *testing.T) {
|
||||
merge := Merge{
|
||||
Table: clause.Table{Name: "users"},
|
||||
Using: []clause.Interface{
|
||||
clause.Select{Columns: []clause.Column{{Name: "id"}, {Name: "name"}}},
|
||||
clause.From{Tables: []clause.Table{{Name: "users"}}},
|
||||
},
|
||||
On: []clause.Expression{
|
||||
clause.Eq{Column: clause.Column{Name: "a"}, Value: clause.Column{Name: "b"}},
|
||||
},
|
||||
}
|
||||
|
||||
sql := buildClauseSQL(t, "MERGE", merge)
|
||||
|
||||
for _, want := range []string{
|
||||
"MERGE INTO", // 前缀 + Insert 构建
|
||||
"USING (", // USING 子查询
|
||||
"SELECT id,name FROM users", // USING 子查询内容
|
||||
") exclude ON (", // exclude 别名 + ON
|
||||
"a = b", // ON 条件
|
||||
} {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Errorf("Merge SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeBuildEmpty(t *testing.T) {
|
||||
var merge Merge
|
||||
|
||||
sql := buildClauseSQL(t, "MERGE", merge)
|
||||
if want := "MERGE INTO users USING () exclude ON ()"; sql != want {
|
||||
t.Errorf("empty Merge SQL = %q, want %q", sql, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeMergeClause(t *testing.T) {
|
||||
merge := Merge{
|
||||
Table: clause.Table{Name: "users"},
|
||||
On: []clause.Expression{
|
||||
clause.Eq{Column: clause.Column{Name: "a"}, Value: clause.Column{Name: "b"}},
|
||||
},
|
||||
}
|
||||
|
||||
cc := &clause.Clause{}
|
||||
merge.MergeClause(cc)
|
||||
|
||||
if cc.Name != "MERGE" {
|
||||
t.Errorf("MergeClause name = %q, want %q", cc.Name, "MERGE")
|
||||
}
|
||||
|
||||
if got, ok := cc.Expression.(Merge); !ok || !reflect.DeepEqual(got, merge) {
|
||||
t.Errorf("MergeClause expression = %#v, want %#v", cc.Expression, merge)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package clauses
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
@@ -8,3 +9,36 @@ type ReturningInto struct {
|
||||
Variables []clause.Column
|
||||
Into []*clause.Values
|
||||
}
|
||||
|
||||
// Name returns the name of the clause
|
||||
func (r ReturningInto) Name() string {
|
||||
return "RETURNING INTO"
|
||||
}
|
||||
|
||||
// Build builds the SQL for the RETURNING INTO clause
|
||||
func (r ReturningInto) Build(builder clause.Builder) {
|
||||
if len(r.Variables) > 0 {
|
||||
builder.WriteString("RETURNING ")
|
||||
for idx, col := range r.Variables {
|
||||
if idx > 0 {
|
||||
builder.WriteByte(',')
|
||||
}
|
||||
builder.WriteQuoted(col)
|
||||
}
|
||||
|
||||
builder.WriteString(" INTO ")
|
||||
for idx := range r.Variables {
|
||||
if idx > 0 {
|
||||
builder.WriteByte(',')
|
||||
}
|
||||
// 写入绑定变量占位符
|
||||
builder.WriteString(fmt.Sprintf(":%d", idx+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MergeClause merge returning into clause
|
||||
func (r ReturningInto) MergeClause(clause *clause.Clause) {
|
||||
clause.Name = r.Name()
|
||||
clause.Expression = r
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package clauses
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func TestReturningIntoName(t *testing.T) {
|
||||
var r ReturningInto
|
||||
if got := r.Name(); got != "RETURNING INTO" {
|
||||
t.Errorf("ReturningInto.Name() = %q, want %q", got, "RETURNING INTO")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturningIntoBuild(t *testing.T) {
|
||||
r := ReturningInto{
|
||||
Variables: []clause.Column{{Name: "col1"}, {Name: "col2"}},
|
||||
}
|
||||
|
||||
sql := buildSQL(t, r)
|
||||
if want := "RETURNING col1,col2 INTO :1,:2"; sql != want {
|
||||
t.Errorf("ReturningInto SQL = %q, want %q", sql, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturningIntoBuildSingle(t *testing.T) {
|
||||
r := ReturningInto{
|
||||
Variables: []clause.Column{{Name: "col1"}},
|
||||
}
|
||||
|
||||
sql := buildSQL(t, r)
|
||||
if want := "RETURNING col1 INTO :1"; sql != want {
|
||||
t.Errorf("ReturningInto SQL = %q, want %q", sql, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturningIntoBuildEmpty(t *testing.T) {
|
||||
var r ReturningInto
|
||||
|
||||
sql := buildSQL(t, r)
|
||||
if sql != "" {
|
||||
t.Errorf("ReturningInto with empty Variables should generate no SQL, got %q", sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturningIntoMergeClause(t *testing.T) {
|
||||
r := ReturningInto{
|
||||
Variables: []clause.Column{{Name: "id"}},
|
||||
}
|
||||
|
||||
cc := &clause.Clause{}
|
||||
r.MergeClause(cc)
|
||||
|
||||
if cc.Name != "RETURNING INTO" {
|
||||
t.Errorf("ReturningInto.MergeClause name = %q, want %q", cc.Name, "RETURNING INTO")
|
||||
}
|
||||
|
||||
if got, ok := cc.Expression.(ReturningInto); !ok || !reflect.DeepEqual(got, r) {
|
||||
t.Errorf("ReturningInto.MergeClause expression = %#v, want %#v", cc.Expression, r)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ func (w WhenMatched) Build(builder clause.Builder) {
|
||||
builder.WriteString(" UPDATE ")
|
||||
builder.WriteString(w.Name())
|
||||
builder.WriteByte(' ')
|
||||
w.Build(builder)
|
||||
builder.WriteString("SET ")
|
||||
w.Set.Build(builder)
|
||||
|
||||
buildWhere := func(where clause.Where) {
|
||||
builder.WriteString(where.Name())
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package clauses
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func TestWhenMatchedName(t *testing.T) {
|
||||
var w WhenMatched
|
||||
if got := w.Name(); got != "WHEN MATCHED" {
|
||||
t.Errorf("WhenMatched.Name() = %q, want %q", got, "WHEN MATCHED")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenMatchedBuild(t *testing.T) {
|
||||
w := WhenMatched{
|
||||
Set: clause.Set{{Column: clause.Column{Name: "name"}, Value: "x"}},
|
||||
}
|
||||
|
||||
sql := buildSQL(t, w)
|
||||
// 期望: THEN UPDATE WHEN MATCHED SET name=:1
|
||||
for _, want := range []string{
|
||||
"THEN UPDATE",
|
||||
"WHEN MATCHED",
|
||||
"SET name=:1",
|
||||
} {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Errorf("WhenMatched SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenMatchedBuildMultipleSet(t *testing.T) {
|
||||
w := WhenMatched{
|
||||
Set: clause.Set{
|
||||
{Column: clause.Column{Name: "name"}, Value: "x"},
|
||||
{Column: clause.Column{Name: "age"}, Value: 1},
|
||||
},
|
||||
}
|
||||
|
||||
sql := buildSQL(t, w)
|
||||
if want := "SET name=:1,age=:2"; !strings.Contains(sql, want) {
|
||||
t.Errorf("WhenMatched SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenMatchedBuildWithWhereAndDelete(t *testing.T) {
|
||||
w := WhenMatched{
|
||||
Set: clause.Set{{Column: clause.Column{Name: "name"}, Value: "x"}},
|
||||
Where: clause.Where{Exprs: []clause.Expression{
|
||||
clause.Eq{Column: clause.Column{Name: "id"}, Value: 1},
|
||||
}},
|
||||
Delete: clause.Where{Exprs: []clause.Expression{
|
||||
clause.Eq{Column: clause.Column{Name: "flag"}, Value: 0},
|
||||
}},
|
||||
}
|
||||
|
||||
sql := buildSQL(t, w)
|
||||
// 期望: THEN UPDATE WHEN MATCHED SET name=:1WHERE id = :2 DELETE WHERE flag = :3
|
||||
for _, want := range []string{
|
||||
"THEN UPDATE",
|
||||
"WHEN MATCHED",
|
||||
"SET name=:1",
|
||||
"WHERE id = :2",
|
||||
"DELETE WHERE flag = :3",
|
||||
} {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Errorf("WhenMatched SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWhenMatchedClauseBuild 验证通过 clause.Clause(gorm 真实构建流程)时的完整输出。
|
||||
func TestWhenMatchedClauseBuild(t *testing.T) {
|
||||
w := WhenMatched{
|
||||
Set: clause.Set{{Column: clause.Column{Name: "name"}, Value: "x"}},
|
||||
}
|
||||
|
||||
sql := buildClauseSQL(t, "WHEN MATCHED", w)
|
||||
for _, want := range []string{
|
||||
"WHEN MATCHED",
|
||||
"THEN UPDATE",
|
||||
"SET name=:1",
|
||||
} {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Errorf("WhenMatched SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenMatchedBuildEmptySet(t *testing.T) {
|
||||
var w WhenMatched
|
||||
|
||||
sql := buildSQL(t, w)
|
||||
if sql != "" {
|
||||
t.Errorf("WhenMatched with empty Set should generate no SQL, got %q", sql)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func (w WhenNotMatched) Build(builder clause.Builder) {
|
||||
|
||||
builder.WriteString(" THEN")
|
||||
builder.WriteString(" INSERT ")
|
||||
w.Build(builder)
|
||||
w.Values.Build(builder)
|
||||
|
||||
if len(w.Where.Exprs) > 0 {
|
||||
builder.WriteString(w.Where.Name())
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package clauses
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func TestWhenNotMatchedName(t *testing.T) {
|
||||
var w WhenNotMatched
|
||||
if got := w.Name(); got != "WHEN NOT MATCHED" {
|
||||
t.Errorf("WhenNotMatched.Name() = %q, want %q", got, "WHEN NOT MATCHED")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenNotMatchedBuild(t *testing.T) {
|
||||
w := WhenNotMatched{
|
||||
Values: clause.Values{
|
||||
Columns: []clause.Column{{Name: "name"}, {Name: "age"}},
|
||||
Values: [][]interface{}{{"x", 1}},
|
||||
},
|
||||
}
|
||||
|
||||
sql := buildClauseSQL(t, "WHEN NOT MATCHED", w)
|
||||
// 期望: WHEN NOT MATCHED THEN INSERT (name,age) VALUES (:1,:2)
|
||||
for _, want := range []string{
|
||||
"WHEN NOT MATCHED",
|
||||
"THEN INSERT",
|
||||
"(name,age)", // 列名
|
||||
"VALUES (:1,:2)", // VALUES 绑定参数
|
||||
} {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Errorf("WhenNotMatched SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenNotMatchedBuildWithWhere(t *testing.T) {
|
||||
w := WhenNotMatched{
|
||||
Values: clause.Values{
|
||||
Columns: []clause.Column{{Name: "name"}},
|
||||
Values: [][]interface{}{{"x"}},
|
||||
},
|
||||
Where: clause.Where{Exprs: []clause.Expression{
|
||||
clause.Eq{Column: clause.Column{Name: "deleted"}, Value: 0},
|
||||
}},
|
||||
}
|
||||
|
||||
sql := buildSQL(t, w)
|
||||
for _, want := range []string{
|
||||
"THEN INSERT",
|
||||
"(name)",
|
||||
"VALUES (:1)",
|
||||
"WHERE deleted = :2",
|
||||
} {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Errorf("WhenNotMatched SQL %q does not contain %q", sql, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhenNotMatchedBuildEmpty(t *testing.T) {
|
||||
var w WhenNotMatched
|
||||
|
||||
sql := buildSQL(t, w)
|
||||
if sql != "" {
|
||||
t.Errorf("WhenNotMatched with empty Columns should generate no SQL, got %q", sql)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWhenNotMatchedBuildPanicsOnMultipleRows 验证多行插入时按 Oracle 限制 panic。
|
||||
func TestWhenNotMatchedBuildPanicsOnMultipleRows(t *testing.T) {
|
||||
w := WhenNotMatched{
|
||||
Values: clause.Values{
|
||||
Columns: []clause.Column{{Name: "name"}},
|
||||
Values: [][]interface{}{{"x"}, {"y"}},
|
||||
},
|
||||
}
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatal("expected panic for multiple insert rows")
|
||||
}
|
||||
msg, ok := r.(string)
|
||||
if !ok || !strings.Contains(msg, "cannot insert more than one rows") {
|
||||
t.Errorf("unexpected panic message: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
buildSQL(t, w)
|
||||
}
|
||||
Reference in New Issue
Block a user