From 40899c061196e8f046ae6612e6acf491da024300 Mon Sep 17 00:00:00 2001 From: Ting-Lan Wang Date: Tue, 28 Oct 2025 10:40:05 -0400 Subject: [PATCH 1/3] Add support for serializer --- oracle/common.go | 48 +++++++++++++++++++++- oracle/update.go | 4 -- tests/serializer_test.go | 89 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 131 insertions(+), 10 deletions(-) diff --git a/oracle/common.go b/oracle/common.go index 70db86a..8a8f44c 100644 --- a/oracle/common.go +++ b/oracle/common.go @@ -79,7 +79,7 @@ func getOracleArrayType(field *schema.Field, values []any) string { case schema.Bytes: return "TABLE OF BLOB" default: - return "TABLE OF VARCHAR2(4000)" // Safe default + return "TABLE OF " + strings.ToUpper(string(field.DataType)) } } @@ -110,6 +110,13 @@ func findFieldByDBName(schema *schema.Schema, dbName string) *schema.Field { return nil } +// Extra data types to determine the destination type for OUT parameters +// when using a serializer +const ( + Timestamp schema.DataType = "timestamp" + TimestampWithTimeZone schema.DataType = "timestamp with time zone" +) + // Create typed destination for OUT parameters func createTypedDestination(f *schema.Field) interface{} { if f == nil { @@ -117,6 +124,38 @@ func createTypedDestination(f *schema.Field) interface{} { return &s } + // If the field has a serializer, the field type may not be directly related to the column type in the database. + // In this case, determine the destination type using the field's data type, which is the column type in the database. + // use the data type as the destination type, + // because + // If the type is declared in the tag, + if f.Serializer != nil { + dt := strings.ToLower(string(f.DataType)) + switch schema.DataType(dt) { + case schema.Bool: + return new(bool) + case schema.Uint: + return new(uint64) + case schema.Int: + return new(int64) + case schema.Float: + return new(float64) + case schema.String: + return new(string) + case Timestamp: + fallthrough + case TimestampWithTimeZone: + fallthrough + case schema.Time: + return new(time.Time) + case schema.Bytes: + return new([]byte) + default: + // Fallback + return new(string) + } + } + ft := f.FieldType for ft.Kind() == reflect.Ptr { ft = ft.Elem() @@ -218,6 +257,13 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{ return nil } + // Deserialize data into objects when a serializer is used + if field.Serializer != nil { + serializerField := field.NewValuePool.Get().(sql.Scanner) + serializerField.Scan(value) + return serializerField + } + targetType := field.FieldType var converted any diff --git a/oracle/update.go b/oracle/update.go index 735eb9d..386abb7 100644 --- a/oracle/update.go +++ b/oracle/update.go @@ -168,7 +168,6 @@ func checkMissingWhereConditions(db *gorm.DB) { } // Has non-soft-delete equality condition, this is valid hasMeaningfulConditions = true - break case clause.IN: // Has IN condition with values, this is valid if len(e.Values) > 0 { @@ -187,11 +186,9 @@ func checkMissingWhereConditions(db *gorm.DB) { } // Has non-soft-delete expression condition, consider it valid hasMeaningfulConditions = true - break case clause.AndConditions, clause.OrConditions: // Complex conditions are likely valid (but we could be more thorough here) hasMeaningfulConditions = true - break case clause.Where: // Handle nested WHERE clauses - recursively check their expressions if len(e.Exprs) > 0 { @@ -208,7 +205,6 @@ func checkMissingWhereConditions(db *gorm.DB) { default: // Unknown condition types - assume they're meaningful for safety hasMeaningfulConditions = true - break } // If we found meaningful conditions, we can stop checking diff --git a/tests/serializer_test.go b/tests/serializer_test.go index 110485a..9f7feb4 100644 --- a/tests/serializer_test.go +++ b/tests/serializer_test.go @@ -60,8 +60,8 @@ type SerializerStruct struct { Roles3 *Roles `gorm:"serializer:json;not null"` Contracts map[string]interface{} `gorm:"serializer:json"` JobInfo Job `gorm:"type:bytes;serializer:gob"` - CreatedTime int64 `gorm:"serializer:unixtime;type:timestamp"` // store time in db, use int as field type - UpdatedTime *int64 `gorm:"serializer:unixtime;type:timestamp"` // store time in db, use int as field type + CreatedTime int64 `gorm:"serializer:unixtime;type:timestamp with time zone"` // store time in db, use int as field type + UpdatedTime *int64 `gorm:"serializer:unixtime;type:timestamp with time zone"` // store time in db, use int as field type CustomSerializerString string `gorm:"serializer:custom"` EncryptedString EncryptedString } @@ -122,13 +122,16 @@ func (c *CustomSerializer) Value(ctx context.Context, field *schema.Field, dst r } func TestSerializer(t *testing.T) { - schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + if _, ok := schema.GetSerializer("custom"); !ok { + schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + } DB.Migrator().DropTable(adaptorSerializerModel(&SerializerStruct{})) if err := DB.Migrator().AutoMigrate(adaptorSerializerModel(&SerializerStruct{})); err != nil { t.Fatalf("no error should happen when migrate scanner, valuer struct, got error %v", err) } createdAt := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + fmt.Printf("======= createdAt1 = %v\n", createdAt.Unix()) updatedAt := createdAt.Unix() data := SerializerStruct{ @@ -168,8 +171,82 @@ func TestSerializer(t *testing.T) { } } +// Issue 48: https://github.com/oracle-samples/gorm-oracle/issues/48 +func TestSerializerBulkInsert(t *testing.T) { + if _, ok := schema.GetSerializer("custom"); !ok { + schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + } + DB.Migrator().DropTable(adaptorSerializerModel(&SerializerStruct{})) + if err := DB.Migrator().AutoMigrate(adaptorSerializerModel(&SerializerStruct{})); err != nil { + t.Fatalf("no error should happen when migrate scanner, valuer struct, got error %v", err) + } + + createdAt := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + updatedAt := createdAt.Unix() + + data := []SerializerStruct{ + { + Name: []byte("jinzhu"), + Roles: []string{"r1", "r2"}, + Roles3: &Roles{}, + Contracts: map[string]interface{}{"name": "jinzhu", "age": 10}, + EncryptedString: EncryptedString("pass"), + CreatedTime: createdAt.Unix(), + UpdatedTime: &updatedAt, + JobInfo: Job{ + Title: "programmer", + Number: 9920, + Location: "Kenmawr", + IsIntern: false, + }, + CustomSerializerString: "world", + }, + { + Name: []byte("john"), + Roles: []string{"l1", "l2"}, + Roles3: &Roles{}, + Contracts: map[string]interface{}{"name": "john", "age": 20}, + EncryptedString: EncryptedString("pass"), + CreatedTime: createdAt.Unix(), + UpdatedTime: &updatedAt, + JobInfo: Job{ + Title: "manager", + Number: 7710, + Location: "Redwood City", + IsIntern: false, + }, + CustomSerializerString: "foo", + }, + } + + if err := DB.Create(&data).Error; err != nil { + t.Fatalf("failed to create data, got error %v", err) + } + + var result []SerializerStruct + if err := DB.Find(&result).Error; err != nil { + t.Fatalf("failed to query data, got error %v", err) + } + + tests.AssertEqual(t, result, data) + + // Update all the "roles" columns to "n1" + if err := DB.Model(&SerializerStruct{}).Where("\"roles\" IS NOT NULL").Update("roles", []string{"n1"}).Error; err != nil { + t.Fatalf("failed to update data's roles, got error %v", err) + } + + var count int64 + if err := DB.Model(&SerializerStruct{}).Where("\"roles\" = ?", "n1").Count(&count).Error; err != nil { + t.Fatalf("failed to query data, got error %v", err) + } + + tests.AssertEqual(t, count, 2) +} + func TestSerializerZeroValue(t *testing.T) { - schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + if _, ok := schema.GetSerializer("custom"); !ok { + schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + } DB.Migrator().DropTable(adaptorSerializerModel(&SerializerStruct{})) if err := DB.Migrator().AutoMigrate(adaptorSerializerModel(&SerializerStruct{})); err != nil { t.Fatalf("no error should happen when migrate scanner, valuer struct, got error %v", err) @@ -200,7 +277,9 @@ func TestSerializerZeroValue(t *testing.T) { } func TestSerializerAssignFirstOrCreate(t *testing.T) { - schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + if _, ok := schema.GetSerializer("custom"); !ok { + schema.RegisterSerializer("custom", NewCustomSerializer("hello")) + } DB.Migrator().DropTable(adaptorSerializerModel(&SerializerStruct{})) if err := DB.Migrator().AutoMigrate(adaptorSerializerModel(&SerializerStruct{})); err != nil { t.Fatalf("no error should happen when migrate scanner, valuer struct, got error %v", err) From 5e80cad66d8ce8b1f2b72c2e4320ead8febbd7df Mon Sep 17 00:00:00 2001 From: Ting-Lan Wang Date: Tue, 28 Oct 2025 11:01:20 -0400 Subject: [PATCH 2/3] minor changes and typos --- oracle/common.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/oracle/common.go b/oracle/common.go index 8a8f44c..aec9d74 100644 --- a/oracle/common.go +++ b/oracle/common.go @@ -120,15 +120,12 @@ const ( // Create typed destination for OUT parameters func createTypedDestination(f *schema.Field) interface{} { if f == nil { - var s string - return &s + return new(string) } // If the field has a serializer, the field type may not be directly related to the column type in the database. - // In this case, determine the destination type using the field's data type, which is the column type in the database. - // use the data type as the destination type, - // because - // If the type is declared in the tag, + // In this case, determine the destination type using the field's data type, which is the column type in the + // database. if f.Serializer != nil { dt := strings.ToLower(string(f.DataType)) switch schema.DataType(dt) { @@ -202,8 +199,7 @@ func createTypedDestination(f *schema.Field) interface{} { } // Fallback - var s string - return &s + return new(string) } // Convert values for Oracle-specific types From 1482dc8893d5da4d92b562cc5dd3cea39bc5c0cc Mon Sep 17 00:00:00 2001 From: Ting-Lan Wang Date: Tue, 28 Oct 2025 11:04:50 -0400 Subject: [PATCH 3/3] remove debug logs --- tests/serializer_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/serializer_test.go b/tests/serializer_test.go index 9f7feb4..c7f3742 100644 --- a/tests/serializer_test.go +++ b/tests/serializer_test.go @@ -131,7 +131,6 @@ func TestSerializer(t *testing.T) { } createdAt := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - fmt.Printf("======= createdAt1 = %v\n", createdAt.Unix()) updatedAt := createdAt.Unix() data := SerializerStruct{