influxdb/tsdb/query_executor_test.go

491 lines
16 KiB
Go

package tsdb_test
import (
"encoding/json"
"reflect"
"strings"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/influxdb/influxdb/influxql"
"github.com/influxdb/influxdb/services/meta"
"github.com/influxdb/influxdb/tsdb"
)
const (
// DefaultDatabase is the default database name used by tests.
DefaultDatabase = "db0"
// DefaultRetentionPolicy is the default retention policy name used by tests.
DefaultRetentionPolicy = "rp0"
)
// Ensure the query executor can execute a basic query.
func TestQueryExecutor_ExecuteQuery_Select(t *testing.T) {
sh := MustOpenShard()
defer sh.Close()
sh.MustWritePointsString(`
cpu,region=serverA value=1 0
cpu,region=serverA value=2 10
cpu,region=serverB value=3 20
`)
e := NewQueryExecutor()
e.MetaClient.ShardIDsByTimeRangeFn = func(sources influxql.Sources, tmin, tmax time.Time) (a []uint64, err error) {
if !reflect.DeepEqual(sources, influxql.Sources{&influxql.Measurement{Database: "db0", RetentionPolicy: "rp0", Name: "cpu"}}) {
t.Fatalf("unexpected sources: %s", spew.Sdump(sources))
} else if tmin.IsZero() {
t.Fatalf("unexpected tmin: %s", tmin)
} else if tmax.IsZero() {
t.Fatalf("unexpected tmax: %s", tmax)
}
return []uint64{100}, nil
}
e.Store.ShardsFn = func(ids []uint64) []*tsdb.Shard {
if !reflect.DeepEqual(ids, []uint64{100}) {
t.Fatalf("unexpected shard ids: %+v", ids)
}
return []*tsdb.Shard{sh.Shard}
}
res := e.MustExecuteQueryString("db0", `SELECT value FROM cpu`)
if s := MustMarshalJSON(res); s != `[{"series":[{"name":"cpu","columns":["time","value"],"values":[["1970-01-01T00:00:00Z",1],["1970-01-01T00:00:10Z",2],["1970-01-01T00:00:20Z",3]]}]}]` {
t.Fatalf("unexpected results: %s", s)
}
}
// Ensure the query executor can select from a tsdb.Store.
func TestQueryExecutor_ExecuteQuery_Select_Wildcard_Intg(t *testing.T) {
s := MustOpenStore()
defer s.Close()
s.MustCreateShardWithData("db0", "rp0", 0,
`cpu,host=serverA value=1 0`,
`cpu,host=serverA value=2 10`,
`cpu,host=serverB value=3 20`,
)
res := NewQueryExecutorStore(s).MustExecuteQueryStringJSON("db0", `SELECT * FROM cpu`)
if res != `[{"series":[{"name":"cpu","columns":["time","host","value"],"values":[["1970-01-01T00:00:00Z","serverA",1],["1970-01-01T00:00:10Z","serverA",2],["1970-01-01T00:00:20Z","serverB",3]]}]}]` {
t.Fatalf("unexpected results: %s", res)
}
}
// Ensure the query executor returns an empty set if no points are returned.
/*
func TestQueryExecutor_ExecuteQuery_Select_Empty(t *testing.T) {
e := NewQueryExecutor()
// Return an empty iterator.
e.IteratorCreator.CreateIteratorFn = func(opt influxql.IteratorOptions) (influxql.Iterator, error) {
return &FloatIterator{}, nil
}
res := e.MustExecuteQueryString("db0", `SELECT value FROM cpu`)
if MustMarshalJSON(res) != `[{}]` {
t.Fatalf("unexpected results: %s", spew.Sdump(res))
}
}
*/
// Ensure the query executor can execute a DROP MEASUREMENT statement.
func TestQueryExecutor_ExecuteQuery_DropMeasurement(t *testing.T) {
e := NewQueryExecutor()
e.Store.DeleteMeasurementFn = func(database, name string) error {
if database != `db0` {
t.Fatalf("unexpected database: %s", database)
} else if name != `memory` {
t.Fatalf("unexpected name: %s", name)
}
return nil
}
res := e.MustExecuteQueryString("db0", `drop measurement memory`)
if s := MustMarshalJSON(res); s != `[{}]` {
t.Fatalf("unexpected results: %s", s)
}
}
// Ensure the query executor can execute a DROP DATABASE statement.
//
// Dropping a database involves executing against the meta store as well as
// removing all associated shards from the local TSDB storage.
func TestQueryExecutor_ExecuteQuery_DropDatabase(t *testing.T) {
e := NewQueryExecutor()
e.MetaClient.DatabaseFn = func(name string) (*meta.DatabaseInfo, error) {
return &meta.DatabaseInfo{
Name: name,
DefaultRetentionPolicy: "rp0",
RetentionPolicies: []meta.RetentionPolicyInfo{
{
Name: "rp0",
ShardGroups: []meta.ShardGroupInfo{
{
ID: 1,
Shards: []meta.ShardInfo{{ID: 10}, {ID: 20}},
},
{
ID: 2,
Shards: []meta.ShardInfo{{ID: 50}},
},
},
},
{
Name: "rp1",
ShardGroups: []meta.ShardGroupInfo{
{
ID: 3,
Shards: []meta.ShardInfo{{ID: 60}},
},
},
},
},
}, nil
}
e.MetaClient.ExecuteStatementFn = func(stmt influxql.Statement) *influxql.Result {
if s := stmt.String(); s != `DROP DATABASE db0` {
t.Fatalf("unexpected meta statement: %s", s)
}
return &influxql.Result{}
}
e.Store.DeleteDatabaseFn = func(name string, shardIDs []uint64) error {
if name != `db0` {
t.Fatalf("unexpected name: %s", name)
} else if !reflect.DeepEqual(shardIDs, []uint64{10, 20, 50, 60}) {
t.Fatalf("unexpected shard ids: %+v", shardIDs)
}
return nil
}
res := e.MustExecuteQueryString("db0", `drop database db0`)
if s := MustMarshalJSON(res); s != `[{}]` {
t.Fatalf("unexpected results: %s", s)
}
}
// Ensure that the query executor doesn't return an error when user count is zero
// and the user is attempting to create a user.
func TestQueryExecutor_Authorize_CreateUser_NoUsers(t *testing.T) {
/*
store, executor := testStoreAndExecutor("")
defer os.RemoveAll(store.Path())
ms := &testMetastore{userCount: 0}
executor.MetaStore = ms
if err := executor.Authorize(nil, MustParseQuery("create user foo with password 'asdf' with all privileges"), ""); err != nil {
t.Fatalf("should have authenticated if no users and attempting to create a user but got error: %s", err.Error())
}
if executor.Authorize(nil, MustParseQuery("create user foo with password 'asdf'"), "") == nil {
t.Fatalf("should have failed authentication if no user given and no users exist for create user query that doesn't grant all privileges")
}
if executor.Authorize(nil, MustParseQuery("select * from foo"), "") == nil {
t.Fatalf("should have failed authentication if no user given and no users exist for any query other than create user")
}
ms.userCount = 1
if executor.Authorize(nil, MustParseQuery("create user foo with password 'asdf'"), "") == nil {
t.Fatalf("should have failed authentication if no user given and users exist")
}
if executor.Authorize(nil, MustParseQuery("select * from foo"), "") == nil {
t.Fatalf("should have failed authentication if no user given and users exist")
}
*/
}
func TestDropDatabase(t *testing.T) {
/*
store, executor := testStoreAndExecutor("")
defer os.RemoveAll(store.Path())
pt := models.MustNewPoint(
"cpu",
map[string]string{"host": "server"},
map[string]interface{}{"value": 1.0},
time.Unix(1, 2),
)
if err := store.WriteToShard(shardID, []models.Point{pt}); err != nil {
t.Fatal(err)
}
got := executeAndGetJSON("SELECT * FROM cpu GROUP BY *", executor)
expected := `[{"series":[{"name":"cpu","tags":{"host":"server"},"columns":["time","value"],"values":[["1970-01-01T00:00:01.000000002Z",1]]}]}]`
if expected != got {
t.Fatalf("exp: %s\ngot: %s", expected, got)
}
var name string
executor.MetaClient = &testMetaClient{
ExecuteStatemenFn: func(stmt influxql.Statement) *influxql.Result {
name = stmt.(*influxql.DropDatabaseStatement).Name
return &influxql.Result{}
},
}
// verify the database is there on disk
dbPath := filepath.Join(store.Path(), "foo")
if _, err := os.Stat(dbPath); err != nil {
t.Fatalf("execpted database dir %s to exist", dbPath)
}
got = executeAndGetJSON("drop database foo", executor)
expected = `[{}]`
if got != expected {
t.Fatalf("exp: %s\ngot: %s", expected, got)
}
*/
}
// ensure that authenticate doesn't return an error if the user count is zero and they're attempting
// to create a user.
func TestAuthenticateIfUserCountZeroAndCreateUser(t *testing.T) {
/*
store, executor := testStoreAndExecutor("")
defer os.RemoveAll(store.Path())
ms := &testMetaClient{userCount: 0}
executor.MetaClient = ms
if err := executor.Authorize(nil, mustParseQuery("create user foo with password 'asdf' with all privileges"), ""); err != nil {
t.Fatalf("should have authenticated if no users and attempting to create a user but got error: %s", err.Error())
}
if executor.Authorize(nil, mustParseQuery("create user foo with password 'asdf'"), "") == nil {
t.Fatalf("should have failed authentication if no user given and no users exist for create user query that doesn't grant all privileges")
}
if executor.Authorize(nil, mustParseQuery("select * from foo"), "") == nil {
t.Fatalf("should have failed authentication if no user given and no users exist for any query other than create user")
}
ms.userCount = 1
*/
}
// QueryExecutor represents a test wrapper for tsdb.QueryExecutor.
type QueryExecutor struct {
*tsdb.QueryExecutor
Store QueryExecutorStore
MetaClient QueryExecutorMetaClient
MonitorStatementExecutor StatementExecutor
IntoWriter IntoWriter
}
// NewQueryExecutor returns a new instance of QueryExecutor.
func NewQueryExecutor() *QueryExecutor {
e := &QueryExecutor{}
e.QueryExecutor = tsdb.NewQueryExecutor()
e.QueryExecutor.Store = &e.Store
e.QueryExecutor.MetaClient = &e.MetaClient
e.QueryExecutor.MonitorStatementExecutor = &e.MonitorStatementExecutor
e.QueryExecutor.IntoWriter = &e.IntoWriter
// By default, always return a database when looking it up.
e.MetaClient.DatabaseFn = MetaClientDatabaseFoundFn
// By default, returns the same sources when expanding.
e.Store.ExpandSourcesFn = DefaultStoreExpandSourcesFn
return e
}
// NewQueryExecutorStore returns a new instance of QueryExecutor attached to a store.
func NewQueryExecutorStore(s *Store) *QueryExecutor {
e := NewQueryExecutor()
e.QueryExecutor.Store = s
// Always return all shards from store.
e.MetaClient.ShardIDsByTimeRangeFn = func(sources influxql.Sources, tmin, tmax time.Time) ([]uint64, error) {
return s.ShardIDs(), nil
}
return e
}
// MustExecuteQuery executes a query. Panic on error.
func (e *QueryExecutor) MustExecuteQueryString(database string, s string) []*influxql.Result {
q := MustParseQuery(s)
// Execute query.
ch, err := e.ExecuteQuery(q, database, 1000, make(chan struct{}))
if err != nil {
panic(err)
}
// Read all results from the channel.
var a []*influxql.Result
for {
select {
case result, ok := <-ch:
if !ok {
return a
}
a = append(a, result)
case <-time.After(10 * time.Second):
panic("query timeout")
}
}
}
// MustExecuteQueryStringJSON executes a query and returns JSON. Panic on error.
func (e *QueryExecutor) MustExecuteQueryStringJSON(database string, s string) string {
return MustMarshalJSON(e.MustExecuteQueryString(database, s))
}
// QueryExecutorStore is a mockable implementation of QueryExecutor.Store.
type QueryExecutorStore struct {
DatabaseIndexFn func(name string) *tsdb.DatabaseIndex
ShardsFn func(ids []uint64) []*tsdb.Shard
ExpandSourcesFn func(sources influxql.Sources) (influxql.Sources, error)
DeleteDatabaseFn func(name string, shardIDs []uint64) error
DeleteMeasurementFn func(database, name string) error
DeleteSeriesFn func(database string, seriesKeys []string) error
}
func (s *QueryExecutorStore) DatabaseIndex(name string) *tsdb.DatabaseIndex {
return s.DatabaseIndexFn(name)
}
func (s *QueryExecutorStore) Shards(ids []uint64) []*tsdb.Shard {
return s.ShardsFn(ids)
}
func (s *QueryExecutorStore) ExpandSources(sources influxql.Sources) (influxql.Sources, error) {
return s.ExpandSourcesFn(sources)
}
func (s *QueryExecutorStore) DeleteDatabase(name string, shardIDs []uint64) error {
return s.DeleteDatabaseFn(name, shardIDs)
}
func (s *QueryExecutorStore) DeleteMeasurement(database, name string) error {
return s.DeleteMeasurementFn(database, name)
}
func (s *QueryExecutorStore) DeleteSeries(database string, seriesKeys []string) error {
return s.DeleteSeriesFn(database, seriesKeys)
}
// DefaultStoreExpandSourcesFn returns the original sources unchanged.
func DefaultStoreExpandSourcesFn(sources influxql.Sources) (influxql.Sources, error) {
return sources, nil
}
// QueryExecutorMetaClient is a mockable implementation of QueryExecutor.MetaClient.
type QueryExecutorMetaClient struct {
DatabaseFn func(name string) (*meta.DatabaseInfo, error)
DatabasesFn func() ([]meta.DatabaseInfo, error)
UserFn func(name string) (*meta.UserInfo, error)
AdminUserExistsFn func() bool
AuthenticateFn func(username, password string) (*meta.UserInfo, error)
RetentionPolicyFn func(database, name string) (rpi *meta.RetentionPolicyInfo, err error)
UserCountFn func() int
ShardIDsByTimeRangeFn func(sources influxql.Sources, tmin, tmax time.Time) (a []uint64, err error)
ExecuteStatementFn func(stmt influxql.Statement) *influxql.Result
}
func (s *QueryExecutorMetaClient) Database(name string) (*meta.DatabaseInfo, error) {
return s.DatabaseFn(name)
}
func (s *QueryExecutorMetaClient) Databases() ([]meta.DatabaseInfo, error) {
return s.DatabasesFn()
}
func (s *QueryExecutorMetaClient) User(name string) (*meta.UserInfo, error) {
return s.UserFn(name)
}
func (s *QueryExecutorMetaClient) AdminUserExists() bool {
return s.AdminUserExistsFn()
}
func (s *QueryExecutorMetaClient) Authenticate(username, password string) (*meta.UserInfo, error) {
return s.AuthenticateFn(username, password)
}
func (s *QueryExecutorMetaClient) RetentionPolicy(database, name string) (rpi *meta.RetentionPolicyInfo, err error) {
return s.RetentionPolicyFn(database, name)
}
func (s *QueryExecutorMetaClient) UserCount() int {
return s.UserCountFn()
}
func (s *QueryExecutorMetaClient) ShardIDsByTimeRange(sources influxql.Sources, tmin, tmax time.Time) (a []uint64, err error) {
return s.ShardIDsByTimeRangeFn(sources, tmin, tmax)
}
func (s *QueryExecutorMetaClient) ExecuteStatement(stmt influxql.Statement) *influxql.Result {
return s.ExecuteStatementFn(stmt)
}
// MetaClientDatabaseFoundFn always returns a database for a database name.
func MetaClientDatabaseFoundFn(name string) (*meta.DatabaseInfo, error) {
return &meta.DatabaseInfo{
Name: name,
DefaultRetentionPolicy: DefaultRetentionPolicy,
}, nil
}
// StatementExecutor is a mockable implementation of QueryExecutor.StatementExecutor.
type StatementExecutor struct {
ExecuteStatementFn func(stmt influxql.Statement) *influxql.Result
}
func (e *StatementExecutor) ExecuteStatement(stmt influxql.Statement) *influxql.Result {
return e.ExecuteStatementFn(stmt)
}
// IteratorCreator is a mockable implementation of SelectStatementExecutor.IteratorCreator.
type IteratorCreator struct {
CreateIteratorFn func(opt influxql.IteratorOptions) (influxql.Iterator, error)
FieldDimensionsFn func(sources influxql.Sources) (field, dimensions map[string]struct{}, err error)
}
func (ic *IteratorCreator) CreateIterator(opt influxql.IteratorOptions) (influxql.Iterator, error) {
return ic.CreateIteratorFn(opt)
}
func (ic *IteratorCreator) FieldDimensions(sources influxql.Sources) (field, dimensions map[string]struct{}, err error) {
return ic.FieldDimensionsFn(sources)
}
// IntoWriter is a mockable implementation of QueryExecutor.IntoWriter.
type IntoWriter struct {
WritePointsIntoFn func(p *tsdb.IntoWriteRequest) error
}
func (w *IntoWriter) WritePointsInto(p *tsdb.IntoWriteRequest) error {
return w.WritePointsIntoFn(p)
}
// FloatIterator is a test implementation of influxql.FloatIterator.
type FloatIterator struct {
Points []influxql.FloatPoint
}
// Close is a no-op.
func (itr *FloatIterator) Close() error { return nil }
// Next returns the next value and shifts it off the beginning of the points slice.
func (itr *FloatIterator) Next() *influxql.FloatPoint {
if len(itr.Points) == 0 {
return nil
}
v := &itr.Points[0]
itr.Points = itr.Points[1:]
return v
}
// MustParseQuery parses an InfluxQL query. Panic on error.
func MustParseQuery(s string) *influxql.Query {
q, err := influxql.NewParser(strings.NewReader(s)).ParseQuery()
if err != nil {
panic(err.Error())
}
return q
}
// MustMarshalJSON marshals a value to a JSON string. Panic on error.
func MustMarshalJSON(v interface{}) string {
buf, err := json.Marshal(v)
if err != nil {
panic(err)
}
return string(buf)
}