Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ module github.com/oracle-samples/gorm-oracle

go 1.24.4

require gorm.io/gorm v1.30.0
require gorm.io/gorm v1.31.0

require github.com/godror/godror v0.49.0
require github.com/godror/godror v0.49.3

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/godror/knownpb v0.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gorm.io/datatypes v1.2.6 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
51 changes: 50 additions & 1 deletion oracle/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ package oracle

import (
"database/sql"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"

"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
Expand Down Expand Up @@ -174,6 +176,17 @@ func convertValue(val interface{}) interface{} {
}

switch v := val.(type) {
case json.RawMessage:
if v == nil {
return nil
}
return []byte(v)
case *json.RawMessage:
if v == nil {
return nil
}
b := []byte(*v)
return b
case bool:
if v {
return 1
Expand All @@ -198,7 +211,25 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
if isPtr {
targetType = targetType.Elem()
}

if field.FieldType == reflect.TypeOf(json.RawMessage{}) {
switch v := value.(type) {
case []byte:
return json.RawMessage(v) // from BLOB
case *[]byte:
if v == nil {
return json.RawMessage(nil)
}
return json.RawMessage(*v)
}
}
if isJSONField(field) {
switch v := value.(type) {
case string:
return datatypes.JSON([]byte(v))
case []byte:
return datatypes.JSON(v)
}
}
var converted interface{}

switch targetType {
Expand Down Expand Up @@ -276,6 +307,24 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
return converted
}

func isJSONField(f *schema.Field) bool {
_rawMsgT := reflect.TypeOf(json.RawMessage{})
_gormJSON := reflect.TypeOf(datatypes.JSON{})
if f == nil {
return false
}
ft := f.FieldType
return ft == _rawMsgT || ft == _gormJSON
}

func isRawMessageField(f *schema.Field) bool {
t := f.FieldType
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t == reflect.TypeOf(json.RawMessage(nil))
}

// Helper function to handle primitive type conversions
func convertPrimitiveType(value interface{}, targetType reflect.Type) interface{} {
switch targetType.Kind() {
Expand Down
60 changes: 51 additions & 9 deletions oracle/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,15 +507,37 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau
}
plsqlBuilder.WriteString("\n BULK COLLECT INTO l_affected_records;\n")

// Add OUT parameter population
// Add OUT parameter population (JSON serialized to CLOB)
outParamIndex := len(stmt.Vars)
for rowIdx := 0; rowIdx < len(createValues.Values); rowIdx++ {
for _, column := range allColumns {
if field := findFieldByDBName(schema, column); field != nil {
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).", rowIdx, outParamIndex+1, rowIdx+1))
db.QuoteTo(&plsqlBuilder, column)
plsqlBuilder.WriteString("; END IF;\n")
if isJSONField(field) {
if isRawMessageField(field) {
// Column is a BLOB, return raw bytes; no JSON_SERIALIZE
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new([]byte)})
plsqlBuilder.WriteString(fmt.Sprintf(
" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).",
rowIdx, outParamIndex+1, rowIdx+1,
))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString("; END IF;\n")
} else {
// datatypes.JSON (text-based) -> serialize to CLOB
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new(string)})
plsqlBuilder.WriteString(fmt.Sprintf(
" IF l_affected_records.COUNT > %d THEN :%d := JSON_SERIALIZE(l_affected_records(%d).",
rowIdx, outParamIndex+1, rowIdx+1,
))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString(" RETURNING CLOB); END IF;\n")
}
} else {
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).", rowIdx, outParamIndex+1, rowIdx+1))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString("; END IF;\n")
}
outParamIndex++
}
}
Expand Down Expand Up @@ -613,7 +635,7 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) {
}
plsqlBuilder.WriteString("\n BULK COLLECT INTO l_inserted_records;\n")

// Add OUT parameter population
// Add OUT parameter population (JSON serialized to CLOB)
outParamIndex := len(stmt.Vars)
for rowIdx := 0; rowIdx < len(createValues.Values); rowIdx++ {
for _, column := range allColumns {
Expand All @@ -622,9 +644,29 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) {
quotedColumn := columnBuilder.String()

if field := findFieldByDBName(schema, column); field != nil {
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn))
if isJSONField(field) {
if isRawMessageField(field) {
// Column is a BLOB, return raw bytes; no JSON_SERIALIZE
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new([]byte)})
plsqlBuilder.WriteString(fmt.Sprintf(
" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn,
))
} else {
// datatypes.JSON (text-based) -> serialize to CLOB
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new(string)})
plsqlBuilder.WriteString(fmt.Sprintf(
" IF l_inserted_records.COUNT > %d THEN :%d := JSON_SERIALIZE(l_inserted_records(%d).%s RETURNING CLOB); END IF;\n",
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn,
))
}
} else {
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
plsqlBuilder.WriteString(fmt.Sprintf(
" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn,
))
}
outParamIndex++
}
}
Expand Down
47 changes: 34 additions & 13 deletions oracle/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,27 +283,48 @@ func buildBulkDeletePLSQL(db *gorm.DB) {
}
plsqlBuilder.WriteString("\n BULK COLLECT INTO l_deleted_records;\n")

