aboutsummaryrefslogtreecommitdiff
path: root/internal/plan.go
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-04-11 20:19:12 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-04-11 20:20:03 +0200
commite9d31443f01eda2ecee66dbc25f154a6949a9c97 (patch)
tree1824c7dc3290e4d38ab111522938e8a33e2f9618 /internal/plan.go
parent3d28ce0650fc85ca054a608bce32f88f2d90295f (diff)
downloadgo-oblast-e9d31443f01eda2ecee66dbc25f154a6949a9c97.tar.gz
reorganize the API from type DB to type Store
Diffstat (limited to 'internal/plan.go')
-rw-r--r--internal/plan.go42
1 files changed, 18 insertions, 24 deletions
diff --git a/internal/plan.go b/internal/plan.go
index 2ed4136..ac199bf 100644
--- a/internal/plan.go
+++ b/internal/plan.go
@@ -4,12 +4,11 @@
package internal
import (
+ "errors"
"fmt"
"reflect"
"slices"
"strings"
-
- "go.xyrillian.de/oblast/info"
)
// Plan holds all information that we can derive from reflecting on a given type.
@@ -39,27 +38,30 @@ type PlannedQuery struct {
ArgumentIndexes [][]int
}
-var (
- tableNameMarkerType = reflect.TypeFor[info.TableNameIs]()
- primaryKeyMarkerType = reflect.TypeFor[info.PrimaryKeyIs]()
-)
+// PlanOpts holds additional arguments to BuildPlan().
+type PlanOpts struct {
+ TableName string
+ PrimaryKeyColumnNames []string
+}
// BuildPlan creates a new plan for the given struct type.
-func BuildPlan(t reflect.Type, dialect Dialect) (Plan, error) {
- p, err := buildPlan(t, dialect)
+func BuildPlan(t reflect.Type, dialect Dialect, opts PlanOpts) (Plan, error) {
+ p, err := buildPlan(t, dialect, opts)
if err != nil {
return Plan{}, fmt.Errorf("cannot use type %s.%s for queries: %w", t.PkgPath(), t.Name(), err)
}
return p, nil
}
-func buildPlan(t reflect.Type, dialect Dialect) (Plan, error) {
+func buildPlan(t reflect.Type, dialect Dialect, opts PlanOpts) (Plan, error) {
if t.Kind() != reflect.Struct {
return Plan{}, fmt.Errorf("expected struct type, but got kind %s", t.Kind().String())
}
var p = Plan{
- IndexByColumnName: make(map[string][]int),
+ TableName: opts.TableName,
+ PrimaryKeyColumnNames: opts.PrimaryKeyColumnNames,
+ IndexByColumnName: make(map[string][]int),
}
// discover addressable fields in this type,
@@ -71,19 +73,6 @@ func buildPlan(t reflect.Type, dialect Dialect) (Plan, error) {
case field.PkgPath != "":
// ignore unexported fields (otherwise reflect.Value.Interface() on the field would panic)
continue
- case field.Type == tableNameMarkerType:
- // only consider this marker when directly on `t` itself, not within embedded fields
- if len(field.Index) == 1 {
- if len(tags) > 1 {
- return Plan{}, fmt.Errorf("invalid table name %q (may not contain commas)", field.Tag.Get("db"))
- }
- p.TableName = tags[0]
- }
- case field.Type == primaryKeyMarkerType:
- // only consider this marker when directly on `t` itself, not within embedded fields
- if len(field.Index) == 1 {
- p.PrimaryKeyColumnNames = tags
- }
case field.Anonymous && field.Type.Kind() == reflect.Struct:
// for embedded struct fields, only consider their members, not the type itself, as a potential column
continue
@@ -115,11 +104,16 @@ func buildPlan(t reflect.Type, dialect Dialect) (Plan, error) {
}
}
+ // validation: defining a primary key only makes sense for records that map onto a single table
+ if len(p.PrimaryKeyColumnNames) > 0 && p.TableName == "" {
+ return Plan{}, errors.New("cannot declare a primary key without also providing the TableNameIs option")
+ }
+
// validation: oblast.PrimaryKeyInfo must refer to columns that exist
for _, columnName := range p.PrimaryKeyColumnNames {
_, ok := p.IndexByColumnName[columnName]
if !ok {
- return Plan{}, fmt.Errorf("PrimaryKeyInfo refers to column %[1]q, but no field has tag `db:%[1]q`", columnName)
+ return Plan{}, fmt.Errorf("no field has tag `db:%q`, but a field of this name was declared in the primary key", columnName)
}
}