From 0d8aee6d027a7284f3bad4c83804d797bbaa85a1 Mon Sep 17 00:00:00 2001 From: Ting-Lan Wang Date: Thu, 18 Sep 2025 16:11:36 -0400 Subject: [PATCH] Add support for nil pointer uuid --- go.mod | 2 +- oracle/common.go | 75 +++++++++++++++++++++++++++-------------- oracle/query.go | 4 +-- tests/json_bulk_test.go | 47 +++++++++++++++++--------- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index 6656387..8af0be1 100644 --- a/go.mod +++ b/go.mod @@ -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 ) @@ -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 diff --git a/oracle/common.go b/oracle/common.go index ba3ca0c..c266137 100644 --- a/oracle/common.go +++ b/oracle/common.go @@ -46,6 +46,7 @@ import ( "strings" "time" + "github.com/google/uuid" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/schema" @@ -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) { @@ -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 @@ -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{}): @@ -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: @@ -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 } diff --git a/oracle/query.go b/oracle/query.go index 5a45c26..6caf89f 100644 --- a/oracle/query.go +++ b/oracle/query.go @@ -39,9 +39,10 @@ package oracle import ( - "gorm.io/gorm" "regexp" "strings" + + "gorm.io/gorm" ) // Identifies the table name alias provided as @@ -63,5 +64,4 @@ func BeforeQuery(db *gorm.DB) { } } } - return } diff --git a/tests/json_bulk_test.go b/tests/json_bulk_test.go index 81cf099..3e21f13 100644 --- a/tests/json_bulk_test.go +++ b/tests/json_bulk_test.go @@ -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{}) @@ -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) @@ -73,6 +76,7 @@ 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{ @@ -80,13 +84,15 @@ func TestBasicCRUD_JSONText(t *testing.T) { {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) } @@ -103,6 +109,7 @@ func TestBasicCRUD_JSONText(t *testing.T) { {Name: "record_id"}, {Name: "name"}, {Name: "properties"}, + {Name: "propertiesPtr"}, }, }). Delete(&deleted).Error; err != nil { @@ -122,9 +129,10 @@ 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{}) @@ -132,10 +140,12 @@ func TestBasicCRUD_RawMessage(t *testing.T) { 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) @@ -145,6 +155,7 @@ 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{ @@ -152,17 +163,22 @@ func TestBasicCRUD_RawMessage(t *testing.T) { {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) } @@ -175,6 +191,7 @@ func TestBasicCRUD_RawMessage(t *testing.T) { {Name: "record_id"}, {Name: "name"}, {Name: "properties"}, + {Name: "propertiesPtr"}, }, }). Delete(&deleted).Error; err != nil {