Skip to content

Commit f67bae1

Browse files
Merge pull request #110 from joboon/PLSQLBatchLOBHandling
fix: PLSQL batch LOB handling
2 parents 009eb56 + fe346ca commit f67bae1

File tree

7 files changed

+877
-166
lines changed

7 files changed

+877
-166
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
@@ -226,6 +235,10 @@ func convertValue(val interface{}) interface{} {
226235
rv = rv.Elem()
227236
val = rv.Interface()
228237
}
238+
isNil := false
239+
if rv.Kind() == reflect.Ptr && rv.IsNil() {
240+
isNil = true
241+
}
229242

230243
switch v := val.(type) {
231244
case json.RawMessage:
@@ -242,7 +255,7 @@ func convertValue(val interface{}) interface{} {
242255
case *uuid.UUID, *datatypes.UUID:
243256
// Convert nil pointer to a UUID to empty string so that it is stored in the database as NULL
244257
// rather than "00000000-0000-0000-0000-000000000000"
245-
if rv.IsNil() {
258+
if isNil {
246259
return ""
247260
}
248261
return val
@@ -253,15 +266,31 @@ func convertValue(val interface{}) interface{} {
253266
return 0
254267
}
255268
case string:
256-
if len(v) > math.MaxInt16 {
269+
// Store strings longer than 4000 characters as CLOB
270+
if len(v) > 4000 {
257271
return godror.Lob{IsClob: true, Reader: strings.NewReader(v)}
258272
}
259273
return v
260274
case []byte:
261-
if len(v) > math.MaxInt16 {
275+
// Store byte slices longer than 4000 bytes as BLOB
276+
if len(v) > 4000 {
262277
return godror.Lob{IsClob: false, Reader: bytes.NewReader(v)}
263278
}
264279
return v
280+
case driver.Valuer:
281+
// Unwrap driver.Valuer to its underlying type by recursing into
282+
// convertValue until we get a non-Valuer type
283+
if v == nil || isNil {
284+
return val
285+
}
286+
unwrappedValue, err := v.Value()
287+
if err != nil {
288+
return val
289+
}
290+
return convertValue(unwrappedValue)
291+
case clause.Expr:
292+
// If we get a clause.Expr, convert it to nil; it should be handled elsewhere
293+
return nil
265294
default:
266295
return val
267296
}
@@ -289,6 +318,13 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
289318
targetType = field.FieldType.Elem()
290319
}
291320

321+
// When PL/SQL LOBs are returned, skip conversion.
322+
// LOB addresses are freed by the driver after the query, so we cannot read their content
323+
// from the return value. If you need to read stored LOB content, do it in a separate query.
324+
if _, ok := value.(godror.Lob); ok {
325+
return nil
326+
}
327+
292328
switch targetType {
293329
case reflect.TypeOf(gorm.DeletedAt{}):
294330
if nullTime, ok := value.(sql.NullTime); ok {
@@ -322,6 +358,16 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
322358
default:
323359
converted = value
324360
}
361+
case reflect.TypeOf(uuid.UUID{}), reflect.TypeOf(datatypes.UUID{}):
362+
uuidStr, ok := value.(string)
363+
if !ok {
364+
return nil
365+
}
366+
parsed, err := uuid.Parse(uuidStr)
367+
if err != nil {
368+
return nil
369+
}
370+
converted = parsed
325371

326372
case reflect.TypeOf(time.Time{}):
327373
switch vv := value.(type) {
@@ -392,6 +438,12 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
392438
}
393439

394440
func isJSONField(f *schema.Field) bool {
441+
// Support detecting JSON fields through the struct's "type" tag.
442+
// Also support jsonb for compatibility with other databases.
443+
if f.DataType == "json" || f.DataType == "jsonb" {
444+
return true
445+
}
446+
395447
_rawMsgT := reflect.TypeOf(json.RawMessage{})
396448
_gormJSON := reflect.TypeOf(datatypes.JSON{})
397449
if f == nil {

0 commit comments

Comments
 (0)