1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
|
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0
package oblast
import (
"context"
"database/sql"
"errors"
"fmt"
"reflect"
. "go.xyrillian.de/gg/option"
"go.xyrillian.de/oblast/handle"
)
// Select executes the provided SQL query and fills an instance of the record type R for each row in the result set,
// according to the column names reported by the database as part of the result set.
//
// An error is returned if any column name in the result set does not correspond to an addressable field in R.
func (s Store[R]) Select(ctx context.Context, db Handle, query string, args ...any) ([]R, error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
rows, indexes, err := startSelectQuery(ctx, db, s.plan, query, args...)
if err != nil {
return nil, err
}
var result []R
slots := make([]any, len(indexes))
for rows.Next() {
var target *R
result, target = growRecordSlice(result)
err = collectRow(rows, s.plan, reflect.ValueOf(target).Elem(), slots, indexes)
if err != nil {
return nil, err
}
}
return result, newIOError(err, "Rows.Err", rows.Err())
}
// Appends an empty R to the slice and returns a pointer to it, as well as the updated slice.
// It is more efficient to write:
//
// var result []R
// for rows.Next() {
// var target *R
// result, target = growRecordSlice(result)
// doSomethingWith(rows, reflect.ValueOf(target).Elem())
// }
//
// Instead of the more obvious:
//
// var result []R
// for rows.Next() {
// var target R
// doSomethingWith(rows, reflect.ValueOf(&target).Elem())
// result = append(result, target)
// }
//
// In the second phrasing, `target` escapes to the heap because of `reflect.ValueOf(&target)`,
// causing an additional allocation for `target` as well as a memcpy of `target` during `append()`.
func growRecordSlice[R any](records []R) (newRecords []R, target *R) {
var zero R
newRecords = append(records, zero)
return newRecords, &newRecords[len(newRecords)-1]
}
// SelectWhere is like [Store.Select], but you only provide the part of the SELECT query that comes after the WHERE.
// The initial part ("SELECT ... FROM ... WHERE") is autogenerated and prepended to partialQuery.
// This has two benefits:
// - It is more efficient because the strategy for loading result rows into the record type R has already been precomputed during [NewStore],
// whereas a regular [Store.Select] must inspect the column names in the result set for each [Store.Select] call.
// - For record types that contain only some of the columns of the corresponding database table,
// the autogenerated SELECT query will only load exactly the necessary fields and nothing else.
//
// partialQuery is implied to start right after the WHERE keyword, which is added automatically.
// To select all records unconditionally, provide a partialQuery of "TRUE", leading to a full query of "SELECT ... FROM ... WHERE TRUE".
// Besides a condition for the WHERE clause, it may contain additional clauses, such as ORDER BY or LIMIT.
//
// Returns an error if [NewStore] was called without the [TableNameIs] option, which is required to generate a query for this method.
func (s Store[R]) SelectWhere(ctx context.Context, db Handle, partialQuery string, args ...any) ([]R, error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
rows, indexes, err := startSelectWhereQuery(ctx, db, s.plan, partialQuery, args...)
if err != nil {
return nil, err
}
var result []R
slots := make([]any, len(indexes))
for rows.Next() {
var target *R
result, target = growRecordSlice(result)
err = collectRow(rows, s.plan, reflect.ValueOf(target).Elem(), slots, indexes)
if err != nil {
return nil, err
}
}
return result, newIOError(err, "Rows.Err", rows.Err())
}
func startSelectQuery(ctx context.Context, db Handle, plan plan, query string, args ...any) (handle.Rows, [][]int, error) {
rows, err := db.Query(ctx, query, args)
if err != nil {
return nil, nil, fmt.Errorf("during Query(): %w", err)
}
columnNames, err := rows.Columns()
if err != nil {
err = fmt.Errorf("during rows.Columns(): %w", err)
return nil, nil, newIOError(err, "Rows.Close", rows.Close())
}
indexes := make([][]int, len(columnNames))
for idx, columnName := range columnNames {
var ok bool
indexes[idx], ok = plan.IndexByColumnName[columnName]
if !ok {
err := fmt.Errorf(
"result has column %q in position %d, but no field in type %s has `db:%[1]q`",
columnName, idx, plan.TypeName,
)
return nil, nil, newIOError(err, "Rows.Close", rows.Close())
}
}
return rows, indexes, nil
}
func startSelectWhereQuery(ctx context.Context, db Handle, plan plan, partialQuery string, args ...any) (rows handle.Rows, indexes [][]int, err error) {
if plan.Select.Query == "" {
return nil, nil, errors.New("cannot execute SelectWhere() because query could not be autogenerated")
}
query := plan.Select.Query + partialQuery
rows, err = db.Query(ctx, query, args)
if err != nil {
err = fmt.Errorf("during Query(): %w", err)
}
return rows, plan.Select.ScanIndexes, err
}
func collectRow(rows handle.Rows, plan plan, v reflect.Value, slots []any, indexes [][]int) error {
for _, field := range plan.TransparentPointerStructFields {
f := v.FieldByIndex(field.Index)
f.Set(reflect.New(f.Type().Elem()))
}
for idx, index := range indexes {
slots[idx] = v.FieldByIndex(index).Addr().Interface()
}
err := rows.Scan(slots...)
if err != nil {
return newIOError(err, "Rows.Close", rows.Close())
}
return nil
}
// SelectOne executes the provided SQL query and fills an instance of the record type R if there is exactly one row in the result set,
// according to the column names reported by the database as part of the result set.
//
// If there are no rows in the result set, [sql.ErrNoRows] is returned.
//
// Warning: Because of limitations in the interface of database/sql, this function is built on [Store.Select] and cannot be any faster than it.
// For maximum performance, use [Store.SelectOneWhere] which avoids the overhead of potentially having to read multiple rows.
func (s Store[R]) SelectOne(ctx context.Context, db Handle, query string, args ...any) (R, error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
//
// NOTE: The "limitation in the interface of database/sql" is that type sql.Row does not have the Columns() method,
// which we need when mapping result columns to struct fields for user-provided queries.
results, err := s.Select(ctx, db, query, args...)
switch {
case err != nil:
var zero R
return zero, err
case len(results) == 0:
var zero R
return zero, sql.ErrNoRows
default:
return results[0], nil
}
}
// SelectOneOrNone is like SelectOne, but returns [None] instead of [sql.ErrNoRows].
func (s Store[R]) SelectOneOrNone(ctx context.Context, db Handle, query string, args ...any) (Option[R], error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
results, err := s.Select(ctx, db, query, args...)
switch {
case err != nil:
return None[R](), err
case len(results) == 0:
return None[R](), nil
default:
return Some(results[0]), nil
}
}
// SelectOneWhere is like [Store.SelectOne], but you only provide the part of the SELECT query that comes after the WHERE.
// See [Store.SelectWhere] for an explanation of how the full query is constructed from this partial query.
//
// This method is more efficient than [Store.SelectOne] on CPU runtime, but has a slight memory allocation overhead per call from query preparation.
// This can be avoided by using [Store.PrepareSelectQueryWhere] instead.
func (s Store[R]) SelectOneWhere(ctx context.Context, db Handle, partialQuery string, args ...any) (R, error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
var result R
err := selectOneWhere(ctx, db, s.plan, reflect.ValueOf(&result).Elem(), partialQuery, args)
return result, err
}
// SelectOneOrNoneWhere is like SelectOneWhere, but returns [None] instead of [sql.ErrNoRows].
func (s Store[R]) SelectOneOrNoneWhere(ctx context.Context, db Handle, partialQuery string, args ...any) (Option[R], error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
return noRowsToNone(s.SelectOneWhere(ctx, db, partialQuery, args...))
}
func selectOneWhere(ctx context.Context, db Handle, plan plan, v reflect.Value, partialQuery string, args []any) error {
if plan.Select.Query == "" {
return errors.New("cannot execute SelectOneWhere() because query could not be autogenerated")
}
query := plan.Select.Query + partialQuery
return selectOne(ctx, db, plan, v, query, args)
}
func selectOne(ctx context.Context, db Handle, plan plan, v reflect.Value, query string, args []any) error {
for _, field := range plan.TransparentPointerStructFields {
f := v.FieldByIndex(field.Index)
f.Set(reflect.New(f.Type().Elem()))
}
slots := make([]any, len(plan.Select.ScanIndexes))
for idx, index := range plan.Select.ScanIndexes {
slots[idx] = v.FieldByIndex(index).Addr().Interface()
}
stmt, err := db.Prepare(ctx, query, false)
if err != nil {
return err
}
err = stmt.QueryRow(ctx, args, slots)
return newIOError(err, "Stmt.Close", stmt.Close())
}
func noRowsToNone[R any](record R, err error) (Option[R], error) {
switch {
case err == nil:
return Some(record), nil
case errors.Is(err, sql.ErrNoRows):
return None[R](), nil
default:
return None[R](), err
}
}
// PrepareSelectQueryWhere performs the same query string preparation as [Store.SelectWhere] or [Store.SelectOneWhere].
// The resulting query can then be executed multiple times without incurring repeated memory allocation overhead from this preparation step.
func (s Store[R]) PrepareSelectQueryWhere(partialQuery string) (PreparedSelectQuery[R], error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
query, err := prepareSelectQueryWhere(s.plan, partialQuery)
return PreparedSelectQuery[R]{s, query}, err
}
// MustPrepareSelectQueryWhere is like [Store.PrepareSelectQueryWhere], but panics on error.
func (s Store[R]) MustPrepareSelectQueryWhere(partialQuery string) PreparedSelectQuery[R] {
q, err := s.PrepareSelectQueryWhere(partialQuery)
if err != nil {
panic(err.Error())
}
return q
}
func prepareSelectQueryWhere(plan plan, partialQuery string) (string, error) {
if plan.Select.Query == "" {
return "", errors.New("cannot execute PrepareSelectQueryWhere() because query could not be autogenerated")
}
return plan.Select.Query + partialQuery, nil
}
// PreparedSelectQuery holds a pre-computed SELECT query that was customized by the user.
// This type is an optimization to avoid performing the same query string manipulations over and over again in hot paths.
//
// It is returned by [Store.PrepareSelectQueryWhere].
type PreparedSelectQuery[R any] struct {
store Store[R]
query string
}
// Select behaves the same as [Store.SelectWhere], but uses the query that was precomputed when q was constructed.
func (q PreparedSelectQuery[R]) Select(ctx context.Context, db Handle, args ...any) ([]R, error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
rows, indexes, err := startSelectQuery(ctx, db, q.store.plan, q.query, args...)
if err != nil {
return nil, err
}
var result []R
slots := make([]any, len(indexes))
for rows.Next() {
var target *R
result, target = growRecordSlice(result)
err = collectRow(rows, q.store.plan, reflect.ValueOf(target).Elem(), slots, indexes)
if err != nil {
return nil, err
}
}
return result, newIOError(err, "Rows.Err", rows.Err())
}
// SelectOne behaves the same as [Store.SelectOneWhere], but uses the query that was precomputed when q was constructed.
func (q PreparedSelectQuery[R]) SelectOne(ctx context.Context, db Handle, args ...any) (R, error) {
// NOTE: This function body should be as short as possible to reduce the binary size after monomorphization.
// Any expression that does not depend on type R should be factored out into a reusable function.
var result R
err := selectOne(ctx, db, q.store.plan, reflect.ValueOf(&result).Elem(), q.query, args)
return result, err
}
// SelectOneOrNone is like SelectOne, but returns [None] instead of [sql.ErrNoRows].
func (q PreparedSelectQuery[R]) SelectOneOrNone(ctx context.Context, db Handle, args ...any) (Option[R], error) {
return noRowsToNone(q.SelectOne(ctx, db, args...))
}
|