Skip to content

Commit 8901e30

Browse files
Merge pull request #64 from oracle-samples/json-support
Fixing issue regarding JSON columns in bulk returning clause
2 parents 2797081 + d987665 commit 8901e30

File tree

6 files changed

+373
-37
lines changed

6 files changed

+373
-37
lines changed

go.mod

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ module github.com/oracle-samples/gorm-oracle
22

33
go 1.24.4
44

5-
require gorm.io/gorm v1.30.0
5+
require gorm.io/gorm v1.31.0
66

7-
require github.com/godror/godror v0.49.0
7+
require github.com/godror/godror v0.49.3
88

99
require (
10+
filippo.io/edwards25519 v1.1.0 // indirect
1011
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
1112
github.com/go-logfmt/logfmt v0.6.0 // indirect
13+
github.com/go-sql-driver/mysql v1.8.1 // indirect
1214
github.com/godror/knownpb v0.3.0 // indirect
15+
github.com/google/uuid v1.6.0 // indirect
1316
github.com/jinzhu/inflection v1.0.0 // indirect
1417
github.com/jinzhu/now v1.1.5 // indirect
15-
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect
16-
golang.org/x/term v0.27.0 // indirect
17-
golang.org/x/text v0.25.0 // indirect
18-
google.golang.org/protobuf v1.36.6 // indirect
18+
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
19+
golang.org/x/sys v0.36.0 // indirect
20+
golang.org/x/term v0.35.0 // indirect
21+
golang.org/x/text v0.29.0 // indirect
22+
google.golang.org/protobuf v1.36.9 // indirect
23+
gorm.io/datatypes v1.2.6 // indirect
24+
gorm.io/driver/mysql v1.5.6 // indirect
1925
)

oracle/common.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ package oracle
4040

4141
import (
4242
"database/sql"
43+
"encoding/json"
4344
"fmt"
4445
"reflect"
4546
"strings"
4647
"time"
4748

49+
"gorm.io/datatypes"
4850
"gorm.io/gorm"
4951
"gorm.io/gorm/schema"
5052
)
@@ -174,6 +176,17 @@ func convertValue(val interface{}) interface{} {
174176
}
175177

176178
switch v := val.(type) {
179+
case json.RawMessage:
180+
if v == nil {
181+
return nil
182+
}
183+
return []byte(v)
184+
case *json.RawMessage:
185+
if v == nil {
186+
return nil
187+
}
188+
b := []byte(*v)
189+
return b
177190
case bool:
178191
if v {
179192
return 1
@@ -198,7 +211,25 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
198211
if isPtr {
199212
targetType = targetType.Elem()
200213
}
201-
214+
if field.FieldType == reflect.TypeOf(json.RawMessage{}) {
215+
switch v := value.(type) {
216+
case []byte:
217+
return json.RawMessage(v) // from BLOB
218+
case *[]byte:
219+
if v == nil {
220+
return json.RawMessage(nil)
221+
}
222+
return json.RawMessage(*v)
223+
}
224+
}
225+
if isJSONField(field) {
226+
switch v := value.(type) {
227+
case string:
228+
return datatypes.JSON([]byte(v))
229+
case []byte:
230+
return datatypes.JSON(v)
231+
}
232+
}
202233
var converted interface{}
203234

204235
switch targetType {
@@ -276,6 +307,24 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
276307
return converted
277308
}
278309

310+
func isJSONField(f *schema.Field) bool {
311+
_rawMsgT := reflect.TypeOf(json.RawMessage{})
312+
_gormJSON := reflect.TypeOf(datatypes.JSON{})
313+
if f == nil {
314+
return false
315+
}
316+
ft := f.FieldType
317+
return ft == _rawMsgT || ft == _gormJSON
318+
}
319+
320+
func isRawMessageField(f *schema.Field) bool {
321+
t := f.FieldType
322+
for t.Kind() == reflect.Ptr {
323+
t = t.Elem()
324+
}
325+
return t == reflect.TypeOf(json.RawMessage(nil))
326+
}
327+
279328
// Helper function to handle primitive type conversions
280329
func convertPrimitiveType(value interface{}, targetType reflect.Type) interface{} {
281330
switch targetType.Kind() {

oracle/create.go

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -507,15 +507,37 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau
507507
}
508508
plsqlBuilder.WriteString("\n BULK COLLECT INTO l_affected_records;\n")
509509

510-
// Add OUT parameter population
510+
// Add OUT parameter population (JSON serialized to CLOB)
511511
outParamIndex := len(stmt.Vars)
512512
for rowIdx := 0; rowIdx < len(createValues.Values); rowIdx++ {
513513
for _, column := range allColumns {
514514
if field := findFieldByDBName(schema, column); field != nil {
515-
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
516-
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).", rowIdx, outParamIndex+1, rowIdx+1))
517-
db.QuoteTo(&plsqlBuilder, column)
518-
plsqlBuilder.WriteString("; END IF;\n")
515+
if isJSONField(field) {
516+
if isRawMessageField(field) {
517+
// Column is a BLOB, return raw bytes; no JSON_SERIALIZE
518+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new([]byte)})
519+
plsqlBuilder.WriteString(fmt.Sprintf(
520+
" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).",
521+
rowIdx, outParamIndex+1, rowIdx+1,
522+
))
523+
writeQuotedIdentifier(&plsqlBuilder, column)
524+
plsqlBuilder.WriteString("; END IF;\n")
525+
} else {
526+
// datatypes.JSON (text-based) -> serialize to CLOB
527+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new(string)})
528+
plsqlBuilder.WriteString(fmt.Sprintf(
529+
" IF l_affected_records.COUNT > %d THEN :%d := JSON_SERIALIZE(l_affected_records(%d).",
530+
rowIdx, outParamIndex+1, rowIdx+1,
531+
))
532+
writeQuotedIdentifier(&plsqlBuilder, column)
533+
plsqlBuilder.WriteString(" RETURNING CLOB); END IF;\n")
534+
}
535+
} else {
536+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
537+
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).", rowIdx, outParamIndex+1, rowIdx+1))
538+
writeQuotedIdentifier(&plsqlBuilder, column)
539+
plsqlBuilder.WriteString("; END IF;\n")
540+
}
519541
outParamIndex++
520542
}
521543
}
@@ -613,7 +635,7 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) {
613635
}
614636
plsqlBuilder.WriteString("\n BULK COLLECT INTO l_inserted_records;\n")
615637

