• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stephenafamo / bob / 15001436469

13 May 2025 04:06PM UTC coverage: 36.665%. Remained the same
15001436469

Pull #415

github

stephenafamo
Refactor Preload and ThenLoad
Pull Request #415: Refactor Preload and ThenLoad

0 of 21 new or added lines in 4 files covered. (0.0%)

6507 of 17747 relevant lines covered (36.67%)

212.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/dialect/sqlite/load.go
1
package sqlite
2

3
import (
4
        "context"
5
        "database/sql"
6
        "fmt"
7
        "reflect"
8

9
        "github.com/aarondl/opt"
10
        "github.com/stephenafamo/bob"
11
        "github.com/stephenafamo/bob/dialect/sqlite/dialect"
12
        "github.com/stephenafamo/bob/dialect/sqlite/sm"
13
        "github.com/stephenafamo/bob/internal"
14
        "github.com/stephenafamo/bob/mods"
15
        "github.com/stephenafamo/bob/orm"
16
        "github.com/stephenafamo/scan"
17
)
18

19
// Preloader builds a query mod that modifies the original query to retrieve related fields
20
// while it can be used as a queryMod, it does not have any direct effect.
21
// if using manually, the ApplyPreload method should be called
22
// with the query's context AFTER other mods have been applied
23
type Preloader = orm.Preloader[*dialect.SelectQuery]
24

25
// Settings for preloading relationships
26
type PreloadSettings = orm.PreloadSettings[*dialect.SelectQuery]
27

28
// Modifies preloading relationships
29
type PreloadOption = orm.PreloadOption[*dialect.SelectQuery]
30

31
func PreloadOnly(cols ...string) PreloadOption {
×
NEW
32
        return orm.PreloadOnly[*dialect.SelectQuery](cols)
×
33
}
×
34

35
func PreloadExcept(cols ...string) PreloadOption {
×
NEW
36
        return orm.PreloadExcept[*dialect.SelectQuery](cols)
×
37
}
×
38

39
func PreloadWhere(f ...func(from, to string) []bob.Expression) PreloadOption {
×
NEW
40
        return orm.PreloadWhere[*dialect.SelectQuery](f)
×
41
}
×
42

43
func PreloadAs(alias string) PreloadOption {
×
NEW
44
        return orm.PreloadAs[*dialect.SelectQuery](alias)
×
45
}
×
46

47
func Preload[T any, Ts ~[]T](rel orm.Relationship, cols []string, opts ...PreloadOption) Preloader {
×
NEW
48
        settings := orm.NewPreloadSettings[T, Ts, *dialect.SelectQuery](cols)
×
49
        for _, o := range opts {
×
50
                if o == nil {
×
51
                        continue
×
52
                }
53
                o.ModifyPreloadSettings(&settings)
×
54
        }
55

56
        return buildPreloader[T](func(parent string) (string, mods.QueryMods[*dialect.SelectQuery]) {
×
57
                if parent == "" {
×
58
                        parent = rel.Sides[0].From
×
59
                }
×
60

61
                var alias string
×
62
                var queryMods mods.QueryMods[*dialect.SelectQuery]
×
63

×
64
                for i, side := range rel.Sides {
×
65
                        alias = settings.Alias
×
66
                        if settings.Alias == "" {
×
67
                                alias = fmt.Sprintf("%s_%d", side.To, internal.RandInt())
×
68
                        }
×
69
                        on := make([]bob.Expression, 0, len(side.FromColumns)+len(side.FromWhere)+len(side.ToWhere))
×
70
                        for i, fromCol := range side.FromColumns {
×
71
                                toCol := side.ToColumns[i]
×
72
                                on = append(on, Quote(parent, fromCol).EQ(Quote(alias, toCol)))
×
73
                        }
×
74
                        for _, from := range side.FromWhere {
×
75
                                on = append(on, Quote(parent, from.Column).EQ(Raw(from.SQLValue)))
×
76
                        }
×
77
                        for _, to := range side.ToWhere {
×
78
                                on = append(on, Quote(alias, to.Column).EQ(Raw(to.SQLValue)))
×
79
                        }
×
80
                        if len(settings.Mods) > i {
×
81
                                for _, additional := range settings.Mods[i] {
×
82
                                        on = append(on, additional(parent, alias)...)
×
83
                                }
×
84
                        }
85

86
                        queryMods = append(queryMods, sm.
×
87
                                LeftJoin(orm.SchemaTable(side.To)).
×
88
                                As(alias).
×
89
                                On(on...))
×
90

×
91
                        // so the condition on the next "side" will be on the right table
×
92
                        parent = alias
×
93
                }
94

95
                queryMods = append(queryMods, mods.Preload[*dialect.SelectQuery]{
×
96
                        orm.NewColumns(settings.Columns...).WithParent(alias).WithPrefix(alias + "."),
×
97
                })
×
98
                return alias, queryMods
×
99
        }, rel.Name, settings)
100
}
101

