Skip to content

Commit 98989fd

Browse files
committed
Merge branch 'main' into query_tests_fix
2 parents 3db5acb + ff09070 commit 98989fd

35 files changed

+255
-63
lines changed

.github/workflows/run-tests.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Run Tests
2+
3+
on: push
4+
5+
jobs:
6+
run-tests:
7+
runs-on: ubuntu-latest
8+
env:
9+
GORM_ORACLEDB_USER: ${{ secrets.GORM_ORACLEDB_USER }}
10+
GORM_ORACLEDB_PASSWORD: ${{ secrets.GORM_ORACLEDB_PASSWORD }}
11+
GORM_ORACLEDB_CONNECTSTRING: ${{ secrets.GORM_ORACLEDB_CONNECTSTRING }}
12+
GORM_ORACLEDB_LIBDIR: /home/runner/work/_temp/instantclient_23_9
13+
services:
14+
oracle:
15+
image: gvenzl/oracle-free:latest
16+
env:
17+
APP_USER: ${{ env.GORM_ORACLEDB_USER }}
18+
APP_USER_PASSWORD: ${{ env.GORM_ORACLEDB_PASSWORD }}
19+
ORACLE_RANDOM_PASSWORD: yes
20+
ports:
21+
- 1521:1521
22+
steps:
23+
24+
- name: Set up Go
25+
uses: actions/setup-go@v4
26+
with:
27+
go-version: '1.24.4'
28+
29+
- name: Install Oracle Instant Client
30+
run: |
31+
cd $RUNNER_TEMP
32+
# Download the desired Oracle Instant Client zip files
33+
curl -sSfLO "https://download.oracle.com/otn_software/linux/instantclient/2390000/instantclient-basic-linux.x64-23.9.0.25.07.zip"
34+
# Unzip the packages into a single directory
35+
unzip -q -o "instantclient-basic-linux.x64-23.9.0.25.07.zip"
36+
# Install the operating system libaio package
37+
sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1
38+
# Update the runtime link path
39+
echo "/home/runner/work/_temp/instantclient_23_9" | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf
40+
sudo ldconfig
41+
42+
- name: Checkout
43+
uses: actions/checkout@v4
44+
45+
- name: Run all tests under tests directory
46+
run: |
47+
cd tests
48+
go get -t github.com/oracle-samples/gorm-oracle/tests
49+
go get .
50+
go test -failfast