// Create OUT parameters for each field and each row that will be deleted
// Create OUT parameters for each field and each row that will be deleted (JSON-safe)
outParamIndex := len(stmt.Vars)
//TODO make it configurable
estimatedRows := 100 // Estimate maximum rows to delete
// keep your current fixed cap (same as other callbacks)
estimatedRows := 100

for rowIdx := 0; rowIdx < estimatedRows; rowIdx++ {
for _, column := range allColumns {
field := findFieldByDBName(schema, column)
if field != nil {
dest := createTypedDestination(field)
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})

plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_deleted_records(%d).", outParamIndex+1, rowIdx+1))
db.QuoteTo(&plsqlBuilder, column)
plsqlBuilder.WriteString(";\n")
plsqlBuilder.WriteString(" END IF;\n")
if field := findFieldByDBName(schema, column); field != nil {
if isJSONField(field) {
if isRawMessageField(field) {
// Column is a BLOB, return raw bytes; no JSON_SERIALIZE
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new([]byte)})
plsqlBuilder.WriteString(fmt.Sprintf(
" IF l_deleted_records.COUNT > %d THEN :%d := l_deleted_records(%d).",
rowIdx, outParamIndex+1, rowIdx+1,
))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString("; END IF;\n")
} else {
// JSON -> text bind
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new(string)})
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := JSON_SERIALIZE(l_deleted_records(%d).", outParamIndex+1, rowIdx+1))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString(" RETURNING CLOB);\n")
plsqlBuilder.WriteString(" END IF;\n")
}
} else {
// non-JSON as before
dest := createTypedDestination(field)
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_deleted_records(%d).", outParamIndex+1, rowIdx+1))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString(";\n")
plsqlBuilder.WriteString(" END IF;\n")
}
outParamIndex++
}
}
}

plsqlBuilder.WriteString("END;")

stmt.SQL.Reset()
Expand Down
41 changes: 33 additions & 8 deletions oracle/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,18 @@ func buildUpdatePLSQL(db *gorm.DB) {
for _, column := range allColumns {
field := findFieldByDBName(schema, column)
if field != nil {
dest := createTypedDestination(field)
var dest interface{}
if isJSONField(field) {
if isRawMessageField(field) {
// RawMessage -> BLOB -> []byte
dest = new([]byte)
} else {
// datatypes.JSON -> text -> string (CLOB)
dest = new(string)
}
} else {
dest = createTypedDestination(field)
}
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
}
}
Expand All @@ -553,18 +564,32 @@ func buildUpdatePLSQL(db *gorm.DB) {
for colIdx, column := range allColumns {
field := findFieldByDBName(schema, column)
if field != nil {
// Calculate the correct parameter index (1-based for Oracle)
paramIndex := outParamStartIndex + (rowIdx * len(allColumns)) + colIdx + 1

// Add the assignment to PL/SQL with correct parameter reference
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_updated_records.COUNT > %d THEN\n", rowIdx))
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_updated_records(%d).", paramIndex, rowIdx+1))
db.QuoteTo(&plsqlBuilder, column)
plsqlBuilder.WriteString(";\n")
plsqlBuilder.WriteString(" END IF;\n")
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_updated_records.COUNT > %d THEN ", rowIdx))
plsqlBuilder.WriteString(fmt.Sprintf(":%d := ", paramIndex))

if isJSONField(field) {
if isRawMessageField(field) {
plsqlBuilder.WriteString(fmt.Sprintf("l_updated_records(%d).", rowIdx+1))
writeQuotedIdentifier(&plsqlBuilder, column)
} else {
// serialize JSON so it binds as text
plsqlBuilder.WriteString("JSON_SERIALIZE(")
plsqlBuilder.WriteString(fmt.Sprintf("l_updated_records(%d).", rowIdx+1))
writeQuotedIdentifier(&plsqlBuilder, column)
plsqlBuilder.WriteString(" RETURNING CLOB)")
}
} else {
plsqlBuilder.WriteString(fmt.Sprintf("l_updated_records(%d).", rowIdx+1))
writeQuotedIdentifier(&plsqlBuilder, column)
}

plsqlBuilder.WriteString("; END IF;\n")
}
}
}

plsqlBuilder.WriteString("END;")

stmt.SQL.Reset()
Expand Down
Loading
Loading