Skip to content

Fixing bulk merge #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 54 additions & 13 deletions oracle/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ func validateCreateData(stmt *gorm.Statement) error {

// Build PL/SQL block for bulk INSERT/MERGE with RETURNING
func buildBulkInsertPLSQL(db *gorm.DB, createValues clause.Values) {
sanitizeCreateValuesForBulkArrays(db.Statement, &createValues)

stmt := db.Statement
schema := stmt.Schema

Expand Down Expand Up @@ -238,6 +240,8 @@ func buildBulkInsertPLSQL(db *gorm.DB, createValues clause.Values) {

// Build PL/SQL block for bulk MERGE with RETURNING (OnConflict case)
func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClause clause.Clause) {
sanitizeCreateValuesForBulkArrays(db.Statement, &createValues)

stmt := db.Statement
schema := stmt.Schema

Expand Down Expand Up @@ -409,6 +413,25 @@ func buildBulkMergePLSQL(db *gorm.DB, createValues clause.Values, onConflictClau
}
}
plsqlBuilder.WriteString("\n")
} else {
onCols := map[string]struct{}{}
for _, c := range conflictColumns {
onCols[strings.ToUpper(c.Name)] = struct{}{}
}

// Picking the first non-ON column from the INSERT/MERGE columns
var noopCol string
for _, c := range createValues.Columns {
if _, inOn := onCols[strings.ToUpper(c.Name)]; !inOn {
noopCol = c.Name
break
}
}
plsqlBuilder.WriteString(" WHEN MATCHED THEN UPDATE SET t.")
writeQuotedIdentifier(&plsqlBuilder, noopCol)
plsqlBuilder.WriteString(" = t.")
writeQuotedIdentifier(&plsqlBuilder, noopCol)
plsqlBuilder.WriteString("\n")
}

// WHEN NOT MATCHED THEN INSERT (unless DoNothing for inserts)
Expand Down Expand Up @@ -791,19 +814,6 @@ func handleSingleRowReturning(db *gorm.DB) {
}
}

// Simplified RETURNING clause addition for single row operations
func addReturningClause(db *gorm.DB, fields []*schema.Field) {
if len(fields) == 0 {
return
}

columns := make([]clause.Column, len(fields))
for idx, field := range fields {
columns[idx] = clause.Column{Name: field.DBName}
}
db.Statement.AddClauseIfNotExists(clause.Returning{Columns: columns})
}

// Handle bulk RETURNING results for PL/SQL operations
func getBulkReturningValues(db *gorm.DB, rowCount int) {
if db.Statement.Schema == nil {
Expand Down Expand Up @@ -923,3 +933,34 @@ func handleLastInsertId(db *gorm.DB, result sql.Result) {
}
}
}

// This replaces expressions (clause.Expr) in bulk insert values
// with appropriate NULL placeholders based on the column's data type. This ensures that
// PL/SQL array binding remains consistent and avoids unsupported expressions during
// FORALL bulk operations.
func sanitizeCreateValuesForBulkArrays(stmt *gorm.Statement, cv *clause.Values) {
for r := range cv.Values {
for c, col := range cv.Columns {
v := cv.Values[r][c]
switch v.(type) {
case clause.Expr:
if f := findFieldByDBName(stmt.Schema, col.Name); f != nil {
switch f.DataType {
case schema.Int, schema.Uint:
cv.Values[r][c] = sql.NullInt64{}
case schema.Float:
cv.Values[r][c] = sql.NullFloat64{}
case schema.String:
cv.Values[r][c] = sql.NullString{}
case schema.Time:
cv.Values[r][c] = sql.NullTime{}
default:
cv.Values[r][c] = nil
}
} else {
cv.Values[r][c] = nil
}
}
}
}
}
51 changes: 22 additions & 29 deletions tests/associations_has_many_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,34 @@ import (
)

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

if err := DB.Create(&user).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
}

