Skip to content

Commit 70a7377

Browse files
Merge branch 'main' into joboon-CaseInsensitiveFieldMatching
2 parents 12346fa + 45d606f commit 70a7377

File tree

5 files changed

+266
-45
lines changed

5 files changed

+266
-45
lines changed

.github/workflows/run-tests.yaml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ jobs:
99
GORM_ORACLEDB_USER: ${{ secrets.GORM_ORACLEDB_USER }}
1010
GORM_ORACLEDB_PASSWORD: ${{ secrets.GORM_ORACLEDB_PASSWORD }}
1111
GORM_ORACLEDB_CONNECTSTRING: ${{ secrets.GORM_ORACLEDB_CONNECTSTRING }}
12+
GORM_SYS_PASSOWRD: ${{ secrets.GORM_SYS_PASSOWRD }}
1213
GORM_ORACLEDB_LIBDIR: /home/runner/work/_temp/instantclient_23_9
1314
services:
1415
oracle:
1516
image: gvenzl/oracle-free:latest
1617
env:
1718
APP_USER: ${{ env.GORM_ORACLEDB_USER }}
1819
APP_USER_PASSWORD: ${{ env.GORM_ORACLEDB_PASSWORD }}
19-
ORACLE_RANDOM_PASSWORD: yes
20+
ORACLE_PASSWORD: ${{ env.GORM_SYS_PASSOWRD }}
2021
ports:
2122
- 1521:1521
2223
steps:
@@ -29,15 +30,38 @@ jobs:
2930
- name: Install Oracle Instant Client
3031
run: |
3132
cd $RUNNER_TEMP
32-
# Download the desired Oracle Instant Client zip files
33+
# Download the desired Oracle Instant Client zip files and SQL*Plus packages
3334
curl -sSfLO "https://download.oracle.com/otn_software/linux/instantclient/2390000/instantclient-basic-linux.x64-23.9.0.25.07.zip"
35+
curl -sSfLO "https://download.oracle.com/otn_software/linux/instantclient/2390000/instantclient-sqlplus-linux.x64-23.9.0.25.07.zip"
3436
# Unzip the packages into a single directory
3537
unzip -q -o "instantclient-basic-linux.x64-23.9.0.25.07.zip"
38+
unzip -q -o "instantclient-sqlplus-linux.x64-23.9.0.25.07.zip"
3639
# Install the operating system libaio package
3740
sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1
3841
# Update the runtime link path
3942
echo "/home/runner/work/_temp/instantclient_23_9" | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf
4043
sudo ldconfig
44+
45+
- name: Wait for Oracle to be ready
46+
run: |
47+
# Wait until Oracle is accepting connections
48+
for i in {1..30}; do
49+
if docker exec $(docker ps -qf "ancestor=gvenzl/oracle-free:latest") \
50+
bash -c "echo exit | sqlplus -s / as sysdba" >/dev/null 2>&1; then
51+
echo "Oracle is ready!"
52+
break
53+
fi
54+
echo "Waiting for Oracle..."
55+
sleep 10
56+
done
57+
58+
- name: Alter user quota on tablespace SYSAUX
59+
run: |
60+
cat <<EOF > alter_user.sql
61+
ALTER USER $GORM_ORACLEDB_USER QUOTA UNLIMITED ON SYSAUX;
62+
EOF
63+
64+
$GORM_ORACLEDB_LIBDIR/sqlplus -s "sys/${GORM_SYS_PASSOWRD}@${GORM_ORACLEDB_CONNECTSTRING} AS SYSDBA" @alter_user.sql
4165
4266
- name: Checkout
4367
uses: actions/checkout@v4

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.24.4
44

