GORM Playground Link

https://github.com/go-gorm/playground/pull/812

Description

Since upgrading to Gorm v1.30.0, Preload with a custom scope that includes an OR condition generates a malformed SQL WHERE clause. The conditions from the scope are duplicated and incorrectly grouped with the foreign key condition, leading to unexpected query results.

This issue was not present in previous versions (e.g., v1.26.1), where the same code produced the correct SQL.

To Reproduce

The following self-contained Go code demonstrates the issue. It defines two models with a one-to-many relationship and attempts to preload the "many" side using a scope with an OR condition.

package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/plugin/soft_delete"
)

// --- Models ---
type Dataset struct {
    ID      uint   `gorm:"primaryKey"`
    Name    string `gorm:"comment:Name;size:30;index;not null;"`
    Columns []*DatasetColumn
}

type DatasetColumn struct {
    ID        uint `gorm:"primaryKey"`
    DatasetID uint
    Dataset   Dataset
    DeletedAt soft_delete.DeletedAt `gorm:"comment:删除时间;uniqueIndex:idx_dc_ddn;not null;"`
    Name      string                `gorm:"comment:名称;size:30;uniqueIndex:idx_dc_ddn;not null;"`
    IsSystem  bool                  `gorm:"comment:是否系统列;not null;default:false;"`
    IsHidden  bool                  `gorm:"comment:是否隐藏;not null;"`
}

func Preload(query string, args ...interface{}) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Preload(query, args...)
    }
}

func QueryColumnsFilter() func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where(db.Where("is_system = ?", true).Or("is_hidden = ?", false))
    }
}

func main() {
    db, err := gorm.Open(sqlite.Open(filepath.Join(os.TempDir(), "gorm.db")), &gorm.Config{})
    if err != nil {
        log.Fatalf("failed to connect to database: %v", err)
    }

    db.Logger = db.Logger.LogMode(logger.Info)

    // --- Setup ---
    db.Migrator().DropTable(&Dataset{}, &DatasetColumn{})
    db.AutoMigrate(&Dataset{}, &DatasetColumn{})

    // Create sample data
    dataset := Dataset{
        Name: "My Dataset",
        Columns: []*DatasetColumn{
            {Name: "System Column 1", IsSystem: true, IsHidden: false},        // Should be loaded
            {Name: "Visible Column 2", IsSystem: false, IsHidden: false},      // Should be loaded
            {Name: "Hidden System Column 3", IsSystem: true, IsHidden: true},  // Should be loaded
            {Name: "Hidden Normal Column 4", IsSystem: false, IsHidden: true}, // Should NOT be loaded
        },
    }
    db.Create(&dataset)

    dataset2 := Dataset{
        Name: "My Dataset2",
        Columns: []*DatasetColumn{
            {Name: "New Colmn", IsSystem: true, IsHidden: false},
        },
    }
    db.Create(&dataset2)

    fmt.Println("--- Reproducing the Bug ---")
    var ds *Dataset
    // Perform the Preload operation that fails
    err = db.Model(&Dataset{}).Where(&Dataset{ID: dataset.ID}).Scopes(Preload("Columns", QueryColumnsFilter())).First(&ds).Error

    if err != nil {
        log.Fatalf("Error during preload: %v", err)
    }

    fmt.Printf("\nLoaded %d columns.\n", len(ds.Columns))
}

Expected Behavior

The Preload query for the Columns association should correctly combine the custom scope's WHERE condition with the foreign key and soft-delete conditions.

The expected SQL (as generated by Gorm v1.26.1) should be:

SELECT * FROM `dataset_columns` WHERE (is_system = true OR is_hidden = false) AND `dataset_columns`.`dataset_id` = 1 AND `dataset_columns`.`deleted_at` = 0

This correctly fetches 3 records based on the sample data.

Actual Behavior

In v1.30.0, the generated SQL for the Preload is incorrect. The WHERE clause is duplicated and improperly nested.

The actual SQL generated is:

SELECT * FROM `dataset_columns` WHERE (`dataset_columns`.`dataset_id` = 1 AND is_system = true OR is_hidden = false AND (`dataset_columns`.`dataset_id` = 1 AND is_system = true OR is_hidden = false)) AND `dataset_columns`.`deleted_at` = 0

This query breaks the intended logic of (A or B) AND C and instead produces a much more complex and incorrect condition.

Possible Cause / Investigation

I believe this bug was introduced in PR #7424 (https://github.com/go-gorm/gorm/pull/7424), specifically in the changes made to callbacks/preload.go. The logic for merging the preloader's conditions with custom scope conditions seems to have been changed in a way that doesn't correctly handle scopes containing OR clauses.

Environment

  • GORM version: v1.30.0
  • GORM driver: sqlite
  • Go version: go1.23.0 (or any recent version)
  • Database: sqlite