CheckUser(t, user, user)
CheckUserSkipUpdatedAt(t, user, user)

// Find
var user2 User
DB.Find(&user2, "id = ?", user.ID)
DB.Find(&user2, "\"id\" = ?", user.ID)
DB.Model(&user2).Association("Pets").Find(&user2.Pets)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

var pets []Pet
DB.Model(&user).Where("name = ?", user.Pets[0].Name).Association("Pets").Find(&pets)
DB.Model(&user).Where("\"name\" = ?", user.Pets[0].Name).Association("Pets").Find(&pets)

if len(pets) != 1 {
t.Fatalf("should only find one pets, but got %v", len(pets))
}

CheckPet(t, pets[0], *user.Pets[0])

if count := DB.Model(&user).Where("name = ?", user.Pets[1].Name).Association("Pets").Count(); count != 1 {
if count := DB.Model(&user).Where("\"name\" = ?", user.Pets[1].Name).Association("Pets").Count(); count != 1 {
t.Fatalf("should only find one pets, but got %v", count)
}

if count := DB.Model(&user).Where("name = ?", "not found").Association("Pets").Count(); count != 0 {
if count := DB.Model(&user).Where("\"name\" = ?", "not found").Association("Pets").Count(); count != 0 {
t.Fatalf("should only find no pet with invalid conditions, but got %v", count)
}

Expand All @@ -94,7 +93,7 @@ func TestHasManyAssociation(t *testing.T) {
}

user.Pets = append(user.Pets, &pet)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Pets", 3, "AfterAppend")

Expand All @@ -113,7 +112,7 @@ func TestHasManyAssociation(t *testing.T) {
user.Pets = append(user.Pets, &pet)
}

CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Pets", 5, "AfterAppendSlice")

Expand All @@ -129,7 +128,7 @@ func TestHasManyAssociation(t *testing.T) {
}

user.Pets = []*Pet{&pet2}
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user2, "Pets", 1, "AfterReplace")

Expand Down Expand Up @@ -160,20 +159,19 @@ func TestHasManyAssociation(t *testing.T) {
}

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

if err := DB.Create(&user).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
}

CheckUser(t, user, user)
CheckUserSkipUpdatedAt(t, user, user)

// Find
var user2 User
DB.Find(&user2, "id = ?", user.ID)
DB.Find(&user2, "\"id\" = ?", user.ID)
DB.Model(&user2).Association("Team").Find(&user2.Team)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

// Count
AssertAssociationCount(t, user, "Team", 2, "")
Expand All @@ -190,7 +188,7 @@ func TestSingleTableHasManyAssociation(t *testing.T) {
}

user.Team = append(user.Team, team)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Team", 3, "AfterAppend")

Expand All @@ -209,7 +207,7 @@ func TestSingleTableHasManyAssociation(t *testing.T) {
user.Team = append(user.Team, team)
}

CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Team", 5, "AfterAppendSlice")

Expand All @@ -225,7 +223,7 @@ func TestSingleTableHasManyAssociation(t *testing.T) {
}

user.Team = []User{team2}
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user2, "Team", 1, "AfterReplace")

Expand Down Expand Up @@ -256,7 +254,6 @@ func TestSingleTableHasManyAssociation(t *testing.T) {
}

func TestHasManyAssociationForSlice(t *testing.T) {
t.Skip()
users := []User{
*GetUser("slice-hasmany-1", Config{Pets: 2}),
*GetUser("slice-hasmany-2", Config{Pets: 0}),
Expand Down Expand Up @@ -311,7 +308,6 @@ func TestHasManyAssociationForSlice(t *testing.T) {
}

func TestSingleTableHasManyAssociationForSlice(t *testing.T) {
t.Skip()
users := []User{
*GetUser("slice-hasmany-1", Config{Team: 2}),
*GetUser("slice-hasmany-2", Config{Team: 0}),
Expand Down Expand Up @@ -368,20 +364,19 @@ func TestSingleTableHasManyAssociationForSlice(t *testing.T) {
}

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

if err := DB.Create(&user).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
}

CheckUser(t, user, user)
CheckUserSkipUpdatedAt(t, user, user)

// Find
var user2 User
DB.Find(&user2, "id = ?", user.ID)
DB.Find(&user2, "\"id\" = ?", user.ID)
DB.Model(&user2).Association("Toys").Find(&user2.Toys)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

// Count
AssertAssociationCount(t, user, "Toys", 2, "")
Expand All @@ -398,7 +393,7 @@ func TestPolymorphicHasManyAssociation(t *testing.T) {
}

user.Toys = append(user.Toys, toy)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Toys", 3, "AfterAppend")

Expand All @@ -417,7 +412,7 @@ func TestPolymorphicHasManyAssociation(t *testing.T) {
user.Toys = append(user.Toys, toy)
}

CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Toys", 5, "AfterAppendSlice")

Expand All @@ -433,7 +428,7 @@ func TestPolymorphicHasManyAssociation(t *testing.T) {
}

user.Toys = []Toy{toy2}
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user2, "Toys", 1, "AfterReplace")

Expand Down Expand Up @@ -464,7 +459,6 @@ func TestPolymorphicHasManyAssociation(t *testing.T) {
}

func TestPolymorphicHasManyAssociationForSlice(t *testing.T) {
t.Skip()
users := []User{
*GetUser("slice-hasmany-1", Config{Toys: 2}),
*GetUser("slice-hasmany-2", Config{Toys: 0, Tools: 2}),
Expand Down Expand Up @@ -601,8 +595,7 @@ func TestHasManyAssociationUnscoped(t *testing.T) {
}

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

if err := DB.Create(&user).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
Expand Down
12 changes: 5 additions & 7 deletions tests/associations_many2many_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,21 +235,20 @@ func TestMany2ManyAssociationForSlice(t *testing.T) {
}

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

if err := DB.Create(&user).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
}

CheckUser(t, user, user)
CheckUserSkipUpdatedAt(t, user, user)

// Find
var user2 User
DB.Find(&user2, "\"id\" = ?", user.ID)
DB.Model(&user2).Association("Friends").Find(&user2.Friends)

CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

// Count
AssertAssociationCount(t, user, "Friends", 2, "")
Expand All @@ -262,7 +261,7 @@ func TestSingleTableMany2ManyAssociation(t *testing.T) {
}

user.Friends = append(user.Friends, &friend)
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Friends", 3, "AfterAppend")

Expand All @@ -274,7 +273,7 @@ func TestSingleTableMany2ManyAssociation(t *testing.T) {

user.Friends = append(user.Friends, friends...)

CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user, "Friends", 5, "AfterAppendSlice")

Expand All @@ -286,7 +285,7 @@ func TestSingleTableMany2ManyAssociation(t *testing.T) {
}

user.Friends = []*User{&friend2}
CheckUser(t, user2, user)
CheckUserSkipUpdatedAt(t, user2, user)

AssertAssociationCount(t, user2, "Friends", 1, "AfterReplace")

Expand Down Expand Up @@ -317,7 +316,6 @@ func TestSingleTableMany2ManyAssociation(t *testing.T) {
}

func TestSingleTableMany2ManyAssociationForSlice(t *testing.T) {
t.Skip()
users := []User{
*GetUser("slice-many2many-1", Config{Team: 2}),
*GetUser("slice-many2many-2", Config{Team: 0}),
Expand Down
2 changes: 1 addition & 1 deletion tests/multi_primary_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ func TestManyToManyWithCustomizedForeignKeys2(t *testing.T) {

func TestCompositePrimaryKeysAssociations(t *testing.T) {
t.Skip()

type Label struct {
BookID *uint `gorm:"primarykey"`
Name string `gorm:"primarykey"`
Expand Down
Loading
Loading