Skip to content

Commit ea7f332

Browse files
authored
Fix Query.One + Filter behavior (#248) (#249)
* fix Query.One + Filter behavior (#248) * Query.One: delay unmarshaling until success (preserves old behavior) * add some docs explaining ErrTooMany
1 parent cb20568 commit ea7f332

File tree

2 files changed

+58
-42
lines changed

2 files changed

+58
-42
lines changed

query.go

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ func (q *Query) ConsumedCapacity(cc *ConsumedCapacity) *Query {
208208

209209
// One executes this query and retrieves a single result,
210210
// unmarshaling the result to out.
211+
// This uses the DynamoDB GetItem API when possible, otherwise Query.
212+
// If the query returns more than one result, [ErrTooMany] may be returned. This is intended as a diagnostic for query mistakes.
213+
// To avoid [ErrTooMany], set the [Query.Limit] to 1.
211214
func (q *Query) One(ctx context.Context, out interface{}) error {
212215
if q.err != nil {
213216
return q.err
@@ -239,34 +242,20 @@ func (q *Query) One(ctx context.Context, out interface{}) error {
239242
}
240243

241244
// If not, try a Query.
242-
req := q.queryInput()
243-
244-
var res *dynamodb.QueryOutput
245-
err := q.table.db.retry(ctx, func() error {
246-
var err error
247-
res, err = q.table.db.client.Query(ctx, req)
248-
q.cc.incRequests()
249-
if err != nil {
250-
return err
251-
}
252-
253-
switch {
254-
case len(res.Items) == 0:
255-
return ErrNotFound
256-
case len(res.Items) > 1 && q.limit != 1:
257-
return ErrTooMany
258-
case res.LastEvaluatedKey != nil && q.searchLimit != 0:
259-
return ErrTooMany
260-
}
261-
262-
return nil
263-
})
264-
if err != nil {
245+
iter := q.newIter(unmarshalItem)
246+
var item Item
247+
ok := iter.Next(ctx, &item)
248+
if err := iter.Err(); err != nil {
265249
return err
266250
}
267-
q.cc.add(res.ConsumedCapacity)
268-
269-
return unmarshalItem(res.Items[0], out)
251+
if !ok {
252+
return ErrNotFound
253+
}
254+
// Best effort: do we have any pending unused items?
255+
if iter.hasMore() {
256+
return ErrTooMany
257+
}
258+
return unmarshalItem(item, out)
270259
}
271260

272261
// Count executes this request, returning the number of results.
@@ -314,6 +303,14 @@ func (q *Query) Count(ctx context.Context) (int, error) {
314303
return count, nil
315304
}
316305

306+
func (q *Query) newIter(unmarshal unmarshalFunc) *queryIter {
307+
return &queryIter{
308+
query: q,
309+
unmarshal: unmarshal,
310+
err: q.err,
311+
}
312+
}
313+
317314
// queryIter is the iterator for Query operations
318315
type queryIter struct {
319316
query *Query
@@ -422,6 +419,13 @@ func (itr *queryIter) Next(ctx context.Context, out interface{}) bool {
422419
return itr.err == nil
423420
}
424421

422+
func (itr *queryIter) hasMore() bool {
423+
if itr.query.limit > 0 && itr.n == itr.query.limit {
424+
return false
425+
}
426+
return itr.output != nil && itr.idx < len(itr.output.Items)
427+
}
428+
425429
// Err returns the error encountered, if any.
426430
// You should check this after Next is finished.
427431
func (itr *queryIter) Err() error {
@@ -458,11 +462,7 @@ func (itr *queryIter) LastEvaluatedKey(ctx context.Context) (PagingKey, error) {
458462

459463
// All executes this request and unmarshals all results to out, which must be a pointer to a slice.
460464
func (q *Query) All(ctx context.Context, out interface{}) error {
461-
iter := &queryIter{
462-
query: q,
463-
unmarshal: unmarshalAppendTo(out),
464-
err: q.err,
465-
}
465+
iter := q.newIter(unmarshalAppendTo(out))
466466
for iter.Next(ctx, out) {
467467
}
468468
return iter.Err()
@@ -471,11 +471,7 @@ func (q *Query) All(ctx context.Context, out interface{}) error {
471471
// AllWithLastEvaluatedKey executes this request and unmarshals all results to out, which must be a pointer to a slice.
472472
// This returns a PagingKey you can use with StartFrom to split up results.
473473
func (q *Query) AllWithLastEvaluatedKey(ctx context.Context, out interface{}) (PagingKey, error) {
474-
iter := &queryIter{
475-
query: q,
476-
unmarshal: unmarshalAppendTo(out),
477-
err: q.err,
478-
}
474+
iter := q.newIter(unmarshalAppendTo(out))
479475
for iter.Next(ctx, out) {
480476
}
481477
lek, err := iter.LastEvaluatedKey(ctx)
@@ -484,12 +480,7 @@ func (q *Query) AllWithLastEvaluatedKey(ctx context.Context, out interface{}) (P
484480

485481
// Iter returns a results iterator for this request.
486482
func (q *Query) Iter() PagingIter {
487-
iter := &queryIter{
488-
query: q,
489-
unmarshal: unmarshalItem,
490-
err: q.err,
491-
}
492-
return iter
483+
return q.newIter(unmarshalItem)
493484
}
494485

495486
// can we use the get item API?

query_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dynamo
22

33
import (
44
"context"
5+
"errors"
56
"reflect"
67
"testing"
78
"time"
@@ -111,6 +112,30 @@ func TestGetAllCount(t *testing.T) {
111112
t.Errorf("bad result for get one. %v ≠ %v", one, item)
112113
}
113114

115+
// trigger ErrTooMany
116+
one = widget{}
117+
err = table.Get("UserID", 42).Range("Time", Greater, "0").Consistent(true).One(ctx, &one)
118+
if !errors.Is(err, ErrTooMany) {
119+
t.Errorf("bad error from get one. %v ≠ %v", err, ErrTooMany)
120+
}
121+
122+
// suppress ErrTooMany with Limit(1)
123+
one = widget{}
124+
err = table.Get("UserID", 42).Range("Time", Greater, "0").Consistent(true).Limit(1).One(ctx, &one)
125+
if err != nil {
126+
t.Error("unexpected error:", err)
127+
}
128+
if one.UserID == 0 {
129+
t.Errorf("bad result for get one: %v", one)
130+
}
131+
132+
// trigger ErrNotFound via SearchLimit + Filter + One
133+
one = widget{}
134+
err = table.Get("UserID", 42).Range("Time", Greater, "0").Filter("Msg = ?", item.Msg).Consistent(true).SearchLimit(1).One(ctx, &one)
135+
if !errors.Is(err, ErrNotFound) {
136+
t.Errorf("bad error from get one. %v ≠ %v", err, ErrNotFound)
137+
}
138+
114139
// GetItem + Project
115140
one = widget{}
116141
projected := widget{

0 commit comments

Comments
 (0)