diff --git a/tests/update_has_one_test.go b/tests/update_has_one_test.go index 28d5e52..1f39d83 100644 --- a/tests/update_has_one_test.go +++ b/tests/update_has_one_test.go @@ -40,6 +40,7 @@ package tests import ( "database/sql" + "errors" "testing" "time" @@ -47,6 +48,7 @@ import ( . "github.com/oracle-samples/gorm-oracle/tests/utils" "gorm.io/gorm" + "gorm.io/gorm/clause" "gorm.io/gorm/utils/tests" ) @@ -128,6 +130,87 @@ func TestUpdateHasOne(t *testing.T) { CheckPetSkipUpdatedAt(t, pet4, pet) }) + t.Run("ReplaceAssociation", func(t *testing.T) { + user := *GetUser("replace-has-one", Config{}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("errors happened when create user: %v", err) + } + + acc1 := Account{AccountNumber: "first-account"} + user.Account = acc1 + + if err := DB.Save(&user).Error; err != nil { + t.Fatalf("errors happened when saving user with first account: %v", err) + } + + acc2 := Account{AccountNumber: "second-account"} + user.Account = acc2 + if err := DB.Session(&gorm.Session{FullSaveAssociations: true}).Save(&user).Error; err != nil { + t.Fatalf("errors happened when replacing association: %v", err) + } + + var result User + DB.Preload("Account").First(&result, user.ID) + if result.Account.AccountNumber != "second-account" { + t.Fatalf("expected replaced account to have AccountNumber 'second-account', got %v", result.Account.AccountNumber) + } + }) + + t.Run("ClearHasOneAssociation", func(t *testing.T) { + user := *GetUser("nullify-has-one", Config{}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("errors happened when create user: %v", err) + } + + user.Account = Account{AccountNumber: "to-be-nullified"} + if err := DB.Save(&user).Error; err != nil { + t.Fatalf("errors happened when saving user: %v", err) + } + + DB.Model(&user).Association("Account").Clear() + + var result User + DB.Preload("Account").First(&result, user.ID) + if result.Account.AccountNumber != "" { + t.Fatalf("expected account to be nullified/empty, got %v", result.Account.AccountNumber) + } + }) + + t.Run("ClearPolymorphicAssociation", func(t *testing.T) { + pet := Pet{Name: "clear-poly"} + pet.Toy = Toy{Name: "polytoy"} + DB.Create(&pet) + + DB.Model(&pet).Association("Toy").Clear() + + var pet2 Pet + DB.Preload("Toy").First(&pet2, pet.ID) + if pet2.Toy.Name != "" { + t.Fatalf("expected Toy cleared, got %v", pet2.Toy.Name) + } + }) + + t.Run("UpdateWithoutAssociation", func(t *testing.T) { + user := *GetUser("no-assoc-update", Config{}) + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("errors happened when create user: %v", err) + } + newName := user.Name + "-updated" + if err := DB.Model(&user).Update("name", newName).Error; err != nil { + t.Fatalf("errors happened when updating only parent: %v", err) + } + var result User + DB.Preload("Account").First(&result, user.ID) + if result.Name != newName { + t.Fatalf("user name not updated as expected") + } + if result.Account.ID != 0 { + t.Fatalf("expected no Account associated, got ID %v", result.Account.ID) + } + }) + t.Run("Restriction", func(t *testing.T) { type CustomizeAccount struct { gorm.Model @@ -175,4 +258,201 @@ func TestUpdateHasOne(t *testing.T) { tests.AssertEqual(t, account2.Number, number) tests.AssertEqual(t, account2.Number2, cusUser.Account.Number2) }) + + t.Run("AssociationWithoutPreload", func(t *testing.T) { + user := *GetUser("no-preload", Config{}) + user.Account = Account{AccountNumber: "np-account"} + DB.Create(&user) + + var result User + DB.First(&result, user.ID) // no preload + if result.Account.AccountNumber != "" { + t.Fatalf("expected Account field empty without preload, got %v", result.Account.AccountNumber) + } + + var acc Account + DB.First(&acc, "\"user_id\" = ?", user.ID) + if acc.AccountNumber != "np-account" { + t.Fatalf("account not found as expected") + } + }) + + t.Run("SkipFullSaveAssociations", func(t *testing.T) { + user := *GetUser("skip-fsa", Config{}) + user.Account = Account{AccountNumber: "skipfsa"} + DB.Create(&user) + + user.Account.AccountNumber = "should-not-update" + if err := DB.Session(&gorm.Session{FullSaveAssociations: false}).Save(&user).Error; err != nil { + t.Fatalf("error saving with FSA false: %v", err) + } + + var acc Account + DB.First(&acc, "\"user_id\" = ?", user.ID) + if acc.AccountNumber != "skipfsa" { + t.Fatalf("account should not have updated, got %v", acc.AccountNumber) + } + }) + + t.Run("HasOneZeroForeignKey", func(t *testing.T) { + now := time.Now() + user := User{Name: "zero-value-clear", Age: 18, Birthday: &now} + DB.Create(&user) + + account := Account{AccountNumber: "to-clear", UserID: sql.NullInt64{Int64: int64(user.ID), Valid: true}} + DB.Create(&account) + + account.UserID = sql.NullInt64{Int64: 0, Valid: false} + DB.Model(&account).Select("UserID").Updates(account) + + var result User + DB.Preload("Account").First(&result, user.ID) + if result.Account.AccountNumber != "" { + t.Fatalf("expected account cleared, got %v", result.Account.AccountNumber) + } + }) + + t.Run("PolymorphicZeroForeignKey", func(t *testing.T) { + pet := Pet{Name: "poly-zero"} + pet.Toy = Toy{Name: "polytoy-zero"} + DB.Create(&pet) + + pet.Toy.OwnerID = "" + DB.Model(&pet.Toy).Select("OwnerID").Updates(&pet.Toy) + + var pet2 Pet + DB.Preload("Toy").First(&pet2, pet.ID) + if pet2.Toy.Name != "" { + t.Fatalf("expected polymorphic association cleared, got %v", pet2.Toy.Name) + } + }) + + t.Run("InvalidForeignKey", func(t *testing.T) { + acc := Account{AccountNumber: "badfk", UserID: sql.NullInt64{Int64: 99999999, Valid: true}} + err := DB.Create(&acc).Error + if err == nil { + t.Fatalf("expected foreign key constraint error, got nil") + } + }) + + t.Run("UpdateWithSelectOmit", func(t *testing.T) { + user := *GetUser("select-omit", Config{}) + user.Account = Account{AccountNumber: "selomit"} + DB.Create(&user) + + user.Name = "selomit-updated" + user.Account.AccountNumber = "selomit-updated" + if err := DB.Select("Name").Omit("Account").Save(&user).Error; err != nil { + t.Fatalf("error on select/omit: %v", err) + } + + var acc Account + DB.First(&acc, "\"user_id\" = ?", user.ID) + if acc.AccountNumber != "selomit" { + t.Fatalf("account should not update with Omit(Account), got %v", acc.AccountNumber) + } + }) + + t.Run("NestedUpdate", func(t *testing.T) { + user := *GetUser("nested-update", Config{}) + user.Account = Account{AccountNumber: "nested"} + DB.Create(&user) + + user.Name = "nested-updated" + user.Account.AccountNumber = "nested-updated" + if err := DB.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user).Error; err != nil { + t.Fatalf("nested update failed: %v", err) + } + + var result User + DB.Preload("Account").First(&result, user.ID) + if result.Name != "nested-updated" || result.Account.AccountNumber != "nested-updated" { + t.Fatalf("nested update didn't apply: %v / %v", result.Name, result.Account.AccountNumber) + } + }) + + t.Run("EmptyStructNoFullSave", func(t *testing.T) { + user := *GetUser("empty-nofsa", Config{}) + user.Account = Account{AccountNumber: "keep"} + DB.Create(&user) + + user.Account = Account{} + if err := DB.Save(&user).Error; err != nil { + t.Fatalf("save failed: %v", err) + } + + var result User + DB.Preload("Account").First(&result, user.ID) + if result.Account.AccountNumber != "keep" { + t.Fatalf("account should not be cleared without FullSaveAssociations") + } + }) + + t.Run("DeleteParentCascade", func(t *testing.T) { + type AccountCascadeDelete struct { + gorm.Model + AccountNumber string + UserID uint + } + + type UserCascadeDelete struct { + gorm.Model + Name string + Account AccountCascadeDelete `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;"` + } + + DB.Migrator().DropTable(&AccountCascadeDelete{}, &UserCascadeDelete{}) + if err := DB.AutoMigrate(&UserCascadeDelete{}, &AccountCascadeDelete{}); err != nil { + t.Fatalf("failed to migrate: %v", err) + } + + user := UserCascadeDelete{ + Name: "delete-parent", + Account: AccountCascadeDelete{ + AccountNumber: "cascade", + }, + } + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("failed to create user: %v", err) + } + + if err := DB.Unscoped().Delete(&user).Error; err != nil { + t.Fatalf("delete parent failed: %v", err) + } + + var acc AccountCascadeDelete + err := DB.First(&acc, "\"user_id\" = ?", user.ID).Error + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("expected account deleted, got %v", acc) + } + }) + + t.Run("OmitAllAssociations", func(t *testing.T) { + user := *GetUser("omit-assoc", Config{}) + user.Account = Account{AccountNumber: "original-child"} + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("failed to create user: %v", err) + } + + newName := "parent-updated" + user.Name = newName + user.Account.AccountNumber = "child-updated" + + if err := DB.Model(&user).Omit(clause.Associations).Updates(user).Error; err != nil { + t.Fatalf("update with omit associations failed: %v", err) + } + + var result User + DB.Preload("Account").First(&result, user.ID) + + if result.Name != newName { + t.Fatalf("expected parent name updated to %v, got %v", newName, result.Name) + } + + if result.Account.AccountNumber != "original-child" { + t.Fatalf("expected child to remain unchanged, got %v", result.Account.AccountNumber) + } + }) + }