102
func buildPreloader[T any](f func(string) (string, mods.QueryMods[*dialect.SelectQuery]), name string, opt PreloadSettings) Preloader {
×
103
        return func(parent string) (bob.Mod[*dialect.SelectQuery], scan.MapperMod, []bob.Loader) {
×
104
                alias, queryMods := f(parent)
×
105
                prefix := alias + "."
×
106

×
107
                var mapperMods []scan.MapperMod
×
108
                extraLoaders := []bob.Loader{opt.ExtraLoader}
×
109

×
110
                for _, l := range opt.SubLoaders {
×
111
                        queryMod, mapperMod, extraLoader := l(alias)
×
112
                        if queryMod != nil {
×
113
                                queryMods = append(queryMods, queryMod)
×
114
                        }
×
115

116
                        if mapperMod != nil {
×
117
                                mapperMods = append(mapperMods, mapperMod)
×
118
                        }
×
119

120
                        if extraLoader != nil {
×
121
                                extraLoaders = append(extraLoaders, extraLoader...)
×
122
                        }
×
123
                }
124

125
                return queryMods, func(ctx context.Context, cols []string) (scan.BeforeFunc, scan.AfterMod) {
×
126
                        before, after := scan.StructMapper[T](
×
127
                                scan.WithStructTagPrefix(prefix),
×
128
                                scan.WithTypeConverter(typeConverter{}),
×
129
                                scan.WithRowValidator(rowValidator),
×
130
                                scan.WithMapperMods(mapperMods...),
×
131
                        )(ctx, cols)
×
132

×
133
                        return before, func(link, retrieved any) error {
×
NEW
134
                                loader, isLoader := retrieved.(orm.Preloadable)
×
135
                                if !isLoader {
×
136
                                        return fmt.Errorf("object %T cannot pre load", retrieved)
×
137
                                }
×
138

139
                                t, err := after(link)
×
140
                                if err != nil {
×
141
                                        return err
×
142
                                }
×
143

144
                                if err = opt.ExtraLoader.Collect(t); err != nil {
×
145
                                        return err
×
146
                                }
×
147

148
                                return loader.Preload(name, t)
×
149
                        }
150
                }, extraLoaders
151
        }
152
}
153

154
// the row is valid if at least one column is not null
155
func rowValidator(_ []string, vals []reflect.Value) bool {
×
156
        for _, v := range vals {
×
157
                v, ok := v.Interface().(*wrapper)
×
158
                if !ok {
×
159
                        return false
×
160
                }
×
161

162
                if !v.IsNull {
×
163
                        return true
×
164
                }
×
165
        }
166

167
        return false
×
168
}
169

170
type wrapper struct {
171
        IsNull bool
172
        V      any
173
}
174

175
// Scan implements the sql.Scanner interface. If the wrapped type implements
176
// sql.Scanner then it will call that.
177
func (v *wrapper) Scan(value any) error {
×
178
        if value == nil {
×
179
                v.IsNull = true
×
180
                return nil
×
181
        }
×
182

183
        if scanner, ok := v.V.(sql.Scanner); ok {
×
184
                return scanner.Scan(value)
×
185
        }
×
186

187
        return opt.ConvertAssign(v.V, value)
×
188
}
189

190
type typeConverter struct{}
191

192
func (typeConverter) TypeToDestination(typ reflect.Type) reflect.Value {
×
193
        val := reflect.ValueOf(&wrapper{
×
194
                V: reflect.New(typ).Interface(),
×
195
        })
×
196

×
197
        return val
×
198
}
×
199

200
func (typeConverter) ValueFromDestination(val reflect.Value) reflect.Value {
×
201
        return val.Elem().FieldByName("V").Elem().Elem()
×
202
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc