aboutsummaryrefslogtreecommitdiff
path: root/select.go
diff options
context:
space:
mode:
Diffstat (limited to 'select.go')
-rw-r--r--select.go77
1 files changed, 75 insertions, 2 deletions
diff --git a/select.go b/select.go
index 23521ed..6616434 100644
--- a/select.go
+++ b/select.go
@@ -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...)
+}