55
require (
66
github.com/godror/godror v0.49.3
7+
github.com/google/uuid v1.6.0
78
gorm.io/datatypes v1.2.6
89
gorm.io/gorm v1.31.0
910
)
@@ -14,7 +15,6 @@ require (
1415
github.com/go-logfmt/logfmt v0.6.0 // indirect
1516
github.com/go-sql-driver/mysql v1.8.1 // indirect
1617
github.com/godror/knownpb v0.3.0 // indirect
17-
github.com/google/uuid v1.6.0 // indirect
1818
github.com/jinzhu/inflection v1.0.0 // indirect
1919
github.com/jinzhu/now v1.1.5 // indirect
2020
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect

oracle/common.go

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
"strings"
4747
"time"
4848

49+
"github.com/google/uuid"
4950
"gorm.io/datatypes"
5051
"gorm.io/gorm"
5152
"gorm.io/gorm/schema"
@@ -165,10 +166,10 @@ func convertValue(val interface{}) interface{} {
165166
}
166167

167168
// Dereference pointers
168-
v := reflect.ValueOf(val)
169-
for v.Kind() == reflect.Ptr && !v.IsNil() {
170-
v = v.Elem()
171-
val = v.Interface()
169+
rv := reflect.ValueOf(val)
170+
for rv.Kind() == reflect.Ptr && !rv.IsNil() {
171+
rv = rv.Elem()
172+
val = rv.Interface()
172173
}
173174

174175
switch v := val.(type) {
@@ -183,6 +184,13 @@ func convertValue(val interface{}) interface{} {
183184
}
184185
b := []byte(*v)
185186
return b
187+
case *uuid.UUID, *datatypes.UUID:
188+
// Convert nil pointer to a UUID to empty string so that it is stored in the database as NULL
189+
// rather than "00000000-0000-0000-0000-000000000000"
190+
if rv.IsNil() {
191+
return ""
192+
}
193+
return val
186194
case bool:
187195
if v {
188196
return 1
@@ -203,30 +211,13 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
203211
}
204212

205213
targetType := field.FieldType
206-
isPtr := targetType.Kind() == reflect.Ptr
214+
var converted any
215+
216+
// dereference the field if it's a pointer
217+
isPtr := field.FieldType.Kind() == reflect.Ptr
207218
if isPtr {
208-
targetType = targetType.Elem()
209-
}
210-
if field.FieldType == reflect.TypeOf(json.RawMessage{}) {
211-
switch v := value.(type) {
212-
case []byte:
213-
return json.RawMessage(v) // from BLOB
214-
case *[]byte:
215-
if v == nil {
216-
return json.RawMessage(nil)
217-
}
218-
return json.RawMessage(*v)
219-
}
219+
targetType = field.FieldType.Elem()
220220
}
221-
if isJSONField(field) {
222-
switch v := value.(type) {
223-
case string:
224-
return datatypes.JSON([]byte(v))
225-
case []byte:
226-
return datatypes.JSON(v)
227-
}
228-
}
229-
var converted interface{}
230221

231222
switch targetType {
232223
case reflect.TypeOf(gorm.DeletedAt{}):
@@ -235,6 +226,33 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
235226
} else {
236227
converted = gorm.DeletedAt{}
237228
}
229+
230+
case reflect.TypeOf(json.RawMessage{}):
231+
if field.FieldType == reflect.TypeOf(json.RawMessage{}) {
232+
switch vv := value.(type) {
233+
case []byte:
234+
converted = json.RawMessage(vv) // from BLOB
235+
case *[]byte:
236+
if vv == nil {
237+
converted = json.RawMessage(nil)
238+
}
239+
converted = json.RawMessage(*vv)
240+
case string:
241+
return datatypes.JSON([]byte(vv))
242+
default:
243+
converted = value
244+
}
245+
}
246+
case reflect.TypeOf(datatypes.JSON{}):
247+
switch vv := value.(type) {
248+
case string:
249+
converted = datatypes.JSON([]byte(vv))
250+
case []byte:
251+
converted = datatypes.JSON(vv)
252+
default:
253+
converted = value
254+
}
255+
238256
case reflect.TypeOf(time.Time{}):
239257
switch vv := value.(type) {
240258
case time.Time:
@@ -309,7 +327,12 @@ func isJSONField(f *schema.Field) bool {
309327
if f == nil {
310328
return false
311329
}
330+
312331
ft := f.FieldType
332+
if ft.Kind() == reflect.Ptr {
333+
ft = ft.Elem()
334+
}
335+
313336
return ft == _rawMsgT || ft == _gormJSON
314337
}
315338

tests/json_bulk_test.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,23 @@ import (
5050

5151
func TestBasicCRUD_JSONText(t *testing.T) {
5252
type JsonRecord struct {
53-
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
54-
Name string `gorm:"column:name"`
55-
Properties datatypes.JSON `gorm:"column:properties"`
53+
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
54+
Name string `gorm:"column:name"`
55+
Properties datatypes.JSON `gorm:"column:properties"`
56+
PropertiesPtr *datatypes.JSON `gorm:"column:propertiesPtr"`
5657
}
5758

5859
DB.Migrator().DropTable(&JsonRecord{})
59-
if err := DB.AutoMigrate(&JsonRecord{}); err != nil {
60+
if err := DB.Set("gorm:table_options", "TABLESPACE SYSAUX").AutoMigrate(&JsonRecord{}); err != nil {
6061
t.Fatalf("migrate failed: %v", err)
6162
}
6263

6364
// INSERT
65+
json := datatypes.JSON([]byte(`{"env":"prod","owner":"team-x"}`))
6466
rec := JsonRecord{
65-
Name: "json-text",
66-
Properties: datatypes.JSON([]byte(`{"env":"prod","owner":"team-x"}`)),
67+
Name: "json-text",
68+
Properties: json,
69+
PropertiesPtr: &json,
6770
}
6871
if err := DB.Create(&rec).Error; err != nil {
6972
t.Fatalf("create failed: %v", err)
@@ -73,20 +76,23 @@ func TestBasicCRUD_JSONText(t *testing.T) {
7376
}
7477

7578
// UPDATE (with RETURNING)
79+
updateJson := datatypes.JSON([]byte(`{"env":"staging","owner":"team-y","flag":true}`))
7680
var ret JsonRecord
7781
if err := DB.
7882
Clauses(clause.Returning{
7983
Columns: []clause.Column{
8084
{Name: "record_id"},
8185
{Name: "name"},
8286
{Name: "properties"},
87+
{Name: "propertiesPtr"},
8388
},
8489
}).
8590
Model(&ret).
8691
Where("\"record_id\" = ?", rec.ID).
8792
Updates(map[string]any{
88-
"name": "json-text-upd",
89-
"properties": datatypes.JSON([]byte(`{"env":"staging","owner":"team-y","flag":true}`)),
93+
"name": "json-text-upd",
94+
"properties": updateJson,
95+
"propertiesPtr": &updateJson,
9096
}).Error; err != nil {
9197
t.Fatalf("update returning failed: %v", err)
9298
}
@@ -103,6 +109,7 @@ func TestBasicCRUD_JSONText(t *testing.T) {
103109
{Name: "record_id"},
104110
{Name: "name"},
105111
{Name: "properties"},
112+
{Name: "propertiesPtr"},
106113
},
107114
}).
108115
Delete(&deleted).Error; err != nil {
@@ -122,20 +129,23 @@ func TestBasicCRUD_JSONText(t *testing.T) {
122129

123130
func TestBasicCRUD_RawMessage(t *testing.T) {
124131
type RawRecord struct {
125-
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
126-
Name string `gorm:"column:name"`
127-
Properties json.RawMessage `gorm:"column:properties"`
132+
ID uint `gorm:"primaryKey;autoIncrement;column:record_id"`
133+
Name string `gorm:"column:name"`
134+
Properties json.RawMessage `gorm:"column:properties"`
135+
PropertiesPtr *json.RawMessage `gorm:"column:propertiesPtr"`
128136
}
129137

130138
DB.Migrator().DropTable(&RawRecord{})
131139
if err := DB.AutoMigrate(&RawRecord{}); err != nil {
132140
t.Fatalf("migrate failed: %v", err)
133141
}
134142

143+
rawMsg := json.RawMessage(`{"a":1,"b":"x"}`)
135144
// INSERT
136145
rec := RawRecord{
137-
Name: "raw-json",
138-
Properties: json.RawMessage(`{"a":1,"b":"x"}`),
146+
Name: "raw-json",
147+
Properties: rawMsg,
148+
PropertiesPtr: &rawMsg,
139149
}
140150
if err := DB.Create(&rec).Error; err != nil {
141151
t.Fatalf("create failed: %v", err)
@@ -145,24 +155,30 @@ func TestBasicCRUD_RawMessage(t *testing.T) {
145155
}
146156

147157
// UPDATE (with RETURNING)
158+
upatedRawMsg := json.RawMessage(`{"a":2,"c":true}`)
148159
var ret RawRecord
149160
if err := DB.
150161
Clauses(clause.Returning{
151162
Columns: []clause.Column{
152163
{Name: "record_id"},
153164
{Name: "name"},
154165
{Name: "properties"},
166+
{Name: "propertiesPtr"},
155167
},
156168
}).
157169
Model(&ret).
158170
Where("\"record_id\" = ?", rec.ID).
159171
Updates(map[string]any{
160-
"name": "raw-json-upd",
161-
"properties": json.RawMessage(`{"a":2,"c":true}`),
172+
"name": "raw-json-upd",
173+
"properties": upatedRawMsg,
174+
"propertiesPtr": &upatedRawMsg,
162175
}).Error; err != nil {
163176
t.Fatalf("update returning failed: %v", err)
164177
}
165-
if ret.ID != rec.ID || ret.Name != "raw-json-upd" || len(ret.Properties) == 0 {
178+
if ret.ID != rec.ID ||
179+
ret.Name != "raw-json-upd" ||
180+
len(ret.Properties) == 0 ||
181+
ret.PropertiesPtr == nil || (ret.PropertiesPtr != nil && len(*ret.PropertiesPtr) == 0) {
166182
t.Fatalf("unexpected returning row: %#v", ret)
167183
}
168184

@@ -175,6 +191,7 @@ func TestBasicCRUD_RawMessage(t *testing.T) {
175191
{Name: "record_id"},
176192
{Name: "name"},
177193
{Name: "properties"},
194+
{Name: "propertiesPtr"},
178195
},
179196
}).
180197
Delete(&deleted).Error; err != nil {

0 commit comments

Comments
 (0)