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
+132
View File
@@ -0,0 +1,132 @@
// Package driver_adapter 提供 Oracle 驱动抽象层
// 支持 go-ora 和 godror 两种底层驱动的切换
package driver_adapter
import (
"context"
"database/sql"
)
// DriverType 驱动类型枚举
type DriverType string
const (
// DriverGoOra 使用纯 Go 实现的 go-ora 驱动
DriverGoOra DriverType = "go-ora"
// DriverGodror 使用基于 ODPI-C 的 godror 驱动
DriverGodror DriverType = "godror"
)
// OutParam 输出参数接口,用于 RETURNING INTO 子句
type OutParam interface {
// GetDest 返回目标指针
GetDest() interface{}
// SetSize 设置缓冲区大小(用于字符串类型)
SetSize(size int)
// GetSize 获取缓冲区大小
GetSize() int
}
// LobData LOB 数据接口
type LobData interface {
// IsCLOB 是否为 CLOB 类型
IsCLOB() bool
// IsBLOB 是否为 BLOB 类型
IsBLOB() bool
// GetString 获取字符串值(CLOB
GetString() string
// GetBytes 获取字节值(BLOB
GetBytes() []byte
// IsValid 是否有效
IsValid() bool
}
// BatchData 批量数据接口
type BatchData interface {
// Len 返回数据长度
Len() int
// GetValues 返回所有值
GetValues() []interface{}
}
// Adapter 驱动适配器接口
// 封装了不同 Oracle 驱动的差异,提供统一的 API
type Adapter interface {
// Name 返回驱动名称
Name() string
// Type 返回驱动类型
Type() DriverType
// Open 打开数据库连接
Open(dsn string) (*sql.DB, error)
// CreateOutParam 创建输出参数(用于 RETURNING INTO
// dest: 目标指针
// size: 缓冲区大小(字符串类型需要)
CreateOutParam(dest interface{}, size int) OutParam
// CreateClob 创建 CLOB 数据
CreateClob(value string) LobData
// CreateBlob 创建 BLOB 数据
CreateBlob(value []byte) LobData
// CreateBatch 创建批量数据
CreateBatch(values []interface{}) BatchData
// NeedsSizeForOut 返回输出参数是否需要指定 Size
// go-ora 对字符串类型的 Out 参数需要指定 Size
// godror 通常不需要
NeedsSizeForOut() bool
// SupportsReturningMultiRow 返回是否支持多行 RETURNING
// go-ora 不支持批量 INSERT + RETURNING
// godror 支持
SupportsReturningMultiRow() bool
// SupportsBulkCopy 返回是否支持 BulkCopy
SupportsBulkCopy() bool
// WrapClobForInsert 包装 CLOB 值用于插入
// 某些驱动需要特殊包装
WrapClobForInsert(value string) interface{}
// WrapBlobForInsert 包装 BLOB 值用于插入
WrapBlobForInsert(value []byte) interface{}
// UnwrapQueryResult 解包查询结果
// 将驱动特定的类型转换为标准 Go 类型
UnwrapQueryResult(value interface{}, typeName string) interface{}
// GetConnection 获取底层连接(用于高级操作)
GetConnection(db *sql.DB) (interface{}, error)
// Ping 检查连接是否可用
Ping(ctx context.Context, db *sql.DB) error
}
// Registry 驱动适配器注册表
var registry = map[DriverType]func() Adapter{}
// Register 注册驱动适配器
func Register(driverType DriverType, factory func() Adapter) {
registry[driverType] = factory
}
// Get 获取驱动适配器
func Get(driverType DriverType) Adapter {
if factory, ok := registry[driverType]; ok {
return factory()
}
return nil
}
// ListDrivers 列出所有已注册的驱动
func ListDrivers() []DriverType {
types := make([]DriverType, 0, len(registry))
for t := range registry {
types = append(types, t)
}
return types
}
+74
View File
@@ -0,0 +1,74 @@
package driver_adapter
import "testing"
// TestRegistryRegisterAndGet 验证 Register 后 Get 能返回对应适配器
func TestRegistryRegisterAndGet(t *testing.T) {
const key DriverType = "test-registry-driver"
callCount := 0
Register(key, func() Adapter {
callCount++
return &GoOraAdapter{}
})
defer delete(registry, key)
adapter := Get(key)
if adapter == nil {
t.Fatalf("Get(%q) 返回 nil,期望返回已注册的适配器", key)
}
if _, ok := adapter.(*GoOraAdapter); !ok {
t.Fatalf("Get(%q) 返回类型 = %T,期望 *GoOraAdapter", key, adapter)
}
if callCount != 1 {
t.Errorf("factory 调用次数 = %d,期望 1", callCount)
}
// 每次 Get 都应调用 factory 返回新实例
if Get(key) == nil {
t.Error("第二次 Get 返回 nil")
}
if callCount != 2 {
t.Errorf("factory 调用次数 = %d,期望 2(每次 Get 都应调用 factory", callCount)
}
}
// TestRegistryGetUnknown 验证 Get 未注册的 DriverType 返回 nil
func TestRegistryGetUnknown(t *testing.T) {
if adapter := Get(DriverType("no-such-driver")); adapter != nil {
t.Fatalf("Get(未注册类型) 返回 %v,期望 nil", adapter)
}
}
// TestRegistryListDrivers 验证 ListDrivers 返回包含 DriverGoOra
func TestRegistryListDrivers(t *testing.T) {
drivers := ListDrivers()
if len(drivers) == 0 {
t.Fatal("ListDrivers() 返回空列表")
}
for _, d := range drivers {
if d == DriverGoOra {
return
}
}
t.Errorf("ListDrivers() = %v,应包含 DriverGoOra", drivers)
}
// TestRegistryGodrorNotCompiled 验证默认构建(无 godror build tag)下不包含 DriverGodror
func TestRegistryGodrorNotCompiled(t *testing.T) {
for _, d := range ListDrivers() {
if d == DriverGodror {
t.Errorf("ListDrivers() = %v,默认构建不应包含 DriverGodrorgodror.go 带有 //go:build godror 标签)", d)
}
}
}
// TestDriverTypeConstants 验证驱动类型常量值
func TestDriverTypeConstants(t *testing.T) {
if DriverGoOra != "go-ora" {
t.Errorf("DriverGoOra = %q,期望 %q", DriverGoOra, "go-ora")
}
if DriverGodror != "godror" {
t.Errorf("DriverGodror = %q,期望 %q", DriverGodror, "godror")
}
}
+197
View File
@@ -0,0 +1,197 @@
//go:build godror
package driver_adapter
import (
"context"
"database/sql"
"fmt"
)
// GodrorAdapter godror 驱动适配器
type GodrorAdapter struct{}
// godrorOutParam godror 输出参数包装
type godrorOutParam struct {
dest interface{}
size int
}
func (p *godrorOutParam) GetDest() interface{} {
return p.dest
}
func (p *godrorOutParam) SetSize(size int) {
p.size = size
}
func (p *godrorOutParam) GetSize() int {
return p.size
}
// godrorLobData godror LOB 数据包装
type godrorLobData struct {
isClob bool
strVal string
byteVal []byte
valid bool
}
func (l *godrorLobData) IsCLOB() bool {
return l.isClob
}
func (l *godrorLobData) IsBLOB() bool {
return !l.isClob
}
func (l *godrorLobData) GetString() string {
return l.strVal
}
func (l *godrorLobData) GetBytes() []byte {
return l.byteVal
}
func (l *godrorLobData) IsValid() bool {
return l.valid
}
// godrorBatchData godror 批量数据包装
type godrorBatchData struct {
values []interface{}
}
func (b *godrorBatchData) Len() int {
return len(b.values)
}
func (b *godrorBatchData) GetValues() []interface{} {
return b.values
}
// Name 返回驱动名称
func (a *GodrorAdapter) Name() string {
return "godror"
}
// Type 返回驱动类型
func (a *GodrorAdapter) Type() DriverType {
return DriverGodror
}
// Open 打开数据库连接
func (a *GodrorAdapter) Open(dsn string) (*sql.DB, error) {
db, err := sql.Open("godror", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open connection with godror: %w", err)
}
return db, nil
}
// CreateOutParam 创建输出参数(用于 RETURNING INTO
func (a *GodrorAdapter) CreateOutParam(dest interface{}, size int) OutParam {
return &godrorOutParam{
dest: dest,
size: size,
}
}
// CreateClob 创建 CLOB 数据
func (a *GodrorAdapter) CreateClob(value string) LobData {
return &godrorLobData{
isClob: true,
strVal: value,
valid: true,
byteVal: nil,
}
}
// CreateBlob 创建 BLOB 数据
func (a *GodrorAdapter) CreateBlob(value []byte) LobData {
return &godrorLobData{
isClob: false,
byteVal: value,
valid: true,
strVal: "",
}
}
// CreateBatch 创建批量数据
func (a *GodrorAdapter) CreateBatch(values []interface{}) BatchData {
return &godrorBatchData{
values: values,
}
}
// NeedsSizeForOut 返回输出参数是否需要指定 Size
func (a *GodrorAdapter) NeedsSizeForOut() bool {
return false
}
// SupportsReturningMultiRow 返回是否支持多行 RETURNING
func (a *GodrorAdapter) SupportsReturningMultiRow() bool {
return true
}
// SupportsBulkCopy 返回是否支持 BulkCopy
func (a *GodrorAdapter) SupportsBulkCopy() bool {
return false
}
// WrapClobForInsert 包装 CLOB 值用于插入
func (a *GodrorAdapter) WrapClobForInsert(value string) interface{} {
// godror 可以直接处理字符串作为 CLOB
return value
}
// WrapBlobForInsert 包装 BLOB 值用于插入
func (a *GodrorAdapter) WrapBlobForInsert(value []byte) interface{} {
// godror 可以直接处理字节数组作为 BLOB
return value
}
// UnwrapQueryResult 解包查询结果
func (a *GodrorAdapter) UnwrapQueryResult(value interface{}, typeName string) interface{} {
// godror 通常返回标准 Go 类型,无需特殊处理
// 但如果遇到特定类型,可以在这里进行转换
switch v := value.(type) {
case *string:
if v == nil {
return nil
}
return *v
case *[]byte:
if v == nil {
return nil
}
return *v
default:
return value
}
}
// GetConnection 获取底层连接(用于高级操作)
func (a *GodrorAdapter) GetConnection(db *sql.DB) (interface{}, error) {
// 从 sql.DB 获取原始连接
conn, err := db.Conn(context.Background())
if err != nil {
return nil, err
}
defer conn.Close()
// 返回原始连接
return conn, nil
}
// Ping 检查连接是否可用
func (a *GodrorAdapter) Ping(ctx context.Context, db *sql.DB) error {
return db.PingContext(ctx)
}
// init 注册 godror 驱动适配器
func init() {
Register(DriverGodror, func() Adapter {
return &GodrorAdapter{}
})
}
+190
View File
@@ -0,0 +1,190 @@
// Package driver_adapter 提供 Oracle 驱动抽象层
// 支持 go-ora 和 godror 两种底层驱动的切换
package driver_adapter
import (
"context"
"database/sql"
go_ora "github.com/sijms/go-ora/v2"
)
// GoOraAdapter go-ora 驱动适配器
type GoOraAdapter struct{}
// goOraOutParam 包装 go_ora.Out
type goOraOutParam struct {
out go_ora.Out
}
// GetDest 返回目标指针
func (o *goOraOutParam) GetDest() interface{} {
return o.out.Dest
}
// SetSize 设置缓冲区大小(用于字符串类型)
func (o *goOraOutParam) SetSize(size int) {
o.out.Size = size
}
// GetSize 获取缓冲区大小
func (o *goOraOutParam) GetSize() int {
return o.out.Size
}
// goOraLobData 包装 go_ora.Clob 和 go_ora.Blob
type goOraLobData struct {
isClob bool
strVal string
byteVal []byte
valid bool
}
// IsCLOB 是否为 CLOB 类型
func (l *goOraLobData) IsCLOB() bool {
return l.isClob
}
// IsBLOB 是否为 BLOB 类型
func (l *goOraLobData) IsBLOB() bool {
return !l.isClob
}
// GetString 获取字符串值(CLOB
func (l *goOraLobData) GetString() string {
return l.strVal
}
// GetBytes 获取字节值(BLOB
func (l *goOraLobData) GetBytes() []byte {
return l.byteVal
}
// IsValid 是否有效
func (l *goOraLobData) IsValid() bool {
return l.valid
}
// goOraBatchData 包装批量数据
type goOraBatchData struct {
values []interface{}
}
// Len 返回数据长度
func (b *goOraBatchData) Len() int {
return len(b.values)
}
// GetValues 返回所有值
func (b *goOraBatchData) GetValues() []interface{} {
return b.values
}
// Name 返回驱动名称
func (a *GoOraAdapter) Name() string {
return "go-ora"
}
// Type 返回驱动类型
func (a *GoOraAdapter) Type() DriverType {
return DriverGoOra
}
// Open 打开数据库连接
func (a *GoOraAdapter) Open(dsn string) (*sql.DB, error) {
return sql.Open("oracle", dsn)
}
// CreateOutParam 创建输出参数(用于 RETURNING INTO
func (a *GoOraAdapter) CreateOutParam(dest interface{}, size int) OutParam {
return &goOraOutParam{
out: go_ora.Out{Dest: dest, Size: size},
}
}
// CreateClob 创建 CLOB 数据
func (a *GoOraAdapter) CreateClob(value string) LobData {
return &goOraLobData{
isClob: true,
strVal: value,
byteVal: nil,
valid: true,
}
}
// CreateBlob 创建 BLOB 数据
func (a *GoOraAdapter) CreateBlob(value []byte) LobData {
return &goOraLobData{
isClob: false,
strVal: "",
byteVal: value,
valid: true,
}
}
// CreateBatch 创建批量数据
func (a *GoOraAdapter) CreateBatch(values []interface{}) BatchData {
return &goOraBatchData{
values: values,
}
}
// NeedsSizeForOut 返回输出参数是否需要指定 Size
func (a *GoOraAdapter) NeedsSizeForOut() bool {
return true
}
// SupportsReturningMultiRow 返回是否支持多行 RETURNING
func (a *GoOraAdapter) SupportsReturningMultiRow() bool {
return false
}
// SupportsBulkCopy 返回是否支持 BulkCopy
func (a *GoOraAdapter) SupportsBulkCopy() bool {
return true
}
// WrapClobForInsert 包装 CLOB 值用于插入
func (a *GoOraAdapter) WrapClobForInsert(value string) interface{} {
return go_ora.Clob{String: value, Valid: true}
}
// WrapBlobForInsert 包装 BLOB 值用于插入
func (a *GoOraAdapter) WrapBlobForInsert(value []byte) interface{} {
return go_ora.Blob{Data: value, Valid: true}
}
// UnwrapQueryResult 解包查询结果
func (a *GoOraAdapter) UnwrapQueryResult(value interface{}, typeName string) interface{} {
// 根据需要处理 go-ora 特有的返回类型转换
// 这里简单返回原始值,可根据实际需求扩展
return value
}
// GetConnection 获取底层连接(用于高级操作)
func (a *GoOraAdapter) GetConnection(db *sql.DB) (interface{}, error) {
conn, err := db.Conn(context.Background())
if err != nil {
return nil, err
}
defer conn.Close()
var rawConn interface{}
err = conn.Raw(func(driverConn interface{}) error {
rawConn = driverConn
return nil
})
return rawConn, err
}
// Ping 检查连接是否可用
func (a *GoOraAdapter) Ping(ctx context.Context, db *sql.DB) error {
return db.PingContext(ctx)
}
// init 函数中注册驱动
func init() {
Register(DriverGoOra, func() Adapter {
return &GoOraAdapter{}
})
}
+243
View File
@@ -0,0 +1,243 @@
package driver_adapter
import (
"context"
"reflect"
"testing"
"time"
go_ora "github.com/sijms/go-ora/v2"
)
// invalidDSN 用于无需真实连接的场景:
// 空字符串在 go-ora 的 dsn 解析阶段即返回错误,避免触发真实 TCP 连接导致测试挂起
const invalidDSN = ""
// newTestAdapter 创建 GoOraAdapter 测试实例
func newTestAdapter() *GoOraAdapter {
return &GoOraAdapter{}
}
// TestGoOraAdapterBasics 验证适配器基本属性
func TestGoOraAdapterBasics(t *testing.T) {
a := newTestAdapter()
if got := a.Name(); got != "go-ora" {
t.Errorf("Name() = %q,期望 %q", got, "go-ora")
}
if got := a.Type(); got != DriverGoOra {
t.Errorf("Type() = %q,期望 %q", got, DriverGoOra)
}
if !a.NeedsSizeForOut() {
t.Error("NeedsSizeForOut() = false,期望 true")
}
if a.SupportsReturningMultiRow() {
t.Error("SupportsReturningMultiRow() = true,期望 false")
}
if !a.SupportsBulkCopy() {
t.Error("SupportsBulkCopy() = false,期望 true")
}
}
// TestGoOraCreateOutParam 验证输出参数创建
func TestGoOraCreateOutParam(t *testing.T) {
a := newTestAdapter()
var id int
out := a.CreateOutParam(&id, 100)
if out == nil {
t.Fatal("CreateOutParam 返回 nil")
}
if got := out.GetDest(); got != &id {
t.Errorf("GetDest() = %v,期望 %v", got, &id)
}
if got := out.GetSize(); got != 100 {
t.Errorf("GetSize() = %d,期望 100", got)
}
out.SetSize(200)
if got := out.GetSize(); got != 200 {
t.Errorf("SetSize 后 GetSize() = %d,期望 200", got)
}
if _, ok := out.(*goOraOutParam); !ok {
t.Errorf("返回类型 = %T,期望 *goOraOutParam", out)
}
}
// TestGoOraCreateClob 验证 CLOB 创建
func TestGoOraCreateClob(t *testing.T) {
a := newTestAdapter()
lob := a.CreateClob("text")
if lob == nil {
t.Fatal("CreateClob 返回 nil")
}
if !lob.IsCLOB() {
t.Error("IsCLOB() = false,期望 true")
}
if lob.IsBLOB() {
t.Error("IsBLOB() = true,期望 false")
}
if got := lob.GetString(); got != "text" {
t.Errorf("GetString() = %q,期望 %q", got, "text")
}
if !lob.IsValid() {
t.Error("IsValid() = false,期望 true")
}
if _, ok := lob.(*goOraLobData); !ok {
t.Errorf("返回类型 = %T,期望 *goOraLobData", lob)
}
}
// TestGoOraCreateBlob 验证 BLOB 创建
func TestGoOraCreateBlob(t *testing.T) {
a := newTestAdapter()
want := []byte{1, 2, 3}
lob := a.CreateBlob(want)
if lob == nil {
t.Fatal("CreateBlob 返回 nil")
}
if !lob.IsBLOB() {
t.Error("IsBLOB() = false,期望 true")
}
if lob.IsCLOB() {
t.Error("IsCLOB() = true,期望 false")
}
if got := lob.GetBytes(); !reflect.DeepEqual(got, want) {
t.Errorf("GetBytes() = %v,期望 %v", got, want)
}
if !lob.IsValid() {
t.Error("IsValid() = false,期望 true")
}
if _, ok := lob.(*goOraLobData); !ok {
t.Errorf("返回类型 = %T,期望 *goOraLobData", lob)
}
}
// TestGoOraCreateBatch 验证批量数据创建
func TestGoOraCreateBatch(t *testing.T) {
a := newTestAdapter()
want := []interface{}{1, "a"}
batch := a.CreateBatch(want)
if batch == nil {
t.Fatal("CreateBatch 返回 nil")
}
if got := batch.Len(); got != 2 {
t.Errorf("Len() = %d,期望 2", got)
}
got := batch.GetValues()
if len(got) != len(want) {
t.Fatalf("GetValues() 长度 = %d,期望 %d", len(got), len(want))
}
for i := range want {
if !reflect.DeepEqual(got[i], want[i]) {
t.Errorf("GetValues()[%d] = %v,期望 %v", i, got[i], want[i])
}
}
if _, ok := batch.(*goOraBatchData); !ok {
t.Errorf("返回类型 = %T,期望 *goOraBatchData", batch)
}
}
// TestGoOraWrapForInsert 验证插入包装
func TestGoOraWrapForInsert(t *testing.T) {
a := newTestAdapter()
// CLOB 包装
clob, ok := a.WrapClobForInsert("text").(go_ora.Clob)
if !ok {
t.Fatalf("WrapClobForInsert 返回类型 = %T,期望 go_ora.Clob", a.WrapClobForInsert("text"))
}
if clob.String != "text" {
t.Errorf("Clob.String = %q,期望 %q", clob.String, "text")
}
if !clob.Valid {
t.Error("Clob.Valid = false,期望 true")
}
// BLOB 包装
blob, ok := a.WrapBlobForInsert([]byte{1}).(go_ora.Blob)
if !ok {
t.Fatalf("WrapBlobForInsert 返回类型 = %T,期望 go_ora.Blob", a.WrapBlobForInsert([]byte{1}))
}
if !reflect.DeepEqual(blob.Data, []byte{1}) {
t.Errorf("Blob.Data = %v,期望 [1]", blob.Data)
}
if !blob.Valid {
t.Error("Blob.Valid = false,期望 true")
}
}
// TestGoOraOpen 验证 Open 只检查驱动名注册,不会真正建立连接
func TestGoOraOpen(t *testing.T) {
a := newTestAdapter()
db, err := a.Open("oracle://user:pass@localhost:1521/service")
if err != nil {
// sql.Open 只在驱动未注册时报错;go-ora 包的 init 已注册 "oracle" 驱动名
t.Fatalf("Open() 返回错误: %v(需要 go-ora 包 init 注册 \"oracle\" 驱动名)", err)
}
if db == nil {
t.Fatal("Open() 返回 db == nil")
}
defer db.Close()
}
// TestGoOraPing 验证对未连接数据库的 Ping 返回错误而非 panic
func TestGoOraPing(t *testing.T) {
a := newTestAdapter()
db, err := a.Open(invalidDSN)
if err != nil {
t.Fatalf("Open() 返回错误: %v", err)
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := a.Ping(ctx, db); err == nil {
t.Error("Ping(未连接数据库) 返回 nil,期望返回错误")
}
}
// TestGoOraUnwrapQueryResult 验证查询结果原样返回
func TestGoOraUnwrapQueryResult(t *testing.T) {
a := newTestAdapter()
cases := []interface{}{
42,
"hello",
[]byte{1, 2, 3},
3.14,
nil,
}
for _, in := range cases {
if got := a.UnwrapQueryResult(in, ""); !reflect.DeepEqual(got, in) {
t.Errorf("UnwrapQueryResult(%v, \"\") = %v,期望原样返回", in, got)
}
}
}
// TestGoOraGetConnection 验证获取底层连接不 panic(未连接数据库时返回错误即可)
func TestGoOraGetConnection(t *testing.T) {
a := newTestAdapter()
db, err := a.Open(invalidDSN)
if err != nil {
t.Fatalf("Open() 返回错误: %v", err)
}
defer db.Close()
raw, err := a.GetConnection(db)
if err != nil {
t.Logf("GetConnection 返回预期错误(未连接数据库): %v", err)
return
}
if raw == nil {
t.Error("GetConnection 返回 nil raw 且无错误")
}
}