Files
orm/query.go
zeekayandHanzo Dev 91d7a22290 Map a row to whatever the caller wanted, on the query itself
A query knows how to fetch T. Turning those rows into the shape a caller
actually wants was the caller's loop every time — fetch, allocate, range,
transform — written once per call site and wrong in the same place each time it
was wrong: an empty result that still runs the transform once, or returns an
empty slice where the rest of the package returns nil.

Map and OneMap put that on the query. They are generic methods (Go 1.27): the
receiver already carries T, and the method declares U itself, which is the thing
1.27 added. Inference does the rest — the closure's return type IS U, so a call
site names no type arguments at all:

	summaries, err := orm.Select[User](db, "users").Map(ctx, func(u User) Summary {
		return Summary{ID: u.ID, Label: u.Email}
	})

Empty is nil and the transform does not run, in all three. That is the branch
worth pinning: a transform invoked once on a zero value reads as one result to
everything that ranges over the answer, and ModelQuery.Map was the one of the
three the suite could not reach — generic_methods_test.go is an external test
and cannot build a ModelQuery, so its test sits inside the package.

go.mod names 1.27 because generic methods require it; the compiler is explicit
about that and the tree builds clean on it.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 21:33:48 -07:00

179 lines
3.8 KiB
Go

package orm
import "context"
// ModelQuery is a typed query wrapper for a single model type.
// It provides a fluent interface for filtering, ordering, and retrieving entities.
type ModelQuery[T any] struct {
model *Model[T]
query Query
}
// Filter adds a filter condition.
func (q *ModelQuery[T]) Filter(filterStr string, value any) *ModelQuery[T] {
q.query = q.query.Filter(filterStr, value)
return q
}
// Order adds an ordering clause.
func (q *ModelQuery[T]) Order(fieldPath string) *ModelQuery[T] {
q.query = q.query.Order(fieldPath)
return q
}
// Limit sets the maximum number of results.
func (q *ModelQuery[T]) Limit(limit int) *ModelQuery[T] {
q.query = q.query.Limit(limit)
return q
}
// Offset sets the number of results to skip.
func (q *ModelQuery[T]) Offset(offset int) *ModelQuery[T] {
q.query = q.query.Offset(offset)
return q
}
// Ancestor restricts the query to descendants of the given key.
func (q *ModelQuery[T]) Ancestor(key Key) *ModelQuery[T] {
q.query = q.query.Ancestor(key)
return q
}
// Get retrieves the first matching entity into the model.
func (q *ModelQuery[T]) Get() (bool, error) {
entity := q.model.self()
key, ok, err := q.query.First(entity)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
q.model.SetKey(key)
q.model.Init(q.model.db)
q.model.captureSnapshot()
// Auto-deserialize
if err := DeserializeFields(entity); err != nil {
return true, err
}
return true, nil
}
// First returns the first matching entity as a new instance, or ErrNotFound.
func (q *ModelQuery[T]) First() (*T, error) {
entity := New[T](q.model.db)
m := getModel[T](entity)
if m == nil {
return nil, ErrNotFound
}
key, ok, err := q.query.First(entity)
if err != nil {
return nil, err
}
if !ok {
return nil, ErrNotFound
}
m.SetKey(key)
m.Init(q.model.db)
m.captureSnapshot()
if err := DeserializeFields(entity); err != nil {
return nil, err
}
return entity, nil
}
// GetAll returns all matching entities.
func (q *ModelQuery[T]) GetAll(ctx context.Context) ([]*T, error) {
if ctx == nil {
ctx = context.Background()
}
var items []*T
keys, err := q.query.GetAll(ctx, &items)
if err != nil {
return nil, err
}
// Wire up each entity
for i, entity := range items {
m := getModel[T](entity)
if m != nil && i < len(keys) {
m.SetKey(keys[i])
m.Init(q.model.db)
m.captureSnapshot()
DeserializeFields(entity)
}
}
return items, nil
}
// Count returns the number of matching entities.
func (q *ModelQuery[T]) Count(ctx context.Context) (int, error) {
if ctx == nil {
ctx = context.Background()
}
return q.query.Count(ctx)
}
// ById retrieves an entity by its string ID.
func (q *ModelQuery[T]) ById(id string) (bool, error) {
entity := q.model.self()
key, ok, err := q.query.ById(id, entity)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
q.model.SetKey(key)
q.model.Init(q.model.db)
q.model.captureSnapshot()
if err := DeserializeFields(entity); err != nil {
return true, err
}
return true, nil
}
// KeyExists checks if the given key exists.
func (q *ModelQuery[T]) KeyExists(key Key) (bool, error) {
return q.query.KeyExists(key)
}
// IdExists checks if the given ID exists.
func (q *ModelQuery[T]) IdExists(id string) (Key, bool, error) {
return q.query.IdExists(id)
}
// Inner returns the underlying Query for advanced operations.
func (q *ModelQuery[T]) Inner() Query {
return q.query
}
// Map executes GetAll and transforms each retrieved *T into U using fn.
// This leverages Go 1.27 generic methods: ModelQuery[T] declares method type parameter U.
func (q *ModelQuery[T]) Map[U any](ctx context.Context, fn func(*T) U) ([]U, error) {
items, err := q.GetAll(ctx)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, nil
}
out := make([]U, len(items))
for i, item := range items {
out[i] = fn(item)
}
return out, nil
}