616-
// Add OUT parameter population
638+
// Add OUT parameter population (JSON serialized to CLOB)
617639
outParamIndex := len(stmt.Vars)
618640
for rowIdx := 0; rowIdx < len(createValues.Values); rowIdx++ {
619641
for _, column := range allColumns {
@@ -622,9 +644,29 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) {
622644
quotedColumn := columnBuilder.String()
623645

624646
if field := findFieldByDBName(schema, column); field != nil {
625-
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
626-
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
627-
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn))
647+
if isJSONField(field) {
648+
if isRawMessageField(field) {
649+
// Column is a BLOB, return raw bytes; no JSON_SERIALIZE
650+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new([]byte)})
651+
plsqlBuilder.WriteString(fmt.Sprintf(
652+
" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
653+
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn,
654+
))
655+
} else {
656+
// datatypes.JSON (text-based) -> serialize to CLOB
657+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new(string)})
658+
plsqlBuilder.WriteString(fmt.Sprintf(
659+
" IF l_inserted_records.COUNT > %d THEN :%d := JSON_SERIALIZE(l_inserted_records(%d).%s RETURNING CLOB); END IF;\n",
660+
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn,
661+
))
662+
}
663+
} else {
664+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
665+
plsqlBuilder.WriteString(fmt.Sprintf(
666+
" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
667+
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn,
668+
))
669+
}
628670
outParamIndex++
629671
}
630672
}

oracle/delete.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -283,27 +283,48 @@ func buildBulkDeletePLSQL(db *gorm.DB) {
283283
}
284284
plsqlBuilder.WriteString("\n BULK COLLECT INTO l_deleted_records;\n")
285285

286-
// Create OUT parameters for each field and each row that will be deleted
286+
// Create OUT parameters for each field and each row that will be deleted (JSON-safe)
287287
outParamIndex := len(stmt.Vars)
288-
//TODO make it configurable
289-
estimatedRows := 100 // Estimate maximum rows to delete
288+
// keep your current fixed cap (same as other callbacks)
289+
estimatedRows := 100
290290