oracle/clause_builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func ReturningClauseBuilder(c clause.Clause, builder clause.Builder) {
165165
var dest interface{}
166166
if stmt.Schema != nil {
167167
if field := findFieldByDBName(stmt.Schema, column.Name); field != nil {
168-
dest = createTypedDestination(field.FieldType)
168+
dest = createTypedDestination(field)
169169
} else {
170170
dest = new(string) // Default to string for unknown fields
171171
}

oracle/common.go

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,30 @@ func findFieldByDBName(schema *schema.Schema, dbName string) *schema.Field {
100100
}
101101

102102
// Create typed destination for OUT parameters
103-
func createTypedDestination(fieldType reflect.Type) interface{} {
104-
// Handle pointer types
105-
if fieldType.Kind() == reflect.Ptr {
106-
fieldType = fieldType.Elem()
103+
func createTypedDestination(f *schema.Field) interface{} {
104+
if f == nil {
105+
var s string
106+
return &s
107107
}
108108

109-
// Type-safe handling for known GORM types and SQL null types
110-
switch fieldType {
111-
case reflect.TypeOf(gorm.DeletedAt{}):
109+
ft := f.FieldType
110+
for ft.Kind() == reflect.Ptr {
111+
ft = ft.Elem()
112+
}
113+
114+
if ft == reflect.TypeOf(gorm.DeletedAt{}) {
112115
return new(sql.NullTime)
113-
case reflect.TypeOf(time.Time{}):
116+
}
117+
if ft == reflect.TypeOf(time.Time{}) {
118+
if !f.NotNull { // nullable column => keep NULLs
119+
return new(sql.NullTime)
120+
}
114121
return new(time.Time)
122+
}
123+
124+
switch ft {
125+
case reflect.TypeOf(sql.NullTime{}):
126+
return new(sql.NullTime)
115127
case reflect.TypeOf(sql.NullInt64{}):
116128
return new(sql.NullInt64)
117129
case reflect.TypeOf(sql.NullInt32{}):
@@ -120,33 +132,28 @@ func createTypedDestination(fieldType reflect.Type) interface{} {
120132
return new(sql.NullFloat64)
121133
case reflect.TypeOf(sql.NullBool{}):
122134
return new(sql.NullBool)
123-
case reflect.TypeOf(sql.NullTime{}):
124-
return new(sql.NullTime)
125135
}
126136

127-
// Handle primitive types by Kind
128-
switch fieldType.Kind() {
137+
switch ft.Kind() {
138+
case reflect.String:
139+
return new(string)
140+
141+
case reflect.Bool:
142+
return new(int64)
143+
129144
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
130-
return new(int64) // Oracle returns NUMBER as int64
131-
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
145+
return new(int64)
146+
147+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
132148
return new(uint64)
149+
133150
case reflect.Float32, reflect.Float64:
134-
return new(float64) // Oracle returns FLOAT as float64
135-
case reflect.Bool:
136-
return new(int64) // Oracle NUMBER(1) for boolean
137-
case reflect.String:
138-
return new(string)
139-
case reflect.Struct:
140-
// For time.Time specifically
141-
if fieldType == reflect.TypeOf(time.Time{}) {
142-
return new(time.Time)
143-
}
144-
// For other structs, use string as safe fallback
145-
return new(string)
146-
default:
147-
// For unknown types, use string as safe fallback
148-
return new(string)
151+
return new(float64)
149152
}
153+
154+
// Fallback
155+
var s string
156+
return &s
150157
}
151158

152159
// Convert values for Oracle-specific types
@@ -182,7 +189,7 @@ func convertValue(val interface{}) interface{} {
182189

183190
// Convert Oracle values back to Go types
184191
func convertFromOracleToField(value interface{}, field *schema.Field) interface{} {
185-
if value == nil {
192+
if value == nil || field == nil {
186193
return nil
187194
}
188195

@@ -194,7 +201,6 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
194201

195202
var converted interface{}
196203

197-
// Handle special types first using type-safe comparisons
198204
switch targetType {
199205
case reflect.TypeOf(gorm.DeletedAt{}):
200206
if nullTime, ok := value.(sql.NullTime); ok {
@@ -203,7 +209,31 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
203209
converted = gorm.DeletedAt{}
204210
}
205211
case reflect.TypeOf(time.Time{}):
206-
converted = value
212+
switch vv := value.(type) {
213+
case time.Time:
214+
converted = vv
215+
case sql.NullTime:
216+
if vv.Valid {
217+
converted = vv.Time
218+
} else {
219+
// DB returned NULL
220+
if isPtr {
221+
return nil // -> *time.Time(nil)
222+
}
223+
// non-pointer time.Time: represent NULL as zero time
224+
return time.Time{}
225+
}
226+
default:
227+
converted = value
228+
}
229+
230+
case reflect.TypeOf(sql.NullTime{}):
231+
if nullTime, ok := value.(sql.NullTime); ok {
232+
converted = nullTime
233+
} else {
234+
converted = sql.NullTime{}
235+
}
236+
207237
case reflect.TypeOf(sql.NullInt64{}):
208238
if nullInt, ok := value.(sql.NullInt64); ok {
209239
converted = nullInt
@@ -228,25 +258,19 @@ func convertFromOracleToField(value interface{}, field *schema.Field) interface{
228258
} else {
229259
converted = sql.NullBool{}
230260
}
231-
case reflect.TypeOf(sql.NullTime{}):
232-
if nullTime, ok := value.(sql.NullTime); ok {
233-
converted = nullTime
234-
} else {
235-
converted = sql.NullTime{}
236-
}
237261
default:
238-
// Handle primitive types
262+
// primitives and everything else
239263
converted = convertPrimitiveType(value, targetType)
240264
}
241265

242-
// Handle pointer types
243-
if isPtr && converted != nil {
244-
if isZeroValueForPointer(converted, targetType) {
266+
// Pointer targets: nil for "zero-ish", else allocate and set.
267+
if isPtr {
268+
if isZeroFor(targetType, converted) {
245269
return nil
246270
}
247271
ptr := reflect.New(targetType)
248272
ptr.Elem().Set(reflect.ValueOf(converted))
249-
converted = ptr.Interface()
273+
return ptr.Interface()
250274
}
251275

252276
return converted
@@ -442,3 +466,28 @@ func isNullValue(value interface{}) bool {
442466
return false
443467
}
444468
}
469+
470+
func isZeroFor(t reflect.Type, v interface{}) bool {
471+
if v == nil {
472+
return true
473+
}
474+
rv := reflect.ValueOf(v)
475+
if !rv.IsValid() {
476+
return true
477+
}
478+
// exact type match?
479+
if rv.Type() == t {
480+
// special-case time.Time
481+
if t == reflect.TypeOf(time.Time{}) {
482+
return rv.Interface().(time.Time).IsZero()
483+
}
484+
// generic zero check
485+
z := reflect.Zero(t)
486+
return reflect.DeepEqual(rv.Interface(), z.Interface())
487+
}
488+
// If types differ (e.g., sql.NullTime), treat invalid as zero
489+
if nt, ok := v.(sql.NullTime); ok {
490+
return !nt.Valid
491+
}
492+
return false
493+
}

oracle/create.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau
490490
for rowIdx := 0; rowIdx < len(createValues.Values); rowIdx++ {
491491
for _, column := range allColumns {
492492
if field := findFieldByDBName(schema, column); field != nil {
493-
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field.FieldType)})
493+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
494494
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_affected_records.COUNT > %d THEN :%d := l_affected_records(%d).", rowIdx, outParamIndex+1, rowIdx+1))
495495
writeQuotedIdentifier(&plsqlBuilder, column)
496496
plsqlBuilder.WriteString("; END IF;\n")
@@ -602,7 +602,7 @@ func buildBulkInsertOnlyPLSQL(db *gorm.DB, createValues clause.Values) {
602602
quotedColumn := columnBuilder.String()
603603

604604
if field := findFieldByDBName(schema, column); field != nil {
605-
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field.FieldType)})
605+
stmt.Vars = append(stmt.Vars, sql.Out{Dest: createTypedDestination(field)})
606606
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_inserted_records.COUNT > %d THEN :%d := l_inserted_records(%d).%s; END IF;\n",
607607
rowIdx, outParamIndex+1, rowIdx+1, quotedColumn))
608608
outParamIndex++

oracle/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ func buildBulkDeletePLSQL(db *gorm.DB) {
278278
for _, column := range allColumns {
279279
field := findFieldByDBName(schema, column)
280280
if field != nil {
281-
dest := createTypedDestination(field.FieldType)
281+
dest := createTypedDestination(field)
282282
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
283283

284284
plsqlBuilder.WriteString(fmt.Sprintf(" IF l_deleted_records.COUNT > %d THEN\n", rowIdx))

oracle/oracle.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,6 @@ func (d Dialector) Migrator(db *gorm.DB) gorm.Migrator {
133133

134134
// Determines the data type for a schema field
135135
func (d Dialector) DataTypeOf(field *schema.Field) string {
136-
// TODO : Not sure why this is added in the reference implementation
137-
if _, found := field.TagSettings["RESTRICT"]; found {
138-
delete(field.TagSettings, "RESTRICT")
139-
}
140-
141136
switch field.DataType {
142137
case schema.Bool:
143138
return d.getBooleanType()

oracle/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ func buildUpdatePLSQL(db *gorm.DB) {
544544
for _, column := range allColumns {
545545
field := findFieldByDBName(schema, column)
546546
if field != nil {
547-
dest := createTypedDestination(field.FieldType)
547+
dest := createTypedDestination(field)
548548
stmt.Vars = append(stmt.Vars, sql.Out{Dest: dest})
549549
}
550550
}

tests/associations_has_many_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
)
4848

4949
func TestHasManyAssociation(t *testing.T) {
50+
t.Skip()
5051
user := *GetUser("hasmany", Config{Pets: 2})
5152

5253
if err := DB.Create(&user).Error; err != nil {
@@ -159,6 +160,7 @@ func TestHasManyAssociation(t *testing.T) {
159160
}
160161

161162
func TestSingleTableHasManyAssociation(t *testing.T) {
163+
t.Skip()
162164
user := *GetUser("hasmany", Config{Team: 2})
163165

164166
if err := DB.Create(&user).Error; err != nil {
@@ -254,6 +256,7 @@ func TestSingleTableHasManyAssociation(t *testing.T) {
254256
}
255257

256258
func TestHasManyAssociationForSlice(t *testing.T) {
259+
t.Skip()
257260
users := []User{
258261
*GetUser("slice-hasmany-1", Config{Pets: 2}),
259262
*GetUser("slice-hasmany-2", Config{Pets: 0}),
@@ -308,6 +311,7 @@ func TestHasManyAssociationForSlice(t *testing.T) {
308311
}
309312

310313
func TestSingleTableHasManyAssociationForSlice(t *testing.T) {
314+
t.Skip()
311315
users := []User{
312316
*GetUser("slice-hasmany-1", Config{Team: 2}),
313317
*GetUser("slice-hasmany-2", Config{Team: 0}),
@@ -364,6 +368,7 @@ func TestSingleTableHasManyAssociationForSlice(t *testing.T) {
364368
}
365369

366370
func TestPolymorphicHasManyAssociation(t *testing.T) {
371+
t.Skip()
367372
user := *GetUser("hasmany", Config{Toys: 2})
368373

369374
if err := DB.Create(&user).Error; err != nil {
@@ -459,6 +464,7 @@ func TestPolymorphicHasManyAssociation(t *testing.T) {
459464
}
460465

461466
func TestPolymorphicHasManyAssociationForSlice(t *testing.T) {
467+
t.Skip()
462468
users := []User{
463469
*GetUser("slice-hasmany-1", Config{Toys: 2}),
464470
*GetUser("slice-hasmany-2", Config{Toys: 0, Tools: 2}),
@@ -595,6 +601,7 @@ func TestHasManyAssociationUnscoped(t *testing.T) {
595601
}
596602

597603
func TestHasManyAssociationReplaceWithNonValidValue(t *testing.T) {
604+
t.Skip()
598605
user := User{Name: "jinzhu", Languages: []Language{{Name: "EN"}}}
599606

600607
if err := DB.Create(&user).Error; err != nil {

tests/associations_many2many_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func TestMany2ManyAssociationForSlice(t *testing.T) {
235235
}
236236

237237
func TestSingleTableMany2ManyAssociation(t *testing.T) {
238+
t.Skip()
238239
user := *GetUser("many2many", Config{Friends: 2})
239240

240241
if err := DB.Create(&user).Error; err != nil {
@@ -316,6 +317,7 @@ func TestSingleTableMany2ManyAssociation(t *testing.T) {
316317
}
317318

318319
func TestSingleTableMany2ManyAssociationForSlice(t *testing.T) {
320+
t.Skip()
319321
users := []User{
320322
*GetUser("slice-many2many-1", Config{Team: 2}),
321323
*GetUser("slice-many2many-2", Config{Team: 0}),
@@ -370,6 +372,7 @@ func TestSingleTableMany2ManyAssociationForSlice(t *testing.T) {
370372
}
371373

372374
func TestDuplicateMany2ManyAssociation(t *testing.T) {
375+
t.Skip()
373376
user1 := User{Name: "TestDuplicateMany2ManyAssociation-1", Languages: []Language{
374377
{Code: "TestDuplicateMany2ManyAssociation-language-1"},
375378
{Code: "TestDuplicateMany2ManyAssociation-language-2"},
@@ -433,6 +436,7 @@ func TestConcurrentMany2ManyAssociation(t *testing.T) {
433436
}
434437

435438
func TestMany2ManyDuplicateBelongsToAssociation(t *testing.T) {
439+
t.Skip()
436440
user1 := User{Name: "TestMany2ManyDuplicateBelongsToAssociation-1", Friends: []*User{
437441
{Name: "TestMany2ManyDuplicateBelongsToAssociation-friend-1", Company: Company{
438442
ID: 1,

0 commit comments

Comments
 (0)