Skip to content

Commit e6988a1

Browse files
committed
Merge remote-tracking branch 'origin/main' into joboon-SupportUnquotedIdentifiers
2 parents f6aa7c0 + 847d907 commit e6988a1

25 files changed

+1960
-253
lines changed

.github/workflows/run-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ jobs:
4747
cd tests
4848
go get -t github.com/oracle-samples/gorm-oracle/tests
4949
go get .
50-
go test -failfast
50+
go test -failfast

oracle/clause_builder.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,16 @@ func OnConflictClauseBuilder(c clause.Clause, builder clause.Builder) {
325325
missingColumns = append(missingColumns, conflictCol.Name)
326326
}
327327
}
328+
328329
if len(missingColumns) > 0 {
330+
// primary keys with auto increment will always be missing from create values columns
331+
for _, missingCol := range missingColumns {
332+
field := stmt.Schema.LookUpField(missingCol)
333+
if field != nil && field.PrimaryKey && field.AutoIncrement {
334+
return
335+
}
336+
}
337+
329338
var selectedColumns []string
330339
for col := range selectedColumnSet {
331340
selectedColumns = append(selectedColumns, col)
@@ -335,6 +344,34 @@ func OnConflictClauseBuilder(c clause.Clause, builder clause.Builder) {
335344
return
336345
}
337346

347+
// exclude primary key, default value columns from merge update clause
348+
if len(onConflict.DoUpdates) > 0 {
349+
hasPrimaryKey := false
350+
351+
for _, assignment := range onConflict.DoUpdates {
352+
field := stmt.Schema.LookUpField(assignment.Column.Name)
353+
if field != nil && field.PrimaryKey {
354+
hasPrimaryKey = true
355+
break
356+
}
357+
}
358+
359+
if hasPrimaryKey {
360+
onConflict.DoUpdates = nil
361+
columns := make([]string, 0, len(values.Columns)-1)
362+
for _, col := range values.Columns {
363+
field := stmt.Schema.LookUpField(col.Name)
364+
365+
if field != nil && !field.PrimaryKey && (!field.HasDefaultValue || field.DefaultValueInterface != nil ||
366+
strings.EqualFold(field.DefaultValue, "NULL")) && field.AutoCreateTime == 0 {
367+
columns = append(columns, col.Name)
368+
}
369+
370+
}
371+
onConflict.DoUpdates = append(onConflict.DoUpdates, clause.AssignmentColumns(columns)...)
372+
}
373+
}
374+
338375
// Build MERGE statement
339376
buildMergeInClause(stmt, onConflict, values, conflictColumns)
340377
}

oracle/create.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,11 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau
267267
valuesColumnMap[strings.ToUpper(column.Name)] = true
268268
}
269269

