diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-04-12 17:59:16 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-04-12 17:59:16 +0200 |
| commit | 9b5b72a549643a9e611f55ae8154fa801c808e5b (patch) | |
| tree | 79c8108ce46e033182c5714a9c1c0a93a80043d0 /select.go | |
| parent | 5e30087db4a06c24c103737d4cb7dcdf06da5b24 (diff) | |
| download | go-oblast-9b5b72a549643a9e611f55ae8154fa801c808e5b.tar.gz | |
add Store.SelectWhere, Store.SelectOneWhere
Diffstat (limited to 'select.go')
| -rw-r--r-- | select.go | 77 |
1 files changed, 75 insertions, 2 deletions
@@ -5,6 +5,7 @@ package oblast import ( "database/sql" + "errors" "fmt" "reflect" @@ -19,7 +20,7 @@ func (s Store[R]) Select(db Handle, query string, args ...any) (result []R, retu // 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 := startQuery(db, s.plan, query, args...) + rows, indexes, err := startSelectQuery(db, s.plan, query, args...) if err != nil { return nil, err } @@ -40,7 +41,45 @@ func (s Store[R]) Select(db Handle, query string, args ...any) (result []R, retu return result, nil } -func startQuery(db Handle, plan internal.Plan, query string, args ...any) (rows *sql.Rows, indexes [][]int, err error) { +// 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(db Handle, partialQuery string, args ...any) (result []R, returnedError 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(db, s.plan, partialQuery, args...) + if err != nil { + return nil, err + } + defer func() { + returnedError = mergeRowsCloseError(returnedError, rows.Close()) + }() + + slots := make([]any, len(indexes)) + for rows.Next() { + var target R + err = collectRow(rows, reflect.ValueOf(&target).Elem(), slots, indexes) + if err != nil { + return nil, err + } + result = append(result, target) + } + + return result, nil +} + +func startSelectQuery(db Handle, plan internal.Plan, query string, args ...any) (rows *sql.Rows, indexes [][]int, err error) { rows, err = db.Query(query, args...) if err != nil { return nil, nil, fmt.Errorf("during Query(): %w", err) @@ -73,6 +112,15 @@ func startQuery(db Handle, plan internal.Plan, query string, args ...any) (rows return rows, indexes, nil } +func startSelectWhereQuery(db Handle, plan internal.Plan, partialQuery string, args ...any) (rows *sql.Rows, indexes [][]int, err error) { + if plan.Select.Query == "" { + return nil, nil, errors.New("cannot execute SelectWhere() because SELECT query could not be autogenerated") + } + query := plan.Select.Query + partialQuery + rows, err = db.Query(query, args...) + return rows, plan.Select.ArgumentIndexes, err +} + func collectRow(rows *sql.Rows, v reflect.Value, slots []any, indexes [][]int) error { for idx, index := range indexes { slots[idx] = v.FieldByIndex(index).Addr().Interface() @@ -106,6 +154,7 @@ func mergeRowsCloseError(err, closeErr error) error { func (s Store[R]) SelectOne(db Handle, query string, args ...any) (result R, err 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 results []R results, err = s.Select(db, query, args...) if err == nil { @@ -120,3 +169,27 @@ func (s Store[R]) SelectOne(db Handle, query string, args ...any) (result R, err } return } + +// 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 significantly more efficient than [Store.SelectWhere] and using it is recommended when possible. +func (s Store[R]) SelectOneWhere(db Handle, partialQuery string, args ...any) (result R, err 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. + + err = selectOneWhere(db, s.plan, reflect.ValueOf(&result).Elem(), partialQuery, args) + return +} + +func selectOneWhere(db Handle, plan internal.Plan, v reflect.Value, partialQuery string, args []any) error { + if plan.Select.Query == "" { + return errors.New("cannot execute SelectOneWhere() because SELECT query could not be autogenerated") + } + query := plan.Select.Query + partialQuery + slots := make([]any, len(plan.Select.ArgumentIndexes)) + for idx, index := range plan.Select.ArgumentIndexes { + slots[idx] = v.FieldByIndex(index).Addr().Interface() + } + return db.QueryRow(query, args...).Scan(slots...) +} |
