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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.4

require (
github.com/godror/godror v0.49.3
github.com/google/uuid v1.6.0
gorm.io/datatypes v1.2.6
gorm.io/gorm v1.31.0
)
Expand All @@ -14,7 +15,6 @@ require (
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-20250911091902-df9299821621 // indirect
Expand Down
75 changes: 49 additions & 26 deletions oracle/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
"strings"
"time"

"github.com/google/uuid"
"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/schema"
Expand Down Expand Up @@ -165,10 +166,10 @@ func convertValue(val interface{}) interface{} {
}

// Dereference pointers
v := reflect.ValueOf(val)
for v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
val = v.Interface()
rv := reflect.ValueOf(val)
for rv.Kind() == reflect.Ptr && !rv.IsNil() {
rv = rv.Elem()
val = rv.Interface()
}

switch v := val.(type) {
Expand All @@ -183,6 +184,13 @@ func convertValue(val interface{}) interface{} {
}
b := []byte(*v)
return b
case *uuid.UUID, *datatypes.UUID:
// Convert nil pointer to a UUID to empty string so that it is stored in the database as NULL
// rather than "00000000-0000-0000-0000-000000000000"
if rv.IsNil() {
return ""
}
return val
case bool:
if v {
return 1
Expand All @@ -203,30 +211,13 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
}

targetType := field.FieldType
isPtr := targetType.Kind() == reflect.Ptr
var converted any

// dereference the field if it's a pointer
isPtr := field.FieldType.Kind() == reflect.Ptr
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)
}
targetType = field.FieldType.Elem()
}
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 {
case reflect.TypeOf(gorm.DeletedAt{}):
Expand All @@ -235,6 +226,33 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
} else {
converted = gorm.DeletedAt{}
}

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

case reflect.TypeOf(time.Time{}):
switch vv := value.(type) {
case time.Time:
Expand Down Expand Up @@ -309,7 +327,12 @@ func isJSONField(f *schema.Field) bool {
if f == nil {
return false
}

ft := f.FieldType
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}

return ft == _rawMsgT || ft == _gormJSON
}

Expand Down
4 changes: 2 additions & 2 deletions oracle/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@
package oracle

import (
"gorm.io/gorm"
"regexp"
"strings"

"gorm.io/gorm"
)

// Identifies the table name alias provided as
Expand All @@ -63,5 +64,4 @@ func BeforeQuery(db *gorm.DB) {
}
}
}
return
}
47 changes: 32 additions & 15 deletions tests/json_bulk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ import (

func TestBasicCRUD_JSONText(t *testing.T) {
type JsonRecord struct {
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
Name string `gorm:"column:name"`
Properties datatypes.JSON `gorm:"column:properties"`
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
Name string `gorm:"column:name"`
Properties datatypes.JSON `gorm:"column:properties"`
PropertiesPtr *datatypes.JSON `gorm:"column:propertiesPtr"`
}

DB.Migrator().DropTable(&JsonRecord{})
Expand All @@ -61,9 +62,11 @@ func TestBasicCRUD_JSONText(t *testing.T) {
}

// INSERT
json := datatypes.JSON([]byte(`{"env":"prod","owner":"team-x"}`))
rec := JsonRecord{
Name: "json-text",
Properties: datatypes.JSON([]byte(`{"env":"prod","owner":"team-x"}`)),
Name: "json-text",
Properties: json,
PropertiesPtr: &json,
}
if err := DB.Create(&rec).Error; err != nil {
t.Fatalf("create failed: %v", err)
Expand All @@ -73,20 +76,23 @@ func TestBasicCRUD_JSONText(t *testing.T) {
}

// UPDATE (with RETURNING)
updateJson := datatypes.JSON([]byte(`{"env":"staging","owner":"team-y","flag":true}`))
var ret JsonRecord
if err := DB.
Clauses(clause.Returning{
Columns: []clause.Column{
{Name: "record_id"},
{Name: "name"},
{Name: "properties"},
{Name: "propertiesPtr"},
},
}).
Model(&ret).
Where("\"record_id\" = ?", rec.ID).
Updates(map[string]any{
"name": "json-text-upd",
"properties": datatypes.JSON([]byte(`{"env":"staging","owner":"team-y","flag":true}`)),
"name": "json-text-upd",
"properties": updateJson,
"propertiesPtr": &updateJson,
}).Error; err != nil {
t.Fatalf("update returning failed: %v", err)
}
Expand All @@ -103,6 +109,7 @@ func TestBasicCRUD_JSONText(t *testing.T) {
{Name: "record_id"},
{Name: "name"},
{Name: "properties"},
{Name: "propertiesPtr"},
},
}).
Delete(&deleted).Error; err != nil {
Expand All @@ -122,20 +129,23 @@ func TestBasicCRUD_JSONText(t *testing.T) {

func TestBasicCRUD_RawMessage(t *testing.T) {
type RawRecord struct {
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
Name string `gorm:"column:name"`
Properties json.RawMessage `gorm:"column:properties"`
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
Name string `gorm:"column:name"`
Properties json.RawMessage `gorm:"column:properties"`
PropertiesPtr *json.RawMessage `gorm:"column:propertiesPtr"`
}

DB.Migrator().DropTable(&RawRecord{})
if err := DB.AutoMigrate(&RawRecord{}); err != nil {
t.Fatalf("migrate failed: %v", err)
}

rawMsg := json.RawMessage(`{"a":1,"b":"x"}`)
// INSERT
rec := RawRecord{
Name: "raw-json",
Properties: json.RawMessage(`{"a":1,"b":"x"}`),
Name: "raw-json",
Properties: rawMsg,
PropertiesPtr: &rawMsg,
}
if err := DB.Create(&rec).Error; err != nil {
t.Fatalf("create failed: %v", err)
Expand All @@ -145,24 +155,30 @@ func TestBasicCRUD_RawMessage(t *testing.T) {
}

// UPDATE (with RETURNING)
upatedRawMsg := json.RawMessage(`{"a":2,"c":true}`)
var ret RawRecord
if err := DB.
Clauses(clause.Returning{
Columns: []clause.Column{
{Name: "record_id"},
{Name: "name"},
{Name: "properties"},
{Name: "propertiesPtr"},
},
}).
Model(&ret).
Where("\"record_id\" = ?", rec.ID).
Updates(map[string]any{
"name": "raw-json-upd",
"properties": json.RawMessage(`{"a":2,"c":true}`),
"name": "raw-json-upd",
"properties": upatedRawMsg,
"propertiesPtr": &upatedRawMsg,
}).Error; err != nil {
t.Fatalf("update returning failed: %v", err)
}
if ret.ID != rec.ID || ret.Name != "raw-json-upd" || len(ret.Properties) == 0 {
if ret.ID != rec.ID ||
ret.Name != "raw-json-upd" ||
len(ret.Properties) == 0 ||
ret.PropertiesPtr == nil || (ret.PropertiesPtr != nil && len(*ret.PropertiesPtr) == 0) {
t.Fatalf("unexpected returning row: %#v", ret)
}

Expand All @@ -175,6 +191,7 @@ func TestBasicCRUD_RawMessage(t *testing.T) {
{Name: "record_id"},
{Name: "name"},
{Name: "properties"},
{Name: "propertiesPtr"},
},
}).
Delete(&deleted).Error; err != nil {
Expand Down
Loading