270+
// Filter conflict columns to remove non unique columns
270271
var filteredConflictColumns []clause.Column
271272
for _, conflictCol := range conflictColumns {
272-
if valuesColumnMap[strings.ToUpper(conflictCol.Name)] {
273+
field := stmt.Schema.LookUpField(conflictCol.Name)
274+
if valuesColumnMap[strings.ToUpper(conflictCol.Name)] && (field.Unique || field.AutoIncrement) {
273275
filteredConflictColumns = append(filteredConflictColumns, conflictCol)
274276
}
275277
}
@@ -336,6 +338,7 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau
336338

337339
// Build ON clause using conflict columns
338340
plsqlBuilder.WriteString(" ON (")
341+
339342
for idx, conflictCol := range conflictColumns {
340343
if idx > 0 {
341344
plsqlBuilder.WriteString(" AND ")

oracle/migrator.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,15 +308,47 @@ func (m Migrator) DropColumn(value interface{}, name string) error {
308308
func (m Migrator) AlterColumn(value interface{}, field string) error {
309309
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
310310
if stmt.Schema != nil {
311-
if field := stmt.Schema.LookUpField(field); field != nil {
312-
fileType := m.FullDataTypeOf(field)
311+
if f := stmt.Schema.LookUpField(field); f != nil {
312+
columnTypes, err := m.ColumnTypes(value)
313+
if err != nil {
314+
return err
315+
}
316+
317+
var currentNullable bool
318+
var currentType string
319+
for _, col := range columnTypes {
320+
if strings.EqualFold(col.Name(), f.DBName) {
321+
currentNullable, _ = col.Nullable()
322+
currentType = strings.ToUpper(col.DatabaseTypeName())
323+
break
324+
}
325+
}
326+
327+
desiredNullable := !f.NotNull
328+
desiredType := strings.ToUpper(m.DataTypeOf(f))
329+
330+
// nullable → non-nullable → skip
331+
if currentNullable && !desiredNullable {
332+
return nil
333+
}
334+
335+
// same type + same nullability → skip
336+
if currentNullable == desiredNullable && strings.Contains(currentType, desiredType) {
337+
return nil
338+
}
339+
340+
sql := "ALTER TABLE ? MODIFY ? " + m.DataTypeOf(f)
341+
if f.NotNull {
342+
sql += " NOT NULL"
343+
} else if !currentNullable && desiredNullable {
344+
sql += " NULL"
345+
}
346+
313347
return m.DB.Exec(
314-
"ALTER TABLE ? MODIFY ? ?",
348+
sql,
315349
clause.Table{Name: stmt.Schema.Table},
316-
clause.Column{Name: field.DBName},
317-
fileType,
350+
clause.Column{Name: f.DBName},
318351
).Error
319-
320352
}
321353
}
322354
return fmt.Errorf("failed to look up field with name: %s", field)

tests/associations_many2many_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,179 @@ func TestMany2ManyDuplicateBelongsToAssociation(t *testing.T) {
461461
tests.AssertEqual(t, nil, err)
462462
tests.AssertEqual(t, user2, findUser2)
463463
}
464+
465+
func TestMany2ManyEmptyAssociations(t *testing.T) {
466+
user := User{Name: "TestEmptyAssociations"}
467+
468+
// Create user with no associations
469+
if err := DB.Create(&user).Error; err != nil {
470+
t.Fatalf("errors happened when create user: %v", err)
471+
}
472+
473+
var languages []Language
474+
if err := DB.Model(&user).Association("Languages").Find(&languages); err != nil {
475+
t.Errorf("Error finding empty associations: %v", err)
476+
}
477+
478+
if len(languages) != 0 {
479+
t.Errorf("Expected 0 languages, got %d", len(languages))
480+
}
481+
482+
count := DB.Model(&user).Association("Languages").Count()
483+
if count != 0 {
484+
t.Errorf("Expected count 0 for empty association, got %d", count)
485+
}
486+
487+
if err := DB.Model(&user).Association("Languages").Clear(); err != nil {
488+
t.Errorf("Error clearing empty association: %v", err)
489+
}
490+
491+
if err := DB.Model(&user).Association("Languages").Delete(&Language{}); err != nil {
492+
t.Errorf("Error deleting from empty association: %v", err)
493+
}
494+
}
495+
496+
func TestMany2ManyAssociationCountValidation(t *testing.T) {
497+
user := *GetUser("count-validation", Config{Languages: 3})
498+
499+
if err := DB.Create(&user).Error; err != nil {
500+
t.Fatalf("errors happened when create: %v", err)
501+
}
502+
503+
// Initial count check
504+
initialCount := DB.Model(&user).Association("Languages").Count()
505+
if initialCount != 3 {
506+
t.Fatalf("Expected initial count 3, got %d", initialCount)
507+
}
508+
509+
newLanguages := []Language{
510+
{Code: "count-test-1", Name: "Count Test 1"},
511+
{Code: "count-test-2", Name: "Count Test 2"},
512+
}
513+
DB.Create(&newLanguages)
514+
515+
if err := DB.Model(&user).Association("Languages").Append(&newLanguages); err != nil {
516+
t.Fatalf("Error appending languages: %v", err)
517+
}
518+
519+
// Check count after append
520+
countAfterAppend := DB.Model(&user).Association("Languages").Count()
521+
if countAfterAppend != 5 {
522+
t.Errorf("Expected count 5 after append, got %d", countAfterAppend)
523+
}
524+
525+
replaceLanguage := Language{Code: "count-replace", Name: "Count Replace"}
526+
DB.Create(&replaceLanguage)
527+
528+
if err := DB.Model(&user).Association("Languages").Replace(&replaceLanguage); err != nil {
529+
t.Fatalf("Error replacing languages: %v", err)
530+
}
531+
532+
// Check count after replace
533+
countAfterReplace := DB.Model(&user).Association("Languages").Count()
534+
if countAfterReplace != 1 {
535+
t.Errorf("Expected count 1 after replace, got %d", countAfterReplace)
536+
}
537+
538+
// Verify actual data matches count
539+
var actualLanguages []Language
540+
DB.Model(&user).Association("Languages").Find(&actualLanguages)
541+
if len(actualLanguages) != int(countAfterReplace) {
542+
t.Errorf("Count mismatch: Count() returned %d but Find() returned %d languages",
543+
countAfterReplace, len(actualLanguages))
544+
}
545+
}
546+
547+
func TestMany2ManyConstraintViolations(t *testing.T) {
548+
user := *GetUser("constraint-test", Config{Languages: 1})
549+
550+
if err := DB.Create(&user).Error; err != nil {
551+
t.Fatalf("errors happened when create: %v", err)
552+
}
553+
554+
existingLanguage := user.Languages[0]
555+
556+
// append the same language again
557+
if err := DB.Model(&user).Association("Languages").Append(&existingLanguage); err != nil {
558+
t.Logf("Appending duplicate language resulted in: %v", err)
559+
}
560+
561+
// Verify count is still correct after duplicate append attempt
562+
count := DB.Model(&user).Association("Languages").Count()
563+
if count > 1 {
564+
t.Errorf("Expected count 1 after duplicate append, got %d", count)
565+
}
566+
567+
tempLanguage := Language{Code: "temp-invalid", Name: "Temp Invalid"}
568+
if err := DB.Create(&tempLanguage).Error; err != nil {
569+
t.Logf("Could not create temp language for FK test: %v", err)
570+
return
571+
}
572+
573+
// append, then delete the language record to create inconsistency
574+
if err := DB.Model(&user).Association("Languages").Append(&tempLanguage); err != nil {
575+
t.Logf("Could not append temp language: %v", err)
576+
}
577+
578+
// Delete the language record directly
579+
DB.Unscoped().Delete(&tempLanguage)
580+
581+
// access associations after deleting referenced record
582+
var languages []Language
583+
if err := DB.Model(&user).Association("Languages").Find(&languages); err != nil {
584+
t.Logf("Finding associations after FK deletion resulted in: %v", err)
585+
}
586+
587+
// Get count before mass operation for verification
588+
countBeforeMass := DB.Model(&user).Association("Languages").Count()
589+
t.Logf("Language count before mass operation: %d", countBeforeMass)
590+
591+
var manyLanguages []Language
592+
for i := 0; i < 10; i++ {
593+
manyLanguages = append(manyLanguages, Language{
594+
Code: fmt.Sprintf("mass-test-%d", i),
595+
Name: fmt.Sprintf("Mass Test %d", i),
596+
})
597+
}
598+
599+
// Create the languages first
600+
if err := DB.Create(&manyLanguages).Error; err != nil {
601+
t.Logf("Creating many languages failed: %v", err)
602+
return
603+
}
604+
605+
// append all at once
606+
if err := DB.Model(&user).Association("Languages").Append(&manyLanguages); err != nil {
607+
t.Logf("Mass append operation resulted in: %v", err)
608+
}
609+
610+
// Verify the operation completed with proper count validation
611+
finalCount := DB.Model(&user).Association("Languages").Count()
612+
// Should have at least the previous count + 10 new languages
613+
expectedMinCount := countBeforeMass + 10
614+
615+
t.Logf("Final language count after mass operation: %d (expected at least %d)", finalCount, expectedMinCount)
616+
617+
if finalCount < expectedMinCount {
618+
t.Errorf("Expected at least %d languages after mass append, got %d", expectedMinCount, finalCount)
619+
}
620+
621+
var actualLanguages []Language
622+
if err := DB.Model(&user).Association("Languages").Find(&actualLanguages); err == nil {
623+
actualCount := len(actualLanguages)
624+
if actualCount != int(finalCount) {
625+
t.Errorf("Count mismatch: Count() returned %d but Find() returned %d languages",
626+
finalCount, actualCount)
627+
}
628+
}
629+
630+
if err := DB.Model(&user).Association("Languages").Clear(); err != nil {
631+
t.Errorf("Error clearing associations after mass operation: %v", err)
632+
}
633+
634+
// Verify clear worked
635+
countAfterClear := DB.Model(&user).Association("Languages").Count()
636+
if countAfterClear != 0 {
637+
t.Errorf("Expected count 0 after clear, got %d", countAfterClear)
638+
}
639+
}

0 commit comments

Comments
 (0)