291291
for rowIdx := 0; rowIdx < estimatedRows; rowIdx++ {
292292
for _, column := range allColumns {
293-
field := findFieldByDBName(schema, column)
294-
if field != nil {
295-
dest := createTypedDestination(field)
296-
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
297-
298-
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))
299-
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_deleted_records(%d).", outParamIndex+1, rowIdx+1))
300-
db.QuoteTo(&plsqlBuilder, column)
301-
plsqlBuilder.WriteString(";\n")
302-
plsqlBuilder.WriteString(" END IF;\n")
293+
if field := findFieldByDBName(schema, column); field != nil {
294+
if isJSONField(field) {
295+
if isRawMessageField(field) {
296+
// Column is a BLOB, return raw bytes; no JSON_SERIALIZE
297+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new([]byte)})
298+
plsqlBuilder.WriteString(fmt.Sprintf(
299+
" IF l_deleted_records.COUNT > %d THEN :%d := l_deleted_records(%d).",
300+
rowIdx, outParamIndex+1, rowIdx+1,
301+
))
302+
writeQuotedIdentifier(&plsqlBuilder, column)
303+
plsqlBuilder.WriteString("; END IF;\n")
304+
} else {
305+
// JSON -> text bind
306+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: new(string)})
307+
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))
308+
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := JSON_SERIALIZE(l_deleted_records(%d).", outParamIndex+1, rowIdx+1))
309+
writeQuotedIdentifier(&plsqlBuilder, column)
310+
plsqlBuilder.WriteString(" RETURNING CLOB);\n")
311+
plsqlBuilder.WriteString(" END IF;\n")
312+
}
313+
} else {
314+
// non-JSON as before
315+
dest := createTypedDestination(field)
316+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
317+
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))
318+
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_deleted_records(%d).", outParamIndex+1, rowIdx+1))
319+
writeQuotedIdentifier(&plsqlBuilder, column)
320+
plsqlBuilder.WriteString(";\n")
321+
plsqlBuilder.WriteString(" END IF;\n")
322+
}
303323
outParamIndex++
304324
}
305325
}
306326
}
327+
307328
plsqlBuilder.WriteString("END;")
308329

309330
stmt.SQL.Reset()

oracle/update.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,18 @@ func buildUpdatePLSQL(db *gorm.DB) {
542542
for _, column := range allColumns {
543543
field := findFieldByDBName(schema, column)
544544
if field != nil {
545-
dest := createTypedDestination(field)
545+
var dest interface{}
546+
if isJSONField(field) {
547+
if isRawMessageField(field) {
548+
// RawMessage -> BLOB -> []byte
549+
dest = new([]byte)
550+
} else {
551+
// datatypes.JSON -> text -> string (CLOB)
552+
dest = new(string)
553+
}
554+
} else {
555+
dest = createTypedDestination(field)
556+
}
546557
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
547558
}
548559
}
@@ -553,18 +564,32 @@ func buildUpdatePLSQL(db *gorm.DB) {
553564
for colIdx, column := range allColumns {
554565
field := findFieldByDBName(schema, column)
555566
if field != nil {
556-
// Calculate the correct parameter index (1-based for Oracle)
557567
paramIndex := outParamStartIndex + (rowIdx * len(allColumns)) + colIdx + 1
558568

559-
// Add the assignment to PL/SQL with correct parameter reference
560-
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_updated_records.COUNT > %d THEN\n", rowIdx))
561-
plsqlBuilder.WriteString(fmt.Sprintf(" :%d := l_updated_records(%d).", paramIndex, rowIdx+1))
562-
db.QuoteTo(&plsqlBuilder, column)
563-
plsqlBuilder.WriteString(";\n")
564-
plsqlBuilder.WriteString(" END IF;\n")
569+
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_updated_records.COUNT > %d THEN ", rowIdx))
570+
plsqlBuilder.WriteString(fmt.Sprintf(":%d := ", paramIndex))
571+
572+
if isJSONField(field) {
573+
if isRawMessageField(field) {
574+
plsqlBuilder.WriteString(fmt.Sprintf("l_updated_records(%d).", rowIdx+1))
575+
writeQuotedIdentifier(&plsqlBuilder, column)
576+
} else {
577+
// serialize JSON so it binds as text
578+
plsqlBuilder.WriteString("JSON_SERIALIZE(")
579+
plsqlBuilder.WriteString(fmt.Sprintf("l_updated_records(%d).", rowIdx+1))
580+
writeQuotedIdentifier(&plsqlBuilder, column)
581+
plsqlBuilder.WriteString(" RETURNING CLOB)")
582+
}
583+
} else {
584+
plsqlBuilder.WriteString(fmt.Sprintf("l_updated_records(%d).", rowIdx+1))
585+
writeQuotedIdentifier(&plsqlBuilder, column)
586+
}
587+
588+
plsqlBuilder.WriteString("; END IF;\n")
565589
}
566590
}
567591
}
592+
568593
plsqlBuilder.WriteString("END;")
569594

570595
stmt.SQL.Reset()

0 commit comments

Comments
 (0)