Skip to content

Commit

Permalink
Expose Query interface, needed for result pagination
Browse files Browse the repository at this point in the history
Add WillSwitchPage and PageState to the Iterator, and an entire Query
interface to support it. This allows using gockle for testing result
pagination.
  • Loading branch information
rtfb authored and willfaught committed Jan 12, 2023
1 parent d53e76c commit 0ae8845
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 6 deletions.
26 changes: 26 additions & 0 deletions iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ type Iterator interface {
// ScanMap puts the current result row in results and returns whether there are
// more result rows.
ScanMap(results map[string]interface{}) bool

// WillSwitchPage detects if iterator reached end of current page and the
// next page is available.
WillSwitchPage() bool

// PageState return the current paging state for a query which can be used
// for subsequent quries to resume paging this point.
PageState() []byte
}

var (
Expand Down Expand Up @@ -44,6 +52,16 @@ func (m IteratorMock) ScanMap(results map[string]interface{}) bool {
return m.Called(results).Bool(0)
}

// WillSwitchPage implements Iterator.
func (m IteratorMock) WillSwitchPage() bool {
return m.Called().Bool(0)
}

// PageState implements Iterator.
func (m IteratorMock) PageState() []byte {
return m.Called().Bytes(0)
}

type iterator struct {
i *gocql.Iter
}
Expand All @@ -59,3 +77,11 @@ func (i iterator) Scan(results ...interface{}) bool {
func (i iterator) ScanMap(results map[string]interface{}) bool {
return i.i.MapScan(results)
}

func (i iterator) WillSwitchPage() bool {
return i.i.WillSwitchPage()
}

func (i iterator) PageState() []byte {
return i.i.PageState()
}
99 changes: 99 additions & 0 deletions iterator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gockle

import (
"context"
"fmt"
"reflect"
"testing"
Expand Down Expand Up @@ -37,6 +38,14 @@ func TestIterator(t *testing.T) {
t.Errorf("Actual more false, expected true")
}

if i.WillSwitchPage() {
t.Errorf("Actual WillSwitchPage true, expected false")
}

if state := i.PageState(); state != nil {
t.Errorf("Actual PageState not nil, expected nil")
}

if id != 1 {
t.Errorf("Actual id %v, expected 1", id)
}
Expand Down Expand Up @@ -69,6 +78,92 @@ func TestIterator(t *testing.T) {
}
}

func TestIteratorPaging(t *testing.T) {
var s = newSession(t)
defer s.Close()
var exec = func(q string) {
if err := s.Exec(q); err != nil {
t.Fatalf("Actual error %v, expected no error", err)
}
}
exec(ksDropIf)
exec(ksCreate)
defer exec(ksDrop)
exec(tabCreate)
defer exec(tabDrop)
exec(rowInsert)
exec(rowInsert2)
q := s.Query("select * from gockle_test.test").
PageSize(1).
WithContext(context.Background())
var state []byte
if i := q.Iter(); i == nil {
t.Error("Actual iterator nil, expected not nil")
} else {
var id, n int
// first row
if !i.Scan(&id, &n) {
t.Errorf("Actual more false, expected true")
}
if !i.WillSwitchPage() {
t.Errorf("Actual WillSwitchPage false, expected true")
}
state = i.PageState()
if state == nil {
t.Errorf("Actual PageState nil, expected not nil")
}
if id != 1 {
t.Errorf("Actual id %v, expected 1", id)
}
if n != 2 {
t.Errorf("Actual n %v, expected 2", n)
}
}
// second row
q = q.PageState(state)
if i := q.Iter(); i == nil {
t.Error("Actual iterator nil, expected not nil")
} else {
var id, n int
if !i.Scan(&id, &n) {
t.Errorf("Actual more false, expected true")
}
if i.WillSwitchPage() {
t.Errorf("Actual WillSwitchPage true, expected false")
}
state = i.PageState()
if state == nil {
t.Errorf("Actual PageState nil, expected not nil")
}
if id != 3 {
t.Errorf("Actual id %v, expected 3", id)
}
if n != 4 {
t.Errorf("Actual n %v, expected 4", n)
}
if err := i.Close(); err != nil {
t.Errorf("Actual error %v, expected no error", err)
}
}
// third row (not existing)
q = q.PageState(state)
if i := q.Iter(); i == nil {
t.Error("Actual iterator nil, expected not nil")
} else {
var id, n int
if i.Scan(&id, &n) {
t.Errorf("Actual more true, expected false")
}
if i.WillSwitchPage() {
t.Errorf("Actual WillSwitchPage true, expected false")
}
state = i.PageState()
if state != nil {
t.Errorf("Actual PageState not nil, expected nil")
}
}
}

func TestIteratorMock(t *testing.T) {
var m, e = &IteratorMock{}, fmt.Errorf("e")

Expand All @@ -83,5 +178,9 @@ func TestIteratorMock(t *testing.T) {
{"Scan", []interface{}{[]interface{}{1}}, []interface{}{true}},
{"ScanMap", []interface{}{map[string]interface{}(nil)}, []interface{}{false}},
{"ScanMap", []interface{}{map[string]interface{}{"a": 1}}, []interface{}{true}},
{"WillSwitchPage", nil, []interface{}{false}},
{"WillSwitchPage", nil, []interface{}{true}},
{"PageState", nil, []interface{}{[]byte{}}},
{"PageState", nil, []interface{}{[]byte{1}}},
})
}
130 changes: 130 additions & 0 deletions query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package gockle

import (
"context"

"github.com/gocql/gocql"
"github.com/maraino/go-mock"
)

// Query represents a CQL query.
type Query interface {
// PageSize will tell the iterator to fetch the result in pages of size n.
PageSize(n int) Query

// WithContext will set the context to use during a query, it will be used
// to timeout when waiting for responses from Cassandra.
WithContext(ctx context.Context) Query

// PageState sets the paging state for the query to resume paging from a
// specific point in time. Setting this will disable to query paging for
// this query, and must be used for all subsequent pages.
PageState(state []byte) Query

// Exec executes the query without returning any rows.
Exec() error

// Iter executes the query and returns an iterator capable of iterating
// over all results.
Iter() Iterator

// MapScan executes the query, copies the columns of the first selected row
// into the map pointed at by m and discards the rest. If no rows
// were selected, ErrNotFound is returned.
MapScan(m map[string]interface{}) error

// Scan executes the query, copies the columns of the first selected row
// into the values pointed at by dest and discards the rest. If no rows
// were selected, ErrNotFound is returned.
Scan(dest ...interface{}) error

// Release releases a query back into a pool of queries. Released Queries
// cannot be reused.
Release()
}

var (
_ Query = QueryMock{}
_ Query = query{}
)

// QueryMock is a mock Query. See github.com/maraino/go-mock.
type QueryMock struct {
mock.Mock
}

// PageSize implements Query.
func (m QueryMock) PageSize(n int) Query {
return m.Called(n).Get(0).(Query)
}

// WithContext implements Query.
func (m QueryMock) WithContext(ctx context.Context) Query {
return m.Called(ctx).Get(0).(Query)
}

// PageState implements Query.
func (m QueryMock) PageState(state []byte) Query {
return m.Called(state).Get(0).(Query)
}

// Exec implements Query.
func (m QueryMock) Exec() error {
return m.Called().Error(0)
}

// Iter implements Query.
func (m QueryMock) Iter() Iterator {
return m.Called().Get(0).(Iterator)
}

// MapScan implements Query.
func (m QueryMock) MapScan(mm map[string]interface{}) error {
return m.Called(mm).Error(0)
}

// Scan implements Query.
func (m QueryMock) Scan(dest ...interface{}) error {
return m.Called(dest).Error(0)
}

// Release implements Query.
func (m QueryMock) Release() {
m.Called()
}

type query struct {
q *gocql.Query
}

func (q query) PageSize(n int) Query {
return &query{q: q.q.PageSize(n)}
}

func (q query) WithContext(ctx context.Context) Query {
return &query{q: q.q.WithContext(ctx)}
}

func (q query) PageState(state []byte) Query {
return &query{q: q.q.PageState(state)}
}

func (q query) Exec() error {
return q.q.Exec()
}

func (q query) Iter() Iterator {
return &iterator{i: q.q.Iter()}
}

func (q query) MapScan(m map[string]interface{}) error {
return q.q.MapScan(m)
}

func (q query) Scan(dest ...interface{}) error {
return q.q.Scan(dest...)
}

func (q query) Release() {
q.q.Release()
}
77 changes: 77 additions & 0 deletions query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package gockle

import (
"context"
"fmt"
"reflect"
"testing"
)

func TestQuery(t *testing.T) {
var s = newSession(t)
defer s.Close()
var exec = func(q string) {
if err := s.Exec(q); err != nil {
t.Fatalf("Actual error %v, expected no error", err)
}
}
exec(ksDropIf)
exec(ksCreate)
defer exec(ksDrop)
exec(tabCreate)
defer exec(tabDrop)
exec(rowInsert)
q := s.Query("select * from gockle_test.test").
PageSize(3).
WithContext(context.Background()).
PageState(nil)
defer q.Release()
if err := q.Exec(); err != nil {
t.Fatalf("Actual error %v, expected no error", err)
}
// Scan
var id, n int
if err := q.Scan(&id, &n); err == nil {
if id != 1 {
t.Errorf("Actual id %v, expected 1", id)
}
if n != 2 {
t.Errorf("Actual n %v, expected 2", n)
}
} else {
t.Errorf("Actual error %v, expected no error", err)
}
// MapScan
var actual = map[string]interface{}{}
var expected = map[string]interface{}{"id": 1, "n": 2}
if err := q.MapScan(actual); err == nil {
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Actual map %v, expected %v", actual, expected)
}
} else {
t.Errorf("Actual error %v, expected no error", err)
}
}

func TestQueryMock(t *testing.T) {
var m, e = &QueryMock{}, fmt.Errorf("e")
ctx := context.Background()
it := &IteratorMock{}
testMock(t, m, &m.Mock, []struct {
method string
arguments []interface{}
results []interface{}
}{
{"PageSize", []interface{}{1}, []interface{}{m}},
{"WithContext", []interface{}{ctx}, []interface{}{m}},
{"PageState", []interface{}{[]byte{1}}, []interface{}{m}},
{"Scan", []interface{}{[]interface{}(nil)}, []interface{}{e}},
{"Scan", []interface{}{[]interface{}{1}}, []interface{}{nil}},
{"Exec", nil, []interface{}{e}},
{"Exec", nil, []interface{}{nil}},
{"Iter", nil, []interface{}{it}},
{"MapScan", []interface{}{map[string]interface{}(nil)}, []interface{}{nil}},
{"MapScan", []interface{}{map[string]interface{}{"a": 1}}, []interface{}{e}},
{"Release", nil, nil},
})
}

0 comments on commit 0ae8845

Please sign in to comment.