Skip to content

Commit 38c9f3f

Browse files
Merge branch 'main' into hkhasnis_boolean_type
Merge with main
2 parents 1e2e957 + bf6e164 commit 38c9f3f

File tree

13 files changed

+2488
-35
lines changed

13 files changed

+2488
-35
lines changed

oracle/common.go

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,30 @@
3939
package oracle
4040

4141
import (
42+
"bytes"
4243
"database/sql"
4344
"encoding/json"
4445
"fmt"
46+
"math"
4547
"reflect"
4648
"strings"
4749
"time"
4850

51+
"github.com/godror/godror"
4952
"github.com/google/uuid"
5053
"gorm.io/datatypes"
5154
"gorm.io/gorm"
5255
"gorm.io/gorm/schema"
5356
)
5457

58+
// Extra data types for the data type that are not declared in the
59+
// default DataType list
60+
const (
61+
JSON schema.DataType = "json"
62+
Timestamp schema.DataType = "timestamp"
63+
TimestampWithTimeZone schema.DataType = "timestamp with time zone"
64+
)
65+
5566
// Helper function to get Oracle array type for a field
5667
func getOracleArrayType(field *schema.Field, values []any) string {
5768
switch field.DataType {
@@ -61,6 +72,10 @@ func getOracleArrayType(field *schema.Field, values []any) string {
6172
return "TABLE OF NUMBER"
6273
case schema.Float:
6374
return "TABLE OF NUMBER"
75+
case JSON:
76+
// PL/SQL does not yet allow declaring collections of JSON (TABLE OF JSON) directly.
77+
// Workaround for JSON type
78+
fallthrough
6479
case schema.String:
6580
if field.Size > 0 && field.Size <= 4000 {
6681
return fmt.Sprintf("TABLE OF VARCHAR2(%d)", field.Size)
@@ -79,7 +94,7 @@ func getOracleArrayType(field *schema.Field, values []any) string {
7994
case schema.Bytes:
8095
return "TABLE OF BLOB"
8196
default:
82-
return "TABLE OF VARCHAR2(4000)" // Safe default
97+
return "TABLE OF " + strings.ToUpper(string(field.DataType))
8398
}
8499
}
85100

@@ -113,8 +128,37 @@ func findFieldByDBName(schema *schema.Schema, dbName string) *schema.Field {
113128
// Create typed destination for OUT parameters
114129
func createTypedDestination(f *schema.Field) interface{} {
115130
if f == nil {
116-
var s string
117-
return &s
131+
return new(string)
132+
}
133+
134+
// If the field has a serializer, the field type may not be directly related to the column type in the database.
135+
// In this case, determine the destination type using the field's data type, which is the column type in the
136+
// database.
137+
if f.Serializer != nil {
138+
dt := strings.ToLower(string(f.DataType))
139+
switch schema.DataType(dt) {
140+
case schema.Bool:
141+
return new(bool)
142+
case schema.Uint:
143+
return new(uint64)
144+
case schema.Int:
145+
return new(int64)
146+
case schema.Float:
147+
return new(float64)
148+
case schema.String:
149+
return new(string)
150+
case Timestamp:
151+
fallthrough
152+
case TimestampWithTimeZone:
153+
fallthrough
154+
case schema.Time:
155+
return new(time.Time)
156+
case schema.Bytes:
157+
return new([]byte)
158+
default:
159+
// Fallback
160+
return new(string)
161+
}
118162
}
119163

120164
ft := f.FieldType
@@ -163,8 +207,7 @@ func createTypedDestination(f *schema.Field) interface{} {
163207
}
164208

165209
// Fallback
166-
var s string
167-
return &s
210+
return new(string)
168211
}
169212

170213
// Convert values for Oracle-specific types
@@ -206,6 +249,14 @@ func convertValue(val interface{}) interface{} {
206249
return 0
207250
}
208251
case string:
252+
if len(v) > math.MaxInt16 {
253+
return godror.Lob{IsClob: true, Reader: strings.NewReader(v)}
254+
}
255+
return v
256+
case []byte:
257+
if len(v) > math.MaxInt16 {
258+
return godror.Lob{IsClob: false, Reader: bytes.NewReader(v)}
259+
}
209260
return v
210261
default:
211262
return val
@@ -218,6 +269,13 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
218269
return nil
219270
}
220271

272+
// Deserialize data into objects when a serializer is used
273+
if field.Serializer != nil {
274+
serializerField := field.NewValuePool.Get().(sql.Scanner)
275+
serializerField.Scan(value)
276+
return serializerField
277+
}
278+
221279
targetType := field.FieldType
222280
var converted any
223281

oracle/migrator.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ func (m Migrator) CreateTable(values ...interface{}) error {
202202
createTableSQL += ")"
203203

204204
if tableOption, ok := m.DB.Get("gorm:table_options"); ok {
205-
createTableSQL += fmt.Sprint(tableOption)
205+
createTableSQL += " " + fmt.Sprint(tableOption)
206206
}
207207

208208
err = tx.Exec(createTableSQL, values...).Error
@@ -484,6 +484,81 @@ func (m Migrator) DropConstraint(value interface{}, name string) error {
484484
return m.Migrator.DropConstraint(value, name)
485485
}
486486

487+
// CreateType creates or replaces an Oracle user-defined type
488+
func (m Migrator) CreateType(typeName string, args ...string) error {
489+
typeName = strings.TrimSpace(typeName)
490+
if typeName == "" {
491+
return fmt.Errorf("typeName is required")
492+
}
493+
var typeKind, typeOf string
494+
if len(args) > 0 {
495+
typeKind = args[0]
496+
}
497+
if len(args) > 1 {
498+
typeOf = args[1]
499+
}
500+
501+
name := strings.ToLower(typeName)
502+
typeKind = strings.TrimSpace(typeKind)
503+
typeOf = strings.TrimSpace(typeOf)
504+
505+
// Incomplete object type
506+
if typeKind == "" && typeOf == "" {
507+
ddl := fmt.Sprintf(`CREATE TYPE "%s"`, name)
508+
return m.DB.Exec(ddl).Error
509+
}
510+
511+
k := strings.ToUpper(typeKind)
512+
var ddl string
513+
514+
switch {
515+
// Standalone varying array (varray) type and Standalone nested table type
516+
case strings.HasPrefix(k, "VARRAY") || strings.HasPrefix(k, "TABLE "):
517+
if typeOf == "" {
518+
return fmt.Errorf("typeof is required for collection types (VARRAY/TABLE)")
519+
}
520+
ddl = fmt.Sprintf(`CREATE OR REPLACE TYPE "%s" AS %s OF %s`, name, typeKind, typeOf)
521+
522+
// Abstract Data Type (ADT)
523+
case k == "OBJECT" || strings.HasPrefix(k, "OBJECT"):
524+
if typeOf == "" {
525+
return fmt.Errorf("attributes definition is required for OBJECT types")
526+
}
527+
attrs := typeOf
528+
if !strings.HasPrefix(attrs, "(") {
529+
attrs = "(" + attrs + ")"
530+
}
531+
ddl = fmt.Sprintf(`CREATE OR REPLACE TYPE "%s" AS OBJECT %s`, name, attrs)
532+
533+
default:
534+
// Invalid or unsupported types
535+
return fmt.Errorf("unsupported type kind %q (must be OBJECT, VARRAY, or TABLE)", typeKind)
536+
}
537+
538+
return m.DB.Exec(ddl).Error
539+
}
540+
541+
// DropType drops a user-defined type
542+
func (m Migrator) DropType(typeName string) error {
543+
typeName = strings.TrimSpace(typeName)
544+
if typeName == "" {
545+
return fmt.Errorf("dropType: typeName is required")
546+
}
547+
ddl := fmt.Sprintf(`DROP TYPE "%s" FORCE`, strings.ToLower(typeName))
548+
return m.DB.Exec(ddl).Error
549+
}
550+
551+
// HasType checks whether a user-defined type exists
552+
func (m Migrator) HasType(typeName string) bool {
553+
if typeName == "" {
554+
return false
555+
}
556+
557+
var count int
558+
err := m.DB.Raw(`SELECT COUNT(*) FROM USER_TYPES WHERE UPPER(TYPE_NAME) = UPPER(?)`, typeName).Scan(&count).Error
559+
return err == nil && count > 0
560+
}
561+
487562
// DropIndex drops the index with the specified `name` from the table associated with `value`
488563
func (m Migrator) DropIndex(value interface{}, name string) error {
489564
return m.RunWithValue(value, func(stmt *gorm.Statement) error {

oracle/query.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ func BeforeQuery(db *gorm.DB) {
5757
name := db.Statement.TableExpr.SQL
5858
if strings.Contains(name, " ") || strings.Contains(name, "`") {
5959
if results := tableRegexp.FindStringSubmatch(name); len(results) == 3 {
60-
if results[2] != "" {
61-
db.Statement.Table = results[2]
62-
} else {
63-
db.Statement.Table = results[1]
64-
}
60+
db.Statement.Table = results[2]
6561
}
6662
}
6763
}

oracle/update.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ func Update(db *gorm.DB) {
109109
// Always use PL/SQL for RETURNING, just like delete callback
110110
buildUpdatePLSQL(db)
111111
} else {
112+
if updateClause, ok := stmt.Clauses["UPDATE"].Expression.(clause.Update); ok {
113+
if updateClause.Table.Name != "" {
114+
stmt.Table = updateClause.Table.Name
115+
}
116+
}
112117
// Use GORM's standard build for non-RETURNING updates
113118
stmt.Build("UPDATE", "SET", "WHERE")
114119
// Convert values for Oracle
@@ -163,7 +168,6 @@ func checkMissingWhereConditions(db *gorm.DB) {
163168
}
164169
// Has non-soft-delete equality condition, this is valid
165170
hasMeaningfulConditions = true
166-
break
167171
case clause.IN:
168172
// Has IN condition with values, this is valid
169173
if len(e.Values) > 0 {
@@ -182,11 +186,9 @@ func checkMissingWhereConditions(db *gorm.DB) {
182186
}
183187
// Has non-soft-delete expression condition, consider it valid
184188
hasMeaningfulConditions = true
185-
break
186189
case clause.AndConditions, clause.OrConditions:
187190
// Complex conditions are likely valid (but we could be more thorough here)
188191
hasMeaningfulConditions = true
189-
break
190192
case clause.Where:
191193
// Handle nested WHERE clauses - recursively check their expressions
192194
if len(e.Exprs) > 0 {
@@ -203,7 +205,6 @@ func checkMissingWhereConditions(db *gorm.DB) {
203205
default:
204206
// Unknown condition types - assume they're meaningful for safety
205207
hasMeaningfulConditions = true
206-
break
207208
}
208209

209210
// If we found meaningful conditions, we can stop checking

tests/blob_test.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,33 +41,44 @@ package tests
4141
import (
4242
"bytes"
4343
"crypto/rand"
44+
"strings"
4445
"testing"
4546
"time"
4647

4748
"gorm.io/gorm"
4849
)
4950

5051
type BlobTestModel struct {
51-
ID uint `gorm:"primaryKey;autoIncrement"`
52-
Name string `gorm:"size:100;not null"`
53-
Data []byte `gorm:"type:blob"`
52+
ID uint `gorm:"primaryKey;autoIncrement"`
53+
Name string `gorm:"size:100;not null"`
54+
Data []byte `gorm:"type:blob"`
5455
OptionalData *[]byte `gorm:"type:blob"`
55-
CreatedAt time.Time
56-
UpdatedAt time.Time
56+
CreatedAt time.Time
57+
UpdatedAt time.Time
5758
}
5859

5960
type BlobVariantModel struct {
60-
ID uint `gorm:"primaryKey"`
61+
ID uint `gorm:"primaryKey"`
6162
SmallBlob []byte `gorm:"type:blob"`
6263
LargeBlob []byte `gorm:"type:blob"`
6364
}
6465

66+
type BlobOneToManyModel struct {
67+
ID uint `gorm:"primaryKey"`
68+
Children []BlobChildModel `gorm:"foreignKey:ID"`
69+
}
70+
71+
type BlobChildModel struct {
72+
ID uint `gorm:"primaryKey"`
73+
Data []byte `gorm:"type:blob"`
74+
}
75+
6576
func setupBlobTestTables(t *testing.T) {
6677
t.Log("Setting up BLOB test tables")
6778

68-
DB.Migrator().DropTable(&BlobTestModel{}, &BlobVariantModel{})
79+
DB.Migrator().DropTable(&BlobTestModel{}, &BlobVariantModel{}, &BlobOneToManyModel{}, &BlobChildModel{})
6980

70-
err := DB.AutoMigrate(&BlobTestModel{}, &BlobVariantModel{})
81+
err := DB.AutoMigrate(&BlobTestModel{}, &BlobVariantModel{}, &BlobOneToManyModel{}, &BlobChildModel{})
7182
if err != nil {
7283
t.Fatalf("Failed to migrate BLOB test tables: %v", err)
7384
}
@@ -423,6 +434,23 @@ func TestBlobWithReturning(t *testing.T) {
423434
}
424435
}
425436

437+
func TestBlobOnConflict(t *testing.T) {
438+
setupBlobTestTables(t)
439+
440+
model := &BlobOneToManyModel{
441+
ID: 1,
442+
Children: []BlobChildModel{
443+
{
444+
Data: []byte(strings.Repeat("X", 32768)),
445+
},
446+
},
447+
}
448+
err := DB.Create(model).Error
449+
if err != nil {
450+
t.Fatalf("Failed to create BLOB record with ON CONFLICT: %v", err)
451+
}
452+
}
453+
426454
func TestBlobErrorHandling(t *testing.T) {
427455
setupBlobTestTables(t)
428456

0 commit comments

Comments
 (0)