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

455 lines
12 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 (
"context"
"database/sql"
"fmt"
"regexp"
"strconv"
"strings"
"gorm.io/gorm/utils"
// _ "github.com/godror/godror"
_ "github.com/sijms/go-ora/v2"
"github.com/thoas/go-funk"
"gorm.io/gorm"
"gorm.io/gorm/callbacks"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"gorm.io/gorm/migrator"
"gorm.io/gorm/schema"
"git.charlienet.top/go/oracle/driver_adapter"
)
const RowNumberAliasForOracle11 = "ROW_NUM"
// Oracle 版本主版本号常量(对应各版本引入的数据库特性)
const (
OracleVersion10 = 10 // Oracle 10g
OracleVersion11 = 11 // Oracle 11g(不含 IDENTITY 列、OFFSET/FETCH 分页)
OracleVersion12 = 12 // Oracle 12c(引入 IDENTITY 列、OFFSET/FETCH 分页;12.1 起支持 Extended 32k VARCHAR2
OracleVersion18 = 18 // Oracle 18c12.2 的再版)
OracleVersion19 = 19 // Oracle 19c
OracleVersion21 = 21 // Oracle 21c(引入原生 BOOLEAN 列类型)
OracleVersion23 = 23 // Oracle 23ai(引入 VECTOR 类型)
)
// oracleMajor 返回数据库版本的主版本号;解析失败返回 0。
// 支持格式如 "11.2.0.4.0"、"19.0.0.0.0"、"23.0.0.0.0"。
func oracleMajor(dbVer string) int {
major, _ := strconv.Atoi(strings.Split(dbVer, ".")[0])
return major
}
// supportsIdentity 是否支持 IDENTITY 列(12c+ 支持 GENERATED ... AS IDENTITY
// 11g 及以下需用序列 + BEFORE INSERT 触发器模拟自增)
func supportsIdentity(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion12 }
// supportsFetchOffset 是否支持 OFFSET/FETCH 分页语法(12c+ 引入;
// 11g 需改写为 ROWNUM 分页)
func supportsFetchOffset(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion12 }
// supportsNativeBoolean 是否支持原生 BOOLEAN 列类型(21c+ 引入;
// 更早版本需用 NUMBER(1) 模拟)
func supportsNativeBoolean(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion21 }
// supportsExtendedString 是否支持 Extended 32k VARCHAR212.2+ 默认开启;
// 12.1 需 MAX_STRING_SIZE=EXTENDED)。保守判定:主版本 >= 12 视为可能支持,
// 具体是否生效依赖数据库参数。
func supportsExtendedString(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion12 }
// supportsVector 是否支持 VECTOR 类型(23ai 引入,用于 AI Vector Search
func supportsVector(dbVer string) bool { return oracleMajor(dbVer) >= OracleVersion23 }
// isOracle11g 判断当前数据库是否低于 12c(11g 及以下不支持 IDENTITY 列)。
// 保留该函数以兼容既有调用,内部委托 supportsIdentity 取反。
func isOracle11g(dbVer string) bool {
return !supportsIdentity(dbVer)
}
type Config struct {
DriverName string
DSN string
Conn gorm.ConnPool //*sql.DB
DefaultStringSize uint
DBName string
DBVer string
DriverType driver_adapter.DriverType // 新增:驱动类型(go-ora 或 godror
SkipQuoteIdentifiers bool // 新增:是否跳过标识符引用
}
type Dialector struct {
*Config
}
func Open(dsn string) gorm.Dialector {
return &Dialector{Config: &Config{DSN: dsn}}
}
func New(config Config) gorm.Dialector {
return &Dialector{Config: &config}
}
func (d Dialector) DummyTableName() string {
return "DUAL"
}
func (d Dialector) Name() string {
return "oracle"
}
func (d Dialector) Initialize(db *gorm.DB) (err error) {
db.NamingStrategy = Namer{
NamingStrategy: db.NamingStrategy,
DBName: d.DBName,
}
d.DefaultStringSize = 1024
// register callbacks
//callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{WithReturning: true})
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT", "RETURNING"},
UpdateClauses: []string{"UPDATE", "SET", "WHERE", "RETURNING"},
DeleteClauses: []string{"DELETE", "FROM", "WHERE", "RETURNING"},
})
// d.DriverName = "godror"
d.DriverName = "oracle"
// godror.Batch
if d.Conn != nil {
db.ConnPool = d.Conn
} else {
db.ConnPool, err = sql.Open(d.DriverName, d.DSN)
if err != nil {
return
}
}
err = db.ConnPool.QueryRowContext(context.Background(), "select version from product_component_version where rownum = 1").Scan(&d.DBVer)
if err != nil {
return err
}
if err = db.Callback().Create().Replace("gorm:create", Create); err != nil {
return
}
// 注册 Update 回调
if err = db.Callback().Update().Replace("gorm:update", Update); err != nil {
return
}
// 注册 Delete 回调
if err = db.Callback().Delete().Replace("gorm:delete", Delete); err != nil {
return
}
// 注册 Query 回调
if err = db.Callback().Query().Replace("gorm:query", Query); err != nil {
return
}
for k, v := range d.ClauseBuilders() {
db.ClauseBuilders[k] = v
}
return
}
func (d Dialector) ClauseBuilders() map[string]clause.ClauseBuilder {
dbver, _ := strconv.Atoi(strings.Split(d.DBVer, ".")[0])
if dbver > 0 && dbver < 12 {
return map[string]clause.ClauseBuilder{
"LIMIT": d.RewriteLimit11,
}
} else {
return map[string]clause.ClauseBuilder{
"LIMIT": d.RewriteLimit,
}
}
}
func (d Dialector) RewriteLimit(c clause.Clause, builder clause.Builder) {
if limit, ok := c.Expression.(clause.Limit); ok {
if stmt, ok := builder.(*gorm.Statement); ok {
if _, ok := stmt.Clauses["ORDER BY"]; !ok {
s := stmt.Schema
builder.WriteString("ORDER BY ")
if s != nil && s.PrioritizedPrimaryField != nil {
builder.WriteQuoted(s.PrioritizedPrimaryField.DBName)
builder.WriteByte(' ')
} else {
builder.WriteString("(SELECT NULL FROM ")
builder.WriteString(d.DummyTableName())
builder.WriteString(")")
}
}
}
if offset := limit.Offset; offset > 0 {
builder.WriteString(" OFFSET ")
builder.WriteString(strconv.Itoa(offset))
builder.WriteString(" ROWS")
}
v := 0
if limit.Limit != nil {
v = *limit.Limit
}
if v > 0 {
builder.WriteString(" FETCH NEXT ")
builder.WriteString(strconv.Itoa(v))
builder.WriteString(" ROWS ONLY")
}
}
}
// Oracle11 Limit
func (d Dialector) RewriteLimit11(c clause.Clause, builder clause.Builder) {
limit, ok := c.Expression.(clause.Limit)
if !ok {
return
}
offsetRows := limit.Offset
hasOffset := offsetRows > 0
limitRows, hasLimit := d.getLimitRows(limit)
if !hasOffset && !hasLimit {
return
}
var stmt *gorm.Statement
if stmt, ok = builder.(*gorm.Statement); !ok {
return
}
if hasLimit && hasOffset {
subQuerySQL := fmt.Sprintf(
"SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY %s) AS %s FROM (%s) T) WHERE %s BETWEEN %d AND %d",
d.getOrderByColumns(stmt),
RowNumberAliasForOracle11,
strings.TrimSpace(stmt.SQL.String()),
RowNumberAliasForOracle11,
offsetRows+1,
offsetRows+limitRows,
)
stmt.SQL.Reset()
stmt.SQL.WriteString(subQuerySQL)
} else if hasLimit {
// 只有 Limit 的情况
subQuerySQL := fmt.Sprintf(
"SELECT * FROM (%s) WHERE ROWNUM <= %d",
strings.TrimSpace(stmt.SQL.String()),
limitRows,
)
// d.rewriteRownumStmt(stmt, builder, " <= ", limitRows)
stmt.SQL.Reset()
stmt.SQL.WriteString(subQuerySQL)
} else {
// 只有 Offset 的情况
// 偏移后取剩余所有记录
subQuerySQL := fmt.Sprintf(
"SELECT * FROM (SELECT T.*, ROW_NUMBER() OVER (ORDER BY %s) AS %s FROM (%s) T) WHERE %s > %d",
d.getOrderByColumns(stmt),
RowNumberAliasForOracle11,
strings.TrimSpace(stmt.SQL.String()),
RowNumberAliasForOracle11,
offsetRows+1,
)
stmt.SQL.Reset()
stmt.SQL.WriteString(subQuerySQL)
// d.rewriteRownumStmt(stmt, builder, " > ", offsetRows)
}
}
func (d Dialector) getOrderByColumns(stmt *gorm.Statement) string {
if orderByClause, ok := stmt.Clauses["ORDER BY"]; ok {
var orderBy clause.OrderBy
if orderBy, ok = orderByClause.Expression.(clause.OrderBy); ok && len(orderBy.Columns) > 0 {
orderByBuilder := strings.Builder{}
for i, column := range orderBy.Columns {
if i > 0 {
orderByBuilder.WriteString(", ")
}
orderByBuilder.WriteString(column.Column.Name)
if column.Desc {
orderByBuilder.WriteString(" DESC")
}
}
return orderByBuilder.String()
}
}
return "NULL"
}
func (d Dialector) getLimitRows(limit clause.Limit) (limitRows int, hasLimit bool) {
if l := limit.Limit; l != nil {
limitRows = *l
hasLimit = limitRows > 0
}
return
}
func (d Dialector) DefaultValueOf(*schema.Field) clause.Expression {
return clause.Expr{SQL: "VALUES (DEFAULT)"}
}
func (d Dialector) Migrator(db *gorm.DB) gorm.Migrator {
return Migrator{
Migrator: migrator.Migrator{
Config: migrator.Config{
DB: db,
Dialector: d,
CreateIndexAfterCreateTable: true,
},
},
}
}
func (d Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
writer.WriteString(":")
writer.WriteString(strconv.Itoa(len(stmt.Vars)))
}
func (d Dialector) QuoteTo(writer clause.Writer, str string) {
if d.SkipQuoteIdentifiers {
writer.WriteString(str)
return
}
if str != "" && IsReservedWord(str) {
writer.WriteByte('"')
writer.WriteString(str)
writer.WriteByte('"')
} else {
writer.WriteString(str)
}
}
var numericPlaceholder = regexp.MustCompile(`:(\d+)`)
func (d Dialector) Explain(sql string, vars ...interface{}) string {
return logger.ExplainSQL(sql, numericPlaceholder, `'`, funk.Map(vars, func(v interface{}) interface{} {
switch v := v.(type) {
case bool:
if v {
return 1
}
return 0
default:
return v
}
}).([]interface{})...)
}
func (d Dialector) DataTypeOf(field *schema.Field) string {
delete(field.TagSettings, "RESTRICT")
var sqlType string
switch field.DataType {
case schema.Bool:
// Oracle 21c+ 支持原生 BOOLEAN 列;更早版本用 NUMBER(1) 模拟
if supportsNativeBoolean(d.DBVer) {
sqlType = "BOOLEAN"
} else {
sqlType = "NUMBER(1)"
}
case schema.Int, schema.Uint:
sqlType = "INTEGER"
if field.Size <= 8 {
sqlType = "SMALLINT"
}
// Oracle 12c+ 支持 IDENTITY 列;Oracle 11g 需要在迁移时创建序列 + 触发器
if field.AutoIncrement && supportsIdentity(d.DBVer) {
sqlType += " GENERATED BY DEFAULT AS IDENTITY"
}
case schema.Float:
sqlType = "FLOAT"
if val, ok := field.TagSettings["AUTOINCREMENT"]; ok && utils.CheckTruth(val) {
sqlType += " GENERATED BY DEFAULT AS IDENTITY"
}
case schema.String, "VARCHAR2":
size := field.Size
defaultSize := d.DefaultStringSize
if size == 0 {
if defaultSize > 0 {
size = int(defaultSize)
} else {
hasIndex := field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUE"] != ""
// TEXT, GEOMETRY or JSON column can't have a default value
if field.PrimaryKey || field.HasDefaultValue || hasIndex {
size = 191 // utf8mb4
}
}
}
// Oracle 12c+Extended)支持最长 32767 字节的 VARCHAR232k 特性);
// 11g 及未开启 Extended 的库超过 4000 必须用 CLOB。
// 保守策略:
// - size 在 2000~4000 之间维持 CLOB 不变(保持历史行为)
// - size > 4000 且版本 >= 12 → VARCHAR2(size)(利用 32k 特性)
// - size > 4000 且 11g → CLOB(保持现状)
if size > 4000 {
if supportsExtendedString(d.DBVer) {
sqlType = fmt.Sprintf("VARCHAR2(%d)", size)
} else {
sqlType = "CLOB"
}
} else if size >= 2000 {
sqlType = "CLOB"
} else {
sqlType = fmt.Sprintf("VARCHAR2(%d)", size)
}
case schema.Time:
if field.Precision > 0 {
sqlType = fmt.Sprintf("TIMESTAMP(%d) WITH TIME ZONE", field.Precision)
} else {
sqlType = "TIMESTAMP WITH TIME ZONE"
}
case schema.Bytes:
sqlType = "BLOB"
default:
sqlType = string(field.DataType)
if strings.EqualFold(sqlType, "text") {
sqlType = "CLOB"
}
if sqlType == "" {
panic(fmt.Sprintf("invalid sql type %s (%s) for oracle", field.FieldType.Name(), field.FieldType.String()))
}
}
return sqlType
}
func (d Dialector) SavePoint(tx *gorm.DB, name string) error {
tx.Exec("SAVEPOINT " + name)
return tx.Error
}
func (d Dialector) RollbackTo(tx *gorm.DB, name string) error {
tx.Exec("ROLLBACK TO SAVEPOINT " + name)
return tx.Error
}
func (d Dialector) GetAdapter() driver_adapter.Adapter {
if d.DriverType == "" {
d.DriverType = driver_adapter.DriverGoOra
}
return driver_adapter.Get(d.DriverType)
}