Skip to content

Commit fe346ca

Browse files
committed
fix: PLSQL batch LOB handling
1 parent fea03f8 commit fe346ca

File tree

7 files changed

+877
-167
lines changed

7 files changed

+877
-167
lines changed

oracle/common.go

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ package oracle
4141
import (
4242
"bytes"
4343
"database/sql"
44+
"database/sql/driver"
4445
"encoding/json"
45-
"fmt"
46-
"math"
4746
"reflect"
4847
"strings"
4948
"time"
@@ -52,50 +51,50 @@ import (
5251
"github.com/google/uuid"
5352
"gorm.io/datatypes"
5453
"gorm.io/gorm"
54+
"gorm.io/gorm/clause"
5555
"gorm.io/gorm/schema"
5656
)
5757

5858
// Extra data types for the data type that are not declared in the
5959
// default DataType list
6060
const (
61-
JSON schema.DataType = "json"
6261
Timestamp schema.DataType = "timestamp"
6362
TimestampWithTimeZone schema.DataType = "timestamp with time zone"
6463
)
6564

6665
// Helper function to get Oracle array type for a field
67-
func getOracleArrayType(field *schema.Field, values []any) string {
68-
switch field.DataType {
69-
case schema.Bool:
70-
return "TABLE OF NUMBER(1)"
71-
case schema.Int, schema.Uint:
72-
return "TABLE OF NUMBER"
73-
case schema.Float:
74-
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
79-
case schema.String:
80-
if field.Size > 0 && field.Size <= 4000 {
81-
return fmt.Sprintf("TABLE OF VARCHAR2(%d)", field.Size)
82-
} else {
83-
for _, value := range values {
84-
if strValue, ok := value.(string); ok {
85-
if len(strValue) > 4000 {
86-
return "TABLE OF CLOB"
87-
}
88-
}
66+
func getOracleArrayType(values []any) string {
67+
arrayType := "TABLE OF VARCHAR2(4000)"
68+
for _, val := range values {
69+
if val == nil {
70+
continue
71+
}
72+
switch v := val.(type) {
73+
case bool:
74+
arrayType = "TABLE OF NUMBER(1)"
75+
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
76+
arrayType = "TABLE OF NUMBER"
77+
case time.Time:
78+
arrayType = "TABLE OF TIMESTAMP WITH TIME ZONE"
79+
case godror.Lob:
80+
if v.IsClob {
81+
return "TABLE OF CLOB"
82+
} else {
83+
return "TABLE OF BLOB"
84+
}
85+
case []byte:
86+
// Store byte slices longer than 4000 bytes as BLOB
87+
if len(v) > 4000 {
88+
return "TABLE OF BLOB"
89+
}
90+
case string:
91+
// Store strings longer than 4000 characters as CLOB
92+
if len(v) > 4000 {
93+
return "TABLE OF CLOB"
8994
}
9095
}
91-
return "TABLE OF VARCHAR2(4000)"
92-
case schema.Time:
93-
return "TABLE OF TIMESTAMP WITH TIME ZONE"
94-
case schema.Bytes:
95-
return "TABLE OF BLOB"
96-
default:
97-
return "TABLE OF " + strings.ToUpper(string(field.DataType))
9896
}
97+
return arrayType
9998
}
10099

101100
// Helper function to get all column names for a table
@@ -131,6 +130,12 @@ func createTypedDestination(f *schema.Field) interface{} {
131130
return new(string)
132131
}
133132

133+
// To differentiate between bool fields stored as NUMBER(1) and bool fields stored as actual BOOLEAN type,
134+
// check the struct's "type" tag.
135+
if f.DataType == "boolean" {
136+
return new(bool)
137+
}
138+
134139
// If the field has a serializer, the field type may not be directly related to the column type in the database.
135140
// In this case, determine the destination type using the field's data type, which is the column type in the
136141
// database.
@@ -204,13 +209,17 @@ func createTypedDestination(f *schema.Field) interface{} {
204209

205210
case reflect.Float32, reflect.Float64:
206211
return new(float64)
212+
213+
case reflect.Slice:
214+
if ft.Elem().Kind() == reflect.Uint8 { // []byte
215+
return new([]byte)
216+
}
207217
}
208218

209219
// Fallback
210220
return new(string)
211221
}
212222

213-
// Convert values for Oracle-specific types
214223
func convertValue(val interface{}) interface{} {
215224
if val == nil {
216225
return nil
@@ -222,6 +231,10 @@ func convertValue(val interface{}) interface{} {
222231
rv = rv.Elem()
223232
val = rv.Interface()
224233
}
234+
isNil := false
235+
if rv.Kind() == reflect.Ptr && rv.IsNil() {
236+
isNil = true
237+
}
225238

226239
switch v := val.(type) {
227240
case json.RawMessage:
@@ -238,7 +251,7 @@ func convertValue(val interface{}) interface{} {
238251
case *uuid.UUID, *datatypes.UUID:
239252
// Convert nil pointer to a UUID to empty string so that it is stored in the database as NULL
240253
// rather than "00000000-0000-0000-0000-000000000000"
241-
if rv.IsNil() {
254+
if isNil {
242255
return ""
243256
}
244257
return val
@@ -249,15 +262,31 @@ func convertValue(val interface{}) interface{} {
249262
return 0
250263
}
251264
case string:
252-
if len(v) > math.MaxInt16 {
265+
// Store strings longer than 4000 characters as CLOB
266+
if len(v) > 4000 {
253267
return godror.Lob{IsClob: true, Reader: strings.NewReader(v)}
254268
}
255269
return v
256270
case []byte:
257-
if len(v) > math.MaxInt16 {
271+
// Store byte slices longer than 4000 bytes as BLOB
272+
if len(v) > 4000 {
258273
return godror.Lob{IsClob: false, Reader: bytes.NewReader(v)}
259274
}
260275
return v
276+
case driver.Valuer:
277+
// Unwrap driver.Valuer to its underlying type by recursing into
278+
// convertValue until we get a non-Valuer type
279+
if v == nil || isNil {
280+
return val
281+
}
282+
unwrappedValue, err := v.Value()
283+
if err != nil {
284+
return val
285+
}
286+
return convertValue(unwrappedValue)
287+
case clause.Expr:
288+
// If we get a clause.Expr, convert it to nil; it should be handled elsewhere
289+
return nil
261290
default:
262291
return val
263292
}
@@ -285,6 +314,13 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
285314
targetType = field.FieldType.Elem()
286315
}
287316

317+
// When PL/SQL LOBs are returned, skip conversion.
318+
// LOB addresses are freed by the driver after the query, so we cannot read their content
319+
// from the return value. If you need to read stored LOB content, do it in a separate query.
320+
if _, ok := value.(godror.Lob); ok {
321+
return nil
322+
}
323+
288324
switch targetType {
289325
case reflect.TypeOf(gorm.DeletedAt{}):
290326
if nullTime, ok := value.(sql.NullTime); ok {
@@ -318,6 +354,16 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
318354
default:
319355
converted = value
320356
}
357+
case reflect.TypeOf(uuid.UUID{}), reflect.TypeOf(datatypes.UUID{}):
358+
uuidStr, ok := value.(string)
359+
if !ok {
360+
return nil
361+
}
362+
parsed, err := uuid.Parse(uuidStr)
363+
if err != nil {
364+
return nil
365+
}
366+
converted = parsed
321367

322368
case reflect.TypeOf(time.Time{}):
323369
switch vv := value.(type) {
@@ -388,6 +434,12 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
388434
}
389435

390436
func isJSONField(f *schema.Field) bool {
437+
// Support detecting JSON fields through the struct's "type" tag.
438+
// Also support jsonb for compatibility with other databases.
439+
if f.DataType == "json" || f.DataType == "jsonb" {
440+
return true
441+
}
442+
391443
_rawMsgT := reflect.TypeOf(json.RawMessage{})
392444
_gormJSON := reflect.TypeOf(datatypes.JSON{})
393445
if f == nil {

0 commit comments

Comments
 (0)