diff --git a/iterator.go b/iterator.go index 85c9dd5..3612cf1 100644 --- a/iterator.go +++ b/iterator.go @@ -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 ( @@ -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 } @@ -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() +} diff --git a/iterator_test.go b/iterator_test.go index 7428fe8..22402c4 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -1,6 +1,7 @@ package gockle import ( + "context" "fmt" "reflect" "testing" @@ -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) } @@ -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") @@ -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}}}, }) } diff --git a/query.go b/query.go new file mode 100644 index 0000000..a43c22f --- /dev/null +++ b/query.go @@ -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() +} diff --git a/query_test.go b/query_test.go new file mode 100644 index 0000000..45dc78a --- /dev/null +++ b/query_test.go @@ -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}, + }) +} diff --git a/session.go b/session.go index a56336b..8ec4ea1 100644 --- a/session.go +++ b/session.go @@ -62,6 +62,12 @@ type Session interface { // Tables returns the table names for keyspace. Schema changes during a session // are not reflected; you must open a new Session to observe them. Tables(keyspace string) ([]string, error) + + // Query generates a new query object for interacting with the database. + // Further details of the query may be tweaked using the resulting query + // value before the query is executed. Query is automatically prepared if + // it has not previously been executed. + Query(statement string, arguments ...interface{}) Query } var ( @@ -153,6 +159,11 @@ func (m SessionMock) Tables(keyspace string) ([]string, error) { return r.Get(0).([]string), r.Error(1) } +// Query implements Session. +func (m SessionMock) Query(statement string, arguments ...interface{}) Query { + return m.Called(statement, arguments).Get(0).(Query) +} + type session struct { s *gocql.Session } @@ -226,3 +237,7 @@ func (s session) Tables(keyspace string) ([]string, error) { return ts, nil } + +func (s session) Query(statement string, arguments ...interface{}) Query { + return query{q: s.s.Query(statement, arguments...)} +} diff --git a/session_test.go b/session_test.go index ba74fac..1e36648 100644 --- a/session_test.go +++ b/session_test.go @@ -13,12 +13,13 @@ import ( const version = 4 const ( - ksCreate = "create keyspace gockle_test with replication = {'class': 'SimpleStrategy', 'replication_factor': 1};" - ksDrop = "drop keyspace gockle_test" - ksDropIf = "drop keyspace if exists gockle_test" - rowInsert = "insert into gockle_test.test (id, n) values (1, 2)" - tabCreate = "create table gockle_test.test(id int primary key, n int)" - tabDrop = "drop table gockle_test.test" + ksCreate = "create keyspace gockle_test with replication = {'class': 'SimpleStrategy', 'replication_factor': 1};" + ksDrop = "drop keyspace gockle_test" + ksDropIf = "drop keyspace if exists gockle_test" + rowInsert = "insert into gockle_test.test (id, n) values (1, 2)" + rowInsert2 = "insert into gockle_test.test (id, n) values (3, 4)" + tabCreate = "create table gockle_test.test(id int primary key, n int)" + tabDrop = "drop table gockle_test.test" ) func TestNewSession(t *testing.T) { @@ -156,6 +157,7 @@ func TestSessionMock(t *testing.T) { {"ScanMapTx", []interface{}{"a", map[string]interface{}{"b": 2}, []interface{}{1}}, []interface{}{true, e}}, {"Tables", []interface{}{""}, []interface{}{[]string(nil), nil}}, {"Tables", []interface{}{"a"}, []interface{}{[]string{"b"}, e}}, + {"Query", []interface{}{"a", []interface{}{1}}, []interface{}{query{}}}, }) }