diff --git a/.gitignore b/.gitignore index 8db43b3a7..fa912d374 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,69 @@ vendor .protoc-gen-gofast .DS_Store build +*~ +release-pilosa-fsck.*.*.tar.gz +/log.* +/tourna.log.* +pilosa +/featurebase +*.dot +.idea/ +.*.swp +.terraform/ +*.tfstate +launch.json +.terraform.lock.hcl +__pycache__/ +report.xml +outputs.json +builds/ +*.tfstate.backup +.vscode + +# copy of .gitignore from archived idk repo +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +vendor + +.terraform +terraform.tfstate* + +bin +build +testenv +.pulled + +pilosa-sec-data-idk + +.idea/ +tags.dot +*.log +*.swp +*__debug_bin + +# SQL3 +/sql3/sql3.html diff --git a/Makefile b/Makefile index f9abfdf85..78ba9217d 100644 --- a/Makefile +++ b/Makefile @@ -379,3 +379,6 @@ install-gometalinter: test-external-lookup: $(GO) test . -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) -run ^TestExternalLookup$$ -externalLookupDSN $(EXTERNAL_LOOKUP_DSN) + +bnf: + ebnf2railroad --no-overview-diagram --no-optimizations ./sql3/sql3.ebnf diff --git a/api.go b/api.go index b29bae334..9fbd00fa6 100644 --- a/api.go +++ b/api.go @@ -26,10 +26,12 @@ import ( "github.com/featurebasedb/featurebase/v3/ingest" "github.com/featurebasedb/featurebase/v3/rbf" - "github.com/featurebasedb/featurebase/v3/pql" - "github.com/featurebasedb/featurebase/v3/roaring" - "github.com/featurebasedb/featurebase/v3/stats" - "github.com/featurebasedb/featurebase/v3/tracing" + //"github.com/molecula/featurebase/v3/pg" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/roaring" + planner_types "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/molecula/featurebase/v3/stats" + "github.com/molecula/featurebase/v3/tracing" "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) @@ -197,7 +199,7 @@ func (api *API) query(ctx context.Context, req *QueryRequest) (QueryResponse, er } // TODO can we get rid of exec options and pass the QueryRequest directly to executor? - execOpts := &execOptions{ + execOpts := &ExecOptions{ Remote: req.Remote, Profile: req.Profile, PreTranslated: req.PreTranslated, @@ -1069,6 +1071,23 @@ func (api *API) Schema(ctx context.Context, withViews bool) ([]*IndexInfo, error return api.holder.limitedSchema() } +// IndexInfo returns the same information as Schema(), but only for a single +// index. +func (api *API) IndexInfo(ctx context.Context, name string) (*IndexInfo, error) { + schema, err := api.Schema(ctx, false) + if err != nil { + return nil, err + } + + for _, idx := range schema { + if idx.Name == name { + return idx, nil + } + } + + return nil, ErrIndexNotFound +} + // ApplySchema takes the given schema and applies it across the // cluster (if remote is false), or just to this node (if remote is // true). This is designed for the use case of replicating a schema @@ -3171,8 +3190,15 @@ processing: } return result, ctx.Err() } -func (api *API) Plan(ctx context.Context, q string) (*Stmt, error) { - return api.server.PlanSQL(ctx, q) + +// CompilePlan takes a sql string and returns a PlanOperator. Note that this is +// different from the internal CompilePlan() method on the CompilePlanner +// interface, which takes a parser statement and returns a PlanOperator. In +// other words, this CompilePlan() both parses and plans the provided sql +// string; it's the equivalent of the CompileExecutionPlan() method on Server. +// TODO: consider renaming this to something with less conflict. +func (api *API) CompilePlan(ctx context.Context, q string) (planner_types.PlanOperator, error) { + return api.server.CompileExecutionPlan(ctx, q) } func (api *API) RBFDebugInfo() map[string]*rbf.DebugInfo { @@ -3312,3 +3338,98 @@ var methodsNormal = map[apiMethod]struct{}{ apiIngestNodeOperations: {}, apiMutexCheck: {}, } + +// SchemaAPI is a subset of the API methods which have to do with schema. This +// interface was introduced in order to remove, from the sql3 package, the +// pointer to API, and instead use this interface. In the current FeatureBase, +// this interface can be implemented directly with API. But in an implementation +// for DAX, for example, we might want something else servicing the +// schema-related calls to the SchemaAPI. +type SchemaAPI interface { + CreateIndexAndFields(ctx context.Context, indexName string, options IndexOptions, fields []CreateFieldObj) error + CreateField(ctx context.Context, indexName string, fieldName string, opts ...FieldOption) (*Field, error) + DeleteField(ctx context.Context, indexName string, fieldName string) error + DeleteIndex(ctx context.Context, indexName string) error + IndexInfo(ctx context.Context, indexName string) (*IndexInfo, error) + Schema(ctx context.Context, withViews bool) ([]*IndexInfo, error) +} + +// CreateFieldObj is used to encapsulate the information required for creating a +// field in the SchemaAPI.CreateIndexAndFields interface method. +type CreateFieldObj struct { + Name string + Options []FieldOption +} + +// ComputeAPI is a subset of the API methods which have to do with compute +// operations such as import. +type ComputeAPI interface { + Import(ctx context.Context, qcx *Qcx, req *ImportRequest, opts ...ImportOption) error + ImportValue(ctx context.Context, qcx *Qcx, req *ImportValueRequest, opts ...ImportOption) error + Txf() *TxFactory +} + +// FeatureBaseSchemaAPI is a wrapper around pilosa.API. It implements the +// SchemaAPI interface with methods which are not a part of pilosa.API. +type FeatureBaseSchemaAPI struct { + *API +} + +func (fapi *FeatureBaseSchemaAPI) CreateIndexAndFields(ctx context.Context, indexName string, options IndexOptions, fields []CreateFieldObj) error { + // Add the index. + if _, err := fapi.CreateIndex(ctx, indexName, options); err != nil { + return err + } + + // Now add fields. + for _, f := range fields { + if _, err := fapi.CreateField(ctx, indexName, f.Name, f.Options...); err != nil { + return err + } + } + + return nil +} + +// IndexInfo wraps the API.IndexInfo method and prepends an _id field to its +// list of fields. +func (fapi *FeatureBaseSchemaAPI) IndexInfo(ctx context.Context, indexName string) (*IndexInfo, error) { + idx, err := fapi.API.IndexInfo(ctx, indexName) + if err != nil { + return nil, err + } + + // sortedFields will contain the sorted list of fields from IndexInfo, along + // with the primary key field (which will always be at the beginning of the + // list). + sortedFields := make([]*FieldInfo, 0, len(idx.Fields)+1) + + // Add the primary key field to the beginning of the list. + idKeys := idx.Options.Keys + idType := "id" + if idKeys { + idType = "string" + } + + idFld := &FieldInfo{ + Name: "_id", + CreatedAt: idx.CreatedAt, + Options: FieldOptions{ + Type: idType, + Keys: idKeys, + }, + } + sortedFields = append(sortedFields, idFld) + + // Sort idx.Fields by CreatedAt before adding them to sortedFields. + sort.Slice(idx.Fields, func(i, j int) bool { + return idx.Fields[i].CreatedAt < idx.Fields[j].CreatedAt + }) + + // Add the sorted fields to sortedFields. + sortedFields = append(sortedFields, idx.Fields...) + + idx.Fields = sortedFields + + return idx, nil +} diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 000000000..d3e236c03 --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,31 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package cmd + +import ( + "context" + "io" + + "github.com/molecula/featurebase/v3/ctl" + "github.com/spf13/cobra" +) + +var cli *ctl.CLICommand + +// newCLICommand runs the FeatureBase CLI subcommand for ingesting bulk data. +func newCLICommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command { + cli = ctl.NewCLICommand(stdin, stdout, stderr) + cliCmd := &cobra.Command{ + Use: "cli", + Short: "Query FB with SQL3 from the command line", + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.Run(context.Background()) + }, + } + + flags := cliCmd.Flags() + flags.StringVarP(&cli.Host, "host", "", cli.Host, "hostname of FeatureBase.") + flags.StringVarP(&cli.Port, "port", "", cli.Port, "port of FeatureBase.") + + return cliCmd +} diff --git a/cmd/featurebase-parse-sql/main.go b/cmd/featurebase-parse-sql/main.go index 32a7f31e0..1a214a78c 100644 --- a/cmd/featurebase-parse-sql/main.go +++ b/cmd/featurebase-parse-sql/main.go @@ -10,7 +10,7 @@ import ( "os" "strings" - "github.com/featurebasedb/featurebase/v3/sql2" + "github.com/molecula/featurebase/v3/sql3/parser" ) func main() { @@ -33,7 +33,7 @@ func run(ctx context.Context, args []string) error { return fmt.Errorf("query required") } - stmt, err := sql2.NewParser(strings.NewReader(q)).ParseStatement() + stmt, err := parser.NewParser(strings.NewReader(q)).ParseStatement() if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index e69987737..fda10b3a4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,6 +66,7 @@ at https://docs.featurebase.com/. rc.AddCommand(newHolderCmd(stdin, stdout, stderr)) rc.AddCommand(newHolderCmd(stdin, stdout, stderr)) rc.AddCommand(newKeygenCommand(stdin, stdout, stderr)) + rc.AddCommand(newCLICommand(stdin, stdout, stderr)) rc.SetOutput(stderr) return rc diff --git a/ctl/cli.go b/ctl/cli.go new file mode 100644 index 000000000..a1eaad6ea --- /dev/null +++ b/ctl/cli.go @@ -0,0 +1,246 @@ +package ctl + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/chzyer/readline" + "github.com/jedib0t/go-pretty/table" + "github.com/jedib0t/go-pretty/text" + featurebase "github.com/molecula/featurebase/v3" + "github.com/pkg/errors" +) + +const ( + promptBegin string = "fbsql> " + promptMid string = " -> " + terminationChar string = ";" + exitCommand string = "exit" + nullValue string = "NULL" +) + +var ( + splash string = fmt.Sprintf(`FeatureBase CLI (%s) +Type "exit" to quit. +`, featurebase.Version) +) + +type CLICommand struct { + Host string `json:"host"` + Port string `json:"port"` + + // commands holds the list of sql commands to be executed. + commands []string +} + +func NewCLICommand(stdin io.Reader, stdout, stderr io.Writer) *CLICommand { + return &CLICommand{ + Host: "localhost", + Port: "10101", + } +} + +func (cmd *CLICommand) Run(ctx context.Context) error { + // Print the splash message. + fmt.Print(splash) + + rl, err := readline.New(promptBegin) + if err != nil { + return errors.Wrap(err, "getting readline") + } + defer rl.Close() + + if !strings.HasPrefix(cmd.Host, "http") { + cmd.Host = "http://" + cmd.Host + } + + // partialCommand holds all input prior to receiving a termination + // character. + var partialCommand string + + // inMidCommand indicates whether a partial command has been received and + // we're still waiting for a termination character. + var inMidCommand bool + + for { + if inMidCommand { + rl.SetPrompt(promptMid) + } else { + rl.SetPrompt(promptBegin) + // Add some white space before each new prompt. + fmt.Println() + } + + // Read user provided input. + line, err := rl.Readline() + if err != nil { + return errors.Wrap(err, "reading line") + } + + if !inMidCommand { + // Handle the exit command. + if line == exitCommand || line == exitCommand+terminationChar { + break + } + } + + // Look for a termination character; + parts := strings.Split(line, terminationChar) + + // Length of 1 means a termination character was not received. + if len(parts) == 1 { + if parts[0] != "" { + partialCommand = appendCommand(partialCommand, parts[0]) + inMidCommand = true + } + continue + } + + for i, part := range parts { + partIsFinal := i == len(parts)-1 + partIsBlank := part == "" + + if partIsBlank && partIsFinal { + continue + } + + if partIsBlank && !partIsFinal { + if inMidCommand { + cmd.commands = append(cmd.commands, strings.TrimSpace(partialCommand)) + partialCommand = "" + inMidCommand = false + } + continue + } + + if !partIsBlank && partIsFinal { + partialCommand = part + inMidCommand = true + continue + } + + if !partIsBlank && !partIsFinal { + partialCommand = appendCommand(partialCommand, part) + cmd.commands = append(cmd.commands, strings.TrimSpace(partialCommand)) + partialCommand = "" + inMidCommand = false + } + } + + if err := cmd.executeCommands(ctx); err != nil { + return errors.Wrap(err, "executing commands") + } + } + + return nil +} + +func appendCommand(orig string, part string) string { + if orig == "" { + return part + } else { + return orig + " " + part + } +} + +func (cmd *CLICommand) executeCommands(ctx context.Context) error { + // Clear out the buffered commands on any exit from this method. + defer func() { + cmd.commands = nil + }() + + for _, sql := range cmd.commands { + resp, err := http.Post(fmt.Sprintf("%s:%s/sql", cmd.Host, cmd.Port), "application/sql", strings.NewReader(sql)) + if err != nil { + return errors.Wrapf(err, "posting query") + } + + var sqlResponse response + dec := json.NewDecoder(resp.Body) + err = dec.Decode(&sqlResponse) + if err != nil { + fmt.Printf("couldn't decode response: %v\n", err) + } + + err = sqlResponse.WriteOut(os.Stdout) + if err != nil { + return errors.Wrap(err, "writing out response") + } + } + + return nil +} + +type response struct { + Schema featurebase.SQLSchema `json:"schema"` + Data [][]interface{} `json:"data"` + Error string `json:"error"` + Warnings []string `json:"warnings"` + ExecutionTime int64 `json:"exec_time"` +} + +func (r *response) WriteOut(w io.Writer) error { + if r.Error != "" { + if _, err := w.Write([]byte("Error: " + r.Error + "\n")); err != nil { + return errors.Wrapf(err, "writing error: %s", r.Error) + } + return nil + } + + t := table.NewWriter() + t.SetOutputMirror(w) + + // Don't uppercase the header values. + t.Style().Format.Header = text.FormatDefault + + t.AppendHeader(schemaToRow(r.Schema)) + for _, row := range r.Data { + // If the value is nil, replace it with a null string; go-pretty doesn't + // expect nil pointers in the data values. + for i := range row { + if row[i] == nil { + row[i] = nullValue + } + } + t.AppendRow(table.Row(row)) + } + t.Render() + + if len(r.Warnings) > 0 { + if _, err := w.Write([]byte("\n")); err != nil { + return errors.Wrapf(err, "writing warning: %s", r.Error) + } + for _, warning := range r.Warnings { + if _, err := w.Write([]byte("Warning: " + warning + "\n")); err != nil { + return errors.Wrapf(err, "writing warning: %s", r.Error) + } + } + } + lifeAffirmingMessage := "" + if r.ExecutionTime < 1000000 { + lifeAffirmingMessage = " (You're welcome! 🚀)" + } + + if r.ExecutionTime > 5000000 { + lifeAffirmingMessage = " (Sorry! That took longer than expected 😭)" + } + + if _, err := w.Write([]byte(fmt.Sprintf("\nExecution time: %dμs%s\n", r.ExecutionTime, lifeAffirmingMessage))); err != nil { + return errors.Wrapf(err, "writing execution time: %s", r.Error) + } + + return nil +} + +func schemaToRow(schema featurebase.SQLSchema) []interface{} { + ret := make([]interface{}, len(schema.Fields)) + for i, field := range schema.Fields { + ret[i] = field.Name + } + return ret +} diff --git a/ctl/server.go b/ctl/server.go index 3204375b0..ea8cc0a89 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -88,15 +88,7 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { // RBF specific flags. See pilosa/rbf/cfg/cfg.go for definitions. srv.Config.RBFConfig.DefineFlags(flags) - // Postgres endpoint - flags.StringVar(&srv.Config.Postgres.Bind, "postgres.bind", srv.Config.Postgres.Bind, "Address to which to bind a postgres endpoint (leave blank to disable)") - SetTLSConfig(flags, "postgres.", &srv.Config.Postgres.TLS.CertificatePath, &srv.Config.Postgres.TLS.CertificateKeyPath, &srv.Config.Postgres.TLS.CACertPath, &srv.Config.Postgres.TLS.SkipVerify, &srv.Config.Postgres.TLS.EnableClientVerification) - flags.DurationVar((*time.Duration)(&srv.Config.Postgres.StartupTimeout), "postgres.startup-timeout", time.Duration(srv.Config.Postgres.StartupTimeout), "Timeout for postgres connection startup. (set 0 to disable)") - flags.DurationVar((*time.Duration)(&srv.Config.Postgres.ReadTimeout), "postgres.read-timeout", time.Duration(srv.Config.Postgres.ReadTimeout), "Timeout for reads on a postgres connection. (set 0 to disable; does not include connection idling)") - flags.DurationVar((*time.Duration)(&srv.Config.Postgres.WriteTimeout), "postgres.write-timeout", time.Duration(srv.Config.Postgres.WriteTimeout), "Timeout for writes on a postgres connection. (set 0 to disable)") - flags.Uint32Var(&srv.Config.Postgres.MaxStartupSize, "postgres.max-startup-size", srv.Config.Postgres.MaxStartupSize, "Maximum acceptable size of a postgres startup packet, in bytes. (set 0 to disable)") - flags.Uint16Var(&srv.Config.Postgres.ConnectionLimit, "postgres.connection-limit", srv.Config.Postgres.ConnectionLimit, "Maximum number of simultaneous postgres connections to allow. (set 0 to disable)") - flags.Uint16Var(&srv.Config.Postgres.SqlVersion, "postgres.sql-version", srv.Config.Postgres.SqlVersion, "Molecula Sql Handling Version (default 1)") + flags.BoolVar(&srv.Config.SQL.EndpointEnabled, "sql.endpoint-enabled", srv.Config.SQL.EndpointEnabled, "Enable FeatureBase SQL /sql endpoint (default false)") // Future flags. flags.BoolVar(&srv.Config.Future.Rename, "future.rename", false, "Present application name as FeatureBase. Defaults to false, will default to true in an upcoming release.") diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 000000000..534760bff --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,85 @@ +// Package errors wraps pkg/errors and includes some custom featurs such as +// error codes. +package errors + +import ( + "github.com/pkg/errors" +) + +// Code is an error code which can be used to check against a given error. For +// example, see the Is() method. +type Code string + +func New(code Code, message string) error { + return errors.WithStack(codedError{ + code: code, + message: message, + }) +} + +func As(err error, target interface{}) bool { + return errors.As(err, target) +} + +func Cause(err error) error { + return errors.Cause(err) +} + +func Errorf(format string, args ...interface{}) error { + return errors.Errorf(format, args...) +} + +// Is is a fork of the Is() method from `pkg/errors` which takes as its target +// an error Code instead of an error. +func Is(err error, target Code) bool { + match := codedError{ + code: target, + } + return errors.Is(err, match) +} + +func Unwrap(err error) error { + return errors.Unwrap(err) +} + +func WithMessage(err error, message string) error { + return errors.WithMessage(err, message) +} + +func WithMessagef(err error, format string, args ...interface{}) error { + return errors.WithMessagef(err, format, args...) +} + +func WithStack(err error) error { + return errors.WithStack(err) +} + +func Wrap(err error, message string) error { + return errors.Wrap(err, message) +} + +func Wrapf(err error, fmt string, args ...interface{}) error { + return errors.Wrapf(err, fmt, args...) +} + +// codedError is the fundamental type used by this package to provide coded +// errors. +type codedError struct { + code Code + message string +} + +func (ce codedError) Error() string { + return ce.message +} + +// func (ce codedError) As(target interface{}) bool { +// return false +// } + +func (ce codedError) Is(err error) bool { + if e, ok := err.(codedError); ok && ce.code == e.code { + return true + } + return false +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 000000000..0f0a4ab8d --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,81 @@ +package errors_test + +import ( + "fmt" + "testing" + + "github.com/molecula/featurebase/v3/errors" + "github.com/stretchr/testify/assert" +) + +func TestErrors(t *testing.T) { + + var errUncoded errors.Code = "TestErrUncoded" + var errFieldNotFound errors.Code = "TestErrFieldNotFound" + var errTableNotFound errors.Code = "TestErrTableNotFound" + + newErrFieldNotFound := func(fld string) error { + return errors.New( + errFieldNotFound, + fmt.Sprintf("field not found '%s'", fld), + ) + } + + newErrTableNotFound := func(tbl string) error { + return errors.New( + errTableNotFound, + fmt.Sprintf("table not found '%s'", tbl), + ) + } + + t.Run("Is", func(t *testing.T) { + uncoded := errors.New(errUncoded, "uncoded error") + fnf := newErrFieldNotFound("fld") + tnf := newErrTableNotFound("tbl") + fnfCustom := errors.New(errFieldNotFound, "custom field message") + + tests := []struct { + err error + target errors.Code + exp bool + }{ + { + err: uncoded, + target: errUncoded, + exp: true, + }, + { + err: uncoded, + target: errFieldNotFound, + exp: false, + }, + { + err: fnf, + target: errFieldNotFound, + exp: true, + }, + { + err: fnf, + target: errTableNotFound, + exp: false, + }, + { + err: errors.Wrap(tnf, "with message"), + target: errTableNotFound, + exp: true, + }, + { + err: fnfCustom, + target: errFieldNotFound, + exp: true, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { + got := errors.Is(test.err, test.target) + assert.Equal(t, test.exp, got) + }) + } + }) +} diff --git a/executor.go b/executor.go index 77857c6ea..cfe02d3d5 100644 --- a/executor.go +++ b/executor.go @@ -44,6 +44,10 @@ const ( errConnectionRefused = "connect: connection refused" ) +type Executor interface { + Execute(context.Context, string, *pql.Query, []uint64, *ExecOptions) (QueryResponse, error) +} + // executor recursively executes calls in a PQL query across all shards. type executor struct { Holder *Holder @@ -71,7 +75,7 @@ type executor struct { maxMemory int64 } -// executorOption is a functional option type for pilosa.Executor +// executorOption is a functional option type for pilosa.executor type executorOption func(e *executor) error func optExecutorInternalQueryClient(c *InternalClient) executorOption { @@ -113,7 +117,7 @@ func emptyResult(c *pql.Call) interface{} { return nil } -// newExecutor returns a new instance of Executor. +// newExecutor returns a new instance of executor. func newExecutor(opts ...executorOption) *executor { e := &executor{ workerPoolSize: 2, @@ -169,8 +173,8 @@ func (e *executor) InitStats() { } // Execute executes a PQL query. -func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shards []uint64, opt *execOptions) (QueryResponse, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.Execute") +func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shards []uint64, opt *ExecOptions) (QueryResponse, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.Execute") span.LogKV("pql", q.String()) defer span.Finish() @@ -204,7 +208,7 @@ func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shar // Default options. if opt == nil { - opt = &execOptions{} + opt = &ExecOptions{} } // Default maximum memory, if not passed in. if opt.MaxMemory == 0 && q.HasCall("Extract") { @@ -336,7 +340,7 @@ func safeCopy(resp QueryResponse) (out QueryResponse) { // handlePreCalls traverses the call tree looking for calls that need // precomputed values (e.g. Distinct, UnionRows, ConstRow...). -func (e *executor) handlePreCalls(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) error { +func (e *executor) handlePreCalls(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) error { if c.Name == "Precomputed" { idx := c.Args["valueidx"].(int64) if idx >= 0 && idx < int64(len(opt.EmbeddedData)) { @@ -435,7 +439,7 @@ func (e *executor) dumpPrecomputedCalls(ctx context.Context, c *pql.Call) { } // handlePreCallChildren handles any pre-calls in the children of a given call. -func (e *executor) handlePreCallChildren(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) error { +func (e *executor) handlePreCallChildren(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) error { for i := range c.Children { if err := ctx.Err(); err != nil { return err @@ -462,8 +466,8 @@ func (e *executor) handlePreCallChildren(ctx context.Context, qcx *Qcx, index st return nil } -func (e *executor) execute(ctx context.Context, qcx *Qcx, index string, q *pql.Query, shards []uint64, opt *execOptions) ([]interface{}, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.execute") +func (e *executor) execute(ctx context.Context, qcx *Qcx, index string, q *pql.Query, shards []uint64, opt *ExecOptions) ([]interface{}, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.execute") defer span.Finish() // Apply translations if necessary. @@ -604,7 +608,7 @@ func (vc *ValCount) cleanup() { } // preprocessQuery expands any calls that need preprocessing. -func (e *executor) preprocessQuery(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*pql.Call, error) { +func (e *executor) preprocessQuery(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*pql.Call, error) { switch c.Name { case "All": _, hasLimit, err := c.UintArg("limit") @@ -651,8 +655,8 @@ func (e *executor) preprocessQuery(ctx context.Context, qcx *Qcx, index string, } // executeCall executes a call. -func (e *executor) executeCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (interface{}, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeCall") +func (e *executor) executeCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (interface{}, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeCall") defer span.Finish() if err := validateQueryContext(ctx); err != nil { @@ -843,11 +847,11 @@ func (e *executor) validateTimeCallArgs(c *pql.Call, indexName string) error { return nil } -func (e *executor) executeOptionsCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (interface{}, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeOptionsCall") +func (e *executor) executeOptionsCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (interface{}, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeOptionsCall") defer span.Finish() - optCopy := &execOptions{} + optCopy := &ExecOptions{} *optCopy = *opt if arg, ok := c.Args["shards"]; ok { if optShards, ok := arg.([]interface{}); ok { @@ -867,7 +871,7 @@ func (e *executor) executeOptionsCall(ctx context.Context, qcx *Qcx, index strin } // executeIncludesColumnCall executes an IncludesColumn() call. -func (e *executor) executeIncludesColumnCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (bool, error) { +func (e *executor) executeIncludesColumnCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (bool, error) { // Get the shard containing the column, since that's the only // shard that needs to execute this query. var shard uint64 @@ -903,7 +907,7 @@ func (e *executor) executeIncludesColumnCall(ctx context.Context, qcx *Qcx, inde } // executeFieldValueCall executes a FieldValue() call. -func (e *executor) executeFieldValueCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ ValCount, err error) { +func (e *executor) executeFieldValueCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) { fieldName, ok := c.Args["field"].(string) if !ok || fieldName == "" { return ValCount{}, ErrFieldRequired @@ -994,7 +998,7 @@ func (e *executor) executeFieldValueCallShard(ctx context.Context, qcx *Qcx, fie } // executeLimitCall executes a Limit() call. -func (e *executor) executeLimitCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*Row, error) { +func (e *executor) executeLimitCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*Row, error) { bitmapCall := c.Children[0] limit, hasLimit, err := c.UintArg("limit") @@ -1071,7 +1075,7 @@ func (e *executor) executeLimitCall(ctx context.Context, qcx *Qcx, index string, // executeIncludesColumnCallShard func (e *executor) executeIncludesColumnCallShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64, column uint64) (_ bool, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeIncludesColumnCallShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeIncludesColumnCallShard") defer span.Finish() if len(c.Children) == 1 { @@ -1086,8 +1090,8 @@ func (e *executor) executeIncludesColumnCallShard(ctx context.Context, qcx *Qcx, } // executeSum executes a Sum() call. -func (e *executor) executeSum(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ ValCount, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSum") +func (e *executor) executeSum(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeSum") defer span.Finish() fieldName, err := c.FirstStringArg("field", "_field") @@ -1140,8 +1144,8 @@ func (e *executor) executeSum(ctx context.Context, qcx *Qcx, index string, c *pq // executeDistinct executes a Distinct call on a field. It returns a // SignedRow for int fields and a *Row for set/mutex/time fields. -func (e *executor) executeDistinct(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (interface{}, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeDistinct") +func (e *executor) executeDistinct(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (interface{}, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeDistinct") defer span.Finish() field, hasField, err := c.StringArg("field") @@ -1192,8 +1196,8 @@ func (e *executor) executeDistinct(ctx context.Context, qcx *Qcx, index string, } // executeMin executes a Min() call. -func (e *executor) executeMin(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ ValCount, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeMin") +func (e *executor) executeMin(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeMin") defer span.Finish() if _, err := c.FirstStringArg("field", "_field"); err != nil { @@ -1228,8 +1232,8 @@ func (e *executor) executeMin(ctx context.Context, qcx *Qcx, index string, c *pq } // executeMax executes a Max() call. -func (e *executor) executeMax(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ ValCount, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeMax") +func (e *executor) executeMax(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeMax") defer span.Finish() if _, err := c.FirstStringArg("field", "_field"); err != nil { @@ -1264,8 +1268,8 @@ func (e *executor) executeMax(ctx context.Context, qcx *Qcx, index string, c *pq } // executePercentile executes a Percentile() call. -func (e *executor) executePercentile(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ ValCount, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executePercentile") +func (e *executor) executePercentile(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executePercentile") defer span.Finish() // get nth @@ -1390,8 +1394,8 @@ func (e *executor) executePercentile(ctx context.Context, qcx *Qcx, index string } // executeMinRow executes a MinRow() call. -func (e *executor) executeMinRow(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ interface{}, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeMinRow") +func (e *executor) executeMinRow(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ interface{}, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeMinRow") defer span.Finish() if field := c.Args["field"]; field == "" { @@ -1429,8 +1433,8 @@ func (e *executor) executeMinRow(ctx context.Context, qcx *Qcx, index string, c } // executeMaxRow executes a MaxRow() call. -func (e *executor) executeMaxRow(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ interface{}, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeMaxRow") +func (e *executor) executeMaxRow(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ interface{}, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeMaxRow") defer span.Finish() if field := c.Args["field"]; field == "" { @@ -1468,8 +1472,8 @@ func (e *executor) executeMaxRow(ctx context.Context, qcx *Qcx, index string, c } // executePrecomputedCall pretends to execute a call that we have a precomputed value for. -func (e *executor) executePrecomputedCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ *Row, err error) { - span, _ := tracing.StartSpanFromContext(ctx, "Executor.executePrecomputedCall") +func (e *executor) executePrecomputedCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ *Row, err error) { + span, _ := tracing.StartSpanFromContext(ctx, "executor.executePrecomputedCall") defer span.Finish() result := NewRow() @@ -1480,8 +1484,8 @@ func (e *executor) executePrecomputedCall(ctx context.Context, qcx *Qcx, index s } // executeBitmapCall executes a call that returns a bitmap. -func (e *executor) executeBitmapCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ *Row, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeBitmapCall") +func (e *executor) executeBitmapCall(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ *Row, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeBitmapCall") span.LogKV("pqlCallName", c.Name) defer span.Finish() @@ -1529,7 +1533,7 @@ func (e *executor) executeBitmapCallShard(ctx context.Context, qcx *Qcx, index s return nil, err } - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeBitmapCallShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeBitmapCallShard") defer span.Finish() switch c.Name { @@ -1563,7 +1567,7 @@ func (e *executor) executeBitmapCallShard(ctx context.Context, qcx *Qcx, index s // executeDistinctShard executes a Distinct call on a single shard, yielding // a SignedRow of the values found. func (e *executor) executeDistinctShard(ctx context.Context, qcx *Qcx, index string, fieldName string, c *pql.Call, shard uint64) (result interface{}, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeDistinctShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeDistinctShard") defer span.Finish() idx := e.Holder.Index(index) @@ -1899,7 +1903,7 @@ func executeDistinctShardBSI(ctx context.Context, qcx *Qcx, idx *Index, fieldNam // executeSumCountShard calculates the sum and count for bsiGroups on a shard. func (e *executor) executeSumCountShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, filter *Row, shard uint64) (_ ValCount, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSumCountShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeSumCountShard") defer span.Finish() // use tx to keep consistency between @@ -1942,7 +1946,7 @@ func (e *executor) executeSumCountShard(ctx context.Context, qcx *Qcx, index str } defer finisher(&err0) - sumspan, _ := tracing.StartSpanFromContext(ctx, "Executor.executeSumCountShard_fragment.sum") + sumspan, _ := tracing.StartSpanFromContext(ctx, "executor.executeSumCountShard_fragment.sum") defer sumspan.Finish() vsum, vcount, err := fragment.sum(tx, filter, bsig.BitDepth) if err != nil { @@ -1962,7 +1966,7 @@ func (e *executor) executeSumCountShard(ctx context.Context, qcx *Qcx, index str // executeMinShard calculates the min for bsiGroups on a shard. func (e *executor) executeMinShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ ValCount, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeMinShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeMinShard") defer span.Finish() idx := e.Holder.Index(index) @@ -2114,8 +2118,8 @@ func (e *executor) executeMaxRowShard(ctx context.Context, qcx *Qcx, index strin }, nil } -func (e *executor) executeTopK(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (interface{}, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeTopK") +func (e *executor) executeTopK(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (interface{}, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeTopK") defer span.Finish() mapFn := func(ctx context.Context, shard uint64, mopt *mapOptions) (_ interface{}, err error) { @@ -2173,7 +2177,7 @@ func (e *executor) executeTopK(ctx context.Context, qcx *Qcx, index string, c *p // executeTopKShard builds a perpendicular BSI bitmap of a shard for TopK. func (e *executor) executeTopKShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ []*Row, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeTopKShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeTopKShard") defer span.Finish() // Look up the index. @@ -2246,7 +2250,7 @@ func (e *executor) executeTopKShard(ctx context.Context, qcx *Qcx, index string, // executeTopKShardSet builds a perpendicular BSI bitmap of a set field within a shard. func (e *executor) executeTopKShardSet(ctx context.Context, tx Tx, filter *Row, index, field string, shard uint64) ([]*Row, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeTopKShardSet") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeTopKShardSet") defer span.Finish() f := e.Holder.fragment(index, field, viewStandard, shard) @@ -2536,8 +2540,8 @@ func (f *topKFilter) fillIt(it roaring.ContainerIterator) { // executeTopN executes a TopN() call. // This first performs the TopN() to determine the top results and then // requeries to retrieve the full counts for each of the top results. -func (e *executor) executeTopN(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*PairsField, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeTopN") +func (e *executor) executeTopN(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*PairsField, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeTopN") defer span.Finish() idsArg, _, err := c.UintSliceArg("ids") @@ -2588,8 +2592,8 @@ func (e *executor) executeTopN(ctx context.Context, qcx *Qcx, index string, c *p }, nil } -func (e *executor) executeTopNShards(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*PairsField, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeTopNShards") +func (e *executor) executeTopNShards(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*PairsField, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeTopNShards") defer span.Finish() // Execute calls in bulk on each remote node and merge. @@ -2627,7 +2631,7 @@ func (e *executor) executeTopNShards(ctx context.Context, qcx *Qcx, index string // executeTopNShard executes a TopN call for a single shard. func (e *executor) executeTopNShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ *PairsField, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeTopNShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeTopNShard") defer span.Finish() fieldName, _ := c.Args["_field"].(string) @@ -2708,7 +2712,7 @@ func (e *executor) executeTopNShard(ctx context.Context, qcx *Qcx, index string, // executeDifferenceShard executes a difference() call for a local shard. func (e *executor) executeDifferenceShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ *Row, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeDifferenceShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeDifferenceShard") defer span.Finish() var other *Row @@ -2938,8 +2942,8 @@ func findGroupCounts(v interface{}) []GroupCount { return nil } -func (e *executor) executeGroupBy(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*GroupCounts, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeGroupBy") +func (e *executor) executeGroupBy(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*GroupCounts, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeGroupBy") defer span.Finish() // validate call if len(c.Children) == 0 { @@ -3666,7 +3670,7 @@ func applyConditionToGroupCounts(gcs []GroupCount, subj string, cond *pql.Condit } func (e *executor) executeGroupByShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, filter *pql.Call, shard uint64, childRows []RowIDs, bases map[int]int64, ignoreLimit bool) (_ []GroupCount, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeGroupByShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeGroupByShard") defer span.Finish() var filterRow *Row @@ -3681,7 +3685,7 @@ func (e *executor) executeGroupByShard(ctx context.Context, qcx *Qcx, index stri return nil, err } - newspan, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeGroupByShard_newGroupByIterator") + newspan, ctx := tracing.StartSpanFromContext(ctx, "executor.executeGroupByShard_newGroupByIterator") iter, err := newGroupByIterator(e, qcx, childRows, c.Children, aggregate, filterRow, index, shard, e.Holder) newspan.Finish() @@ -3734,7 +3738,7 @@ func (e *executor) executeGroupByShard(ctx context.Context, qcx *Qcx, index stri return results, nil } -func (e *executor) executeRows(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (RowIDs, error) { +func (e *executor) executeRows(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (RowIDs, error) { // Fetch field name from argument. // Check "field" first for backwards compatibility. // TODO: remove at Pilosa 2.0 @@ -4104,7 +4108,7 @@ var ( typeSQLNullInt64 = reflect.TypeOf(sql.NullInt64{}) ) -func (e *executor) executeExternalLookup(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (res ExtractedTable, err error) { +func (e *executor) executeExternalLookup(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (res ExtractedTable, err error) { if e.Holder.lookupDB == nil { return ExtractedTable{}, errors.New("external DB connection is not configured") } @@ -4352,7 +4356,7 @@ type TimeArgs struct { To time.Time } -func (e *executor) executeExtract(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (ExtractedIDMatrix, error) { +func (e *executor) executeExtract(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (ExtractedIDMatrix, error) { // Extract the column filter call. if len(c.Children) < 1 { return ExtractedIDMatrix{}, errors.New("missing column filter in Extract") @@ -4758,7 +4762,7 @@ func (e *executor) executeExtractShard(ctx context.Context, qcx *Qcx, index stri } func (e *executor) executeRowShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ *Row, err0 error) { - span, _ := tracing.StartSpanFromContext(ctx, "Executor.executeRowShard") + span, _ := tracing.StartSpanFromContext(ctx, "executor.executeRowShard") defer span.Finish() // Handle bsiGroup ranges differently. @@ -4871,7 +4875,7 @@ func (e *executor) executeRowShard(ctx context.Context, qcx *Qcx, index string, // executeRowBSIGroupShard executes a range(bsiGroup) call for a local shard. func (e *executor) executeRowBSIGroupShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (cloneable *Row, err0 error) { - span, _ := tracing.StartSpanFromContext(ctx, "Executor.executeRowBSIGroupShard") + span, _ := tracing.StartSpanFromContext(ctx, "executor.executeRowBSIGroupShard") defer span.Finish() // Only one conditional should be present. @@ -5038,7 +5042,7 @@ func (e *executor) executeRowBSIGroupShard(ctx context.Context, qcx *Qcx, index // executeIntersectShard executes a intersect() call for a local shard. func (e *executor) executeIntersectShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ *Row, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeIntersectShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeIntersectShard") defer span.Finish() var other *Row @@ -5063,7 +5067,7 @@ func (e *executor) executeIntersectShard(ctx context.Context, qcx *Qcx, index st // executeUnionShard executes a union() call for a local shard. func (e *executor) executeUnionShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (out *Row, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeUnionShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeUnionShard") defer span.Finish() if len(c.Children) == 0 { @@ -5194,7 +5198,7 @@ func (e *executor) executeInnerUnionRowsShard(ctx context.Context, qcx *Qcx, ind // executeXorShard executes a xor() call for a local shard. func (e *executor) executeXorShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ *Row, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeXorShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeXorShard") defer span.Finish() other := NewRow() @@ -5235,7 +5239,7 @@ func (e *executor) executePrecomputedCallShard(ctx context.Context, qcx *Qcx, in // executeNotShard executes a Not() call for a local shard. func (e *executor) executeNotShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ *Row, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeNotShard") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeNotShard") defer span.Finish() if len(c.Children) == 0 { @@ -5294,7 +5298,7 @@ func (e *executor) executeConstRow(ctx context.Context, index string, c *pql.Cal return NewRow(ids...), nil } -func (e *executor) executeUnionRows(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*Row, error) { +func (e *executor) executeUnionRows(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*Row, error) { // Turn UnionRows(Rows(...)) into Union(Row(...), ...). var rows []*pql.Call for _, child := range c.Children { @@ -5380,7 +5384,7 @@ func (e *executor) executeUnionRows(ctx context.Context, qcx *Qcx, index string, // executeAllCallShard executes an All() call for a local shard. func (e *executor) executeAllCallShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (res *Row, err0 error) { - span, _ := tracing.StartSpanFromContext(ctx, "Executor.executeAllCallShard") + span, _ := tracing.StartSpanFromContext(ctx, "executor.executeAllCallShard") defer span.Finish() if len(c.Children) > 0 { @@ -5437,8 +5441,8 @@ func (e *executor) executeShiftShard(ctx context.Context, qcx *Qcx, index string } // executeCount executes a count() call. -func (e *executor) executeCount(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (uint64, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeCount") +func (e *executor) executeCount(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (uint64, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeCount") defer span.Finish() if len(c.Children) == 0 { @@ -5493,8 +5497,8 @@ func (e *executor) executeCount(ctx context.Context, qcx *Qcx, index string, c * } // executeClearBit executes a Clear() call. -func (e *executor) executeClearBit(ctx context.Context, qcx *Qcx, index string, c *pql.Call, opt *execOptions) (bool, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeClearBit") +func (e *executor) executeClearBit(ctx context.Context, qcx *Qcx, index string, c *pql.Call, opt *ExecOptions) (bool, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeClearBit") defer span.Finish() // Read colID @@ -5537,8 +5541,8 @@ func (e *executor) executeClearBit(ctx context.Context, qcx *Qcx, index string, } // executeClearBitField executes a Clear() call for a field. -func (e *executor) executeClearBitField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID, rowID uint64, opt *execOptions) (_ bool, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeClearBitField") +func (e *executor) executeClearBitField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID, rowID uint64, opt *ExecOptions) (_ bool, err0 error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeClearBitField") defer span.Finish() shard := colID / ShardWidth @@ -5583,8 +5587,8 @@ func (e *executor) executeClearBitField(ctx context.Context, qcx *Qcx, index str } // executeClearRow executes a ClearRow() call. -func (e *executor) executeClearRow(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (_ bool, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeClearRow") +func (e *executor) executeClearRow(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ bool, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeClearRow") defer span.Finish() // Ensure the field type supports ClearRow(). @@ -5635,7 +5639,7 @@ func (e *executor) executeClearRow(ctx context.Context, qcx *Qcx, index string, // executeClearRowShard executes a ClearRow() call for a single shard. func (e *executor) executeClearRowShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (_ bool, err0 error) { - span, _ := tracing.StartSpanFromContext(ctx, "Executor.executeClearRowShard") + span, _ := tracing.StartSpanFromContext(ctx, "executor.executeClearRowShard") defer span.Finish() fieldName, err := c.FieldArg() @@ -5684,7 +5688,7 @@ func (e *executor) executeClearRowShard(ctx context.Context, qcx *Qcx, index str // executeSetRow executes a Store() call. -func (e *executor) executeSetRow(ctx context.Context, qcx *Qcx, indexName string, c *pql.Call, shards []uint64, opt *execOptions) (bool, error) { +func (e *executor) executeSetRow(ctx context.Context, qcx *Qcx, indexName string, c *pql.Call, shards []uint64, opt *ExecOptions) (bool, error) { // Parse arguments. fieldName, err := c.FieldArg() if err != nil { @@ -5801,8 +5805,8 @@ func (e *executor) executeSetRowShard(ctx context.Context, qcx *Qcx, index strin } // executeSet executes a Set() call. -func (e *executor) executeSet(ctx context.Context, qcx *Qcx, index string, c *pql.Call, opt *execOptions) (_ bool, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSet") +func (e *executor) executeSet(ctx context.Context, qcx *Qcx, index string, c *pql.Call, opt *ExecOptions) (_ bool, err0 error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeSet") defer span.Finish() // Read colID. @@ -5896,8 +5900,8 @@ func (e *executor) executeSet(ctx context.Context, qcx *Qcx, index string, c *pq } // executeSetBitField executes a Set() call for a specific field. -func (e *executor) executeSetBitField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID, rowID uint64, timestamp *time.Time, opt *execOptions) (_ bool, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSetBitField") +func (e *executor) executeSetBitField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID, rowID uint64, timestamp *time.Time, opt *ExecOptions) (_ bool, err0 error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeSetBitField") defer span.Finish() shard := colID / ShardWidth @@ -5942,8 +5946,8 @@ func (e *executor) executeSetBitField(ctx context.Context, qcx *Qcx, index strin } // executeSetValueField executes a Set() call for a specific int field. -func (e *executor) executeSetValueField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID uint64, value int64, opt *execOptions) (_ bool, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSetValueField") +func (e *executor) executeSetValueField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID uint64, value int64, opt *ExecOptions) (_ bool, err0 error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeSetValueField") defer span.Finish() shard := colID / ShardWidth @@ -5989,8 +5993,8 @@ func (e *executor) executeSetValueField(ctx context.Context, qcx *Qcx, index str } // executeClearValueField removes value for colID if present -func (e *executor) executeClearValueField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID uint64, opt *execOptions) (_ bool, err0 error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeClearValueField") +func (e *executor) executeClearValueField(ctx context.Context, qcx *Qcx, index string, c *pql.Call, f *Field, colID uint64, opt *ExecOptions) (_ bool, err0 error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeClearValueField") defer span.Finish() shard := colID / ShardWidth @@ -6035,7 +6039,7 @@ func (e *executor) executeClearValueField(ctx context.Context, qcx *Qcx, index s // remoteExec executes a PQL query remotely for a set of shards on a node. func (e *executor) remoteExec(ctx context.Context, node *disco.Node, index string, q *pql.Query, shards []uint64, embed []*Row, maxMemory int64) (results []interface{}, err error) { // nolint: interfacer - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeExec") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeExec") defer span.Finish() // Encode request object. @@ -6090,8 +6094,8 @@ loop: // mapReduce has to ensure that it never returns before any work it spawned has // terminated. It's not enough to cancel the jobs; we have to wait for them to be // done, or we can unmap resources they're still using. -func (e *executor) mapReduce(ctx context.Context, index string, shards []uint64, c *pql.Call, opt *execOptions, mapFn mapFunc, reduceFn reduceFunc) (result interface{}, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.mapReduce") +func (e *executor) mapReduce(ctx context.Context, index string, shards []uint64, c *pql.Call, opt *ExecOptions, mapFn mapFunc, reduceFn reduceFunc) (result interface{}, err error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.mapReduce") defer span.Finish() ch := make(chan mapResponse) @@ -6220,8 +6224,8 @@ func makeEmbeddedDataForShards(allRows []*Row, shards []uint64) []*Row { return newRows } -func (e *executor) mapper(ctx context.Context, eg *errgroup.Group, ch chan mapResponse, nodes []*disco.Node, index string, shards []uint64, c *pql.Call, opt *execOptions, lastAttempt bool, mapFn mapFunc, reduceFn reduceFunc) (reterr error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.mapper") +func (e *executor) mapper(ctx context.Context, eg *errgroup.Group, ch chan mapResponse, nodes []*disco.Node, index string, shards []uint64, c *pql.Call, opt *ExecOptions, lastAttempt bool, mapFn mapFunc, reduceFn reduceFunc) (reterr error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.mapper") defer span.Finish() // Group shards together by nodes. @@ -6403,7 +6407,7 @@ var errShutdown = errors.New("executor has shut down") // mapperLocal performs map & reduce entirely on the local node. func (e *executor) mapperLocal(ctx context.Context, shards []uint64, mapFn mapFunc, reduceFn reduceFunc, memoryAvailable int64) (_ interface{}, err error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.mapperLocal") + span, ctx := tracing.StartSpanFromContext(ctx, "executor.mapperLocal") defer span.Finish() ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -7177,7 +7181,7 @@ func (e *executor) callZero(c *pql.Call) *pql.Call { } func (e *executor) translateResults(ctx context.Context, index string, idx *Index, calls []*pql.Call, results []interface{}, memoryAvailable int64) (err error) { - span, _ := tracing.StartSpanFromContext(ctx, "Executor.translateResults") + span, _ := tracing.StartSpanFromContext(ctx, "executor.translateResults") defer span.Finish() idMap := make(map[uint64]string) @@ -7859,8 +7863,8 @@ type mapResponse struct { err error } -// execOptions represents an execution context for a single Execute() call. -type execOptions struct { +// ExecOptions represents an execution context for a single Execute() call. +type ExecOptions struct { Remote bool Profile bool PreTranslated bool @@ -8713,8 +8717,8 @@ func decimalToInt64(dec pql.Decimal, opt FieldOptions) int64 { } // executeDeleteRecords executes a delete() call. -func (e *executor) executeDeleteRecords(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (bool, error) { - span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeDelete") +func (e *executor) executeDeleteRecords(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (bool, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "executor.executeDelete") defer span.Finish() if len(c.Children) == 0 { @@ -8978,7 +8982,7 @@ func deleteKeyTranslation(ctx context.Context, idx *Index, shard uint64, records return idx.TranslateStore(paritionID).Delete(records) } -func (e *executor) executeSort(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*SortedRow, error) { +func (e *executor) executeSort(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*SortedRow, error) { span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSort") defer span.Finish() diff --git a/go.mod b/go.mod index 60818781b..be55e715b 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/benbjohnson/immutable v0.3.0 github.com/buger/jsonparser v1.1.1 github.com/cespare/xxhash v1.1.0 + github.com/chzyer/readline v1.5.0 github.com/confluentinc/confluent-kafka-go v1.9.1 github.com/davecgh/go-spew v1.1.1 github.com/denisenkom/go-mssqldb v0.11.0 @@ -21,6 +22,7 @@ require ( github.com/getsentry/sentry-go v0.13.0 github.com/glycerine/vprint v0.0.0-20200730000117-76cea49a68ea github.com/go-avro/avro v0.0.0-20171219232920-444163702c11 + github.com/go-openapi/strfmt v0.21.2 // indirect github.com/go-sql-driver/mysql v1.6.0 github.com/go-test/deep v1.0.7 github.com/gogo/protobuf v1.3.2 @@ -32,6 +34,9 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.1 github.com/improbable-eng/grpc-web v0.15.0 + github.com/jedib0t/go-pretty v4.3.0+incompatible + github.com/jonboulle/clockwork v0.3.0 // indirect + github.com/klauspost/compress v1.15.1 // indirect github.com/lib/pq v1.10.5 github.com/molecula/apophenia v0.0.0-20190827192002-68b7a14a478b github.com/opentracing/opentracing-go v1.2.0 @@ -83,6 +88,7 @@ require ( github.com/DataDog/datadog-go/v5 v5.1.0 // indirect github.com/DataDog/gostackparse v0.5.0 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -93,6 +99,8 @@ require ( github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/errors v0.19.8 // indirect + github.com/go-stack/stack v1.8.0 // indirect github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/snappy v0.0.4 // indirect @@ -107,16 +115,16 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jonboulle/clockwork v0.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.1 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-runewidth v0.0.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect @@ -138,6 +146,7 @@ require ( go.etcd.io/etcd/client/v2 v2.305.4 // indirect go.etcd.io/etcd/pkg/v3 v3.5.4 // indirect go.etcd.io/etcd/raft/v3 v3.5.4 // indirect + go.mongodb.org/mongo-driver v1.7.5 // indirect go.opentelemetry.io/contrib v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect go.opentelemetry.io/otel v0.20.0 // indirect @@ -153,7 +162,7 @@ require ( go.uber.org/zap v1.17.0 // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect + golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29 // indirect diff --git a/go.sum b/go.sum index b2769115d..7dc6b3174 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -152,8 +154,14 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= @@ -276,9 +284,13 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/strfmt v0.21.2 h1:5NDNgadiX1Vhemth/TH4gCGopWSTdDjxl60H3B7f+os= +github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-pg/pg/v10 v10.0.0/go.mod h1:XHU1AkQW534GFuUdSiQ46+Xw6Ah+9+b8DlT4YwhiXL8= github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= @@ -297,6 +309,7 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= @@ -601,6 +614,8 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jaffee/commandeer v0.5.0 h1:241M9N+gHQmPyjIG+yy8GGcZPfzFuIyOmJHzm5ka92g= github.com/jaffee/commandeer v0.5.0/go.mod h1:kCwfuSvZ2T0NVEr3LDSo6fDUgi0xSBnAVDdkOKTtpLQ= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= @@ -653,6 +668,7 @@ github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -723,6 +739,7 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -742,6 +759,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -778,6 +796,7 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -997,6 +1016,7 @@ github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= @@ -1089,6 +1109,8 @@ go.etcd.io/etcd/raft/v3 v3.5.4/go.mod h1:SCuunjYvZFC0fBX0vxMSPjuZmpcSk+XaAcMrD6D go.etcd.io/etcd/server/v3 v3.5.4 h1:CMAZd0g8Bn5NRhynW6pKhc4FRg41/0QYy3d7aNm9874= go.etcd.io/etcd/server/v3 v3.5.4/go.mod h1:S5/YTU15KxymM5l3T6b09sNOHPXqGYIZStpuuGbb65c= go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= +go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1396,8 +1418,9 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1644,6 +1667,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= diff --git a/http_handler.go b/http_handler.go index cdd186338..1f80a5f6d 100644 --- a/http_handler.go +++ b/http_handler.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "math" + "math/big" "mime" "net" "net/http" @@ -40,6 +41,17 @@ import ( "github.com/felixge/fgprof" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/molecula/featurebase/v3/authn" + "github.com/molecula/featurebase/v3/authz" + "github.com/molecula/featurebase/v3/disco" + "github.com/molecula/featurebase/v3/ingest" + "github.com/molecula/featurebase/v3/logger" + "github.com/molecula/featurebase/v3/monitor" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/rbf" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/molecula/featurebase/v3/storage" + "github.com/molecula/featurebase/v3/tracing" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" dto "github.com/prometheus/client_model/go" @@ -80,6 +92,10 @@ type Handler struct { auth *authn.Auth permissions *authz.GroupPermissions + + // sqlEnabled is serving as a feature flag for turning on/off the /sql + // endpoint. + sqlEnabled bool } // externalPrefixFlag denotes endpoints that are intended to be exposed to clients. @@ -212,6 +228,14 @@ func OptHandlerCloseTimeout(d time.Duration) handlerOption { } } +// OptHandlerSQLEnabled enables the /sql endpoint. +func OptHandlerSQLEnabled(v bool) handlerOption { + return func(h *Handler) error { + h.sqlEnabled = v + return nil + } +} + var makeImportOk sync.Once var importOk []byte @@ -520,6 +544,12 @@ func newRouter(handler *Handler) http.Handler { router.HandleFunc("/transaction/{id}/finish", handler.chkAuthZ(handler.handlePostFinishTransaction, authz.Read)).Methods("POST").Name("PostFinishTransaction") router.HandleFunc("/transactions", handler.chkAuthZ(handler.handleGetTransactions, authz.Read)).Methods("GET").Name("GetTransactions") router.HandleFunc("/queries", handler.chkAuthZ(handler.handleGetActiveQueries, authz.Admin)).Methods("GET").Name("GetActiveQueries") + + // enable this endpoint based on config + if handler.sqlEnabled { + router.HandleFunc("/sql", handler.chkAuthZ(handler.handlePostSQL, authz.Admin)).Methods("POST").Name("PostSQL") + } + router.HandleFunc("/query-history", handler.chkAuthZ(handler.handleGetPastQueries, authz.Admin)).Methods("GET").Name("GetPastQueries") router.HandleFunc("/version", handler.handleGetVersion).Methods("GET").Name("GetVersion") @@ -1325,6 +1355,159 @@ func (h *Handler) handlePostQuery(w http.ResponseWriter, r *http.Request) { } } +func (h *Handler) writeBadRequest(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(http.StatusBadRequest) + e := h.writeQueryResponse(w, r, &QueryResponse{Err: err}) + if e != nil { + h.logger.Errorf("write query response error: %v (while trying to write another error: %v)", e, err) + } + +} + +// handlePostSQL handles /sql requests. +func (h *Handler) handlePostSQL(w http.ResponseWriter, r *http.Request) { + + b, err := io.ReadAll(r.Body) + + if err != nil { + h.writeBadRequest(w, r, err) + return + } + + start := time.Now() + + rootOperator, err := h.api.CompilePlan(r.Context(), string(b)) + if err != nil { + h.writeBadRequest(w, r, err) + return + } + + // Write response back to client. + w.Header().Set("Content-Type", "application/json") + + // the pandas data frame format in json per https://molecula.atlassian.net/wiki/spaces/MOLECULA/pages/540999700/Queries + + // Opening bracket. + w.Write([]byte("{")) + + // Write the closing bracket on any exit from this method. + defer func() { + duration := time.Since(start) + value, err := json.Marshal(duration.Microseconds()) + if err != nil { + value = big.NewInt(-1).Bytes() + } + w.Write([]byte(`,"exec_time":`)) + w.Write(value) + w.Write([]byte("}")) + }() + + // writeError is a helper function that can be called anywhere during the + // output handling to insert an error into the json output. + writeError := func(err error) { + if err != nil { + errMsg, err := json.Marshal(err.Error()) + if err != nil { + errMsg = []byte(`"PROBLEM ENCODING ERROR MESSAGE"`) + } + w.Write([]byte(`,"error":`)) + w.Write(errMsg) + } + } + + // writeWarnings is a helper function that can be called anywhere during the + // output handling to insert warnings into the json output. + writeWarnings := func(warnings []string) { + if len(warnings) > 0 { + w.Write([]byte(`,"warnings": [`)) + for i, warn := range warnings { + warnMsg, err := json.Marshal(warn) + if err != nil { + warnMsg = []byte(`"PROBLEM ENCODING WARNING"`) + } + w.Write(warnMsg) + if i < len(warnings)-1 { + w.Write([]byte(`,`)) + } + } + w.Write([]byte(`]`)) + } + } + + // Get a query iterator. + iter, err := rootOperator.Iterator(r.Context(), nil) + if err != nil { + writeError(err) + writeWarnings(rootOperator.Warnings()) + return + } + + // Read schema & write to response. + columns := rootOperator.Schema() + schema := SQLSchema{ + Fields: make([]*SQLField, len(columns)), + } + for i, col := range columns { + schema.Fields[i] = &SQLField{ + Name: col.Name, + Type: col.Type.TypeName(), + } + } + w.Write([]byte(`"schema":`)) + jsonSchema, err := json.Marshal(schema) + if err != nil { + h.logger.Errorf("write schema response error: %s", err) + // Provide an empty list as the schema value to maintain valid json. + w.Write([]byte("[]")) + writeError(err) + writeWarnings(rootOperator.Warnings()) + return + } + w.Write(jsonSchema) + + // Write the data (rows). + w.Write([]byte(`,"data":[`)) + + var rowErr error + var currentRow types.Row + var nextErr error + + rowCounter := 1 + for currentRow, nextErr = iter.Next(r.Context()); nextErr == nil; currentRow, nextErr = iter.Next(r.Context()) { + jsonRow, err := json.Marshal(currentRow) + if err != nil { + h.logger.Errorf("json encoding error: %s", err) + rowErr = err + break + } + + if rowCounter > 1 { + // Include a comma between data rows. + w.Write([]byte(",")) + } + w.Write(jsonRow) + + rowCounter++ + } + if nextErr != nil && nextErr != types.ErrNoMoreRows { + rowErr = nextErr + } + + w.Write([]byte("]")) + + writeError(rowErr) + writeWarnings(rootOperator.Warnings()) +} + +type SQLField struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type SQLSchema struct { + Fields []*SQLField `json:"fields"` +} + func (h *Handler) handleCPUProfileStart(w http.ResponseWriter, r *http.Request) { if h.pprofCPUProfileBuffer == nil { diff --git a/index.go b/index.go index 2c17e4928..4ab793c24 100644 --- a/index.go +++ b/index.go @@ -938,6 +938,17 @@ type IndexInfo struct { ShardWidth uint64 `json:"shardWidth"` } +// Field returns the FieldInfo the provided field name. If the field does not +// exist, it returns nil +func (ii *IndexInfo) Field(name string) *FieldInfo { + for _, fld := range ii.Fields { + if fld.Name == name { + return fld + } + } + return nil +} + type indexInfoSlice []*IndexInfo func (p indexInfoSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } @@ -948,6 +959,7 @@ func (p indexInfoSlice) Less(i, j int) bool { return p[i].Name < p[j].Name } type IndexOptions struct { Keys bool `json:"keys"` TrackExistence bool `json:"trackExistence"` + PartitionN int `json:"partitionN"` } type importData struct { diff --git a/pg/cancel.go b/pg/cancel.go deleted file mode 100644 index d9e626261..000000000 --- a/pg/cancel.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import ( - "context" - "encoding/binary" - "io" - "sync" - - "github.com/pkg/errors" -) - -// ErrCancelledMissingConnection is an error triggered by cancelling a connection that does not exist. -var ErrCancelledMissingConnection = errors.New("cancelled connection does not exist") - -// CancellationToken is a value used to identify a backend for cancellation. -type CancellationToken struct { - PID, Key int32 -} - -// CancellationManager manages postgres connection cancellation. -type CancellationManager interface { - // Token acquires a new cancellation token. - // The returned channel is sent to every time the connection is cancelled. - // The connection may be cancelled an unlimited number of times. - Token() (<-chan struct{}, context.CancelFunc, CancellationToken, error) - - // Cancel sends a cancellation notification to the connection with the associated token. - // If the token is not associated with a connection, this returns ErrCancelledMissingConnection. - Cancel(CancellationToken) error -} - -// NewLocalCancellationManager creates an in-memory CancellationManager using randomly generated tokens. -// The provided reader is expected to be secure (e.g. crypto/rand.Reader). -func NewLocalCancellationManager(rand io.Reader) CancellationManager { - return &localCancellationManager{ - rand: rand, - connections: make(map[CancellationToken]chan<- struct{}), - } -} - -type localCancellationManager struct { - mu sync.RWMutex - rand io.Reader - connections map[CancellationToken]chan<- struct{} -} - -func (c *localCancellationManager) Token() (<-chan struct{}, context.CancelFunc, CancellationToken, error) { - notify := make(chan struct{}, 1) - -gen: - token, err := c.generateToken() - if err != nil { - return nil, nil, CancellationToken{}, err - } - cancel := c.registerToken(token, notify) - if cancel == nil { - goto gen - } - - return notify, cancel, token, nil -} - -func (c *localCancellationManager) generateToken() (CancellationToken, error) { - var data [8]byte - for { - var n int - for n < 8 { - nn, err := c.rand.Read(data[n:]) - if err != nil { - return CancellationToken{}, errors.Wrap(err, "generating a cancellation token") - } - n += nn - } - - pid := int32(binary.LittleEndian.Uint32(data[:4])) - if pid < 0 { - continue - } - key := int32(binary.LittleEndian.Uint32(data[4:])) - if key < 0 { - continue - } - - return CancellationToken{PID: pid, Key: key}, nil - } -} - -func (c *localCancellationManager) registerToken(token CancellationToken, notify chan<- struct{}) context.CancelFunc { - c.mu.Lock() - defer c.mu.Unlock() - - if _, collision := c.connections[token]; collision { - return nil - } - - c.connections[token] = notify - - return func() { - c.mu.Lock() - defer c.mu.Unlock() - - delete(c.connections, token) - - close(notify) - } -} - -func (c *localCancellationManager) Cancel(token CancellationToken) error { - c.mu.RLock() - defer c.mu.RUnlock() - - ch := c.connections[token] - if ch == nil { - return ErrCancelledMissingConnection - } - - select { - case ch <- struct{}{}: - default: - } - - return nil -} diff --git a/pg/cancel_test.go b/pg/cancel_test.go deleted file mode 100644 index 0872b5f43..000000000 --- a/pg/cancel_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import ( - "crypto/rand" - "testing" -) - -func TestCancel(t *testing.T) { - mgr := NewLocalCancellationManager(rand.Reader) - notify, cancel, token, err := mgr.Token() - if err != nil { - t.Fatal(err) - } - - select { - case <-notify: - t.Fatal("unexpected cancellation") - default: - } - - err = mgr.Cancel(token) - if err != nil { - t.Fatal(err) - } - - select { - case <-notify: - default: - t.Fatal("cancellation not propogated") - } - - select { - case <-notify: - t.Fatal("unexpected cancellation") - default: - } - - err = mgr.Cancel(CancellationToken{PID: -1, Key: -1}) - if err == nil { - t.Fatal("invalid cancellation completed") - } - - select { - case <-notify: - t.Fatal("unexpected cancellation") - default: - } - - cancel() - - select { - case _, ok := <-notify: - if ok { - t.Fatal("unexpected cancellation") - } - default: - t.Fatal("expected cancellation channel to be closed") - } - - err = mgr.Cancel(token) - if err == nil { - t.Fatal("invalid cancellation completed") - } -} diff --git a/pg/io.go b/pg/io.go deleted file mode 100644 index 6e062314b..000000000 --- a/pg/io.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import ( - "net" - "sync" - "sync/atomic" - "time" - "unsafe" - - "github.com/pkg/errors" -) - -// timeoutWriter wraps a connection and implements io.Writer with a timeout for each write. -type timeoutWriter struct { - conn net.Conn - timeout time.Duration -} - -func (w *timeoutWriter) Write(data []byte) (int, error) { - err := w.conn.SetWriteDeadline(time.Now().Add(w.timeout)) - if err != nil { - return 0, err - } - return w.conn.Write(data) -} - -// errPreempted is an error used to indicate preemption of an idle connection. -var errPreempted = errors.New("preempted during idle") - -// idleState is an atomic state value used to track a preemptible connection. -type idleState uint32 - -const ( - idleStateActive idleState = iota - idleStateIdle - idleStatePreempted - idleStatePendingPreemption -) - -func (s *idleState) load() idleState { - return idleState(atomic.LoadUint32((*uint32)(unsafe.Pointer(s)))) -} - -func (s *idleState) cas(old, new idleState) bool { - return atomic.CompareAndSwapUint32((*uint32)(unsafe.Pointer(s)), uint32(old), uint32(new)) -} - -// idleReader is an io.Reader implementation on a preemptible network connection. -// The connection has 2 modes: "idle" and "active". -// While in idle mode, the connection has no timeout but can be preempted. -// While in active mode, the connection may have a read timeout but cannot be immediately preempted. -// When a read completes in idle mode, the connection returns to active mode. -// If the connection is preempted in active mode, the preemption will be deferred until the connection returns to idle mode. -// This also provides a read timeout. -type idleReader struct { - conn net.Conn - timeout time.Duration - state idleState - preemptMu sync.Mutex -} - -// setIdle pushes the reader into idle mode. -// If a preemption is pending, it will be delivered on the next call to Read. -func (r *idleReader) setIdle() error { - // Clear the read deadline. - err := r.conn.SetReadDeadline(time.Time{}) - if err != nil { - return errors.Wrap(err, "failed to clear deadline") - } - - for { - // Transition to idle mode. - state := r.state.load() - var target idleState - switch state { - case idleStateActive: - // active -> idle - target = idleStateIdle - case idleStatePendingPreemption: - // pending preemption -> preempted - // Switching to idle mode activates the preemption. - target = idleStatePreempted - default: - panic("inconsistent state") - } - - if r.state.cas(state, target) { - return nil - } - } -} - -// preempt the reader. -// If the reader is not currently idle, the preemption will be delivered next time the connection enters idle mode. -// This does not wait until the preemption error is delivered. -func (r *idleReader) preempt() error { - r.preemptMu.Lock() - defer r.preemptMu.Unlock() - for { - state := r.state.load() - var target idleState - switch state { - case idleStateActive: - // active -> pending preemption - target = idleStatePendingPreemption - case idleStateIdle: - // idle -> preempted - target = idleStatePreempted - case idleStatePendingPreemption, idleStatePreempted: - // A preemption has already been delivered. - return nil - default: - panic("inconsistent state") - } - - ok := r.state.cas(state, target) - if ok && target == idleStatePreempted { - // We have entered preemption mode. - // Preempt the current read on the connection. - return r.conn.SetReadDeadline(time.Now()) - } - } -} - -// Read from the connection. -func (r *idleReader) Read(data []byte) (int, error) { - var needsDeadlineReset bool - state := r.state.load() - switch state { - case idleStateActive, idleStatePendingPreemption: - // Connection is active. - // There is no need to worry about preemption. - if r.timeout != 0 { - // Apply a read timeout. - err := r.conn.SetReadDeadline(time.Now().Add(r.timeout)) - if err != nil { - return 0, err - } - } - return r.conn.Read(data) - - case idleStateIdle: - // Read, and handle preemption. - n, err := r.conn.Read(data) - if err != nil { - // Check if the error was caused by preemption. - state = r.state.load() - switch { - case state == idleStatePreempted && n != 0: - // Some data was read before the preemption was delivered. - // Re-activate and discard the error. - - // Synchronize against the preempter. - // This is necessary to ensure that the cancellation deadline is cleared. - r.preemptMu.Lock() - defer r.preemptMu.Unlock() - - // Re-activate the connection. - // No CAS loop is necessary since we are synchronized against preempters. - r.state = idleStatePendingPreemption - - // The deadline may need to reset since the preempter may have changed it. - needsDeadlineReset = true - - case state == idleStatePreempted: - // The read was preempted. - return 0, errPreempted - - case state != idleStateIdle: - // No other states make sense here. - panic("inconsistent state") - - default: - // No preemption was involved. - // It is just a regular network error. - return n, err - } - } else { - // The read went through. - - // Exit from idle mode. - // Ideally, transition to active mode. - // However, a preemption may trigger while this is running. - for state == idleStateIdle { - if r.state.cas(idleStateIdle, idleStateActive) { - state = idleStateActive - break - } - - state = r.state.load() - } - - switch state { - case idleStateActive: - // The connection was reactivated normally. - - case idleStatePreempted: - // The connection was preempted after the read completed. - // Defer the preemption and complete successfully. - - // Synchronize against the preempter. - // This is necessary to ensure that the cancellation deadline is cleared. - r.preemptMu.Lock() - defer r.preemptMu.Unlock() - - // Re-activate the connection. - // No CAS loop is necessary since we are synchronized against preempters. - r.state = idleStatePendingPreemption - - // The deadline may need to reset since the preempter may have changed it. - needsDeadlineReset = true - - default: - panic("inconsistent state") - } - } - - if needsDeadlineReset && r.timeout == 0 { - // Clear the deadline. - err := r.conn.SetReadDeadline(time.Time{}) - if err != nil { - return n, err - } - } - - return n, nil - - case idleStatePreempted: - // The connection is preempted. - return 0, errPreempted - - default: - panic("inconsistent state") - } -} diff --git a/pg/lookerToFeaturebase.md b/pg/lookerToFeaturebase.md deleted file mode 100644 index 05a639a35..000000000 --- a/pg/lookerToFeaturebase.md +++ /dev/null @@ -1,652 +0,0 @@ -```mermaid -sequenceDiagram - -participant 213070643358480 as c0 -participant 213070643358482 as c1 -participant 213070643358484 as c2 -participant 213070643358486 as c3 -participant 213070643358488 as c4 -participant 213070643358490 as c5 -participant 213070643358492 as c6 -participant 213070643358494 as c7 -participant 213070643358496 as c8 -participant 213070643358498 as c9 -participant 213070643358500 as c10 -participant 213070643358502 as c11 -213070643358480->>server:+SSL REQUEST -server-->>213070643358480:-SSL BACKEND ANSWER: N -213070643358480->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358480:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358480:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358480:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358480:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358480:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358480:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358480:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358480:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358480:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358480:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358480:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358480:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358480:-BACKEND KEY DATA pid=1459324827, key=1506254533 -server-->>213070643358480:-READY FOR QUERY type= -213070643358480->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358480->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358480->>server:+EXECUTE name='', nb_rows=1 -213070643358480->>server:+SYNC -server-->>213070643358480:-PARSE COMPLETE -server-->>213070643358480:-BIND COMPLETE -server-->>213070643358480:-COMMAND COMPLETE command='SET' -server-->>213070643358480:-READY FOR QUERY type= -213070643358480->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358480->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358480->>server:+EXECUTE name='', nb_rows=1 -213070643358480->>server:+SYNC -server-->>213070643358480:-PARSE COMPLETE -server-->>213070643358480:-BIND COMPLETE -server-->>213070643358480:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358480:-COMMAND COMPLETE command='SET' -server-->>213070643358480:-READY FOR QUERY type= -213070643358480->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358480->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358480->>server:+DESCRIBE kind='P', name='' -213070643358480->>server:+EXECUTE name='', nb_rows=1 -213070643358480->>server:+SYNC -server-->>213070643358480:-PARSE COMPLETE -server-->>213070643358480:-BIND COMPLETE -server-->>213070643358480:-NO DATA -server-->>213070643358480:-EMPTY QUERY RESPONSE -server-->>213070643358480:-READY FOR QUERY type= -213070643358480->>server:+DISCONNECT -213070643358482->>server:+SSL REQUEST -server-->>213070643358482:-SSL BACKEND ANSWER: N -213070643358482->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358482:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358482:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358482:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358482:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358482:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358482:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358482:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358482:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358482:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358482:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358482:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358482:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358482:-BACKEND KEY DATA pid=1742342691, key=1299317425 -server-->>213070643358482:-READY FOR QUERY type= -213070643358482->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358482->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358482->>server:+EXECUTE name='', nb_rows=1 -213070643358482->>server:+SYNC -server-->>213070643358482:-PARSE COMPLETE -server-->>213070643358482:-BIND COMPLETE -server-->>213070643358482:-COMMAND COMPLETE command='SET' -server-->>213070643358482:-READY FOR QUERY type= -213070643358482->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358482->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358482->>server:+EXECUTE name='', nb_rows=1 -213070643358482->>server:+SYNC -server-->>213070643358482:-PARSE COMPLETE -server-->>213070643358482:-BIND COMPLETE -server-->>213070643358482:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358482:-COMMAND COMPLETE command='SET' -server-->>213070643358482:-READY FOR QUERY type= -213070643358482->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358482->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358482->>server:+DESCRIBE kind='P', name='' -213070643358482->>server:+EXECUTE name='', nb_rows=1 -213070643358482->>server:+SYNC -server-->>213070643358482:-PARSE COMPLETE -server-->>213070643358482:-BIND COMPLETE -server-->>213070643358482:-NO DATA -server-->>213070643358482:-EMPTY QUERY RESPONSE -server-->>213070643358482:-READY FOR QUERY type= -213070643358482->>server:+DISCONNECT -213070643358484->>server:+SSL REQUEST -server-->>213070643358484:-SSL BACKEND ANSWER: N -213070643358484->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358484:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358484:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358484:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358484:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358484:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358484:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358484:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358484:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358484:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358484:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358484:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358484:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358484:-BACKEND KEY DATA pid=700852804, key=1377869267 -server-->>213070643358484:-READY FOR QUERY type= -213070643358484->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358484->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358484->>server:+EXECUTE name='', nb_rows=1 -213070643358484->>server:+SYNC -server-->>213070643358484:-PARSE COMPLETE -server-->>213070643358484:-BIND COMPLETE -server-->>213070643358484:-COMMAND COMPLETE command='SET' -server-->>213070643358484:-READY FOR QUERY type= -213070643358484->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358484->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358484->>server:+EXECUTE name='', nb_rows=1 -213070643358484->>server:+SYNC -server-->>213070643358484:-PARSE COMPLETE -server-->>213070643358484:-BIND COMPLETE -server-->>213070643358484:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358484:-COMMAND COMPLETE command='SET' -server-->>213070643358484:-READY FOR QUERY type= -213070643358484->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358484->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358484->>server:+DESCRIBE kind='P', name='' -213070643358484->>server:+EXECUTE name='', nb_rows=1 -213070643358484->>server:+SYNC -server-->>213070643358484:-PARSE COMPLETE -server-->>213070643358484:-BIND COMPLETE -server-->>213070643358484:-NO DATA -server-->>213070643358484:-EMPTY QUERY RESPONSE -server-->>213070643358484:-READY FOR QUERY type= -213070643358484->>server:+DISCONNECT -213070643358486->>server:+SSL REQUEST -server-->>213070643358486:-SSL BACKEND ANSWER: N -213070643358486->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358486:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358486:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358486:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358486:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358486:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358486:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358486:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358486:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358486:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358486:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358486:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358486:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358486:-BACKEND KEY DATA pid=413235241, key=1302652759 -server-->>213070643358486:-READY FOR QUERY type= -213070643358486->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358486->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358486->>server:+EXECUTE name='', nb_rows=1 -213070643358486->>server:+SYNC -server-->>213070643358486:-PARSE COMPLETE -server-->>213070643358486:-BIND COMPLETE -server-->>213070643358486:-COMMAND COMPLETE command='SET' -server-->>213070643358486:-READY FOR QUERY type= -213070643358486->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358486->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358486->>server:+EXECUTE name='', nb_rows=1 -213070643358486->>server:+SYNC -server-->>213070643358486:-PARSE COMPLETE -server-->>213070643358486:-BIND COMPLETE -server-->>213070643358486:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358486:-COMMAND COMPLETE command='SET' -server-->>213070643358486:-READY FOR QUERY type= -213070643358486->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358486->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358486->>server:+DESCRIBE kind='P', name='' -213070643358486->>server:+EXECUTE name='', nb_rows=1 -213070643358486->>server:+SYNC -server-->>213070643358486:-PARSE COMPLETE -server-->>213070643358486:-BIND COMPLETE -server-->>213070643358486:-NO DATA -server-->>213070643358486:-EMPTY QUERY RESPONSE -server-->>213070643358486:-READY FOR QUERY type= -213070643358486->>server:+PARSE name='', num_params=0, params_type=, query=SELECT pg_backend_pid() -213070643358486->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358486->>server:+DESCRIBE kind='P', name='' -213070643358486->>server:+EXECUTE name='', nb_rows=0 -213070643358486->>server:+SYNC -server-->>213070643358486:-PARSE COMPLETE -server-->>213070643358486:-BIND COMPLETE -server-->>213070643358486:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='pg_backend_pid' type=23 type_len=4 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358486:-DATA ROW num_values=1 ---[Value 0001]--- length=9 value='413235241' -server-->>213070643358486:-COMMAND COMPLETE command='SELECT' -server-->>213070643358486:-READY FOR QUERY type= -213070643358486->>server:+PARSE name='', num_params=0, params_type=, query=SELECT VERSION() AS version -213070643358486->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358486->>server:+DESCRIBE kind='P', name='' -213070643358486->>server:+EXECUTE name='', nb_rows=0 -213070643358486->>server:+SYNC -server-->>213070643358486:-PARSE COMPLETE -server-->>213070643358486:-BIND COMPLETE -server-->>213070643358486:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='version' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358486:-DATA ROW num_values=1 ---[Value 0001]--- length=27 value='PostgresSQL 13.0 (molecula)' -server-->>213070643358486:-COMMAND COMPLETE command='SELECT' -server-->>213070643358486:-READY FOR QUERY type= -213070643358486->>server:+PARSE name='', num_params=0, params_type=, query= SELECT COUNT(*) FROM pg_type AS t0, pg_aggregate AS t1, pg_settings AS t2, pg_settings AS t3 -213070643358486->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358486->>server:+DESCRIBE kind='P', name='' -213070643358486->>server:+EXECUTE name='', nb_rows=0 -213070643358486->>server:+SYNC -213070643358488->>server:+SSL REQUEST -server-->>213070643358488:-SSL BACKEND ANSWER: N -213070643358488->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358488:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358488:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358488:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358488:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358488:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358488:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358488:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358488:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358488:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358488:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358488:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358488:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358488:-BACKEND KEY DATA pid=180554708, key=1504602717 -server-->>213070643358488:-READY FOR QUERY type= -213070643358488->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358488->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358488->>server:+EXECUTE name='', nb_rows=1 -213070643358488->>server:+SYNC -server-->>213070643358488:-PARSE COMPLETE -server-->>213070643358488:-BIND COMPLETE -server-->>213070643358488:-COMMAND COMPLETE command='SET' -server-->>213070643358488:-READY FOR QUERY type= -213070643358488->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358488->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358488->>server:+EXECUTE name='', nb_rows=1 -213070643358488->>server:+SYNC -server-->>213070643358488:-PARSE COMPLETE -server-->>213070643358488:-BIND COMPLETE -server-->>213070643358488:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358488:-COMMAND COMPLETE command='SET' -server-->>213070643358488:-READY FOR QUERY type= -213070643358488->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358488->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358488->>server:+DESCRIBE kind='P', name='' -213070643358488->>server:+EXECUTE name='', nb_rows=1 -213070643358488->>server:+SYNC -server-->>213070643358488:-PARSE COMPLETE -server-->>213070643358488:-BIND COMPLETE -server-->>213070643358488:-NO DATA -server-->>213070643358488:-EMPTY QUERY RESPONSE -server-->>213070643358488:-READY FOR QUERY type= -213070643358488->>server:+DISCONNECT -213070643358490->>server:+SSL REQUEST -server-->>213070643358490:-SSL BACKEND ANSWER: N -213070643358490->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358490:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358490:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358490:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358490:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358490:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358490:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358490:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358490:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358490:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358490:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358490:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358490:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358490:-BACKEND KEY DATA pid=1713101217, key=29850369 -server-->>213070643358490:-READY FOR QUERY type= -213070643358490->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358490->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358490->>server:+EXECUTE name='', nb_rows=1 -213070643358490->>server:+SYNC -server-->>213070643358490:-PARSE COMPLETE -server-->>213070643358490:-BIND COMPLETE -server-->>213070643358490:-COMMAND COMPLETE command='SET' -server-->>213070643358490:-READY FOR QUERY type= -213070643358490->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358490->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358490->>server:+EXECUTE name='', nb_rows=1 -213070643358490->>server:+SYNC -server-->>213070643358490:-PARSE COMPLETE -server-->>213070643358490:-BIND COMPLETE -server-->>213070643358490:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358490:-COMMAND COMPLETE command='SET' -server-->>213070643358490:-READY FOR QUERY type= -213070643358490->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358490->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358490->>server:+DESCRIBE kind='P', name='' -213070643358490->>server:+EXECUTE name='', nb_rows=1 -213070643358490->>server:+SYNC -server-->>213070643358490:-PARSE COMPLETE -server-->>213070643358490:-BIND COMPLETE -server-->>213070643358490:-NO DATA -server-->>213070643358490:-EMPTY QUERY RESPONSE -server-->>213070643358490:-READY FOR QUERY type= -213070643358490->>server:+PARSE name='', num_params=0, params_type=, query=SELECT VERSION() AS version -213070643358490->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358490->>server:+DESCRIBE kind='P', name='' -213070643358490->>server:+EXECUTE name='', nb_rows=0 -213070643358490->>server:+SYNC -server-->>213070643358490:-PARSE COMPLETE -server-->>213070643358490:-BIND COMPLETE -server-->>213070643358490:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='version' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358490:-DATA ROW num_values=1 ---[Value 0001]--- length=27 value='PostgresSQL 13.0 (molecula)' -server-->>213070643358490:-COMMAND COMPLETE command='SELECT' -server-->>213070643358490:-READY FOR QUERY type= -213070643358490->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358490->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358490->>server:+DESCRIBE kind='P', name='' -213070643358490->>server:+EXECUTE name='', nb_rows=1 -213070643358490->>server:+SYNC -server-->>213070643358490:-PARSE COMPLETE -server-->>213070643358490:-BIND COMPLETE -server-->>213070643358490:-NO DATA -server-->>213070643358490:-EMPTY QUERY RESPONSE -server-->>213070643358490:-READY FOR QUERY type= -213070643358490->>server:+PARSE name='', num_params=0, params_type=, query= SELECT pid as id, query as stmt, EXTRACT(seconds from query_start - NOW()) as elapsed_time FROM pg_stat_activity WHERE usename='docker' -213070643358490->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358490->>server:+DESCRIBE kind='P', name='' -213070643358490->>server:+EXECUTE name='', nb_rows=0 -213070643358490->>server:+SYNC -server-->>213070643358490:-PARSE COMPLETE -server-->>213070643358490:-BIND COMPLETE -server-->>213070643358490:-ROW DESCRIPTION: num_fields=3 ---[Field 01]--- name='id' type=23 type_len=4 type_mod=4294967295 relid=0 attnum=0 format=0 ---[Field 02]--- name='stmt' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 ---[Field 03]--- name='elapsed_time' type=701 type_len=8 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358490:-DATA ROW num_values=3 ---[Value 0001]--- length=9 value='413235241' ---[Value 0002]--- length=148 value=' SELECT COUNT(*). FROM pg_type AS t0,. pg_aggregate AS t1,. pg_settings AS t2,. pg_settings AS t3.' ---[Value 0003]--- length=11 value='1.311371547' -server-->>213070643358490:-DATA ROW num_values=3 ---[Value 0001]--- length=10 value='1713101217' ---[Value 0002]--- length=190 value=' SELECT pid as id,. query as stmt,. EXTRACT(seconds from query_start - NOW()) as elapsed_time. FROM pg_stat_activity. WHERE usename='docker'.' ---[Value 0003]--- length=11 value='0.000169969' -server-->>213070643358490:-COMMAND COMPLETE command='SELECT' -server-->>213070643358490:-READY FOR QUERY type= -213070643358492->>server:+SSL REQUEST -server-->>213070643358492:-SSL BACKEND ANSWER: N -213070643358492->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358492:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358492:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358492:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358492:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358492:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358492:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358492:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358492:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358492:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358492:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358492:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358492:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358492:-BACKEND KEY DATA pid=2034361563, key=1419995666 -server-->>213070643358492:-READY FOR QUERY type= -213070643358492->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358492->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358492->>server:+EXECUTE name='', nb_rows=1 -213070643358492->>server:+SYNC -server-->>213070643358492:-PARSE COMPLETE -server-->>213070643358492:-BIND COMPLETE -server-->>213070643358492:-COMMAND COMPLETE command='SET' -server-->>213070643358492:-READY FOR QUERY type= -213070643358492->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358492->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358492->>server:+EXECUTE name='', nb_rows=1 -213070643358492->>server:+SYNC -server-->>213070643358492:-PARSE COMPLETE -server-->>213070643358492:-BIND COMPLETE -server-->>213070643358492:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358492:-COMMAND COMPLETE command='SET' -server-->>213070643358492:-READY FOR QUERY type= -213070643358492->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358492->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358492->>server:+DESCRIBE kind='P', name='' -213070643358492->>server:+EXECUTE name='', nb_rows=1 -213070643358492->>server:+SYNC -server-->>213070643358492:-PARSE COMPLETE -server-->>213070643358492:-BIND COMPLETE -server-->>213070643358492:-NO DATA -server-->>213070643358492:-EMPTY QUERY RESPONSE -server-->>213070643358492:-READY FOR QUERY type= -213070643358492->>server:+DISCONNECT -213070643358494->>server:+SSL REQUEST -server-->>213070643358494:-SSL BACKEND ANSWER: N -213070643358494->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358494:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358494:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358494:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358494:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358494:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358494:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358494:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358494:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358494:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358494:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358494:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358494:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358494:-BACKEND KEY DATA pid=964429462, key=1673405115 -server-->>213070643358494:-READY FOR QUERY type= -213070643358494->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358494->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358494->>server:+EXECUTE name='', nb_rows=1 -213070643358494->>server:+SYNC -server-->>213070643358494:-PARSE COMPLETE -server-->>213070643358494:-BIND COMPLETE -server-->>213070643358494:-COMMAND COMPLETE command='SET' -server-->>213070643358494:-READY FOR QUERY type= -213070643358494->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358494->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358494->>server:+EXECUTE name='', nb_rows=1 -213070643358494->>server:+SYNC -server-->>213070643358494:-PARSE COMPLETE -server-->>213070643358494:-BIND COMPLETE -server-->>213070643358494:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358494:-COMMAND COMPLETE command='SET' -server-->>213070643358494:-READY FOR QUERY type= -213070643358494->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358494->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358494->>server:+DESCRIBE kind='P', name='' -213070643358494->>server:+EXECUTE name='', nb_rows=1 -213070643358494->>server:+SYNC -server-->>213070643358494:-PARSE COMPLETE -server-->>213070643358494:-BIND COMPLETE -server-->>213070643358494:-NO DATA -server-->>213070643358494:-EMPTY QUERY RESPONSE -server-->>213070643358494:-READY FOR QUERY type= -213070643358494->>server:+PARSE name='', num_params=0, params_type=, query=select pg_terminate_backend(413235241) -213070643358494->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358494->>server:+DESCRIBE kind='P', name='' -213070643358494->>server:+EXECUTE name='', nb_rows=0 -213070643358494->>server:+SYNC -server-->>213070643358494:-PARSE COMPLETE -server-->>213070643358494:-BIND COMPLETE -server-->>213070643358494:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='pg_terminate_backend' type=16 type_len=1 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358494:-DATA ROW num_values=1 ---[Value 0001]--- length=1 value='t' -server-->>213070643358494:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643358486:-PARSE COMPLETE -server-->>213070643358486:-BIND COMPLETE -server-->>213070643358486:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='count' type=20 type_len=8 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358486:-ERROR RESPONSE Severity: 'FATAL' Message: 'terminating connection due to administrator command' Code: '57P01' -server-->>213070643358494:-READY FOR QUERY type= -213070643358490->>server:+DISCONNECT -213070643358494->>server:+DISCONNECT -213070643358496->>server:+SSL REQUEST -server-->>213070643358496:-SSL BACKEND ANSWER: N -213070643358496->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358496:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358496:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358496:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358496:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358496:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358496:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358496:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358496:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358496:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358496:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358496:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358496:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358496:-BACKEND KEY DATA pid=841257432, key=992978867 -server-->>213070643358496:-READY FOR QUERY type= -213070643358496->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358496->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358496->>server:+EXECUTE name='', nb_rows=1 -213070643358496->>server:+SYNC -server-->>213070643358496:-PARSE COMPLETE -server-->>213070643358496:-BIND COMPLETE -server-->>213070643358496:-COMMAND COMPLETE command='SET' -server-->>213070643358496:-READY FOR QUERY type= -213070643358496->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358496->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358496->>server:+EXECUTE name='', nb_rows=1 -213070643358496->>server:+SYNC -server-->>213070643358496:-PARSE COMPLETE -server-->>213070643358496:-BIND COMPLETE -server-->>213070643358496:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358496:-COMMAND COMPLETE command='SET' -server-->>213070643358496:-READY FOR QUERY type= -213070643358496->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358496->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358496->>server:+DESCRIBE kind='P', name='' -213070643358496->>server:+EXECUTE name='', nb_rows=1 -213070643358496->>server:+SYNC -server-->>213070643358496:-PARSE COMPLETE -server-->>213070643358496:-BIND COMPLETE -server-->>213070643358496:-NO DATA -server-->>213070643358496:-EMPTY QUERY RESPONSE -server-->>213070643358496:-READY FOR QUERY type= -213070643358496->>server:+DISCONNECT -213070643358498->>server:+SSL REQUEST -server-->>213070643358498:-SSL BACKEND ANSWER: N -213070643358498->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358498:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358498:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358498:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358498:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358498:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358498:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358498:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358498:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358498:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358498:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358498:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358498:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358498:-BACKEND KEY DATA pid=645931978, key=579078180 -server-->>213070643358498:-READY FOR QUERY type= -213070643358498->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358498->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358498->>server:+EXECUTE name='', nb_rows=1 -213070643358498->>server:+SYNC -server-->>213070643358498:-PARSE COMPLETE -server-->>213070643358498:-BIND COMPLETE -server-->>213070643358498:-COMMAND COMPLETE command='SET' -server-->>213070643358498:-READY FOR QUERY type= -213070643358498->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358498->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358498->>server:+EXECUTE name='', nb_rows=1 -213070643358498->>server:+SYNC -server-->>213070643358498:-PARSE COMPLETE -server-->>213070643358498:-BIND COMPLETE -server-->>213070643358498:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358498:-COMMAND COMPLETE command='SET' -server-->>213070643358498:-READY FOR QUERY type= -213070643358498->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358498->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358498->>server:+DESCRIBE kind='P', name='' -213070643358498->>server:+EXECUTE name='', nb_rows=1 -213070643358498->>server:+SYNC -server-->>213070643358498:-PARSE COMPLETE -server-->>213070643358498:-BIND COMPLETE -server-->>213070643358498:-NO DATA -server-->>213070643358498:-EMPTY QUERY RESPONSE -server-->>213070643358498:-READY FOR QUERY type= -213070643358498->>server:+PARSE name='', num_params=0, params_type=, query=SELECT 1 -213070643358498->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358498->>server:+DESCRIBE kind='P', name='' -213070643358498->>server:+EXECUTE name='', nb_rows=0 -213070643358498->>server:+SYNC -server-->>213070643358498:-PARSE COMPLETE -server-->>213070643358498:-BIND COMPLETE -server-->>213070643358498:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='?column?' type=23 type_len=4 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358498:-DATA ROW num_values=1 ---[Value 0001]--- length=1 value='1' -server-->>213070643358498:-COMMAND COMPLETE command='SELECT' -server-->>213070643358498:-READY FOR QUERY type= -213070643358498->>server:+DISCONNECT -213070643358500->>server:+SSL REQUEST -server-->>213070643358500:-SSL BACKEND ANSWER: N -213070643358500->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358500:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358500:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358500:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358500:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358500:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358500:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358500:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358500:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358500:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358500:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358500:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358500:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358500:-BACKEND KEY DATA pid=1632401438, key=1341645778 -server-->>213070643358500:-READY FOR QUERY type= -213070643358500->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358500->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358500->>server:+EXECUTE name='', nb_rows=1 -213070643358500->>server:+SYNC -server-->>213070643358500:-PARSE COMPLETE -server-->>213070643358500:-BIND COMPLETE -server-->>213070643358500:-COMMAND COMPLETE command='SET' -server-->>213070643358500:-READY FOR QUERY type= -213070643358500->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358500->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358500->>server:+EXECUTE name='', nb_rows=1 -213070643358500->>server:+SYNC -server-->>213070643358500:-PARSE COMPLETE -server-->>213070643358500:-BIND COMPLETE -server-->>213070643358500:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358500:-COMMAND COMPLETE command='SET' -server-->>213070643358500:-READY FOR QUERY type= -213070643358500->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358500->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358500->>server:+DESCRIBE kind='P', name='' -213070643358500->>server:+EXECUTE name='', nb_rows=1 -213070643358500->>server:+SYNC -server-->>213070643358500:-PARSE COMPLETE -server-->>213070643358500:-BIND COMPLETE -server-->>213070643358500:-NO DATA -server-->>213070643358500:-EMPTY QUERY RESPONSE -server-->>213070643358500:-READY FOR QUERY type= -213070643358500->>server:+DISCONNECT -213070643358502->>server:+SSL REQUEST -server-->>213070643358502:-SSL BACKEND ANSWER: N -213070643358502->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643358502:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643358502:-PARAMETER STATUS name='application_name', value='' -server-->>213070643358502:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643358502:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643358502:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643358502:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643358502:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643358502:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643358502:-PARAMETER STATUS name='server_version', value='13.0.0' -server-->>213070643358502:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643358502:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643358502:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643358502:-BACKEND KEY DATA pid=232586748, key=226557416 -server-->>213070643358502:-READY FOR QUERY type= -213070643358502->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643358502->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358502->>server:+EXECUTE name='', nb_rows=1 -213070643358502->>server:+SYNC -server-->>213070643358502:-PARSE COMPLETE -server-->>213070643358502:-BIND COMPLETE -server-->>213070643358502:-COMMAND COMPLETE command='SET' -server-->>213070643358502:-READY FOR QUERY type= -213070643358502->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643358502->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358502->>server:+EXECUTE name='', nb_rows=1 -213070643358502->>server:+SYNC -server-->>213070643358502:-PARSE COMPLETE -server-->>213070643358502:-BIND COMPLETE -server-->>213070643358502:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643358502:-COMMAND COMPLETE command='SET' -server-->>213070643358502:-READY FOR QUERY type= -213070643358502->>server:+PARSE name='', num_params=0, params_type=, query= -213070643358502->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358502->>server:+DESCRIBE kind='P', name='' -213070643358502->>server:+EXECUTE name='', nb_rows=1 -213070643358502->>server:+SYNC -server-->>213070643358502:-PARSE COMPLETE -server-->>213070643358502:-BIND COMPLETE -server-->>213070643358502:-NO DATA -server-->>213070643358502:-EMPTY QUERY RESPONSE -server-->>213070643358502:-READY FOR QUERY type= -213070643358502->>server:+PARSE name='', num_params=0, params_type=, query=SELECT VERSION() AS version -213070643358502->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643358502->>server:+DESCRIBE kind='P', name='' -213070643358502->>server:+EXECUTE name='', nb_rows=0 -213070643358502->>server:+SYNC -server-->>213070643358502:-PARSE COMPLETE -server-->>213070643358502:-BIND COMPLETE -server-->>213070643358502:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='version' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643358502:-DATA ROW num_values=1 ---[Value 0001]--- length=27 value='PostgresSQL 13.0 (molecula)' -server-->>213070643358502:-COMMAND COMPLETE command='SELECT' -server-->>213070643358502:-READY FOR QUERY type= -213070643358502->>server:+DISCONNECT -``` diff --git a/pg/lookerToPostgres.md b/pg/lookerToPostgres.md deleted file mode 100644 index 775ec79bd..000000000 --- a/pg/lookerToPostgres.md +++ /dev/null @@ -1,677 +0,0 @@ -```mermaid -sequenceDiagram - -participant 213070643360888 as c0 -participant 213070643360892 as c1 -participant 213070643360896 as c2 -participant 213070643360900 as c3 -participant 213070643360904 as c4 -participant 213070643360908 as c5 -participant 213070643360912 as c6 -participant 213070643360916 as c7 -participant 213070643360920 as c8 -participant 213070643360924 as c9 -participant 213070643360928 as c10 -participant 213070643360932 as c11 -213070643360888->>server:+SSL REQUEST -server-->>213070643360888:-SSL BACKEND ANSWER: N -213070643360888->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360888:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='affbc4bf') -213070643360888->>server:+PASSWORD MESSAGE password=md5de1ae46649b137ee14e14d8fd5fc6cb6 -server-->>213070643360888:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360888:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360888:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360888:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360888:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360888:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360888:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360888:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360888:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360888:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360888:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360888:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360888:-BACKEND KEY DATA pid=97, key=2120775944 -server-->>213070643360888:-READY FOR QUERY type= -213070643360888->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360888->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360888->>server:+EXECUTE name='', nb_rows=1 -213070643360888->>server:+SYNC -server-->>213070643360888:-PARSE COMPLETE -server-->>213070643360888:-BIND COMPLETE -server-->>213070643360888:-COMMAND COMPLETE command='SET' -server-->>213070643360888:-READY FOR QUERY type= -213070643360888->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360888->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360888->>server:+EXECUTE name='', nb_rows=1 -213070643360888->>server:+SYNC -server-->>213070643360888:-PARSE COMPLETE -server-->>213070643360888:-BIND COMPLETE -server-->>213070643360888:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360888:-COMMAND COMPLETE command='SET' -server-->>213070643360888:-READY FOR QUERY type= -213070643360888->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360888->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360888->>server:+DESCRIBE kind='P', name='' -213070643360888->>server:+EXECUTE name='', nb_rows=1 -213070643360888->>server:+SYNC -server-->>213070643360888:-PARSE COMPLETE -server-->>213070643360888:-BIND COMPLETE -server-->>213070643360888:-NO DATA -server-->>213070643360888:-EMPTY QUERY RESPONSE -server-->>213070643360888:-READY FOR QUERY type= -213070643360888->>server:+DISCONNECT -213070643360892->>server:+SSL REQUEST -server-->>213070643360892:-SSL BACKEND ANSWER: N -213070643360892->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360892:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='0b76ce86') -213070643360892->>server:+PASSWORD MESSAGE password=md5b82e4b6283fe5694c0199ca058378bb8 -server-->>213070643360892:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360892:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360892:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360892:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360892:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360892:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360892:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360892:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360892:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360892:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360892:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360892:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360892:-BACKEND KEY DATA pid=98, key=1329847468 -server-->>213070643360892:-READY FOR QUERY type= -213070643360892->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360892->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360892->>server:+EXECUTE name='', nb_rows=1 -213070643360892->>server:+SYNC -server-->>213070643360892:-PARSE COMPLETE -server-->>213070643360892:-BIND COMPLETE -server-->>213070643360892:-COMMAND COMPLETE command='SET' -server-->>213070643360892:-READY FOR QUERY type= -213070643360892->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360892->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360892->>server:+EXECUTE name='', nb_rows=1 -213070643360892->>server:+SYNC -server-->>213070643360892:-PARSE COMPLETE -server-->>213070643360892:-BIND COMPLETE -server-->>213070643360892:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360892:-COMMAND COMPLETE command='SET' -server-->>213070643360892:-READY FOR QUERY type= -213070643360892->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360892->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360892->>server:+DESCRIBE kind='P', name='' -213070643360892->>server:+EXECUTE name='', nb_rows=1 -213070643360892->>server:+SYNC -server-->>213070643360892:-PARSE COMPLETE -server-->>213070643360892:-BIND COMPLETE -server-->>213070643360892:-NO DATA -server-->>213070643360892:-EMPTY QUERY RESPONSE -server-->>213070643360892:-READY FOR QUERY type= -213070643360892->>server:+DISCONNECT -213070643360896->>server:+SSL REQUEST -server-->>213070643360896:-SSL BACKEND ANSWER: N -213070643360896->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360896:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='80e88231') -213070643360896->>server:+PASSWORD MESSAGE password=md59023db05ad8c94976359641cf0ada45a -server-->>213070643360896:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360896:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360896:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360896:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360896:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360896:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360896:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360896:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360896:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360896:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360896:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360896:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360896:-BACKEND KEY DATA pid=99, key=1084480878 -server-->>213070643360896:-READY FOR QUERY type= -213070643360896->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360896->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360896->>server:+EXECUTE name='', nb_rows=1 -213070643360896->>server:+SYNC -server-->>213070643360896:-PARSE COMPLETE -server-->>213070643360896:-BIND COMPLETE -server-->>213070643360896:-COMMAND COMPLETE command='SET' -server-->>213070643360896:-READY FOR QUERY type= -213070643360896->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360896->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360896->>server:+EXECUTE name='', nb_rows=1 -213070643360896->>server:+SYNC -server-->>213070643360896:-PARSE COMPLETE -server-->>213070643360896:-BIND COMPLETE -server-->>213070643360896:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360896:-COMMAND COMPLETE command='SET' -server-->>213070643360896:-READY FOR QUERY type= -213070643360896->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360896->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360896->>server:+DESCRIBE kind='P', name='' -213070643360896->>server:+EXECUTE name='', nb_rows=1 -213070643360896->>server:+SYNC -server-->>213070643360896:-PARSE COMPLETE -server-->>213070643360896:-BIND COMPLETE -server-->>213070643360896:-NO DATA -server-->>213070643360896:-EMPTY QUERY RESPONSE -server-->>213070643360896:-READY FOR QUERY type= -213070643360896->>server:+DISCONNECT -213070643360900->>server:+SSL REQUEST -server-->>213070643360900:-SSL BACKEND ANSWER: N -213070643360900->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360900:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='68a07783') -213070643360900->>server:+PASSWORD MESSAGE password=md5136c8b8ec47347f93827ec5d7023199c -server-->>213070643360900:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360900:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360900:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360900:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360900:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360900:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360900:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360900:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360900:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360900:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360900:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360900:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360900:-BACKEND KEY DATA pid=100, key=594556468 -server-->>213070643360900:-READY FOR QUERY type= -213070643360900->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360900->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360900->>server:+EXECUTE name='', nb_rows=1 -213070643360900->>server:+SYNC -server-->>213070643360900:-PARSE COMPLETE -server-->>213070643360900:-BIND COMPLETE -server-->>213070643360900:-COMMAND COMPLETE command='SET' -server-->>213070643360900:-READY FOR QUERY type= -213070643360900->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360900->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360900->>server:+EXECUTE name='', nb_rows=1 -213070643360900->>server:+SYNC -server-->>213070643360900:-PARSE COMPLETE -server-->>213070643360900:-BIND COMPLETE -server-->>213070643360900:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360900:-COMMAND COMPLETE command='SET' -server-->>213070643360900:-READY FOR QUERY type= -213070643360900->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360900->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360900->>server:+DESCRIBE kind='P', name='' -213070643360900->>server:+EXECUTE name='', nb_rows=1 -213070643360900->>server:+SYNC -server-->>213070643360900:-PARSE COMPLETE -server-->>213070643360900:-BIND COMPLETE -server-->>213070643360900:-NO DATA -server-->>213070643360900:-EMPTY QUERY RESPONSE -server-->>213070643360900:-READY FOR QUERY type= -213070643360900->>server:+PARSE name='', num_params=0, params_type=, query=SELECT pg_backend_pid() -213070643360900->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360900->>server:+DESCRIBE kind='P', name='' -213070643360900->>server:+EXECUTE name='', nb_rows=0 -213070643360900->>server:+SYNC -server-->>213070643360900:-PARSE COMPLETE -server-->>213070643360900:-BIND COMPLETE -server-->>213070643360900:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='pg_backend_pid' type=23 type_len=4 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360900:-DATA ROW num_values=1 ---[Value 0001]--- length=3 value='100' -server-->>213070643360900:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643360900:-READY FOR QUERY type= -213070643360900->>server:+PARSE name='', num_params=0, params_type=, query=SELECT VERSION() AS version -213070643360900->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360900->>server:+DESCRIBE kind='P', name='' -213070643360900->>server:+EXECUTE name='', nb_rows=0 -213070643360900->>server:+SYNC -server-->>213070643360900:-PARSE COMPLETE -server-->>213070643360900:-BIND COMPLETE -server-->>213070643360900:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='version' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360900:-DATA ROW num_values=1 ---[Value 0001]--- length=112 value='PostgreSQL 13.3 (Debian 13.3-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit' -server-->>213070643360900:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643360900:-READY FOR QUERY type= -213070643360900->>server:+PARSE name='', num_params=0, params_type=, query= SELECT COUNT(*) FROM pg_type AS t0, pg_aggregate AS t1, pg_settings AS t2, pg_settings AS t3 -213070643360900->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360900->>server:+DESCRIBE kind='P', name='' -213070643360900->>server:+EXECUTE name='', nb_rows=0 -213070643360900->>server:+SYNC -213070643360904->>server:+SSL REQUEST -server-->>213070643360904:-SSL BACKEND ANSWER: N -213070643360904->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360904:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='a03ee692') -213070643360904->>server:+PASSWORD MESSAGE password=md5cfedb52ae0cafa87a4f1066df3ba6802 -server-->>213070643360904:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360904:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360904:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360904:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360904:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360904:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360904:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360904:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360904:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360904:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360904:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360904:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360904:-BACKEND KEY DATA pid=101, key=2854304053 -server-->>213070643360904:-READY FOR QUERY type= -213070643360904->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360904->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360904->>server:+EXECUTE name='', nb_rows=1 -213070643360904->>server:+SYNC -server-->>213070643360904:-PARSE COMPLETE -server-->>213070643360904:-BIND COMPLETE -server-->>213070643360904:-COMMAND COMPLETE command='SET' -server-->>213070643360904:-READY FOR QUERY type= -213070643360904->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360904->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360904->>server:+EXECUTE name='', nb_rows=1 -213070643360904->>server:+SYNC -server-->>213070643360904:-PARSE COMPLETE -server-->>213070643360904:-BIND COMPLETE -server-->>213070643360904:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360904:-COMMAND COMPLETE command='SET' -server-->>213070643360904:-READY FOR QUERY type= -213070643360904->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360904->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360904->>server:+DESCRIBE kind='P', name='' -213070643360904->>server:+EXECUTE name='', nb_rows=1 -213070643360904->>server:+SYNC -server-->>213070643360904:-PARSE COMPLETE -server-->>213070643360904:-BIND COMPLETE -server-->>213070643360904:-NO DATA -server-->>213070643360904:-EMPTY QUERY RESPONSE -server-->>213070643360904:-READY FOR QUERY type= -213070643360904->>server:+DISCONNECT -213070643360908->>server:+SSL REQUEST -server-->>213070643360908:-SSL BACKEND ANSWER: N -213070643360908->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360908:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='be3afb9d') -213070643360908->>server:+PASSWORD MESSAGE password=md57b5cfa30f89fc7733c814addcd548c03 -server-->>213070643360908:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360908:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360908:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360908:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360908:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360908:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360908:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360908:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360908:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360908:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360908:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360908:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360908:-BACKEND KEY DATA pid=102, key=3089396074 -server-->>213070643360908:-READY FOR QUERY type= -213070643360908->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360908->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360908->>server:+EXECUTE name='', nb_rows=1 -213070643360908->>server:+SYNC -server-->>213070643360908:-PARSE COMPLETE -server-->>213070643360908:-BIND COMPLETE -server-->>213070643360908:-COMMAND COMPLETE command='SET' -server-->>213070643360908:-READY FOR QUERY type= -213070643360908->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360908->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360908->>server:+EXECUTE name='', nb_rows=1 -213070643360908->>server:+SYNC -server-->>213070643360908:-PARSE COMPLETE -server-->>213070643360908:-BIND COMPLETE -server-->>213070643360908:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360908:-COMMAND COMPLETE command='SET' -server-->>213070643360908:-READY FOR QUERY type= -213070643360908->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360908->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360908->>server:+DESCRIBE kind='P', name='' -213070643360908->>server:+EXECUTE name='', nb_rows=1 -213070643360908->>server:+SYNC -server-->>213070643360908:-PARSE COMPLETE -server-->>213070643360908:-BIND COMPLETE -server-->>213070643360908:-NO DATA -server-->>213070643360908:-EMPTY QUERY RESPONSE -server-->>213070643360908:-READY FOR QUERY type= -213070643360908->>server:+PARSE name='', num_params=0, params_type=, query=SELECT VERSION() AS version -213070643360908->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360908->>server:+DESCRIBE kind='P', name='' -213070643360908->>server:+EXECUTE name='', nb_rows=0 -213070643360908->>server:+SYNC -server-->>213070643360908:-PARSE COMPLETE -server-->>213070643360908:-BIND COMPLETE -server-->>213070643360908:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='version' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360908:-DATA ROW num_values=1 ---[Value 0001]--- length=112 value='PostgreSQL 13.3 (Debian 13.3-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit' -server-->>213070643360908:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643360908:-READY FOR QUERY type= -213070643360908->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360908->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360908->>server:+DESCRIBE kind='P', name='' -213070643360908->>server:+EXECUTE name='', nb_rows=1 -213070643360908->>server:+SYNC -server-->>213070643360908:-PARSE COMPLETE -server-->>213070643360908:-BIND COMPLETE -server-->>213070643360908:-NO DATA -server-->>213070643360908:-EMPTY QUERY RESPONSE -server-->>213070643360908:-READY FOR QUERY type= -213070643360908->>server:+PARSE name='', num_params=0, params_type=, query= SELECT pid as id, query as stmt, EXTRACT(seconds from query_start - NOW()) as elapsed_time FROM pg_stat_activity WHERE usename='docker' -213070643360908->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360908->>server:+DESCRIBE kind='P', name='' -213070643360908->>server:+EXECUTE name='', nb_rows=0 -213070643360908->>server:+SYNC -server-->>213070643360908:-PARSE COMPLETE -server-->>213070643360908:-BIND COMPLETE -server-->>213070643360908:-ROW DESCRIPTION: num_fields=3 ---[Field 01]--- name='id' type=23 type_len=4 type_mod=4294967295 relid=12250 attnum=3 format=0 ---[Field 02]--- name='stmt' type=25 type_len=65535 type_mod=4294967295 relid=12250 attnum=20 format=0 ---[Field 03]--- name='elapsed_time' type=701 type_len=8 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360908:-DATA ROW num_values=3 ---[Value 0001]--- length=2 value='81' ---[Value 0002]--- length=0 value='' ---[Value 0003]--- length=-1 value=NULL -server-->>213070643360908:-DATA ROW num_values=3 ---[Value 0001]--- length=3 value='100' ---[Value 0002]--- length=148 value=' SELECT COUNT(*). FROM pg_type AS t0,. pg_aggregate AS t1,. pg_settings AS t2,. pg_settings AS t3.' ---[Value 0003]--- length=9 value='-1.155449' -server-->>213070643360908:-DATA ROW num_values=3 ---[Value 0001]--- length=3 value='102' ---[Value 0002]--- length=190 value=' SELECT pid as id,. query as stmt,. EXTRACT(seconds from query_start - NOW()) as elapsed_time. FROM pg_stat_activity. WHERE usename='docker'.' ---[Value 0003]--- length=8 value='0.002903' -server-->>213070643360908:-COMMAND COMPLETE command='SELECT 3' -server-->>213070643360908:-READY FOR QUERY type= -213070643360912->>server:+SSL REQUEST -server-->>213070643360912:-SSL BACKEND ANSWER: N -213070643360912->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360912:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='14444412') -213070643360912->>server:+PASSWORD MESSAGE password=md59cef18c88b7988d7ac2f215fc1569c62 -server-->>213070643360912:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360912:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360912:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360912:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360912:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360912:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360912:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360912:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360912:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360912:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360912:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360912:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360912:-BACKEND KEY DATA pid=103, key=1911102642 -server-->>213070643360912:-READY FOR QUERY type= -213070643360912->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360912->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360912->>server:+EXECUTE name='', nb_rows=1 -213070643360912->>server:+SYNC -server-->>213070643360912:-PARSE COMPLETE -server-->>213070643360912:-BIND COMPLETE -server-->>213070643360912:-COMMAND COMPLETE command='SET' -server-->>213070643360912:-READY FOR QUERY type= -213070643360912->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360912->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360912->>server:+EXECUTE name='', nb_rows=1 -213070643360912->>server:+SYNC -server-->>213070643360912:-PARSE COMPLETE -server-->>213070643360912:-BIND COMPLETE -server-->>213070643360912:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360912:-COMMAND COMPLETE command='SET' -server-->>213070643360912:-READY FOR QUERY type= -213070643360912->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360912->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360912->>server:+DESCRIBE kind='P', name='' -213070643360912->>server:+EXECUTE name='', nb_rows=1 -213070643360912->>server:+SYNC -server-->>213070643360912:-PARSE COMPLETE -server-->>213070643360912:-BIND COMPLETE -server-->>213070643360912:-NO DATA -server-->>213070643360912:-EMPTY QUERY RESPONSE -server-->>213070643360912:-READY FOR QUERY type= -213070643360912->>server:+DISCONNECT -213070643360916->>server:+SSL REQUEST -server-->>213070643360916:-SSL BACKEND ANSWER: N -213070643360916->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360916:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='9eb7546f') -213070643360916->>server:+PASSWORD MESSAGE password=md5c7655226306ab14fa17e44dba876a348 -server-->>213070643360916:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360916:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360916:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360916:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360916:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360916:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360916:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360916:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360916:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360916:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360916:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360916:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360916:-BACKEND KEY DATA pid=104, key=927987783 -server-->>213070643360916:-READY FOR QUERY type= -213070643360916->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360916->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360916->>server:+EXECUTE name='', nb_rows=1 -213070643360916->>server:+SYNC -server-->>213070643360916:-PARSE COMPLETE -server-->>213070643360916:-BIND COMPLETE -server-->>213070643360916:-COMMAND COMPLETE command='SET' -server-->>213070643360916:-READY FOR QUERY type= -213070643360916->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360916->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360916->>server:+EXECUTE name='', nb_rows=1 -213070643360916->>server:+SYNC -server-->>213070643360916:-PARSE COMPLETE -server-->>213070643360916:-BIND COMPLETE -server-->>213070643360916:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360916:-COMMAND COMPLETE command='SET' -server-->>213070643360916:-READY FOR QUERY type= -213070643360916->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360916->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360916->>server:+DESCRIBE kind='P', name='' -213070643360916->>server:+EXECUTE name='', nb_rows=1 -213070643360916->>server:+SYNC -server-->>213070643360916:-PARSE COMPLETE -server-->>213070643360916:-BIND COMPLETE -server-->>213070643360916:-NO DATA -server-->>213070643360916:-EMPTY QUERY RESPONSE -server-->>213070643360916:-READY FOR QUERY type= -213070643360916->>server:+PARSE name='', num_params=0, params_type=, query=select pg_terminate_backend(100) -213070643360916->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360916->>server:+DESCRIBE kind='P', name='' -213070643360916->>server:+EXECUTE name='', nb_rows=0 -213070643360916->>server:+SYNC -server-->>213070643360916:-PARSE COMPLETE -server-->>213070643360916:-BIND COMPLETE -server-->>213070643360916:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='pg_terminate_backend' type=16 type_len=1 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360916:-DATA ROW num_values=1 ---[Value 0001]--- length=1 value='t' -server-->>213070643360916:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643360916:-READY FOR QUERY type= -server-->>213070643360900:-PARSE COMPLETE -server-->>213070643360900:-BIND COMPLETE -server-->>213070643360900:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='count' type=20 type_len=8 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360900:-ERROR RESPONSE File: 'postgres.c' Severity: 'FATAL' Message: 'terminating connection due to administrator command' Code: '57P01' Routine: 'ProcessInterrupts' Line: '3090' -213070643360908->>server:+DISCONNECT -213070643360916->>server:+DISCONNECT -213070643360920->>server:+SSL REQUEST -server-->>213070643360920:-SSL BACKEND ANSWER: N -213070643360920->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360920:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='1ad4dcf7') -213070643360920->>server:+PASSWORD MESSAGE password=md554de511c5219f67a8bbe92da445fa5a2 -server-->>213070643360920:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360920:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360920:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360920:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360920:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360920:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360920:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360920:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360920:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360920:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360920:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360920:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360920:-BACKEND KEY DATA pid=105, key=3630332440 -server-->>213070643360920:-READY FOR QUERY type= -213070643360920->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360920->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360920->>server:+EXECUTE name='', nb_rows=1 -213070643360920->>server:+SYNC -server-->>213070643360920:-PARSE COMPLETE -server-->>213070643360920:-BIND COMPLETE -server-->>213070643360920:-COMMAND COMPLETE command='SET' -server-->>213070643360920:-READY FOR QUERY type= -213070643360920->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360920->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360920->>server:+EXECUTE name='', nb_rows=1 -213070643360920->>server:+SYNC -server-->>213070643360920:-PARSE COMPLETE -server-->>213070643360920:-BIND COMPLETE -server-->>213070643360920:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360920:-COMMAND COMPLETE command='SET' -server-->>213070643360920:-READY FOR QUERY type= -213070643360920->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360920->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360920->>server:+DESCRIBE kind='P', name='' -213070643360920->>server:+EXECUTE name='', nb_rows=1 -213070643360920->>server:+SYNC -server-->>213070643360920:-PARSE COMPLETE -server-->>213070643360920:-BIND COMPLETE -server-->>213070643360920:-NO DATA -server-->>213070643360920:-EMPTY QUERY RESPONSE -server-->>213070643360920:-READY FOR QUERY type= -213070643360920->>server:+DISCONNECT -213070643360924->>server:+SSL REQUEST -server-->>213070643360924:-SSL BACKEND ANSWER: N -213070643360924->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360924:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='494c352f') -213070643360924->>server:+PASSWORD MESSAGE password=md5919fc9ed056904fa0d8e1dd00625a556 -server-->>213070643360924:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360924:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360924:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360924:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360924:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360924:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360924:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360924:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360924:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360924:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360924:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360924:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360924:-BACKEND KEY DATA pid=106, key=3364308023 -server-->>213070643360924:-READY FOR QUERY type= -213070643360924->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360924->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360924->>server:+EXECUTE name='', nb_rows=1 -213070643360924->>server:+SYNC -server-->>213070643360924:-PARSE COMPLETE -server-->>213070643360924:-BIND COMPLETE -server-->>213070643360924:-COMMAND COMPLETE command='SET' -server-->>213070643360924:-READY FOR QUERY type= -213070643360924->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360924->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360924->>server:+EXECUTE name='', nb_rows=1 -213070643360924->>server:+SYNC -server-->>213070643360924:-PARSE COMPLETE -server-->>213070643360924:-BIND COMPLETE -server-->>213070643360924:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360924:-COMMAND COMPLETE command='SET' -server-->>213070643360924:-READY FOR QUERY type= -213070643360924->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360924->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360924->>server:+DESCRIBE kind='P', name='' -213070643360924->>server:+EXECUTE name='', nb_rows=1 -213070643360924->>server:+SYNC -server-->>213070643360924:-PARSE COMPLETE -server-->>213070643360924:-BIND COMPLETE -server-->>213070643360924:-NO DATA -server-->>213070643360924:-EMPTY QUERY RESPONSE -server-->>213070643360924:-READY FOR QUERY type= -213070643360924->>server:+PARSE name='', num_params=0, params_type=, query=SELECT 1 -213070643360924->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360924->>server:+DESCRIBE kind='P', name='' -213070643360924->>server:+EXECUTE name='', nb_rows=0 -213070643360924->>server:+SYNC -server-->>213070643360924:-PARSE COMPLETE -server-->>213070643360924:-BIND COMPLETE -server-->>213070643360924:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='?column?' type=23 type_len=4 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360924:-DATA ROW num_values=1 ---[Value 0001]--- length=1 value='1' -server-->>213070643360924:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643360924:-READY FOR QUERY type= -213070643360924->>server:+DISCONNECT -213070643360928->>server:+SSL REQUEST -server-->>213070643360928:-SSL BACKEND ANSWER: N -213070643360928->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360928:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='4b2173db') -213070643360928->>server:+PASSWORD MESSAGE password=md53977c39c5c7cdef7f80e74b256a8ce25 -server-->>213070643360928:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360928:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360928:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360928:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360928:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360928:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360928:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360928:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360928:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360928:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360928:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360928:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360928:-BACKEND KEY DATA pid=107, key=1988416582 -server-->>213070643360928:-READY FOR QUERY type= -213070643360928->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360928->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360928->>server:+EXECUTE name='', nb_rows=1 -213070643360928->>server:+SYNC -server-->>213070643360928:-PARSE COMPLETE -server-->>213070643360928:-BIND COMPLETE -server-->>213070643360928:-COMMAND COMPLETE command='SET' -server-->>213070643360928:-READY FOR QUERY type= -213070643360928->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360928->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360928->>server:+EXECUTE name='', nb_rows=1 -213070643360928->>server:+SYNC -server-->>213070643360928:-PARSE COMPLETE -server-->>213070643360928:-BIND COMPLETE -server-->>213070643360928:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360928:-COMMAND COMPLETE command='SET' -server-->>213070643360928:-READY FOR QUERY type= -213070643360928->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360928->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360928->>server:+DESCRIBE kind='P', name='' -213070643360928->>server:+EXECUTE name='', nb_rows=1 -213070643360928->>server:+SYNC -server-->>213070643360928:-PARSE COMPLETE -server-->>213070643360928:-BIND COMPLETE -server-->>213070643360928:-NO DATA -server-->>213070643360928:-EMPTY QUERY RESPONSE -server-->>213070643360928:-READY FOR QUERY type= -213070643360928->>server:+DISCONNECT -213070643360932->>server:+SSL REQUEST -server-->>213070643360932:-SSL BACKEND ANSWER: N -213070643360932->>server:+STARTUP MESSAGE version: 3 database=trait_store extra_float_digits=2 TimeZone=GMT client_encoding=UTF8 user=docker DateStyle=ISO -server-->>213070643360932:-AUTHENTIFICATION REQUEST code=5 (MD5 salt='f6d0f51d') -213070643360932->>server:+PASSWORD MESSAGE password=md5b5b322fe9bcc7c242c6b44cc8a7898e8 -server-->>213070643360932:-AUTHENTIFICATION REQUEST code=0 (SUCCESS) -server-->>213070643360932:-PARAMETER STATUS name='application_name', value='' -server-->>213070643360932:-PARAMETER STATUS name='client_encoding', value='UTF8' -server-->>213070643360932:-PARAMETER STATUS name='DateStyle', value='ISO, MDY' -server-->>213070643360932:-PARAMETER STATUS name='integer_datetimes', value='on' -server-->>213070643360932:-PARAMETER STATUS name='IntervalStyle', value='postgres' -server-->>213070643360932:-PARAMETER STATUS name='is_superuser', value='on' -server-->>213070643360932:-PARAMETER STATUS name='server_encoding', value='UTF8' -server-->>213070643360932:-PARAMETER STATUS name='server_version', value='13.3 (Debian 13.3-1.pgdg100+1)' -server-->>213070643360932:-PARAMETER STATUS name='session_authorization', value='docker' -server-->>213070643360932:-PARAMETER STATUS name='standard_conforming_strings', value='on' -server-->>213070643360932:-PARAMETER STATUS name='TimeZone', value='GMT' -server-->>213070643360932:-BACKEND KEY DATA pid=108, key=997991029 -server-->>213070643360932:-READY FOR QUERY type= -213070643360932->>server:+PARSE name='', num_params=0, params_type=, query=SET extra_float_digits = 3 -213070643360932->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360932->>server:+EXECUTE name='', nb_rows=1 -213070643360932->>server:+SYNC -server-->>213070643360932:-PARSE COMPLETE -server-->>213070643360932:-BIND COMPLETE -server-->>213070643360932:-COMMAND COMPLETE command='SET' -server-->>213070643360932:-READY FOR QUERY type= -213070643360932->>server:+PARSE name='', num_params=0, params_type=, query=SET application_name = 'PostgreSQL JDBC Driver' -213070643360932->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360932->>server:+EXECUTE name='', nb_rows=1 -213070643360932->>server:+SYNC -server-->>213070643360932:-PARSE COMPLETE -server-->>213070643360932:-BIND COMPLETE -server-->>213070643360932:-PARAMETER STATUS name='application_name', value='PostgreSQL JDBC Driver' -server-->>213070643360932:-COMMAND COMPLETE command='SET' -server-->>213070643360932:-READY FOR QUERY type= -213070643360932->>server:+PARSE name='', num_params=0, params_type=, query= -213070643360932->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360932->>server:+DESCRIBE kind='P', name='' -213070643360932->>server:+EXECUTE name='', nb_rows=1 -213070643360932->>server:+SYNC -server-->>213070643360932:-PARSE COMPLETE -server-->>213070643360932:-BIND COMPLETE -server-->>213070643360932:-NO DATA -server-->>213070643360932:-EMPTY QUERY RESPONSE -server-->>213070643360932:-READY FOR QUERY type= -213070643360932->>server:+PARSE name='', num_params=0, params_type=, query=SELECT VERSION() AS version -213070643360932->>server:+BIND portal='', name='', num_formats=0, formats=, num_params=0, params= -213070643360932->>server:+DESCRIBE kind='P', name='' -213070643360932->>server:+EXECUTE name='', nb_rows=0 -213070643360932->>server:+SYNC -server-->>213070643360932:-PARSE COMPLETE -server-->>213070643360932:-BIND COMPLETE -server-->>213070643360932:-ROW DESCRIPTION: num_fields=1 ---[Field 01]--- name='version' type=25 type_len=65535 type_mod=4294967295 relid=0 attnum=0 format=0 -server-->>213070643360932:-DATA ROW num_values=1 ---[Value 0001]--- length=112 value='PostgreSQL 13.3 (Debian 13.3-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit' -server-->>213070643360932:-COMMAND COMPLETE command='SELECT 1' -server-->>213070643360932:-READY FOR QUERY type= -213070643360932->>server:+DISCONNECT -``` diff --git a/pg/message/io.go b/pg/message/io.go deleted file mode 100644 index 42de8343c..000000000 --- a/pg/message/io.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package message - -import ( - "bufio" - "encoding/binary" - "errors" - "io" -) - -// Reader reads messages. -type Reader interface { - ReadMessage() (Message, error) -} - -// Writer writes messages. -type Writer interface { - WriteMessage(Message) error - Flush() error -} - -// WireReader reads messages in Postgres wire protocol format. -type WireReader struct { - buf []byte - r *bufio.Reader - scratch [4]byte -} - -// ReadMessage reads a single message off of the wire. -// The returned message is only valid until the next read call, as the data buffer may be re-used. -func (r *WireReader) ReadMessage() (Message, error) { - t, err := r.r.ReadByte() - if err != nil { - return Message{}, err - } - - _, err = r.r.Read(r.scratch[:]) - if err != nil { - return Message{}, err - } - - len := binary.BigEndian.Uint32(r.scratch[:4]) - if len < 4 { - return Message{}, errors.New("invalid message length") - } - len -= 4 - - if cap(r.buf) < int(len) { - r.buf = make([]byte, len) - } else { - r.buf = r.buf[:len] - } - _, err = io.ReadFull(r.r, r.buf) - if err != nil { - return Message{}, err - } - - return Message{ - Type: Type(t), - Data: r.buf, - }, nil -} - -var _ Reader = (*WireReader)(nil) - -// NewWireReader returns a message reader that reads postgres wire protocol format. -func NewWireReader(r *bufio.Reader) *WireReader { - return &WireReader{r: r} -} - -// ErrMessageTooBig is an error indicating that a message is too big to be sent or received. -var ErrMessageTooBig = errors.New("message is too big") - -// WireWriter writes messages in Postgres wire protocol. -type WireWriter struct { - w *bufio.Writer - scratch [4]byte -} - -// WriteMessage writes a message onto the wire. -func (w *WireWriter) WriteMessage(message Message) error { - if uint(len(message.Data))+4 >= 1<<31 { - return ErrMessageTooBig - } - - err := w.w.WriteByte(byte(message.Type)) - if err != nil { - return err - } - - binary.BigEndian.PutUint32(w.scratch[:], uint32(len(message.Data))+4) - _, err = w.w.Write(w.scratch[:]) - if err != nil { - return err - } - - _, err = w.w.Write(message.Data) - return err -} - -// Flush writes any buffered data to the underlying stream. -func (w *WireWriter) Flush() error { - return w.w.Flush() -} - -var _ Writer = (*WireWriter)(nil) - -// NewWireWriter returns a message writer that writes in postgres wire protocol format. -func NewWireWriter(w *bufio.Writer) *WireWriter { - return &WireWriter{w: w} -} diff --git a/pg/message/message.go b/pg/message/message.go deleted file mode 100644 index 1eb0817b0..000000000 --- a/pg/message/message.go +++ /dev/null @@ -1,532 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package message - -import ( - "bytes" - "encoding/binary" - "fmt" -) - -// Type is a byte indicating the type of a Postgres message. -type Type byte - -const ( - // TypeAuthentication is a message used to transfer authentication info. - TypeAuthentication Type = 'R' - - // TypeReadyForQuery is a message used to indicate that the server is ready for another query. - TypeReadyForQuery Type = 'Z' - - // TypeCommandComplete is a Backend message used to indicate that a query has completed. - TypeCommandComplete Type = 'C' - - // TypeClos is a message used to indicate that a query has completed. Frontend - TypeClose Type = 'C' - - // TypeRowDescription is a message indicating the column types of the result rows from a query. - TypeRowDescription Type = 'T' - - // TypeDataRow is a message with the contents of a single row. - TypeDataRow Type = 'D' - - // TypeTermination is a message indicating a request to terminate a connection. - TypeTermination Type = 'X' - - // TypeNegotiateProtocolVersion is a message used when a client attempts to connect with a newer minor version than the server supports. - TypeNegotiateProtocolVersion Type = 'v' - - // TypeSimpleQuery is a simple query request. - TypeSimpleQuery Type = 'Q' - - // TypeBackendKeyData contains a cancellation key for the client to use later. - TypeBackendKeyData Type = 'K' - - TypeParse Type = 'P' - TypeParseComplete Type = '1' - - TypeBind Type = 'B' - TypeBindComplete Type = '2' - - TypeExecute Type = 'E' // Frontend - TypeError Type = 'E' // Backend - TypeSync Type = 'S' // Frontend - TypeParameterStatus Type = 'S' // Backend - TypeDescribe Type = 'D' // Frontend - TypeNoData Type = 'n' // backend - TypeEmptyQueryResponse Type = 'I' // backend -) - -// AuthenticationOK is a message indicating that authentication has completed. -var AuthenticationOK = Message{ - Type: TypeAuthentication, - Data: []byte{0, 0, 0, 0}, -} -var ParseOK = Message{ - Type: TypeParseComplete, - Data: []byte{}, -} -var BindComplete = Message{ - Type: TypeBindComplete, - Data: []byte{}, -} -var NoData = Message{ - Type: TypeNoData, - Data: []byte{}, -} -var EmptyQueryResponse = Message{ - Type: TypeEmptyQueryResponse, - Data: []byte{}, -} - -// Message is a Postgres message value. -type Message struct { - Type Type - Data []byte -} - -//debug tools -func viewString(b []byte) string { - r := []rune(string(b)) - for i := range r { - if r[i] < 32 || r[i] > 126 { - r[i] = '.' - } - } - return string(r) -} -func min(a, b int) int { - if a < b { - return a - } - return b -} -func (m *Message) Dump(prefix string) { - n := len(m.Data) - rowcount := 0 - stop := (n / 8) * 8 - k := 0 - fmt.Printf("\n %s type: '%c'\n", prefix, m.Type) - for i := 0; i <= stop; i += 8 { - k++ - if i+8 < n { - rowcount = 8 - } else { - rowcount = min(k*8, n) % 8 - } - - fmt.Printf("pos %02d hex: ", i) - for j := 0; j < rowcount; j++ { - fmt.Printf("%02x ", m.Data[i+j]) - } - for j := rowcount; j < 8; j++ { - fmt.Printf(" ") - } - fmt.Printf(" '%s'\n", viewString(m.Data[i:(i+rowcount)])) - } -} - -//endTools - -// TransactionStatus is the current transaction state. -type TransactionStatus byte - -const ( - // TransactionStatusIdle indicates that there is no active transaction. - TransactionStatusIdle TransactionStatus = 'I' - - // TransactionStatusActive indicates that the connection currently has an active transaction. - TransactionStatusActive TransactionStatus = 'T' - - // TransactionStatusFailed indicates that the connection currently has a failed transaction. - TransactionStatusFailed TransactionStatus = 'E' -) - -// Encoder encodes messages. -type Encoder struct { - buf bytes.Buffer - scratch [4]byte -} - -func (e *Encoder) i16(i int16) error { - binary.BigEndian.PutUint16(e.scratch[:2], uint16(i)) - _, err := e.buf.Write(e.scratch[:2]) - return err -} - -func (e *Encoder) i32(i int32) error { - binary.BigEndian.PutUint32(e.scratch[:], uint32(i)) - _, err := e.buf.Write(e.scratch[:]) - return err -} - -/* removed for linter now -func (e *Encoder) u32(i uint32) error { - binary.BigEndian.PutUint32(e.scratch[:], i) - _, err := e.buf.Write(e.scratch[:]) - return err -} -*/ - -// ReadyForQuery encodes a "ready for query" message. -func (e *Encoder) ReadyForQuery(status TransactionStatus) (Message, error) { - e.buf.Reset() - - err := e.buf.WriteByte(byte(status)) - if err != nil { - return Message{}, err - } - - return Message{ - Type: TypeReadyForQuery, - Data: e.buf.Bytes(), - }, nil -} - -// CommandComplete encodes a command completion message. -func (e *Encoder) CommandComplete(tag string) (Message, error) { - e.buf.Reset() - - _, err := e.buf.WriteString(tag) - if err != nil { - return Message{}, err - } - - err = e.buf.WriteByte(0) - if err != nil { - return Message{}, err - } - - return Message{ - Type: TypeCommandComplete, - Data: e.buf.Bytes(), - }, nil -} - -// NoticeFieldType indicates the type of a notice/error field. -// https://www.postgresql.org/docs/9.3/protocol-error-fields.html -type NoticeFieldType byte - -const ( - // NoticeFieldSeverity indicates the severity of a notice/error. - NoticeFieldSeverity NoticeFieldType = 'S' - - // NoticeFieldMessage is a short human-readable error/notice message. - NoticeFieldMessage NoticeFieldType = 'M' - - // NoticeFieldDetail is an optional extended description of the error. - NoticeFieldDetail NoticeFieldType = 'D' - - // NoticeFieldHint is a suggestion of how to address the issue. - NoticeFieldHint NoticeFieldType = 'H' - - // NoticeFieldHint the SQLSTATE code for the error (see Appendix A). Not localizable. Always present. - NoticeFieldCode NoticeFieldType = 'C' -) - -// NoticeField is a field in an error or notice. -type NoticeField struct { - Type NoticeFieldType - Data string -} - -func (e *Encoder) messageOrNotice(fields ...NoticeField) error { - for _, f := range fields { - err := e.buf.WriteByte(byte(f.Type)) - if err != nil { - return err - } - - _, err = e.buf.WriteString(f.Data) - if err != nil { - return err - } - - err = e.buf.WriteByte(0) - if err != nil { - return err - } - } - - return e.buf.WriteByte(0) -} - -// Error encodes a Postgres error message. -func (e *Encoder) Error(fields ...NoticeField) (Message, error) { - e.buf.Reset() - err := e.messageOrNotice(fields...) - if err != nil { - return Message{}, err - } - return Message{ - Type: TypeError, - Data: e.buf.Bytes(), - }, nil -} - -// GoError creates a simple Postgres error message from a Go error value. -func (e *Encoder) GoError(err error) (Message, error) { - return e.Error( - NoticeField{ - Type: NoticeFieldSeverity, - Data: "ERROR", - }, - NoticeField{ - Type: NoticeFieldMessage, - Data: err.Error(), - }, - ) -} - -// ColumnDescription is a description of a data column. -type ColumnDescription struct { - Name string - TableID int32 //either a table/col id or 0 - FieldID int16 //either a table/col id or 0 - TypeID int32 //field type - TypeLen int16 //size in bytes of field - TypeModifier int32 //type modifer? - Mode int16 //0=text 1=binary -} - -// RowDescription describes the response rows from a query. -func (e *Encoder) RowDescription(cols ...ColumnDescription) (Message, error) { - if len(cols) >= 1<<15 { - return Message{}, ErrMessageTooBig - } - - e.buf.Reset() - - err := e.i16(int16(len(cols))) - if err != nil { - return Message{}, nil - } - - for _, col := range cols { - _, err := e.buf.WriteString(col.Name) - if err != nil { - return Message{}, err - } - err = e.buf.WriteByte(0) - if err != nil { - return Message{}, err - } - - err = e.i32(col.TableID) - if err != nil { - return Message{}, err - } - - err = e.i16(col.FieldID) - if err != nil { - return Message{}, err - } - - err = e.i32(col.TypeID) - if err != nil { - return Message{}, err - } - - err = e.i16(col.TypeLen) - if err != nil { - return Message{}, err - } - - err = e.i32(col.TypeModifier) - if err != nil { - return Message{}, err - } - - err = e.i16(col.Mode) - if err != nil { - return Message{}, err - } - } - - return Message{ - Type: TypeRowDescription, - Data: e.buf.Bytes(), - }, nil -} - -// TextRow encodes a data row in textual format. -func (e *Encoder) TextRow(row ...string) (Message, error) { - if len(row) >= 1<<15 { - return Message{}, ErrMessageTooBig - } - - e.buf.Reset() - - err := e.i16(int16(len(row))) - if err != nil { - return Message{}, err - } - - for _, val := range row { - if uint(len(val)) >= 1<<31 { - return Message{}, ErrMessageTooBig - } - - err = e.i32(int32(len(val))) - if err != nil { - return Message{}, err - } - - _, err = e.buf.WriteString(val) - if err != nil { - return Message{}, err - } - } - - return Message{ - Type: TypeDataRow, - Data: e.buf.Bytes(), - }, nil -} - -// NegotiateProtocolVersion encodes a protocol negotiation packet. -func (e *Encoder) NegotiateProtocolVersion(maxMinor int32, unrecognizedOptions ...string) (Message, error) { - if uint64(len(unrecognizedOptions)) >= 1<<31 { - return Message{}, ErrMessageTooBig - } - - e.buf.Reset() - - err := e.i32(maxMinor) - if err != nil { - return Message{}, err - } - - err = e.i32(int32(len(unrecognizedOptions))) - if err != nil { - return Message{}, err - } - for _, opt := range unrecognizedOptions { - _, err = e.buf.WriteString(opt) - if err != nil { - return Message{}, err - } - - err = e.buf.WriteByte(0) - if err != nil { - return Message{}, err - } - } - - return Message{ - Type: TypeNegotiateProtocolVersion, - Data: e.buf.Bytes(), - }, nil -} - -// BackendKeyData encodes a Message with a cancellation key. -func (e *Encoder) BackendKeyData(pid, key int32) (Message, error) { - e.buf.Reset() - - err := e.i32(pid) - if err != nil { - return Message{}, err - } - - err = e.i32(key) - if err != nil { - return Message{}, err - } - - return Message{ - Type: TypeBackendKeyData, - Data: e.buf.Bytes(), - }, nil -} - -func (e *Encoder) ParameterStatus(param, value string) (Message, error) { - e.buf.Reset() - //param + NULL + value+ NULL - _, err := e.buf.WriteString(param) - if err != nil { - return Message{}, err - } - err = e.buf.WriteByte(0) - if err != nil { - return Message{}, err - } - _, err = e.buf.WriteString(value) - if err != nil { - return Message{}, err - } - err = e.buf.WriteByte(0) - if err != nil { - return Message{}, err - } - return Message{ - Type: TypeParameterStatus, - Data: e.buf.Bytes(), - }, nil -} - -type SimpleColumn struct { - Name string - Typeid int32 - Typelen int16 -} - -func (e *Encoder) EncodeColumn(name string, typeid int32, typelen int16) (Message, error) { - return e.EncodeColumns(SimpleColumn{ - Name: name, - Typeid: typeid, - Typelen: typelen, - }) -} -func (e *Encoder) EncodeColumns(cols ...SimpleColumn) (Message, error) { - - e.buf.Reset() - err := e.i16(int16(len(cols))) // number of columns in result - if err != nil { - return Message{}, nil - } - for _, col := range cols { - - _, err = e.buf.WriteString(col.Name) // column name - if err != nil { - return Message{}, err - } - err = e.buf.WriteByte(0) //null terminate - if err != nil { - return Message{}, err - } - - err = e.i32(0) //tabel id - if err != nil { - return Message{}, err - } - - err = e.i16(0) //field id(attnum) - if err != nil { - return Message{}, err - } - - err = e.i32(col.Typeid) //type_id - if err != nil { - return Message{}, err - } - - err = e.i16(col.Typelen) //type_len - if err != nil { - return Message{}, err - } - - err = e.i32(-1) //type_mod - if err != nil { - return Message{}, err - } - - err = e.i16(0) //format 0 text 1 binary - if err != nil { - return Message{}, err - } - } - return Message{ - Type: TypeRowDescription, - Data: e.buf.Bytes(), - }, nil -} diff --git a/pg/pgtest/handler.go b/pg/pgtest/handler.go deleted file mode 100644 index 9a35c0fdd..000000000 --- a/pg/pgtest/handler.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pgtest - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/featurebasedb/featurebase/v3/pg" -) - -// HandlerFunc implements a postgres query handler with a function. -type HandlerFunc func(context.Context, pg.QueryResultWriter, pg.Query) error - -// HandleQuery calls the user's query handler function. -func (h HandlerFunc) HandleQuery(ctx context.Context, w pg.QueryResultWriter, q pg.Query) error { - return h(ctx, w, q) -} -func (h HandlerFunc) HandleSchema(ctx context.Context, portal *pg.Portal) error { - return nil -} -func (h HandlerFunc) Version() string { - return "testv1" -} - -var _ pg.QueryHandler = HandlerFunc(nil) - -// ResultSet is a QueryResultWriter that accumulates results in a slice. -type ResultSet struct { - Columns []pg.ColumnInfo - Data [][]string - ResultTag string -} - -func (s ResultSet) String() string { - if len(s.Columns) == 0 || len(s.Data) == 0 { - return "EMPTY" - } - colHdr := make([]string, len(s.Columns)) - for i, c := range s.Columns { - colHdr[i] = fmt.Sprintf("%s:%v", c.Name, c.Type) - } - dataBody := make([][]string, len(s.Data)) - for i, v := range s.Data { - dataBody[i] = append([]string(nil), v...) - } - colWidth := make([]int, len(s.Columns)) - for i, c := range colHdr { - colWidth[i] = len(c) - } - for _, row := range dataBody { - for i, c := range row { - if len(c) > colWidth[i] { - colWidth[i] = len(c) - } - } - } - for i, c := range colHdr { - c += strings.Repeat(" ", colWidth[i]-len(c)) - colHdr[i] = c - } - for _, row := range dataBody { - for i, c := range row { - c += strings.Repeat(" ", colWidth[i]-len(c)) - row[i] = c - } - } - var totalWidth int - for _, width := range colWidth { - totalWidth += width - } - data := make([]string, len(dataBody)) - for i, row := range dataBody { - data[i] = strings.Join(row, "|") - } - return strings.Join(colHdr, "|") + "\n" + strings.Repeat("-", totalWidth+(2*len(colHdr)-1)) + "\n" + strings.Join(data, "\n") -} - -// WriteHeader writes headers to the result set. -func (rs *ResultSet) WriteHeader(cols ...pg.ColumnInfo) error { - if rs.Columns != nil { - return errors.New("double-write of headers") - } - - colsCopy := make([]pg.ColumnInfo, len(cols)) - copy(colsCopy, cols) - rs.Columns = colsCopy - - return nil -} - -// WriteRowText writes a row to the result set. -func (rs *ResultSet) WriteRowText(vals ...string) error { - if rs.Columns == nil { - return errors.New("wrote a row without headers") - } - - row := make([]string, len(vals)) - copy(row, vals) - - rs.Data = append(rs.Data, row) - - return nil -} - -// Tag applies a tag to the result set. -func (rs *ResultSet) Tag(tag string) { - rs.ResultTag = tag -} - -var _ pg.QueryResultWriter = (*ResultSet)(nil) diff --git a/pg/pgtest/memnet.go b/pg/pgtest/memnet.go deleted file mode 100644 index 7cada5882..000000000 --- a/pg/pgtest/memnet.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pgtest - -import ( - "errors" - "net" - "sync" -) - -// errListenerClosed is an error returned when the listener is closed. -var errListenerClosed = errors.New("listener closed") - -type inMemoryListener struct { - ch chan net.Conn - closed chan struct{} - once sync.Once -} - -func (l *inMemoryListener) Accept() (net.Conn, error) { - select { - case <-l.closed: - return nil, errListenerClosed - default: - } - select { - case conn := <-l.ch: - return conn, nil - case <-l.closed: - return nil, errListenerClosed - } -} - -func (l *inMemoryListener) Close() error { - l.once.Do(func() { close(l.closed) }) - - return nil -} - -type memAddr struct{} - -func (a memAddr) Network() string { return "memory" } -func (a memAddr) String() string { return "memory" } - -func (l *inMemoryListener) Addr() net.Addr { - return memAddr{} -} - -func (l *inMemoryListener) Dial() (net.Conn, error) { - serverConn, clientConn := net.Pipe() - select { - case l.ch <- serverConn: - return clientConn, nil - case <-l.closed: - serverConn.Close() - clientConn.Close() - return nil, errListenerClosed - } -} diff --git a/pg/pgtest/server.go b/pg/pgtest/server.go deleted file mode 100644 index ed59ac037..000000000 --- a/pg/pgtest/server.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pgtest - -import ( - "context" - "fmt" - "net" - "testing" - - "github.com/featurebasedb/featurebase/v3/pg" - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" -) - -// ShutdownFunc is a function to use to shut down a test fixture. -// This function will send a shutdown signal and then wait for completion. -type ShutdownFunc func() error - -// Finish invokes the shutdown function and fails the test if an error occurs. -func (f ShutdownFunc) Finish(tb testing.TB, name string) { - err := f() - if err != nil { - tb.Errorf("failed to shut down %s: %v", name, err) - } -} - -// ServeListener serves postgres wire protocol on a listener. -func ServeListener(listener net.Listener, server *pg.Server) (net.Addr, ShutdownFunc, error) { - laddr := listener.Addr() - - ctx, cancel := context.WithCancel(context.Background()) - var eg errgroup.Group - eg.Go(func() error { return server.Serve(ctx, listener) }) - - return laddr, - func() error { - cancel() - return eg.Wait() - }, - nil -} - -// ServeTCP creates a TCP listener and serves postgres wire protocol on it. -func ServeTCP(addr string, server *pg.Server) (net.Addr, ShutdownFunc, error) { - listener, err := net.Listen("tcp", addr) - if err != nil { - return nil, nil, errors.Wrap(err, "listening on TCP") - } - return ServeListener(listener, server) -} - -// ServeTLSListener sets up TLS on the server and invokes ServeListener. -func ServeTLSListener(listener net.Listener, server *pg.Server) (net.Addr, ShutdownFunc, error) { - err := SetupTLS(server) - if err != nil { - return nil, nil, errors.Wrap(err, "server TLS setup failed") - } - - var tries int = 5 - var netAddr net.Addr - var shutdown ShutdownFunc - - for i := 0; i < tries; i++ { - if i > 0 { - fmt.Printf("--- try serving TLS again: %d\n", i) - } - if netAddr, shutdown, err = ServeListener(listener, server); err == nil { - break - } - } - return netAddr, shutdown, err -} - -// ServeTLS sets up TLS on the server and invokes ServeTCP. -func ServeTLS(addr string, server *pg.Server) (net.Addr, ShutdownFunc, error) { - err := SetupTLS(server) - if err != nil { - return nil, nil, errors.Wrap(err, "server TLS setup failed") - } - - var tries int = 5 - var netAddr net.Addr - var shutdown ShutdownFunc - - for i := 0; i < tries; i++ { - if i > 0 { - fmt.Printf("--- try serving TLS again: %d\n", i) - } - if netAddr, shutdown, err = ServeTCP(addr, server); err == nil { - break - } - } - return netAddr, shutdown, err -} - -// ConnectFunc is a function to connect to a server. -type ConnectFunc func() (net.Conn, error) - -// ServeMem serves postgres on in-memory connections. -// TLS does not work here, as it relies on the OS to buffer and discard data. -func ServeMem(server *pg.Server) (ConnectFunc, ShutdownFunc, error) { - listener := &inMemoryListener{ - ch: make(chan net.Conn), - closed: make(chan struct{}), - } - - ctx, cancel := context.WithCancel(context.Background()) - var eg errgroup.Group - eg.Go(func() error { return server.Serve(ctx, listener) }) - - return listener.Dial, - func() error { - cancel() - return eg.Wait() - }, - nil -} diff --git a/pg/pgtest/tls.go b/pg/pgtest/tls.go deleted file mode 100644 index 848843292..000000000 --- a/pg/pgtest/tls.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pgtest - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "time" - - "github.com/featurebasedb/featurebase/v3/pg" - "github.com/pkg/errors" -) - -// SetupTLS generates a TLS certificate and installs it into the server. -// TODO: have the client properly trust this (generate a CA to install instead of using self-signed). -func SetupTLS(server *pg.Server) error { - // Generate an ecdsa key for the cert. - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return errors.Wrap(err, "generating TLS key") - } - - // Generate a random 128-bit serial number. - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - return errors.Wrap(err, "generating serial number") - } - - // Make the certificate valid starting now. - now := time.Now() - - // Create a certificate template. - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"Molecula"}, - }, - NotBefore: now, - NotAfter: now.Add(time.Hour), - - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - // Generate a self-signed x509 cert from the template and the key. - certData, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) - if err != nil { - return errors.Wrap(err, "encoding certificate x509") - } - - // Encode the cert to PEM so that the TLS package can load it. - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certData, - }) - - // Encode the private key to x509. - keyData, err := x509.MarshalPKCS8PrivateKey(key) - if err != nil { - return errors.Wrap(err, "encoding key x509") - } - - // Encode the key to PEM so that the TLS package can load it. - keyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: keyData, - }) - - // Load the certificate and key from their PEM encodings. - cert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - return errors.Wrap(err, "loading TLS key pair") - } - - // Install the certificate into the server. - if server.TLSConfig == nil { - server.TLSConfig = &tls.Config{} - } - server.TLSConfig.Certificates = append(server.TLSConfig.Certificates, cert) - - return nil -} diff --git a/pg/protocol.go b/pg/protocol.go deleted file mode 100644 index fffd01760..000000000 --- a/pg/protocol.go +++ /dev/null @@ -1,1101 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import ( - "bufio" - "bytes" - "context" - "crypto/tls" - "encoding/binary" - "encoding/hex" - "fmt" - "io" - "net" - "regexp" - "strings" - "sync" - "time" - - "github.com/featurebasedb/featurebase/v3/pg/message" - "github.com/featurebasedb/featurebase/v3/sql" - "github.com/pkg/errors" - "vitess.io/vitess/go/vt/sqlparser" -) - -// Protocol is a Postgres protocol version. -type Protocol uint32 - -const ( - // ProtocolPostgres30 is version 3.0 of the Postgres wire protocol. - ProtocolPostgres30 Protocol = (3 << 16) - - // ProtocolCancel is the protocol used for query cancellation. - ProtocolCancel Protocol = (1234 << 16) | 5678 - - // ProtocolSSL is the protocol used for SSL upgrades. - ProtocolSSL Protocol = (1234 << 16) | 5679 - - // ProtocolSupported is the main protocol version supported by this package. - ProtocolSupported Protocol = ProtocolPostgres30 - - // PgServerVersion is the latest version of postgres that we claim to support. - PgServerVersion = "13.0.0" -) - -// Major returns the major revision of the protocol. -func (p Protocol) Major() uint16 { - return uint16(p >> 16) -} - -// Minor returns the minor revision of the protocol. -func (p Protocol) Minor() uint16 { - return uint16(p) -} - -func (p Protocol) String() string { - switch p { - case ProtocolCancel: - return "cancel" - case ProtocolSSL: - return "SSL" - } - - return fmt.Sprintf("v%d.%d", p.Major(), p.Minor()) -} - -// handle reads the startup packet and dispatches an appropriate protocol handler for the connection. -func (s *Server) handle(ctx context.Context, conn net.Conn) (err error) { - var hasTLS bool - - defer func() { - cerr := conn.Close() - if cerr != nil && err == nil { - if hasTLS { - if nerr, ok := cerr.(net.Error); ok && nerr.Timeout() { - // TLS does this sometimes. - return - } - } - err = errors.Wrap(cerr, "closing connection") - } - }() - - if tcpconn, ok := conn.(*net.TCPConn); ok { - // Postgres does not have any real mechanism for confirming that a connection is still alive. - // Without this, a connection that breaks while idle would live indefinitely. - // With a TCP keepalive, this should return an error after approximately 2 hours (depending on OS configuration). - err := tcpconn.SetKeepAlive(true) - if err != nil { - return errors.Wrap(err, "enabling TCP keepalive") - } - } - - var startupDeadline time.Time - if s.StartupTimeout > 0 { - // Set deadline for processing the startup. - startupDeadline = time.Now().Add(s.StartupTimeout) - err = conn.SetDeadline(startupDeadline) - if err != nil { - return errors.Wrap(err, "setting deadline on protocol startup") - } - } - -startup: - // Read startup packet. - var buf [4]byte - _, err = io.ReadFull(conn, buf[:]) - if err != nil { - return errors.Wrap(err, "reading startup message length") - } - size := binary.BigEndian.Uint32(buf[:]) - if size < 4 { - return errors.Errorf("invalid startup packet length: %d bytes", size) - } - maxLen := s.MaxStartupSize - if maxLen == 0 { - maxLen = 1024 * 1024 - } - if size > maxLen { - return errors.Errorf("oversized startup frame of %d bytes (max: %d bytes)", size, maxLen) - } - data := make([]byte, size-4) - _, err = io.ReadFull(conn, data) - if err != nil { - return errors.Wrap(err, "reading startup packet") - } - - // Extract protocol ID. - if len(data) < 4 { - return errors.Errorf("startup packet is too small for protocol ID: %d bytes", len(data)) - } - proto := Protocol(binary.BigEndian.Uint32(data)) - data = data[4:] - - if proto == ProtocolSSL { - if s.TLSConfig != nil { - // Upgrade the connection to TLS and renegotiate on the tunneled connection. - _, err = conn.Write([]byte{'S'}) - if err != nil { - return errors.Wrap(err, "sending SSL support confirmation") - } - conn = tls.Server(conn, s.TLSConfig) - if s.StartupTimeout > 0 { - err := conn.SetDeadline(startupDeadline) - if err != nil { - return errors.Wrap(err, "transferring startup deadline to TLS connection") - } - } - hasTLS = true - goto startup - } - - // Inform the client that SSL is not available and try again. - s.Logger.Debugf("client at %s requested a secure postgres connection but TLS is not configured", conn.RemoteAddr()) - _, err = conn.Write([]byte{'N'}) - if err != nil { - return errors.Wrap(err, "sending SSL unsupported notification") - } - goto startup - } - - if s.TLSConfig != nil && !hasTLS { - // Reject the unsecured connection. - return errors.Errorf("client at %s attempted to initiate an unsecured postgres conenction", conn.RemoteAddr()) - } - - switch proto { - case ProtocolCancel: - // Handle cancellation. - return s.handleCancel(ctx, conn, data) - default: - // Handle regular postgres. - return s.handleStandard(ctx, proto, conn, data) - } -} - -// parseParams parses a parameter list from a startup packet. -func parseParams(data []byte) (map[string]string, error) { - params := make(map[string]string) - for { - idx := bytes.IndexByte(data, 0) - switch idx { - case 0: - return params, nil - case -1: - return nil, errors.New("malformed startup parameter list") - } - - key := string(data[:idx]) - data = data[idx+1:] - - idx = bytes.IndexByte(data, 0) - if idx == -1 { - return nil, errors.New("malformed startup parameter list") - } - val := string(data[:idx]) - data = data[idx+1:] - - params[key] = val - } -} - -// handleCancel handles cancel request connections. -func (s *Server) handleCancel(ctx context.Context, conn net.Conn, data []byte) error { - if len(data) != 8 { - return errors.New("malformed cancellation packet") - } - - if s.CancellationManager == nil { - return errors.New("cancellation is not configured") - } - - pid := int32(binary.BigEndian.Uint32(data[:4])) - key := int32(binary.BigEndian.Uint32(data[4:])) - - err := s.CancellationManager.Cancel(CancellationToken{PID: pid, Key: key}) - switch err { - case nil: - case ErrCancelledMissingConnection: - // This is usually not a real error (race condition in the protocol). - // This can happen if a client cancels a request and shuts down. - s.Logger.Debugf("client at %v sent a mismatched cancellation token (is a load balancer misconfigured?)", conn.RemoteAddr()) - default: - return err - } - - return nil -} -func (s *Server) SendParameterStatus(w *message.WireWriter, param, value string, encoder *message.Encoder) error { - msg, err := encoder.ParameterStatus(param, value) - if err != nil { - return err - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "sending parameter status") - } - return nil -} - -type Result struct { -} -type PgType byte - -// Constants used to indicated query interception -// Only pgPassOn is allowed to be processed in Featurebase query handling -const ( - pgPassOn PgType = 'x' - pgBackendPid PgType = 'a' - pgVersion PgType = 'b' - pgCountType PgType = 'c' - pgQueryTime PgType = 'd' - pgTerminate PgType = 'e' - pgEmpty PgType = 'f' - pgSetApplication PgType = 'g' - pgSelect1 PgType = 'h' - pgSchema PgType = 'i' - pgBegin PgType = 'j' - pgTypeLen PgType = 'k' -) - -type Portal struct { - Name string - Writer *message.WireWriter - commands []message.Message - Encoder *message.Encoder - mapper *sql.Mapper - sql string - pgspecial PgType - pid int32 - queryStart time.Time - server *Server - cancelNotify <-chan struct{} -} - -func (p *Portal) Reset() { - p.Name = "" - p.sql = "" - p.pgspecial = pgPassOn - p.commands = p.commands[:0] -} - -func (p *Portal) Bind() { - p.Add(message.BindComplete) -} - -var lookPQL = regexp.MustCompile(`\[.*\].*\)\z`) - -const POSTGRESLENSQL = `SELECT t.typlen FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n WHERE t.typnamespace=n.oid AND t.typname='name' AND n.nspname='pg_catalog'` - -func (p *Portal) Parse(data []byte) { - p.queryStart = time.Now() - queryStr := string(bytes.Trim(data, "\x00")) - foundPQL := lookPQL.FindStringSubmatch(queryStr) - if len(foundPQL) > 0 { - - p.sql = foundPQL[0] - p.Name = "PQL" - p.pgspecial = pgPassOn - p.Add(message.ParseOK) - return - } - if strings.Contains(queryStr, "EXTRACT") { - // had to add this hack because the vitis parser doesn't handle... - /* - SELECT pid as id, - query as stmt, - EXTRACT(seconds from query_start - NOW()) as elapsed_time - FROM pg_stat_activity - WHERE usename='docker'` - */ - p.pgspecial = pgQueryTime - p.Name = "SELECT" - p.sql = queryStr - p.Add(message.ParseOK) - return - } - - if len(queryStr) > 2 { - query, err := p.mapper.MapSQL(queryStr) - if err != nil { - return - } - - if strings.Contains(strings.ToLower(query.SQL), "select 1") { - p.pgspecial = pgSelect1 - p.Name = "SELECT" - } else if strings.Contains(queryStr, POSTGRESLENSQL) { - p.pgspecial = pgTypeLen - p.Name = "SELECT" - } else { - switch query.SQLType { - case sql.SQLTypeSet: - p.Name = "SET" - set := query.Statement.(*sqlparser.Set) - p.pgspecial = 0 - for _, item := range set.Exprs { - if item.Name.String() == "application_name" { - switch item.Expr.(type) { - case *sqlparser.SQLVal: - p.pgspecial = pgSetApplication - } - } - } - case sql.SQLTypeSelect: - p.Name = "SELECT" - p.pgspecial = pgPassOn - stmt := query.Statement.(*sqlparser.Select) - for _, item := range stmt.SelectExprs { - switch expr := item.(type) { - case *sqlparser.AliasedExpr: - switch colExpr := expr.Expr.(type) { - case *sqlparser.FuncExpr: - funcName := strings.ToLower(colExpr.Name.String()) - switch funcName { - case "pg_backend_pid": - //SELECT pg_backend_pid() - p.pgspecial = pgBackendPid - case "pg_terminate_backend": - //select pg_terminate_backend(100) - p.pgspecial = pgTerminate - case "version": - //SELECT VERSION() AS version - p.pgspecial = pgVersion - } - //need to return the pid from the cancelation object - //add row description object - //add data row for item - } - } - - } - for _, item := range stmt.From { - switch from := item.(type) { - case *sqlparser.AliasedTableExpr: - tableName := from.Expr.(sqlparser.TableName).ToViewName().Name.String() - switch tableName { - case "pg_type": - p.pgspecial = pgCountType - case "pg_stat_activity": - p.pgspecial = pgQueryTime - case "tables": - p.pgspecial = pgSchema - } - } - } - p.sql = queryStr - case sql.SQLTypeBegin: - // Ignore BEGIN - p.pgspecial = pgBegin - case sql.SQLTypeShow: - p.Name = "SHOW" - p.pgspecial = pgPassOn - p.sql = queryStr - } - } - } else { - p.pgspecial = pgEmpty - } - p.Add(message.ParseOK) -} -func (p *Portal) Describe() { - // Placeholder should we need to handle the Decribe request -} - -func (p *Portal) Execute() (shouldTerminate bool, queryReady bool, err error) { - queryReady = true - switch p.pgspecial { - case pgBackendPid: - rowDescription, e := p.Encoder.EncodeColumn("pg_backend_pid", int32(23), 4) - if e != nil { - err = e - return - } - p.Add(rowDescription) - pid := fmt.Sprintf("%v", p.pid) - dataRow, _ := p.Encoder.TextRow(pid) - p.Add(dataRow) - //needs data row with cancel token - case pgVersion: - rowDescription, e := p.Encoder.EncodeColumn("version", int32(25), -1) - if e != nil { - err = e - return - } - p.Add(rowDescription) - mesg := fmt.Sprintf("PostgresSQL 13.0 (molecula.%v)", p.server.QueryHandler.Version()) - dataRow, _ := p.Encoder.TextRow(mesg) - p.Add(dataRow) - case pgSelect1: - rowDescription, e := p.Encoder.EncodeColumn("?column?", int32(23), 4) - if e != nil { - err = e - return - } - p.Add(rowDescription) - dataRow, _ := p.Encoder.TextRow("1") - p.Add(dataRow) - case pgCountType: - //need to block - <-p.server.lookerChannel - rowDescription, e := p.Encoder.EncodeColumn("count", int32(20), 8) - if err != nil { - err = e - return - } - p.Add(rowDescription) - errorResponse, _ := p.Encoder.Error( - message.NoticeField{ - Type: message.NoticeFieldSeverity, - Data: "FATAL", - }, - message.NoticeField{ - Type: message.NoticeFieldMessage, - Data: "terminating connection due to administrator command", - }, - message.NoticeField{ - Type: message.NoticeFieldCode, - Data: "57P01", - }, - ) - p.Add(errorResponse) - e = p.Sync() //send and Reset - - if e != nil { - err = e - return - } - return true, queryReady, nil - case pgQueryTime: - // need to return something so that the id can be queried - //need to return SELECT pid as id, query as stmt, EXTRACT(seconds from query_start - NOW()) as elapsed_time FROM pg_stat_activity - //seems like we need a map of pids to querys - e := p.server.dumpPortalsTo(p) - if e != nil { - err = e - return - } - - case pgTerminate: - //note just have 1 lock that blocks all who try to count the activities - //TODO (twg) lock this - close(p.server.lookerChannel) //release all the other blockers and allow them to terminate - p.server.lookerChannel = make(chan struct{}) //create a new one just in case - // i think it needs to return boolean true - rowDescription, e := p.Encoder.EncodeColumn("pg_terminate_backend", int32(16), 1) - if e != nil { - err = e - return - } - p.Add(rowDescription) - dataRow, _ := p.Encoder.TextRow("t") - p.Add(dataRow) - commandComplete, e := p.Encoder.CommandComplete("SELECT 1") - if e != nil { - err = e - return - } - p.Add(commandComplete) - e = p.Sync() - if e != nil { - err = e - return - } - return false, queryReady, nil - case pgEmpty: - p.Add(message.NoData) - p.Add(message.EmptyQueryResponse) - e := p.Sync() - if e != nil { - err = e - return - } - queryReady = true - return false, queryReady, nil - case pgSetApplication: - //needs to add/send status - msg, e := p.Encoder.ParameterStatus("application_name", "PostgreSQL JDBC Driver") - if e != nil { - err = e - return - } - p.Add(msg) - case pgSchema: - parts := []message.SimpleColumn{ - { - Name: "table_schema", - Typeid: int32(19), - Typelen: 64, - }, - { - Name: "table_name", - Typeid: int32(19), - Typelen: 64, - }, - } - rowDescription, e := p.Encoder.EncodeColumns(parts...) - if e != nil { - err = e - return - } - p.Add(rowDescription) - e = p.HandleSchema() - if e != nil { - err = e - return - } - - case pgPassOn: - query := SimpleQuery(p.sql) - e := p.server.handleQuery(p, query, p.cancelNotify) - if e != nil { - err = e - return - } - return - - case pgBegin: - p.Name = "BEGIN" - case pgTypeLen: - rowDescription, e := p.Encoder.EncodeColumn("typelen", int32(21), 2) - if e != nil { - err = e - return - } - p.Add(rowDescription) - mesg := "64" - dataRow, _ := p.Encoder.TextRow(mesg) - p.Add(dataRow) - } - - //maybe add in the number of items in select clause - if len(p.Name) > 0 { //only send command complete for those that have names - message, _ := p.Encoder.CommandComplete(p.Name) - p.Add(message) - } - return -} - -// handleStandard handles a connection in the standard postgres wire protocol. -func (p *Portal) Sync() error { - for _, m := range p.commands { - err := p.Writer.WriteMessage(m) - if err != nil { - return err - } - } - p.Writer.Flush() - p.Reset() - return nil -} -func (p *Portal) Add(m message.Message) { - cp := message.Message{Type: m.Type, Data: make([]byte, len(m.Data))} - copy(cp.Data, m.Data) - p.commands = append(p.commands, cp) -} - -func (p *Portal) DumpComands() { - - for _, m := range p.commands { - m.Dump("DUMPING:") - } -} -func (p *Portal) HandleSchema() error { - return p.server.QueryHandler.HandleSchema(context.Background(), p) -} - -func (p *Portal) WriteMessage(m message.Message) error { - p.Add(m) - return nil -} -func (p *Portal) Flush() error { - return nil -} - -// handleStandard handles a connection in the standard postgres wire protocol. -// The client is responsible for closing the connection when this finishes. -func (s *Server) handleStandard(ctx context.Context, proto Protocol, conn net.Conn, data []byte) error { - // Wait for helper goroutines to finish. - var wg sync.WaitGroup - defer wg.Wait() - - // Set up context. - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Check the major version. - if proto.Major() != ProtocolSupported.Major() { - return errors.Errorf("unsupported protocol %v", proto) - } - - // Parse the parameters bundled in the startup packet. - params, err := parseParams(data) - if err != nil { - return errors.Wrap(err, "parsing parameters") - } - if user, ok := params["user"]; ok { - // Log the connection. - s.Logger.Debugf("new postgres connection from user %q at %v", user, conn.RemoteAddr()) - } else { - // We do not use this much yet, but the wire protocol says that it is required. - return errors.New("missing username") - } - - // Set up message input and output. - // Set up a reader that will preempt the connection when the context is canceled. - ir := idleReader{ - conn: conn, - timeout: s.ReadTimeout, - } - wg.Add(1) - go func() { - defer wg.Done() - - <-ctx.Done() - - ir.preempt() //nolint:errcheck - }() - - // Clear the startup deadline. - err = conn.SetDeadline(time.Time{}) - if err != nil { - return err - } - - // Set up a message reader with buffering. - rbuf := bufio.NewReader(&ir) - r := message.NewWireReader(rbuf) - - // Set up a writer on the connection. - var ww io.Writer = conn - if s.WriteTimeout != 0 { - // Apply the write timeout. - ww = &timeoutWriter{ - conn: conn, - timeout: s.WriteTimeout, - } - } - - // Set up a message writer with buffering. - w := message.NewWireWriter(bufio.NewWriter(ww)) - - var encoder message.Encoder - if proto.Minor() > ProtocolSupported.Minor() { - // Negotiate the version down. - s.Logger.Debugf("client requested unsupported protocol version %v; attempting to downgrade to %v", proto, ProtocolSupported) - msg, err := encoder.NegotiateProtocolVersion(int32(ProtocolSupported.Minor())) - if err != nil { - return errors.Wrap(err, "negotiating version") - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "negotiating version") - } - } - - // TODO: real auth - err = w.WriteMessage(message.AuthenticationOK) - if err != nil { - return errors.Wrap(err, "sending authentication confirmation") - } - err = s.SendParameterStatus(w, "application_name", "", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "client_encoding", "UTF8", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "DateStyle", "ISO, MDY", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "integer_datetimes", "on", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "IntervalStyle", "postgres", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "is_superuser", "on", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "server_encoding", "UTF8", &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "server_version", PgServerVersion, &encoder) - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - /* - -PARAMETER STATUS name='TimeZone', value='GMT' - */ - err = s.SendParameterStatus(w, "session_authorization", "docker", &encoder) //TODO(twg) figure out valid values here - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "standard_conforming_strings", "on", &encoder) //TODO(twg) figure out valid values here - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - err = s.SendParameterStatus(w, "TimeZone", "GMT", &encoder) //TODO(twg) figure out valid values here - if err != nil { - return errors.Wrap(err, "sending parameter status server version") - } - - var cancelNotify <-chan struct{} - var pid int32 - if s.CancellationManager != nil { - notify, cancel, token, err := s.CancellationManager.Token() - if err != nil { - return errors.Wrap(err, "setting up cancellation") - } - defer cancel() - - msg, err := encoder.BackendKeyData(token.PID, token.Key) - pid = token.PID - if err != nil { - return errors.Wrap(err, "encoding cancellation key data") - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "sending cancellation key data") - } - cancelNotify = notify - } - - var queryReady bool - portal := &Portal{ - Writer: w, - Encoder: &encoder, - commands: make([]message.Message, 0), - mapper: sql.NewMapper(), - pid: pid, - server: s, - cancelNotify: cancelNotify, - } - s.addPortal(portal) - defer s.removePortal(portal) - //mapper.Logger = logger - for { - if !queryReady { - // Indicate that we are ready for a query. - portal.sql = "" - msg, err := encoder.ReadyForQuery(message.TransactionStatusIdle) - if err != nil { - return errors.Wrap(err, "sending query ready status") - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "sending query ready status") - } - - // Flush the write buffer so that the client can respond. - err = w.Flush() - if err != nil { - return errors.Wrap(err, "flushing status") - } - - if rbuf.Buffered() == 0 { - // Put the connection into idle mode. - err = ir.setIdle() - if err != nil { - return errors.Wrap(err, "setting idle mode") - } - } else { - // If the client follows the spec, then it should not have sent anything more. - // However, it seems that no clients completely follow the spec, so we shouldn't rely on anything that isn't entirely straightforward. - s.Logger.Debugf("postgres client sent additional data without waiting for completion") - } - queryReady = true - } - - // Read the next packet. - msg, err := r.ReadMessage() - if err != nil { - if err == errPreempted { - // The server is shutting down. - return errors.Wrap(s.handleShutdown( - conn, w, &encoder, - - message.NoticeField{ - Type: message.NoticeFieldSeverity, - Data: "ERROR", - }, - message.NoticeField{ - Type: message.NoticeFieldMessage, - Data: "server shutting down", - }, - message.NoticeField{ - Type: message.NoticeFieldHint, - Data: "This is normal. This message is sent when a server is shutting down and terminating its connections.", - }, - ), "processing connection shutdown") - } - - return err - } - - switch msg.Type { - case message.TypeTermination: - // We are done. - return w.Flush() - - case message.TypeParse: - portal.Parse(msg.Data) - case message.TypeBind: - portal.Bind() - case message.TypeExecute: - term, qr, err := portal.Execute() - if err != nil { - return err - } - if term { - return w.Flush() - } - queryReady = qr - case message.TypeSync: - err := portal.Sync() - if err != nil { - return err - } - queryReady = false - case message.TypeSimpleQuery: - queryReady = false - query := SimpleQuery(strings.TrimSuffix(string(msg.Data), "\x00")) - - // Execute the query. - err := s.handleQuery(w, query, cancelNotify) - if err != nil { - return err - } - case message.TypeDescribe: - portal.Describe() - case message.TypeClose: - return w.Flush() - default: - // The message is not supported yet. - // Send an error. - s.Logger.Errorf("unrecognized postgres packet %v", msg) - msg, err = encoder.Error( - message.NoticeField{ - Type: message.NoticeFieldSeverity, - Data: "ERROR", - }, - message.NoticeField{ - Type: message.NoticeFieldMessage, - Data: fmt.Sprintf("unrecognized message type %q", msg.Type), - }, - message.NoticeField{ - Type: message.NoticeFieldDetail, - Data: "message body:" + hex.Dump(msg.Data), - }, - ) - if err != nil { - return errors.Wrap(err, "sending unrecognized message error") - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "sending unrecognized message error") - } - err = w.Flush() - if err != nil { - return errors.Wrap(err, "sending unrecognized message error") - } - } - } -} -func (s *Server) addPortal(p *Portal) { - s.mu.Lock() - defer s.mu.Unlock() - s.portals = append(s.portals, p) -} -func (s *Server) removePortal(p *Portal) { - s.mu.Lock() - defer s.mu.Unlock() - for i, portal := range s.portals { - if portal.pid == p.pid { - //remove i - s.portals = append(s.portals[:i], s.portals[i+1:]...) - return - } - - } - -} -func (s *Server) dumpPortalsTo(p *Portal) error { - s.mu.Lock() - defer s.mu.Unlock() - //need to add the descrition for the 3 fields - // <-:-ROW DESCRIPTION: num_fields=3 - //---[Field 01]--- name='id' type=23 type_len=4 type_mod=4294967295 relid=12250 attnum=3 format=0 - //---[Field 02]--- name='stmt' type=25 type_len=65535 type_mod=4294967295 relid=12250 attnum=20 format=0 - - //--[Field 03]--- name='elapsed_time' type=701 type_len=8 type_mod=4294967295 relid=0 attnum=0 format=0 - parts := []message.SimpleColumn{ - { - Name: "id", - Typeid: int32(23), - Typelen: 4, - }, - { - Name: "stmt", - Typeid: int32(25), - Typelen: -1, - }, - { - Name: "elapsed_time", - Typeid: int32(701), - Typelen: 8, - }, - } - rowDescription, err := p.Encoder.EncodeColumns(parts...) - if err != nil { - return err - } - p.Add(rowDescription) - - for _, portal := range s.portals { - dataRow, err := p.Encoder.TextRow( - fmt.Sprintf("%v", portal.pid), - portal.sql, - fmt.Sprintf("%v", time.Since(portal.queryStart).Seconds())) - if err != nil { - return err - } - //if sql == "" need to put in a null record - //need to add the dararow - //also need to figure out null types - p.Add(dataRow) - } - return nil -} - -// handleQuery processes a single query on a connection. -func (s *Server) handleQuery(w message.Writer, query Query, cancelNotify <-chan struct{}) error { - // Configure cancellation. - // This is not the connection context, since we want the request to finish safely before connection shutdown. - ctx := context.Background() - if cancelNotify != nil { - defer func() { - // Flush any cancel notifications. - // This works on a best-effort basis. - // It is still entirely possible that the cancel notification may be delivered to the next request. - // Regardless of what we do, we either get false positives or false negatives. - // This code chooses false positives. - for len(cancelNotify) > 0 { - <-cancelNotify - } - }() - - var wg sync.WaitGroup - defer wg.Add(1) - - var cancel context.CancelFunc - ctx, cancel = context.WithCancel(ctx) - defer cancel() - - wg.Add(1) - go func() { - defer wg.Done() - - select { - case <-ctx.Done(): - case <-cancelNotify: - cancel() - } - }() - } - - // Set up a result writer. - // SELECT is used as a default tag, which seems to be handled decently by clients. - // The encoder is intentionally not re-used because its buffer may be huge. - qwriter := &queryResultWriter{ - w: w, - te: s.TypeEngine, - tag: "SELECT", - } - // Dispatch the query handler. - qerr := s.QueryHandler.HandleQuery(ctx, qwriter, query) - if qerr != nil { - // There was an error in processing the query. - // Send the error back to the client and keep going. - s.Logger.Debugf("failed to execute query %q: %v", query, qerr) - msg, err := qwriter.enc.GoError(qerr) - if err != nil { - return errors.Wrap(err, "failed to send query error to client") - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "failed to send query error to client") - } - } else { - if !qwriter.wroteHeaders { - // The handler did not write headers. - // Write back an empty set of headers. - err := qwriter.WriteHeader() - if err != nil { - return errors.Wrap(err, "sending empty column headers") - } - } - // The query completed normally. - // Notify the client of completion. - msg, err := qwriter.enc.CommandComplete(qwriter.tag) - if err != nil { - return errors.Wrap(err, "sending command completion notification") - } - err = w.WriteMessage(msg) - if err != nil { - return errors.Wrap(err, "sending command completion notification") - } - } - - // The data will be flushed after we write back the "ready for query" state. - return nil -} - -func (s *Server) handleShutdown(conn net.Conn, w message.Writer, encoder *message.Encoder, notice ...message.NoticeField) error { - var wg sync.WaitGroup - defer wg.Wait() - - // Try to send a message to the client before closing the connection. - msg, err := encoder.Error(notice...) - if err != nil { - return errors.Wrap(err, "generating shutdown notification") - } - - if s.WriteTimeout == 0 { - // The client is likely to not listen for incoming messages. - // Force a write timeout to ensure that this terminates. - err := conn.SetWriteDeadline(time.Now().Add(time.Second)) - if err != nil { - return errors.Wrap(err, "setting shutdown write deadline") - } - } - - // The client may be waiting on a write, so we need to drain the incoming data stream. - err = conn.SetReadDeadline(time.Time{}) - if err != nil { - return errors.Wrap(err, "clearing read deadline for shutdown") - } - defer conn.SetReadDeadline(time.Now()) //nolint:errcheck - wg.Add(1) - go func() { - defer wg.Done() - - io.Copy(io.Discard, conn) //nolint:errcheck - }() - - // Attempt to send the shutdown notification. - // This will fail under many scenarios, as the client is not necessarily reading. - err = w.WriteMessage(msg) - if err != nil { - return nil - } - w.Flush() - - return nil -} diff --git a/pg/query.go b/pg/query.go deleted file mode 100644 index e514de951..000000000 --- a/pg/query.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import ( - "context" - "fmt" - - "github.com/featurebasedb/featurebase/v3/pg/message" - "github.com/pkg/errors" -) - -// Query is an interface to be implemented by queries. -type Query interface { - fmt.Stringer -} - -// SimpleQuery is a query sent as only a string. -// It has no parameters. -type SimpleQuery string - -func (q SimpleQuery) String() string { - return string(q) -} - -// ColumnInfo contains metadata about a column. -type ColumnInfo struct { - Name string - Type Type - TableID int32 - FieldID int16 -} - -// QueryResultWriter is used to write the results of a query back over the connection. -type QueryResultWriter interface { - // WriteHeader sets the column header information. - WriteHeader(...ColumnInfo) error - - // WriteRowText sends a row of data in textual format. - WriteRowText(...string) error - - // Tag assigns a tag to the query. - // This should be called before the query is completed. - Tag(tag string) -} - -// QueryHandler handles a query. -type QueryHandler interface { - // HandleQuery executes a query and writes the results back. - HandleQuery(context.Context, QueryResultWriter, Query) error - HandleSchema(context.Context, *Portal) error - Version() string -} - -// queryResultWriter implements QueryResultWrtiter over postgres wire protocol. -// The underlying message writer must be flushed by the caller once the query has finished. -type queryResultWriter struct { - w message.Writer - te TypeEngine - enc message.Encoder - width int - wroteHeaders bool - tag string -} - -func (w *queryResultWriter) WriteHeader(info ...ColumnInfo) error { - if w.wroteHeaders { - return errors.New("double-write of query headers") - } - - // Translate column information into a row description message. - desc := make([]message.ColumnDescription, len(info)) - for i, c := range info { - t, err := w.te.TranslateType(c.Type) - if err != nil { - return errors.Wrap(err, "translating column type") - } - t.Name = c.Name - t.TableID = c.TableID - t.FieldID = c.FieldID - desc[i] = t - } - - // Encode the row description. - msg, err := w.enc.RowDescription(desc...) - if err != nil { - return errors.Wrap(err, "encoding query header") - } - - w.wroteHeaders = true - w.width = len(desc) - - // Write the row description. - return w.w.WriteMessage(msg) -} - -func (w *queryResultWriter) WriteRowText(text ...string) error { - // Check preconditions of the call. - switch { - case !w.wroteHeaders: - return errors.New("writing rows without headers") - case len(text) != w.width: - return errors.Errorf("expected %d columns but found %d", w.width, len(text)) - } - - // Encode the row data as text into a DataRow message. - msg, err := w.enc.TextRow(text...) - if err != nil { - return err - } - - // Write the data row over the network. - return w.w.WriteMessage(msg) -} - -func (w *queryResultWriter) Tag(tag string) { - w.tag = tag -} - -var _ QueryResultWriter = (*queryResultWriter)(nil) diff --git a/pg/server.go b/pg/server.go deleted file mode 100644 index ef9bab059..000000000 --- a/pg/server.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import ( - "context" - "crypto/tls" - "net" - "sync" - "time" - - "github.com/featurebasedb/featurebase/v3/logger" -) - -// Server is a postgres wire protocol server. -type Server struct { - // QueryHandler will be used to serve query requests. - QueryHandler QueryHandler - - // TypeEngine is the type engine to use to result columns in query requests. - TypeEngine TypeEngine - - // TLSConfig is the TLS configuration to use to serve postgres TLS connections. - TLSConfig *tls.Config - - // StartupTimeout is the timeout to use for connection startup. - // If a connection fails to set up a protocol before this completes, it will be terminated. - StartupTimeout time.Duration - - // ReadTimeout is the timeout to apply for active reads (reads during the lifetime of a command). - // This timeout does not apply to an idling connection. - ReadTimeout time.Duration - - // WriteTimeout is the timeout to apply to network writes. - WriteTimeout time.Duration - - // MaxStartupSize is the maximum size of the startup packet (in bytes). - // This defaults to 2^31-1 bytes, which is the maximum size allowed by the protocol. - MaxStartupSize uint32 - - // ConnectionLimit is the maximum number of connections to allow at once. - ConnectionLimit uint16 - - // Logger is the logger to use for error conditions and state changes. - Logger logger.Logger - - // CancellationManager is the cancellation manager to use. - // If this is not set, no cancellations will be applied. - CancellationManager CancellationManager - lookerChannel chan struct{} - mu sync.Mutex - portals []*Portal -} - -// ServeConn serves a single connection. -func (s *Server) ServeConn(ctx context.Context, conn net.Conn) error { - return s.handle(ctx, conn) -} - -// Serve accepts postgres connections from a listener and processes them. -// If the context is cancelled, this will stop accepting requests and wait until all connections have terminated. -// No error will be returned if terminated by context cancellation. -// This will close the connection for the caller. -func (s *Server) Serve(ctx context.Context, l net.Listener) (err error) { - // Ignore errors triggered by a shutdown. - // Also propogate any error from terminating the listener. - var cerr error - defer func(ctx context.Context) { - if ctx.Err() == context.Canceled { - err = cerr - } - }(ctx) - // TODO (twg) added for looker - s.lookerChannel = make(chan struct{}) - - // Wait for the listener to be closed and all connections to shut down. - var wg sync.WaitGroup - defer wg.Wait() - - // Wrap the context to propogate a shutdown to the listeners and connection handlers. - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Start a goroutine to shut down the listener when the context is canceled. - wg.Add(1) - go func() { - defer wg.Done() - - <-ctx.Done() - cerr = l.Close() - }() - - // Set up a semaphore for the connection limit. - var limit chan struct{} - done := ctx.Done() - if s.ConnectionLimit != 0 { - limit = make(chan struct{}, s.ConnectionLimit) - } - - for { - if limit != nil { - // Wait for connection limit. - if len(limit) == cap(limit) { - s.Logger.Warnf("postgres connection limit reached") - } - select { - case limit <- struct{}{}: - case <-done: - return nil - } - } - - // Accept a connection. - conn, err := l.Accept() - if err != nil { - return err - } - - // Handle the connection in another goroutine. - wg.Add(1) - go func() { - defer wg.Done() - if limit != nil { - // Restore connection limit when done. - defer func() { <-limit }() - } - err := s.handle(ctx, conn) - if err != nil { - s.Logger.Errorf("postgres connection terminated with error: %v", err) - } - }() - } -} diff --git a/pg/server_test.go b/pg/server_test.go deleted file mode 100644 index 19b84cd79..000000000 --- a/pg/server_test.go +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg_test - -import ( - "bytes" - "context" - "crypto/rand" - "fmt" - "net" - "os/exec" - "strconv" - "sync" - "syscall" - "testing" - "time" - - "github.com/lib/pq" - "github.com/featurebasedb/featurebase/v3/logger" - "github.com/featurebasedb/featurebase/v3/pg" - "github.com/featurebasedb/featurebase/v3/pg/pgtest" -) - -// TestStartupTimeout tests that an incoming connection that does nothing times out and gets closed. -func TestStartupTimeout(t *testing.T) { - t.Parallel() - - connect, shutdown, err := pgtest.ServeMem(&pg.Server{ - StartupTimeout: time.Millisecond, - Logger: logger.NopLogger, - }) - if err != nil { - t.Fatalf("starting in-memory postgres server: %v", err) - } - defer shutdown.Finish(t, "in-memory postgres server") - - conn, err := connect() - if err != nil { - t.Fatalf("failed to acquire connection: %v", err) - } - defer conn.Close() - - // The server isn't sending anything, so this should block until the connection dies. - conn.Read(make([]byte, 1024)) //nolint:errcheck -} - -// TestStartupInvalidLength tests that sending an HTTP GET request does not cause the server to allocate 1.2 GiB of memory. -func TestStartupInvalidLength(t *testing.T) { - t.Parallel() - - res := testing.Benchmark(func(b *testing.B) { - connect, shutdown, err := pgtest.ServeMem(&pg.Server{ - MaxStartupSize: 1024, - Logger: logger.NopLogger, - }) - if err != nil { - t.Fatalf("starting in-memory postgres server: %v", err) - } - defer shutdown.Finish(t, "in-memory postgres server") - - b.ReportAllocs() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - conn, err := connect() - if err != nil { - t.Fatalf("failed to acquire connection: %v", err) - } - - _, err = conn.Write([]byte("GET ")) - if err != nil { - t.Fatalf("failed to write invalid length: %v", err) - } - - // The server isn't sending anything, so this should block until the connection dies. - conn.Read(make([]byte, 1024)) //nolint:errcheck - - err = conn.Close() - if err != nil { - t.Fatalf("failed to close connection: %v", err) - } - } - }) - bpo := res.AllocedBytesPerOp() - t.Logf("allocated %d bytes per op", bpo) - if bpo > 1024*1024 { - t.Errorf("allocated too much memory: %d bytes/connection", bpo) - } -} - -// TestPQConnect tests connecting the Go SQL driver `pq` to this postgres server. -func TestPQConnect(t *testing.T) { - t.Parallel() - - server := &pg.Server{ - StartupTimeout: time.Second, - Logger: logger.NopLogger, - } - - addr, shutdown, err := pgtest.ServeTCP("localhost:0", server) - if err != nil { - t.Fatalf("starting postgres server: %v", err) - } - defer shutdown.Finish(t, "postgres server") - - tcpAddr := addr.(*net.TCPAddr) - - connector, err := pq.NewConnector(fmt.Sprintf("user=molecula dbname=pilosa sslmode=disable host=%s port=%d", tcpAddr.IP, tcpAddr.Port)) - if err != nil { - t.Fatalf("failed to create connector: %v", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - conn, err := connector.Connect(ctx) - if err != nil { - t.Fatalf("failed to connect to postgres: %v", err) - } - defer pgtest.ShutdownFunc(conn.Close).Finish(t, "postgres TLS conn") -} - -// TestPQConnectSSL tests connecting the Go SQL driver `pq` to this postgres server, with SSL enabled. -func TestPQConnectSSL(t *testing.T) { - t.Parallel() - - server := &pg.Server{ - StartupTimeout: time.Second, - Logger: logger.NopLogger, - } - - addr, shutdown, err := pgtest.ServeTLS("localhost:0", server) - if err != nil { - t.Fatalf("starting postgres server: %v", err) - } - defer shutdown.Finish(t, "postgres TLS server") - - tcpAddr := addr.(*net.TCPAddr) - - connector, err := pq.NewConnector(fmt.Sprintf("user=molecula dbname=pilosa sslmode=require host=%s port=%d", tcpAddr.IP, tcpAddr.Port)) - if err != nil { - t.Fatalf("failed to create connector: %v", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - conn, err := connector.Connect(ctx) - if err != nil { - t.Fatalf("failed to connect to postgres: %v", err) - } - defer pgtest.ShutdownFunc(conn.Close).Finish(t, "postgres TLS conn") -} - -// TestPSQLQuery tests sending a query from the `psql` command line tool. -func TestPSQLQuery(t *testing.T) { - // Check if psql is present. - // Skip this test if it is not. - _, err := exec.LookPath("psql") - if err != nil { - if err, ok := err.(*exec.Error); ok { - if err.Err == exec.ErrNotFound { - t.Skip("psql is not available") - } - } - t.Fatalf("searching for psql: %v", err) - } - - t.Run("Query", func(t *testing.T) { - server := &pg.Server{ - QueryHandler: pgtest.HandlerFunc(func(ctx context.Context, w pg.QueryResultWriter, q pg.Query) error { - err := w.WriteHeader(pg.ColumnInfo{ - Name: "field", - Type: pg.TypeCharoid, - }) - if err != nil { - return err - } - - err = w.WriteRowText("h") - if err != nil { - return err - } - - err = w.WriteRowText("xyzzy") - if err != nil { - return err - } - - return nil - }), - TypeEngine: pg.PrimitiveTypeEngine{}, - StartupTimeout: time.Second, - Logger: logger.NopLogger, - } - - addr, shutdown, err := pgtest.ServeTCP("localhost:0", server) - if err != nil { - t.Fatalf("starting postgres server: %v", err) - } - defer shutdown.Finish(t, "postgres server") - - tcpAddr := addr.(*net.TCPAddr) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - cmd := exec.CommandContext(ctx, "psql", "-h", tcpAddr.IP.String(), "-p", strconv.Itoa(tcpAddr.Port), "-c", "test query") - data, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("psql failed: %v", string(data)) - } - }) - - t.Run("Cancel", func(t *testing.T) { - var term func() error - var qerr error - var qdone bool - defer func() { - if qerr != nil { - t.Fatal(qerr) - } - if !qdone { - t.Fatal("query not done") - } - }() - - var mutex sync.Mutex - mutex.Lock() - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - server := &pg.Server{ - QueryHandler: pgtest.HandlerFunc(func(qctx context.Context, w pg.QueryResultWriter, q pg.Query) error { - defer func() { qdone = true }() - - mutex.Lock() - defer mutex.Unlock() - - qerr = term() - if qerr != nil { - return qerr - } - select { - case <-qctx.Done(): - case <-ctx.Done(): - qerr = ctx.Err() - return qerr - } - return nil - }), - TypeEngine: pg.PrimitiveTypeEngine{}, - StartupTimeout: time.Second, - Logger: logger.NopLogger, - CancellationManager: pg.NewLocalCancellationManager(rand.Reader), - } - - addr, shutdown, err := pgtest.ServeTCP("localhost:0", server) - if err != nil { - t.Fatalf("starting postgres server: %v", err) - } - defer shutdown.Finish(t, "postgres server") - - tcpAddr := addr.(*net.TCPAddr) - - cmd := exec.CommandContext(ctx, "psql", "-h", tcpAddr.IP.String(), "-p", strconv.Itoa(tcpAddr.Port), "-c", "test query") - term = func() error { return cmd.Process.Signal(syscall.SIGINT) } - var buf bytes.Buffer - cmd.Stderr = &buf - cmd.Stdout = &buf - err = cmd.Start() - if err != nil { - t.Fatalf("starting postgres client: %v", err) - } - mutex.Unlock() - err = cmd.Wait() - if err != nil { - t.Fatalf("psql failed (%v): %s", err, buf.String()) - } - }) -} diff --git a/pg/type.go b/pg/type.go deleted file mode 100644 index 672fe8df3..000000000 --- a/pg/type.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pg - -import "github.com/featurebasedb/featurebase/v3/pg/message" - -// Type represents a postgres type. -type Type struct { - // I am not entirely sure what should be in here long term. - // For now, I am just going to leave it like this. - Id int32 - Typelen int16 -} - -// TypeCharoid is a postgres type for text. -// found in postgres source src/include/catalog/pg_type.h -var TypeCharoid = Type{Id: 18, Typelen: -1} -var TypeNAMEOID = Type{Id: 19, Typelen: 64} -var TypeINT4OID = Type{Id: 23, Typelen: 4} -var TypeTEXTOID = Type{Id: 25, Typelen: -1} -var TypeFLOAT8OID = Type{Id: 701, Typelen: 8} - -// TypeEngine is a system for managing types. -// This is necessary for compound types like arrays which need ID generation. -type TypeEngine interface { - // TranslateType populates a column description with type information. - TranslateType(Type) (message.ColumnDescription, error) -} - -// PrimitiveTypeEngine is a simple type engine that only works on primitive types. -type PrimitiveTypeEngine struct{} - -// TranslateType translates a type to a column description. -func (pte PrimitiveTypeEngine) TranslateType(t Type) (message.ColumnDescription, error) { - var TypeLen int16 - var TypeID int32 - switch t { - case TypeCharoid: - TypeID = TypeCharoid.Id - TypeLen = TypeCharoid.Typelen - case TypeNAMEOID: - TypeID = TypeNAMEOID.Id - TypeLen = TypeNAMEOID.Typelen - case TypeINT4OID: - TypeID = TypeINT4OID.Id - TypeLen = TypeINT4OID.Typelen - case TypeTEXTOID: - TypeID = TypeTEXTOID.Id - TypeLen = TypeTEXTOID.Typelen - case TypeFLOAT8OID: - TypeID = TypeFLOAT8OID.Id - TypeLen = TypeFLOAT8OID.Typelen - default: // treat like TypeCharoid: - TypeID = TypeCharoid.Id - TypeLen = TypeCharoid.Typelen - } - return message.ColumnDescription{ - TypeID: TypeID, - TypeLen: TypeLen, - TypeModifier: -1, - Mode: 0, // send as text - }, nil -} diff --git a/planner.go b/planner.go deleted file mode 100644 index ce10a1b01..000000000 --- a/planner.go +++ /dev/null @@ -1,1330 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pilosa - -import ( - "context" - "database/sql" - "fmt" - "strconv" - "strings" - - "github.com/featurebasedb/featurebase/v3/pql" - "github.com/featurebasedb/featurebase/v3/sql2" -) - -type Planner struct { - executor *executor -} - -func NewPlanner(executor *executor) *Planner { - return &Planner{executor: executor} -} - -func (p *Planner) PlanStatement(ctx context.Context, stmt sql2.Statement) (*Stmt, error) { - node, err := p.planStatement(ctx, stmt) - if err != nil { - return nil, err - } - return &Stmt{node: node}, nil -} - -func (p *Planner) planStatement(ctx context.Context, stmt sql2.Statement) (StmtNode, error) { - if err := p.checkStatement(stmt); err != nil { - return nil, err - } - - switch stmt := stmt.(type) { - case *sql2.SelectStatement: - return p.planSelectStatement(ctx, stmt) - default: - return nil, fmt.Errorf("cannot plan statement: %T", stmt) - } -} - -func (p *Planner) planSelectStatement(ctx context.Context, stmt *sql2.SelectStatement) (_ StmtNode, err error) { - if stmt.IsAggregate() { - return p.planAggregateSelectStatement(ctx, stmt) - } - return p.planNonAggregateSelectStatement(ctx, stmt) -} - -func (p *Planner) planAggregateSelectStatement(ctx context.Context, stmt *sql2.SelectStatement) (_ StmtNode, err error) { - // Handle specific case of a two-table INNER JOIN with a COUNT(). - if _, ok := stmt.Source.(*sql2.JoinClause); ok { - return p.planAggregateCountJoin(ctx, stmt) - } - - indexName, err := statementTableName(stmt) - if err != nil { - return nil, err - } - - // Convert WHERE clause. - cond, err := p.planExprPQL(ctx, stmt, stmt.WhereExpr) - if err != nil { - return nil, err - } - - // Extract calls and grouped expressions from column list. - // TODO: Recursively traverse all expression trees. - var calls []*sql2.Call - var columns []*StmtColumn - var resultCols []string - for _, c := range stmt.Columns { - columns = append(columns, &StmtColumn{ - Name: c.Name(), - Type: sql2.ExprDataType(c.Expr), - }) - - switch expr := c.Expr.(type) { - case *sql2.Call: - calls = append(calls, expr) - resultCols = append(resultCols, "_aggregate") - case *sql2.QualifiedRef: - resultCols = append(resultCols, expr.Column.Name) - default: - return nil, fmt.Errorf("unsupported expression type in aggregate query: %T", expr) - } - } - - // TODO: Support multiple calls per query. - if len(calls) > 1 { - return nil, fmt.Errorf("only one aggregate call allowed") - } - - // Extract column names in GROUP BY clause. - var groupByCols []string - for _, expr := range stmt.GroupByExprs { - switch expr := expr.(type) { - case *sql2.QualifiedRef: - groupByCols = append(groupByCols, expr.Column.Name) - default: - return nil, fmt.Errorf("unsupported expression type in GROUP BY clause: %T", expr) - } - } - - // Extract aggregate call and build execution node. - callName := strings.ToUpper(sql2.IdentName(calls[0].Name)) - switch callName { - case "COUNT": - if len(groupByCols) == 0 { - if cond == nil { - cond = &pql.Call{Name: "All"} - } - return NewCountNode(p.executor, indexName, columns[0], &pql.Call{ - Name: "Count", - Children: []*pql.Call{cond}, - }), nil - } - - var aggregate *pql.Call - if calls[0].Distinct.IsValid() { - if len(calls[0].Args) != 1 { - return nil, fmt.Errorf("distinct count must have exactly one field specified") - } - ref, ok := calls[0].Args[0].(*sql2.QualifiedRef) - if !ok { - return nil, fmt.Errorf("distinct count argument must be a field name") - } - - aggregate = &pql.Call{ - Name: "Count", - Children: []*pql.Call{{ - Name: "Distinct", - Args: map[string]interface{}{"field": ref.Column.Name}, - }}, - } - } - - return NewGroupByNode(p.executor, indexName, resultCols, groupByCols, columns, aggregate, cond), nil - - case "SUM": - if len(calls[0].Args) != 1 { - return nil, fmt.Errorf("sum must have exactly one field specified") - } - ref, ok := calls[0].Args[0].(*sql2.QualifiedRef) - if !ok { - return nil, fmt.Errorf("sum argument must be a field name") - } - - aggregate := &pql.Call{ - Name: "Sum", - Args: map[string]interface{}{"field": ref.Column.Name}, - } - - return NewGroupByNode(p.executor, indexName, resultCols, groupByCols, columns, aggregate, cond), nil - - default: - return nil, fmt.Errorf("unsupported call in aggregate query: %s", callName) - } - - // TODO: Support HAVING -} - -func (p *Planner) planAggregateCountJoin(ctx context.Context, stmt *sql2.SelectStatement) (_ StmtNode, err error) { - // Ensure we have an INNER JOIN. - join := stmt.Source.(*sql2.JoinClause) // caller checked - if !join.Operator.Inner.IsValid() { - return nil, fmt.Errorf("only inner joins are currently supported") - } - - // Determine the two tables we are joining. - tbl0, ok := join.X.(*sql2.QualifiedTableName) - if !ok { - return nil, fmt.Errorf("left side of join must be a table") - } - tbl1, ok := join.Y.(*sql2.QualifiedTableName) - if !ok { - return nil, fmt.Errorf("left side of join must be a table") - } - - // Ensure INNER JOIN has an "ON" constraint. - if join.Constraint == nil { - return nil, fmt.Errorf("joins must have an ON constraint") - } - cons, ok := join.Constraint.(*sql2.OnConstraint) - if !ok { - return nil, fmt.Errorf("joins only support an ON constraint") - } - - // Determine the joined columns. - cx, ok := cons.X.(*sql2.BinaryExpr) - if !ok { - return nil, fmt.Errorf("join must use a binary expression") - } else if cx.Op != sql2.EQ { - return nil, fmt.Errorf("join must use an equality expression") - } - - // Extract join columns & validate that they reference known tables and join on "_id". - x, ok := cx.X.(*sql2.QualifiedRef) - if !ok { - return nil, fmt.Errorf("left-hand side of join expression must be a table-qualified column") - } else if x.Table.Name != tbl0.TableName() && x.Table.Name != tbl1.TableName() { - return nil, fmt.Errorf("no such table: %q", x.Table.Name) - } - - y, ok := cx.Y.(*sql2.QualifiedRef) - if !ok { - return nil, fmt.Errorf("right-hand side of join expression must be a table-qualified column") - } else if y.Table.Name != tbl0.TableName() && y.Table.Name != tbl1.TableName() { - return nil, fmt.Errorf("no such table: %q", y.Table.Name) - } - - if x.Column.Name != "_id" && y.Column.Name != "_id" { - return nil, fmt.Errorf("must join table on _id column") - } else if x.Column.Name == "_id" && y.Column.Name == "_id" { - return nil, fmt.Errorf("cannot join _id field of two tables") - } - - // Move ID column to LHS. - if x.Column.Name != "_id" { - x, y = y, x - } - - // Move parent table to LHS. - if x.Table.Name != tbl0.TableName() { - tbl0, tbl1 = tbl1, tbl0 - } - - // Ensure column expression is a single COUNT. - if len(stmt.Columns) != 1 { - return nil, fmt.Errorf("only COUNT() is supported on joined tables") - } - expr, ok := stmt.Columns[0].Expr.(*sql2.Call) - if !ok || strings.ToUpper(expr.Name.Name) != "COUNT" { - return nil, fmt.Errorf("only COUNT() is supported on joined tables") - } - - // Extract WHERE clause and separate by parent/child tables. - var cond0, cond1 sql2.Expr - for _, cond := range sql2.SplitExprTree(stmt.WhereExpr) { - tblName, ok := sql2.ExprTableName(cond) - if !ok { - return nil, fmt.Errorf("cannot filter across multiple tables in an expression") - } else if tblName == "" { - return nil, fmt.Errorf("expression must reference a table name") - } else if tblName != tbl0.TableName() && tblName != tbl1.TableName() { - return nil, fmt.Errorf("no such table: %q", tblName) - } - - // Match to parent table. - if tblName == tbl0.TableName() { - if cond0 == nil { - cond0 = cond - } else { - cond0 = &sql2.BinaryExpr{X: cond0, Op: sql2.AND, Y: cond} - } - continue - } - - // Match to child table. - if cond1 == nil { - cond1 = cond - } - cond1 = &sql2.BinaryExpr{X: cond1, Op: sql2.AND, Y: cond} - } - - // Convert conditions to PQL. - pqlCond0, err := p.planExprPQL(ctx, stmt, cond0) - if err != nil { - return nil, err - } else if pqlCond0 == nil { - pqlCond0 = &pql.Call{Name: "All"} - } - - pqlCond1, err := p.planExprPQL(ctx, stmt, cond1) - if err != nil { - return nil, err - } else if pqlCond1 == nil { - pqlCond1 = &pql.Call{ - Name: "Row", - Args: map[string]interface{}{y.Column.Name: &pql.Condition{ - Op: pql.NEQ, - }}, - } - } - - return NewCountNode(p.executor, tbl0.Name.Name, - &StmtColumn{ - Name: stmt.Columns[0].Name(), - Type: sql2.DataTypeInt, - }, - &pql.Call{ - Name: "Count", - Children: []*pql.Call{{ - Name: "Intersect", - Children: []*pql.Call{ - pqlCond0, - { - Name: "Distinct", - Children: []*pql.Call{ - pqlCond1, - }, - Args: map[string]interface{}{ - "index": tbl1.Name.Name, - "field": y.Column.Name, - }, - }, - }, - }}, - }, - ), nil -} - -func (p *Planner) planNonAggregateSelectStatement(ctx context.Context, stmt *sql2.SelectStatement) (_ StmtNode, err error) { - indexName, err := statementTableName(stmt) - if err != nil { - return nil, err - } - - // Lookup index. - idx := p.executor.Holder.Index(indexName) - if idx == nil { - return nil, newNotFoundError(ErrIndexNotFound, indexName) - } - - // Convert WHERE clause. - cond, err := p.planExprPQL(ctx, stmt, stmt.WhereExpr) - if err != nil { - return nil, err - } - - // Build column list. - var srcs []string - var columns []*StmtColumn - for _, col := range stmt.Columns { - // Handle expressions and qualified references. - switch expr := col.Expr.(type) { - case *sql2.QualifiedRef: - srcs = append(srcs, sql2.IdentName(expr.Column)) - columns = append(columns, &StmtColumn{ - Name: sql2.IdentName(expr.Column), - Type: sql2.ExprDataType(col.Expr), - }) - - default: - return nil, fmt.Errorf("unsupported column expression: %T", expr) - } - } - - return NewExtractNode(p.executor, indexName, srcs, columns, cond), nil -} - -// planExprPQL returns a PQL call tree for a given expression. -func (p *Planner) planExprPQL(ctx context.Context, stmt *sql2.SelectStatement, expr sql2.Expr) (_ *pql.Call, err error) { - if expr == nil { - return nil, nil - } - - switch expr := expr.(type) { - case *sql2.BinaryExpr: - return p.planBinaryExprPQL(ctx, stmt, expr) - case *sql2.BindExpr: - return nil, fmt.Errorf("bind expressions are not supported") - case *sql2.BlobLit: - return nil, fmt.Errorf("blob literals are not supported") - case *sql2.BoolLit: - return nil, fmt.Errorf("boolean literals are not supported") - case *sql2.Call: - return nil, fmt.Errorf("call expressions are not supported") - case *sql2.CaseExpr: - return nil, fmt.Errorf("case expressions are not supported") - case *sql2.CastExpr: - return nil, fmt.Errorf("cast expressions are not supported") - case *sql2.Exists: - return nil, fmt.Errorf("exists expressions are not supported") - case *sql2.ExprList: - return nil, fmt.Errorf("expression lists are not supported") - case *sql2.Ident: - return nil, fmt.Errorf("identifiers are not supported") - case *sql2.NullLit: - return nil, fmt.Errorf("NULL expressions are not supported") - case *sql2.NumberLit: - return nil, fmt.Errorf("number expressions are not supported") - case *sql2.ParenExpr: - return p.planExprPQL(ctx, stmt, expr.X) - case *sql2.QualifiedRef: - return nil, fmt.Errorf("qualified references are not supported") - case *sql2.Raise: - return nil, fmt.Errorf("raise expressions are not supported") - case *sql2.Range: - return nil, fmt.Errorf("range expressions are not supported") - case *sql2.StringLit: - return nil, fmt.Errorf("string literals are not supported") - case *sql2.UnaryExpr: - return nil, fmt.Errorf("unary expressions are not supported") - default: - return nil, fmt.Errorf("unexpected SQL expression type: %T", expr) - } -} - -func (p *Planner) planBinaryExprPQL(ctx context.Context, stmt *sql2.SelectStatement, expr *sql2.BinaryExpr) (_ *pql.Call, err error) { - switch op := expr.Op; op { - case sql2.AND, sql2.OR: - name := "Intersect" - if op == sql2.OR { - name = "Union" - } - - x, err := p.planExprPQL(ctx, stmt, expr.X) - if err != nil { - return nil, err - } - y, err := p.planExprPQL(ctx, stmt, expr.Y) - if err != nil { - return nil, err - } - - return &pql.Call{ - Name: name, - Children: []*pql.Call{x, y}, - }, nil - - case sql2.EQ, sql2.NE, sql2.LT, sql2.LE, sql2.GT, sql2.GE: - // Ensure field reference exists in binary expression. - x, y := expr.X, expr.Y - xRef, xOk := x.(*sql2.QualifiedRef) - yRef, yOk := y.(*sql2.QualifiedRef) - if xOk && yOk { - return nil, fmt.Errorf("cannot compare fields in a WHERE clause") - } else if !xOk && !yOk { - return nil, fmt.Errorf("expression must reference one field") - } - - // Rewrite expression so field ref is LHS. - if !xOk && yOk { - xRef, y = yRef, x - switch op { - case sql2.LT: - op = sql2.GT - case sql2.LE: - op = sql2.GE - case sql2.GT: - op = sql2.LT - case sql2.GE: - op = sql2.LE - } - } - - pqlValue, err := sqlToPQLValue(y) - if err != nil { - return nil, err - } - - isBSI := true // TODO: Check field if it is a BSI field. - if !isBSI { - return &pql.Call{ - Name: "Row", - Args: map[string]interface{}{ - sql2.IdentName(xRef.Column): pqlValue, - }, - }, nil - } - - pqlOp, err := sqlToPQLOp(op) - if err != nil { - return nil, err - } - return &pql.Call{ - Name: "Row", - Args: map[string]interface{}{ - sql2.IdentName(xRef.Column): &pql.Condition{ - Op: pqlOp, - Value: pqlValue, - }, - }, - }, nil - - case sql2.BITAND, sql2.BITOR, sql2.BITNOT, sql2.LSHIFT, sql2.RSHIFT: - return nil, fmt.Errorf("bitwise operators are not supported in WHERE clause") - case sql2.PLUS, sql2.MINUS, sql2.STAR, sql2.SLASH, sql2.REM: // + - return nil, fmt.Errorf("arithmetic operators are not supported in WHERE clause") - case sql2.CONCAT: - return nil, fmt.Errorf("concatenation operator is not supported in WHERE clause") - case sql2.IN, sql2.NOTIN: - return nil, fmt.Errorf("IN operator is not supported") - case sql2.BETWEEN, sql2.NOTBETWEEN: - return nil, fmt.Errorf("BETWEEN operator is not supported") - default: - return nil, fmt.Errorf("unexpected binary expression operator: %s", expr.Op) - } -} - -// sqlToPQLOp converts a SQL2 operation token to PQL. -func sqlToPQLOp(op sql2.Token) (pql.Token, error) { - switch op { - case sql2.EQ: - return pql.EQ, nil - case sql2.NE: - return pql.NEQ, nil - case sql2.LT: - return pql.LT, nil - case sql2.LE: - return pql.LTE, nil - case sql2.GT: - return pql.GT, nil - case sql2.GE: - return pql.GTE, nil - default: - return pql.ILLEGAL, fmt.Errorf("cannot convert SQL op %q to PQL", op) - } -} - -// sqlToPQLValue converts a literal SQL2 expression node to a PQL Go value. -func sqlToPQLValue(expr sql2.Expr) (interface{}, error) { - switch expr := expr.(type) { - case *sql2.StringLit: - return expr.Value, nil - case *sql2.NumberLit: - if expr.IsFloat() { - return strconv.ParseFloat(expr.Value, 64) - } - return strconv.ParseInt(expr.Value, 10, 64) - case *sql2.BoolLit: - return expr.Value, nil - default: - return nil, fmt.Errorf("cannot convert SQL expression %T to a literal value", expr) - } -} - -func (p *Planner) checkStatement(stmt sql2.Statement) error { - switch stmt := stmt.(type) { - case *sql2.SelectStatement: - return p.checkSelectStatement(stmt) - default: - return nil - } -} - -func (p *Planner) checkSelectStatement(stmt *sql2.SelectStatement) error { - if err := p.expandSelectStatementWildcards(stmt); err != nil { - return err - } - - // Type check expressions in statement. - for _, col := range stmt.Columns { - if err := p.checkExpr(&col.Expr, stmt); err != nil { - return err - } - } - - if err := p.checkExpr(&stmt.WhereExpr, stmt); err != nil { - return err - } - - for i := range stmt.GroupByExprs { - if err := p.checkExpr(&stmt.GroupByExprs[i], stmt); err != nil { - return err - } - } - - if err := p.checkExpr(&stmt.HavingExpr, stmt); err != nil { - return err - } - - for _, term := range stmt.OrderingTerms { - if err := p.checkExpr(&term.X, stmt); err != nil { - return err - } - } - - if err := p.checkExpr(&stmt.LimitExpr, stmt); err != nil { - return err - } - - if err := p.checkExpr(&stmt.OffsetExpr, stmt); err != nil { - return err - } - - return nil -} - -func (p *Planner) expandSelectStatementWildcards(stmt *sql2.SelectStatement) error { - if !stmt.HasWildcard() { - return nil - } - - indexName, err := statementTableName(stmt) - if err != nil { - return err - } - - // Look up index. - idx := p.executor.Holder.Index(indexName) - if idx == nil { - return newNotFoundError(ErrIndexNotFound, indexName) - } - - // Replace wildcards with column references. - columns := make([]*sql2.ResultColumn, 0, len(stmt.Columns)) - for _, col := range stmt.Columns { - // Unqualified wildcard. - isWildcard := col.Star.IsValid() - if ref, ok := col.Expr.(*sql2.QualifiedRef); ok && ref.Star.IsValid() { - if ref.Table.Name != indexName { - return fmt.Errorf("no such table: %q", ref.Table.Name) - } - isWildcard = true - } - - // Simply add column as-is if it is not a wildcard. - if !isWildcard { - columns = append(columns, col) - continue - } - - // Add identifier field first. - columns = append(columns, &sql2.ResultColumn{ - Expr: &sql2.QualifiedRef{ - Table: &sql2.Ident{Name: idx.Name()}, - Column: &sql2.Ident{Name: "_id"}, - }, - }) - - // Then add all fields besides the existence bit. - for _, field := range idx.Fields() { - if field.Name() == "_exists" { - continue - } - columns = append(columns, &sql2.ResultColumn{ - Expr: &sql2.QualifiedRef{ - Table: &sql2.Ident{Name: idx.Name()}, - Column: &sql2.Ident{Name: field.Name()}, - }, - }) - } - } - stmt.Columns = columns - - return nil -} -func (p *Planner) checkExpr(expr *sql2.Expr, stmt sql2.Statement) error { - if e, err := sql2.Walk(&sqlExprTypeChecker{ - holder: p.executor.Holder, - stmt: stmt, - }, *expr); err != nil { - return err - } else if e != nil { - *expr = e.(sql2.Expr) - } else { - *expr = nil - } - return nil -} - -// sqlExprTypeChecker recursively performs type checking within an expression. -// Called by sqlTypeChecker. Implements sql2.Visitor. -type sqlExprTypeChecker struct { - holder *Holder - stmt sql2.Statement // scope -} - -var _ sql2.Visitor = (*sqlExprTypeChecker)(nil) - -func (v *sqlExprTypeChecker) Visit(node sql2.Node) (_ sql2.Visitor, _ sql2.Node, err error) { - switch n := node.(type) { - case *sql2.Call: - for i := range n.Args { - if err := v.checkExpr(&n.Args[i]); err != nil { - return nil, nil, err - } - } - return nil, node, nil // skip - case *sql2.Ident: - if node, err = v.visitIdent(n); err != nil { - return nil, nil, err - } - return nil, node, nil - case *sql2.QualifiedRef: - if node, err = v.visitQualifiedRef(n); err != nil { - return nil, nil, err - } - return nil, node, nil - default: - return v, node, nil - } -} - -func (v *sqlExprTypeChecker) visitIdent(ident *sql2.Ident) (sql2.Node, error) { - indexName, err := statementTableName(v.stmt) - if err != nil { - return nil, err - } - - // Convert to a table qualified reference and validate through ref visit function. - return v.visitQualifiedRef(&sql2.QualifiedRef{ - Table: &sql2.Ident{Name: indexName}, - Column: &sql2.Ident{Name: ident.Name}, - }) -} - -func (v *sqlExprTypeChecker) visitQualifiedRef(ref *sql2.QualifiedRef) (sql2.Node, error) { - idx := v.holder.Index(ref.Table.Name) - if idx == nil { - return nil, newNotFoundError(ErrIndexNotFound, ref.Table.Name) - } - - switch name := ref.Column.Name; name { - case "_id": - ref.DataType = sql2.DataTypeInt - default: - field := idx.Field(ref.Column.Name) - if field == nil { - return nil, newNotFoundError(ErrFieldNotFound, ref.Column.Name) - } - ref.DataType = fieldSQLDataType(field) - } - - return ref, nil -} - -func (v *sqlExprTypeChecker) checkExpr(node *sql2.Expr) error { - if expr, err := sql2.Walk(&sqlExprTypeChecker{ - holder: v.holder, - stmt: v.stmt, - }, *node); err != nil { - return err - } else if expr != nil { - *node = expr.(sql2.Expr) - } else { - *node = nil - } - return nil -} - -func (v *sqlExprTypeChecker) VisitEnd(node sql2.Node) (sql2.Node, error) { return node, nil } - -func fieldSQLDataType(f *Field) string { - if f.Keys() { - return sql2.DataTypeText - } - - switch f.Type() { - case FieldTypeInt, FieldTypeMutex, FieldTypeSet: - return sql2.DataTypeInt - case FieldTypeBool: - return sql2.DataTypeBool - case FieldTypeDecimal: - return sql2.DataTypeDecimal - case FieldTypeTime, FieldTypeTimestamp: - return sql2.DataTypeTimestamp - default: - return "" - } -} - -type Stmt struct { - node StmtNode -} - -func (stmt *Stmt) Close() error { return nil } - -func (stmt *Stmt) QueryRowContext(ctx context.Context, args ...interface{}) *StmtRow { - rows, err := stmt.QueryContext(ctx, args...) - if err != nil { - return &StmtRow{err: err} - } - return &StmtRow{rows: rows} -} - -func (stmt *Stmt) QueryContext(ctx context.Context, args ...interface{}) (*StmtRows, error) { - // TODO: Handle bind arguments. - - rows := &StmtRows{ - ctx: ctx, - node: stmt.node, - } - - // Initialize the node. - if err := rows.node.First(ctx); err != nil { - return nil, fmt.Errorf("Query: initialize statement: %w", err) - } - - return rows, nil -} - -type StmtRows struct { - ctx context.Context - node StmtNode - err error -} - -func (rs *StmtRows) Close() error { - return nil -} - -func (rs *StmtRows) Err() error { - if rs.err != nil && rs.err != sql.ErrNoRows { - return rs.err - } - return nil -} - -func (rs *StmtRows) Columns() []*StmtColumn { - return rs.node.Columns() -} - -/* -func (rs *StmtRows) Row() int64 { - return rs.node.Row()[0].(int64) -} -*/ - -func (rs *StmtRows) Next() bool { - if rs.err != nil { - return false - } - - if rs.err = rs.node.Next(rs.ctx); rs.err != nil { - return false - } - return true -} - -func (rs *StmtRows) Scan(dst ...interface{}) error { - if rs.err != nil { - return rs.err - } - - // Check len(dest) against node row length. - row := rs.node.Row() - if len(dst) != len(row) { - return fmt.Errorf("Scan(): expected %d values, received %d values", len(dst), len(row)) - } - - // Copy values from row to destination pointers. - for i := range dst { - // Handle null values. - // TODO: Handle double pointers. - if row[i] == nil { - switch p := dst[i].(type) { - case *int: - *p = 0 - case *int64: - *p = 0 - case *uint: - *p = 0 - case *uint64: - *p = 0 - case *interface{}: - *p = nil - default: - return fmt.Errorf("cannot scan NULL value into %T destination at index %d", p, i) - } - continue - } - - // Copy row value to scan destination. - switch v := row[i].(type) { - case bool: - switch p := dst[i].(type) { - case *bool: - *p = v - case *interface{}: - *p = v - default: - return fmt.Errorf("cannot scan %T value into %T destination at index %d", v, p, i) - } - case int64: - switch p := dst[i].(type) { - case *int: - *p = int(v) - case *int64: - *p = v - case *uint: - *p = uint(v) - case *uint64: - *p = uint64(v) - case *interface{}: - *p = v - default: - return fmt.Errorf("cannot scan %T value into %T destination at index %d", v, p, i) - } - case uint64: - switch p := dst[i].(type) { - case *int: - *p = int(v) - case *int64: - *p = int64(v) - case *uint: - *p = uint(v) - case *uint64: - *p = uint64(v) - case *interface{}: - *p = v - default: - return fmt.Errorf("cannot scan %T value into %T destination at index %d", v, p, i) - } - case []uint64: - switch p := dst[i].(type) { - case *[]uint64: - *p = []uint64(v) - case *interface{}: - *p = joinUint64Slice(v) - default: - return fmt.Errorf("cannot scan %T value into %T destination at index %d", v, p, i) - } - case string: - switch p := dst[i].(type) { - case *string: - *p = v - case *interface{}: - *p = v - default: - return fmt.Errorf("cannot scan %T value into %T destination at index %d", v, p, i) - } - case []string: - switch p := dst[i].(type) { - case *[]string: - *p = []string(v) - case *interface{}: - *p = strings.Join(v, ",") - default: - return fmt.Errorf("cannot scan %T value into %T destination at index %d", v, p, i) - } - default: - return fmt.Errorf("unexpected %T value at index %d", v, i) - } - } - - return nil -} - -type StmtRow struct { - err error - rows *StmtRows -} - -func (r *StmtRow) Scan(dest ...interface{}) error { - if r.err != nil { - return r.err - } - defer r.rows.Close() - - if !r.rows.Next() { - if err := r.rows.Err(); err != nil { - return err - } - return sql.ErrNoRows - } - - if err := r.rows.Scan(dest...); err != nil { - return err - } - return r.rows.Close() -} - -func (r *StmtRow) Err() error { - return r.err -} - -type StmtColumn struct { - Name string - Type string -} - -type StmtNode interface { - // Initializes the node to its start. - First(ctx context.Context) error - - // Moves the node to the next available row. Returns sql.ErrNoRows if done. - Next(ctx context.Context) error - - // Returns the current row in the node. - Row() []interface{} - - // Returns column definitions for the node. - Columns() []*StmtColumn - - // Returns a reference to the value register for a named column. - // Lookup(table, column string) (interface{}, error) -} - -var _ StmtNode = (*ExtractNode)(nil) - -// ExtractNode executes an Extract() query against a FeatureBase index. -type ExtractNode struct { - executor *executor - indexName string - srcs []string - columns []*StmtColumn - mapping []int // map of output column indices to source column indices - cond *pql.Call - - result []ExtractedTableColumn - row []interface{} -} - -func NewExtractNode(executor *executor, indexName string, srcs []string, columns []*StmtColumn, cond *pql.Call) *ExtractNode { - if cond == nil { - cond = &pql.Call{Name: "All"} - } - - // Determine mapping between result elements & columns. - // We'll exclude "_id" from the source columns here as well. - mapping := make([]int, len(columns)) - srcs2 := make([]string, 0, len(srcs)) - for i := range mapping { - if srcs[i] == "_id" { - mapping[i] = -1 - continue - } - - mapping[i] = len(srcs2) - srcs2 = append(srcs2, srcs[i]) - } - - return &ExtractNode{ - executor: executor, - indexName: indexName, - srcs: srcs2, // source column names (excluding "id") - columns: columns, // external column alias - mapping: mapping, - cond: cond, - row: make([]interface{}, len(srcs)), - } -} - -func (n *ExtractNode) Columns() []*StmtColumn { - return n.columns -} - -func (n *ExtractNode) First(ctx context.Context) error { - n.result = nil - return nil -} - -func (n *ExtractNode) Next(ctx context.Context) error { - // Fetch results if we haven't yet. - if err := n.init(ctx); err != nil { - return err - } - - // Exit if no result rows remain. - if len(n.result) == 0 { - for i := range n.row { - n.row[i] = nil - } - return sql.ErrNoRows - } - - // Map result elements to row elements. - for i, index := range n.mapping { - result := n.result[0] - - // Map row array element to position in result row. - if index >= 0 { - n.row[i] = result.Rows[index] - continue - } - - // Otherwise use ID for value. - if result.Column.Keyed { - n.row[i] = result.Column.Key - } else { - n.row[i] = int64(result.Column.ID) - } - } - - // Move to next result element. - n.result = n.result[1:] - - return nil -} - -func (n *ExtractNode) init(ctx context.Context) error { - if n.result != nil { - return nil - } - - // Generate PQL query with all specified rows. - // Skip first column as it is the ID column. - call := &pql.Call{Name: "Extract", Children: []*pql.Call{n.cond}} - for _, src := range n.srcs { - call.Children = append(call.Children, - &pql.Call{ - Name: "Rows", - Args: map[string]interface{}{"field": src}, - }, - ) - } - - // Execute Extract() against cluster. - result, err := n.executor.Execute(ctx, n.indexName, &pql.Query{Calls: []*pql.Call{call}}, nil, nil) - if err != nil { - return err - } else if result.Err != nil { - return result.Err - } else if len(result.Results) != 1 { - return fmt.Errorf("expected single result table from Extract(), got %d results", len(result.Results)) - } - - // Extract out the column/row data from resultset. - tbl, ok := result.Results[0].(ExtractedTable) - if !ok { - return fmt.Errorf("unexpected Extract() result type: %T", result.Results[0]) - } - n.result = tbl.Columns - - return nil -} - -func (n *ExtractNode) Row() []interface{} { return n.row } - -var _ StmtNode = (*CountNode)(nil) - -// CountNode executes a COUNT(*) against a FeatureBase index and returns a single row. -type CountNode struct { - executor *executor - indexName string - column *StmtColumn - call *pql.Call - - row []interface{} -} - -func NewCountNode(executor *executor, indexName string, column *StmtColumn, call *pql.Call) *CountNode { - return &CountNode{ - executor: executor, - indexName: indexName, - column: column, - call: call, - } -} - -func (n *CountNode) Columns() []*StmtColumn { - return []*StmtColumn{n.column} -} - -func (n *CountNode) First(ctx context.Context) error { - n.row = nil - return nil -} - -func (n *CountNode) Next(ctx context.Context) error { - if n.row != nil { - return sql.ErrNoRows - } - - result, err := n.executor.Execute(ctx, n.indexName, &pql.Query{Calls: []*pql.Call{n.call}}, nil, nil) - if err != nil { - return err - } - - n.row = []interface{}{int64(result.Results[0].(uint64))} - return nil -} - -func (n *CountNode) Row() []interface{} { return n.row } - -// GroupByNode executes an aggregate with a GROUP BY against a FeatureBase index. -type GroupByNode struct { - executor *executor - indexName string - groupByCols []string - columns []*StmtColumn - mapping []int - aggregate *pql.Call - cond *pql.Call - - result *GroupCounts - index int - - row []interface{} -} - -func NewGroupByNode(executor *executor, indexName string, resultCols, groupByCols []string, columns []*StmtColumn, aggregate, cond *pql.Call) *GroupByNode { - // Map result columns to output columns. - mapping := make([]int, len(columns)) - for i := range mapping { - if resultCols[i] == "_aggregate" { - mapping[i] = -1 - continue - } - - mapping[i] = stringSliceIndex(groupByCols, resultCols[i]) - } - - return &GroupByNode{ - executor: executor, - indexName: indexName, - groupByCols: groupByCols, - columns: columns, - mapping: mapping, - aggregate: aggregate, - cond: cond, - row: make([]interface{}, len(columns)), - } -} - -func (n *GroupByNode) Columns() []*StmtColumn { - return n.columns -} - -func (n *GroupByNode) First(ctx context.Context) error { - n.result = nil - return nil -} - -func (n *GroupByNode) Next(ctx context.Context) (err error) { - // Fetch resultset if it doesn't exist yet. - if n.result == nil { - if n.result, err = n.fetch(ctx); err != nil { - return err - } - } - - // Exit if no more rows exist. - if n.index >= len(n.result.groups) { - return sql.ErrNoRows - } - - // Copy results into current row. - group := n.result.groups[n.index] - n.index++ - - for i, index := range n.mapping { - // Assign aggregate to unmapped column. - if index == -1 { - if n.aggregate != nil { - n.row[i] = int64(group.Agg) - } else { - n.row[i] = int64(group.Count) - } - continue - } - - // Otherwise map from group value to result column index. - g := group.Group[index] - if g.Value != nil { - n.row[i] = *g.Value - } else if g.RowKey != "" { - n.row[i] = g.RowKey - } else { - n.row[i] = int64(g.RowID) - } - } - - return nil -} - -// fetch executes a call to compute the PQL results. -func (n *GroupByNode) fetch(ctx context.Context) (*GroupCounts, error) { - call := &pql.Call{ - Name: "GroupBy", - Args: map[string]interface{}{}, - } - - // Choose fields to group by. - for _, name := range n.groupByCols { - call.Children = append(call.Children, &pql.Call{ - Name: "Rows", Args: map[string]interface{}{"_field": name}, - }) - } - - // Apply filter & aggregate, if set. - if n.aggregate != nil { - call.Args["aggregate"] = n.aggregate - } - if n.cond != nil { - call.Args["filter"] = n.cond - } - - result, err := n.executor.Execute(ctx, n.indexName, &pql.Query{Calls: []*pql.Call{call}}, nil, nil) - if err != nil { - return nil, err - } - return result.Results[0].(*GroupCounts), nil -} - -func (n *GroupByNode) Row() []interface{} { return n.row } - -// statementTableName returns the table name for a single table SELECT statement. -// -// NOTE: This function is only temporary until we support more source types. -func statementTableName(stmt sql2.Statement) (string, error) { - switch stmt := stmt.(type) { - case *sql2.SelectStatement: - return sourceTableName(stmt.Source) - default: - return "", fmt.Errorf("statement not currently supported") - } -} - -func sourceTableName(source sql2.Source) (string, error) { - switch source := source.(type) { - case *sql2.JoinClause: - return "", fmt.Errorf("joins are not currently supported") - case *sql2.ParenSource: - return "", fmt.Errorf("parenthesized source is not currently supported") - case *sql2.QualifiedTableName: - return sql2.IdentName(source.Name), nil - case *sql2.SelectStatement: - return "", fmt.Errorf("sub-selects are not currently supported") - default: - return "", fmt.Errorf("unexpected source type: %T", source) - } -} - -// stringSliceIndex returns position of v in a. Returns -1 if not found. -func stringSliceIndex(a []string, v string) int { - for i := range a { - if a[i] == v { - return i - } - } - return -1 -} - -func joinUint64Slice(a []uint64) string { - b := []byte("[") - for i, v := range a { - b = strconv.AppendUint(b, v, 10) - if i < len(a)-1 { - b = append(b, ',') - } - } - b = append(b, ']') - return string(b) -} diff --git a/planner_test.go b/planner_test.go deleted file mode 100644 index 3ef87b87d..000000000 --- a/planner_test.go +++ /dev/null @@ -1,531 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package pilosa_test - -import ( - "context" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - pilosa "github.com/featurebasedb/featurebase/v3" - "github.com/featurebasedb/featurebase/v3/test" -) - -func TestPlanner_Count(t *testing.T) { - c := test.MustRunCluster(t, 1) - defer c.Close() - - index, err := c.GetHolder(0).CreateIndex("i", pilosa.IndexOptions{TrackExistence: true}) - if err != nil { - t.Fatal(err) - } - - if _, err := index.CreateField("f", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } else if _, err := index.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } - - // Populate with data. - if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ - Index: "i", - Query: ` - Set(1, f=10) - Set(2, f=10) - Set(3, f=11) - Set(4, f=12) - Set(5, f=12) - Set(6, f=13) - - Set(1, x=100) - Set(2, x=200) - `}); err != nil { - t.Fatal(err) - } - - t.Run("ALL", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 6; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - - t.Run("WHERE", func(t *testing.T) { - t.Run("EQ", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i WHERE f = 10` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 2; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - t.Run("NE", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i WHERE f != 10` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 4; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - t.Run("LT", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i WHERE f < 12` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 3; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - t.Run("GT", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i WHERE f > 12` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 1; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - - t.Run("AND", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i WHERE f = 10 AND x = 100` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 1; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - - t.Run("OR", func(t *testing.T) { - q := `SELECT COUNT(*) AS "count" FROM i WHERE f = 10 OR x = 200 OR f = 12` - stmt, err := c.GetNode(0).Server.PlanSQL(context.Background(), q) - if err != nil { - t.Fatal(err) - } - defer stmt.Close() - - var n int - if err := stmt.QueryRowContext(context.Background()).Scan(&n); err != nil { - t.Fatal(err) - } else if got, want := n, 4; got != want { - t.Fatalf("Scan()=%d, want %d", got, want) - } - }) - }) -} - -func TestPlanner_Select(t *testing.T) { - c := test.MustRunCluster(t, 1) - defer c.Close() - - i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) - if err != nil { - t.Fatal(err) - } - - if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } - - i1, err := c.GetHolder(0).CreateIndex("i1", pilosa.IndexOptions{TrackExistence: true}) - if err != nil { - t.Fatal(err) - } - - if _, err := i1.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } else if _, err := i1.CreateField("y", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } - - // Populate with data. - if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ - Index: "i0", - Query: ` - Set(1, a=10) - Set(1, b=100) - Set(2, a=20) - Set(2, b=200) - `}); err != nil { - t.Fatal(err) - } - - t.Run("UnqualifiedColumns", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT a, b, _id FROM i0`) - if diff := cmp.Diff([][]interface{}{ - {int64(10), int64(100), int64(1)}, - {int64(20), int64(200), int64(2)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "a", Type: "INT"}, - {Name: "b", Type: "INT"}, - {Name: "_id", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("QualifiedColumns", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT i0._id, i0.a, i0.b FROM i0`) - if diff := cmp.Diff([][]interface{}{ - {int64(1), int64(10), int64(100)}, - {int64(2), int64(20), int64(200)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "_id", Type: "INT"}, - {Name: "a", Type: "INT"}, - {Name: "b", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("UnqualifiedStar", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT * FROM i0`) - if diff := cmp.Diff([][]interface{}{ - {int64(1), int64(10), int64(100)}, - {int64(2), int64(20), int64(200)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "_id", Type: "INT"}, - {Name: "a", Type: "INT"}, - {Name: "b", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("QualifiedStar", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT i0.* FROM i0`) - if diff := cmp.Diff([][]interface{}{ - {int64(1), int64(10), int64(100)}, - {int64(2), int64(20), int64(200)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "_id", Type: "INT"}, - {Name: "a", Type: "INT"}, - {Name: "b", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("NoIdentifier", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT a, b FROM i0`) - if diff := cmp.Diff([][]interface{}{ - {int64(10), int64(100)}, - {int64(20), int64(200)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "a", Type: "INT"}, - {Name: "b", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("ErrFieldNotFound", func(t *testing.T) { - _, err := c.GetNode(0).Server.PlanSQL(context.Background(), `SELECT xyz FROM i0`) - if err == nil || !strings.Contains(err.Error(), `xyz: field not found`) { - t.Fatalf("unexpected error: %v", err) - } - }) -} - -func TestPlanner_GroupBy(t *testing.T) { - c := test.MustRunCluster(t, 1) - defer c.Close() - - i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) - if err != nil { - t.Fatal(err) - } - - if _, err := i0.CreateField("x"); err != nil { - t.Fatal(err) - } else if _, err := i0.CreateField("y", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } else if _, err := i0.CreateField("z", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } - - // Populate with data. - if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ - Index: "i0", - Query: ` - Set(1, x=10) - Set(1, x=20) - Set(1, y=100) - Set(1, z=500) - - Set(2, x=10) - Set(2, y=200) - Set(2, z=500) - - Set(3, x=20) - Set(3, z=600) - `}); err != nil { - t.Fatal(err) - } - - t.Run("Count", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*), x FROM i0 GROUP BY x`) - if diff := cmp.Diff([][]interface{}{ - {int64(2), int64(10)}, - {int64(2), int64(20)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "count", Type: "INT"}, - {Name: "x", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("DistinctCount", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(DISTINCT z), x FROM i0 GROUP BY x`) - if diff := cmp.Diff([][]interface{}{ - {int64(1), int64(10)}, - {int64(2), int64(20)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "count", Type: "INT"}, - {Name: "x", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("Sum", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT sum(y), x FROM i0 GROUP BY x`) - if diff := cmp.Diff([][]interface{}{ - {int64(300), int64(10)}, - {int64(100), int64(20)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "sum", Type: "INT"}, - {Name: "x", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("ReorderColumns", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT x, COUNT(*) FROM i0 GROUP BY x`) - if diff := cmp.Diff([][]interface{}{ - {int64(10), int64(2)}, - {int64(20), int64(2)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "x", Type: "INT"}, - {Name: "count", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("NoResultColumn", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 GROUP BY x`) - if diff := cmp.Diff([][]interface{}{ - {int64(2)}, - {int64(2)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "count", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) -} - -func TestPlanner_InnerJoin(t *testing.T) { - c := test.MustRunCluster(t, 1) - defer c.Close() - - i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) - if err != nil { - t.Fatal(err) - } - - if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } - - i1, err := c.GetHolder(0).CreateIndex("i1", pilosa.IndexOptions{TrackExistence: true}) - if err != nil { - t.Fatal(err) - } - - if _, err := i1.CreateField("parentid", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } else if _, err := i1.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { - t.Fatal(err) - } - - // Populate with data. - if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ - Index: "i0", - Query: ` - Set(1, a=10) - Set(2, a=20) - Set(3, a=30) - `}); err != nil { - t.Fatal(err) - } - - if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ - Index: "i1", - Query: ` - Set(1, parentid=1) - Set(1, x=100) - - Set(2, parentid=1) - Set(2, x=200) - - Set(3, parentid=2) - Set(3, x=300) - `}); err != nil { - t.Fatal(err) - } - - t.Run("Count", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid`) - if diff := cmp.Diff([][]interface{}{ - {int64(2)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "count", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) - - t.Run("CountWithParentCondition", func(t *testing.T) { - results, columns := mustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid WHERE i0.a = 10`) - if diff := cmp.Diff([][]interface{}{ - {int64(1)}, - }, results); diff != "" { - t.Fatal(diff) - } - - if diff := cmp.Diff([]*pilosa.StmtColumn{ - {Name: "count", Type: "INT"}, - }, columns); diff != "" { - t.Fatal(diff) - } - }) -} - -func mustQueryRows(tb testing.TB, svr *pilosa.Server, q string) (results [][]interface{}, columns []*pilosa.StmtColumn) { - tb.Helper() - - stmt, err := svr.PlanSQL(context.Background(), q) - if err != nil { - tb.Fatal(err) - } - defer stmt.Close() - - rows, err := stmt.QueryContext(context.Background()) - if err != nil { - tb.Fatal(err) - } - - results = make([][]interface{}, 0) - for rows.Next() { - result := make([]interface{}, len(rows.Columns())) - - // Create list of scan destination pointers. - dsts := make([]interface{}, len(result)) - for i := range result { - dsts[i] = &result[i] - } - - if err := rows.Scan(dsts...); err != nil { - tb.Fatal(err) - } - - results = append(results, result) - } - if err := rows.Err(); err != nil { - tb.Fatal(err) - } - - return results, rows.Columns() -} diff --git a/server.go b/server.go index 77ac2cafd..dc09cbb95 100644 --- a/server.go +++ b/server.go @@ -17,14 +17,16 @@ import ( uuid "github.com/satori/go.uuid" - "github.com/featurebasedb/featurebase/v3/disco" - "github.com/featurebasedb/featurebase/v3/logger" - pnet "github.com/featurebasedb/featurebase/v3/net" - rbfcfg "github.com/featurebasedb/featurebase/v3/rbf/cfg" - "github.com/featurebasedb/featurebase/v3/roaring" - "github.com/featurebasedb/featurebase/v3/sql2" - "github.com/featurebasedb/featurebase/v3/stats" - "github.com/featurebasedb/featurebase/v3/storage" + "github.com/molecula/featurebase/v3/disco" + "github.com/molecula/featurebase/v3/logger" + pnet "github.com/molecula/featurebase/v3/net" + rbfcfg "github.com/molecula/featurebase/v3/rbf/cfg" + "github.com/molecula/featurebase/v3/roaring" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + planner_types "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/molecula/featurebase/v3/stats" + "github.com/molecula/featurebase/v3/storage" "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -90,8 +92,12 @@ type Server struct { // nolint: maligned // Threshold for logging long-running queries longQueryTime time.Duration queryHistoryLength int + + executionPlannerFn ExecutionPlannerFn } +type ExecutionPlannerFn func(executor Executor, api *API, sql string) sql3.CompilePlanner + // Holder returns the holder for server. func (s *Server) Holder() *Holder { return s.holder @@ -424,6 +430,13 @@ func OptServerPartitionAssigner(p string) ServerOption { } } +func OptServerExecutionPlannerFn(fn ExecutionPlannerFn) ServerOption { + return func(s *Server) error { + s.executionPlannerFn = fn + return nil + } +} + // NewServer returns a new instance of Server. func NewServer(opts ...ServerOption) (*Server, error) { cluster := newCluster() @@ -454,6 +467,10 @@ func NewServer(opts ...ServerOption) (*Server, error) { resetTranslationSyncCh: make(chan struct{}, 1), logger: logger.NopLogger, + + executionPlannerFn: func(e Executor, a *API, s string) sql3.CompilePlanner { + return sql3.NewNopCompilePlanner() + }, } s.cluster.InternalClient = s.defaultClient @@ -1413,13 +1430,14 @@ func (srv *Server) GetTransaction(ctx context.Context, id string, remote bool) ( return trns, nil } -// PlanSQL parses and prepares a SQL statement. -func (s *Server) PlanSQL(ctx context.Context, q string) (*Stmt, error) { - st, err := sql2.NewParser(strings.NewReader(q)).ParseStatement() +// CompileExecutionPlan parses and compiles an execution plan from a SQL +// statement using a new parser and planner. +func (s *Server) CompileExecutionPlan(ctx context.Context, q string) (planner_types.PlanOperator, error) { + st, err := parser.NewParser(strings.NewReader(q)).ParseStatement() if err != nil { return nil, err } - return NewPlanner(s.executor).PlanStatement(ctx, st) + return s.executionPlannerFn(s.executor, s.executor.client.api, q).CompilePlan(ctx, st) } // countOpenFiles on operating systems that support lsof. diff --git a/server/config.go b/server/config.go index 8505babde..fc90d31f9 100644 --- a/server/config.go +++ b/server/config.go @@ -177,27 +177,10 @@ type Config struct { MutexFraction int `toml:"mutex-fraction"` } `toml:"profile"` - Postgres struct { - // Bind is the address to which to bind a postgres endpoint. - // If this is empty, no endpoint will be created. - Bind string `toml:"bind"` - // TLS configuration for postgres connections. - TLS TLSConfig `toml:"tls"` - - StartupTimeout toml.Duration `toml:"startup-timeout"` - ReadTimeout toml.Duration `toml:"read-timeout"` - WriteTimeout toml.Duration `toml:"write-timout"` - - MaxStartupSize uint32 `toml:"max-startup-size"` - - // ConnectionLimit is the maximum number of postgres connections to allow simultaneously. - // Setting this to 0 disables the limit. - // This mostly exists because other DBs seem to have it. - ConnectionLimit uint16 `toml:"max-connections"` - // SqlVersion is which type of sqlhandling to be applied. - // The constant SqlV2 can be used to try the new experimental Molecula SQL handling - SqlVersion uint16 `toml:"sql-version"` - } `toml:"postgres"` + SQL struct { + // EndpointEnabled enables the /sql endpoint. + EndpointEnabled bool `toml:"endpoint-enabled"` + } `toml:"sql"` // Storage.Backend determines which Tx implementation the holder/Index will // use; one of the available transactional-storage engines. Choices are @@ -287,7 +270,6 @@ func (c *Config) validate() error { "Etcd.LPeerURL", c.Etcd.LPeerURL, // ":" "Etcd.APeerURL", c.Etcd.APeerURL, // "" "Etcd.ClusterURL", c.Etcd.ClusterURL, - "Postgres.Bind", c.Postgres.Bind, } ports := make(map[int]bool) @@ -383,12 +365,7 @@ func NewConfig() *Config { c.Profile.BlockRate = 10000000 // 1 sample per 10 ms c.Profile.MutexFraction = 100 // 1% sampling - // Postgres config (off by default). - c.Postgres.MaxStartupSize = 8 * 1024 * 1024 - c.Postgres.StartupTimeout = toml.Duration(5 * time.Second) - c.Postgres.ReadTimeout = toml.Duration(10 * time.Second) - c.Postgres.WriteTimeout = toml.Duration(10 * time.Second) - // we don't really need a connection limit + c.SQL.EndpointEnabled = false c.Etcd.AClientURL = "" c.Etcd.LClientURL = "http://localhost:10301" diff --git a/server/pg.go b/server/pg.go deleted file mode 100644 index e30ce083b..000000000 --- a/server/pg.go +++ /dev/null @@ -1,665 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package server - -import ( - "context" - "crypto/rand" - "crypto/tls" - "encoding/json" - "fmt" - "net" - "strconv" - "strings" - "time" - - pilosa "github.com/featurebasedb/featurebase/v3" - "github.com/featurebasedb/featurebase/v3/logger" - "github.com/featurebasedb/featurebase/v3/pg" - "github.com/featurebasedb/featurebase/v3/sql2" - - //"github.com/featurebasedb/featurebase/v3/pg" - "github.com/featurebasedb/featurebase/v3/pql" - pb "github.com/featurebasedb/featurebase/v3/proto" - - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" -) - -// PostgresServer provides a postgres endpoint on pilosa. -type PostgresServer struct { - api *pilosa.API - logger logger.Logger - eg errgroup.Group - s pg.Server - stop context.CancelFunc -} -type SqlVersion uint16 - -const ( - SqlV1 SqlVersion = 0 - SqlV2 SqlVersion = 2 -) - -// NewPostgresServer creates a postgres server. -func NewPostgresServer(api *pilosa.API, logger logger.Logger, tls *tls.Config, sqlVersion SqlVersion) *PostgresServer { - return &PostgresServer{ - api: api, - logger: logger, - s: pg.Server{ - QueryHandler: NewPostgresHandler(api, logger, sqlVersion), - TypeEngine: pg.PrimitiveTypeEngine{}, - StartupTimeout: 5 * time.Second, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxStartupSize: 8 * 1024 * 1024, - Logger: logger, - TLSConfig: tls, - - // This is somewhat limited right now: it does not work with load balancers. - CancellationManager: pg.NewLocalCancellationManager(rand.Reader), - }, - } -} - -// NewPostgresHandler creates a postgres query handler wrapping the pilosa API. -func NewPostgresHandler(api *pilosa.API, logger logger.Logger, sqlVersion SqlVersion) pg.QueryHandler { - return &QueryDecodeHandler{ - Child: &PilosaQueryHandler{ - Api: api, - logger: logger, - sqlVersion: sqlVersion, - }, - } -} - -// Start a postgres endpoint at the specified address. -func (s *PostgresServer) Start(addr string) error { - l, err := net.Listen("tcp", addr) - if err != nil { - return errors.Wrap(err, "creating listener") - } - - s.logger.Infof("serving postgres wire protocol on %s", l.Addr()) - - ctx, cancel := context.WithCancel(context.Background()) - s.stop = cancel - - s.eg.Go(func() error { return s.s.Serve(ctx, l) }) - - return nil -} - -func (s *PostgresServer) Close() error { - if s == nil { - return nil - } - - if s.stop == nil { - return nil - } - s.stop() - - s.logger.Infof("waiting for postgres connections to shut down") - - return s.eg.Wait() -} - -func (s *PostgresServer) GetAPI() *pilosa.API { - return s.api -} - -type pgPQLQuery struct { - index string - query string -} - -func (q pgPQLQuery) String() string { - return fmt.Sprintf("[%s]%s", q.index, q.query) -} - -func pgDecodePQL(str string) (q pg.Query, err error) { - defer func() { - err = errors.Wrap(err, "not a valid PQL-over-postgres query") - }() - - if !strings.HasPrefix(str, "[") { - return nil, errors.New("missing index specification") - } - idx := strings.IndexRune(str, ']') - if idx == -1 { - return nil, errors.New("unclosed bracket in index specification") - } - - return pgPQLQuery{ - index: str[1:idx], - query: str[idx+1:], - }, nil -} - -type PilosaQueryHandler struct { - Api *pilosa.API - logger logger.Logger - sqlVersion SqlVersion -} - -func pgWriteDistinctTimestamp(w pg.QueryResultWriter, val pilosa.DistinctTimestamp) error { - err := w.WriteHeader(pg.ColumnInfo{ - Name: val.Name, - Type: pg.TypeCharoid, - }) - if err != nil { - return errors.Wrap(err, "writing result header") - } - - for _, k := range val.Values { - err = w.WriteRowText(k) - if err != nil { - return errors.Wrap(err, "writing key") - } - } - return nil -} - -func pgWriteRow(w pg.QueryResultWriter, row *pilosa.Row) error { - err := w.WriteHeader(pg.ColumnInfo{ - Name: "_id", - Type: pg.TypeCharoid, - }) - if err != nil { - return errors.Wrap(err, "writing result header") - } - - if row.Keys != nil { - for _, k := range row.Keys { - err = w.WriteRowText(k) - if err != nil { - return errors.Wrap(err, "writing key") - } - } - } else { - for _, col := range row.Columns() { - err = w.WriteRowText(strconv.FormatUint(col, 10)) - if err != nil { - return errors.Wrap(err, "writing column ID") - } - } - } - - return nil -} - -func pgWriteRows(w pg.QueryResultWriter, rows pilosa.RowIdentifiers) error { - err := w.WriteHeader(pg.ColumnInfo{ - Name: rows.Field(), - Type: pg.TypeCharoid, - }) - if err != nil { - return errors.Wrap(err, "writing result header") - } - - if rows.Keys != nil { - for _, k := range rows.Keys { - err = w.WriteRowText(k) - if err != nil { - return errors.Wrap(err, "writing key") - } - } - } else { - for _, row := range rows.Rows { - err = w.WriteRowText(strconv.FormatUint(row, 10)) - if err != nil { - return errors.Wrap(err, "writing row ID") - } - } - } - - return nil -} - -func pgFormatVal(val interface{}) string { - switch val := val.(type) { - case bool: - return strconv.FormatBool(val) - case int64: - return strconv.FormatInt(val, 10) - case uint64: - return strconv.FormatUint(val, 10) - case string: - return val - case pql.Decimal: - return val.String() - default: - data, _ := json.Marshal(val) - return string(data) - } -} - -func pgWriteExtractedTable(w pg.QueryResultWriter, tbl pilosa.ExtractedTable) error { - headers := make([]pg.ColumnInfo, len(tbl.Fields)+1) - headers[0] = pg.ColumnInfo{ - Name: "_id", - Type: pg.TypeCharoid, - } - dataHeaders := headers[1:] - for i, f := range tbl.Fields { - dataHeaders[i] = pg.ColumnInfo{ - Name: f.Name, - Type: pg.TypeCharoid, - } - } - err := w.WriteHeader(headers...) - if err != nil { - return errors.Wrap(err, "writing result header") - } - - vals := make([]string, len(headers)) - dataVals := vals[1:] - for _, col := range tbl.Columns { - if col.Column.Keyed { - vals[0] = col.Column.Key - } else { - vals[0] = strconv.FormatUint(col.Column.ID, 10) - } - for i, v := range col.Rows { - dataVals[i] = pgFormatVal(v) - } - err = w.WriteRowText(vals...) - if err != nil { - return errors.Wrap(err, "writing result row") - } - } - - return nil -} - -func pgWriteGroupCount(w pg.QueryResultWriter, counts *pilosa.GroupCounts) error { - groups := counts.Groups() - if len(groups) == 0 { - // Not enough information is available to construct the header. - // This is a significant flaw in the data type. - return nil - } - expectedLen := len(groups[0].Group) + 1 - - agg := counts.AggregateColumn() - if agg != "" { - expectedLen++ - } - - headers := make([]pg.ColumnInfo, expectedLen) - for i, g := range groups[0].Group { - headers[i] = pg.ColumnInfo{ - Name: g.Field, - Type: pg.TypeCharoid, - } - } - next := len(groups[0].Group) - headers[next] = pg.ColumnInfo{ - Name: "count", - Type: pg.TypeCharoid, - } - if agg != "" { - next++ - headers[next] = pg.ColumnInfo{ - Name: agg, - Type: pg.TypeCharoid, - } - } - err := w.WriteHeader(headers...) - if err != nil { - return errors.Wrap(err, "writing result header") - } - - vals := make([]string, len(headers)) - for _, gc := range groups { - var j int - var g pilosa.FieldRow - for j, g = range gc.Group { - var v string - switch { - case g.Value != nil: - if g.FieldOptions.Type == pilosa.FieldTypeTimestamp { - ts, err := pilosa.ValToTimestamp(g.FieldOptions.TimeUnit, int64(*g.Value)+g.FieldOptions.Base) - if err != nil { - return errors.Wrap(err, "translating val to timestamp") - } - - v = ts.Format(time.RFC3339Nano) - } else { - v = strconv.FormatInt(*g.Value, 10) - } - case g.RowKey != "": - v = g.RowKey - default: - v = strconv.FormatUint(g.RowID, 10) - } - vals[j] = v - } - j++ - vals[j] = strconv.FormatUint(gc.Count, 10) - if agg != "" { - j++ - vals[j] = strconv.FormatInt(gc.Agg, 10) - } - - err := w.WriteRowText(vals...) - if err != nil { - return errors.Wrap(err, "writing group count result") - } - } - - return nil -} - -// TODO(twg) move this to a better area -func getPgType(sql2type string) pg.Type { - ret := pg.TypeCharoid - switch sql2type { - case sql2.DataTypeInt: - ret = pg.TypeINT4OID - } - return ret -} -func pgWriteStmtRows(w pg.QueryResultWriter, rows *pilosa.StmtRows) error { - //TODO(twg) writeHeader - //TODO(twg) writeColumns - first := true - var data []string - var err error - for rows.Next() { - if first { - columns := rows.Columns() - //TODO (twg) types:=rows.Types() - headers := make([]pg.ColumnInfo, len(columns)) - for i, column := range columns { - pgType := getPgType(column.Type) - headers[i] = pg.ColumnInfo{ - Name: column.Name, - Type: pgType, - } - } - err := w.WriteHeader(headers...) - if err != nil { - return err - } - - data = make([]string, len(headers)) - first = false - } - result := make([]interface{}, len(rows.Columns())) - // Create list of scan destination pointers. - dsts := make([]interface{}, len(result)) - for i := range result { - dsts[i] = &result[i] - } - - if err := rows.Scan(dsts...); err != nil { - return err - } - //TODO(twg) conversion should be happening in Scan as described in https://pkg.go.dev/database/sql - // .... - // Scan also converts between string and numeric types, as long as no information - // would be lost. While Scan stringifies all numbers scanned from numeric database columns into *string, - // scans into numeric types are checked for overflow. For example, a float64 with value 300 or a string - // with value "300" can scan into a uint16, but not into a uint8, though float64(255) or "255" can scan - // into a uint8. One exception is that scans of some float64 numbers to strings may lose information when stringifying. - // In general, scan floating point columns into *float64. - // ... - for i, col := range dsts { - var v string - switch col := col.(type) { - case nil: - v = "null" - // - //v = strconv.FormatUint(col.Uint64Val, 10) - case *interface{}: - v = fmt.Sprintf("%v", *col) - default: - return errors.Errorf("unable to process value of type %T", col) - } - - data[i] = v - } - - err = w.WriteRowText(data...) - if err != nil { - return err - } - - } - if err := rows.Err(); err != nil { - return err - } - return nil -} -func getPgTypeFromColumnInfo(sql2type string) pg.Type { - switch sql2type { - case sql2.DataTypeInt: - return pg.TypeINT4OID - default: - return pg.TypeCharoid - - } -} - -var _ = getPgTypeFromColumnInfo //make linter happy for this function will be needed in future - -func pgWriteRowser(w pg.QueryResultWriter, result pb.ToRowser) error { - var data []string - return result.ToRows(func(row *pb.RowResponse) error { - if data == nil { - headers := make([]pg.ColumnInfo, len(row.Columns)) - for i, h := range row.Headers { - headers[i] = pg.ColumnInfo{ - Name: h.Name, - Type: pg.TypeCharoid, // TODO(twg) this needs to be updated with type - // information so it works from psql client - } - } - err := w.WriteHeader(headers...) - if err != nil { - return errors.Wrap(err, "writing headers") - } - - data = make([]string, len(headers)) - } - - for i, col := range row.Columns { - var v string - switch col := col.ColumnVal.(type) { - case nil: - v = "null" - case *pb.ColumnResponse_BoolVal: - v = strconv.FormatBool(col.BoolVal) - case *pb.ColumnResponse_DecimalVal: - v = pql.NewDecimal( - col.DecimalVal.Value, - col.DecimalVal.Scale, - ).String() - case *pb.ColumnResponse_Float64Val: - v = strconv.FormatFloat(col.Float64Val, 'g', -1, 64) - case *pb.ColumnResponse_Int64Val: - v = strconv.FormatInt(col.Int64Val, 10) - case *pb.ColumnResponse_Uint64Val: - v = strconv.FormatUint(col.Uint64Val, 10) - case *pb.ColumnResponse_StringVal: - v = col.StringVal - case *pb.ColumnResponse_StringArrayVal: - data, _ := json.Marshal(col.StringArrayVal.Vals) - v = string(data) - case *pb.ColumnResponse_Uint64ArrayVal: - data, _ := json.Marshal(col.Uint64ArrayVal.Vals) - v = string(data) - case *pb.ColumnResponse_TimestampVal: - v = col.TimestampVal - default: - return errors.Errorf("unable to process value of type %T", col) - } - - data[i] = v - } - - return w.WriteRowText(data...) - }) -} - -func pgWriteResult(w pg.QueryResultWriter, result interface{}) error { - switch result := result.(type) { - case *pilosa.Row: - return pgWriteRow(w, result) - case pilosa.RowIdentifiers: - return pgWriteRows(w, result) - case pilosa.ExtractedTable: - return pgWriteExtractedTable(w, result) - case []pilosa.GroupCount: - gc := pilosa.NewGroupCounts("", result...) - return pgWriteGroupCount(w, gc) - case *pilosa.GroupCounts: - return pgWriteGroupCount(w, result) - case pb.ToRowser: // we should avoid protobuf where we can... - return pgWriteRowser(w, result) - case *pilosa.StmtRows: - return pgWriteStmtRows(w, result) - case uint64: - err := w.WriteHeader(pg.ColumnInfo{ - Name: "count", - Type: pg.TypeCharoid, - }) - if err != nil { - return errors.Wrap(err, "writing headers") - } - - err = w.WriteRowText(strconv.FormatUint(result, 10)) - if err != nil { - return errors.Wrap(err, "writing count") - } - - return nil - case int64: - err := w.WriteHeader(pg.ColumnInfo{ - Name: "value", - Type: pg.TypeCharoid, - }) - if err != nil { - return errors.Wrap(err, "writing headers") - } - - err = w.WriteRowText(strconv.FormatInt(result, 10)) - if err != nil { - return errors.Wrap(err, "writing count") - } - - return nil - case bool: - err := w.WriteHeader(pg.ColumnInfo{ - Name: "result", - Type: pg.TypeCharoid, - }) - if err != nil { - return errors.Wrap(err, "writing headers") - } - - err = w.WriteRowText(strconv.FormatBool(result)) - if err != nil { - return errors.Wrap(err, "writing count") - } - - return nil - case pilosa.DistinctTimestamp: - return pgWriteDistinctTimestamp(w, result) - case nil: - return nil - - default: - return errors.Errorf("result type %T not yet supported", result) - } -} - -func (pqh *PilosaQueryHandler) HandleQuery(ctx context.Context, w pg.QueryResultWriter, q pg.Query) error { - switch q := q.(type) { - case pgPQLQuery: - resp, err := pqh.Api.Query(ctx, &pilosa.QueryRequest{ - Index: q.index, - Query: q.query, - }) - if err != nil { - return errors.Wrap(err, "executing query") - } - if len(resp.Results) != 1 { - return errors.Errorf("expected 1 query result but found %d", len(resp.Results)) - } - return errors.Wrap(pgWriteResult(w, resp.Results[0]), "writing query result") - - case pg.SimpleQuery: - if pqh.sqlVersion == SqlV2 { - stmt, err := pqh.Api.Plan(ctx, string(q)) - if err != nil { - return err - } - resp, err := stmt.QueryContext(ctx) - if err != nil { - return err - } - return errors.Wrap(pgWriteResult(w, resp), "writing sql2 query result") - //version 2.0 - } else { - //version 1.0 - resp, err := execSQL(ctx, pqh.Api, pqh.logger, string(q)) - if err != nil { - return errors.Wrap(err, "executing query") - } - return errors.Wrap(pgWriteResult(w, resp), "writing query result") - } - - default: - return errors.Errorf("query type %T not yet supported (query: %s)", q, q) - } -} - -type QueryDecodeHandler struct { - Child pg.QueryHandler -} - -func (qdh *QueryDecodeHandler) HandleQuery(ctx context.Context, w pg.QueryResultWriter, q pg.Query) error { - switch qv := q.(type) { - case pg.SimpleQuery: - if strings.HasPrefix(string(qv), "[") { - pqlQuery, err := pgDecodePQL(strings.TrimSuffix(string(qv), ";")) - if err != nil { - return errors.Wrap(err, "decoding query") - } - q = pqlQuery - } - } - - return qdh.Child.HandleQuery(ctx, w, q) -} -func (qdh *QueryDecodeHandler) Version() string { - return qdh.Child.Version() -} - -func (pqh *PilosaQueryHandler) Version() string { - if pqh.sqlVersion > 0 { - return "v2" - } - return "v1" -} -func (pqh *PilosaQueryHandler) HandleSchema(ctx context.Context, portal *pg.Portal) error { - schema, err := pqh.Api.Schema(context.Background(), false) - if err != nil { - return err - } - for _, ii := range schema { - dataRow, err := portal.Encoder.TextRow("featurebase", ii.Name) - if err != nil { - return err - } - portal.Add(dataRow) - } - return nil -} - -func (qdh *QueryDecodeHandler) HandleSchema(ctx context.Context, portal *pg.Portal) error { - return qdh.Child.HandleSchema(ctx, portal) -} diff --git a/server/pg_internal_test.go b/server/pg_internal_test.go deleted file mode 100644 index 9ae63e353..000000000 --- a/server/pg_internal_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package server - -import ( - "testing" - - pilosa "github.com/featurebasedb/featurebase/v3" - "github.com/featurebasedb/featurebase/v3/pg" - "github.com/stretchr/testify/assert" -) - -// pg_internal_test.go tests unexported methods from server/pg.go - -// TestQueryResultWriter implements the QueryResultWriter interface for testing -type TestQueryResultWriter struct { - Header []pg.ColumnInfo - RowText []string - TagTag string -} - -func (t *TestQueryResultWriter) WriteHeader(headers ...pg.ColumnInfo) error { - t.Header = append(t.Header, headers...) - return nil -} - -func (t *TestQueryResultWriter) WriteRowText(rowTexts ...string) error { - t.RowText = append(t.RowText, rowTexts...) - return nil -} - -func (t *TestQueryResultWriter) Tag(tag string) { - t.TagTag = tag -} - -func TestPgWriteDistinctTimestamp(t *testing.T) { - w := TestQueryResultWriter{} - expected := pilosa.DistinctTimestamp{Name: "test", Values: []string{"date1", "date2", "date3"}} - err := pgWriteDistinctTimestamp(&w, expected) - assert.NoError(t, err) - - if w.Header[0].Name != expected.Name { - t.Fatalf("Header Name is wrong. got %v, want %v", w.Header[0], expected.Name) - } - if w.Header[0].Type != pg.TypeCharoid { - t.Fatalf("Header Type is wrong. got %v, want %v", w.Header[0].Type, pg.TypeCharoid) - } - for i, value := range w.RowText { - if value != expected.Values[i] { - t.Fatalf("Value not written properly. got %v, want %v", value, expected.Values[i]) - } - } - -} diff --git a/server/pg_test.go b/server/pg_test.go deleted file mode 100644 index ee75def2a..000000000 --- a/server/pg_test.go +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package server_test - -import ( - "context" - "math" - "reflect" - "testing" - "time" - - pilosa "github.com/featurebasedb/featurebase/v3" - "github.com/featurebasedb/featurebase/v3/logger" - "github.com/featurebasedb/featurebase/v3/pg" - "github.com/featurebasedb/featurebase/v3/pg/pgtest" - "github.com/featurebasedb/featurebase/v3/server" - "github.com/featurebasedb/featurebase/v3/test" -) - -func TestPostgresHandler(t *testing.T) { - m := test.RunCommand(t) - defer m.Close() - - pgh := server.NewPostgresHandler(m.API, logger.NewLogfLogger(t), server.SqlV1) - - m.MustCreateIndex(t, "i", pilosa.IndexOptions{TrackExistence: true}) - m.MustCreateField(t, "i", "set") - m.MustCreateField(t, "i", "keyset", pilosa.OptFieldKeys()) - m.MustCreateField(t, "i", "mutex", pilosa.OptFieldTypeMutex(pilosa.CacheTypeNone, 0)) - m.MustCreateField(t, "i", "keymutex", pilosa.OptFieldKeys(), pilosa.OptFieldTypeMutex(pilosa.CacheTypeNone, 0)) - m.MustCreateField(t, "i", "int", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) - m.MustCreateField(t, "i", "decimal", pilosa.OptFieldTypeDecimal(2)) - m.MustCreateField(t, "i", "time", pilosa.OptFieldTypeTime("YMDH", "0")) - m.MustCreateField(t, "i", "bool", pilosa.OptFieldTypeBool()) - - m.MustCreateIndex(t, "j", pilosa.IndexOptions{TrackExistence: true, Keys: true}) - m.MustCreateField(t, "j", "set") - - storeOK := pgtest.ResultSet{ - Columns: []pg.ColumnInfo{ - { - Name: "result", - Type: pg.TypeCharoid, - }, - }, - Data: [][]string{ - { - "true", - }, - }, - } - - cases := []struct { - Name string - Queries []string - Results []pgtest.ResultSet - }{ - { - Name: "Extract-Nothing", - Queries: []string{ - `[i]Extract(All(), Rows(set))`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - { - Name: "_id", - Type: pg.TypeCharoid, - }, - { - Name: "set", - Type: pg.TypeCharoid, - }, - }, - }, - }, - }, - { - Name: "Store", - Queries: []string{ - `[i]Store(ConstRow(columns=[1, 2, 3]), set=4)`, - `[i]Store(ConstRow(columns=[0, 2, 4]), set=5)`, - `[j]Store(ConstRow(columns=[1, 2, 3]), set=4)`, - `[j]Store(ConstRow(columns=[0, 2, 4]), set=5)`, - }, - Results: []pgtest.ResultSet{ - storeOK, - storeOK, - storeOK, - storeOK, - }, - }, - { - Name: "Set", - Queries: []string{ - `[i]Set(1, keyset="a")`, - `[i]Set(2, keyset="b")`, - `[i]Set(3, mutex=3)`, - `[i]Set(4, keymutex="d")`, - `[i]Set(1, int=5)`, - `[i]Set(2, decimal=6.01)`, - `[i]Set(3, time=7, 2016-01-01T00:00)`, - `[i]Set(4, bool=false)`, - }, - Results: []pgtest.ResultSet{ - storeOK, - storeOK, - storeOK, - storeOK, - storeOK, - storeOK, - storeOK, - storeOK, - }, - }, - { - Name: "Extract", - Queries: []string{ - `[i]Extract( - All(), - Rows(set), Rows(keyset), - Rows(mutex), Rows(keymutex), - Rows(int), Rows(decimal), - Rows(time), - Rows(bool) - )`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "_id", Type: pg.TypeCharoid}, - {Name: "set", Type: pg.TypeCharoid}, - {Name: "keyset", Type: pg.TypeCharoid}, - {Name: "mutex", Type: pg.TypeCharoid}, - {Name: "keymutex", Type: pg.TypeCharoid}, - {Name: "int", Type: pg.TypeCharoid}, - {Name: "decimal", Type: pg.TypeCharoid}, - {Name: "time", Type: pg.TypeCharoid}, - {Name: "bool", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {`1`, `[4]`, `["a"]`, `null`, `null`, `5`, `null`, `[]`, `null`}, - {`2`, `[4,5]`, `["b"]`, `null`, `null`, `null`, `6.01`, `[]`, `null`}, - {`3`, `[4]`, `[]`, `3`, `null`, `null`, `null`, `[7]`, `null`}, - {`4`, `[5]`, `[]`, `null`, `d`, `null`, `null`, `[]`, `false`}, - }, - }, - }, - }, - { - Name: "GroupBy", - Queries: []string{ - `[i]GroupBy(Rows(set))`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "set", Type: pg.TypeCharoid}, - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"4", "3"}, - {"5", "3"}, - }, - }, - }, - }, - { - Name: "Count", - Queries: []string{ - `[i]Count(Row(set=4))`, - `[i]Count(Row(int > 0))`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{{"3"}}, - }, - { - Columns: []pg.ColumnInfo{ - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{{"1"}}, - }, - }, - }, - { - Name: "FieldValue", - Queries: []string{ - `[i]FieldValue(field=int, column=1)`, - `[i]FieldValue(field=decimal, column=2)`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "value", Type: pg.TypeCharoid}, - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"5", "1"}, - }, - }, - { - Columns: []pg.ColumnInfo{ - {Name: "value", Type: pg.TypeCharoid}, - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"6.01", "1"}, - }, - }, - }, - }, - { - Name: "Rows", - Queries: []string{ - `[i]Rows(set)`, - `[i]Rows(keyset)`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "set", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"4"}, - {"5"}, - }, - }, - { - Columns: []pg.ColumnInfo{ - {Name: "keyset", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"a"}, - {"b"}, - }, - }, - }, - }, - { - Name: "TopN", - Queries: []string{ - `[i]TopN(set)`, - `[i]TopN(keyset)`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "set", Type: pg.TypeCharoid}, - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"5", "3"}, - {"4", "3"}, - }, - }, - { - Columns: []pg.ColumnInfo{ - {Name: "keyset", Type: pg.TypeCharoid}, - {Name: "count", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {"b", "1"}, - {"a", "1"}, - }, - }, - }, - }, - { - Name: "SQL", - Queries: []string{ - `select _id from i;`, - }, - Results: []pgtest.ResultSet{ - { - Columns: []pg.ColumnInfo{ - {Name: "_id", Type: pg.TypeCharoid}, - }, - Data: [][]string{ - {`1`}, - {`2`}, - {`3`}, - {`4`}, - }, - }, - }, - }, - } - - for _, c := range cases { - c := c - t.Run(c.Name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - for i, q := range c.Queries { - var res pgtest.ResultSet - err := pgh.HandleQuery(ctx, &res, pg.SimpleQuery(q)) - if err != nil { - t.Errorf("query %q failed: %v", q, err) - continue - } - - expected := c.Results[i] - if !reflect.DeepEqual(res, expected) { - t.Errorf("query %q returned incorrect results: expected %v but got %v", q, expected, res) - } - } - }) - } -} diff --git a/server/server.go b/server/server.go index 5bd40c238..6604398f7 100644 --- a/server/server.go +++ b/server/server.go @@ -28,22 +28,24 @@ import ( "golang.org/x/sync/errgroup" - pilosa "github.com/featurebasedb/featurebase/v3" - "github.com/featurebasedb/featurebase/v3/authn" - "github.com/featurebasedb/featurebase/v3/authz" - "github.com/featurebasedb/featurebase/v3/boltdb" - "github.com/featurebasedb/featurebase/v3/encoding/proto" - petcd "github.com/featurebasedb/featurebase/v3/etcd" - "github.com/featurebasedb/featurebase/v3/gcnotify" - "github.com/featurebasedb/featurebase/v3/gopsutil" - "github.com/featurebasedb/featurebase/v3/logger" - pnet "github.com/featurebasedb/featurebase/v3/net" - "github.com/featurebasedb/featurebase/v3/prometheus" - "github.com/featurebasedb/featurebase/v3/statik" - "github.com/featurebasedb/featurebase/v3/stats" - "github.com/featurebasedb/featurebase/v3/statsd" - "github.com/featurebasedb/featurebase/v3/syswrap" - "github.com/featurebasedb/featurebase/v3/testhook" + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/authn" + "github.com/molecula/featurebase/v3/authz" + "github.com/molecula/featurebase/v3/boltdb" + "github.com/molecula/featurebase/v3/encoding/proto" + petcd "github.com/molecula/featurebase/v3/etcd" + "github.com/molecula/featurebase/v3/gcnotify" + "github.com/molecula/featurebase/v3/gopsutil" + "github.com/molecula/featurebase/v3/logger" + pnet "github.com/molecula/featurebase/v3/net" + "github.com/molecula/featurebase/v3/prometheus" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner" + "github.com/molecula/featurebase/v3/statik" + "github.com/molecula/featurebase/v3/stats" + "github.com/molecula/featurebase/v3/statsd" + "github.com/molecula/featurebase/v3/syswrap" + "github.com/molecula/featurebase/v3/testhook" "github.com/pelletier/go-toml" "github.com/pkg/errors" ) @@ -81,7 +83,6 @@ type Command struct { listenURI *pnet.URI tlsConfig *tls.Config closeTimeout time.Duration - pgserver *PostgresServer serverOptions []pilosa.ServerOption auth *authn.Auth @@ -248,29 +249,6 @@ func (m *Command) Start() (err error) { } }() - // Initialize postgres. - m.pgserver = nil - if m.Config.Postgres.Bind != "" { - var tlsConf *tls.Config - if m.Config.Postgres.TLS.CertificatePath != "" { - conf, err := GetTLSConfig(&m.Config.Postgres.TLS, m.logger) - if err != nil { - return errors.Wrap(err, "setting up postgres TLS") - } - tlsConf = conf - } - m.pgserver = NewPostgresServer(m.API, m.logger, tlsConf, SqlVersion(m.Config.Postgres.SqlVersion)) - m.pgserver.s.StartupTimeout = time.Duration(m.Config.Postgres.StartupTimeout) - m.pgserver.s.ReadTimeout = time.Duration(m.Config.Postgres.ReadTimeout) - m.pgserver.s.WriteTimeout = time.Duration(m.Config.Postgres.WriteTimeout) - m.pgserver.s.MaxStartupSize = m.Config.Postgres.MaxStartupSize - m.pgserver.s.ConnectionLimit = m.Config.Postgres.ConnectionLimit - err := m.pgserver.Start(m.Config.Postgres.Bind) - if err != nil { - return errors.Wrap(err, "starting postgres") - } - } - _ = testhook.Opened(pilosa.NewAuditor(), m, nil) close(m.Started) return nil @@ -461,6 +439,11 @@ func (m *Command) SetupServer() error { m.Config.Etcd.Id = m.Config.Name // TODO(twg) rethink this e := petcd.NewEtcd(m.Config.Etcd, m.logger, m.Config.Cluster.ReplicaN, version) + executionPlannerFn := func(e pilosa.Executor, a *pilosa.API, s string) sql3.CompilePlanner { + fapi := &pilosa.FeatureBaseSchemaAPI{API: a} + return planner.NewExecutionPlanner(e, fapi, a, s) + } + serverOptions := []pilosa.ServerOption{ pilosa.OptServerAntiEntropyInterval(time.Duration(m.Config.AntiEntropy.Interval)), pilosa.OptServerLongQueryTime(time.Duration(longQueryTime)), @@ -488,6 +471,7 @@ func (m *Command) SetupServer() error { pilosa.OptServerQueryHistoryLength(m.Config.QueryHistoryLength), pilosa.OptServerPartitionAssigner(m.Config.Cluster.PartitionToNodeAssignment), pilosa.OptServerDisCo(e, e, e, e), + pilosa.OptServerExecutionPlannerFn(executionPlannerFn), } if m.Config.LookupDBDSN != "" { @@ -549,9 +533,6 @@ func (m *Command) SetupServer() error { m.queryLogger.Infof("Configured IPs for allowed networks: %v", ac.ConfiguredIPs) } - // disable postgres binding if auth is enabled - m.Config.Postgres.Bind = "" - // TLS must be enabled if auth is if m.Config.TLS.CertificatePath == "" || m.Config.TLS.CertificateKeyPath == "" { return fmt.Errorf("transport layer security (TLS) is not configured properly. TLS is required when AuthN/Z is enabled, current configuration: %v", m.Config.TLS) @@ -586,6 +567,7 @@ func (m *Command) SetupServer() error { pilosa.OptHandlerAuthZ(&p), pilosa.OptHandlerSerializer(proto.Serializer{}), pilosa.OptHandlerRoaringSerializer(proto.RoaringSerializer), + pilosa.OptHandlerSQLEnabled(m.Config.SQL.EndpointEnabled), ) return errors.Wrap(err, "new handler") } @@ -673,7 +655,6 @@ func (m *Command) Close() error { eg.Go(m.Handler.Close) eg.Go(m.Server.Close) eg.Go(m.API.Close) - eg.Go(m.pgserver.Close) if closer, ok := m.logOutput.(io.Closer); ok { // If closer is os.Stdout or os.Stderr, don't close it. if closer != os.Stdout && closer != os.Stderr { diff --git a/sql2/ast_test.go b/sql2/ast_test.go deleted file mode 100644 index 2a8dc28d7..000000000 --- a/sql2/ast_test.go +++ /dev/null @@ -1,1149 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2_test - -import ( - "reflect" - "strings" - "testing" - - "github.com/go-test/deep" - sql "github.com/featurebasedb/featurebase/v3/sql2" -) - -func TestExprString(t *testing.T) { - if got, want := sql.ExprString(&sql.NullLit{}), "NULL"; got != want { - t.Fatalf("ExprString()=%q, want %q", got, want) - } else if got, want := sql.ExprString(nil), ""; got != want { - t.Fatalf("ExprString()=%q, want %q", got, want) - } -} - -func TestSplitExprTree(t *testing.T) { - t.Run("AND-only", func(t *testing.T) { - AssertSplitExprTree(t, `x = 1 AND y = 2 AND z = 3`, []sql.Expr{ - &sql.BinaryExpr{X: &sql.Ident{Name: "x"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "1"}}, - &sql.BinaryExpr{X: &sql.Ident{Name: "y"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "2"}}, - &sql.BinaryExpr{X: &sql.Ident{Name: "z"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "3"}}, - }) - }) - - t.Run("OR", func(t *testing.T) { - AssertSplitExprTree(t, `x = 1 AND (y = 2 OR y = 3) AND z = 4`, []sql.Expr{ - &sql.BinaryExpr{X: &sql.Ident{Name: "x"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "1"}}, - &sql.BinaryExpr{ - X: &sql.BinaryExpr{X: &sql.Ident{Name: "y"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "2"}}, - Op: sql.OR, - Y: &sql.BinaryExpr{X: &sql.Ident{Name: "y"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "3"}}, - }, - &sql.BinaryExpr{X: &sql.Ident{Name: "z"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "4"}}, - }) - }) - - t.Run("ParenExpr", func(t *testing.T) { - AssertSplitExprTree(t, `x = 1 AND (y = 2 AND z = 3)`, []sql.Expr{ - &sql.BinaryExpr{X: &sql.Ident{Name: "x"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "1"}}, - &sql.BinaryExpr{X: &sql.Ident{Name: "y"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "2"}}, - &sql.BinaryExpr{X: &sql.Ident{Name: "z"}, Op: sql.EQ, Y: &sql.NumberLit{Value: "3"}}, - }) - }) -} - -func AssertSplitExprTree(tb testing.TB, s string, want []sql.Expr) { - tb.Helper() - if diff := deep.Equal(sql.SplitExprTree(StripExprPos(sql.MustParseExprString(s))), want); diff != nil { - tb.Fatal("mismatch: \n" + strings.Join(diff, "\n")) - } -} - -func TestAlterTableStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.AlterTableStatement{ - Name: &sql.Ident{Name: "foo"}, - NewName: &sql.Ident{Name: "bar"}, - }, `ALTER TABLE "foo" RENAME TO "bar"`) - - AssertStatementStringer(t, &sql.AlterTableStatement{ - Name: &sql.Ident{Name: "foo"}, - ColumnName: &sql.Ident{Name: "col1"}, - NewColumnName: &sql.Ident{Name: "col2"}, - }, `ALTER TABLE "foo" RENAME COLUMN "col1" TO "col2"`) - - AssertStatementStringer(t, &sql.AlterTableStatement{ - Name: &sql.Ident{Name: "foo"}, - ColumnDef: &sql.ColumnDefinition{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}, - }, - }, `ALTER TABLE "foo" ADD COLUMN "bar" INTEGER`) -} - -func TestAnalyzeStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.AnalyzeStatement{Name: &sql.Ident{Name: "foo"}}, `ANALYZE "foo"`) -} - -func TestBeginStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.BeginStatement{}, `BEGIN`) - AssertStatementStringer(t, &sql.BeginStatement{Deferred: pos(0)}, `BEGIN DEFERRED`) - AssertStatementStringer(t, &sql.BeginStatement{Immediate: pos(0)}, `BEGIN IMMEDIATE`) - AssertStatementStringer(t, &sql.BeginStatement{Exclusive: pos(0)}, `BEGIN EXCLUSIVE`) - AssertStatementStringer(t, &sql.BeginStatement{Immediate: pos(0), Transaction: pos(0)}, `BEGIN IMMEDIATE TRANSACTION`) -} - -func TestCommitStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.CommitStatement{}, `COMMIT`) - AssertStatementStringer(t, &sql.CommitStatement{End: pos(0)}, `END`) - AssertStatementStringer(t, &sql.CommitStatement{End: pos(0), Transaction: pos(0)}, `END TRANSACTION`) -} - -func TestCreateIndexStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.CreateIndexStatement{ - Name: &sql.Ident{Name: "foo"}, - Table: &sql.Ident{Name: "bar"}, - Columns: []*sql.IndexedColumn{{X: &sql.Ident{Name: "baz"}}}, - }, `CREATE INDEX "foo" ON "bar" ("baz")`) - - AssertStatementStringer(t, &sql.CreateIndexStatement{ - Unique: pos(0), - IfNotExists: pos(0), - Name: &sql.Ident{Name: "foo"}, - Table: &sql.Ident{Name: "bar"}, - Columns: []*sql.IndexedColumn{ - {X: &sql.Ident{Name: "baz"}}, - {X: &sql.Ident{Name: "bat"}}, - }, - WhereExpr: &sql.BoolLit{Value: true}, - }, `CREATE UNIQUE INDEX IF NOT EXISTS "foo" ON "bar" ("baz", "bat") WHERE TRUE`) -} - -func TestCreateTableStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - IfNotExists: pos(0), - Columns: []*sql.ColumnDefinition{ - { - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}, - }, - { - Name: &sql.Ident{Name: "baz"}, - Type: &sql.Type{Name: &sql.Ident{Name: "TEXT"}}, - }, - }, - }, `CREATE TABLE IF NOT EXISTS "foo" ("bar" INTEGER, "baz" TEXT)`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Columns: []*sql.ColumnDefinition{{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}, - Constraints: []sql.Constraint{ - &sql.PrimaryKeyConstraint{Autoincrement: pos(0)}, - &sql.NotNullConstraint{Name: &sql.Ident{Name: "nn"}}, - &sql.DefaultConstraint{Name: &sql.Ident{Name: "def"}, Expr: &sql.NumberLit{Value: "123"}}, - &sql.DefaultConstraint{Expr: &sql.NumberLit{Value: "456"}, Lparen: pos(0)}, - &sql.UniqueConstraint{}, - }, - }}, - }, `CREATE TABLE "foo" ("bar" INTEGER PRIMARY KEY AUTOINCREMENT CONSTRAINT "nn" NOT NULL CONSTRAINT "def" DEFAULT 123 DEFAULT (456) UNIQUE)`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Columns: []*sql.ColumnDefinition{{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}, - Constraints: []sql.Constraint{ - &sql.ForeignKeyConstraint{ - ForeignTable: &sql.Ident{Name: "x"}, - ForeignColumns: []*sql.Ident{{Name: "y"}}, - Args: []*sql.ForeignKeyArg{ - {OnDelete: pos(0), SetNull: pos(0)}, - {OnUpdate: pos(0), SetDefault: pos(0)}, - {OnUpdate: pos(0), Cascade: pos(0)}, - {OnUpdate: pos(0), Restrict: pos(0)}, - {OnUpdate: pos(0), NoAction: pos(0)}, - }, - }, - }, - }}, - }, `CREATE TABLE "foo" ("bar" INTEGER REFERENCES "x" ("y") ON DELETE SET NULL ON UPDATE SET DEFAULT ON UPDATE CASCADE ON UPDATE RESTRICT ON UPDATE NO ACTION)`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Columns: []*sql.ColumnDefinition{{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}, - Constraints: []sql.Constraint{ - &sql.ForeignKeyConstraint{ - ForeignTable: &sql.Ident{Name: "x"}, - ForeignColumns: []*sql.Ident{{Name: "y"}}, - Deferrable: pos(0), - InitiallyDeferred: pos(0), - }, - }, - }}, - }, `CREATE TABLE "foo" ("bar" INTEGER REFERENCES "x" ("y") DEFERRABLE INITIALLY DEFERRED)`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Columns: []*sql.ColumnDefinition{{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}, - Constraints: []sql.Constraint{ - &sql.ForeignKeyConstraint{ - ForeignTable: &sql.Ident{Name: "x"}, - ForeignColumns: []*sql.Ident{{Name: "y"}}, - NotDeferrable: pos(0), - InitiallyImmediate: pos(0), - }, - }, - }}, - }, `CREATE TABLE "foo" ("bar" INTEGER REFERENCES "x" ("y") NOT DEFERRABLE INITIALLY IMMEDIATE)`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Columns: []*sql.ColumnDefinition{{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "DECIMAL"}, Precision: &sql.NumberLit{Value: "100"}}, - }}, - Constraints: []sql.Constraint{ - &sql.PrimaryKeyConstraint{ - Name: &sql.Ident{Name: "pk"}, - Columns: []*sql.Ident{ - {Name: "x"}, - {Name: "y"}, - }, - }, - &sql.UniqueConstraint{ - Name: &sql.Ident{Name: "uniq"}, - Columns: []*sql.Ident{ - {Name: "x"}, - {Name: "y"}, - }, - }, - &sql.CheckConstraint{ - Name: &sql.Ident{Name: "chk"}, - Expr: &sql.BoolLit{Value: true}, - }, - }, - }, `CREATE TABLE "foo" ("bar" DECIMAL(100), CONSTRAINT "pk" PRIMARY KEY ("x", "y"), CONSTRAINT "uniq" UNIQUE ("x", "y"), CONSTRAINT "chk" CHECK (TRUE))`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Columns: []*sql.ColumnDefinition{{ - Name: &sql.Ident{Name: "bar"}, - Type: &sql.Type{Name: &sql.Ident{Name: "DECIMAL"}, Precision: &sql.NumberLit{Value: "100"}, Scale: &sql.NumberLit{Value: "200"}}, - }}, - Constraints: []sql.Constraint{ - &sql.ForeignKeyConstraint{ - Name: &sql.Ident{Name: "fk"}, - Columns: []*sql.Ident{{Name: "a"}, {Name: "b"}}, - ForeignTable: &sql.Ident{Name: "x"}, - ForeignColumns: []*sql.Ident{{Name: "y"}, {Name: "z"}}, - }, - }, - }, `CREATE TABLE "foo" ("bar" DECIMAL(100,200), CONSTRAINT "fk" FOREIGN KEY ("a", "b") REFERENCES "x" ("y", "z"))`) - - AssertStatementStringer(t, &sql.CreateTableStatement{ - Name: &sql.Ident{Name: "foo"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `CREATE TABLE "foo" AS SELECT *`) -} - -func TestCreateTriggerStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.CreateTriggerStatement{ - Name: &sql.Ident{Name: "trig"}, - Insert: pos(0), - Table: &sql.Ident{Name: "tbl"}, - Body: []sql.Statement{ - &sql.DeleteStatement{Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl2"}}}, - }, - }, `CREATE TRIGGER "trig" INSERT ON "tbl" BEGIN DELETE FROM "tbl2"; END`) - - AssertStatementStringer(t, &sql.CreateTriggerStatement{ - Name: &sql.Ident{Name: "trig"}, - Before: pos(0), - Delete: pos(0), - ForEachRow: pos(0), - Table: &sql.Ident{Name: "tbl"}, - Body: []sql.Statement{ - &sql.DeleteStatement{Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}}, - }, - }, `CREATE TRIGGER "trig" BEFORE DELETE ON "tbl" FOR EACH ROW BEGIN DELETE FROM "x"; END`) - - AssertStatementStringer(t, &sql.CreateTriggerStatement{ - IfNotExists: pos(0), - Name: &sql.Ident{Name: "trig"}, - After: pos(0), - Update: pos(0), - Table: &sql.Ident{Name: "tbl"}, - WhenExpr: &sql.BoolLit{Value: true}, - Body: []sql.Statement{ - &sql.DeleteStatement{Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}}, - }, - }, `CREATE TRIGGER IF NOT EXISTS "trig" AFTER UPDATE ON "tbl" WHEN TRUE BEGIN DELETE FROM "x"; END`) - - AssertStatementStringer(t, &sql.CreateTriggerStatement{ - Name: &sql.Ident{Name: "trig"}, - InsteadOf: pos(0), - Update: pos(0), - UpdateOf: pos(0), - UpdateOfColumns: []*sql.Ident{{Name: "x"}, {Name: "y"}}, - Table: &sql.Ident{Name: "tbl"}, - Body: []sql.Statement{ - &sql.DeleteStatement{Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}}, - }, - }, `CREATE TRIGGER "trig" INSTEAD OF UPDATE OF "x", "y" ON "tbl" BEGIN DELETE FROM "x"; END`) -} - -func TestCreateViewStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.CreateViewStatement{ - Name: &sql.Ident{Name: "vw"}, - Columns: []*sql.Ident{ - {Name: "x"}, - {Name: "y"}, - }, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `CREATE VIEW "vw" ("x", "y") AS SELECT *`) - - AssertStatementStringer(t, &sql.CreateViewStatement{ - IfNotExists: pos(0), - Name: &sql.Ident{Name: "vw"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `CREATE VIEW IF NOT EXISTS "vw" AS SELECT *`) -} - -func TestDeleteStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.DeleteStatement{ - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}, Alias: &sql.Ident{Name: "tbl2"}}, - }, `DELETE FROM "tbl" AS "tbl2"`) - - AssertStatementStringer(t, &sql.DeleteStatement{ - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}, Index: &sql.Ident{Name: "idx"}}, - }, `DELETE FROM "tbl" INDEXED BY "idx"`) - - AssertStatementStringer(t, &sql.DeleteStatement{ - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}, NotIndexed: pos(0)}, - }, `DELETE FROM "tbl" NOT INDEXED`) - - AssertStatementStringer(t, &sql.DeleteStatement{ - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - WhereExpr: &sql.BoolLit{Value: true}, - OrderingTerms: []*sql.OrderingTerm{ - {X: &sql.Ident{Name: "x"}}, - {X: &sql.Ident{Name: "y"}}, - }, - LimitExpr: &sql.NumberLit{Value: "10"}, - OffsetExpr: &sql.NumberLit{Value: "5"}, - }, `DELETE FROM "tbl" WHERE TRUE ORDER BY "x", "y" LIMIT 10 OFFSET 5`) - - AssertStatementStringer(t, &sql.DeleteStatement{ - WithClause: &sql.WithClause{ - Recursive: pos(0), - CTEs: []*sql.CTE{{ - TableName: &sql.Ident{Name: "cte"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }}, - }, - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - }, `WITH RECURSIVE "cte" AS (SELECT *) DELETE FROM "tbl"`) -} - -func TestDropIndexStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.DropIndexStatement{ - Name: &sql.Ident{Name: "idx"}, - }, `DROP INDEX "idx"`) - - AssertStatementStringer(t, &sql.DropIndexStatement{ - IfExists: pos(0), - Name: &sql.Ident{Name: "idx"}, - }, `DROP INDEX IF EXISTS "idx"`) -} - -func TestDropTableStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.DropTableStatement{ - Name: &sql.Ident{Name: "tbl"}, - }, `DROP TABLE "tbl"`) - - AssertStatementStringer(t, &sql.DropTableStatement{ - IfExists: pos(0), - Name: &sql.Ident{Name: "tbl"}, - }, `DROP TABLE IF EXISTS "tbl"`) -} - -func TestDropTriggerStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.DropTriggerStatement{ - Name: &sql.Ident{Name: "trig"}, - }, `DROP TRIGGER "trig"`) - - AssertStatementStringer(t, &sql.DropTriggerStatement{ - IfExists: pos(0), - Name: &sql.Ident{Name: "trig"}, - }, `DROP TRIGGER IF EXISTS "trig"`) -} - -func TestDropViewStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.DropViewStatement{ - Name: &sql.Ident{Name: "vw"}, - }, `DROP VIEW "vw"`) - - AssertStatementStringer(t, &sql.DropViewStatement{ - IfExists: pos(0), - Name: &sql.Ident{Name: "vw"}, - }, `DROP VIEW IF EXISTS "vw"`) -} - -func TestExplainStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.ExplainStatement{ - Stmt: &sql.DropViewStatement{ - Name: &sql.Ident{Name: "vw"}, - }, - }, `EXPLAIN DROP VIEW "vw"`) - - AssertStatementStringer(t, &sql.ExplainStatement{ - QueryPlan: pos(0), - Stmt: &sql.DropViewStatement{ - Name: &sql.Ident{Name: "vw"}, - }, - }, `EXPLAIN QUERY PLAN DROP VIEW "vw"`) -} - -func TestInsertStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.InsertStatement{ - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `INSERT INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - Table: &sql.Ident{Name: "tbl"}, - Alias: &sql.Ident{Name: "x"}, - DefaultValues: pos(0), - }, `INSERT INTO "tbl" AS "x" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - InsertOrReplace: pos(0), - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `INSERT OR REPLACE INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - InsertOrRollback: pos(0), - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `INSERT OR ROLLBACK INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - InsertOrAbort: pos(0), - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `INSERT OR ABORT INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - InsertOrFail: pos(0), - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `INSERT OR FAIL INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - InsertOrIgnore: pos(0), - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `INSERT OR IGNORE INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - Replace: pos(0), - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `REPLACE INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - Table: &sql.Ident{Name: "tbl"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `INSERT INTO "tbl" SELECT *`) - - AssertStatementStringer(t, &sql.InsertStatement{ - Table: &sql.Ident{Name: "tbl"}, - Columns: []*sql.Ident{ - {Name: "x"}, - {Name: "y"}, - }, - ValueLists: []*sql.ExprList{ - {Exprs: []sql.Expr{&sql.NullLit{}, &sql.NullLit{}}}, - {Exprs: []sql.Expr{&sql.NullLit{}, &sql.NullLit{}}}, - }, - }, `INSERT INTO "tbl" ("x", "y") VALUES (NULL, NULL), (NULL, NULL)`) - - AssertStatementStringer(t, &sql.InsertStatement{ - WithClause: &sql.WithClause{ - CTEs: []*sql.CTE{ - { - TableName: &sql.Ident{Name: "cte"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, - { - TableName: &sql.Ident{Name: "cte2"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, - }, - }, - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - }, `WITH "cte" AS (SELECT *), "cte2" AS (SELECT *) INSERT INTO "tbl" DEFAULT VALUES`) - - AssertStatementStringer(t, &sql.InsertStatement{ - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - UpsertClause: &sql.UpsertClause{ - DoNothing: pos(0), - }, - }, `INSERT INTO "tbl" DEFAULT VALUES ON CONFLICT DO NOTHING`) - - AssertStatementStringer(t, &sql.InsertStatement{ - Table: &sql.Ident{Name: "tbl"}, - DefaultValues: pos(0), - UpsertClause: &sql.UpsertClause{ - Columns: []*sql.IndexedColumn{ - {X: &sql.Ident{Name: "x"}, Asc: pos(0)}, - {X: &sql.Ident{Name: "y"}, Desc: pos(0)}, - }, - WhereExpr: &sql.BoolLit{Value: true}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - {Columns: []*sql.Ident{{Name: "y"}, {Name: "z"}}, Expr: &sql.NumberLit{Value: "200"}}, - }, - UpdateWhereExpr: &sql.BoolLit{Value: false}, - }, - }, `INSERT INTO "tbl" DEFAULT VALUES ON CONFLICT ("x" ASC, "y" DESC) WHERE TRUE DO UPDATE SET "x" = 100, ("y", "z") = 200 WHERE FALSE`) -} - -func TestReleaseStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.ReleaseStatement{Name: &sql.Ident{Name: "x"}}, `RELEASE "x"`) - AssertStatementStringer(t, &sql.ReleaseStatement{Savepoint: pos(0), Name: &sql.Ident{Name: "x"}}, `RELEASE SAVEPOINT "x"`) -} - -func TestRollbackStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.RollbackStatement{}, `ROLLBACK`) - AssertStatementStringer(t, &sql.RollbackStatement{Transaction: pos(0)}, `ROLLBACK TRANSACTION`) - AssertStatementStringer(t, &sql.RollbackStatement{SavepointName: &sql.Ident{Name: "x"}}, `ROLLBACK TO "x"`) - AssertStatementStringer(t, &sql.RollbackStatement{Savepoint: pos(0), SavepointName: &sql.Ident{Name: "x"}}, `ROLLBACK TO SAVEPOINT "x"`) -} - -func TestSavepointStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.SavepointStatement{Name: &sql.Ident{Name: "x"}}, `SAVEPOINT "x"`) -} - -func TestSelectStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{ - {Expr: &sql.Ident{Name: "x"}, Alias: &sql.Ident{Name: "y"}}, - {Expr: &sql.Ident{Name: "z"}}, - }, - }, `SELECT "x" AS "y", "z"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Distinct: pos(0), - Columns: []*sql.ResultColumn{ - {Expr: &sql.Ident{Name: "x"}}, - }, - }, `SELECT DISTINCT "x"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - All: pos(0), - Columns: []*sql.ResultColumn{ - {Expr: &sql.Ident{Name: "x"}}, - }, - }, `SELECT ALL "x"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - WhereExpr: &sql.BoolLit{Value: true}, - GroupByExprs: []sql.Expr{&sql.Ident{Name: "x"}, &sql.Ident{Name: "y"}}, - HavingExpr: &sql.Ident{Name: "z"}, - }, `SELECT * FROM "tbl" WHERE TRUE GROUP BY "x", "y" HAVING "z"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.ParenSource{ - X: &sql.SelectStatement{Columns: []*sql.ResultColumn{{Star: pos(0)}}}, - Alias: &sql.Ident{Name: "tbl"}, - }, - }, `SELECT * FROM (SELECT *) AS "tbl"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.ParenSource{ - X: &sql.SelectStatement{Columns: []*sql.ResultColumn{{Star: pos(0)}}}, - }, - }, `SELECT * FROM (SELECT *)`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Windows: []*sql.Window{ - { - Name: &sql.Ident{Name: "win1"}, - Definition: &sql.WindowDefinition{ - Base: &sql.Ident{Name: "base"}, - Partitions: []sql.Expr{&sql.Ident{Name: "x"}, &sql.Ident{Name: "y"}}, - OrderingTerms: []*sql.OrderingTerm{ - {X: &sql.Ident{Name: "x"}, Asc: pos(0), NullsFirst: pos(0)}, - {X: &sql.Ident{Name: "y"}, Desc: pos(0), NullsLast: pos(0)}, - }, - Frame: &sql.FrameSpec{ - Range: pos(0), - UnboundedX: pos(0), - PrecedingX: pos(0), - }, - }, - }, - { - Name: &sql.Ident{Name: "win2"}, - Definition: &sql.WindowDefinition{ - Base: &sql.Ident{Name: "base2"}, - }, - }, - }, - }, `SELECT * FROM "tbl" WINDOW "win1" AS ("base" PARTITION BY "x", "y" ORDER BY "x" ASC NULLS FIRST, "y" DESC NULLS LAST RANGE UNBOUNDED PRECEDING), "win2" AS ("base2")`) - - AssertStatementStringer(t, &sql.SelectStatement{ - WithClause: &sql.WithClause{ - CTEs: []*sql.CTE{{ - TableName: &sql.Ident{Name: "cte"}, - Columns: []*sql.Ident{ - {Name: "x"}, - {Name: "y"}, - }, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }}, - }, - ValueLists: []*sql.ExprList{ - {Exprs: []sql.Expr{&sql.NumberLit{Value: "1"}, &sql.NumberLit{Value: "2"}}}, - {Exprs: []sql.Expr{&sql.NumberLit{Value: "3"}, &sql.NumberLit{Value: "4"}}}, - }, - }, `WITH "cte" ("x", "y") AS (SELECT *) VALUES (1, 2), (3, 4)`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Union: pos(0), - Compound: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `SELECT * UNION SELECT *`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Union: pos(0), - UnionAll: pos(0), - Compound: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `SELECT * UNION ALL SELECT *`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Intersect: pos(0), - Compound: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `SELECT * INTERSECT SELECT *`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Except: pos(0), - Compound: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }, `SELECT * EXCEPT SELECT *`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - OrderingTerms: []*sql.OrderingTerm{ - {X: &sql.Ident{Name: "x"}}, - {X: &sql.Ident{Name: "y"}}, - }, - }, `SELECT * ORDER BY "x", "y"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - LimitExpr: &sql.NumberLit{Value: "1"}, - OffsetExpr: &sql.NumberLit{Value: "2"}, - }, `SELECT * LIMIT 1 OFFSET 2`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}, - Operator: &sql.JoinOperator{Comma: pos(0)}, - Y: &sql.QualifiedTableName{Name: &sql.Ident{Name: "y"}}, - }, - }, `SELECT * FROM "x", "y"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}, - Operator: &sql.JoinOperator{}, - Y: &sql.QualifiedTableName{Name: &sql.Ident{Name: "y"}}, - Constraint: &sql.OnConstraint{X: &sql.BoolLit{Value: true}}, - }, - }, `SELECT * FROM "x" JOIN "y" ON TRUE`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}, - Operator: &sql.JoinOperator{Natural: pos(0), Inner: pos(0)}, - Y: &sql.QualifiedTableName{Name: &sql.Ident{Name: "y"}}, - Constraint: &sql.UsingConstraint{ - Columns: []*sql.Ident{{Name: "a"}, {Name: "b"}}, - }, - }, - }, `SELECT * FROM "x" NATURAL INNER JOIN "y" USING ("a", "b")`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}, - Operator: &sql.JoinOperator{Left: pos(0)}, - Y: &sql.QualifiedTableName{Name: &sql.Ident{Name: "y"}}, - }, - }, `SELECT * FROM "x" LEFT JOIN "y"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}, - Operator: &sql.JoinOperator{Left: pos(0), Outer: pos(0)}, - Y: &sql.QualifiedTableName{Name: &sql.Ident{Name: "y"}}, - }, - }, `SELECT * FROM "x" LEFT OUTER JOIN "y"`) - - AssertStatementStringer(t, &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{Name: &sql.Ident{Name: "x"}}, - Operator: &sql.JoinOperator{Cross: pos(0)}, - Y: &sql.QualifiedTableName{Name: &sql.Ident{Name: "y"}}, - }, - }, `SELECT * FROM "x" CROSS JOIN "y"`) -} - -func TestUpdateStatement_String(t *testing.T) { - AssertStatementStringer(t, &sql.UpdateStatement{ - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - {Columns: []*sql.Ident{{Name: "y"}}, Expr: &sql.NumberLit{Value: "200"}}, - }, - WhereExpr: &sql.BoolLit{Value: true}, - }, `UPDATE "tbl" SET "x" = 100, "y" = 200 WHERE TRUE`) - - AssertStatementStringer(t, &sql.UpdateStatement{ - UpdateOrRollback: pos(0), - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - }, - }, `UPDATE OR ROLLBACK "tbl" SET "x" = 100`) - - AssertStatementStringer(t, &sql.UpdateStatement{ - UpdateOrAbort: pos(0), - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - }, - }, `UPDATE OR ABORT "tbl" SET "x" = 100`) - - AssertStatementStringer(t, &sql.UpdateStatement{ - UpdateOrReplace: pos(0), - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - }, - }, `UPDATE OR REPLACE "tbl" SET "x" = 100`) - - AssertStatementStringer(t, &sql.UpdateStatement{ - UpdateOrFail: pos(0), - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - }, - }, `UPDATE OR FAIL "tbl" SET "x" = 100`) - - AssertStatementStringer(t, &sql.UpdateStatement{ - UpdateOrIgnore: pos(0), - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - }, - }, `UPDATE OR IGNORE "tbl" SET "x" = 100`) - - AssertStatementStringer(t, &sql.UpdateStatement{ - WithClause: &sql.WithClause{ - CTEs: []*sql.CTE{{ - TableName: &sql.Ident{Name: "cte"}, - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{{Star: pos(0)}}, - }, - }}, - }, - Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, - Assignments: []*sql.Assignment{ - {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, - }, - }, `WITH "cte" AS (SELECT *) UPDATE "tbl" SET "x" = 100`) -} - -func TestIdent_String(t *testing.T) { - AssertExprStringer(t, &sql.Ident{Name: "foo"}, `"foo"`) - AssertExprStringer(t, &sql.Ident{Name: "foo \" bar"}, `"foo "" bar"`) -} - -func TestStringLit_String(t *testing.T) { - AssertExprStringer(t, &sql.StringLit{Value: "foo"}, `'foo'`) - AssertExprStringer(t, &sql.StringLit{Value: "foo ' bar"}, `'foo '' bar'`) -} - -func TestNumberLit_String(t *testing.T) { - AssertExprStringer(t, &sql.NumberLit{Value: "123.45"}, `123.45`) -} - -func TestBlobLit_String(t *testing.T) { - AssertExprStringer(t, &sql.BlobLit{Value: "0123abcd"}, `x'0123abcd'`) -} - -func TestBoolLit_String(t *testing.T) { - AssertExprStringer(t, &sql.BoolLit{Value: true}, `TRUE`) - AssertExprStringer(t, &sql.BoolLit{Value: false}, `FALSE`) -} - -func TestNullLit_String(t *testing.T) { - AssertExprStringer(t, &sql.NullLit{}, `NULL`) -} - -func TestBindExpr_String(t *testing.T) { - AssertExprStringer(t, &sql.BindExpr{Name: "foo"}, `$foo`) -} - -func TestParenExpr_String(t *testing.T) { - AssertExprStringer(t, &sql.ParenExpr{X: &sql.NullLit{}}, `(NULL)`) -} - -func TestUnaryExpr_String(t *testing.T) { - AssertExprStringer(t, &sql.UnaryExpr{Op: sql.PLUS, X: &sql.NumberLit{Value: "100"}}, `+100`) - AssertExprStringer(t, &sql.UnaryExpr{Op: sql.MINUS, X: &sql.NumberLit{Value: "100"}}, `-100`) - AssertNodeStringerPanic(t, &sql.UnaryExpr{X: &sql.NumberLit{Value: "100"}}, `sql.UnaryExpr.String(): invalid op ILLEGAL`) -} - -func TestBinaryExpr_String(t *testing.T) { - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.PLUS, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 + 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.MINUS, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 - 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.STAR, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 * 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.SLASH, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 / 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.REM, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 % 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.CONCAT, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 || 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.BETWEEN, X: &sql.NumberLit{Value: "1"}, Y: &sql.Range{X: &sql.NumberLit{Value: "2"}, Y: &sql.NumberLit{Value: "3"}}}, `1 BETWEEN 2 AND 3`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NOTBETWEEN, X: &sql.NumberLit{Value: "1"}, Y: &sql.BinaryExpr{Op: sql.AND, X: &sql.NumberLit{Value: "2"}, Y: &sql.NumberLit{Value: "3"}}}, `1 NOT BETWEEN 2 AND 3`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.LSHIFT, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 << 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.RSHIFT, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 >> 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.BITAND, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 & 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.BITOR, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 | 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.LT, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 < 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.LE, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 <= 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.GT, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 > 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.GE, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 >= 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.EQ, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 = 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NE, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 != 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.IS, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 IS 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.ISNOT, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 IS NOT 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.IN, X: &sql.NumberLit{Value: "1"}, Y: &sql.ExprList{Exprs: []sql.Expr{&sql.NumberLit{Value: "2"}}}}, `1 IN (2)`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NOTIN, X: &sql.NumberLit{Value: "1"}, Y: &sql.ExprList{Exprs: []sql.Expr{&sql.NumberLit{Value: "2"}}}}, `1 NOT IN (2)`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.LIKE, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 LIKE 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NOTLIKE, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 NOT LIKE 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.GLOB, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 GLOB 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NOTGLOB, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 NOT GLOB 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.MATCH, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 MATCH 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NOTMATCH, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 NOT MATCH 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.REGEXP, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 REGEXP 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.NOTREGEXP, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 NOT REGEXP 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.AND, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 AND 2`) - AssertExprStringer(t, &sql.BinaryExpr{Op: sql.OR, X: &sql.NumberLit{Value: "1"}, Y: &sql.NumberLit{Value: "2"}}, `1 OR 2`) - AssertNodeStringerPanic(t, &sql.BinaryExpr{}, `sql.BinaryExpr.String(): invalid op ILLEGAL`) -} - -func TestCastExpr_String(t *testing.T) { - AssertExprStringer(t, &sql.CastExpr{X: &sql.NumberLit{Value: "1"}, Type: &sql.Type{Name: &sql.Ident{Name: "INTEGER"}}}, `CAST(1 AS INTEGER)`) -} - -func TestCaseExpr_String(t *testing.T) { - AssertExprStringer(t, &sql.CaseExpr{ - Operand: &sql.Ident{Name: "foo"}, - Blocks: []*sql.CaseBlock{ - {Condition: &sql.NumberLit{Value: "1"}, Body: &sql.BoolLit{Value: true}}, - {Condition: &sql.NumberLit{Value: "2"}, Body: &sql.BoolLit{Value: false}}, - }, - ElseExpr: &sql.NullLit{}, - }, `CASE "foo" WHEN 1 THEN TRUE WHEN 2 THEN FALSE ELSE NULL END`) - - AssertExprStringer(t, &sql.CaseExpr{ - Blocks: []*sql.CaseBlock{ - {Condition: &sql.NumberLit{Value: "1"}, Body: &sql.BoolLit{Value: true}}, - }, - }, `CASE WHEN 1 THEN TRUE END`) -} - -func TestExprList_String(t *testing.T) { - AssertExprStringer(t, &sql.ExprList{Exprs: []sql.Expr{&sql.NullLit{}}}, `(NULL)`) - AssertExprStringer(t, &sql.ExprList{Exprs: []sql.Expr{&sql.NullLit{}, &sql.NullLit{}}}, `(NULL, NULL)`) -} - -func TestQualifiedRef_String(t *testing.T) { - AssertExprStringer(t, &sql.QualifiedRef{Table: &sql.Ident{Name: "tbl"}, Column: &sql.Ident{Name: "col"}}, `"tbl"."col"`) - AssertExprStringer(t, &sql.QualifiedRef{Table: &sql.Ident{Name: "tbl"}, Star: pos(0)}, `"tbl".*`) -} - -func TestCall_String(t *testing.T) { - AssertExprStringer(t, &sql.Call{Name: &sql.Ident{Name: "foo"}}, `foo()`) - AssertExprStringer(t, &sql.Call{Name: &sql.Ident{Name: "foo"}, Star: pos(0)}, `foo(*)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Distinct: pos(0), - Args: []sql.Expr{ - &sql.NullLit{}, - &sql.NullLit{}, - }, - }, `foo(DISTINCT NULL, NULL)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Filter: &sql.FilterClause{ - X: &sql.BoolLit{Value: true}, - }, - }, `foo() FILTER (WHERE TRUE)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Name: &sql.Ident{Name: "win"}, - }, - }, `foo() OVER "win"`) - - t.Run("FrameSpec", func(t *testing.T) { - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Rows: pos(0), - X: &sql.NullLit{}, - PrecedingX: pos(0), - ExcludeNoOthers: pos(0), - }, - }, - }, - }, `foo() OVER (ROWS NULL PRECEDING EXCLUDE NO OTHERS)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Groups: pos(0), - CurrentRowX: pos(0), - ExcludeCurrentRow: pos(0), - }, - }, - }, - }, `foo() OVER (GROUPS CURRENT ROW EXCLUDE CURRENT ROW)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Rows: pos(0), - UnboundedX: pos(0), - PrecedingX: pos(0), - Between: pos(0), - CurrentRowY: pos(0), - }, - }, - }, - }, `foo() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Rows: pos(0), - X: &sql.NullLit{}, - PrecedingX: pos(0), - Between: pos(0), - CurrentRowY: pos(0), - }, - }, - }, - }, `foo() OVER (ROWS BETWEEN NULL PRECEDING AND CURRENT ROW)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Range: pos(0), - X: &sql.NullLit{}, - FollowingX: pos(0), - Between: pos(0), - Y: &sql.BoolLit{Value: true}, - PrecedingY: pos(0), - ExcludeGroup: pos(0), - }, - }, - }, - }, `foo() OVER (RANGE BETWEEN NULL FOLLOWING AND TRUE PRECEDING EXCLUDE GROUP)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Range: pos(0), - CurrentRowX: pos(0), - Between: pos(0), - Y: &sql.BoolLit{Value: true}, - FollowingY: pos(0), - ExcludeTies: pos(0), - }, - }, - }, - }, `foo() OVER (RANGE BETWEEN CURRENT ROW AND TRUE FOLLOWING EXCLUDE TIES)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Range: pos(0), - CurrentRowX: pos(0), - Between: pos(0), - CurrentRowY: pos(0), - }, - }, - }, - }, `foo() OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW)`) - - AssertExprStringer(t, &sql.Call{ - Name: &sql.Ident{Name: "foo"}, - Over: &sql.OverClause{ - Definition: &sql.WindowDefinition{ - Frame: &sql.FrameSpec{ - Range: pos(0), - CurrentRowX: pos(0), - Between: pos(0), - UnboundedY: pos(0), - FollowingY: pos(0), - }, - }, - }, - }, `foo() OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)`) - }) -} - -func TestRaise_String(t *testing.T) { - AssertExprStringer(t, &sql.Raise{Rollback: pos(0), Error: &sql.StringLit{Value: "err"}}, `RAISE(ROLLBACK, 'err')`) - AssertExprStringer(t, &sql.Raise{Abort: pos(0), Error: &sql.StringLit{Value: "err"}}, `RAISE(ABORT, 'err')`) - AssertExprStringer(t, &sql.Raise{Fail: pos(0), Error: &sql.StringLit{Value: "err"}}, `RAISE(FAIL, 'err')`) - AssertExprStringer(t, &sql.Raise{Ignore: pos(0)}, `RAISE(IGNORE)`) -} - -func TestExists_String(t *testing.T) { - AssertExprStringer(t, &sql.Exists{ - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{ - {Star: pos(0)}, - }, - }, - }, `EXISTS (SELECT *)`) - - AssertExprStringer(t, &sql.Exists{ - Not: pos(0), - Exists: pos(0), - Select: &sql.SelectStatement{ - Columns: []*sql.ResultColumn{ - {Star: pos(0)}, - }, - }, - }, `NOT EXISTS (SELECT *)`) -} - -func AssertExprStringer(tb testing.TB, expr sql.Expr, s string) { - tb.Helper() - if str := expr.String(); str != s { - tb.Fatalf("String()=%s, expected %s", str, s) - } else if _, err := sql.NewParser(strings.NewReader(str)).ParseExpr(); err != nil { - tb.Fatalf("cannot parse string: %s; err=%s", str, err) - } -} - -func AssertStatementStringer(tb testing.TB, stmt sql.Statement, s string) { - tb.Helper() - if str := stmt.String(); str != s { - tb.Fatalf("String()=%s, expected %s", str, s) - } else if _, err := sql.NewParser(strings.NewReader(str)).ParseStatement(); err != nil { - tb.Fatalf("cannot parse string: %s; err=%s", str, err) - } -} - -func AssertNodeStringerPanic(tb testing.TB, node sql.Node, msg string) { - tb.Helper() - var r interface{} - func() { - defer func() { r = recover() }() - _ = node.String() - }() - if r == nil { - tb.Fatal("expected node stringer to panic") - } else if r != msg { - tb.Fatalf("recover()=%s, want %s", r, msg) - } -} - -// StripPos removes the position data from a node and its children. -// This function returns the root argument passed in. -func StripPos(root sql.Node) sql.Node { - zero := reflect.ValueOf(sql.Pos{}) - - _, _ = sql.Walk(sql.VisitFunc(func(node sql.Node) (sql.Node, error) { - value := reflect.Indirect(reflect.ValueOf(node)) - for i := 0; i < value.NumField(); i++ { - if field := value.Field(i); field.Type() == zero.Type() { - field.Set(zero) - } - } - return node, nil - }), root) - return root -} - -func StripExprPos(root sql.Expr) sql.Expr { - StripPos(root) - return root -} diff --git a/sql2/scanner_test.go b/sql2/scanner_test.go deleted file mode 100644 index aa315b998..000000000 --- a/sql2/scanner_test.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2_test - -import ( - "strings" - "testing" - - sql "github.com/featurebasedb/featurebase/v3/sql2" -) - -func TestScanner_Scan(t *testing.T) { - t.Run("IDENT", func(t *testing.T) { - t.Run("Unquoted", func(t *testing.T) { - AssertScan(t, `foo_BAR123`, sql.IDENT, `foo_BAR123`) - }) - t.Run("Quoted", func(t *testing.T) { - AssertScan(t, `"crazy ~!#*&# column name"" foo"`, sql.QIDENT, `crazy ~!#*&# column name" foo`) - }) - t.Run("NoEndQuote", func(t *testing.T) { - AssertScan(t, `"unfinished`, sql.ILLEGAL, `"unfinished`) - }) - t.Run("x", func(t *testing.T) { - AssertScan(t, `x`, sql.IDENT, `x`) - }) - t.Run("StartingX", func(t *testing.T) { - AssertScan(t, `xyz`, sql.IDENT, `xyz`) - }) - t.Run("WithComment", func(t *testing.T) { - AssertScan(t, "-- this is a comment\n\n-- more comments\nfoo", sql.IDENT, `foo`) - }) - }) - - t.Run("KEYWORD", func(t *testing.T) { - AssertScan(t, `BEGIN`, sql.BEGIN, `BEGIN`) - }) - - t.Run("STRING", func(t *testing.T) { - t.Run("OK", func(t *testing.T) { - AssertScan(t, `'this is ''a'' string'`, sql.STRING, `this is 'a' string`) - }) - t.Run("NoEndQuote", func(t *testing.T) { - AssertScan(t, `'unfinished`, sql.ILLEGAL, `'unfinished`) - }) - }) - t.Run("BLOB", func(t *testing.T) { - t.Run("LowerX", func(t *testing.T) { - AssertScan(t, `x'0123456789abcdef'`, sql.BLOB, `0123456789abcdef`) - }) - t.Run("UpperX", func(t *testing.T) { - AssertScan(t, `X'0123456789ABCDEF'`, sql.BLOB, `0123456789ABCDEF`) - }) - t.Run("NoEndQuote", func(t *testing.T) { - AssertScan(t, `x'0123`, sql.ILLEGAL, `x'0123`) - }) - t.Run("BadHex", func(t *testing.T) { - AssertScan(t, `x'hello`, sql.ILLEGAL, `x'h`) - }) - }) - - t.Run("INTEGER", func(t *testing.T) { - AssertScan(t, `123`, sql.INTEGER, `123`) - }) - - t.Run("FLOAT", func(t *testing.T) { - AssertScan(t, `123.456`, sql.FLOAT, `123.456`) - AssertScan(t, `.1`, sql.FLOAT, `.1`) - AssertScan(t, `123e456`, sql.FLOAT, `123e456`) - AssertScan(t, `123E456`, sql.FLOAT, `123E456`) - AssertScan(t, `123.456E78`, sql.FLOAT, `123.456E78`) - AssertScan(t, `123.E45`, sql.FLOAT, `123.E45`) - AssertScan(t, `123E+4`, sql.FLOAT, `123E+4`) - AssertScan(t, `123E-4`, sql.FLOAT, `123E-4`) - AssertScan(t, `123E`, sql.ILLEGAL, `123E`) - AssertScan(t, `123E+`, sql.ILLEGAL, `123E+`) - AssertScan(t, `123E-`, sql.ILLEGAL, `123E-`) - }) - t.Run("BIND", func(t *testing.T) { - AssertScan(t, `?'`, sql.BIND, `?`) - AssertScan(t, `?123'`, sql.BIND, `?123`) - AssertScan(t, `:foo_bar123'`, sql.BIND, `:foo_bar123`) - AssertScan(t, `@bar'`, sql.BIND, `@bar`) - AssertScan(t, `$baz'`, sql.BIND, `$baz`) - }) - - t.Run("EOF", func(t *testing.T) { - AssertScan(t, " \n\t\r", sql.EOF, ``) - }) - - t.Run("SEMI", func(t *testing.T) { - AssertScan(t, ";", sql.SEMI, ";") - }) - t.Run("LP", func(t *testing.T) { - AssertScan(t, "(", sql.LP, "(") - }) - t.Run("RP", func(t *testing.T) { - AssertScan(t, ")", sql.RP, ")") - }) - t.Run("COMMA", func(t *testing.T) { - AssertScan(t, ",", sql.COMMA, ",") - }) - t.Run("NE", func(t *testing.T) { - AssertScan(t, "!=", sql.NE, "!=") - }) - t.Run("BITNOT", func(t *testing.T) { - AssertScan(t, "!", sql.BITNOT, "!") - }) - t.Run("EQ", func(t *testing.T) { - AssertScan(t, "=", sql.EQ, "=") - }) - t.Run("LE", func(t *testing.T) { - AssertScan(t, "<=", sql.LE, "<=") - }) - t.Run("LSHIFT", func(t *testing.T) { - AssertScan(t, "<<", sql.LSHIFT, "<<") - }) - t.Run("LT", func(t *testing.T) { - AssertScan(t, "<", sql.LT, "<") - }) - t.Run("GE", func(t *testing.T) { - AssertScan(t, ">=", sql.GE, ">=") - }) - t.Run("RSHIFT", func(t *testing.T) { - AssertScan(t, ">>", sql.RSHIFT, ">>") - }) - t.Run("GT", func(t *testing.T) { - AssertScan(t, ">", sql.GT, ">") - }) - t.Run("BITAND", func(t *testing.T) { - AssertScan(t, "&", sql.BITAND, "&") - }) - t.Run("CONCAT", func(t *testing.T) { - AssertScan(t, "||", sql.CONCAT, "||") - }) - t.Run("BITOR", func(t *testing.T) { - AssertScan(t, "|", sql.BITOR, "|") - }) - t.Run("PLUS", func(t *testing.T) { - AssertScan(t, "+", sql.PLUS, "+") - }) - t.Run("MINUS", func(t *testing.T) { - AssertScan(t, "-", sql.MINUS, "-") - }) - t.Run("STAR", func(t *testing.T) { - AssertScan(t, "*", sql.STAR, "*") - }) - t.Run("SLASH", func(t *testing.T) { - AssertScan(t, "/", sql.SLASH, "/") - }) - t.Run("REM", func(t *testing.T) { - AssertScan(t, "%", sql.REM, "%") - }) - t.Run("DOT", func(t *testing.T) { - AssertScan(t, ".", sql.DOT, ".") - }) - t.Run("ILLEGAL", func(t *testing.T) { - AssertScan(t, "^", sql.ILLEGAL, "^") - }) -} - -// AssertScan asserts the value of the first scan to s. -func AssertScan(tb testing.TB, s string, expectedTok sql.Token, expectedLit string) { - tb.Helper() - _, tok, lit := sql.NewScanner(strings.NewReader(s)).Scan() - if tok != expectedTok || lit != expectedLit { - tb.Fatalf("Scan(%q)=<%s,%s>, want <%s,%s>", s, tok, lit, expectedTok, expectedLit) - } -} diff --git a/sql2/token_test.go b/sql2/token_test.go deleted file mode 100644 index 4b71f5e62..000000000 --- a/sql2/token_test.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2_test - -import ( - "testing" - - sql "github.com/featurebasedb/featurebase/v3/sql2" -) - -func TestPos_String(t *testing.T) { - if got, want := (sql.Pos{}).String(), `-`; got != want { - t.Fatalf("String()=%q, want %q", got, want) - } -} diff --git a/sql3/errors.go b/sql3/errors.go new file mode 100644 index 000000000..827971491 --- /dev/null +++ b/sql3/errors.go @@ -0,0 +1,501 @@ +package sql3 + +import ( + "fmt" + "runtime" + + "github.com/molecula/featurebase/v3/errors" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +const ( + ErrInternal errors.Code = "ErrInternal" + + ErrDuplicateColumn errors.Code = "ErrDuplicateColumn" + ErrUnknownType errors.Code = "ErrUnknownType" + + ErrTypeIncompatibleWithBitwiseOperator errors.Code = "ErrTypeIncompatibleWithBitwiseOperator" + ErrTypeIncompatibleWithLogicalOperator errors.Code = "ErrTypeIncompatibleWithLogicalOperator" + ErrTypeIncompatibleWithEqualityOperator errors.Code = "ErrTypeIncompatibleWithEqualityOperator" + ErrTypeIncompatibleWithComparisonOperator errors.Code = "ErrTypeIncompatibleWithComparisonOperator" + ErrTypeIncompatibleWithArithmeticOperator errors.Code = "ErrTypeIncompatibleWithArithmeticOperator" + ErrTypeIncompatibleWithConcatOperator errors.Code = "ErrTypeIncompatibleWithConcatOperator" + ErrTypeIncompatibleWithLikeOperator errors.Code = "ErrTypeIncompatibleWithLikeOperator" + ErrTypeIncompatibleWithBetweenOperator errors.Code = "ErrTypeIncompatibleWithBetweenOperator" + ErrTypeCannotBeUsedAsRangeSubscript errors.Code = "ErrTypeCannotBeUsedAsRangeSubscript" + ErrTypesAreNotEquatable errors.Code = "ErrTypesAreNotEquatable" + ErrTypeMismatch errors.Code = "ErrTypeMismatch" + ErrIncompatibleTypesForRangeSubscripts errors.Code = "ErrIncompatibleTypesForRangeSubscripts" + ErrExpressionListExpected errors.Code = "ErrExpressionListExpected" + ErrBooleanExpressionExpected errors.Code = "ErrBooleanExpressionExpected" + ErrIntExpressionExpected errors.Code = "ErrIntExpressionExpected" + ErrIntOrDecimalExpressionExpected errors.Code = "ErrIntOrDecimalExpressionExpected" + ErrIntOrDecimalOrTimestampExpressionExpected errors.Code = "ErrIntOrDecimalOrTimestampExpressionExpected" + ErrStringExpressionExpected errors.Code = "ErrStringExpressionExpected" + ErrSetExpressionExpected errors.Code = "ErrSetExpressionExpected" + ErrSingleRowExpected errors.Code = "ErrSingleRowExpected" + + ErrInvalidCast errors.Code = "ErrInvalidCast" + ErrInvalidTypeCoercion errors.Code = "ErrInvalidTypeCoercion" + + ErrLiteralExpected errors.Code = "ErrLiteralExpected" + ErrIntegerLiteral errors.Code = "ErrIntegerLiteral" + ErrStringLiteral errors.Code = "ErrStringLiteral" + ErrLiteralEmptySetNotAllowed errors.Code = "ErrLiteralEmptySetNotAllowed" + ErrSetLiteralMustContainIntOrString errors.Code = "ErrSetLiteralMustContainIntOrString" + + ErrTypeAssignmentIncompatible errors.Code = "ErrTypeAssignmentIncompatible" + + ErrInvalidTimeUnit errors.Code = "ErrInvalidTimeUnit" + ErrInvalidTimeEpoch errors.Code = "ErrInvalidTimeEpoch" + ErrInvalidTimeQuantum errors.Code = "ErrInvalidTimeQuantum" + ErrInvalidDuration errors.Code = "ErrInvalidDuration" + + ErrInsertExprTargetCountMismatch errors.Code = "ErrInsertExprTargetCountMismatch" + ErrInsertMustHaveIDColumn errors.Code = "ErrInsertMustHaveIDColumn" + ErrInsertMustAtLeastOneNonIDColumn errors.Code = "ErrInsertMustAtLeastOneNonIDColumn" + + ErrTableMustHaveIDColumn errors.Code = "ErrTableMustHaveIDColumn" + ErrTableIDColumnType errors.Code = "ErrTableIDColumnType" + ErrTableIDColumnConstraints errors.Code = "ErrTableIDColumnConstraints" + ErrTableIDColumnAlter errors.Code = "ErrTableIDColumnAlter" + ErrTableNotFound errors.Code = "ErrTableNotFound" + ErrColumnNotFound errors.Code = "ErrColumnNotFound" + ErrTableColumnNotFound errors.Code = "ErrTableColumnNotFound" + ErrInvalidKeyPartitionsValue errors.Code = "ErrInvalidKeyPartitionsValue" + ErrInvalidShardWidthValue errors.Code = "ErrInvalidShardWidthValue" + + ErrBadColumnConstraint errors.Code = "ErrBadColumnConstraint" + ErrConflictingColumnConstraint errors.Code = "ErrConflictingColumnConstraint" + + // expected errors + ErrExpectedColumnReference errors.Code = "ErrExpectedColumnReference" + + // call errors + ErrCallUnknownFunction errors.Code = "ErrCallUnknownFunction" + ErrCallParameterCountMismatch errors.Code = "ErrCallParameterCountMismatch" + ErrIdColumnNotValidForAggregateFunction errors.Code = "ErrIdColumnNotValidForAggregateFunction" + ErrParameterTypeMistmatch errors.Code = "ErrParameterTypeMistmatch" + ErrCallParameterValueInvalid errors.Code = "ErrCallParameterValueInvalid" + + //optimizer errors + ErrAggregateNotAllowedInGroupBy errors.Code = "ErrIdPercentileNotAllowedInGroupBy" +) + +func NewErrDuplicateColumn(line int, col int, column string) error { + return errors.New( + ErrDuplicateColumn, + fmt.Sprintf("[%d:%d] duplicate column '%s'", line, col, column), + ) +} + +func NewErrUnknownType(line int, col int, typ string) error { + return errors.New( + ErrUnknownType, + fmt.Sprintf("[%d:%d] unknown type '%s'", line, col, typ), + ) +} + +func NewErrInternal(msg string) error { + preamble := "internal error" + _, filename, line, ok := runtime.Caller(1) + if ok { + preamble = fmt.Sprintf("internal error (%s:%d)", filename, line) + } + errorMessage := fmt.Sprintf("%s %s", preamble, msg) + return errors.New( + ErrInternal, + errorMessage, + ) +} + +func NewErrInternalf(format string, a ...interface{}) error { + preamble := "internal error" + _, filename, line, ok := runtime.Caller(1) + if ok { + preamble = fmt.Sprintf("internal error (%s:%d)", filename, line) + } + errorMessage := fmt.Sprintf(format, a...) + errorMessage = fmt.Sprintf("%s %s", preamble, errorMessage) + return errors.New( + ErrInternal, + errorMessage, + ) +} + +func NewErrTypeAssignmentIncompatible(line, col int, type1, type2 string) error { + return errors.New( + ErrTypeAssignmentIncompatible, + fmt.Sprintf("[%d:%d] an expression of type '%s' cannot be assigned to type '%s'", line, col, type1, type2), + ) +} + +func NewErrInvalidCast(line, col int, from, to string) error { + return errors.New( + ErrInvalidCast, + fmt.Sprintf("[%d:%d] '%s' cannot be cast to '%s'", line, col, from, to), + ) +} + +func NewErrInvalidTypeCoercion(line, col int, from, to string) error { + return errors.New( + ErrInvalidTypeCoercion, + fmt.Sprintf("[%d:%d] unable to convert '%s' to type '%s'", line, col, from, to), + ) +} + +func NewErrLiteralExpected(line, col int) error { + return errors.New( + ErrLiteralExpected, + fmt.Sprintf("[%d:%d] literal expression expected", line, col), + ) +} + +func NewErrIntegerLiteral(line, col int) error { + return errors.New( + ErrIntegerLiteral, + fmt.Sprintf("[%d:%d] integer literal expected", line, col), + ) +} + +func NewErrStringLiteral(line, col int) error { + return errors.New( + ErrStringLiteral, + fmt.Sprintf("[%d:%d] string literal expected", line, col), + ) +} + +func NewErrLiteralEmptySetNotAllowed(line, col int) error { + return errors.New( + ErrLiteralEmptySetNotAllowed, + fmt.Sprintf("[%d:%d] set literal must contain at least one member", line, col), + ) +} + +func NewErrSetLiteralMustContainIntOrString(line, col int) error { + return errors.New( + ErrSetLiteralMustContainIntOrString, + fmt.Sprintf("[%d:%d] set literal must contain ints or strings", line, col), + ) +} + +func NewErrTypeIncompatibleWithBitwiseOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithBitwiseOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithLogicalOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithLogicalOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithEqualityOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithEqualityOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithComparisonOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithComparisonOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithArithmeticOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithArithmeticOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithConcatOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithConcatOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithLikeOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithLikeOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeIncompatibleWithBetweenOperator(line, col int, operator, type1 string) error { + return errors.New( + ErrTypeIncompatibleWithBetweenOperator, + fmt.Sprintf("[%d:%d] operator '%s' incompatible with type '%s'", line, col, operator, type1), + ) +} + +func NewErrTypeCannotBeUsedAsRangeSubscript(line, col int, type1 string) error { + return errors.New( + ErrTypeCannotBeUsedAsRangeSubscript, + fmt.Sprintf("[%d:%d] type '%s' cannot be used a range subscript", line, col, type1), + ) +} + +func NewErrIncompatibleTypesForRangeSubscripts(line, col int, type1 string, type2 string) error { + return errors.New( + ErrIncompatibleTypesForRangeSubscripts, + fmt.Sprintf("[%d:%d] incompatible types '%s' and '%s' useds as range subscripts", line, col, type1, type2), + ) +} + +func NewErrTypesAreNotEquatable(line, col int, type1, type2 string) error { + return errors.New( + ErrTypesAreNotEquatable, + fmt.Sprintf("[%d:%d] types '%s' and '%s' are not equatable", line, col, type1, type2), + ) +} + +func NewErrTypeMismatch(line, col int, type1, type2 string) error { + return errors.New( + ErrTypeMismatch, + fmt.Sprintf("[%d:%d] types '%s' and '%s' do not match", line, col, type1, type2), + ) +} + +func NewErrExpressionListExpected(line, col int) error { + return errors.New( + ErrExpressionListExpected, + fmt.Sprintf("[%d:%d] expression list expected", line, col), + ) +} + +func NewErrBooleanExpressionExpected(line, col int) error { + return errors.New( + ErrBooleanExpressionExpected, + fmt.Sprintf("[%d:%d] boolean expression expected", line, col), + ) +} + +func NewErrIntExpressionExpected(line, col int) error { + return errors.New( + ErrIntExpressionExpected, + fmt.Sprintf("[%d:%d] integer expression expected", line, col), + ) +} + +func NewErrIntOrDecimalExpressionExpected(line, col int) error { + return errors.New( + ErrIntOrDecimalExpressionExpected, + fmt.Sprintf("[%d:%d] integer or decimal expression expected", line, col), + ) +} + +func NewErrIntOrDecimalOrTimestampExpressionExpected(line, col int) error { + return errors.New( + ErrIntOrDecimalOrTimestampExpressionExpected, + fmt.Sprintf("[%d:%d] integer, decimal or timestamp expression expected", line, col), + ) +} + +func NewErrStringExpressionExpected(line, col int) error { + return errors.New( + ErrStringExpressionExpected, + fmt.Sprintf("[%d:%d] string expression expected", line, col), + ) +} + +func NewErrSetExpressionExpected(line, col int) error { + return errors.New( + ErrSetExpressionExpected, + fmt.Sprintf("[%d:%d] set expression expected", line, col), + ) +} + +func NewErrSingleRowExpected(line, col int) error { + return errors.New( + ErrSingleRowExpected, + fmt.Sprintf("[%d:%d] single row expected", line, col), + ) +} + +func NewErrInvalidTimeUnit(line, col int, unit string) error { + return errors.New( + ErrInvalidTimeUnit, + fmt.Sprintf("[%d:%d] '%s' is not a valid time unit", line, col, unit), + ) +} + +func NewErrInvalidTimeEpoch(line, col int, epoch string) error { + return errors.New( + ErrInvalidTimeEpoch, + fmt.Sprintf("[%d:%d] '%s' is not a valid epoch", line, col, epoch), + ) +} + +func NewErrInvalidTimeQuantum(line, col int, quantum string) error { + return errors.New( + ErrInvalidTimeQuantum, + fmt.Sprintf("[%d:%d] '%s' is not a valid time quantum", line, col, quantum), + ) +} + +func NewErrInvalidDuration(line, col int, duration string) error { + return errors.New( + ErrInvalidDuration, + fmt.Sprintf("[%d:%d] '%s' is not a valid time duration", line, col, duration), + ) +} + +func NewErrInsertExprTargetCountMismatch(line int, col int) error { + return errors.New( + ErrInsertExprTargetCountMismatch, + fmt.Sprintf("[%d:%d] mismatch in the count of expressions and target columns", line, col), + ) +} + +func NewErrInsertMustHaveIDColumn(line int, col int) error { + return errors.New( + ErrInsertMustHaveIDColumn, + fmt.Sprintf("[%d:%d] insert column list must have '_id' column specified", line, col), + ) +} + +func NewErrInsertMustAtLeastOneNonIDColumn(line int, col int) error { + return errors.New( + ErrInsertMustAtLeastOneNonIDColumn, + fmt.Sprintf("[%d:%d] insert column list must have at least one non '_id' column specified", line, col), + ) +} + +func NewErrTableMustHaveIDColumn(line, col int) error { + return errors.New( + ErrTableMustHaveIDColumn, + fmt.Sprintf("[%d:%d] _id column must be specified", line, col), + ) +} + +func NewErrTableIDColumnType(line, col int) error { + return errors.New( + ErrTableIDColumnType, + fmt.Sprintf("[%d:%d] _id column must be specified with type ID or STRING", line, col), + ) +} + +func NewErrTableIDColumnConstraints(line, col int) error { + return errors.New( + ErrTableIDColumnConstraints, + fmt.Sprintf("[%d:%d] _id column must be specified with no constraints", line, col), + ) +} + +func NewErrTableIDColumnAlter(line, col int) error { + return errors.New( + ErrTableIDColumnAlter, + fmt.Sprintf("[%d:%d] _id column cannot be added to an existing table", line, col), + ) +} + +func NewErrTableNotFound(line, col int, tableName string) error { + return errors.New( + ErrTableNotFound, + fmt.Sprintf("[%d:%d] table '%s' not found", line, col, tableName), + ) +} + +func NewErrColumnNotFound(line, col int, columnName string) error { + return errors.New( + ErrColumnNotFound, + fmt.Sprintf("[%d:%d] column '%s' not found", line, col, columnName), + ) +} + +func NewErrTableColumnNotFound(line, col int, tableName string, columnName string) error { + return errors.New( + ErrTableColumnNotFound, + fmt.Sprintf("[%d:%d] column '%s' not found in table '%s'", line, col, columnName, tableName), + ) +} + +func NewErrInvalidKeyPartitionsValue(line, col int, keypartitions int64) error { + return errors.New( + ErrInvalidKeyPartitionsValue, + fmt.Sprintf("[%d:%d] invalid value '%d' for key partitions (should be a number between 1-10000)", line, col, keypartitions), + ) +} + +func NewErrInvalidShardWidthValue(line, col int, shardwidth int64) error { + return errors.New( + ErrInvalidShardWidthValue, + fmt.Sprintf("[%d:%d] invalid value '%d' for shardwidth (should be a number that is a power of 2 and greater or equal to 2^16)", line, col, shardwidth), + ) +} + +func NewErrBadColumnConstraint(line, col int, constraint, columnType string) error { + return errors.New( + ErrBadColumnConstraint, + fmt.Sprintf("[%d:%d] '%s' constraint cannot be applied to a column of type '%s'", line, col, constraint, columnType), + ) +} + +func NewErrConflictingColumnConstraint(line, col int, token1, token2 parser.Token) error { + return errors.New( + ErrConflictingColumnConstraint, + fmt.Sprintf("[%d:%d] '%s' constraint conflicts with '%s'", line, col, token1, token2), + ) +} + +// expected + +func NewErrExpectedColumnReference(line, col int) error { + return errors.New( + ErrExpectedColumnReference, + fmt.Sprintf("[%d:%d] column reference expected", line, col), + ) +} + +// calls + +func NewErrCallParameterCountMismatch(line, col int, functionName string, formalCount, actualCount int) error { + return errors.New( + ErrCallParameterCountMismatch, + fmt.Sprintf("[%d:%d] '%s': count of formal parameters (%d) does not match count of actual parameters (%d)", line, col, functionName, formalCount, actualCount), + ) +} + +func NewErrCallUnknownFunction(line, col int, functionName string) error { + return errors.New( + ErrCallUnknownFunction, + fmt.Sprintf("[%d:%d] unknown function '%s'", line, col, functionName), + ) +} + +func NewErrIdColumnNotValidForAggregateFunction(line, col int, functionName string) error { + return errors.New( + ErrIdColumnNotValidForAggregateFunction, + fmt.Sprintf("[%d:%d] _id column cannot be used in aggregate function '%s'", line, col, functionName), + ) +} + +func NewErrParameterTypeMistmatch(line, col int, type1, type2 string) error { + return errors.New( + ErrParameterTypeMistmatch, + fmt.Sprintf("[%d:%d] an expression of type '%s' cannot be passed to a parameter of type '%s'", line, col, type1, type2), + ) +} + +func NewErrCallParameterValueInvalid(line, col int, badParameterValue string, parameterName string) error { + return errors.New( + ErrCallParameterValueInvalid, + fmt.Sprintf("[%d:%d] invalid value '%s' for parameter '%s'", line, col, badParameterValue, parameterName), + ) +} + +// optimizer + +func NewErrAggregateNotAllowedInGroupBy(line, col int, aggName string) error { + return errors.New( + ErrAggregateNotAllowedInGroupBy, + fmt.Sprintf("[%d:%d] aggregate '%s' not allowed in GROUP BY", line, col, aggName), + ) +} diff --git a/sql3/interfaces.go b/sql3/interfaces.go new file mode 100644 index 000000000..399449e8e --- /dev/null +++ b/sql3/interfaces.go @@ -0,0 +1,27 @@ +// Package sql3 contains the latest version of FeatureBase SQL support. +package sql3 + +import ( + "context" + + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +type CompilePlanner interface { + CompilePlan(context.Context, parser.Statement) (types.PlanOperator, error) +} + +// Ensure type implements interface. +var _ CompilePlanner = (*NopCompilePlanner)(nil) + +// NopCompilePlanner is a no-op implementation of the CompilePlanner interface. +type NopCompilePlanner struct{} + +func NewNopCompilePlanner() *NopCompilePlanner { + return &NopCompilePlanner{} +} + +func (p *NopCompilePlanner) CompilePlan(ctx context.Context, stmt parser.Statement) (types.PlanOperator, error) { + return nil, nil +} diff --git a/sql2/ast.go b/sql3/parser/ast.go similarity index 77% rename from sql2/ast.go rename to sql3/parser/ast.go index 9ab0d8f96..0ebaca648 100644 --- a/sql2/ast.go +++ b/sql3/parser/ast.go @@ -1,11 +1,11 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2 +// Copyright 2021 Molecula Corp. All rights reserved. +package parser import ( "bytes" "fmt" "strings" + "time" ) type Node interface { @@ -16,11 +16,13 @@ type Node interface { func (*AlterTableStatement) node() {} func (*AnalyzeStatement) node() {} func (*Assignment) node() {} +func (*ShowTablesStatement) node() {} +func (*ShowColumnsStatement) node() {} func (*BeginStatement) node() {} func (*BinaryExpr) node() {} -func (*BindExpr) node() {} -func (*BlobLit) node() {} func (*BoolLit) node() {} +func (*BulkInsertStatement) node() {} +func (*CacheTypeConstraint) node() {} func (*Call) node() {} func (*CaseBlock) node() {} func (*CaseExpr) node() {} @@ -32,6 +34,7 @@ func (*CreateIndexStatement) node() {} func (*CreateTableStatement) node() {} func (*CreateTriggerStatement) node() {} func (*CreateViewStatement) node() {} +func (*DateLit) node() {} func (*DefaultConstraint) node() {} func (*DeleteStatement) node() {} func (*DropIndexStatement) node() {} @@ -42,6 +45,7 @@ func (*Exists) node() {} func (*ExplainStatement) node() {} func (*ExprList) node() {} func (*FilterClause) node() {} +func (*FloatLit) node() {} func (*ForeignKeyArg) node() {} func (*ForeignKeyConstraint) node() {} func (*FrameSpec) node() {} @@ -50,25 +54,31 @@ func (*IndexedColumn) node() {} func (*InsertStatement) node() {} func (*JoinClause) node() {} func (*JoinOperator) node() {} +func (*KeyPartitionsOption) node() {} +func (*MinConstraint) node() {} +func (*MaxConstraint) node() {} func (*NotNullConstraint) node() {} func (*NullLit) node() {} -func (*NumberLit) node() {} +func (*IntegerLit) node() {} func (*OnConstraint) node() {} func (*OrderingTerm) node() {} func (*OverClause) node() {} func (*ParenExpr) node() {} +func (*SetLiteralExpr) node() {} func (*ParenSource) node() {} func (*PrimaryKeyConstraint) node() {} func (*QualifiedRef) node() {} func (*QualifiedTableName) node() {} -func (*Raise) node() {} func (*Range) node() {} func (*ReleaseStatement) node() {} func (*ResultColumn) node() {} func (*RollbackStatement) node() {} func (*SavepointStatement) node() {} func (*SelectStatement) node() {} +func (*ShardWidthOption) node() {} func (*StringLit) node() {} +func (*TimeUnitConstraint) node() {} +func (*TimeQuantumConstraint) node() {} func (*Type) node() {} func (*UnaryExpr) node() {} func (*UniqueConstraint) node() {} @@ -87,6 +97,9 @@ type Statement interface { func (*AlterTableStatement) stmt() {} func (*AnalyzeStatement) stmt() {} func (*BeginStatement) stmt() {} +func (*BulkInsertStatement) stmt() {} +func (*ShowTablesStatement) stmt() {} +func (*ShowColumnsStatement) stmt() {} func (*CommitStatement) stmt() {} func (*CreateIndexStatement) stmt() {} func (*CreateTableStatement) stmt() {} @@ -182,51 +195,35 @@ func StatementSource(stmt Statement) Source { } } -// Data types -const ( - DataTypeBool = "BOOL" - DataTypeDecimal = "DECIMAL" - DataTypeInt = "INT" - DataTypeSet = "SET" - DataTypeText = "TEXT" - DataTypeTimestamp = "TIMESTAMP" -) - -// IsDataTypeValid returns true if typ is a valid data type. -func IsDataTypeValid(typ string) bool { - switch typ { - case DataTypeBool, DataTypeInt, DataTypeDecimal, DataTypeText: - return true - default: - return false - } -} - type Expr interface { Node expr() - IsAggregate() bool + IsLiteral() bool + DataType() ExprDataType + Pos() Pos } -func (*BinaryExpr) expr() {} -func (*BindExpr) expr() {} -func (*BlobLit) expr() {} -func (*BoolLit) expr() {} -func (*Call) expr() {} -func (*CaseExpr) expr() {} -func (*CastExpr) expr() {} -func (*Exists) expr() {} -func (*ExprList) expr() {} -func (*Ident) expr() {} -func (*NullLit) expr() {} -func (*NumberLit) expr() {} -func (*ParenExpr) expr() {} -func (*QualifiedRef) expr() {} -func (*Raise) expr() {} -func (*Range) expr() {} -func (*StringLit) expr() {} -func (*UnaryExpr) expr() {} +func (*BinaryExpr) expr() {} +func (*BoolLit) expr() {} +func (*Call) expr() {} +func (*CaseExpr) expr() {} +func (*CaseBlock) expr() {} +func (*CastExpr) expr() {} +func (*DateLit) expr() {} +func (*Exists) expr() {} +func (*ExprList) expr() {} +func (*Ident) expr() {} +func (*NullLit) expr() {} +func (*IntegerLit) expr() {} +func (*FloatLit) expr() {} +func (*ParenExpr) expr() {} +func (*SetLiteralExpr) expr() {} +func (*QualifiedRef) expr() {} +func (*Range) expr() {} +func (*StringLit) expr() {} +func (*UnaryExpr) expr() {} +func (*SelectStatement) expr() {} // CloneExpr returns a deep copy expr. func CloneExpr(expr Expr) Expr { @@ -237,10 +234,6 @@ func CloneExpr(expr Expr) Expr { switch expr := expr.(type) { case *BinaryExpr: return expr.Clone() - case *BindExpr: - return expr.Clone() - case *BlobLit: - return expr.Clone() case *BoolLit: return expr.Clone() case *Call: @@ -257,14 +250,12 @@ func CloneExpr(expr Expr) Expr { return expr.Clone() case *NullLit: return expr.Clone() - case *NumberLit: + case *IntegerLit: return expr.Clone() case *ParenExpr: return expr.Clone() case *QualifiedRef: return expr.Clone() - case *Raise: - return expr.Clone() case *Range: return expr.Clone() case *StringLit: @@ -287,49 +278,6 @@ func cloneExprs(a []Expr) []Expr { return other } -// ExprDataType returns the data type for an expression. -func ExprDataType(expr Expr) string { - if expr == nil { - return "" - } - - switch expr := expr.(type) { - // Simple type assertions - case *BindExpr, *ExprList, *Ident, *NullLit, *Raise: - return "" - case *BlobLit, *StringLit: - return DataTypeText - case *BoolLit, *Exists, *Range: - return DataTypeBool - case *NumberLit: - return DataTypeInt - - // Complex type assertions - case *BinaryExpr: - return ExprDataType(expr.X) - case *Call: - return DataTypeInt // TODO: May be different for some aggregations - case *CaseExpr: - if len(expr.Blocks) > 0 { - return ExprDataType(expr.Blocks[0].Body) - } else if expr.ElseExpr != nil { - return ExprDataType(expr.ElseExpr) - } - return "" - case *CastExpr: - return "" // TODO: Inspect expr.Type.Name - case *ParenExpr: - return ExprDataType(expr.X) - case *QualifiedRef: - return expr.DataType - case *UnaryExpr: - return ExprDataType(expr.X) - - default: - panic(fmt.Sprintf("invalid expr type: %T", expr)) - } -} - // ExprString returns the string representation of expr. // Returns a blank string if expr is nil. func ExprString(expr Expr) string { @@ -339,119 +287,6 @@ func ExprString(expr Expr) string { return expr.String() } -// ExprTableName returns the name of the table referenced in an expression. -// Returns ok as false if more than one table referenced. Returns a blank string -// if no tables are referenced. -func ExprTableName(expr Expr) (table string, ok bool) { - switch expr := expr.(type) { - case *BindExpr, *BlobLit, *BoolLit, *Ident, *NullLit, *NumberLit, *StringLit: - return "", true - - case *BinaryExpr: - x, ok := ExprTableName(expr.X) - if !ok { - return "", false - } - - y, ok := ExprTableName(expr.Y) - if !ok { - return "", false - } - - if x == "" { - return y, true - } else if y == "" { - return x, true - } else if x == y { - return x, true - } - return "", false - - case *Call: - for _, arg := range expr.Args { - tbl, ok := ExprTableName(arg) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - } - return table, true - - case *CaseExpr: - tbl, ok := ExprTableName(expr.Operand) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - - tbl, ok = ExprTableName(expr.ElseExpr) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - - for _, blk := range expr.Blocks { - tbl, ok := ExprTableName(blk.Condition) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - - tbl, ok = ExprTableName(blk.Body) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - } - return table, true - - case *CastExpr: - return ExprTableName(expr.X) - - case *Exists: - return "", false // TODO - - case *ExprList: - for _, e := range expr.Exprs { - tbl, ok := ExprTableName(e) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - } - return table, true - - case *ParenExpr: - return ExprTableName(expr.X) - - case *QualifiedRef: - return expr.Table.Name, true - - case *Raise: - return "", true - - case *Range: - tbl, ok := ExprTableName(expr.X) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - - tbl, ok = ExprTableName(expr.Y) - if !ok || (table != "" && tbl != table) { - return "", false - } - table = tbl - return table, true - - case *UnaryExpr: - return ExprTableName(expr.X) - - default: - return "", false - } -} - // SplitExprTree splits apart expr so it is a list of all AND joined expressions. // For example, the expression "A AND B AND (C OR (D AND E))" would be split into // a list of "A", "B", "C OR (D AND E)". @@ -481,17 +316,31 @@ func splitExprTree(expr Expr, a *[]Expr) { } } -// Scope represents a context for name resolution. -// Names can be resolved at the current source or in parent scopes. -type Scope struct { - Parent *Scope - Source Source +// SourceOutputColumn is an identifier that is either a possible output column +// for a Source or a referenced output column for a Source. These are computed during +// the analysis phase +type SourceOutputColumn struct { + TableName string + ColumnName string + ColumnIndex int + Datatype ExprDataType } -// Source represents a table or subquery. +// Source represents a data source for a select statement. +// A select statement has one source, but they can be one of a table ref, a join, +// another select statement or any of the above parenthesiszed. For join operators, the Source +// can form a graph, with the join terms being themselves a Source. type Source interface { Node source() + SourceFromAlias(alias string) Source + + // get the possible output columns from the source + PossibleOutputColumns() []*SourceOutputColumn + + // find output columns by name + OutputColumnNamed(name string) (*SourceOutputColumn, error) + OutputColumnQualifierNamed(qualifier string, name string) (*SourceOutputColumn, error) } func (*JoinClause) source() {} @@ -504,7 +353,6 @@ func CloneSource(src Source) Source { if src == nil { return nil } - switch src := src.(type) { case *JoinClause: return src.Clone() @@ -519,22 +367,7 @@ func CloneSource(src Source) Source { } } -// SourceName returns the name of the source. -// Only returns for QualifiedTableName & ParenSource. -func SourceName(src Source) string { - switch src := src.(type) { - case *JoinClause, *SelectStatement: - return "" - case *ParenSource: - return IdentName(src.Alias) - case *QualifiedTableName: - return src.TableName() - default: - return "" - } -} - -// SourceList returns a list of scopes in the current scope. +// SourceList returns a list of sources starting from a source. func SourceList(src Source) []Source { var a []Source ForEachSource(src, func(s Source) bool { @@ -570,26 +403,6 @@ func forEachSource(src Source, fn func(Source) bool) bool { return true } -// ResolveSource returns a source with the given name. -// This can either be the table name or the alias for a source. -func ResolveSource(root Source, name string) Source { - var ret Source - ForEachSource(root, func(src Source) bool { - switch src := src.(type) { - case *ParenSource: - if IdentName(src.Alias) == name { - ret = src - } - case *QualifiedTableName: - if src.TableName() == name { - ret = src - } - } - return ret == nil // continue until we find the matching source - }) - return ret -} - // JoinConstraint represents either an ON or USING join constraint. type JoinConstraint interface { Node @@ -643,6 +456,34 @@ func (s *ExplainStatement) String() string { return buf.String() } +type ShowTablesStatement struct { + Show Pos // position of SHOW + Tables Pos // position of TABLES +} + +// String returns the string representation of the statement. +func (s *ShowTablesStatement) String() string { + return "SHOW TABLES" +} + +type ShowColumnsStatement struct { + Show Pos // position of SHOW + Columns Pos // position of COLUMNS + From Pos // position of FROM + TableName *Ident // name of table +} + +// String returns the string representation of the statement. +func (s *ShowColumnsStatement) String() string { + var buf bytes.Buffer + buf.WriteString("SHOW COLUMNS ") + if s.TableName != nil { + buf.WriteString(" FROM") + fmt.Fprintf(&buf, " %s", s.TableName.String()) + } + return buf.String() +} + type BeginStatement struct { Begin Pos // position of BEGIN Deferred Pos // position of DEFERRED keyword @@ -803,8 +644,9 @@ type CreateTableStatement struct { Constraints []Constraint // table constraints Rparen Pos // position of right paren of column list - As Pos // position of AS keyword (optional) - Select *SelectStatement // select stmt to build from + As Pos // position of AS keyword (optional) + Select *SelectStatement // select stmt to build from + Options []TableOption // table options } // Clone returns a deep copy of s. @@ -893,17 +735,56 @@ func (c *ColumnDefinition) String() string { return buf.String() } +type TableOption interface { + Node + option() +} + +func (*KeyPartitionsOption) option() {} +func (*ShardWidthOption) option() {} + +type KeyPartitionsOption struct { + KeyPartitions Pos // position of KEYPARTITIONS keyword + Expr Expr // expression +} + +func (o *KeyPartitionsOption) String() string { + var buf bytes.Buffer + buf.WriteString("KEYPARTITIONS (") + buf.WriteString(o.Expr.String()) + buf.WriteString(")") + return buf.String() +} + +type ShardWidthOption struct { + ShardWidth Pos // position of SHARDWIDTH keyword + Expr Expr // expression +} + +func (o *ShardWidthOption) String() string { + var buf bytes.Buffer + buf.WriteString("SHARDWIDTH (") + buf.WriteString(o.Expr.String()) + buf.WriteString(")") + return buf.String() +} + type Constraint interface { Node constraint() } -func (*PrimaryKeyConstraint) constraint() {} -func (*NotNullConstraint) constraint() {} -func (*UniqueConstraint) constraint() {} -func (*CheckConstraint) constraint() {} -func (*DefaultConstraint) constraint() {} -func (*ForeignKeyConstraint) constraint() {} +func (*PrimaryKeyConstraint) constraint() {} +func (*NotNullConstraint) constraint() {} +func (*UniqueConstraint) constraint() {} +func (*CheckConstraint) constraint() {} +func (*DefaultConstraint) constraint() {} +func (*ForeignKeyConstraint) constraint() {} +func (*MinConstraint) constraint() {} +func (*MaxConstraint) constraint() {} +func (*CacheTypeConstraint) constraint() {} +func (*TimeUnitConstraint) constraint() {} +func (*TimeQuantumConstraint) constraint() {} // CloneConstraint returns a deep copy cons. func CloneConstraint(cons Constraint) Constraint { @@ -1069,6 +950,136 @@ func (c *UniqueConstraint) String() string { return buf.String() } +type MinConstraint struct { + Min Pos // position of MIN keyword + Expr Expr // min expression +} + +// Clone returns a deep copy of c. +func (c *MinConstraint) Clone() *MinConstraint { + if c == nil { + return c + } + other := *c + other.Expr = CloneExpr(c.Expr) + return &other +} + +// String returns the string representation of the constraint. +func (c *MinConstraint) String() string { + var buf bytes.Buffer + buf.WriteString("MIN ") + buf.WriteString(c.Expr.String()) + return buf.String() +} + +type MaxConstraint struct { + Max Pos // position of MAX keyword + Expr Expr // check expression +} + +// Clone returns a deep copy of c. +func (c *MaxConstraint) Clone() *MaxConstraint { + if c == nil { + return c + } + other := *c + other.Expr = CloneExpr(c.Expr) + return &other +} + +// String returns the string representation of the constraint. +func (c *MaxConstraint) String() string { + var buf bytes.Buffer + buf.WriteString("MAX ") + buf.WriteString(c.Expr.String()) + return buf.String() +} + +type CacheTypeConstraint struct { + CacheType Pos // position of CACHETYPE keyword + CacheTypeValue string + Size Pos // position of SIZE keyword + SizeExpr Expr // check expression +} + +// Clone returns a deep copy of c. +func (c *CacheTypeConstraint) Clone() *CacheTypeConstraint { + if c == nil { + return c + } + other := *c + other.SizeExpr = CloneExpr(c.SizeExpr) + return &other +} + +// String returns the string representation of the constraint. +func (c *CacheTypeConstraint) String() string { + var buf bytes.Buffer + buf.WriteString("CACHETYPE ") + buf.WriteString(c.CacheTypeValue) + if c.Size.IsValid() { + buf.WriteString(" SIZE ") + buf.WriteString(c.SizeExpr.String()) + } + return buf.String() +} + +type TimeUnitConstraint struct { + TimeUnit Pos // position of TIMEUNIT keyword + Expr Expr // expression + Epoch Pos // position of TIMEUNIT keyword + EpochExpr Expr // expression +} + +// Clone returns a deep copy of c. +func (c *TimeUnitConstraint) Clone() *TimeUnitConstraint { + if c == nil { + return c + } + other := *c + other.Expr = CloneExpr(c.Expr) + return &other +} + +// String returns the string representation of the constraint. +func (c *TimeUnitConstraint) String() string { + var buf bytes.Buffer + buf.WriteString("TIMEUNIT ") + buf.WriteString(c.Expr.String()) + if c.Epoch.IsValid() { + buf.WriteString(" EPOCH ") + buf.WriteString(c.EpochExpr.String()) + } + return buf.String() +} + +type TimeQuantumConstraint struct { + TimeQuantum Pos // position of TIMEQUANTUM keyword + Expr Expr // expression + Ttl Pos + TtlExpr Expr +} + +// Clone returns a deep copy of c. +func (c *TimeQuantumConstraint) Clone() *TimeQuantumConstraint { + if c == nil { + return c + } + other := *c + other.Expr = CloneExpr(c.Expr) + return &other +} + +// String returns the string representation of the constraint. +func (c *TimeQuantumConstraint) String() string { + var buf bytes.Buffer + buf.WriteString("TIMEQUANTUM (") + buf.WriteString(c.Expr.String()) + buf.WriteString(")") + return buf.String() +} + type CheckConstraint struct { Constraint Pos // position of CONSTRAINT keyword Name *Ident // constraint name @@ -1320,18 +1331,22 @@ type AlterTableStatement struct { Table Pos // position of TABLE keyword Name *Ident // table name - Rename Pos // position of RENAME keyword - RenameTo Pos // position of TO keyword after RENAME - NewName *Ident // new table name + Rename Pos // position of RENAME keyword + //RenameTo Pos // position of TO keyword after RENAME + //NewName *Ident // new table name RenameColumn Pos // position of COLUMN keyword after RENAME - ColumnName *Ident // new column name + OldColumnName *Ident // old column name To Pos // position of TO keyword NewColumnName *Ident // new column name Add Pos // position of ADD keyword AddColumn Pos // position of COLUMN keyword after ADD ColumnDef *ColumnDefinition // new column definition + + Drop Pos // position of ADD keyword + DropColumn Pos // position of COLUMN keyword after ADD + DropColumnName *Ident // drop column name } // Clone returns a deep copy of s. @@ -1341,10 +1356,11 @@ func (s *AlterTableStatement) Clone() *AlterTableStatement { } other := *s other.Name = other.Name.Clone() - other.NewName = s.NewName.Clone() - other.ColumnName = s.ColumnName.Clone() + //other.NewName = s.NewName.Clone() + other.OldColumnName = s.OldColumnName.Clone() other.NewColumnName = s.NewColumnName.Clone() other.ColumnDef = s.ColumnDef.Clone() + other.DropColumnName = s.DropColumnName.Clone() return &other } @@ -1354,19 +1370,21 @@ func (s *AlterTableStatement) String() string { buf.WriteString("ALTER TABLE ") buf.WriteString(s.Name.String()) - if s.NewName != nil { - buf.WriteString(" RENAME TO ") - buf.WriteString(s.NewName.String()) - } else if s.ColumnName != nil { + if s.OldColumnName != nil { buf.WriteString(" RENAME COLUMN ") - buf.WriteString(s.ColumnName.String()) + buf.WriteString(s.OldColumnName.String()) buf.WriteString(" TO ") buf.WriteString(s.NewColumnName.String()) + } else if s.DropColumnName != nil { + buf.WriteString(" DROP COLUMN ") + buf.WriteString(s.DropColumnName.String()) } else if s.ColumnDef != nil { buf.WriteString(" ADD COLUMN ") + if s.AddColumn.IsValid() { + buf.WriteString(" COLUMN ") + } buf.WriteString(s.ColumnDef.String()) } - return buf.String() } @@ -1376,8 +1394,15 @@ type Ident struct { Quoted bool // true if double quoted } -// IsAggregate returns false. -func (expr *Ident) IsAggregate() bool { return false } +func (expr *Ident) IsLiteral() bool { return false } + +func (expr *Ident) DataType() ExprDataType { + return NewDataTypeVoid() +} + +func (expr *Ident) Pos() Pos { + return expr.NamePos +} // Clone returns a deep copy of i. func (i *Ident) Clone() *Ident { @@ -1413,11 +1438,11 @@ func IdentName(ident *Ident) string { } type Type struct { - Name *Ident // type name - Lparen Pos // position of left paren (optional) - Precision *NumberLit // precision (optional) - Scale *NumberLit // scale (optional) - Rparen Pos // position of right paren (optional) + Name *Ident // type name + Lparen Pos // position of left paren (optional) + Precision *IntegerLit // precision (optional) + Scale *IntegerLit // scale (optional) + Rparen Pos // position of right paren (optional) } // Clone returns a deep copy of t. @@ -1447,8 +1472,28 @@ type StringLit struct { Value string // literal value (without quotes) } -// IsAggregate returns false. -func (expr *StringLit) IsAggregate() bool { return false } +func (expr *StringLit) IsLiteral() bool { return true } + +func (expr *StringLit) DataType() ExprDataType { + return NewDataTypeString() +} + +func (expr *StringLit) Pos() Pos { + return expr.ValuePos +} + +func (expr *StringLit) ConvertToTimestamp() *DateLit { + //try to coerce to a date + if tm, err := time.ParseInLocation(time.RFC3339Nano, expr.Value, time.UTC); err == nil { + return &DateLit{ValuePos: expr.ValuePos, Value: tm} + } else if tm, err := time.ParseInLocation(time.RFC3339, expr.Value, time.UTC); err == nil { + return &DateLit{ValuePos: expr.ValuePos, Value: tm} + } else if tm, err := time.ParseInLocation("2006-01-02", expr.Value, time.UTC); err == nil { + return &DateLit{ValuePos: expr.ValuePos, Value: tm} + } else { + return nil + } +} // Clone returns a deep copy of lit. func (lit *StringLit) Clone() *StringLit { @@ -1464,16 +1509,23 @@ func (lit *StringLit) String() string { return `'` + strings.Replace(lit.Value, `'`, `''`, -1) + `'` } -type BlobLit struct { +type IntegerLit struct { ValuePos Pos // literal position Value string // literal value } -// IsAggregate returns false. -func (expr *BlobLit) IsAggregate() bool { return false } +func (expr *IntegerLit) IsLiteral() bool { return true } + +func (expr *IntegerLit) DataType() ExprDataType { + return NewDataTypeInt() +} + +func (expr *IntegerLit) Pos() Pos { + return expr.ValuePos +} // Clone returns a deep copy of lit. -func (lit *BlobLit) Clone() *BlobLit { +func (lit *IntegerLit) Clone() *IntegerLit { if lit == nil { return nil } @@ -1482,27 +1534,29 @@ func (lit *BlobLit) Clone() *BlobLit { } // String returns the string representation of the expression. -func (lit *BlobLit) String() string { - return `x'` + lit.Value + `'` +func (lit *IntegerLit) String() string { + return lit.Value } -type NumberLit struct { +type FloatLit struct { ValuePos Pos // literal position Value string // literal value } -// IsAggregate returns false. -func (expr *NumberLit) IsAggregate() bool { return false } +func (expr *FloatLit) IsLiteral() bool { return true } -// IsFloat returns true if literal contains a dot or 'e'. -func (expr *NumberLit) IsFloat() bool { - return strings.Contains(expr.Value, ".") || - strings.Contains(expr.Value, "e") || - strings.Contains(expr.Value, "E") +func (expr *FloatLit) DataType() ExprDataType { + //how many decimal places do we have on the right of the point? + scale := NumDecimalPlaces(expr.Value) + return NewDataTypeDecimal(int64(scale)) +} + +func (expr *FloatLit) Pos() Pos { + return expr.ValuePos } // Clone returns a deep copy of lit. -func (lit *NumberLit) Clone() *NumberLit { +func (lit *FloatLit) Clone() *FloatLit { if lit == nil { return nil } @@ -1511,16 +1565,23 @@ func (lit *NumberLit) Clone() *NumberLit { } // String returns the string representation of the expression. -func (lit *NumberLit) String() string { +func (lit *FloatLit) String() string { return lit.Value } type NullLit struct { - Pos Pos + ValuePos Pos } -// IsAggregate returns false. -func (expr *NullLit) IsAggregate() bool { return false } +func (expr *NullLit) IsLiteral() bool { return true } + +func (expr *NullLit) DataType() ExprDataType { + return NewDataTypeVoid() +} + +func (expr *NullLit) Pos() Pos { + return expr.ValuePos +} // Clone returns a deep copy of lit. func (lit *NullLit) Clone() *NullLit { @@ -1541,8 +1602,15 @@ type BoolLit struct { Value bool // literal value } -// IsAggregate returns false. -func (expr *BoolLit) IsAggregate() bool { return false } +func (expr *BoolLit) IsLiteral() bool { return true } + +func (expr *BoolLit) DataType() ExprDataType { + return NewDataTypeBool() +} + +func (expr *BoolLit) Pos() Pos { + return expr.ValuePos +} // Clone returns a deep copy of lit. func (lit *BoolLit) Clone() *BoolLit { @@ -1561,38 +1629,53 @@ func (lit *BoolLit) String() string { return "FALSE" } -type BindExpr struct { - NamePos Pos // name position - Name string // binding name +type DateLit struct { + ValuePos Pos // literal position + Value time.Time // literal value } -// IsAggregate returns false. -func (expr *BindExpr) IsAggregate() bool { return false } +func (expr *DateLit) IsLiteral() bool { return true } -// Clone returns a deep copy of expr. -func (expr *BindExpr) Clone() *BindExpr { - if expr == nil { +func (expr *DateLit) DataType() ExprDataType { + return NewDataTypeTimestamp() +} + +func (expr *DateLit) Pos() Pos { + return expr.ValuePos +} + +// Clone returns a deep copy of lit. +func (lit *DateLit) Clone() *DateLit { + if lit == nil { return nil } - other := *expr + other := *lit return &other } // String returns the string representation of the expression. -func (expr *BindExpr) String() string { - // TODO(BBJ): Support all bind characters. - return "$" + expr.Name +func (lit *DateLit) String() string { + return lit.Value.Format(time.RFC3339) } type UnaryExpr struct { OpPos Pos // operation position Op Token // operation X Expr // target expression + + ResultDataType ExprDataType } -// IsAggregate returns true if it contains an aggregate call. -func (expr *UnaryExpr) IsAggregate() bool { - return expr.X.IsAggregate() +func (expr *UnaryExpr) IsLiteral() bool { + return expr.X.IsLiteral() +} + +func (expr *UnaryExpr) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *UnaryExpr) Pos() Pos { + return expr.OpPos } // Clone returns a deep copy of expr. @@ -1612,6 +1695,8 @@ func (expr *UnaryExpr) String() string { return "+" + expr.X.String() case MINUS: return "-" + expr.X.String() + case BITNOT: + return "!" + expr.X.String() default: panic(fmt.Sprintf("sql.UnaryExpr.String(): invalid op %s", expr.Op)) } @@ -1622,14 +1707,20 @@ type BinaryExpr struct { OpPos Pos // position of Op Op Token // operator Y Expr // rhs + + ResultDataType ExprDataType } -// IsAggregate returns true if it contains an aggregate call. -func (expr *BinaryExpr) IsAggregate() bool { - if expr.X.IsAggregate() { - return true - } - return expr.Y.IsAggregate() +func (expr *BinaryExpr) IsLiteral() bool { + return expr.X.IsLiteral() && expr.Y.IsLiteral() +} + +func (expr *BinaryExpr) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *BinaryExpr) Pos() Pos { + return expr.X.Pos() } // Clone returns a deep copy of expr. @@ -1722,11 +1813,20 @@ type CastExpr struct { As Pos // position of AS keyword Type *Type // cast type Rparen Pos // position of right paren + + ResultDataType ExprDataType } -// IsAggregate returns true if it contains an aggregate call. -func (expr *CastExpr) IsAggregate() bool { - return expr.X.IsAggregate() +func (expr *CastExpr) IsLiteral() bool { + return expr.X.IsLiteral() +} + +func (expr *CastExpr) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *CastExpr) Pos() Pos { + return expr.Cast } // Clone returns a deep copy of expr. @@ -1752,10 +1852,19 @@ type CaseExpr struct { Else Pos // position of ELSE keyword ElseExpr Expr // expression used by default case End Pos // position of END keyword + + ResultDataType ExprDataType } -// IsAggregate returns false -func (expr *CaseExpr) IsAggregate() bool { return false } +func (expr *CaseExpr) IsLiteral() bool { return false } + +func (expr *CaseExpr) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *CaseExpr) Pos() Pos { + return expr.Case +} // Clone returns a deep copy of expr. func (expr *CaseExpr) Clone() *CaseExpr { @@ -1796,6 +1905,16 @@ type CaseBlock struct { Body Expr // result expression } +func (expr *CaseBlock) IsLiteral() bool { return false } + +func (expr *CaseBlock) DataType() ExprDataType { + return expr.Body.DataType() +} + +func (expr *CaseBlock) Pos() Pos { + return expr.When +} + // Clone returns a deep copy of blk. func (blk *CaseBlock) Clone() *CaseBlock { if blk == nil { @@ -1823,48 +1942,6 @@ func (b *CaseBlock) String() string { return fmt.Sprintf("WHEN %s THEN %s", b.Condition.String(), b.Body.String()) } -type Raise struct { - Raise Pos // position of RAISE keyword - Lparen Pos // position of left paren - Ignore Pos // position of IGNORE keyword - Rollback Pos // position of ROLLBACK keyword - Abort Pos // position of ABORT keyword - Fail Pos // position of FAIL keyword - Comma Pos // position of comma - Error *StringLit // error message - Rparen Pos // position of right paren -} - -// IsAggregate returns false. -func (expr *Raise) IsAggregate() bool { return false } - -// Clone returns a deep copy of r. -func (r *Raise) Clone() *Raise { - if r == nil { - return nil - } - other := *r - other.Error = r.Error.Clone() - return &other -} - -// String returns the string representation of the raise function. -func (r *Raise) String() string { - var buf bytes.Buffer - buf.WriteString("RAISE(") - if r.Rollback.IsValid() { - fmt.Fprintf(&buf, "ROLLBACK, %s", r.Error.String()) - } else if r.Abort.IsValid() { - fmt.Fprintf(&buf, "ABORT, %s", r.Error.String()) - } else if r.Fail.IsValid() { - fmt.Fprintf(&buf, "FAIL, %s", r.Error.String()) - } else { - buf.WriteString("IGNORE") - } - buf.WriteString(")") - return buf.String() -} - type Exists struct { Not Pos // position of optional NOT keyword Exists Pos // position of EXISTS keyword @@ -1873,8 +1950,18 @@ type Exists struct { Rparen Pos // position of right paren } -// IsAggregate returns false. -func (expr *Exists) IsAggregate() bool { return false } +func (expr *Exists) IsLiteral() bool { return false } + +func (expr *Exists) DataType() ExprDataType { + return NewDataTypeBool() +} + +func (expr *Exists) Pos() Pos { + if expr.Not.IsValid() { + return expr.Not + } + return expr.Exists +} // Clone returns a deep copy of expr. func (expr *Exists) Clone() *Exists { @@ -1900,14 +1987,21 @@ type ExprList struct { Rparen Pos // position of right paren } -// IsAggregate returns true if any child expression is an aggregate. -func (expr *ExprList) IsAggregate() bool { +func (expr *ExprList) IsLiteral() bool { for _, e := range expr.Exprs { - if e.IsAggregate() { - return true + if !e.IsLiteral() { + return false } } - return false + return true +} + +func (expr *ExprList) DataType() ExprDataType { + return NewDataTypeVoid() +} + +func (expr *ExprList) Pos() Pos { + return expr.Lparen } // Clone returns a deep copy of l. @@ -1920,7 +2014,7 @@ func (l *ExprList) Clone() *ExprList { return &other } -func cloneExprLists(a []*ExprList) []*ExprList { +/*func cloneExprLists(a []*ExprList) []*ExprList { if a == nil { return nil } @@ -1929,7 +2023,7 @@ func cloneExprLists(a []*ExprList) []*ExprList { other[i] = a[i].Clone() } return other -} +}*/ // String returns the string representation of the expression. func (l *ExprList) String() string { @@ -1949,10 +2043,19 @@ type Range struct { X Expr // lhs expression And Pos // position of AND keyword Y Expr // rhs expression + + ResultDataType ExprDataType } -// IsAggregate returns false. -func (expr *Range) IsAggregate() bool { return false } +func (expr *Range) IsLiteral() bool { return false } + +func (expr *Range) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *Range) Pos() Pos { + return expr.X.Pos() +} // Clone returns a deep copy of r. func (r *Range) Clone() *Range { @@ -1971,17 +2074,25 @@ func (r *Range) String() string { } type QualifiedRef struct { - Table *Ident // table name - Dot Pos // position of dot - Star Pos // position of * (result column only) - Column *Ident // column name + Table *Ident // table name + Dot Pos // position of dot + Star Pos // position of * (result column only) + Column *Ident // column name + ColumnIndex int // Set by the planner; not at parse-time - DataType string + RefDataType ExprDataType } -// IsAggregate returns false. -func (expr *QualifiedRef) IsAggregate() bool { return false } +func (expr *QualifiedRef) IsLiteral() bool { return false } + +func (expr *QualifiedRef) DataType() ExprDataType { + return expr.RefDataType +} + +func (expr *QualifiedRef) Pos() Pos { + return expr.Table.Pos() +} // Clone returns a deep copy of r. func (r *QualifiedRef) Clone() *QualifiedRef { @@ -2011,23 +2122,18 @@ type Call struct { Rparen Pos // position of right paren Filter *FilterClause // filter clause Over *OverClause // over clause + + ResultDataType ExprDataType } -// IsAggregate returns true if call is an aggregate function or it contains one. -func (expr *Call) IsAggregate() bool { - // Check if this is an aggregate call. - switch strings.ToUpper(IdentName(expr.Name)) { - case "COUNT", "MIN", "MAX", "SUM": - return true - } +func (expr *Call) IsLiteral() bool { return false } - // Check if any arguments to the call are aggregate. - for _, arg := range expr.Args { - if arg.IsAggregate() { - return true - } - } - return false +func (expr *Call) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *Call) Pos() Pos { + return expr.Name.Pos() } // Clone returns a deep copy of c. @@ -2599,7 +2705,6 @@ func (s *DropTriggerStatement) Clone() *DropTriggerStatement { return &other } -// String returns the string representation of the statement. func (s *DropTriggerStatement) String() string { var buf bytes.Buffer buf.WriteString("DROP TRIGGER") @@ -2610,18 +2715,38 @@ func (s *DropTriggerStatement) String() string { return buf.String() } -type InsertStatement struct { - WithClause *WithClause // clause containing CTEs +type BulkInsertStatement struct { + Bulk Pos // position of BULK keyword + Insert Pos // position of INSERT keyword - Insert Pos // position of INSERT keyword - Replace Pos // position of REPLACE keyword - InsertOr Pos // position of OR keyword after INSERT - InsertOrReplace Pos // position of REPLACE keyword after INSERT OR - InsertOrRollback Pos // position of ROLLBACK keyword after INSERT OR - InsertOrAbort Pos // position of ABORT keyword after INSERT OR - InsertOrFail Pos // position of FAIL keyword after INSERT OR - InsertOrIgnore Pos // position of IGNORE keyword after INSERT OR - Into Pos // position of INTO keyword + Table *Ident // table name + + From Pos // position of FROM keyword + DataFile Expr // data file name + With Pos // position of WITH keyword +} + +func (s *BulkInsertStatement) String() string { + var buf bytes.Buffer + buf.WriteString("BULK INSERT ") + fmt.Fprintf(&buf, " %s", s.Table.String()) + buf.WriteString(" FROM ") + fmt.Fprintf(&buf, " %s", s.DataFile.String()) + return buf.String() +} + +type InsertStatement struct { + //WithClause *WithClause // clause containing CTEs + + Insert Pos // position of INSERT keyword + Replace Pos // position of REPLACE keyword + InsertOr Pos // position of OR keyword after INSERT + InsertOrReplace Pos // position of REPLACE keyword after INSERT OR + // InsertOrRollback Pos // position of ROLLBACK keyword after INSERT OR + // InsertOrAbort Pos // position of ABORT keyword after INSERT OR + // InsertOrFail Pos // position of FAIL keyword after INSERT OR + // InsertOrIgnore Pos // position of IGNORE keyword after INSERT OR + Into Pos // position of INTO keyword Table *Ident // table name As Pos // position of AS keyword @@ -2631,15 +2756,15 @@ type InsertStatement struct { Columns []*Ident // optional column list ColumnsRparen Pos // position of column list right paren - Values Pos // position of VALUES keyword - ValueLists []*ExprList // lists of lists of values + Values Pos // position of VALUES keyword + ValueList *ExprList // list of values - Select *SelectStatement // SELECT statement + // Select *SelectStatement // SELECT statement - Default Pos // position of DEFAULT keyword - DefaultValues Pos // position of VALUES keyword after DEFAULT + // Default Pos // position of DEFAULT keyword + // DefaultValues Pos // position of VALUES keyword after DEFAULT - UpsertClause *UpsertClause // optional upsert clause + // UpsertClause *UpsertClause // optional upsert clause } // Clone returns a deep copy of s. @@ -2648,39 +2773,38 @@ func (s *InsertStatement) Clone() *InsertStatement { return nil } other := *s - other.WithClause = s.WithClause.Clone() + //other.WithClause = s.WithClause.Clone() other.Table = s.Table.Clone() other.Alias = s.Alias.Clone() other.Columns = cloneIdents(s.Columns) - other.ValueLists = cloneExprLists(s.ValueLists) - other.Select = s.Select.Clone() - other.UpsertClause = s.UpsertClause.Clone() + other.ValueList = s.ValueList.Clone() + //other.Select = s.Select.Clone() + //other.UpsertClause = s.UpsertClause.Clone() return &other } -// String returns the string representation of the statement. func (s *InsertStatement) String() string { var buf bytes.Buffer - if s.WithClause != nil { - buf.WriteString(s.WithClause.String()) - buf.WriteString(" ") - } + //if s.WithClause != nil { + // buf.WriteString(s.WithClause.String()) + // buf.WriteString(" ") + //} - if s.Replace.IsValid() { - buf.WriteString("REPLACE") - } else { - buf.WriteString("INSERT") - if s.InsertOrReplace.IsValid() { - buf.WriteString(" OR REPLACE") - } else if s.InsertOrRollback.IsValid() { - buf.WriteString(" OR ROLLBACK") - } else if s.InsertOrAbort.IsValid() { - buf.WriteString(" OR ABORT") - } else if s.InsertOrFail.IsValid() { - buf.WriteString(" OR FAIL") - } else if s.InsertOrIgnore.IsValid() { - buf.WriteString(" OR IGNORE") - } + //if s.Replace.IsValid() { + // buf.WriteString("REPLACE") + //} else { + buf.WriteString("INSERT") + if s.InsertOrReplace.IsValid() { + buf.WriteString(" OR REPLACE") + //} else if s.InsertOrRollback.IsValid() { + // buf.WriteString(" OR ROLLBACK") + //} else if s.InsertOrAbort.IsValid() { + // buf.WriteString(" OR ABORT") + //} else if s.InsertOrFail.IsValid() { + // buf.WriteString(" OR FAIL") + //} else if s.InsertOrIgnore.IsValid() { + // buf.WriteString(" OR IGNORE") + //} } fmt.Fprintf(&buf, " INTO %s", s.Table.String()) @@ -2699,30 +2823,25 @@ func (s *InsertStatement) String() string { buf.WriteString(")") } - if s.DefaultValues.IsValid() { - buf.WriteString(" DEFAULT VALUES") - } else if s.Select != nil { - fmt.Fprintf(&buf, " %s", s.Select.String()) - } else { - buf.WriteString(" VALUES") - for i := range s.ValueLists { - if i != 0 { - buf.WriteString(",") - } - buf.WriteString(" (") - for j, expr := range s.ValueLists[i].Exprs { - if j != 0 { - buf.WriteString(", ") - } - buf.WriteString(expr.String()) - } - buf.WriteString(")") + //if s.DefaultValues.IsValid() { + // buf.WriteString(" DEFAULT VALUES") + //} else if s.Select != nil { + // fmt.Fprintf(&buf, " %s", s.Select.String()) + //} else { + buf.WriteString(" VALUES") + buf.WriteString(" (") + for j, expr := range s.ValueList.Exprs { + if j != 0 { + buf.WriteString(", ") } + buf.WriteString(expr.String()) } + buf.WriteString(")") + //} - if s.UpsertClause != nil { - fmt.Fprintf(&buf, " %s", s.UpsertClause.String()) - } + //if s.UpsertClause != nil { + // fmt.Fprintf(&buf, " %s", s.UpsertClause.String()) + //} return buf.String() } @@ -3031,13 +3150,17 @@ func (c *IndexedColumn) String() string { type SelectStatement struct { WithClause *WithClause // clause containing CTEs - Values Pos // position of VALUES keyword - ValueLists []*ExprList // lists of lists of values + // Values Pos // position of VALUES keyword + // ValueLists []*ExprList // lists of lists of values - Select Pos // position of SELECT keyword - Distinct Pos // position of DISTINCT keyword - All Pos // position of ALL keyword - Columns []*ResultColumn // list of result columns in the SELECT clause + Select Pos // position of SELECT keyword + Distinct Pos // position of DISTINCT keyword + // All Pos // position of ALL keyword + Columns []*ResultColumn // list of result columns in the SELECT clause + + Top Pos // position of TOP keyword + TopN Pos // position of TOPN keyword + TopExpr Expr // TOP expr From Pos // position of FROM keyword Source Source // chain of tables & subqueries in FROM clause @@ -3064,11 +3187,6 @@ type SelectStatement struct { OrderBy Pos // position of BY keyword after ORDER OrderingTerms []*OrderingTerm // terms of ORDER BY clause - Limit Pos // position of LIMIT keyword - LimitExpr Expr // limit expression - Offset Pos // position of OFFSET keyword - OffsetComma Pos // position of COMMA (instead of OFFSET) - OffsetExpr Expr // offset expression } // Clone returns a deep copy of s. @@ -3078,7 +3196,8 @@ func (s *SelectStatement) Clone() *SelectStatement { } other := *s other.WithClause = s.WithClause.Clone() - other.ValueLists = cloneExprLists(s.ValueLists) + //other.ValueLists = cloneExprLists(s.ValueLists) + other.TopExpr = CloneExpr(s.TopExpr) other.Columns = cloneResultColumns(s.Columns) other.Source = CloneSource(s.Source) other.WhereExpr = CloneExpr(s.WhereExpr) @@ -3087,20 +3206,10 @@ func (s *SelectStatement) Clone() *SelectStatement { other.Windows = cloneWindows(s.Windows) other.Compound = s.Compound.Clone() other.OrderingTerms = cloneOrderingTerms(s.OrderingTerms) - other.LimitExpr = CloneExpr(s.LimitExpr) - other.OffsetExpr = CloneExpr(s.OffsetExpr) return &other } -// IsAggregate returns true if statement contains aggregate columns. -func (s *SelectStatement) IsAggregate() bool { - for _, col := range s.Columns { - if col.IsAggregate() { - return true - } - } - return false -} +func (expr *SelectStatement) IsLiteral() bool { return false } // HasWildcard returns true any result column contains a wildcard (STAR). func (s *SelectStatement) HasWildcard() bool { @@ -3119,6 +3228,14 @@ func (s *SelectStatement) HasWildcard() bool { return false } +func (s *SelectStatement) DataType() ExprDataType { + return nil +} + +func (s *SelectStatement) Pos() Pos { + return s.Select +} + // String returns the string representation of the statement. func (s *SelectStatement) String() string { var buf bytes.Buffer @@ -3127,7 +3244,7 @@ func (s *SelectStatement) String() string { buf.WriteString(" ") } - if len(s.ValueLists) > 0 { + /*if len(s.ValueLists) > 0 { buf.WriteString("VALUES ") for i, exprs := range s.ValueLists { if i != 0 { @@ -3143,54 +3260,60 @@ func (s *SelectStatement) String() string { } buf.WriteString(")") } - } else { - buf.WriteString("SELECT ") - if s.Distinct.IsValid() { - buf.WriteString("DISTINCT ") - } else if s.All.IsValid() { - buf.WriteString("ALL ") - } + } else {*/ + buf.WriteString("SELECT ") + if s.Distinct.IsValid() { + buf.WriteString("DISTINCT ") + } //else if s.All.IsValid() { + // buf.WriteString("ALL ") + //} + if s.Top.IsValid() { + fmt.Fprintf(&buf, "TOP(%s) ", s.TopExpr.String()) + } + if s.TopN.IsValid() { + fmt.Fprintf(&buf, "TOPN(%s) ", s.TopExpr.String()) + } - for i, col := range s.Columns { + for i, col := range s.Columns { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(col.String()) + } + + if s.Source != nil { + fmt.Fprintf(&buf, " FROM %s", s.Source.String()) + } + + if s.WhereExpr != nil { + fmt.Fprintf(&buf, " WHERE %s", s.WhereExpr.String()) + } + + if len(s.GroupByExprs) != 0 { + buf.WriteString(" GROUP BY ") + for i, expr := range s.GroupByExprs { if i != 0 { buf.WriteString(", ") } - buf.WriteString(col.String()) + buf.WriteString(expr.String()) } - if s.Source != nil { - fmt.Fprintf(&buf, " FROM %s", s.Source.String()) - } - - if s.WhereExpr != nil { - fmt.Fprintf(&buf, " WHERE %s", s.WhereExpr.String()) - } - - if len(s.GroupByExprs) != 0 { - buf.WriteString(" GROUP BY ") - for i, expr := range s.GroupByExprs { - if i != 0 { - buf.WriteString(", ") - } - buf.WriteString(expr.String()) - } - - if s.HavingExpr != nil { - fmt.Fprintf(&buf, " HAVING %s", s.HavingExpr.String()) - } - } - - if len(s.Windows) != 0 { - buf.WriteString(" WINDOW ") - for i, window := range s.Windows { - if i != 0 { - buf.WriteString(", ") - } - buf.WriteString(window.String()) - } + if s.HavingExpr != nil { + fmt.Fprintf(&buf, " HAVING %s", s.HavingExpr.String()) } } + if len(s.Windows) != 0 { + buf.WriteString(" WINDOW ") + for i, window := range s.Windows { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(window.String()) + } + } + // } + // Write compound operator. if s.Compound != nil { switch { @@ -3219,15 +3342,41 @@ func (s *SelectStatement) String() string { } } - // Write LIMIT/OFFSET. - if s.LimitExpr != nil { - fmt.Fprintf(&buf, " LIMIT %s", s.LimitExpr.String()) - if s.OffsetExpr != nil { - fmt.Fprintf(&buf, " OFFSET %s", s.OffsetExpr.String()) + return buf.String() +} + +func (c *SelectStatement) SourceFromAlias(alias string) Source { + return nil +} + +func (c *SelectStatement) PossibleOutputColumns() []*SourceOutputColumn { + result := make([]*SourceOutputColumn, 0) + // populate the output columns from the columns in the select list + for idx, col := range c.Columns { + soc := &SourceOutputColumn{ + TableName: "", + ColumnName: col.Name(), + ColumnIndex: idx, + Datatype: col.Expr.DataType(), + } + result = append(result, soc) + } + return result +} + +func (c *SelectStatement) OutputColumnNamed(name string) (*SourceOutputColumn, error) { + ocs := c.PossibleOutputColumns() + + for _, oc := range ocs { + if strings.EqualFold(oc.ColumnName, name) { + return oc, nil } } + return nil, nil +} - return buf.String() +func (c *SelectStatement) OutputColumnQualifierNamed(qualifier string, name string) (*SourceOutputColumn, error) { + return nil, nil } type ResultColumn struct { @@ -3245,8 +3394,6 @@ func (c *ResultColumn) Name() string { } switch expr := c.Expr.(type) { - case *Call: - return strings.ToLower(IdentName(expr.Name)) case *Ident: return IdentName(expr) case *QualifiedRef: @@ -3256,13 +3403,7 @@ func (c *ResultColumn) Name() string { } } -// IsAggregate returns true if column contains an aggregate function expression. -func (c *ResultColumn) IsAggregate() bool { - if c.Star.IsValid() { - return false - } - return c.Expr.IsAggregate() -} +func (expr *ResultColumn) IsLiteral() bool { return false } // Clone returns a deep copy of c. func (c *ResultColumn) Clone() *ResultColumn { @@ -3297,14 +3438,15 @@ func (c *ResultColumn) String() string { } type QualifiedTableName struct { - Name *Ident // table name - As Pos // position of AS keyword - Alias *Ident // optional table alias - Indexed Pos // position of INDEXED keyword - IndexedBy Pos // position of BY keyword after INDEXED - Not Pos // position of NOT keyword before INDEXED - NotIndexed Pos // position of NOT keyword before INDEXED - Index *Ident // name of index + Name *Ident // table name + As Pos // position of AS keyword + Alias *Ident // optional table alias + Indexed Pos // position of INDEXED keyword + IndexedBy Pos // position of BY keyword after INDEXED + Not Pos // position of NOT keyword before INDEXED + NotIndexed Pos // position of NOT keyword before INDEXED + Index *Ident // name of index + OutputColumns []*SourceOutputColumn // output columns - populated during analysis } // TableName returns the name used to identify n. @@ -3316,6 +3458,10 @@ func (n *QualifiedTableName) TableName() string { return IdentName(n.Name) } +func (n *QualifiedTableName) MatchesTablenameOrAlias(match string) bool { + return strings.EqualFold(IdentName(n.Alias), match) || strings.EqualFold(IdentName(n.Name), match) +} + // Clone returns a deep copy of n. func (n *QualifiedTableName) Clone() *QualifiedTableName { if n == nil { @@ -3344,6 +3490,36 @@ func (n *QualifiedTableName) String() string { return buf.String() } +func (c *QualifiedTableName) SourceFromAlias(alias string) Source { + if strings.EqualFold(IdentName(c.Alias), alias) { + return c + } + if strings.EqualFold(IdentName(c.Name), alias) { + return c + } + return nil +} + +func (c *QualifiedTableName) PossibleOutputColumns() []*SourceOutputColumn { + return c.OutputColumns +} + +func (c *QualifiedTableName) OutputColumnNamed(name string) (*SourceOutputColumn, error) { + for _, oc := range c.OutputColumns { + if strings.EqualFold(oc.ColumnName, name) { + return oc, nil + } + } + return nil, nil +} + +func (c *QualifiedTableName) OutputColumnQualifierNamed(qualifier string, name string) (*SourceOutputColumn, error) { + if strings.EqualFold(IdentName(c.Alias), qualifier) || strings.EqualFold(IdentName(c.Name), qualifier) { + return c.OutputColumnNamed(name) + } + return nil, nil +} + type ParenSource struct { Lparen Pos // position of left paren X Source // nested source @@ -3371,11 +3547,34 @@ func (s *ParenSource) String() string { return fmt.Sprintf("(%s)", s.X.String()) } +func (c *ParenSource) SourceFromAlias(alias string) Source { + if strings.EqualFold(IdentName(c.Alias), alias) { + return c + } + return c.X.SourceFromAlias(alias) +} + +func (c *ParenSource) PossibleOutputColumns() []*SourceOutputColumn { + return c.X.PossibleOutputColumns() +} + +func (c *ParenSource) OutputColumnNamed(name string) (*SourceOutputColumn, error) { + return c.X.OutputColumnNamed(name) +} + +func (c *ParenSource) OutputColumnQualifierNamed(qualifier string, name string) (*SourceOutputColumn, error) { + if strings.EqualFold(IdentName(c.Alias), qualifier) { + return c.OutputColumnNamed(name) + } + return nil, nil +} + type JoinClause struct { - X Source // lhs source - Operator *JoinOperator // join operator - Y Source // rhs source - Constraint JoinConstraint // join constraint + X Source // lhs source + Operator *JoinOperator // join operator + Y Source // rhs source + Constraint JoinConstraint // join constraint + OutputColumns []*SourceOutputColumn // output columns - populated during analysis } // Clone returns a deep copy of c. @@ -3400,6 +3599,52 @@ func (c *JoinClause) String() string { return buf.String() } +func (c *JoinClause) PossibleOutputColumns() []*SourceOutputColumn { + return c.OutputColumns +} + +func (c *JoinClause) OutputColumnNamed(name string) (*SourceOutputColumn, error) { + if col, err := c.X.OutputColumnNamed(name); err != nil { + return nil, err + } else if col != nil { + return col, nil + } + + if col, err := c.Y.OutputColumnNamed(name); err != nil { + return nil, err + } else if col != nil { + return col, nil + } + + return nil, nil +} + +func (c *JoinClause) OutputColumnQualifierNamed(qualifier string, name string) (*SourceOutputColumn, error) { + if col, err := c.X.OutputColumnQualifierNamed(qualifier, name); err != nil { + return nil, err + } else if col != nil { + return col, nil + } + + if col, err := c.Y.OutputColumnQualifierNamed(qualifier, name); err != nil { + return nil, err + } else if col != nil { + return col, nil + } + + return nil, nil +} + +func (c *JoinClause) SourceFromAlias(alias string) Source { + if src := c.X.SourceFromAlias(alias); src != nil { + return src + } + if src := c.Y.SourceFromAlias(alias); src != nil { + return src + } + return nil +} + type JoinOperator struct { Comma Pos // position of comma Natural Pos // position of NATURAL keyword @@ -3591,8 +3836,17 @@ type ParenExpr struct { Rparen Pos // position of right paren } -// IsAggregate returns true if inner expression has an aggregate function. -func (expr *ParenExpr) IsAggregate() bool { return false } +func (expr *ParenExpr) IsLiteral() bool { + return expr.X.IsLiteral() +} + +func (expr *ParenExpr) DataType() ExprDataType { + return expr.X.DataType() +} + +func (expr *ParenExpr) Pos() Pos { + return expr.Lparen +} // Clone returns a deep copy of expr. func (expr *ParenExpr) Clone() *ParenExpr { @@ -3609,6 +3863,54 @@ func (expr *ParenExpr) String() string { return fmt.Sprintf("(%s)", expr.X.String()) } +type SetLiteralExpr struct { + Lbracket Pos // position of left bracket + Members []Expr // bracketed expression + Rbracket Pos // position of right bracket + + ResultDataType ExprDataType +} + +func (expr *SetLiteralExpr) IsLiteral() bool { + return true +} + +func (expr *SetLiteralExpr) DataType() ExprDataType { + return expr.ResultDataType +} + +func (expr *SetLiteralExpr) Pos() Pos { + return expr.Lbracket +} + +// Clone returns a deep copy of expr. +func (expr *SetLiteralExpr) Clone() *SetLiteralExpr { + if expr == nil { + return nil + } + other := *expr + other.Members = cloneExprs(expr.Members) + return &other +} + +// String returns the string representation of the expression. +func (expr *SetLiteralExpr) String() string { + var buf bytes.Buffer + + if len(expr.Members) != 0 { + buf.WriteString("[") + for i, col := range expr.Members { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(col.String()) + } + buf.WriteString("]") + } + + return buf.String() +} + type Window struct { Name *Ident // name of window As Pos // position of AS keyword diff --git a/sql3/parser/ast_test.go b/sql3/parser/ast_test.go new file mode 100644 index 000000000..a5b87843a --- /dev/null +++ b/sql3/parser/ast_test.go @@ -0,0 +1,1246 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package parser_test + +import ( + "reflect" + "strings" + "testing" + + "github.com/go-test/deep" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +func TestExprString(t *testing.T) { + if got, want := parser.ExprString(&parser.NullLit{}), "NULL"; got != want { + t.Fatalf("ExprString()=%q, want %q", got, want) + } else if got, want := parser.ExprString(nil), ""; got != want { + t.Fatalf("ExprString()=%q, want %q", got, want) + } +} + +func TestSplitExprTree(t *testing.T) { + t.Run("AND-only", func(t *testing.T) { + AssertSplitExprTree(t, `x = 1 AND y = 2 AND z = 3`, []parser.Expr{ + &parser.BinaryExpr{X: &parser.Ident{Name: "x"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "1"}}, + &parser.BinaryExpr{X: &parser.Ident{Name: "y"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "2"}}, + &parser.BinaryExpr{X: &parser.Ident{Name: "z"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "3"}}, + }) + }) + + t.Run("OR", func(t *testing.T) { + AssertSplitExprTree(t, `x = 1 AND (y = 2 OR y = 3) AND z = 4`, []parser.Expr{ + &parser.BinaryExpr{X: &parser.Ident{Name: "x"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "1"}}, + &parser.BinaryExpr{ + X: &parser.BinaryExpr{X: &parser.Ident{Name: "y"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "2"}}, + Op: parser.OR, + Y: &parser.BinaryExpr{X: &parser.Ident{Name: "y"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "3"}}, + }, + &parser.BinaryExpr{X: &parser.Ident{Name: "z"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "4"}}, + }) + }) + + t.Run("ParenExpr", func(t *testing.T) { + AssertSplitExprTree(t, `x = 1 AND (y = 2 AND z = 3)`, []parser.Expr{ + &parser.BinaryExpr{X: &parser.Ident{Name: "x"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "1"}}, + &parser.BinaryExpr{X: &parser.Ident{Name: "y"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "2"}}, + &parser.BinaryExpr{X: &parser.Ident{Name: "z"}, Op: parser.EQ, Y: &parser.IntegerLit{Value: "3"}}, + }) + }) +} + +func AssertSplitExprTree(tb testing.TB, s string, want []parser.Expr) { + tb.Helper() + if diff := deep.Equal(parser.SplitExprTree(StripExprPos(parser.MustParseExprString(s))), want); diff != nil { + tb.Fatal("mismatch: \n" + strings.Join(diff, "\n")) + } +} + +func TestAlterTableStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.AlterTableStatement{ + Name: &parser.Ident{Name: "foo"}, + OldColumnName: &parser.Ident{Name: "col1"}, + NewColumnName: &parser.Ident{Name: "col2"}, + }, `ALTER TABLE "foo" RENAME COLUMN "col1" TO "col2"`) + + AssertStatementStringer(t, &parser.AlterTableStatement{ + Name: &parser.Ident{Name: "foo"}, + ColumnDef: &parser.ColumnDefinition{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + }, + }, `ALTER TABLE "foo" ADD COLUMN "bar" INTEGER`) + AssertStatementStringer(t, &parser.AlterTableStatement{ + Name: &parser.Ident{Name: "foo"}, + DropColumnName: &parser.Ident{Name: "bar"}, + }, `ALTER TABLE "foo" DROP COLUMN "bar"`) +} + +/*func TestAnalyzeStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.AnalyzeStatement{Name: &parser.Ident{Name: "foo"}}, `ANALYZE "foo"`) +}*/ + +func TestBeginStatement_String(t *testing.T) { + t.Skip("BEGIN is currently disabled in the parser") + AssertStatementStringer(t, &parser.BeginStatement{}, `BEGIN`) + AssertStatementStringer(t, &parser.BeginStatement{Deferred: pos(0)}, `BEGIN DEFERRED`) + AssertStatementStringer(t, &parser.BeginStatement{Immediate: pos(0)}, `BEGIN IMMEDIATE`) + AssertStatementStringer(t, &parser.BeginStatement{Exclusive: pos(0)}, `BEGIN EXCLUSIVE`) + AssertStatementStringer(t, &parser.BeginStatement{Immediate: pos(0), Transaction: pos(0)}, `BEGIN IMMEDIATE TRANSACTION`) +} + +func TestCommitStatement_String(t *testing.T) { + t.Skip("COMMIT is currently disabled in the parser") + AssertStatementStringer(t, &parser.CommitStatement{}, `COMMIT`) + AssertStatementStringer(t, &parser.CommitStatement{End: pos(0)}, `END`) + AssertStatementStringer(t, &parser.CommitStatement{End: pos(0), Transaction: pos(0)}, `END TRANSACTION`) +} + +func TestCreateIndexStatement_String(t *testing.T) { + t.Skip("CREATE INDEX is currently disabled in the parser") + AssertStatementStringer(t, &parser.CreateIndexStatement{ + Name: &parser.Ident{Name: "foo"}, + Table: &parser.Ident{Name: "bar"}, + Columns: []*parser.IndexedColumn{{X: &parser.Ident{Name: "baz"}}}, + }, `CREATE INDEX "foo" ON "bar" ("baz")`) + + AssertStatementStringer(t, &parser.CreateIndexStatement{ + Unique: pos(0), + IfNotExists: pos(0), + Name: &parser.Ident{Name: "foo"}, + Table: &parser.Ident{Name: "bar"}, + Columns: []*parser.IndexedColumn{ + {X: &parser.Ident{Name: "baz"}}, + {X: &parser.Ident{Name: "bat"}}, + }, + WhereExpr: &parser.BoolLit{Value: true}, + }, `CREATE UNIQUE INDEX IF NOT EXISTS "foo" ON "bar" ("baz", "bat") WHERE TRUE`) +} + +func TestCreateTableStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + IfNotExists: pos(0), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + }, + { + Name: &parser.Ident{Name: "baz"}, + Type: &parser.Type{Name: &parser.Ident{Name: "STRING"}}, + }, + }, + }, `CREATE TABLE IF NOT EXISTS "foo" ("bar" INTEGER, "baz" STRING)`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + IfNotExists: pos(0), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "boolcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "BOOL"}}, + }, + { + Name: &parser.Ident{Name: "decimalcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "DECIMAL"}}, + Constraints: []parser.Constraint{ + &parser.MinConstraint{Expr: &parser.IntegerLit{Value: "100.25"}}, + &parser.MaxConstraint{Expr: &parser.IntegerLit{Value: "1000.75"}}, + }, + }, + { + Name: &parser.Ident{Name: "idcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "ID"}}, + Constraints: []parser.Constraint{ + &parser.CacheTypeConstraint{ + CacheTypeValue: "RANKED", + Size: pos(0), + SizeExpr: &parser.IntegerLit{Value: "10000"}, + }, + }, + }, + { + Name: &parser.Ident{Name: "idsetcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "IDSET"}}, + Constraints: []parser.Constraint{ + &parser.CacheTypeConstraint{ + CacheTypeValue: "RANKED", + Size: pos(0), + SizeExpr: &parser.IntegerLit{Value: "10000"}, + }, + }, + }, + { + Name: &parser.Ident{Name: "intcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + Constraints: []parser.Constraint{ + &parser.MinConstraint{Expr: &parser.IntegerLit{Value: "100"}}, + &parser.MaxConstraint{Expr: &parser.IntegerLit{Value: "1000"}}, + }, + }, + { + Name: &parser.Ident{Name: "stringcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "STRING"}}, + Constraints: []parser.Constraint{ + &parser.CacheTypeConstraint{ + CacheTypeValue: "RANKED", + Size: pos(0), + SizeExpr: &parser.IntegerLit{Value: "10000"}, + }, + }, + }, + { + Name: &parser.Ident{Name: "stringsetcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "STRINGSET"}}, + Constraints: []parser.Constraint{ + &parser.CacheTypeConstraint{ + CacheTypeValue: "RANKED", + Size: pos(0), + SizeExpr: &parser.IntegerLit{Value: "10000"}, + }, + }, + }, + { + Name: &parser.Ident{Name: "timestampcol"}, + Type: &parser.Type{Name: &parser.Ident{Name: "TIMESTAMP"}}, + Constraints: []parser.Constraint{ + &parser.TimeUnitConstraint{ + Expr: &parser.StringLit{Value: "s"}, + Epoch: pos(0), + EpochExpr: &parser.StringLit{Value: "2021-01-01T00:00:00Z"}, + }, + }, + }, + }, + }, `CREATE TABLE IF NOT EXISTS "foo" (`+ + `"boolcol" BOOL, `+ + `"decimalcol" DECIMAL MIN 100.25 MAX 1000.75, `+ + `"idcol" ID CACHETYPE RANKED SIZE 10000, `+ + `"idsetcol" IDSET CACHETYPE RANKED SIZE 10000, `+ + `"intcol" INTEGER MIN 100 MAX 1000, `+ + `"stringcol" STRING CACHETYPE RANKED SIZE 10000, `+ + `"stringsetcol" STRINGSET CACHETYPE RANKED SIZE 10000, `+ + `"timestampcol" TIMESTAMP TIMEUNIT 's' EPOCH '2021-01-01T00:00:00Z'`+ + `)`) +} + +func OLDTestCreateTableStatement_String(t *testing.T) { + t.Skip("These tests refer to an older version of CREATE TABLE") + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + IfNotExists: pos(0), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + }, + { + Name: &parser.Ident{Name: "baz"}, + Type: &parser.Type{Name: &parser.Ident{Name: "TEXT"}}, + }, + }, + }, `CREATE TABLE IF NOT EXISTS "foo" ("bar" INTEGER, "baz" TEXT)`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Columns: []*parser.ColumnDefinition{{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + Constraints: []parser.Constraint{ + &parser.PrimaryKeyConstraint{Autoincrement: pos(0)}, + &parser.NotNullConstraint{Name: &parser.Ident{Name: "nn"}}, + &parser.DefaultConstraint{Name: &parser.Ident{Name: "def"}, Expr: &parser.IntegerLit{Value: "123"}}, + &parser.DefaultConstraint{Expr: &parser.IntegerLit{Value: "456"}, Lparen: pos(0)}, + &parser.UniqueConstraint{}, + }, + }}, + }, `CREATE TABLE "foo" ("bar" INTEGER PRIMARY KEY AUTOINCREMENT CONSTRAINT "nn" NOT NULL CONSTRAINT "def" DEFAULT 123 DEFAULT (456) UNIQUE)`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Columns: []*parser.ColumnDefinition{{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + Constraints: []parser.Constraint{ + &parser.ForeignKeyConstraint{ + ForeignTable: &parser.Ident{Name: "x"}, + ForeignColumns: []*parser.Ident{{Name: "y"}}, + Args: []*parser.ForeignKeyArg{ + {OnDelete: pos(0), SetNull: pos(0)}, + {OnUpdate: pos(0), SetDefault: pos(0)}, + {OnUpdate: pos(0), Cascade: pos(0)}, + {OnUpdate: pos(0), Restrict: pos(0)}, + {OnUpdate: pos(0), NoAction: pos(0)}, + }, + }, + }, + }}, + }, `CREATE TABLE "foo" ("bar" INTEGER REFERENCES "x" ("y") ON DELETE SET NULL ON UPDATE SET DEFAULT ON UPDATE CASCADE ON UPDATE RESTRICT ON UPDATE NO ACTION)`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Columns: []*parser.ColumnDefinition{{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + Constraints: []parser.Constraint{ + &parser.ForeignKeyConstraint{ + ForeignTable: &parser.Ident{Name: "x"}, + ForeignColumns: []*parser.Ident{{Name: "y"}}, + Deferrable: pos(0), + InitiallyDeferred: pos(0), + }, + }, + }}, + }, `CREATE TABLE "foo" ("bar" INTEGER REFERENCES "x" ("y") DEFERRABLE INITIALLY DEFERRED)`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Columns: []*parser.ColumnDefinition{{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}, + Constraints: []parser.Constraint{ + &parser.ForeignKeyConstraint{ + ForeignTable: &parser.Ident{Name: "x"}, + ForeignColumns: []*parser.Ident{{Name: "y"}}, + NotDeferrable: pos(0), + InitiallyImmediate: pos(0), + }, + }, + }}, + }, `CREATE TABLE "foo" ("bar" INTEGER REFERENCES "x" ("y") NOT DEFERRABLE INITIALLY IMMEDIATE)`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Columns: []*parser.ColumnDefinition{{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "DECIMAL"}, Precision: &parser.IntegerLit{Value: "100"}}, + }}, + Constraints: []parser.Constraint{ + &parser.PrimaryKeyConstraint{ + Name: &parser.Ident{Name: "pk"}, + Columns: []*parser.Ident{ + {Name: "x"}, + {Name: "y"}, + }, + }, + &parser.UniqueConstraint{ + Name: &parser.Ident{Name: "uniq"}, + Columns: []*parser.Ident{ + {Name: "x"}, + {Name: "y"}, + }, + }, + &parser.CheckConstraint{ + Name: &parser.Ident{Name: "chk"}, + Expr: &parser.BoolLit{Value: true}, + }, + }, + }, `CREATE TABLE "foo" ("bar" DECIMAL(100), CONSTRAINT "pk" PRIMARY KEY ("x", "y"), CONSTRAINT "uniq" UNIQUE ("x", "y"), CONSTRAINT "chk" CHECK (TRUE))`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Columns: []*parser.ColumnDefinition{{ + Name: &parser.Ident{Name: "bar"}, + Type: &parser.Type{Name: &parser.Ident{Name: "DECIMAL"}, Precision: &parser.IntegerLit{Value: "100"}, Scale: &parser.IntegerLit{Value: "200"}}, + }}, + Constraints: []parser.Constraint{ + &parser.ForeignKeyConstraint{ + Name: &parser.Ident{Name: "fk"}, + Columns: []*parser.Ident{{Name: "a"}, {Name: "b"}}, + ForeignTable: &parser.Ident{Name: "x"}, + ForeignColumns: []*parser.Ident{{Name: "y"}, {Name: "z"}}, + }, + }, + }, `CREATE TABLE "foo" ("bar" DECIMAL(100,200), CONSTRAINT "fk" FOREIGN KEY ("a", "b") REFERENCES "x" ("y", "z"))`) + + AssertStatementStringer(t, &parser.CreateTableStatement{ + Name: &parser.Ident{Name: "foo"}, + Select: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `CREATE TABLE "foo" AS SELECT *`) +} + +func TestCreateTriggerStatement_String(t *testing.T) { + t.Skip("CREATE TRIGGER is currently disabled in the parser") + AssertStatementStringer(t, &parser.CreateTriggerStatement{ + Name: &parser.Ident{Name: "trig"}, + Insert: pos(0), + Table: &parser.Ident{Name: "tbl"}, + Body: []parser.Statement{ + &parser.DeleteStatement{Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl2"}}}, + }, + }, `CREATE TRIGGER "trig" INSERT ON "tbl" BEGIN DELETE FROM "tbl2"; END`) + + AssertStatementStringer(t, &parser.CreateTriggerStatement{ + Name: &parser.Ident{Name: "trig"}, + Before: pos(0), + Delete: pos(0), + ForEachRow: pos(0), + Table: &parser.Ident{Name: "tbl"}, + Body: []parser.Statement{ + &parser.DeleteStatement{Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}}, + }, + }, `CREATE TRIGGER "trig" BEFORE DELETE ON "tbl" FOR EACH ROW BEGIN DELETE FROM "x"; END`) + + AssertStatementStringer(t, &parser.CreateTriggerStatement{ + IfNotExists: pos(0), + Name: &parser.Ident{Name: "trig"}, + After: pos(0), + Update: pos(0), + Table: &parser.Ident{Name: "tbl"}, + WhenExpr: &parser.BoolLit{Value: true}, + Body: []parser.Statement{ + &parser.DeleteStatement{Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}}, + }, + }, `CREATE TRIGGER IF NOT EXISTS "trig" AFTER UPDATE ON "tbl" WHEN TRUE BEGIN DELETE FROM "x"; END`) + + AssertStatementStringer(t, &parser.CreateTriggerStatement{ + Name: &parser.Ident{Name: "trig"}, + InsteadOf: pos(0), + Update: pos(0), + UpdateOf: pos(0), + UpdateOfColumns: []*parser.Ident{{Name: "x"}, {Name: "y"}}, + Table: &parser.Ident{Name: "tbl"}, + Body: []parser.Statement{ + &parser.DeleteStatement{Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}}, + }, + }, `CREATE TRIGGER "trig" INSTEAD OF UPDATE OF "x", "y" ON "tbl" BEGIN DELETE FROM "x"; END`) +} + +func TestCreateViewStatement_String(t *testing.T) { + t.Skip("CREATE VIEW is currently disabled in the parser") + AssertStatementStringer(t, &parser.CreateViewStatement{ + Name: &parser.Ident{Name: "vw"}, + Columns: []*parser.Ident{ + {Name: "x"}, + {Name: "y"}, + }, + Select: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `CREATE VIEW "vw" ("x", "y") AS SELECT *`) + + AssertStatementStringer(t, &parser.CreateViewStatement{ + IfNotExists: pos(0), + Name: &parser.Ident{Name: "vw"}, + Select: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `CREATE VIEW IF NOT EXISTS "vw" AS SELECT *`) +} + +func TestDeleteStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.DeleteStatement{ + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}, Alias: &parser.Ident{Name: "tbl2"}}, + }, `DELETE FROM "tbl" AS "tbl2"`) + + // AssertStatementStringer(t, &sql.DeleteStatement{ + // Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}, Index: &sql.Ident{Name: "idx"}}, + // }, `DELETE FROM "tbl" INDEXED BY "idx"`) + + // AssertStatementStringer(t, &sql.DeleteStatement{ + // Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}, NotIndexed: pos(0)}, + // }, `DELETE FROM "tbl" NOT INDEXED`) + + /*AssertStatementStringer(t, &parser.DeleteStatement{ + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + WhereExpr: &parser.BoolLit{Value: true}, + OrderingTerms: []*parser.OrderingTerm{ + {X: &parser.Ident{Name: "x"}}, + {X: &parser.Ident{Name: "y"}}, + }, + LimitExpr: &parser.IntegerLit{Value: "10"}, + OffsetExpr: &parser.IntegerLit{Value: "5"}, + }, `DELETE FROM "tbl" WHERE TRUE ORDER BY "x", "y" LIMIT 10 OFFSET 5`)*/ + + // AssertStatementStringer(t, &sql.DeleteStatement{ + // WithClause: &sql.WithClause{ + // Recursive: pos(0), + // CTEs: []*sql.CTE{{ + // TableName: &sql.Ident{Name: "cte"}, + // Select: &sql.SelectStatement{ + // Columns: []*sql.ResultColumn{{Star: pos(0)}}, + // }, + // }}, + // }, + // Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, + // }, `WITH RECURSIVE "cte" AS (SELECT *) DELETE FROM "tbl"`) +} + +func TestDropIndexStatement_String(t *testing.T) { + t.Skip("DROP INDEX is currently disabled in the parser") + AssertStatementStringer(t, &parser.DropIndexStatement{ + Name: &parser.Ident{Name: "idx"}, + }, `DROP INDEX "idx"`) + + AssertStatementStringer(t, &parser.DropIndexStatement{ + IfExists: pos(0), + Name: &parser.Ident{Name: "idx"}, + }, `DROP INDEX IF EXISTS "idx"`) +} + +func TestDropTableStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.DropTableStatement{ + Name: &parser.Ident{Name: "tbl"}, + }, `DROP TABLE "tbl"`) + + AssertStatementStringer(t, &parser.DropTableStatement{ + IfExists: pos(0), + Name: &parser.Ident{Name: "tbl"}, + }, `DROP TABLE IF EXISTS "tbl"`) +} + +func TestDropTriggerStatement_String(t *testing.T) { + t.Skip("DROP TRIGGER is currently disabled in the parser") + AssertStatementStringer(t, &parser.DropTriggerStatement{ + Name: &parser.Ident{Name: "trig"}, + }, `DROP TRIGGER "trig"`) + + AssertStatementStringer(t, &parser.DropTriggerStatement{ + IfExists: pos(0), + Name: &parser.Ident{Name: "trig"}, + }, `DROP TRIGGER IF EXISTS "trig"`) +} + +func TestDropViewStatement_String(t *testing.T) { + t.Skip("DROP VIEW is currently disabled in the parser") + AssertStatementStringer(t, &parser.DropViewStatement{ + Name: &parser.Ident{Name: "vw"}, + }, `DROP VIEW "vw"`) + + AssertStatementStringer(t, &parser.DropViewStatement{ + IfExists: pos(0), + Name: &parser.Ident{Name: "vw"}, + }, `DROP VIEW IF EXISTS "vw"`) +} + +func TestExplainStatement_String(t *testing.T) { + t.Skip("These are currently disabled in the parser") + AssertStatementStringer(t, &parser.ExplainStatement{ + Stmt: &parser.DropViewStatement{ + Name: &parser.Ident{Name: "vw"}, + }, + }, `EXPLAIN DROP VIEW "vw"`) + + AssertStatementStringer(t, &parser.ExplainStatement{ + QueryPlan: pos(0), + Stmt: &parser.DropViewStatement{ + Name: &parser.Ident{Name: "vw"}, + }, + }, `EXPLAIN QUERY PLAN DROP VIEW "vw"`) +} + +func TestInsertStatement_String(t *testing.T) { + /*AssertStatementStringer(t, &parser.InsertStatement{ + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `INSERT INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + Table: &parser.Ident{Name: "tbl"}, + Alias: &parser.Ident{Name: "x"}, + DefaultValues: pos(0), + }, `INSERT INTO "tbl" AS "x" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + InsertOrReplace: pos(0), + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `INSERT OR REPLACE INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + InsertOrRollback: pos(0), + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `INSERT OR ROLLBACK INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + InsertOrAbort: pos(0), + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `INSERT OR ABORT INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + InsertOrFail: pos(0), + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `INSERT OR FAIL INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + InsertOrIgnore: pos(0), + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `INSERT OR IGNORE INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + Replace: pos(0), + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + }, `REPLACE INTO "tbl" DEFAULT VALUES`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + Table: &parser.Ident{Name: "tbl"}, + Select: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `INSERT INTO "tbl" SELECT *`)*/ + + AssertStatementStringer(t, &parser.InsertStatement{ + Table: &parser.Ident{Name: "tbl"}, + Columns: []*parser.Ident{ + {Name: "x"}, + {Name: "y"}, + }, + ValueList: &parser.ExprList{ + Exprs: []parser.Expr{&parser.NullLit{}, &parser.NullLit{}}, + }, + }, `INSERT INTO "tbl" ("x", "y") VALUES (NULL, NULL)`) + + // AssertStatementStringer(t, &sql.InsertStatement{ + // WithClause: &sql.WithClause{ + // CTEs: []*sql.CTE{ + // { + // TableName: &sql.Ident{Name: "cte"}, + // Select: &sql.SelectStatement{ + // Columns: []*sql.ResultColumn{{Star: pos(0)}}, + // }, + // }, + // { + // TableName: &sql.Ident{Name: "cte2"}, + // Select: &sql.SelectStatement{ + // Columns: []*sql.ResultColumn{{Star: pos(0)}}, + // }, + // }, + // }, + // }, + // Table: &sql.Ident{Name: "tbl"}, + // DefaultValues: pos(0), + // }, `WITH "cte" AS (SELECT *), "cte2" AS (SELECT *) INSERT INTO "tbl" DEFAULT VALUES`) + + /*AssertStatementStringer(t, &parser.InsertStatement{ + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + UpsertClause: &parser.UpsertClause{ + DoNothing: pos(0), + }, + }, `INSERT INTO "tbl" DEFAULT VALUES ON CONFLICT DO NOTHING`)*/ + + /*AssertStatementStringer(t, &parser.InsertStatement{ + Table: &parser.Ident{Name: "tbl"}, + DefaultValues: pos(0), + UpsertClause: &parser.UpsertClause{ + Columns: []*parser.IndexedColumn{ + {X: &parser.Ident{Name: "x"}, Asc: pos(0)}, + {X: &parser.Ident{Name: "y"}, Desc: pos(0)}, + }, + WhereExpr: &parser.BoolLit{Value: true}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + {Columns: []*parser.Ident{{Name: "y"}, {Name: "z"}}, Expr: &parser.IntegerLit{Value: "200"}}, + }, + UpdateWhereExpr: &parser.BoolLit{Value: false}, + }, + }, `INSERT INTO "tbl" DEFAULT VALUES ON CONFLICT ("x" ASC, "y" DESC) WHERE TRUE DO UPDATE SET "x" = 100, ("y", "z") = 200 WHERE FALSE`)*/ +} + +func TestReleaseStatement_String(t *testing.T) { + t.Skip("RELEASE is currently disabled in the parser") + AssertStatementStringer(t, &parser.ReleaseStatement{Name: &parser.Ident{Name: "x"}}, `RELEASE "x"`) + AssertStatementStringer(t, &parser.ReleaseStatement{Savepoint: pos(0), Name: &parser.Ident{Name: "x"}}, `RELEASE SAVEPOINT "x"`) +} + +func TestRollbackStatement_String(t *testing.T) { + t.Skip("ROLLBACK is currently disabled in the parser") + AssertStatementStringer(t, &parser.RollbackStatement{}, `ROLLBACK`) + AssertStatementStringer(t, &parser.RollbackStatement{Transaction: pos(0)}, `ROLLBACK TRANSACTION`) + AssertStatementStringer(t, &parser.RollbackStatement{SavepointName: &parser.Ident{Name: "x"}}, `ROLLBACK TO "x"`) + AssertStatementStringer(t, &parser.RollbackStatement{Savepoint: pos(0), SavepointName: &parser.Ident{Name: "x"}}, `ROLLBACK TO SAVEPOINT "x"`) +} + +func TestSavepointStatement_String(t *testing.T) { + t.Skip("SAVEPOINT is currently disabled in the parser") + AssertStatementStringer(t, &parser.SavepointStatement{Name: &parser.Ident{Name: "x"}}, `SAVEPOINT "x"`) +} + +func TestSelectStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{ + {Expr: &parser.Ident{Name: "x"}, Alias: &parser.Ident{Name: "y"}}, + {Expr: &parser.Ident{Name: "z"}}, + }, + }, `SELECT "x" AS "y", "z"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Distinct: pos(0), + Columns: []*parser.ResultColumn{ + {Expr: &parser.Ident{Name: "x"}}, + }, + }, `SELECT DISTINCT "x"`) + + // AssertStatementStringer(t, &sql.SelectStatement{ + // All: pos(0), + // Columns: []*sql.ResultColumn{ + // {Expr: &sql.Ident{Name: "x"}}, + // }, + // }, `SELECT ALL "x"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + WhereExpr: &parser.BoolLit{Value: true}, + GroupByExprs: []parser.Expr{&parser.Ident{Name: "x"}, &parser.Ident{Name: "y"}}, + HavingExpr: &parser.Ident{Name: "z"}, + }, `SELECT * FROM "tbl" WHERE TRUE GROUP BY "x", "y" HAVING "z"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.ParenSource{ + X: &parser.SelectStatement{Columns: []*parser.ResultColumn{{Star: pos(0)}}}, + Alias: &parser.Ident{Name: "tbl"}, + }, + }, `SELECT * FROM (SELECT *) AS "tbl"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.ParenSource{ + X: &parser.SelectStatement{Columns: []*parser.ResultColumn{{Star: pos(0)}}}, + }, + }, `SELECT * FROM (SELECT *)`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Windows: []*parser.Window{ + { + Name: &parser.Ident{Name: "win1"}, + Definition: &parser.WindowDefinition{ + Base: &parser.Ident{Name: "base"}, + Partitions: []parser.Expr{&parser.Ident{Name: "x"}, &parser.Ident{Name: "y"}}, + OrderingTerms: []*parser.OrderingTerm{ + {X: &parser.Ident{Name: "x"}, Asc: pos(0), NullsFirst: pos(0)}, + {X: &parser.Ident{Name: "y"}, Desc: pos(0), NullsLast: pos(0)}, + }, + Frame: &parser.FrameSpec{ + Range: pos(0), + UnboundedX: pos(0), + PrecedingX: pos(0), + }, + }, + }, + { + Name: &parser.Ident{Name: "win2"}, + Definition: &parser.WindowDefinition{ + Base: &parser.Ident{Name: "base2"}, + }, + }, + }, + }, `SELECT * FROM "tbl" WINDOW "win1" AS ("base" PARTITION BY "x", "y" ORDER BY "x" ASC NULLS FIRST, "y" DESC NULLS LAST RANGE UNBOUNDED PRECEDING), "win2" AS ("base2")`) + + // AssertStatementStringer(t, &sql.SelectStatement{ + // WithClause: &sql.WithClause{ + // CTEs: []*sql.CTE{{ + // TableName: &sql.Ident{Name: "cte"}, + // Columns: []*sql.Ident{ + // {Name: "x"}, + // {Name: "y"}, + // }, + // Select: &sql.SelectStatement{ + // Columns: []*sql.ResultColumn{{Star: pos(0)}}, + // }, + // }}, + // }, + // ValueLists: []*sql.ExprList{ + // {Exprs: []sql.Expr{&sql.NumberLit{Value: "1"}, &sql.NumberLit{Value: "2"}}}, + // {Exprs: []sql.Expr{&sql.NumberLit{Value: "3"}, &sql.NumberLit{Value: "4"}}}, + // }, + // }, `WITH "cte" ("x", "y") AS (SELECT *) VALUES (1, 2), (3, 4)`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Union: pos(0), + Compound: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `SELECT * UNION SELECT *`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Union: pos(0), + UnionAll: pos(0), + Compound: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `SELECT * UNION ALL SELECT *`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Intersect: pos(0), + Compound: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `SELECT * INTERSECT SELECT *`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Except: pos(0), + Compound: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + }, + }, `SELECT * EXCEPT SELECT *`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + OrderingTerms: []*parser.OrderingTerm{ + {X: &parser.Ident{Name: "x"}}, + {X: &parser.Ident{Name: "y"}}, + }, + }, `SELECT * ORDER BY "x", "y"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}, + Operator: &parser.JoinOperator{Comma: pos(0)}, + Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}}, + }, + }, `SELECT * FROM "x", "y"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}, + Operator: &parser.JoinOperator{}, + Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}}, + Constraint: &parser.OnConstraint{X: &parser.BoolLit{Value: true}}, + }, + }, `SELECT * FROM "x" JOIN "y" ON TRUE`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}, + Operator: &parser.JoinOperator{Natural: pos(0), Inner: pos(0)}, + Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}}, + Constraint: &parser.UsingConstraint{ + Columns: []*parser.Ident{{Name: "a"}, {Name: "b"}}, + }, + }, + }, `SELECT * FROM "x" NATURAL INNER JOIN "y" USING ("a", "b")`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}, + Operator: &parser.JoinOperator{Left: pos(0)}, + Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}}, + }, + }, `SELECT * FROM "x" LEFT JOIN "y"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}, + Operator: &parser.JoinOperator{Left: pos(0), Outer: pos(0)}, + Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}}, + }, + }, `SELECT * FROM "x" LEFT OUTER JOIN "y"`) + + AssertStatementStringer(t, &parser.SelectStatement{ + Columns: []*parser.ResultColumn{{Star: pos(0)}}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}}, + Operator: &parser.JoinOperator{Cross: pos(0)}, + Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}}, + }, + }, `SELECT * FROM "x" CROSS JOIN "y"`) +} + +func TestUpdateStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.UpdateStatement{ + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + {Columns: []*parser.Ident{{Name: "y"}}, Expr: &parser.IntegerLit{Value: "200"}}, + }, + WhereExpr: &parser.BoolLit{Value: true}, + }, `UPDATE "tbl" SET "x" = 100, "y" = 200 WHERE TRUE`) + + AssertStatementStringer(t, &parser.UpdateStatement{ + UpdateOrRollback: pos(0), + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + }, + }, `UPDATE OR ROLLBACK "tbl" SET "x" = 100`) + + AssertStatementStringer(t, &parser.UpdateStatement{ + UpdateOrAbort: pos(0), + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + }, + }, `UPDATE OR ABORT "tbl" SET "x" = 100`) + + AssertStatementStringer(t, &parser.UpdateStatement{ + UpdateOrReplace: pos(0), + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + }, + }, `UPDATE OR REPLACE "tbl" SET "x" = 100`) + + AssertStatementStringer(t, &parser.UpdateStatement{ + UpdateOrFail: pos(0), + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + }, + }, `UPDATE OR FAIL "tbl" SET "x" = 100`) + + AssertStatementStringer(t, &parser.UpdateStatement{ + UpdateOrIgnore: pos(0), + Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}, + Assignments: []*parser.Assignment{ + {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}}, + }, + }, `UPDATE OR IGNORE "tbl" SET "x" = 100`) + + // AssertStatementStringer(t, &sql.UpdateStatement{ + // WithClause: &sql.WithClause{ + // CTEs: []*sql.CTE{{ + // TableName: &sql.Ident{Name: "cte"}, + // Select: &sql.SelectStatement{ + // Columns: []*sql.ResultColumn{{Star: pos(0)}}, + // }, + // }}, + // }, + // Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}}, + // Assignments: []*sql.Assignment{ + // {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}}, + // }, + // }, `WITH "cte" AS (SELECT *) UPDATE "tbl" SET "x" = 100`) +} + +func TestIdent_String(t *testing.T) { + AssertExprStringer(t, &parser.Ident{Name: "foo"}, `"foo"`) + AssertExprStringer(t, &parser.Ident{Name: "foo \" bar"}, `"foo "" bar"`) +} + +func TestStringLit_String(t *testing.T) { + AssertExprStringer(t, &parser.StringLit{Value: "foo"}, `'foo'`) + AssertExprStringer(t, &parser.StringLit{Value: "foo ' bar"}, `'foo '' bar'`) +} + +func TestNumberLit_String(t *testing.T) { + AssertExprStringer(t, &parser.IntegerLit{Value: "123.45"}, `123.45`) +} + +func TestBoolLit_String(t *testing.T) { + AssertExprStringer(t, &parser.BoolLit{Value: true}, `TRUE`) + AssertExprStringer(t, &parser.BoolLit{Value: false}, `FALSE`) +} + +func TestNullLit_String(t *testing.T) { + AssertExprStringer(t, &parser.NullLit{}, `NULL`) +} + +func TestParenExpr_String(t *testing.T) { + AssertExprStringer(t, &parser.ParenExpr{X: &parser.NullLit{}}, `(NULL)`) +} + +func TestUnaryExpr_String(t *testing.T) { + AssertExprStringer(t, &parser.UnaryExpr{Op: parser.PLUS, X: &parser.IntegerLit{Value: "100"}}, `+100`) + AssertExprStringer(t, &parser.UnaryExpr{Op: parser.MINUS, X: &parser.IntegerLit{Value: "100"}}, `-100`) + AssertNodeStringerPanic(t, &parser.UnaryExpr{X: &parser.IntegerLit{Value: "100"}}, `sql.UnaryExpr.String(): invalid op ILLEGAL`) +} + +func TestBinaryExpr_String(t *testing.T) { + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.PLUS, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 + 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.MINUS, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 - 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.STAR, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 * 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.SLASH, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 / 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.REM, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 % 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.CONCAT, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 || 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.BETWEEN, X: &parser.IntegerLit{Value: "1"}, Y: &parser.Range{X: &parser.IntegerLit{Value: "2"}, Y: &parser.IntegerLit{Value: "3"}}}, `1 BETWEEN 2 AND 3`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NOTBETWEEN, X: &parser.IntegerLit{Value: "1"}, Y: &parser.BinaryExpr{Op: parser.AND, X: &parser.IntegerLit{Value: "2"}, Y: &parser.IntegerLit{Value: "3"}}}, `1 NOT BETWEEN 2 AND 3`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.LSHIFT, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 << 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.RSHIFT, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 >> 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.BITAND, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 & 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.BITOR, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 | 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.LT, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 < 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.LE, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 <= 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.GT, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 > 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.GE, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 >= 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.EQ, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 = 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NE, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 != 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.IS, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 IS 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.ISNOT, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 IS NOT 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.IN, X: &parser.IntegerLit{Value: "1"}, Y: &parser.ExprList{Exprs: []parser.Expr{&parser.IntegerLit{Value: "2"}}}}, `1 IN (2)`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NOTIN, X: &parser.IntegerLit{Value: "1"}, Y: &parser.ExprList{Exprs: []parser.Expr{&parser.IntegerLit{Value: "2"}}}}, `1 NOT IN (2)`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.LIKE, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 LIKE 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NOTLIKE, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 NOT LIKE 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.GLOB, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 GLOB 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NOTGLOB, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 NOT GLOB 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.MATCH, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 MATCH 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NOTMATCH, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 NOT MATCH 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.REGEXP, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 REGEXP 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.NOTREGEXP, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 NOT REGEXP 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.AND, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 AND 2`) + AssertExprStringer(t, &parser.BinaryExpr{Op: parser.OR, X: &parser.IntegerLit{Value: "1"}, Y: &parser.IntegerLit{Value: "2"}}, `1 OR 2`) + AssertNodeStringerPanic(t, &parser.BinaryExpr{}, `sql.BinaryExpr.String(): invalid op ILLEGAL`) +} + +func TestCastExpr_String(t *testing.T) { + AssertExprStringer(t, &parser.CastExpr{X: &parser.IntegerLit{Value: "1"}, Type: &parser.Type{Name: &parser.Ident{Name: "INTEGER"}}}, `CAST(1 AS INTEGER)`) +} + +func TestCaseExpr_String(t *testing.T) { + AssertExprStringer(t, &parser.CaseExpr{ + Operand: &parser.Ident{Name: "foo"}, + Blocks: []*parser.CaseBlock{ + {Condition: &parser.IntegerLit{Value: "1"}, Body: &parser.BoolLit{Value: true}}, + {Condition: &parser.IntegerLit{Value: "2"}, Body: &parser.BoolLit{Value: false}}, + }, + ElseExpr: &parser.NullLit{}, + }, `CASE "foo" WHEN 1 THEN TRUE WHEN 2 THEN FALSE ELSE NULL END`) + + AssertExprStringer(t, &parser.CaseExpr{ + Blocks: []*parser.CaseBlock{ + {Condition: &parser.IntegerLit{Value: "1"}, Body: &parser.BoolLit{Value: true}}, + }, + }, `CASE WHEN 1 THEN TRUE END`) +} + +func TestExprList_String(t *testing.T) { + AssertExprStringer(t, &parser.ExprList{Exprs: []parser.Expr{&parser.NullLit{}}}, `(NULL)`) + AssertExprStringer(t, &parser.ExprList{Exprs: []parser.Expr{&parser.NullLit{}, &parser.NullLit{}}}, `(NULL, NULL)`) +} + +func TestQualifiedRef_String(t *testing.T) { + AssertExprStringer(t, &parser.QualifiedRef{Table: &parser.Ident{Name: "tbl"}, Column: &parser.Ident{Name: "col"}}, `"tbl"."col"`) + AssertExprStringer(t, &parser.QualifiedRef{Table: &parser.Ident{Name: "tbl"}, Star: pos(0)}, `"tbl".*`) +} + +func TestCall_String(t *testing.T) { + AssertExprStringer(t, &parser.Call{Name: &parser.Ident{Name: "foo"}}, `foo()`) + AssertExprStringer(t, &parser.Call{Name: &parser.Ident{Name: "foo"}, Star: pos(0)}, `foo(*)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Distinct: pos(0), + Args: []parser.Expr{ + &parser.NullLit{}, + &parser.NullLit{}, + }, + }, `foo(DISTINCT NULL, NULL)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Filter: &parser.FilterClause{ + X: &parser.BoolLit{Value: true}, + }, + }, `foo() FILTER (WHERE TRUE)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Name: &parser.Ident{Name: "win"}, + }, + }, `foo() OVER "win"`) + + t.Run("FrameSpec", func(t *testing.T) { + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Rows: pos(0), + X: &parser.NullLit{}, + PrecedingX: pos(0), + ExcludeNoOthers: pos(0), + }, + }, + }, + }, `foo() OVER (ROWS NULL PRECEDING EXCLUDE NO OTHERS)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Groups: pos(0), + CurrentRowX: pos(0), + ExcludeCurrentRow: pos(0), + }, + }, + }, + }, `foo() OVER (GROUPS CURRENT ROW EXCLUDE CURRENT ROW)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Rows: pos(0), + UnboundedX: pos(0), + PrecedingX: pos(0), + Between: pos(0), + CurrentRowY: pos(0), + }, + }, + }, + }, `foo() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Rows: pos(0), + X: &parser.NullLit{}, + PrecedingX: pos(0), + Between: pos(0), + CurrentRowY: pos(0), + }, + }, + }, + }, `foo() OVER (ROWS BETWEEN NULL PRECEDING AND CURRENT ROW)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Range: pos(0), + X: &parser.NullLit{}, + FollowingX: pos(0), + Between: pos(0), + Y: &parser.BoolLit{Value: true}, + PrecedingY: pos(0), + ExcludeGroup: pos(0), + }, + }, + }, + }, `foo() OVER (RANGE BETWEEN NULL FOLLOWING AND TRUE PRECEDING EXCLUDE GROUP)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Range: pos(0), + CurrentRowX: pos(0), + Between: pos(0), + Y: &parser.BoolLit{Value: true}, + FollowingY: pos(0), + ExcludeTies: pos(0), + }, + }, + }, + }, `foo() OVER (RANGE BETWEEN CURRENT ROW AND TRUE FOLLOWING EXCLUDE TIES)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Range: pos(0), + CurrentRowX: pos(0), + Between: pos(0), + CurrentRowY: pos(0), + }, + }, + }, + }, `foo() OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW)`) + + AssertExprStringer(t, &parser.Call{ + Name: &parser.Ident{Name: "foo"}, + Over: &parser.OverClause{ + Definition: &parser.WindowDefinition{ + Frame: &parser.FrameSpec{ + Range: pos(0), + CurrentRowX: pos(0), + Between: pos(0), + UnboundedY: pos(0), + FollowingY: pos(0), + }, + }, + }, + }, `foo() OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)`) + }) +} + +func TestExists_String(t *testing.T) { + AssertExprStringer(t, &parser.Exists{ + Select: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{ + {Star: pos(0)}, + }, + }, + }, `EXISTS (SELECT *)`) + + AssertExprStringer(t, &parser.Exists{ + Not: pos(0), + Exists: pos(0), + Select: &parser.SelectStatement{ + Columns: []*parser.ResultColumn{ + {Star: pos(0)}, + }, + }, + }, `NOT EXISTS (SELECT *)`) +} + +func AssertExprStringer(tb testing.TB, expr parser.Expr, s string) { + tb.Helper() + if str := expr.String(); str != s { + tb.Fatalf("String()=%s, expected %s", str, s) + } else if _, err := parser.NewParser(strings.NewReader(str)).ParseExpr(); err != nil { + tb.Fatalf("cannot parse string: %s; err=%s", str, err) + } +} + +func AssertStatementStringer(tb testing.TB, stmt parser.Statement, s string) { + tb.Helper() + if str := stmt.String(); str != s { + tb.Fatalf("String()=%s, expected %s", str, s) + } else if _, err := parser.NewParser(strings.NewReader(str)).ParseStatement(); err != nil { + tb.Fatalf("cannot parse string: %s; err=%s", str, err) + } +} + +func AssertNodeStringerPanic(tb testing.TB, node parser.Node, msg string) { + tb.Helper() + var r interface{} + func() { + defer func() { r = recover() }() + _ = node.String() + }() + if r == nil { + tb.Fatal("expected node stringer to panic") + } else if r != msg { + tb.Fatalf("recover()=%s, want %s", r, msg) + } +} + +// StripPos removes the position data from a node and its children. +// This function returns the root argument passed in. +func StripPos(root parser.Node) parser.Node { + zero := reflect.ValueOf(parser.Pos{}) + + _, _ = parser.Walk(parser.VisitFunc(func(node parser.Node) (parser.Node, error) { + value := reflect.Indirect(reflect.ValueOf(node)) + for i := 0; i < value.NumField(); i++ { + if field := value.Field(i); field.Type() == zero.Type() { + field.Set(zero) + } + } + return node, nil + }), root) + return root +} + +func StripExprPos(root parser.Expr) parser.Expr { + StripPos(root) + return root +} diff --git a/sql3/parser/astdatatype.go b/sql3/parser/astdatatype.go new file mode 100644 index 000000000..7844fd564 --- /dev/null +++ b/sql3/parser/astdatatype.go @@ -0,0 +1,182 @@ +package parser + +import ( + "fmt" + "math" + "strings" + + "github.com/molecula/featurebase/v3/pql" +) + +const ( + FieldTypeBool = "BOOL" + FieldTypeDecimal = "DECIMAL" + FieldTypeID = "ID" + FieldTypeIDSet = "IDSET" + FieldTypeInt = "INT" + FieldTypeString = "STRING" + FieldTypeStringSet = "STRINGSET" + FieldTypeTimestamp = "TIMESTAMP" +) + +func IsValidTypeName(typeName string) bool { + switch strings.ToUpper(typeName) { + case FieldTypeBool, + FieldTypeDecimal, + FieldTypeID, + FieldTypeIDSet, + FieldTypeInt, + FieldTypeString, + FieldTypeStringSet, + FieldTypeTimestamp: + return true + default: + return false + } +} + +type ExprDataType interface { + exprDataType() + TypeName() string +} + +func (*DataTypeVoid) exprDataType() {} +func (*DataTypeRange) exprDataType() {} +func (*DataTypeBool) exprDataType() {} +func (*DataTypeDecimal) exprDataType() {} +func (*DataTypeID) exprDataType() {} +func (*DataTypeIDSet) exprDataType() {} +func (*DataTypeInt) exprDataType() {} +func (*DataTypeString) exprDataType() {} +func (*DataTypeStringSet) exprDataType() {} +func (*DataTypeTimestamp) exprDataType() {} + +type DataTypeVoid struct { +} + +func NewDataTypeVoid() *DataTypeVoid { + return &DataTypeVoid{} +} + +func (*DataTypeVoid) TypeName() string { + return "VOID" +} + +type DataTypeRange struct { + SubscriptType ExprDataType +} + +func NewDataTypeRange(subscriptType ExprDataType) *DataTypeRange { + return &DataTypeRange{ + SubscriptType: subscriptType, + } +} + +func (dt *DataTypeRange) TypeName() string { + return fmt.Sprintf("RANGE(%s)", dt.SubscriptType.TypeName()) +} + +type DataTypeBool struct { +} + +func NewDataTypeBool() *DataTypeBool { + return &DataTypeBool{} +} + +func (*DataTypeBool) TypeName() string { + return FieldTypeBool +} + +type DataTypeDecimal struct { + Scale int64 +} + +func NewDataTypeDecimal(scale int64) *DataTypeDecimal { + return &DataTypeDecimal{ + Scale: scale, + } +} + +func (d *DataTypeDecimal) TypeName() string { + return fmt.Sprintf("%s(%d)", FieldTypeDecimal, d.Scale) +} + +type DataTypeID struct { +} + +func NewDataTypeID() *DataTypeID { + return &DataTypeID{} +} + +func (*DataTypeID) TypeName() string { + return FieldTypeID +} + +type DataTypeIDSet struct { +} + +func NewDataTypeIDSet() *DataTypeIDSet { + return &DataTypeIDSet{} +} + +func (*DataTypeIDSet) TypeName() string { + return FieldTypeIDSet +} + +type DataTypeInt struct { +} + +func NewDataTypeInt() *DataTypeInt { + return &DataTypeInt{} +} + +func (*DataTypeInt) TypeName() string { + return FieldTypeInt +} + +type DataTypeString struct { +} + +func NewDataTypeString() *DataTypeString { + return &DataTypeString{} +} + +func (*DataTypeString) TypeName() string { + return FieldTypeString +} + +type DataTypeStringSet struct { +} + +func NewDataTypeStringSet() *DataTypeStringSet { + return &DataTypeStringSet{} +} + +func (*DataTypeStringSet) TypeName() string { + return FieldTypeStringSet +} + +type DataTypeTimestamp struct { +} + +func NewDataTypeTimestamp() *DataTypeTimestamp { + return &DataTypeTimestamp{} +} + +func (*DataTypeTimestamp) TypeName() string { + return FieldTypeTimestamp +} + +func FloatToDecimal(v float64) pql.Decimal { + scale := NumDecimalPlaces(fmt.Sprintf("%v", v)) + unscaledValue := int64(v * math.Pow(10, float64(scale))) + return pql.NewDecimal(unscaledValue, int64(scale)) +} + +func NumDecimalPlaces(v string) int { + i := strings.IndexByte(v, '.') + if i > -1 { + return len(v) - i - 1 + } + return 0 +} diff --git a/sql2/parser.go b/sql3/parser/parser.go similarity index 79% rename from sql2/parser.go rename to sql3/parser/parser.go index 2910bf250..f4c3ce643 100644 --- a/sql2/parser.go +++ b/sql3/parser/parser.go @@ -1,10 +1,10 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2 +// Copyright 2021 Molecula Corp. All rights reserved. +package parser import ( "io" "strings" + "time" ) // Parser represents a SQL parser. @@ -45,10 +45,10 @@ func (p *Parser) ParseStatement() (stmt Statement, err error) { switch tok := p.peek(); tok { case EOF: return nil, io.EOF - case EXPLAIN: - if stmt, err = p.parseExplainStatement(); err != nil { - return stmt, err - } + //case EXPLAIN: + // if stmt, err = p.parseExplainStatement(); err != nil { + // return stmt, err + // } default: if stmt, err = p.parseNonExplainStatement(); err != nil { return stmt, err @@ -64,6 +64,7 @@ func (p *Parser) ParseStatement() (stmt Statement, err error) { return stmt, nil } +/* // parseExplain parses EXPLAIN [QUERY PLAN] STMT. func (p *Parser) parseExplainStatement() (_ *ExplainStatement, err error) { var tok Token @@ -88,30 +89,32 @@ func (p *Parser) parseExplainStatement() (_ *ExplainStatement, err error) { return &stmt, err } return &stmt, nil -} +}*/ // parseStmt parses all statement types. func (p *Parser) parseNonExplainStatement() (Statement, error) { switch p.peek() { - case ANALYZE: - return p.parseAnalyzeStatement() + //case ANALYZE: + // return p.parseAnalyzeStatement() case ALTER: return p.parseAlterTableStatement() - case BEGIN: - return p.parseBeginStatement() - case COMMIT, END: - return p.parseCommitStatement() - case ROLLBACK: - return p.parseRollbackStatement() - case SAVEPOINT: - return p.parseSavepointStatement() - case RELEASE: - return p.parseReleaseStatement() + // case BEGIN: + // return p.parseBeginStatement() + // case COMMIT, END: + // return p.parseCommitStatement() + // case ROLLBACK: + // return p.parseRollbackStatement() + // case SAVEPOINT: + // return p.parseSavepointStatement() + // case RELEASE: + // return p.parseReleaseStatement() + case BULK: + return p.parseBulkInsertStatement() case CREATE: return p.parseCreateStatement() case DROP: return p.parseDropStatement() - case SELECT, VALUES: + case SELECT: return p.parseSelectStatement(false, nil) case INSERT, REPLACE: return p.parseInsertStatement(nil) @@ -119,8 +122,10 @@ func (p *Parser) parseNonExplainStatement() (Statement, error) { return p.parseUpdateStatement(nil) case DELETE: return p.parseDeleteStatement(nil) - case WITH: - return p.parseWithStatement() + // case WITH: + // return p.parseWithStatement() + case SHOW: + return p.parseShowStatement() default: return nil, p.errorExpected(p.pos, p.tok, "statement") } @@ -128,7 +133,7 @@ func (p *Parser) parseNonExplainStatement() (Statement, error) { // parseWithStatement is called only from parseNonExplainStatement as we don't // know what kind of statement we'll have after the CTEs (e.g. SELECT, INSERT, etc). -func (p *Parser) parseWithStatement() (Statement, error) { +/*func (p *Parser) parseWithStatement() (Statement, error) { withClause, err := p.parseWithClause() if err != nil { return nil, err @@ -146,9 +151,55 @@ func (p *Parser) parseWithStatement() (Statement, error) { default: return nil, p.errorExpected(p.pos, p.tok, "SELECT, VALUES, INSERT, REPLACE, UPDATE, or DELETE") } +}*/ + +func (p *Parser) parseShowStatement() (Statement, error) { + assert(p.peek() == SHOW) + show, _, _ := p.scan() + + switch p.peek() { + case TABLES: + return p.parseShowTablesStatement(show) + case COLUMNS: + return p.parseShowColumnsStatement(show) + default: + return nil, p.errorExpected(p.pos, p.tok, "TABLES, COLUMNS") + } } -func (p *Parser) parseBeginStatement() (*BeginStatement, error) { +func (p *Parser) parseShowTablesStatement(showPos Pos) (*ShowTablesStatement, error) { + switch p.peek() { + case TABLES: + var stmt ShowTablesStatement + stmt.Show = showPos + stmt.Tables, _, _ = p.scan() + return &stmt, nil + default: + return nil, p.errorExpected(p.pos, p.tok, "TABLES") + } +} + +func (p *Parser) parseShowColumnsStatement(showPos Pos) (_ *ShowColumnsStatement, err error) { + assert(p.peek() == COLUMNS) + columns, _, _ := p.scan() + + var stmt ShowColumnsStatement + stmt.Show = showPos + stmt.Columns = columns + + switch p.peek() { + case FROM: + stmt.From, _, _ = p.scan() + if stmt.TableName, err = p.parseIdent("table name"); err != nil { + return &stmt, err + } + return &stmt, nil + default: + return nil, p.errorExpected(p.pos, p.tok, "FROM") + } +} + +/*func (p *Parser) parseBeginStatement() (*BeginStatement, error) { assert(p.peek() == BEGIN) var stmt BeginStatement @@ -169,9 +220,9 @@ func (p *Parser) parseBeginStatement() (*BeginStatement, error) { stmt.Transaction, _, _ = p.scan() } return &stmt, nil -} +}*/ -func (p *Parser) parseCommitStatement() (*CommitStatement, error) { +/*func (p *Parser) parseCommitStatement() (*CommitStatement, error) { assert(p.peek() == COMMIT || p.peek() == END) var stmt CommitStatement @@ -185,9 +236,9 @@ func (p *Parser) parseCommitStatement() (*CommitStatement, error) { stmt.Transaction, _, _ = p.scan() } return &stmt, nil -} +}*/ -func (p *Parser) parseRollbackStatement() (_ *RollbackStatement, err error) { +/*func (p *Parser) parseRollbackStatement() (_ *RollbackStatement, err error) { assert(p.peek() == ROLLBACK) var stmt RollbackStatement @@ -209,9 +260,9 @@ func (p *Parser) parseRollbackStatement() (_ *RollbackStatement, err error) { } } return &stmt, nil -} +}*/ -func (p *Parser) parseSavepointStatement() (_ *SavepointStatement, err error) { +/*func (p *Parser) parseSavepointStatement() (_ *SavepointStatement, err error) { assert(p.peek() == SAVEPOINT) var stmt SavepointStatement @@ -220,9 +271,9 @@ func (p *Parser) parseSavepointStatement() (_ *SavepointStatement, err error) { return &stmt, err } return &stmt, nil -} +}*/ -func (p *Parser) parseReleaseStatement() (_ *ReleaseStatement, err error) { +/*func (p *Parser) parseReleaseStatement() (_ *ReleaseStatement, err error) { assert(p.peek() == RELEASE) var stmt ReleaseStatement @@ -236,7 +287,7 @@ func (p *Parser) parseReleaseStatement() (_ *ReleaseStatement, err error) { return &stmt, err } return &stmt, nil -} +}*/ func (p *Parser) parseCreateStatement() (Statement, error) { assert(p.peek() == CREATE) @@ -245,14 +296,14 @@ func (p *Parser) parseCreateStatement() (Statement, error) { switch p.peek() { case TABLE: return p.parseCreateTableStatement(pos) - case VIEW: - return p.parseCreateViewStatement(pos) - case INDEX, UNIQUE: - return p.parseCreateIndexStatement(pos) - case TRIGGER: - return p.parseCreateTriggerStatement(pos) + /* case VIEW: + return p.parseCreateViewStatement(pos) + case INDEX, UNIQUE: + return p.parseCreateIndexStatement(pos) + case TRIGGER: + return p.parseCreateTriggerStatement(pos)*/ default: - return nil, p.errorExpected(pos, tok, "TABLE, VIEW, INDEX, TRIGGER") + return nil, p.errorExpected(pos, tok, "TABLE") } } @@ -263,14 +314,14 @@ func (p *Parser) parseDropStatement() (Statement, error) { switch p.peek() { case TABLE: return p.parseDropTableStatement(pos) - case VIEW: - return p.parseDropViewStatement(pos) - case INDEX: - return p.parseDropIndexStatement(pos) - case TRIGGER: - return p.parseDropTriggerStatement(pos) + /* case VIEW: + return p.parseDropViewStatement(pos) + case INDEX: + return p.parseDropIndexStatement(pos) + case TRIGGER: + return p.parseDropTriggerStatement(pos)*/ default: - return nil, p.errorExpected(pos, tok, "TABLE, VIEW, INDEX, or TRIGGER") + return nil, p.errorExpected(pos, tok, "TABLE") } } @@ -309,24 +360,95 @@ func (p *Parser) parseCreateTableStatement(createPos Pos) (_ *CreateTableStateme if stmt.Columns, err = p.parseColumnDefinitions(); err != nil { return &stmt, err - } else if stmt.Constraints, err = p.parseTableConstraints(); err != nil { + } /*else if stmt.Constraints, err = p.parseTableConstraints(); err != nil { return &stmt, err - } + }*/ if p.peek() != RP { return &stmt, p.errorExpected(p.pos, p.tok, "right paren") } stmt.Rparen, _, _ = p.scan() - return &stmt, nil - case AS: - stmt.As, _, _ = p.scan() - if stmt.Select, err = p.parseSelectStatement(false, nil); err != nil { + + //look for table options + if stmt.Options, err = p.parseTableOptions(); err != nil { return &stmt, err } return &stmt, nil - default: - return &stmt, p.errorExpected(p.pos, p.tok, "AS or left paren") + /*case AS: + stmt.As, _, _ = p.scan() + if stmt.Select, err = p.parseSelectStatement(false, nil); err != nil { + return &stmt, err } + return &stmt, nil*/ + default: + return &stmt, p.errorExpected(p.pos, p.tok, "left paren") + } +} + +func (p *Parser) parseTableOptions() (_ []TableOption, err error) { + if !isTableOptionStartToken(p.peek()) { + return nil, nil + } + + var a []TableOption + + for { + if !isTableOptionStartToken(p.peek()) { + return a, nil + } + cons, err := p.parseTableOption() + if cons != nil { + a = append(a, cons) + } + if err != nil { + return a, err + } + } +} + +func (p *Parser) parseTableOption() (_ TableOption, err error) { + assert(isTableOptionStartToken(p.peek())) + + var optionPos Pos + + // Parse column constraints. + switch p.peek() { + case KEYPARTITIONS: + return p.parseKeyPartitionsOption(optionPos) + default: + assert(p.peek() == SHARDWIDTH) + return p.parseShardWidthOption(optionPos) + } +} + +func (p *Parser) parseKeyPartitionsOption(optionPos Pos) (_ *KeyPartitionsOption, err error) { + assert(p.peek() == KEYPARTITIONS) + + var opt KeyPartitionsOption + opt.KeyPartitions, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + opt.Expr = p.mustParseLiteral() + } else { + return &opt, p.errorExpected(p.pos, p.tok, "literal") + } + + return &opt, nil +} + +func (p *Parser) parseShardWidthOption(optionPos Pos) (_ *ShardWidthOption, err error) { + assert(p.peek() == SHARDWIDTH) + + var opt ShardWidthOption + opt.ShardWidth, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + opt.Expr = p.mustParseLiteral() + } else { + return &opt, p.errorExpected(p.pos, p.tok, "literal") + } + + return &opt, nil } func (p *Parser) parseColumnDefinitions() (_ []*ColumnDefinition, err error) { @@ -345,7 +467,7 @@ func (p *Parser) parseColumnDefinitions() (_ []*ColumnDefinition, err error) { case p.peek() == RP || isConstraintStartToken(p.peek(), true): return columns, nil default: - return columns, p.errorExpected(p.pos, p.tok, "column name, CONSTRAINT, or right paren") + return columns, p.errorExpected(p.pos, p.tok, "column name, or right paren") } } } @@ -364,7 +486,7 @@ func (p *Parser) parseColumnDefinition() (_ *ColumnDefinition, err error) { return &col, nil } -func (p *Parser) parseTableConstraints() (_ []Constraint, err error) { +/*func (p *Parser) parseTableConstraints() (_ []Constraint, err error) { if !isConstraintStartToken(p.peek(), true) { return nil, nil } @@ -385,15 +507,13 @@ func (p *Parser) parseTableConstraints() (_ []Constraint, err error) { } p.scan() } -} +}*/ func (p *Parser) parseColumnConstraints() (_ []Constraint, err error) { var a []Constraint for isConstraintStartToken(p.peek(), false) { cons, err := p.parseConstraint(false) - if cons != nil { - a = append(a, cons) - } + a = append(a, cons) if err != nil { return a, err } @@ -408,16 +528,16 @@ func (p *Parser) parseConstraint(isTable bool) (_ Constraint, err error) { var name *Ident // Parse constraint name, if specified. - if p.peek() == CONSTRAINT { + /*if p.peek() == CONSTRAINT { constraintPos, _, _ = p.scan() if name, err = p.parseIdent("constraint name"); err != nil { return nil, err } - } + }*/ // Table constraints only use a subset of column constraints. - if isTable { + /*if isTable { switch p.peek() { case PRIMARY: return p.parsePrimaryKeyConstraint(constraintPos, name, isTable) @@ -429,27 +549,35 @@ func (p *Parser) parseConstraint(isTable bool) (_ Constraint, err error) { assert(p.peek() == FOREIGN) return p.parseForeignKeyConstraint(constraintPos, name, isTable) } - } + }*/ // Parse column constraints. switch p.peek() { - case PRIMARY: - return p.parsePrimaryKeyConstraint(constraintPos, name, isTable) - case NOT: - return p.parseNotNullConstraint(constraintPos, name) - case UNIQUE: - return p.parseUniqueConstraint(constraintPos, name, isTable) - case CHECK: - return p.parseCheckConstraint(constraintPos, name) - case DEFAULT: - return p.parseDefaultConstraint(constraintPos, name) + //case PRIMARY: + // return p.parsePrimaryKeyConstraint(constraintPos, name, isTable) + //case NOT: + // return p.parseNotNullConstraint(constraintPos, name) + case MIN: + return p.parseMinConstraint(constraintPos, name) + case MAX: + return p.parseMaxConstraint(constraintPos, name) + case TIMEUNIT: + return p.parseTimeUnitConstraint(constraintPos, name) + case TIMEQUANTUM: + return p.parseTimeQuantumConstraint(constraintPos, name) + //case UNIQUE: + // return p.parseUniqueConstraint(constraintPos, name, isTable) + //case CHECK: + // return p.parseCheckConstraint(constraintPos, name) + //case DEFAULT: + // return p.parseDefaultConstraint(constraintPos, name) default: - assert(p.peek() == REFERENCES) - return p.parseForeignKeyConstraint(constraintPos, name, isTable) + assert(p.peek() == CACHETYPE) + return p.parseCacheTypeConstraint(constraintPos, name) } } -func (p *Parser) parsePrimaryKeyConstraint(constraintPos Pos, name *Ident, isTable bool) (_ *PrimaryKeyConstraint, err error) { +/*func (p *Parser) parsePrimaryKeyConstraint(constraintPos Pos, name *Ident, isTable bool) (_ *PrimaryKeyConstraint, err error) { assert(p.peek() == PRIMARY) var cons PrimaryKeyConstraint @@ -493,9 +621,9 @@ func (p *Parser) parsePrimaryKeyConstraint(constraintPos Pos, name *Ident, isTab } } return &cons, nil -} +}*/ -func (p *Parser) parseNotNullConstraint(constraintPos Pos, name *Ident) (_ *NotNullConstraint, err error) { +/*func (p *Parser) parseNotNullConstraint(constraintPos Pos, name *Ident) (_ *NotNullConstraint, err error) { assert(p.peek() == NOT) var cons NotNullConstraint @@ -508,10 +636,119 @@ func (p *Parser) parseNotNullConstraint(constraintPos Pos, name *Ident) (_ *NotN } cons.Null, _, _ = p.scan() + return &cons, nil +}*/ + +func (p *Parser) parseMinConstraint(constraintPos Pos, name *Ident) (_ *MinConstraint, err error) { + assert(p.peek() == MIN) + + var cons MinConstraint + cons.Min, _, _ = p.scan() + + // This parses an expression, as opposed to just a literal, because a + // negative value is a unary expression with Op = MINUS, so we need to allow + // for that. + expr, err := p.ParseExpr() + if err != nil { + return nil, err + } + cons.Expr = expr + return &cons, nil } -func (p *Parser) parseUniqueConstraint(constraintPos Pos, name *Ident, isTable bool) (_ *UniqueConstraint, err error) { +func (p *Parser) parseMaxConstraint(constraintPos Pos, name *Ident) (_ *MaxConstraint, err error) { + assert(p.peek() == MAX) + + var cons MaxConstraint + cons.Max, _, _ = p.scan() + + // This parses an expression, as opposed to just a literal, because a + // negative value is a unary expression with Op = MINUS, so we need to allow + // for that. + expr, err := p.ParseExpr() + if err != nil { + return nil, err + } + cons.Expr = expr + + return &cons, nil +} + +func (p *Parser) parseCacheTypeConstraint(constraintPos Pos, name *Ident) (_ *CacheTypeConstraint, err error) { + assert(p.peek() == CACHETYPE) + + var cons CacheTypeConstraint + cons.CacheType, _, _ = p.scan() + + switch p.peek() { + case RANKED, LRU: + _, _, cacheTypeValue := p.scan() + // FeatureBase expects a lowercase cache type value. + cons.CacheTypeValue = strings.ToLower(cacheTypeValue) + default: + return &cons, p.errorExpected(p.pos, p.tok, "RANKED or LRU") + } + + if p.peek() == SIZE { + cons.Size, _, _ = p.scan() + if isLiteralToken(p.peek()) { + cons.SizeExpr = p.mustParseLiteral() + } else { + return &cons, p.errorExpected(p.pos, p.tok, "literal") + } + } + + return &cons, nil +} + +func (p *Parser) parseTimeUnitConstraint(constraintPos Pos, name *Ident) (_ *TimeUnitConstraint, err error) { + assert(p.peek() == TIMEUNIT) + + var cons TimeUnitConstraint + cons.TimeUnit, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + cons.Expr = p.mustParseLiteral() + } else { + return &cons, p.errorExpected(p.pos, p.tok, "literal") + } + if p.peek() == EPOCH { + cons.Epoch, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + cons.EpochExpr = p.mustParseLiteral() + } else { + return &cons, p.errorExpected(p.pos, p.tok, "literal") + } + } + return &cons, nil +} + +func (p *Parser) parseTimeQuantumConstraint(constraintPos Pos, name *Ident) (_ *TimeQuantumConstraint, err error) { + assert(p.peek() == TIMEQUANTUM) + + var cons TimeQuantumConstraint + cons.TimeQuantum, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + cons.Expr = p.mustParseLiteral() + } else { + return &cons, p.errorExpected(p.pos, p.tok, "literal") + } + if p.peek() == TTL { + cons.Ttl, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + cons.TtlExpr = p.mustParseLiteral() + } else { + return &cons, p.errorExpected(p.pos, p.tok, "literal") + } + } + return &cons, nil +} + +/*func (p *Parser) parseUniqueConstraint(constraintPos Pos, name *Ident, isTable bool) (_ *UniqueConstraint, err error) { assert(p.peek() == UNIQUE) var cons UniqueConstraint @@ -543,9 +780,9 @@ func (p *Parser) parseUniqueConstraint(constraintPos Pos, name *Ident, isTable b } return &cons, nil -} +}*/ -func (p *Parser) parseCheckConstraint(constraintPos Pos, name *Ident) (_ *CheckConstraint, err error) { +/*func (p *Parser) parseCheckConstraint(constraintPos Pos, name *Ident) (_ *CheckConstraint, err error) { assert(p.peek() == CHECK) var cons CheckConstraint @@ -568,9 +805,9 @@ func (p *Parser) parseCheckConstraint(constraintPos Pos, name *Ident) (_ *CheckC cons.Rparen, _, _ = p.scan() return &cons, nil -} +}*/ -func (p *Parser) parseDefaultConstraint(constraintPos Pos, name *Ident) (_ *DefaultConstraint, err error) { +/*func (p *Parser) parseDefaultConstraint(constraintPos Pos, name *Ident) (_ *DefaultConstraint, err error) { assert(p.peek() == DEFAULT) var cons DefaultConstraint @@ -599,9 +836,9 @@ func (p *Parser) parseDefaultConstraint(constraintPos Pos, name *Ident) (_ *Defa cons.Rparen, _, _ = p.scan() } return &cons, nil -} +}*/ -func (p *Parser) parseForeignKeyConstraint(constraintPos Pos, name *Ident, isTable bool) (_ *ForeignKeyConstraint, err error) { +/*func (p *Parser) parseForeignKeyConstraint(constraintPos Pos, name *Ident, isTable bool) (_ *ForeignKeyConstraint, err error) { var cons ForeignKeyConstraint cons.Constraint = constraintPos cons.Name = name @@ -735,7 +972,7 @@ func (p *Parser) parseForeignKeyConstraint(constraintPos Pos, name *Ident, isTab } return &cons, nil -} +}*/ func (p *Parser) parseDropTableStatement(dropPos Pos) (_ *DropTableStatement, err error) { assert(p.peek() == TABLE) @@ -760,7 +997,7 @@ func (p *Parser) parseDropTableStatement(dropPos Pos) (_ *DropTableStatement, er return &stmt, nil } -func (p *Parser) parseCreateViewStatement(createPos Pos) (_ *CreateViewStatement, err error) { +/*func (p *Parser) parseCreateViewStatement(createPos Pos) (_ *CreateViewStatement, err error) { assert(p.peek() == VIEW) var stmt CreateViewStatement @@ -815,9 +1052,9 @@ func (p *Parser) parseCreateViewStatement(createPos Pos) (_ *CreateViewStatement return &stmt, err } return &stmt, nil -} +}*/ -func (p *Parser) parseDropViewStatement(dropPos Pos) (_ *DropViewStatement, err error) { +/*func (p *Parser) parseDropViewStatement(dropPos Pos) (_ *DropViewStatement, err error) { assert(p.peek() == VIEW) var stmt DropViewStatement @@ -838,9 +1075,9 @@ func (p *Parser) parseDropViewStatement(dropPos Pos) (_ *DropViewStatement, err } return &stmt, nil -} +}*/ -func (p *Parser) parseCreateIndexStatement(createPos Pos) (_ *CreateIndexStatement, err error) { +/*func (p *Parser) parseCreateIndexStatement(createPos Pos) (_ *CreateIndexStatement, err error) { assert(p.peek() == INDEX || p.peek() == UNIQUE) var stmt CreateIndexStatement @@ -910,9 +1147,9 @@ func (p *Parser) parseCreateIndexStatement(createPos Pos) (_ *CreateIndexStateme } } return &stmt, nil -} +}*/ -func (p *Parser) parseDropIndexStatement(dropPos Pos) (_ *DropIndexStatement, err error) { +/*func (p *Parser) parseDropIndexStatement(dropPos Pos) (_ *DropIndexStatement, err error) { assert(p.peek() == INDEX) var stmt DropIndexStatement @@ -933,9 +1170,9 @@ func (p *Parser) parseDropIndexStatement(dropPos Pos) (_ *DropIndexStatement, er } return &stmt, nil -} +}*/ -func (p *Parser) parseCreateTriggerStatement(createPos Pos) (_ *CreateTriggerStatement, err error) { +/*func (p *Parser) parseCreateTriggerStatement(createPos Pos) (_ *CreateTriggerStatement, err error) { assert(p.peek() == TRIGGER) var stmt CreateTriggerStatement @@ -1052,9 +1289,9 @@ func (p *Parser) parseCreateTriggerStatement(createPos Pos) (_ *CreateTriggerSta stmt.End, _, _ = p.scan() return &stmt, nil -} +}*/ -func (p *Parser) parseTriggerBodyStatement() (stmt Statement, err error) { +/*func (p *Parser) parseTriggerBodyStatement() (stmt Statement, err error) { switch p.peek() { case SELECT, VALUES: stmt, err = p.parseSelectStatement(false, nil) @@ -1064,8 +1301,8 @@ func (p *Parser) parseTriggerBodyStatement() (stmt Statement, err error) { stmt, err = p.parseUpdateStatement(nil) case DELETE: stmt, err = p.parseDeleteStatement(nil) - case WITH: - stmt, err = p.parseWithStatement() + //case WITH: + // stmt, err = p.parseWithStatement() default: return nil, p.errorExpected(p.pos, p.tok, "statement") } @@ -1080,9 +1317,9 @@ func (p *Parser) parseTriggerBodyStatement() (stmt Statement, err error) { p.scan() return stmt, nil -} +}*/ -func (p *Parser) parseDropTriggerStatement(dropPos Pos) (_ *DropTriggerStatement, err error) { +/*func (p *Parser) parseDropTriggerStatement(dropPos Pos) (_ *DropTriggerStatement, err error) { assert(p.peek() == TRIGGER) var stmt DropTriggerStatement @@ -1103,7 +1340,7 @@ func (p *Parser) parseDropTriggerStatement(dropPos Pos) (_ *DropTriggerStatement } return &stmt, nil -} +}*/ func (p *Parser) parseIdent(desc string) (*Ident, error) { pos, tok, lit := p.scan() @@ -1121,20 +1358,13 @@ func (p *Parser) parseType() (_ *Type, err error) { return &typ, err } - // Optionally parse precision & scale. + // Optionally parse scale. if p.peek() == LP { typ.Lparen, _, _ = p.scan() - if typ.Precision, err = p.parseSignedNumber("precision"); err != nil { + if typ.Scale, err = p.parseIntegerLiteral("scale"); err != nil { return &typ, err } - if p.peek() == COMMA { - p.scan() - if typ.Scale, err = p.parseSignedNumber("scale"); err != nil { - return &typ, err - } - } - if p.peek() != RP { return nil, p.errorExpected(p.pos, p.tok, "right paren") } @@ -1144,11 +1374,46 @@ func (p *Parser) parseType() (_ *Type, err error) { return &typ, nil } +func (p *Parser) parseBulkInsertStatement() (_ *BulkInsertStatement, err error) { + if p.peek() != BULK { + return nil, p.errorExpected(p.pos, p.tok, "BULK") + } + + var stmt BulkInsertStatement + + stmt.Bulk, _, _ = p.scan() + + if p.peek() != INSERT { + return nil, p.errorExpected(p.pos, p.tok, "INSERT") + } + stmt.Insert, _, _ = p.scan() + + // Parse table name & optional alias. + if stmt.Table, err = p.parseIdent("table name"); err != nil { + return nil, err + } + + if p.peek() != FROM { + return nil, p.errorExpected(p.pos, p.tok, "FROM") + } + stmt.From, _, _ = p.scan() + + if isLiteralToken(p.peek()) { + stmt.DataFile = p.mustParseLiteral() + } else { + return nil, p.errorExpected(p.pos, p.tok, "literal") + } + + return &stmt, nil +} + func (p *Parser) parseInsertStatement(withClause *WithClause) (_ *InsertStatement, err error) { - assert(p.peek() == INSERT || p.peek() == REPLACE) + if pk := p.peek(); pk != INSERT && pk != REPLACE { + return nil, p.errorExpected(p.pos, p.tok, "INSERT or REPLACE") + } var stmt InsertStatement - stmt.WithClause = withClause + //stmt.WithClause = withClause if p.peek() == INSERT { stmt.Insert, _, _ = p.scan() @@ -1157,18 +1422,18 @@ func (p *Parser) parseInsertStatement(withClause *WithClause) (_ *InsertStatemen stmt.InsertOr, _, _ = p.scan() switch p.peek() { - case ROLLBACK: - stmt.InsertOrRollback, _, _ = p.scan() + //case ROLLBACK: + // stmt.InsertOrRollback, _, _ = p.scan() case REPLACE: stmt.InsertOrReplace, _, _ = p.scan() - case ABORT: - stmt.InsertOrAbort, _, _ = p.scan() - case FAIL: - stmt.InsertOrFail, _, _ = p.scan() - case IGNORE: - stmt.InsertOrIgnore, _, _ = p.scan() + //case ABORT: + // stmt.InsertOrAbort, _, _ = p.scan() + //case FAIL: + // stmt.InsertOrFail, _, _ = p.scan() + //case IGNORE: + // stmt.InsertOrIgnore, _, _ = p.scan() default: - return &stmt, p.errorExpected(p.pos, p.tok, "ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE") + return &stmt, p.errorExpected(p.pos, p.tok, "REPLACE") } } } else { @@ -1236,38 +1501,38 @@ func (p *Parser) parseInsertStatement(withClause *WithClause) (_ *InsertStatemen p.scan() } list.Rparen, _, _ = p.scan() - stmt.ValueLists = append(stmt.ValueLists, &list) + stmt.ValueList = &list if p.peek() != COMMA { break } p.scan() } - case SELECT: - if stmt.Select, err = p.parseSelectStatement(false, nil); err != nil { - return &stmt, err - } - case DEFAULT: - stmt.Default, _, _ = p.scan() - if p.peek() != VALUES { - return &stmt, p.errorExpected(p.pos, p.tok, "VALUES") - } - stmt.DefaultValues, _, _ = p.scan() + //case SELECT: + // if stmt.Select, err = p.parseSelectStatement(false, nil); err != nil { + // return &stmt, err + // } + //case DEFAULT: + // stmt.Default, _, _ = p.scan() + // if p.peek() != VALUES { + // return &stmt, p.errorExpected(p.pos, p.tok, "VALUES") + // } + // stmt.DefaultValues, _, _ = p.scan() default: - return &stmt, p.errorExpected(p.pos, p.tok, "VALUES, SELECT, or DEFAULT VALUES") + return &stmt, p.errorExpected(p.pos, p.tok, "VALUES") } // Parse optional upsert clause. - if p.peek() == ON { - if stmt.UpsertClause, err = p.parseUpsertClause(); err != nil { - return &stmt, err - } - } + //if p.peek() == ON { + // if stmt.UpsertClause, err = p.parseUpsertClause(); err != nil { + // return &stmt, err + // } + //} return &stmt, nil } -func (p *Parser) parseUpsertClause() (_ *UpsertClause, err error) { +/*func (p *Parser) parseUpsertClause() (_ *UpsertClause, err error) { assert(p.peek() == ON) var clause UpsertClause @@ -1350,9 +1615,9 @@ func (p *Parser) parseUpsertClause() (_ *UpsertClause, err error) { } return &clause, nil -} +}*/ -func (p *Parser) parseIndexedColumn() (_ *IndexedColumn, err error) { +/*func (p *Parser) parseIndexedColumn() (_ *IndexedColumn, err error) { var col IndexedColumn if col.X, err = p.ParseExpr(); err != nil { return &col, err @@ -1363,7 +1628,7 @@ func (p *Parser) parseIndexedColumn() (_ *IndexedColumn, err error) { col.Desc, _, _ = p.scan() } return &col, nil -} +}*/ func (p *Parser) parseUpdateStatement(withClause *WithClause) (_ *UpdateStatement, err error) { assert(p.peek() == UPDATE) @@ -1451,7 +1716,7 @@ func (p *Parser) parseDeleteStatement(withClause *WithClause) (_ *DeleteStatemen // Parse ORDER BY clause. This differs from the SELECT parsing in that // if an ORDER BY is specified then the LIMIT is required. - if p.peek() == ORDER || p.peek() == LIMIT { + if p.peek() == ORDER { if p.peek() == ORDER { stmt.Order, _, _ = p.scan() if p.peek() != BY { @@ -1472,26 +1737,6 @@ func (p *Parser) parseDeleteStatement(withClause *WithClause) (_ *DeleteStatemen p.scan() } } - - // Parse LIMIT/OFFSET clause. - if p.peek() != LIMIT { - return &stmt, p.errorExpected(p.pos, p.tok, "LIMIT") - } - stmt.Limit, _, _ = p.scan() - if stmt.LimitExpr, err = p.ParseExpr(); err != nil { - return &stmt, err - } - - if tok := p.peek(); tok == OFFSET || tok == COMMA { - if tok == OFFSET { - stmt.Offset, _, _ = p.scan() - } else { - stmt.OffsetComma, _, _ = p.scan() - } - if stmt.OffsetExpr, err = p.ParseExpr(); err != nil { - return &stmt, err - } - } } return &stmt, nil @@ -1538,63 +1783,87 @@ func (p *Parser) parseAssignment() (_ *Assignment, err error) { } // parseSelectStatement parses a SELECT statement. -// If compounded is true, WITH, ORDER BY, & LIMIT/OFFSET are skipped. +// If compounded is true, some parts of the SELECT syntax are skipped. func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) (_ *SelectStatement, err error) { var stmt SelectStatement - stmt.WithClause = withClause + //stmt.WithClause = withClause // Parse optional "WITH [RECURSIVE} cte, cte..." // This is only called here if this method is called directly. Generic // statement parsing will parse the WITH clause and pass it in instead. - if !compounded && stmt.WithClause == nil && p.peek() == WITH { - if stmt.WithClause, err = p.parseWithClause(); err != nil { - return &stmt, err - } - } + //if !compounded && stmt.WithClause == nil && p.peek() == WITH { + // if stmt.WithClause, err = p.parseWithClause(); err != nil { + // return &stmt, err + // } + //} switch p.peek() { - case VALUES: - stmt.Values, _, _ = p.scan() + /*case VALUES: + stmt.Values, _, _ = p.scan() + + for { + var list ExprList + if p.peek() != LP { + return &stmt, p.errorExpected(p.pos, p.tok, "left paren") + } + list.Lparen, _, _ = p.scan() for { - var list ExprList - if p.peek() != LP { - return &stmt, p.errorExpected(p.pos, p.tok, "left paren") + expr, err := p.ParseExpr() + if err != nil { + return &stmt, err } - list.Lparen, _, _ = p.scan() + list.Exprs = append(list.Exprs, expr) - for { - expr, err := p.ParseExpr() - if err != nil { - return &stmt, err - } - list.Exprs = append(list.Exprs, expr) - - if p.peek() == RP { - break - } else if p.peek() != COMMA { - return &stmt, p.errorExpected(p.pos, p.tok, "comma or right paren") - } - p.scan() - } - list.Rparen, _, _ = p.scan() - stmt.ValueLists = append(stmt.ValueLists, &list) - - if p.peek() != COMMA { + if p.peek() == RP { break + } else if p.peek() != COMMA { + return &stmt, p.errorExpected(p.pos, p.tok, "comma or right paren") } p.scan() - } + list.Rparen, _, _ = p.scan() + stmt.ValueLists = append(stmt.ValueLists, &list) + + if p.peek() != COMMA { + break + } + p.scan() + + }*/ case SELECT: stmt.Select, _, _ = p.scan() - // Parse optional "DISTINCT" or "ALL". + // Parse optional "DISTINCT". if tok := p.peek(); tok == DISTINCT { stmt.Distinct, _, _ = p.scan() - } else if tok == ALL { - stmt.All, _, _ = p.scan() + } + + if p.peek() == TOP { + stmt.Top, _, _ = p.scan() + if p.peek() == LP { + _, _, _ = p.scan() + } + if stmt.TopExpr, err = p.ParseExpr(); err != nil { + return &stmt, err + } + if p.peek() == RP { + _, _, _ = p.scan() + } + } + + if p.peek() == TOPN { + stmt.TopN, _, _ = p.scan() + if p.peek() == LP { + _, _, _ = p.scan() + } + if stmt.TopExpr, err = p.ParseExpr(); err != nil { + return &stmt, err + } + if p.peek() == RP { + _, _, _ = p.scan() + } } // Parse result columns. @@ -1685,7 +1954,7 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) ( } } default: - return &stmt, p.errorExpected(p.pos, p.tok, "SELECT or VALUES") + return &stmt, p.errorExpected(p.pos, p.tok, "SELECT") } // Optionally compound additional SELECT/VALUES. @@ -1729,27 +1998,6 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) ( } } - // Parse LIMIT/OFFSET clause. - // The offset is optional. Can be specified with COMMA or OFFSET. - // e.g. "LIMIT 1 OFFSET 2" or "LIMIT 1, 2" - if !compounded && p.peek() == LIMIT { - stmt.Limit, _, _ = p.scan() - if stmt.LimitExpr, err = p.ParseExpr(); err != nil { - return &stmt, err - } - - if tok := p.peek(); tok == OFFSET || tok == COMMA { - if tok == OFFSET { - stmt.Offset, _, _ = p.scan() - } else { - stmt.OffsetComma, _, _ = p.scan() - } - if stmt.OffsetExpr, err = p.ParseExpr(); err != nil { - return &stmt, err - } - } - } - return &stmt, nil } @@ -1987,7 +2235,7 @@ func (p *Parser) parseQualifiedTableName() (_ *QualifiedTableName, err error) { } // Parse optional "INDEXED BY index-name" or "NOT INDEXED". - switch p.peek() { + /*switch p.peek() { case INDEXED: tbl.Indexed, _, _ = p.scan() if p.peek() != BY { @@ -2004,12 +2252,12 @@ func (p *Parser) parseQualifiedTableName() (_ *QualifiedTableName, err error) { return &tbl, p.errorExpected(p.pos, p.tok, "INDEXED") } tbl.NotIndexed, _, _ = p.scan() - } + }*/ return &tbl, nil } -func (p *Parser) parseWithClause() (*WithClause, error) { +/*func (p *Parser) parseWithClause() (*WithClause, error) { assert(p.peek() == WITH) var clause WithClause @@ -2032,9 +2280,9 @@ func (p *Parser) parseWithClause() (*WithClause, error) { p.scan() } return &clause, nil -} +}*/ -func (p *Parser) parseCTE() (_ *CTE, err error) { +/*func (p *Parser) parseCTE() (_ *CTE, err error) { var cte CTE if cte.TableName, err = p.parseIdent("table name"); err != nil { return &cte, err @@ -2082,7 +2330,7 @@ func (p *Parser) parseCTE() (_ *CTE, err error) { cte.SelectRparen, _, _ = p.scan() return &cte, nil -} +}*/ func (p *Parser) mustParseLiteral() Expr { assert(isLiteralToken(p.tok)) @@ -2090,15 +2338,15 @@ func (p *Parser) mustParseLiteral() Expr { switch tok { case STRING: return &StringLit{ValuePos: pos, Value: lit} - case BLOB: - return &BlobLit{ValuePos: pos, Value: lit} - case FLOAT, INTEGER: - return &NumberLit{ValuePos: pos, Value: lit} + case INTEGER: + return &IntegerLit{ValuePos: pos, Value: lit} + case FLOAT: + return &FloatLit{ValuePos: pos, Value: lit} case TRUE, FALSE: return &BoolLit{ValuePos: pos, Value: tok == TRUE} default: assert(tok == NULL) - return &NullLit{Pos: pos} + return &NullLit{ValuePos: pos} } } @@ -2117,19 +2365,27 @@ func (p *Parser) parseOperand() (expr Expr, err error) { return p.parseCall(ident) } return ident, nil + case MIN, MAX: + ident := &Ident{Name: lit, NamePos: pos, Quoted: tok == QIDENT} + return p.parseCall(ident) case STRING: return &StringLit{ValuePos: pos, Value: lit}, nil - case BLOB: - return &BlobLit{ValuePos: pos, Value: lit}, nil - case FLOAT, INTEGER: - return &NumberLit{ValuePos: pos, Value: lit}, nil + case FLOAT: + return &FloatLit{ValuePos: pos, Value: lit}, nil + case INTEGER: + return &IntegerLit{ValuePos: pos, Value: lit}, nil case NULL: - return &NullLit{Pos: pos}, nil + return &NullLit{ValuePos: pos}, nil + case CURRENT_DATE: + now := time.Now().UTC() + nowDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + return &DateLit{ValuePos: pos, Value: nowDate}, nil + case CURRENT_TIMESTAMP: + now := time.Now().UTC() + return &DateLit{ValuePos: pos, Value: now}, nil case TRUE, FALSE: return &BoolLit{ValuePos: pos, Value: tok == TRUE}, nil - case BIND: - return &BindExpr{NamePos: pos, Name: lit}, nil - case PLUS, MINUS: + case PLUS, MINUS, BITNOT: expr, err = p.parseOperand() if err != nil { return nil, err @@ -2138,18 +2394,22 @@ func (p *Parser) parseOperand() (expr Expr, err error) { case LP: p.unscan() return p.parseParenExpr() + case LB: + p.unscan() + return p.parseSetLiteralExpr() case CAST: p.unscan() return p.parseCastExpr() case CASE: p.unscan() return p.parseCaseExpr() - case RAISE: - p.unscan() - return p.parseRaise() case NOT, EXISTS: p.unscan() return p.parseExists() + case SELECT: + p.unscan() + return p.parseSelectStatement(false, nil) + default: return nil, p.errorExpected(p.pos, p.tok, "expression") } @@ -2577,6 +2837,29 @@ func (p *Parser) parseParenExpr() (_ *ParenExpr, err error) { return &expr, nil } +func (p *Parser) parseSetLiteralExpr() (_ *SetLiteralExpr, err error) { + var expr SetLiteralExpr + expr.Lbracket, _, _ = p.scan() + + for p.peek() != RB { + x, err := p.ParseExpr() + if err != nil { + return &expr, err + } + expr.Members = append(expr.Members, x) + + if p.peek() == RB { + break + } else if p.peek() != COMMA { + return &expr, p.errorExpected(p.pos, p.tok, "comma or right bracket") + } + p.scan() + } + + expr.Rbracket, _, _ = p.scan() + return &expr, nil +} + func (p *Parser) parseCastExpr() (_ *CastExpr, err error) { assert(p.peek() == CAST) @@ -2698,66 +2981,12 @@ func (p *Parser) parseExists() (_ *Exists, err error) { return &expr, nil } -func (p *Parser) parseRaise() (_ *Raise, err error) { - assert(p.peek() == RAISE) - - var expr Raise - expr.Raise, _, _ = p.scan() - - if p.peek() != LP { - return &expr, p.errorExpected(p.pos, p.tok, "left paren") - } - expr.Lparen, _, _ = p.scan() - - // Parse either IGNORE, ROLLBACK, ABORT, or FAIL. - // ROLLBACK also has an error message. - if p.peek() == IGNORE { - expr.Ignore, _, _ = p.scan() - } else { - switch p.peek() { - case ROLLBACK: - expr.Rollback, _, _ = p.scan() - case ABORT: - expr.Abort, _, _ = p.scan() - case FAIL: - expr.Fail, _, _ = p.scan() - default: - return &expr, p.errorExpected(p.pos, p.tok, "IGNORE, ROLLBACK, ABORT, or FAIL") - } - - if p.peek() != COMMA { - return &expr, p.errorExpected(p.pos, p.tok, "comma") - } - expr.Comma, _, _ = p.scan() - - if p.peek() != STRING { - return &expr, p.errorExpected(p.pos, p.tok, "error message") - } - pos, _, lit := p.scan() - expr.Error = &StringLit{ValuePos: pos, Value: lit} - } - - if p.peek() != RP { - return &expr, p.errorExpected(p.pos, p.tok, "right paren") - } - expr.Rparen, _, _ = p.scan() - - return &expr, nil -} - -func (p *Parser) parseSignedNumber(desc string) (*NumberLit, error) { +func (p *Parser) parseIntegerLiteral(desc string) (*IntegerLit, error) { pos, tok, lit := p.scan() - // Prepend "+" or "-" to the next number value. - if tok == PLUS || tok == MINUS { - prefix := lit - _, tok, lit = p.scan() - lit = prefix + lit - } - switch tok { - case FLOAT, INTEGER: - return &NumberLit{ValuePos: pos, Value: lit}, nil + case INTEGER: + return &IntegerLit{ValuePos: pos, Value: lit}, nil default: return nil, p.errorExpected(p.pos, p.tok, desc) } @@ -2781,14 +3010,14 @@ func (p *Parser) parseAlterTableStatement() (_ *AlterTableStatement, err error) case RENAME: stmt.Rename, _, _ = p.scan() - // Parse "RENAME TO new-table-name". + /*// Parse "RENAME TO new-table-name". if p.peek() == TO { stmt.RenameTo, _, _ = p.scan() if stmt.NewName, err = p.parseIdent("new table name"); err != nil { return &stmt, err } return &stmt, nil - } + }*/ // Otherwise parse "RENAME [COLUMN] column-name TO new-column-name". if p.peek() == COLUMN { @@ -2796,7 +3025,7 @@ func (p *Parser) parseAlterTableStatement() (_ *AlterTableStatement, err error) } else if !isIdentToken(p.peek()) { return &stmt, p.errorExpected(p.pos, p.tok, "COLUMN keyword or column name") } - if stmt.ColumnName, err = p.parseIdent("column name"); err != nil { + if stmt.OldColumnName, err = p.parseIdent("column name"); err != nil { return &stmt, err } if p.peek() != TO { @@ -2819,12 +3048,25 @@ func (p *Parser) parseAlterTableStatement() (_ *AlterTableStatement, err error) return &stmt, err } return &stmt, nil + + case DROP: + stmt.Drop, _, _ = p.scan() + if p.peek() == COLUMN { + stmt.DropColumn, _, _ = p.scan() + } else if !isIdentToken(p.peek()) { + return &stmt, p.errorExpected(p.pos, p.tok, "COLUMN keyword or column name") + } + if stmt.DropColumnName, err = p.parseIdent("column name"); err != nil { + return &stmt, err + } + return &stmt, nil + default: - return &stmt, p.errorExpected(p.pos, p.tok, "ADD or RENAME") + return &stmt, p.errorExpected(p.pos, p.tok, "ADD, DROP or RENAME") } } -func (p *Parser) parseAnalyzeStatement() (_ *AnalyzeStatement, err error) { +/*func (p *Parser) parseAnalyzeStatement() (_ *AnalyzeStatement, err error) { assert(p.peek() == ANALYZE) var stmt AnalyzeStatement @@ -2833,7 +3075,7 @@ func (p *Parser) parseAnalyzeStatement() (_ *AnalyzeStatement, err error) { return &stmt, err } return &stmt, nil -} +}*/ func (p *Parser) scan() (Pos, Token, string) { if p.full { @@ -2850,7 +3092,8 @@ func (p *Parser) scanBinaryOp() (Pos, Token, error) { pos, tok, _ := p.scan() switch tok { case IS: - if p.peek() == NOT { + switch p.peek() { + case NOT: p.scan() return pos, ISNOT, nil } @@ -2922,14 +3165,24 @@ func (e Error) Error() string { return e.Msg } +// isTableOptionStartToken returns true if tok is the initial token of a table option. +func isTableOptionStartToken(tok Token) bool { + switch tok { + case KEYPARTITIONS, SHARDWIDTH: + return true + default: + return false + } +} + // isConstraintStartToken returns true if tok is the initial token of a constraint. func isConstraintStartToken(tok Token, isTable bool) bool { switch tok { - case CONSTRAINT, PRIMARY, UNIQUE, CHECK: - return true // table & column - case FOREIGN: - return isTable // table only - case NOT, DEFAULT, REFERENCES: + //case CONSTRAINT, PRIMARY, UNIQUE, CHECK: + // return true // table & column + //case FOREIGN: + // return isTable // table only + case MIN, MAX, TIMEUNIT, TIMEQUANTUM, CACHETYPE: return !isTable // column only default: return false @@ -2940,7 +3193,7 @@ func isConstraintStartToken(tok Token, isTable bool) bool { func isLiteralToken(tok Token) bool { switch tok { case FLOAT, INTEGER, STRING, BLOB, TRUE, FALSE, NULL, - CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP: + CURRENT_DATE, CURRENT_TIMESTAMP: return true default: return false diff --git a/sql2/parser_test.go b/sql3/parser/parser_test.go similarity index 62% rename from sql2/parser_test.go rename to sql3/parser/parser_test.go index a893c016c..c96c9676a 100644 --- a/sql2/parser_test.go +++ b/sql3/parser/parser_test.go @@ -1,30 +1,486 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2_test +// Copyright 2021 Molecula Corp. All rights reserved. +package parser_test import ( "strings" "testing" "github.com/go-test/deep" - sql "github.com/featurebasedb/featurebase/v3/sql2" + "github.com/molecula/featurebase/v3/sql3/parser" ) +func TestParser_ParseNonNullColumnConstraints(t *testing.T) { + t.Run("NotNull", func(t *testing.T) { + t.Run("ErrNoKey", func(t *testing.T) { + AssertParseStatementError(t, `CREATE TABLE tbl (col1 STRING`, `1:29: expected column name, or right paren, found 'EOF'`) + }) + t.Run("Simple", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 STRING)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "STRING", NamePos: pos(23)}, + }, + }, + }, + Rparen: pos(29), + }) + }) + }) +} + +func TestParser_ParseMinMaxColumnConstraints(t *testing.T) { + t.Run("Min", func(t *testing.T) { + t.Run("ErrNoKey", func(t *testing.T) { + AssertParseStatementError(t, `CREATE TABLE tbl (col1 INT MIN`, `1:30: expected expression, found 'EOF'`) + }) + t.Run("Simple", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 INT MIN 0)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "INT", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.MinConstraint{ + Min: pos(27), + Expr: &parser.IntegerLit{ + ValuePos: pos(31), + Value: "0", + }, + }, + }, + }, + }, + Rparen: pos(32), + }) + }) + t.Run("SimpleBoth", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 INT MIN 0 MAX 1024)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "INT", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.MinConstraint{ + Min: pos(27), + Expr: &parser.IntegerLit{ + ValuePos: pos(31), + Value: "0", + }, + }, + &parser.MaxConstraint{ + Max: pos(33), + Expr: &parser.IntegerLit{ + ValuePos: pos(37), + Value: "1024", + }, + }, + }, + }, + }, + Rparen: pos(41), + }) + }) + t.Run("NegativeIntegers", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 INT MIN -400 MAX -100)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "INT", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.MinConstraint{ + Min: pos(27), + Expr: &parser.UnaryExpr{ + OpPos: pos(31), + Op: parser.MINUS, + X: &parser.IntegerLit{ + ValuePos: pos(32), + Value: `400`, + }, + }, + }, + &parser.MaxConstraint{ + Max: pos(36), + Expr: &parser.UnaryExpr{ + OpPos: pos(40), + Op: parser.MINUS, + X: &parser.IntegerLit{ + ValuePos: pos(41), + Value: `100`, + }, + }, + }, + }, + }, + }, + Rparen: pos(44), + }) + }) + t.Run("PositiveDecimals", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 DECIMAL MIN 2.34 MAX 12.87)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "DECIMAL", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.MinConstraint{ + Min: pos(31), + Expr: &parser.FloatLit{ + ValuePos: pos(35), + Value: "2.34", + }, + }, + &parser.MaxConstraint{ + Max: pos(40), + Expr: &parser.FloatLit{ + ValuePos: pos(44), + Value: "12.87", + }, + }, + }, + }, + }, + Rparen: pos(49), + }) + }) + t.Run("NegativeDecimals", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 DECIMAL MIN -12.34 MAX -2.87)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "DECIMAL", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.MinConstraint{ + Min: pos(31), + Expr: &parser.UnaryExpr{ + OpPos: pos(35), + Op: parser.MINUS, + X: &parser.FloatLit{ + ValuePos: pos(36), + Value: `12.34`, + }, + }, + }, + &parser.MaxConstraint{ + Max: pos(42), + Expr: &parser.UnaryExpr{ + OpPos: pos(46), + Op: parser.MINUS, + X: &parser.FloatLit{ + ValuePos: pos(47), + Value: `2.87`, + }, + }, + }, + }, + }, + }, + Rparen: pos(51), + }) + }) + }) +} + +func TestParser_ParseTimeUnitConstraints(t *testing.T) { + t.Run("TimeUnit", func(t *testing.T) { + t.Run("ErrNoKey", func(t *testing.T) { + AssertParseStatementError(t, `CREATE TABLE tbl (col1 STRING TIMEUNIT`, `1:38: expected literal, found 'EOF'`) + }) + t.Run("Simple", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 STRING TIMEUNIT 's')`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "STRING", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.TimeUnitConstraint{ + TimeUnit: pos(30), + Expr: &parser.StringLit{ + ValuePos: pos(39), + Value: "s", + }, + }, + }, + }, + }, + Rparen: pos(42), + }) + }) + }) +} + +func TestParser_ParseTimeQuantumConstraints(t *testing.T) { + t.Run("TimeQuantum", func(t *testing.T) { + t.Run("ErrNoKey", func(t *testing.T) { + AssertParseStatementError(t, `CREATE TABLE tbl (col1 STRING TIMEQUANTUM`, `1:41: expected literal, found 'EOF'`) + }) + t.Run("Simple", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 STRING TIMEQUANTUM 'YMD')`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "STRING", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.TimeQuantumConstraint{ + TimeQuantum: pos(30), + Expr: &parser.StringLit{ + ValuePos: pos(42), + Value: "YMD", + }, + }, + }, + }, + }, + Rparen: pos(47), + }) + }) + }) +} + +func TestParser_ParseCacheTypeConstraints(t *testing.T) { + t.Run("CacheType", func(t *testing.T) { + t.Run("ErrNoKey", func(t *testing.T) { + AssertParseStatementError(t, `CREATE TABLE tbl (col1 INT CACHETYPE`, `1:36: expected RANKED or LRU, found 'EOF'`) + }) + t.Run("Simple", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 INT CACHETYPE RANKED SIZE 1024)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "INT", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.CacheTypeConstraint{ + CacheType: pos(27), + CacheTypeValue: "ranked", + Size: pos(44), + SizeExpr: &parser.IntegerLit{ + ValuePos: pos(49), + Value: "1024", + }, + }, + }, + }, + }, + Rparen: pos(53), + }) + }) + t.Run("Simple2", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 INT CACHETYPE LRU SIZE 1024)`, &parser.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &parser.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*parser.ColumnDefinition{ + { + Name: &parser.Ident{Name: "col1", NamePos: pos(18)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "INT", NamePos: pos(23)}, + }, + Constraints: []parser.Constraint{ + &parser.CacheTypeConstraint{ + CacheType: pos(27), + CacheTypeValue: "lru", + Size: pos(41), + SizeExpr: &parser.IntegerLit{ + ValuePos: pos(46), + Value: "1024", + }, + }, + }, + }, + }, + Rparen: pos(50), + }) + }) + }) +} + +func TestParser_ParseAlterStatement(t *testing.T) { + + t.Run("AlterTable", func(t *testing.T) { + /*AssertParseStatement(t, `ALTER TABLE tbl RENAME TO new_tbl`, &sql.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Rename: pos(16), + RenameTo: pos(23), + NewName: &sql.Ident{NamePos: pos(26), Name: "new_tbl"}, + })*/ + AssertParseStatement(t, `ALTER TABLE tbl RENAME COLUMN col TO new_col`, &parser.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, + Rename: pos(16), + RenameColumn: pos(23), + OldColumnName: &parser.Ident{NamePos: pos(30), Name: "col"}, + To: pos(34), + NewColumnName: &parser.Ident{NamePos: pos(37), Name: "new_col"}, + }) + AssertParseStatement(t, `ALTER TABLE tbl RENAME col TO new_col`, &parser.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, + Rename: pos(16), + OldColumnName: &parser.Ident{NamePos: pos(23), Name: "col"}, + To: pos(27), + NewColumnName: &parser.Ident{NamePos: pos(30), Name: "new_col"}, + }) + /*AssertParseStatement(t, `ALTER TABLE tbl ADD COLUMN col TEXT PRIMARY KEY`, &sql.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Add: pos(16), + AddColumn: pos(20), + ColumnDef: &sql.ColumnDefinition{ + Name: &sql.Ident{Name: "col", NamePos: pos(27)}, + Type: &sql.Type{ + Name: &sql.Ident{Name: "TEXT", NamePos: pos(31)}, + }, + Constraints: []sql.Constraint{ + &sql.PrimaryKeyConstraint{ + Primary: pos(36), + Key: pos(44), + }, + }, + }, + })*/ + AssertParseStatement(t, `ALTER TABLE tbl ADD col TEXT`, &parser.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, + Add: pos(16), + ColumnDef: &parser.ColumnDefinition{ + Name: &parser.Ident{Name: "col", NamePos: pos(20)}, + Type: &parser.Type{ + Name: &parser.Ident{Name: "TEXT", NamePos: pos(24)}, + }, + }, + }) + + AssertParseStatement(t, `ALTER TABLE tbl DROP col`, &parser.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, + Drop: pos(16), + DropColumnName: &parser.Ident{NamePos: pos(21), Name: "col"}, + }) + + AssertParseStatement(t, `ALTER TABLE tbl DROP COLUMN col`, &parser.AlterTableStatement{ + Alter: pos(0), + Table: pos(6), + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, + Drop: pos(16), + DropColumn: pos(21), + DropColumnName: &parser.Ident{NamePos: pos(28), Name: "col"}, + }) + + AssertParseStatementError(t, `ALTER`, `1:5: expected TABLE, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE`, `1:11: expected table name, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl`, `1:15: expected ADD, DROP or RENAME, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl RENAME`, `1:22: expected COLUMN keyword or column name, found 'EOF'`) + //AssertParseStatementError(t, `ALTER TABLE tbl RENAME TO`, `1:25: expected new table name, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl RENAME COLUMN`, `1:29: expected column name, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl RENAME COLUMN col`, `1:33: expected TO, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl RENAME COLUMN col TO`, `1:36: expected new column name, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl ADD`, `1:19: expected COLUMN keyword or column name, found 'EOF'`) + AssertParseStatementError(t, `ALTER TABLE tbl ADD COLUMN`, `1:26: expected column name, found 'EOF'`) + }) +} + func TestParser_ParseStatement(t *testing.T) { t.Run("ErrNoStatement", func(t *testing.T) { AssertParseStatementError(t, `123`, `1:1: expected statement, found 123`) }) + t.Run("ShowTables", func(t *testing.T) { + AssertParseStatement(t, `SHOW TABLES`, &parser.ShowTablesStatement{ + Show: pos(0), + Tables: pos(5), + }) + AssertParseStatementError(t, `SHOW`, `1:4: expected TABLES, COLUMNS, found 'EOF'`) + AssertParseStatementError(t, `SHOW BLAH`, `1:6: expected TABLES, COLUMNS, found BLAH`) + }) + + t.Run("ShowColumns", func(t *testing.T) { + AssertParseStatement(t, `SHOW COLUMNS FROM FOO`, &parser.ShowColumnsStatement{ + Show: pos(0), + Columns: pos(5), + From: pos(13), + TableName: &parser.Ident{ + Name: "FOO", + NamePos: pos(18), + }, + }) + AssertParseStatementError(t, `SHOW`, `1:4: expected TABLES, COLUMNS, found 'EOF'`) + AssertParseStatementError(t, `SHOW COLUMNS`, `1:12: expected FROM, found 'EOF'`) + AssertParseStatementError(t, `SHOW COLUMNS FOO`, `1:14: expected FROM, found FOO`) + AssertParseStatementError(t, `SHOW COLUMNS FROM`, `1:17: expected table name, found 'EOF'`) + AssertParseStatementError(t, `SHOW COLUMNS FROM 12`, `1:19: expected table name, found 12`) + }) + t.Run("Explain", func(t *testing.T) { - t.Run("", func(t *testing.T) { + /*t.Run("", func(t *testing.T) { AssertParseStatement(t, `EXPLAIN BEGIN`, &sql.ExplainStatement{ Explain: pos(0), Stmt: &sql.BeginStatement{ Begin: pos(8), }, }) - }) - t.Run("QueryPlan", func(t *testing.T) { + })*/ + /*t.Run("QueryPlan", func(t *testing.T) { AssertParseStatement(t, `EXPLAIN QUERY PLAN BEGIN`, &sql.ExplainStatement{ Explain: pos(0), Query: pos(8), @@ -33,16 +489,16 @@ func TestParser_ParseStatement(t *testing.T) { Begin: pos(19), }, }) - }) - t.Run("ErrNoPlan", func(t *testing.T) { + })*/ + /*t.Run("ErrNoPlan", func(t *testing.T) { AssertParseStatementError(t, `EXPLAIN QUERY`, `1:13: expected PLAN, found 'EOF'`) - }) - t.Run("ErrStmt", func(t *testing.T) { - AssertParseStatementError(t, `EXPLAIN CREATE`, `1:9: expected TABLE, VIEW, INDEX, TRIGGER`) - }) + })*/ + /* t.Run("ErrStmt", func(t *testing.T) { + AssertParseStatementError(t, `EXPLAIN CREATE`, `1:9: expected TABLE, VIEW, INDEX, TRIGGER`) + })*/ }) - t.Run("Begin", func(t *testing.T) { + /*t.Run("Begin", func(t *testing.T) { t.Run("", func(t *testing.T) { AssertParseStatement(t, `BEGIN`, &sql.BeginStatement{ Begin: pos(0), @@ -76,9 +532,9 @@ func TestParser_ParseStatement(t *testing.T) { t.Run("ErrOverrun", func(t *testing.T) { AssertParseStatementError(t, `BEGIN COMMIT`, `1:7: expected semicolon or EOF, found 'COMMIT'`) }) - }) + })*/ - t.Run("Commit", func(t *testing.T) { + /*t.Run("Commit", func(t *testing.T) { t.Run("", func(t *testing.T) { AssertParseStatement(t, `COMMIT`, &sql.CommitStatement{ Commit: pos(0), @@ -90,9 +546,9 @@ func TestParser_ParseStatement(t *testing.T) { Transaction: pos(7), }) }) - }) + })*/ - t.Run("End", func(t *testing.T) { + /*t.Run("End", func(t *testing.T) { t.Run("", func(t *testing.T) { AssertParseStatement(t, `END`, &sql.CommitStatement{ End: pos(0), @@ -104,9 +560,9 @@ func TestParser_ParseStatement(t *testing.T) { Transaction: pos(4), }) }) - }) + })*/ - t.Run("Rollback", func(t *testing.T) { + /*t.Run("Rollback", func(t *testing.T) { t.Run("", func(t *testing.T) { AssertParseStatement(t, `ROLLBACK`, &sql.RollbackStatement{ Rollback: pos(0), @@ -144,9 +600,9 @@ func TestParser_ParseStatement(t *testing.T) { t.Run("ErrSavepointName", func(t *testing.T) { AssertParseStatementError(t, `ROLLBACK TO SAVEPOINT 123`, `1:23: expected savepoint name, found 123`) }) - }) + })*/ - t.Run("Savepoint", func(t *testing.T) { + /*t.Run("Savepoint", func(t *testing.T) { t.Run("Ident", func(t *testing.T) { AssertParseStatement(t, `SAVEPOINT svpt`, &sql.SavepointStatement{ Savepoint: pos(0), @@ -169,9 +625,9 @@ func TestParser_ParseStatement(t *testing.T) { t.Run("ErrSavepointName", func(t *testing.T) { AssertParseStatementError(t, `SAVEPOINT 123`, `1:11: expected savepoint name, found 123`) }) - }) + })*/ - t.Run("Release", func(t *testing.T) { + /*t.Run("Release", func(t *testing.T) { t.Run("Ident", func(t *testing.T) { AssertParseStatement(t, `RELEASE svpt`, &sql.ReleaseStatement{ Release: pos(0), @@ -204,62 +660,61 @@ func TestParser_ParseStatement(t *testing.T) { t.Run("ErrSavepointName", func(t *testing.T) { AssertParseStatementError(t, `RELEASE 123`, `1:9: expected savepoint name, found 123`) }) - }) + })*/ t.Run("CreateTable", func(t *testing.T) { - AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, col2 DECIMAL(10,5))`, &sql.CreateTableStatement{ + AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, col2 DECIMAL(2))`, &parser.CreateTableStatement{ Create: pos(0), Table: pos(7), - Name: &sql.Ident{ + Name: &parser.Ident{ Name: "tbl", NamePos: pos(13), }, Lparen: pos(17), - Columns: []*sql.ColumnDefinition{ + Columns: []*parser.ColumnDefinition{ { - Name: &sql.Ident{NamePos: pos(18), Name: "col1"}, - Type: &sql.Type{ - Name: &sql.Ident{NamePos: pos(23), Name: "TEXT"}, + Name: &parser.Ident{NamePos: pos(18), Name: "col1"}, + Type: &parser.Type{ + Name: &parser.Ident{NamePos: pos(23), Name: "TEXT"}, }, }, { - Name: &sql.Ident{NamePos: pos(29), Name: "col2"}, - Type: &sql.Type{ - Name: &sql.Ident{NamePos: pos(34), Name: "DECIMAL"}, - Lparen: pos(41), - Precision: &sql.NumberLit{ValuePos: pos(42), Value: "10"}, - Scale: &sql.NumberLit{ValuePos: pos(45), Value: "5"}, - Rparen: pos(46), + Name: &parser.Ident{NamePos: pos(29), Name: "col2"}, + Type: &parser.Type{ + Name: &parser.Ident{NamePos: pos(34), Name: "DECIMAL"}, + Lparen: pos(41), + Scale: &parser.IntegerLit{ValuePos: pos(42), Value: "2"}, + Rparen: pos(43), }, }, }, - Rparen: pos(47), + Rparen: pos(44), }) AssertParseStatementError(t, `CREATE TABLE`, `1:12: expected table name, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl `, `1:17: expected AS or left paren, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (`, `1:18: expected column name, CONSTRAINT, or right paren, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT`, `1:27: expected column name, CONSTRAINT, or right paren, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl `, `1:17: expected left paren, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (`, `1:18: expected column name, or right paren, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT`, `1:27: expected column name, or right paren, found 'EOF'`) - AssertParseStatement(t, `CREATE TABLE IF NOT EXISTS tbl (col1 TEXT)`, &sql.CreateTableStatement{ + AssertParseStatement(t, `CREATE TABLE IF NOT EXISTS tbl (col1 TEXT)`, &parser.CreateTableStatement{ Create: pos(0), Table: pos(7), If: pos(13), IfNot: pos(16), IfNotExists: pos(20), - Name: &sql.Ident{ + Name: &parser.Ident{ Name: "tbl", NamePos: pos(27), }, Lparen: pos(31), - Columns: []*sql.ColumnDefinition{ + Columns: []*parser.ColumnDefinition{ { - Name: &sql.Ident{ + Name: &parser.Ident{ NamePos: pos(32), Name: "col1", }, - Type: &sql.Type{ - Name: &sql.Ident{ + Type: &parser.Type{ + Name: &parser.Ident{ NamePos: pos(37), Name: "TEXT", }, @@ -271,13 +726,13 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE TABLE IF`, `1:15: expected NOT, found 'EOF'`) AssertParseStatementError(t, `CREATE TABLE IF NOT`, `1:19: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1`, `1:22: expected type name, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(`, `1:31: expected precision, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(-12,`, `1:35: expected scale, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(1,2`, `1:34: expected right paren, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(`, `1:31: expected scale, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(12,`, `1:34: expected right paren, found ','`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(1,2`, `1:33: expected right paren, found ','`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 DECIMAL(1`, `1:32: expected right paren, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT CONSTRAINT`, `1:38: expected constraint name, found 'EOF'`) + /*AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT CONSTRAINT`, `1:38: expected constraint name, found 'EOF'`)*/ - AssertParseStatement(t, `CREATE TABLE tbl AS SELECT foo`, &sql.CreateTableStatement{ + /*AssertParseStatement(t, `CREATE TABLE tbl AS SELECT foo`, &sql.CreateTableStatement{ Create: pos(0), Table: pos(7), Name: &sql.Ident{ @@ -291,8 +746,8 @@ func TestParser_ParseStatement(t *testing.T) { {Expr: &sql.Ident{NamePos: pos(27), Name: "foo"}}, }, }, - }) - AssertParseStatement(t, `CREATE TABLE tbl AS WITH cte (x) AS (SELECT y) SELECT foo`, &sql.CreateTableStatement{ + })*/ + /*AssertParseStatement(t, `CREATE TABLE tbl AS WITH cte (x) AS (SELECT y) SELECT foo`, &sql.CreateTableStatement{ Create: pos(0), Table: pos(7), Name: &sql.Ident{ @@ -328,12 +783,12 @@ func TestParser_ParseStatement(t *testing.T) { {Expr: &sql.Ident{NamePos: pos(54), Name: "foo"}}, }, }, - }) - AssertParseStatementError(t, `CREATE TABLE tbl AS`, `1:19: expected SELECT or VALUES, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl AS WITH`, `1:24: expected table name, found 'EOF'`) + })*/ + /*AssertParseStatementError(t, `CREATE TABLE tbl AS`, `1:19: expected SELECT or VALUES, found 'EOF'`)*/ + /*AssertParseStatementError(t, `CREATE TABLE tbl AS WITH`, `1:24: expected table name, found 'EOF'`)*/ t.Run("ColumnConstraint", func(t *testing.T) { - t.Run("PrimaryKey", func(t *testing.T) { + /*t.Run("PrimaryKey", func(t *testing.T) { t.Run("Simple", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT PRIMARY KEY)`, &sql.CreateTableStatement{ Create: pos(0), @@ -372,40 +827,9 @@ func TestParser_ParseStatement(t *testing.T) { t.Run("ErrNoKey", func(t *testing.T) { AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT PRIMARY`, `1:35: expected KEY, found 'EOF'`) }) - }) + })*/ - t.Run("NotNull", func(t *testing.T) { - t.Run("ErrNoKey", func(t *testing.T) { - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT NOT`, `1:31: expected NULL, found 'EOF'`) - }) - t.Run("Simple", func(t *testing.T) { - AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT CONSTRAINT con1 NOT NULL)`, &sql.CreateTableStatement{ - Create: pos(0), - Table: pos(7), - Name: &sql.Ident{Name: "tbl", NamePos: pos(13)}, - Lparen: pos(17), - Columns: []*sql.ColumnDefinition{ - { - Name: &sql.Ident{Name: "col1", NamePos: pos(18)}, - Type: &sql.Type{ - Name: &sql.Ident{Name: "TEXT", NamePos: pos(23)}, - }, - Constraints: []sql.Constraint{ - &sql.NotNullConstraint{ - Constraint: pos(28), - Name: &sql.Ident{Name: "con1", NamePos: pos(39)}, - Not: pos(44), - Null: pos(48), - }, - }, - }, - }, - Rparen: pos(52), - }) - }) - }) - - t.Run("Unique", func(t *testing.T) { + /*t.Run("Unique", func(t *testing.T) { t.Run("Simple", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT CONSTRAINT con1 UNIQUE)`, &sql.CreateTableStatement{ Create: pos(0), @@ -430,8 +854,8 @@ func TestParser_ParseStatement(t *testing.T) { Rparen: pos(50), }) }) - }) - t.Run("Check", func(t *testing.T) { + })*/ + /*t.Run("Check", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT CHECK (col1 > 1))`, &sql.CreateTableStatement{ Create: pos(0), Table: pos(7), @@ -459,8 +883,8 @@ func TestParser_ParseStatement(t *testing.T) { }, Rparen: pos(44), }) - }) - t.Run("Default", func(t *testing.T) { + })*/ + /*t.Run("Default", func(t *testing.T) { t.Run("Expr", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT DEFAULT (1))`, &sql.CreateTableStatement{ Create: pos(0), @@ -537,8 +961,8 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT DEFAULT `, `1:36: expected literal value or left paren, found 'EOF'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT DEFAULT (TABLE`, `1:38: expected expression, found 'TABLE'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT DEFAULT (true`, `1:41: expected right paren, found 'EOF'`) - }) - t.Run("ForeignKey", func(t *testing.T) { + })*/ + /*t.Run("ForeignKey", func(t *testing.T) { t.Run("Simple", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT REFERENCES foo (col2))`, &sql.CreateTableStatement{ Create: pos(0), @@ -766,46 +1190,50 @@ func TestParser_ParseStatement(t *testing.T) { t.Fatal(diff) } }) - }) + })*/ }) t.Run("TableConstraint", func(t *testing.T) { - t.Run("PrimaryKey", func(t *testing.T) { - AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (col1, col2))`, &sql.CreateTableStatement{ - Create: pos(0), - Table: pos(7), - Name: &sql.Ident{Name: "tbl", NamePos: pos(13)}, - Lparen: pos(17), - Columns: []*sql.ColumnDefinition{ - { - Name: &sql.Ident{Name: "col1", NamePos: pos(18)}, - Type: &sql.Type{ - Name: &sql.Ident{Name: "TEXT", NamePos: pos(23)}, - }, - }, - }, - Constraints: []sql.Constraint{ - &sql.PrimaryKeyConstraint{ - Primary: pos(29), - Key: pos(37), - Lparen: pos(41), - Columns: []*sql.Ident{ - {Name: "col1", NamePos: pos(42)}, - {Name: "col2", NamePos: pos(48)}, - }, - Rparen: pos(52), - }, - }, - Rparen: pos(53), - }) + /* - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY`, `1:36: expected KEY, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY`, `1:40: expected left paren, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (col1)`, `1:47: expected right paren, found 'EOF'`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (1`, `1:43: expected column name, found 1`) - AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (foo x`, `1:47: expected comma or right paren, found x`) - }) - t.Run("Unique", func(t *testing.T) { + !!Not supporting PRIMARY KEY, UNIQUE, CHECK or REFERENCES table constraints!! + + t.Run("PrimaryKey", func(t *testing.T) { + AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (col1, col2))`, &sql.CreateTableStatement{ + Create: pos(0), + Table: pos(7), + Name: &sql.Ident{Name: "tbl", NamePos: pos(13)}, + Lparen: pos(17), + Columns: []*sql.ColumnDefinition{ + { + Name: &sql.Ident{Name: "col1", NamePos: pos(18)}, + Type: &sql.Type{ + Name: &sql.Ident{Name: "TEXT", NamePos: pos(23)}, + }, + }, + }, + Constraints: []sql.Constraint{ + &sql.PrimaryKeyConstraint{ + Primary: pos(29), + Key: pos(37), + Lparen: pos(41), + Columns: []*sql.Ident{ + {Name: "col1", NamePos: pos(42)}, + {Name: "col2", NamePos: pos(48)}, + }, + Rparen: pos(52), + }, + }, + Rparen: pos(53), + }) + + AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY`, `1:36: expected KEY, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY`, `1:40: expected left paren, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (col1)`, `1:47: expected right paren, found 'EOF'`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (1`, `1:43: expected column name, found 1`) + AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, PRIMARY KEY (foo x`, `1:47: expected comma or right paren, found x`) + })*/ + /*t.Run("Unique", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, CONSTRAINT con1 UNIQUE (col1, col2))`, &sql.CreateTableStatement{ Create: pos(0), Table: pos(7), @@ -837,8 +1265,8 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, UNIQUE`, `1:35: expected left paren, found 'EOF'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, UNIQUE (1`, `1:38: expected column name, found 1`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, UNIQUE (x y`, `1:40: expected comma or right paren, found y`) - }) - t.Run("Check", func(t *testing.T) { + })*/ + /*(t.Run("Check", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, CHECK(foo = bar))`, &sql.CreateTableStatement{ Create: pos(0), Table: pos(7), @@ -870,8 +1298,8 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, CHECK`, `1:34: expected left paren, found 'EOF'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, CHECK (TABLE`, `1:37: expected expression, found 'TABLE'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, CHECK (true`, `1:40: expected right paren, found 'EOF'`) - }) - t.Run("ForeignKey", func(t *testing.T) { + })*/ + /*t.Run("ForeignKey", func(t *testing.T) { AssertParseStatement(t, `CREATE TABLE tbl (col1 TEXT, FOREIGN KEY (col1, col2) REFERENCES tbl2 (x, y))`, &sql.CreateTableStatement{ Create: pos(0), Table: pos(7), @@ -922,29 +1350,29 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, FOREIGN KEY (x) REFERENCES tbl (x) ON UPDATE NO`, `1:76: expected ACTION, found 'EOF'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, FOREIGN KEY (x) REFERENCES tbl (x) ON UPDATE TABLE`, `1:75: expected SET NULL, SET DEFAULT, CASCADE, RESTRICT, or NO ACTION, found 'TABLE'`) AssertParseStatementError(t, `CREATE TABLE tbl (col1 TEXT, FOREIGN KEY (x) REFERENCES tbl (x) ON UPDATE CASCADE NOT`, `1:85: expected DEFERRABLE, found 'EOF'`) - }) + })*/ }) }) t.Run("DropTable", func(t *testing.T) { - AssertParseStatement(t, `DROP TABLE vw`, &sql.DropTableStatement{ + AssertParseStatement(t, `DROP TABLE vw`, &parser.DropTableStatement{ Drop: pos(0), Table: pos(5), - Name: &sql.Ident{NamePos: pos(11), Name: "vw"}, + Name: &parser.Ident{NamePos: pos(11), Name: "vw"}, }) - AssertParseStatement(t, `DROP TABLE IF EXISTS vw`, &sql.DropTableStatement{ + AssertParseStatement(t, `DROP TABLE IF EXISTS vw`, &parser.DropTableStatement{ Drop: pos(0), Table: pos(5), If: pos(11), IfExists: pos(14), - Name: &sql.Ident{NamePos: pos(21), Name: "vw"}, + Name: &parser.Ident{NamePos: pos(21), Name: "vw"}, }) AssertParseStatementError(t, `DROP TABLE`, `1:10: expected table name, found 'EOF'`) AssertParseStatementError(t, `DROP TABLE IF`, `1:13: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `DROP TABLE IF EXISTS`, `1:20: expected table name, found 'EOF'`) }) - t.Run("CreateView", func(t *testing.T) { + /*t.Run("CreateView", func(t *testing.T) { AssertParseStatement(t, `CREATE VIEW vw (col1, col2) AS SELECT x, y`, &sql.CreateViewStatement{ Create: pos(0), View: pos(7), @@ -999,9 +1427,9 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE VIEW vw (x`, `1:17: expected comma or right paren, found 'EOF'`) AssertParseStatementError(t, `CREATE VIEW vw AS`, `1:17: expected SELECT or VALUES, found 'EOF'`) AssertParseStatementError(t, `CREATE VIEW vw AS SELECT`, `1:24: expected expression, found 'EOF'`) - }) + })*/ - t.Run("DropView", func(t *testing.T) { + /*t.Run("DropView", func(t *testing.T) { AssertParseStatement(t, `DROP VIEW vw`, &sql.DropViewStatement{ Drop: pos(0), View: pos(5), @@ -1018,9 +1446,9 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `DROP VIEW`, `1:9: expected view name, found 'EOF'`) AssertParseStatementError(t, `DROP VIEW IF`, `1:12: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `DROP VIEW IF EXISTS`, `1:19: expected view name, found 'EOF'`) - }) + })*/ - t.Run("CreateIndex", func(t *testing.T) { + /*t.Run("CreateIndex", func(t *testing.T) { AssertParseStatement(t, `CREATE INDEX idx ON tbl (x ASC, y DESC, z)`, &sql.CreateIndexStatement{ Create: pos(0), Index: pos(7), @@ -1087,9 +1515,9 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE INDEX idx ON tbl (`, `1:25: expected expression, found 'EOF'`) AssertParseStatementError(t, `CREATE INDEX idx ON tbl (x`, `1:26: expected comma or right paren, found 'EOF'`) AssertParseStatementError(t, `CREATE INDEX idx ON tbl (x) WHERE`, `1:33: expected expression, found 'EOF'`) - }) + })*/ - t.Run("DropIndex", func(t *testing.T) { + /*t.Run("DropIndex", func(t *testing.T) { AssertParseStatement(t, `DROP INDEX idx`, &sql.DropIndexStatement{ Drop: pos(0), Index: pos(5), @@ -1105,9 +1533,9 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `DROP INDEX`, `1:10: expected index name, found 'EOF'`) AssertParseStatementError(t, `DROP INDEX IF`, `1:13: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `DROP INDEX IF EXISTS`, `1:20: expected index name, found 'EOF'`) - }) + })*/ - t.Run("CreateTrigger", func(t *testing.T) { + /*t.Run("CreateTrigger", func(t *testing.T) { AssertParseStatement(t, `CREATE TRIGGER trig DELETE ON tbl BEGIN INSERT INTO new DEFAULT VALUES; UPDATE new SET x = 1; END`, &sql.CreateTriggerStatement{ Create: pos(0), Trigger: pos(7), @@ -1265,9 +1693,9 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE TRIGGER trig AFTER INSERT ON tbl BEGIN SELECT`, `1:52: expected expression, found 'EOF'`) AssertParseStatementError(t, `CREATE TRIGGER trig AFTER INSERT ON tbl BEGIN SELECT *`, `1:54: expected semicolon, found 'EOF'`) AssertParseStatementError(t, `CREATE TRIGGER trig AFTER INSERT ON tbl BEGIN SELECT *;`, `1:55: expected statement, found 'EOF'`) - }) + })*/ - t.Run("DropTrigger", func(t *testing.T) { + /*t.Run("DropTrigger", func(t *testing.T) { AssertParseStatement(t, `DROP TRIGGER trig`, &sql.DropTriggerStatement{ Drop: pos(0), Trigger: pos(5), @@ -1283,93 +1711,81 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `DROP TRIGGER`, `1:12: expected trigger name, found 'EOF'`) AssertParseStatementError(t, `DROP TRIGGER IF`, `1:15: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `DROP TRIGGER IF EXISTS`, `1:22: expected trigger name, found 'EOF'`) - }) + })*/ t.Run("Select", func(t *testing.T) { - AssertParseStatement(t, `SELECT * FROM tbl`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM tbl`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "tbl"}, + Source: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "tbl"}, }, }) - AssertParseStatement(t, `SELECT DISTINCT * FROM tbl`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT DISTINCT * FROM tbl`, &parser.SelectStatement{ Select: pos(0), Distinct: pos(7), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(16)}, }, From: pos(18), - Source: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(23), Name: "tbl"}, + Source: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(23), Name: "tbl"}, }, }) - AssertParseStatement(t, `SELECT ALL * FROM tbl`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT foo AS FOO, bar baz, tbl.* FROM tbl`, &parser.SelectStatement{ Select: pos(0), - All: pos(7), - Columns: []*sql.ResultColumn{ - {Star: pos(11)}, - }, - From: pos(13), - Source: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(18), Name: "tbl"}, - }, - }) - - AssertParseStatement(t, `SELECT foo AS FOO, bar baz, tbl.* FROM tbl`, &sql.SelectStatement{ - Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ { - Expr: &sql.Ident{NamePos: pos(7), Name: "foo"}, + Expr: &parser.Ident{NamePos: pos(7), Name: "foo"}, As: pos(11), - Alias: &sql.Ident{NamePos: pos(14), Name: "FOO"}, + Alias: &parser.Ident{NamePos: pos(14), Name: "FOO"}, }, { - Expr: &sql.Ident{NamePos: pos(19), Name: "bar"}, - Alias: &sql.Ident{NamePos: pos(23), Name: "baz"}, + Expr: &parser.Ident{NamePos: pos(19), Name: "bar"}, + Alias: &parser.Ident{NamePos: pos(23), Name: "baz"}, }, { - Expr: &sql.QualifiedRef{ - Table: &sql.Ident{NamePos: pos(28), Name: "tbl"}, + Expr: &parser.QualifiedRef{ + Table: &parser.Ident{NamePos: pos(28), Name: "tbl"}, Dot: pos(31), Star: pos(32), }, }, }, From: pos(34), - Source: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(39), Name: "tbl"}, + Source: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(39), Name: "tbl"}, }, }) - AssertParseStatement(t, `SELECT * FROM tbl tbl2`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM tbl tbl2`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "tbl"}, - Alias: &sql.Ident{NamePos: pos(18), Name: "tbl2"}, + Source: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "tbl"}, + Alias: &parser.Ident{NamePos: pos(18), Name: "tbl2"}, }, }) - AssertParseStatement(t, `SELECT * FROM tbl AS tbl2`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM tbl AS tbl2`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "tbl"}, + Source: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "tbl"}, As: pos(18), - Alias: &sql.Ident{NamePos: pos(21), Name: "tbl2"}, + Alias: &parser.Ident{NamePos: pos(21), Name: "tbl2"}, }, }) - AssertParseStatement(t, `SELECT * FROM tbl INDEXED BY idx`, &sql.SelectStatement{ + /*AssertParseStatement(t, `SELECT * FROM tbl INDEXED BY idx`, &sql.SelectStatement{ Select: pos(0), Columns: []*sql.ResultColumn{ {Star: pos(7)}, @@ -1381,8 +1797,8 @@ func TestParser_ParseStatement(t *testing.T) { IndexedBy: pos(26), Index: &sql.Ident{NamePos: pos(29), Name: "idx"}, }, - }) - AssertParseStatement(t, `SELECT * FROM tbl NOT INDEXED`, &sql.SelectStatement{ + })*/ + /*AssertParseStatement(t, `SELECT * FROM tbl NOT INDEXED`, &sql.SelectStatement{ Select: pos(0), Columns: []*sql.ResultColumn{ {Star: pos(7)}, @@ -1393,114 +1809,114 @@ func TestParser_ParseStatement(t *testing.T) { Not: pos(18), NotIndexed: pos(22), }, - }) + })*/ - AssertParseStatement(t, `SELECT * FROM (SELECT *) AS tbl`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM (SELECT *) AS tbl`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.ParenSource{ + Source: &parser.ParenSource{ Lparen: pos(14), - X: &sql.SelectStatement{ + X: &parser.SelectStatement{ Select: pos(15), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(22)}, }, }, Rparen: pos(23), As: pos(25), - Alias: &sql.Ident{NamePos: pos(28), Name: "tbl"}, + Alias: &parser.Ident{NamePos: pos(28), Name: "tbl"}, }, }) - AssertParseStatement(t, `SELECT * FROM foo, bar`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo, bar`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Comma: pos(17)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(19), Name: "bar"}, + Operator: &parser.JoinOperator{Comma: pos(17)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(19), Name: "bar"}, }, }, }) - AssertParseStatement(t, `SELECT * FROM foo JOIN bar`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo JOIN bar`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Join: pos(18)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(23), Name: "bar"}, + Operator: &parser.JoinOperator{Join: pos(18)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(23), Name: "bar"}, }, }, }) - AssertParseStatement(t, `SELECT * FROM foo NATURAL JOIN bar`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo NATURAL JOIN bar`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Natural: pos(18), Join: pos(26)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(31), Name: "bar"}, + Operator: &parser.JoinOperator{Natural: pos(18), Join: pos(26)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(31), Name: "bar"}, }, }, }) - AssertParseStatement(t, `SELECT * FROM foo INNER JOIN bar ON true`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo INNER JOIN bar ON true`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Inner: pos(18), Join: pos(24)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(29), Name: "bar"}, + Operator: &parser.JoinOperator{Inner: pos(18), Join: pos(24)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(29), Name: "bar"}, }, - Constraint: &sql.OnConstraint{ + Constraint: &parser.OnConstraint{ On: pos(33), - X: &sql.BoolLit{ValuePos: pos(36), Value: true}, + X: &parser.BoolLit{ValuePos: pos(36), Value: true}, }, }, }) - AssertParseStatement(t, `SELECT * FROM foo LEFT JOIN bar USING (x, y)`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo LEFT JOIN bar USING (x, y)`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Left: pos(18), Join: pos(23)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(28), Name: "bar"}, + Operator: &parser.JoinOperator{Left: pos(18), Join: pos(23)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(28), Name: "bar"}, }, - Constraint: &sql.UsingConstraint{ + Constraint: &parser.UsingConstraint{ Using: pos(32), Lparen: pos(38), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(39), Name: "x"}, {NamePos: pos(42), Name: "y"}, }, @@ -1508,70 +1924,70 @@ func TestParser_ParseStatement(t *testing.T) { }, }, }) - AssertParseStatement(t, `SELECT * FROM X INNER JOIN Y ON true INNER JOIN Z ON false`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM X INNER JOIN Y ON true INNER JOIN Z ON false`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "X"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "X"}, }, - Operator: &sql.JoinOperator{Inner: pos(16), Join: pos(22)}, - Y: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(27), Name: "Y"}, + Operator: &parser.JoinOperator{Inner: pos(16), Join: pos(22)}, + Y: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(27), Name: "Y"}, }, - Operator: &sql.JoinOperator{Inner: pos(37), Join: pos(43)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(48), Name: "Z"}, + Operator: &parser.JoinOperator{Inner: pos(37), Join: pos(43)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(48), Name: "Z"}, }, - Constraint: &sql.OnConstraint{ + Constraint: &parser.OnConstraint{ On: pos(50), - X: &sql.BoolLit{ValuePos: pos(53), Value: false}, + X: &parser.BoolLit{ValuePos: pos(53), Value: false}, }, }, - Constraint: &sql.OnConstraint{ + Constraint: &parser.OnConstraint{ On: pos(29), - X: &sql.BoolLit{ValuePos: pos(32), Value: true}, + X: &parser.BoolLit{ValuePos: pos(32), Value: true}, }, }, }) - AssertParseStatement(t, `SELECT * FROM foo LEFT OUTER JOIN bar`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo LEFT OUTER JOIN bar`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Left: pos(18), Outer: pos(23), Join: pos(29)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(34), Name: "bar"}, + Operator: &parser.JoinOperator{Left: pos(18), Outer: pos(23), Join: pos(29)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(34), Name: "bar"}, }, }, }) - AssertParseStatement(t, `SELECT * FROM foo CROSS JOIN bar`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * FROM foo CROSS JOIN bar`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, From: pos(9), - Source: &sql.JoinClause{ - X: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(14), Name: "foo"}, + Source: &parser.JoinClause{ + X: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(14), Name: "foo"}, }, - Operator: &sql.JoinOperator{Cross: pos(18), Join: pos(24)}, - Y: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(29), Name: "bar"}, + Operator: &parser.JoinOperator{Cross: pos(18), Join: pos(24)}, + Y: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(29), Name: "bar"}, }, }, }) - AssertParseStatement(t, `WITH cte (foo, bar) AS (SELECT baz), xxx AS (SELECT yyy) SELECT bat`, &sql.SelectStatement{ + /*AssertParseStatement(t, `WITH cte (foo, bar) AS (SELECT baz), xxx AS (SELECT yyy) SELECT bat`, &sql.SelectStatement{ WithClause: &sql.WithClause{ With: pos(0), CTEs: []*sql.CTE{ @@ -1611,8 +2027,8 @@ func TestParser_ParseStatement(t *testing.T) { Columns: []*sql.ResultColumn{ {Expr: &sql.Ident{NamePos: pos(64), Name: "bat"}}, }, - }) - AssertParseStatement(t, `WITH RECURSIVE cte AS (SELECT foo) SELECT bar`, &sql.SelectStatement{ + })*/ + /*AssertParseStatement(t, `WITH RECURSIVE cte AS (SELECT foo) SELECT bar`, &sql.SelectStatement{ WithClause: &sql.WithClause{ With: pos(0), Recursive: pos(5), @@ -1635,53 +2051,53 @@ func TestParser_ParseStatement(t *testing.T) { Columns: []*sql.ResultColumn{ {Expr: &sql.Ident{NamePos: pos(42), Name: "bar"}}, }, - }) + })*/ - AssertParseStatement(t, `SELECT * WHERE true`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * WHERE true`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{{Star: pos(7)}}, + Columns: []*parser.ResultColumn{{Star: pos(7)}}, Where: pos(9), - WhereExpr: &sql.BoolLit{ValuePos: pos(15), Value: true}, + WhereExpr: &parser.BoolLit{ValuePos: pos(15), Value: true}, }) - AssertParseStatement(t, `SELECT * GROUP BY foo, bar`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * GROUP BY foo, bar`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{{Star: pos(7)}}, + Columns: []*parser.ResultColumn{{Star: pos(7)}}, Group: pos(9), GroupBy: pos(15), - GroupByExprs: []sql.Expr{ - &sql.Ident{NamePos: pos(18), Name: "foo"}, - &sql.Ident{NamePos: pos(23), Name: "bar"}, + GroupByExprs: []parser.Expr{ + &parser.Ident{NamePos: pos(18), Name: "foo"}, + &parser.Ident{NamePos: pos(23), Name: "bar"}, }, }) - AssertParseStatement(t, `SELECT * GROUP BY foo HAVING true`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * GROUP BY foo HAVING true`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{{Star: pos(7)}}, + Columns: []*parser.ResultColumn{{Star: pos(7)}}, Group: pos(9), GroupBy: pos(15), - GroupByExprs: []sql.Expr{ - &sql.Ident{NamePos: pos(18), Name: "foo"}, + GroupByExprs: []parser.Expr{ + &parser.Ident{NamePos: pos(18), Name: "foo"}, }, Having: pos(22), - HavingExpr: &sql.BoolLit{ValuePos: pos(29), Value: true}, + HavingExpr: &parser.BoolLit{ValuePos: pos(29), Value: true}, }) - AssertParseStatement(t, `SELECT * WINDOW win1 AS (), win2 AS ()`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * WINDOW win1 AS (), win2 AS ()`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{{Star: pos(7)}}, + Columns: []*parser.ResultColumn{{Star: pos(7)}}, Window: pos(9), - Windows: []*sql.Window{ + Windows: []*parser.Window{ { - Name: &sql.Ident{NamePos: pos(16), Name: "win1"}, + Name: &parser.Ident{NamePos: pos(16), Name: "win1"}, As: pos(21), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(24), Rparen: pos(25), }, }, { - Name: &sql.Ident{NamePos: pos(28), Name: "win2"}, + Name: &parser.Ident{NamePos: pos(28), Name: "win2"}, As: pos(33), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(36), Rparen: pos(37), }, @@ -1689,107 +2105,79 @@ func TestParser_ParseStatement(t *testing.T) { }, }) - AssertParseStatement(t, `SELECT * ORDER BY foo ASC, bar DESC`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * ORDER BY foo ASC, bar DESC`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, Order: pos(9), OrderBy: pos(15), - OrderingTerms: []*sql.OrderingTerm{ - {X: &sql.Ident{NamePos: pos(18), Name: "foo"}, Asc: pos(22)}, - {X: &sql.Ident{NamePos: pos(27), Name: "bar"}, Desc: pos(31)}, + OrderingTerms: []*parser.OrderingTerm{ + {X: &parser.Ident{NamePos: pos(18), Name: "foo"}, Asc: pos(22)}, + {X: &parser.Ident{NamePos: pos(27), Name: "bar"}, Desc: pos(31)}, }, }) - AssertParseStatement(t, `SELECT * LIMIT 1`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * UNION SELECT * ORDER BY foo`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ - {Star: pos(7)}, - }, - Limit: pos(9), - LimitExpr: &sql.NumberLit{ValuePos: pos(15), Value: "1"}, - }) - AssertParseStatement(t, `SELECT * LIMIT 1 OFFSET 2`, &sql.SelectStatement{ - Select: pos(0), - Columns: []*sql.ResultColumn{ - {Star: pos(7)}, - }, - Limit: pos(9), - LimitExpr: &sql.NumberLit{ValuePos: pos(15), Value: "1"}, - Offset: pos(17), - OffsetExpr: &sql.NumberLit{ValuePos: pos(24), Value: "2"}, - }) - AssertParseStatement(t, `SELECT * LIMIT 1, 2`, &sql.SelectStatement{ - Select: pos(0), - Columns: []*sql.ResultColumn{ - {Star: pos(7)}, - }, - Limit: pos(9), - LimitExpr: &sql.NumberLit{ValuePos: pos(15), Value: "1"}, - OffsetComma: pos(16), - OffsetExpr: &sql.NumberLit{ValuePos: pos(18), Value: "2"}, - }) - AssertParseStatement(t, `SELECT * UNION SELECT * ORDER BY foo`, &sql.SelectStatement{ - Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, Union: pos(9), - Compound: &sql.SelectStatement{ + Compound: &parser.SelectStatement{ Select: pos(15), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(22)}, }, }, Order: pos(24), OrderBy: pos(30), - OrderingTerms: []*sql.OrderingTerm{ - {X: &sql.Ident{NamePos: pos(33), Name: "foo"}}, + OrderingTerms: []*parser.OrderingTerm{ + {X: &parser.Ident{NamePos: pos(33), Name: "foo"}}, }, }) - AssertParseStatement(t, `SELECT * UNION ALL SELECT *`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * UNION ALL SELECT *`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, Union: pos(9), UnionAll: pos(15), - Compound: &sql.SelectStatement{ + Compound: &parser.SelectStatement{ Select: pos(19), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(26)}, }, }, }) - AssertParseStatement(t, `SELECT * INTERSECT SELECT *`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * INTERSECT SELECT *`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, Intersect: pos(9), - Compound: &sql.SelectStatement{ + Compound: &parser.SelectStatement{ Select: pos(19), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(26)}, }, }, }) - AssertParseStatement(t, `SELECT * EXCEPT SELECT *`, &sql.SelectStatement{ + AssertParseStatement(t, `SELECT * EXCEPT SELECT *`, &parser.SelectStatement{ Select: pos(0), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(7)}, }, Except: pos(9), - Compound: &sql.SelectStatement{ + Compound: &parser.SelectStatement{ Select: pos(16), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(23)}, }, }, }) - AssertParseStatement(t, `VALUES (1, 2), (3, 4)`, &sql.SelectStatement{ + /*AssertParseStatement(t, `VALUES (1, 2), (3, 4)`, &sql.SelectStatement{ Values: pos(0), ValueLists: []*sql.ExprList{ { @@ -1809,9 +2197,9 @@ func TestParser_ParseStatement(t *testing.T) { Rparen: pos(20), }, }, - }) + })*/ - AssertParseStatementError(t, `WITH `, `1:5: expected table name, found 'EOF'`) + /*AssertParseStatementError(t, `WITH `, `1:5: expected table name, found 'EOF'`) AssertParseStatementError(t, `WITH cte`, `1:8: expected AS, found 'EOF'`) AssertParseStatementError(t, `WITH cte (`, `1:10: expected column name, found 'EOF'`) AssertParseStatementError(t, `WITH cte (foo`, `1:13: expected comma or right paren, found 'EOF'`) @@ -1819,16 +2207,16 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `WITH cte AS`, `1:11: expected left paren, found 'EOF'`) AssertParseStatementError(t, `WITH cte AS (`, `1:13: expected SELECT or VALUES, found 'EOF'`) AssertParseStatementError(t, `WITH cte AS (SELECT foo`, `1:23: expected right paren, found 'EOF'`) - AssertParseStatementError(t, `WITH cte AS (SELECT foo)`, `1:24: expected SELECT, VALUES, INSERT, REPLACE, UPDATE, or DELETE, found 'EOF'`) + AssertParseStatementError(t, `WITH cte AS (SELECT foo)`, `1:24: expected SELECT, VALUES, INSERT, REPLACE, UPDATE, or DELETE, found 'EOF'`)*/ AssertParseStatementError(t, `SELECT `, `1:7: expected expression, found 'EOF'`) AssertParseStatementError(t, `SELECT 1+`, `1:9: expected expression, found 'EOF'`) AssertParseStatementError(t, `SELECT foo,`, `1:11: expected expression, found 'EOF'`) AssertParseStatementError(t, `SELECT foo AS`, `1:13: expected column alias, found 'EOF'`) AssertParseStatementError(t, `SELECT foo.* AS`, `1:14: expected semicolon or EOF, found 'AS'`) AssertParseStatementError(t, `SELECT foo FROM`, `1:15: expected table name or left paren, found 'EOF'`) - AssertParseStatementError(t, `SELECT foo FROM foo INDEXED`, `1:27: expected BY, found 'EOF'`) - AssertParseStatementError(t, `SELECT foo FROM foo INDEXED BY`, `1:30: expected index name, found 'EOF'`) - AssertParseStatementError(t, `SELECT foo FROM foo NOT`, `1:23: expected INDEXED, found 'EOF'`) + /*AssertParseStatementError(t, `SELECT foo FROM foo INDEXED`, `1:27: expected BY, found 'EOF'`) + AssertParseStatementError(t, `SELECT foo FROM foo INDEXED BY`, `1:30: expected index name, found 'EOF'`)*/ + /*AssertParseStatementError(t, `SELECT foo FROM foo NOT`, `1:23: expected INDEXED, found 'EOF'`)*/ AssertParseStatementError(t, `SELECT * FROM foo INNER`, `1:23: expected JOIN, found 'EOF'`) AssertParseStatementError(t, `SELECT * FROM foo CROSS`, `1:23: expected JOIN, found 'EOF'`) AssertParseStatementError(t, `SELECT * FROM foo NATURAL`, `1:25: expected JOIN, found 'EOF'`) @@ -1859,161 +2247,161 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `SELECT * ORDER`, `1:14: expected BY, found 'EOF'`) AssertParseStatementError(t, `SELECT * ORDER BY`, `1:17: expected expression, found 'EOF'`) AssertParseStatementError(t, `SELECT * ORDER BY 1,`, `1:20: expected expression, found 'EOF'`) - AssertParseStatementError(t, `SELECT * LIMIT`, `1:14: expected expression, found 'EOF'`) - AssertParseStatementError(t, `SELECT * LIMIT 1,`, `1:17: expected expression, found 'EOF'`) - AssertParseStatementError(t, `SELECT * LIMIT 1 OFFSET`, `1:23: expected expression, found 'EOF'`) - AssertParseStatementError(t, `VALUES`, `1:6: expected left paren, found 'EOF'`) + //AssertParseStatementError(t, `SELECT * LIMIT`, `1:14: expected expression, found 'EOF'`) + //AssertParseStatementError(t, `SELECT * LIMIT 1,`, `1:17: expected expression, found 'EOF'`) + //AssertParseStatementError(t, `SELECT * LIMIT 1 OFFSET`, `1:23: expected expression, found 'EOF'`) + /*AssertParseStatementError(t, `VALUES`, `1:6: expected left paren, found 'EOF'`) AssertParseStatementError(t, `VALUES (`, `1:8: expected expression, found 'EOF'`) AssertParseStatementError(t, `VALUES (1`, `1:9: expected comma or right paren, found 'EOF'`) - AssertParseStatementError(t, `VALUES (1,`, `1:10: expected expression, found 'EOF'`) - AssertParseStatementError(t, `SELECT * UNION`, `1:14: expected SELECT or VALUES, found 'EOF'`) + AssertParseStatementError(t, `VALUES (1,`, `1:10: expected expression, found 'EOF'`)*/ + //AssertParseStatementError(t, `SELECT * UNION`, `1:14: expected SELECT or VALUES, found 'EOF'`) }) t.Run("Insert", func(t *testing.T) { - AssertParseStatement(t, `INSERT INTO tbl (x, y) VALUES (1, 2)`, &sql.InsertStatement{ + AssertParseStatement(t, `INSERT INTO tbl (x, y) VALUES (1, 2)`, &parser.InsertStatement{ Insert: pos(0), Into: pos(7), - Table: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(12), Name: "tbl"}, ColumnsLparen: pos(16), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(17), Name: "x"}, {NamePos: pos(20), Name: "y"}, }, ColumnsRparen: pos(21), Values: pos(23), - ValueLists: []*sql.ExprList{{ + ValueList: &parser.ExprList{ Lparen: pos(30), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(31), Value: "1"}, - &sql.NumberLit{ValuePos: pos(34), Value: "2"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(31), Value: "1"}, + &parser.IntegerLit{ValuePos: pos(34), Value: "2"}, }, Rparen: pos(35), - }}, + }, }) - AssertParseStatement(t, `REPLACE INTO tbl (x, y) VALUES (1, 2), (3, 4)`, &sql.InsertStatement{ + /*AssertParseStatement(t, `REPLACE INTO tbl (x, y) VALUES (1, 2), (3, 4)`, &parser.InsertStatement{ Replace: pos(0), Into: pos(8), - Table: &sql.Ident{NamePos: pos(13), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(13), Name: "tbl"}, ColumnsLparen: pos(17), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(18), Name: "x"}, {NamePos: pos(21), Name: "y"}, }, ColumnsRparen: pos(22), Values: pos(24), - ValueLists: []*sql.ExprList{ + ValueLists: []*parser.ExprList{ { Lparen: pos(31), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(32), Value: "1"}, - &sql.NumberLit{ValuePos: pos(35), Value: "2"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(32), Value: "1"}, + &parser.IntegerLit{ValuePos: pos(35), Value: "2"}, }, Rparen: pos(36), }, { Lparen: pos(39), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(40), Value: "3"}, - &sql.NumberLit{ValuePos: pos(43), Value: "4"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(40), Value: "3"}, + &parser.IntegerLit{ValuePos: pos(43), Value: "4"}, }, Rparen: pos(44), }, }, - }) - AssertParseStatement(t, `INSERT OR REPLACE INTO tbl (x) VALUES (1)`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `INSERT OR REPLACE INTO tbl (x) VALUES (1)`, &parser.InsertStatement{ Insert: pos(0), InsertOr: pos(7), InsertOrReplace: pos(10), Into: pos(18), - Table: &sql.Ident{NamePos: pos(23), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(23), Name: "tbl"}, ColumnsLparen: pos(27), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(28), Name: "x"}, }, ColumnsRparen: pos(29), Values: pos(31), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(38), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(39), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(39), Value: "1"}, }, Rparen: pos(40), }}, - }) - AssertParseStatement(t, `INSERT OR ROLLBACK INTO tbl (x) VALUES (1)`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `INSERT OR ROLLBACK INTO tbl (x) VALUES (1)`, &parser.InsertStatement{ Insert: pos(0), InsertOr: pos(7), InsertOrRollback: pos(10), Into: pos(19), - Table: &sql.Ident{NamePos: pos(24), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(24), Name: "tbl"}, ColumnsLparen: pos(28), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(29), Name: "x"}, }, ColumnsRparen: pos(30), Values: pos(32), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(39), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(40), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(40), Value: "1"}, }, Rparen: pos(41), }}, - }) - AssertParseStatement(t, `INSERT OR ABORT INTO tbl (x) VALUES (1)`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `INSERT OR ABORT INTO tbl (x) VALUES (1)`, &parser.InsertStatement{ Insert: pos(0), InsertOr: pos(7), InsertOrAbort: pos(10), Into: pos(16), - Table: &sql.Ident{NamePos: pos(21), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(21), Name: "tbl"}, ColumnsLparen: pos(25), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(26), Name: "x"}, }, ColumnsRparen: pos(27), Values: pos(29), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(36), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(37), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(37), Value: "1"}, }, Rparen: pos(38), }}, - }) - AssertParseStatement(t, `INSERT OR FAIL INTO tbl VALUES (1)`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `INSERT OR FAIL INTO tbl VALUES (1)`, &parser.InsertStatement{ Insert: pos(0), InsertOr: pos(7), InsertOrFail: pos(10), Into: pos(15), - Table: &sql.Ident{NamePos: pos(20), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(20), Name: "tbl"}, Values: pos(24), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(31), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(32), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(32), Value: "1"}, }, Rparen: pos(33), }}, - }) - AssertParseStatement(t, `INSERT OR IGNORE INTO tbl AS tbl2 VALUES (1)`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `INSERT OR IGNORE INTO tbl AS tbl2 VALUES (1)`, &parser.InsertStatement{ Insert: pos(0), InsertOr: pos(7), InsertOrIgnore: pos(10), Into: pos(17), - Table: &sql.Ident{NamePos: pos(22), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(22), Name: "tbl"}, As: pos(26), - Alias: &sql.Ident{NamePos: pos(29), Name: "tbl2"}, + Alias: &parser.Ident{NamePos: pos(29), Name: "tbl2"}, Values: pos(34), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(41), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(42), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(42), Value: "1"}, }, Rparen: pos(43), }}, - }) + })*/ - AssertParseStatement(t, `WITH cte (foo) AS (SELECT bar) INSERT INTO tbl VALUES (1)`, &sql.InsertStatement{ + /*AssertParseStatement(t, `WITH cte (foo) AS (SELECT bar) INSERT INTO tbl VALUES (1)`, &sql.InsertStatement{ WithClause: &sql.WithClause{ With: pos(0), CTEs: []*sql.CTE{{ @@ -2045,8 +2433,8 @@ func TestParser_ParseStatement(t *testing.T) { }, Rparen: pos(56), }}, - }) - AssertParseStatement(t, `WITH cte (foo) AS (SELECT bar) INSERT INTO tbl VALUES (1)`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `WITH cte (foo) AS (SELECT bar) INSERT INTO tbl VALUES (1)`, &sql.InsertStatement{ WithClause: &sql.WithClause{ With: pos(0), CTEs: []*sql.CTE{{ @@ -2078,134 +2466,134 @@ func TestParser_ParseStatement(t *testing.T) { }, Rparen: pos(56), }}, - }) + })*/ - AssertParseStatement(t, `INSERT INTO tbl (x) SELECT y`, &sql.InsertStatement{ + /*AssertParseStatement(t, `INSERT INTO tbl (x) SELECT y`, &parser.InsertStatement{ Insert: pos(0), Into: pos(7), - Table: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(12), Name: "tbl"}, ColumnsLparen: pos(16), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(17), Name: "x"}, }, ColumnsRparen: pos(18), - Select: &sql.SelectStatement{ + Select: &parser.SelectStatement{ Select: pos(20), - Columns: []*sql.ResultColumn{ - {Expr: &sql.Ident{NamePos: pos(27), Name: "y"}}, + Columns: []*parser.ResultColumn{ + {Expr: &parser.Ident{NamePos: pos(27), Name: "y"}}, }, }, - }) + })*/ - AssertParseStatement(t, `INSERT INTO tbl (x) DEFAULT VALUES`, &sql.InsertStatement{ + /*AssertParseStatement(t, `INSERT INTO tbl (x) DEFAULT VALUES`, &parser.InsertStatement{ Insert: pos(0), Into: pos(7), - Table: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(12), Name: "tbl"}, ColumnsLparen: pos(16), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(17), Name: "x"}, }, ColumnsRparen: pos(18), Default: pos(20), DefaultValues: pos(28), - }) + })*/ - AssertParseStatement(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (y ASC, z DESC) DO NOTHING`, &sql.InsertStatement{ + /*AssertParseStatement(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (y ASC, z DESC) DO NOTHING`, &parser.InsertStatement{ Insert: pos(0), Into: pos(7), - Table: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(12), Name: "tbl"}, ColumnsLparen: pos(16), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(17), Name: "x"}, }, ColumnsRparen: pos(18), Values: pos(20), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(27), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(28), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(28), Value: "1"}, }, Rparen: pos(29), }}, - UpsertClause: &sql.UpsertClause{ + UpsertClause: &parser.UpsertClause{ On: pos(31), OnConflict: pos(34), Lparen: pos(43), - Columns: []*sql.IndexedColumn{ - {X: &sql.Ident{NamePos: pos(44), Name: "y"}, Asc: pos(46)}, - {X: &sql.Ident{NamePos: pos(51), Name: "z"}, Desc: pos(53)}, + Columns: []*parser.IndexedColumn{ + {X: &parser.Ident{NamePos: pos(44), Name: "y"}, Asc: pos(46)}, + {X: &parser.Ident{NamePos: pos(51), Name: "z"}, Desc: pos(53)}, }, Rparen: pos(57), Do: pos(59), DoNothing: pos(62), }, - }) - AssertParseStatement(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (y) WHERE true DO UPDATE SET foo = 1, (bar, baz) = 2 WHERE false`, &sql.InsertStatement{ + })*/ + /*AssertParseStatement(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (y) WHERE true DO UPDATE SET foo = 1, (bar, baz) = 2 WHERE false`, &parser.InsertStatement{ Insert: pos(0), Into: pos(7), - Table: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.Ident{NamePos: pos(12), Name: "tbl"}, ColumnsLparen: pos(16), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(17), Name: "x"}, }, ColumnsRparen: pos(18), Values: pos(20), - ValueLists: []*sql.ExprList{{ + ValueLists: []*parser.ExprList{{ Lparen: pos(27), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(28), Value: "1"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(28), Value: "1"}, }, Rparen: pos(29), }}, - UpsertClause: &sql.UpsertClause{ + UpsertClause: &parser.UpsertClause{ On: pos(31), OnConflict: pos(34), Lparen: pos(43), - Columns: []*sql.IndexedColumn{ - {X: &sql.Ident{NamePos: pos(44), Name: "y"}}, + Columns: []*parser.IndexedColumn{ + {X: &parser.Ident{NamePos: pos(44), Name: "y"}}, }, Rparen: pos(45), Where: pos(47), - WhereExpr: &sql.BoolLit{ValuePos: pos(53), Value: true}, + WhereExpr: &parser.BoolLit{ValuePos: pos(53), Value: true}, Do: pos(58), DoUpdate: pos(61), DoUpdateSet: pos(68), - Assignments: []*sql.Assignment{ + Assignments: []*parser.Assignment{ { - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(72), Name: "foo"}, }, Eq: pos(76), - Expr: &sql.NumberLit{ValuePos: pos(78), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(78), Value: "1"}, }, { Lparen: pos(81), - Columns: []*sql.Ident{ + Columns: []*parser.Ident{ {NamePos: pos(82), Name: "bar"}, {NamePos: pos(87), Name: "baz"}, }, Rparen: pos(90), Eq: pos(92), - Expr: &sql.NumberLit{ValuePos: pos(94), Value: "2"}, + Expr: &parser.IntegerLit{ValuePos: pos(94), Value: "2"}, }, }, UpdateWhere: pos(96), - UpdateWhereExpr: &sql.BoolLit{ValuePos: pos(102), Value: false}, + UpdateWhereExpr: &parser.BoolLit{ValuePos: pos(102), Value: false}, }, - }) + })*/ AssertParseStatementError(t, `INSERT`, `1:6: expected INTO, found 'EOF'`) - AssertParseStatementError(t, `INSERT OR`, `1:9: expected ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE, found 'EOF'`) + //AssertParseStatementError(t, `INSERT OR`, `1:9: expected ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO`, `1:11: expected table name, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl AS`, `1:18: expected alias, found 'EOF'`) - AssertParseStatementError(t, `INSERT INTO tbl `, `1:16: expected VALUES, SELECT, or DEFAULT VALUES, found 'EOF'`) + AssertParseStatementError(t, `INSERT INTO tbl `, `1:16: expected VALUES, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (`, `1:17: expected column name, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x`, `1:18: expected comma or right paren, found 'EOF'`) - AssertParseStatementError(t, `INSERT INTO tbl (x)`, `1:19: expected VALUES, SELECT, or DEFAULT VALUES, found 'EOF'`) + AssertParseStatementError(t, `INSERT INTO tbl (x)`, `1:19: expected VALUES, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES`, `1:26: expected left paren, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (`, `1:28: expected expression, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1`, `1:29: expected comma or right paren, found 'EOF'`) - AssertParseStatementError(t, `INSERT INTO tbl (x) SELECT`, `1:26: expected expression, found 'EOF'`) + /*AssertParseStatementError(t, `INSERT INTO tbl (x) SELECT`, `1:26: expected expression, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) DEFAULT`, `1:27: expected VALUES, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON`, `1:33: expected CONFLICT, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (`, `1:44: expected expression, found 'EOF'`) @@ -2218,118 +2606,118 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (x) DO UPDATE SET foo =`, `1:66: expected expression, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (x) DO UPDATE SET foo = 1 WHERE`, `1:74: expected expression, found 'EOF'`) AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (x) DO UPDATE SET (`, `1:62: expected column name, found 'EOF'`) - AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (x) DO UPDATE SET (foo`, `1:65: expected comma or right paren, found 'EOF'`) + AssertParseStatementError(t, `INSERT INTO tbl (x) VALUES (1) ON CONFLICT (x) DO UPDATE SET (foo`, `1:65: expected comma or right paren, found 'EOF'`)*/ }) t.Run("Update", func(t *testing.T) { - AssertParseStatement(t, `UPDATE tbl SET x = 1, y = 2`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE tbl SET x = 1, y = 2`, &parser.UpdateStatement{ Update: pos(0), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(7), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(7), Name: "tbl"}, }, Set: pos(11), - Assignments: []*sql.Assignment{ + Assignments: []*parser.Assignment{ { - Columns: []*sql.Ident{{NamePos: pos(15), Name: "x"}}, + Columns: []*parser.Ident{{NamePos: pos(15), Name: "x"}}, Eq: pos(17), - Expr: &sql.NumberLit{ValuePos: pos(19), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(19), Value: "1"}, }, { - Columns: []*sql.Ident{{NamePos: pos(22), Name: "y"}}, + Columns: []*parser.Ident{{NamePos: pos(22), Name: "y"}}, Eq: pos(24), - Expr: &sql.NumberLit{ValuePos: pos(26), Value: "2"}, + Expr: &parser.IntegerLit{ValuePos: pos(26), Value: "2"}, }, }, }) - AssertParseStatement(t, `UPDATE tbl SET x = 1 WHERE y = 2`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE tbl SET x = 1 WHERE y = 2`, &parser.UpdateStatement{ Update: pos(0), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(7), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(7), Name: "tbl"}, }, Set: pos(11), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(15), Name: "x"}}, + Assignments: []*parser.Assignment{{ + Columns: []*parser.Ident{{NamePos: pos(15), Name: "x"}}, Eq: pos(17), - Expr: &sql.NumberLit{ValuePos: pos(19), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(19), Value: "1"}, }}, Where: pos(21), - WhereExpr: &sql.BinaryExpr{ - X: &sql.Ident{NamePos: pos(27), Name: "y"}, - OpPos: pos(29), Op: sql.EQ, - Y: &sql.NumberLit{ValuePos: pos(31), Value: "2"}, + WhereExpr: &parser.BinaryExpr{ + X: &parser.Ident{NamePos: pos(27), Name: "y"}, + OpPos: pos(29), Op: parser.EQ, + Y: &parser.IntegerLit{ValuePos: pos(31), Value: "2"}, }, }) - AssertParseStatement(t, `UPDATE OR ROLLBACK tbl SET x = 1`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE OR ROLLBACK tbl SET x = 1`, &parser.UpdateStatement{ Update: pos(0), UpdateOr: pos(7), UpdateOrRollback: pos(10), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(19), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(19), Name: "tbl"}, }, Set: pos(23), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(27), Name: "x"}}, + Assignments: []*parser.Assignment{{ + Columns: []*parser.Ident{{NamePos: pos(27), Name: "x"}}, Eq: pos(29), - Expr: &sql.NumberLit{ValuePos: pos(31), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(31), Value: "1"}, }}, }) - AssertParseStatement(t, `UPDATE OR ABORT tbl SET x = 1`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE OR ABORT tbl SET x = 1`, &parser.UpdateStatement{ Update: pos(0), UpdateOr: pos(7), UpdateOrAbort: pos(10), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(16), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(16), Name: "tbl"}, }, Set: pos(20), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(24), Name: "x"}}, + Assignments: []*parser.Assignment{{ + Columns: []*parser.Ident{{NamePos: pos(24), Name: "x"}}, Eq: pos(26), - Expr: &sql.NumberLit{ValuePos: pos(28), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(28), Value: "1"}, }}, }) - AssertParseStatement(t, `UPDATE OR REPLACE tbl SET x = 1`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE OR REPLACE tbl SET x = 1`, &parser.UpdateStatement{ Update: pos(0), UpdateOr: pos(7), UpdateOrReplace: pos(10), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(18), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(18), Name: "tbl"}, }, Set: pos(22), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(26), Name: "x"}}, + Assignments: []*parser.Assignment{{ + Columns: []*parser.Ident{{NamePos: pos(26), Name: "x"}}, Eq: pos(28), - Expr: &sql.NumberLit{ValuePos: pos(30), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(30), Value: "1"}, }}, }) - AssertParseStatement(t, `UPDATE OR FAIL tbl SET x = 1`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE OR FAIL tbl SET x = 1`, &parser.UpdateStatement{ Update: pos(0), UpdateOr: pos(7), UpdateOrFail: pos(10), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(15), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(15), Name: "tbl"}, }, Set: pos(19), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(23), Name: "x"}}, + Assignments: []*parser.Assignment{{ + Columns: []*parser.Ident{{NamePos: pos(23), Name: "x"}}, Eq: pos(25), - Expr: &sql.NumberLit{ValuePos: pos(27), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(27), Value: "1"}, }}, }) - AssertParseStatement(t, `UPDATE OR IGNORE tbl SET x = 1`, &sql.UpdateStatement{ + AssertParseStatement(t, `UPDATE OR IGNORE tbl SET x = 1`, &parser.UpdateStatement{ Update: pos(0), UpdateOr: pos(7), UpdateOrIgnore: pos(10), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(17), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(17), Name: "tbl"}, }, Set: pos(21), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(25), Name: "x"}}, + Assignments: []*parser.Assignment{{ + Columns: []*parser.Ident{{NamePos: pos(25), Name: "x"}}, Eq: pos(27), - Expr: &sql.NumberLit{ValuePos: pos(29), Value: "1"}, + Expr: &parser.IntegerLit{ValuePos: pos(29), Value: "1"}, }}, }) - AssertParseStatement(t, `WITH cte (x) AS (SELECT y) UPDATE tbl SET x = 1`, &sql.UpdateStatement{ + /*AssertParseStatement(t, `WITH cte (x) AS (SELECT y) UPDATE tbl SET x = 1`, &sql.UpdateStatement{ WithClause: &sql.WithClause{ With: pos(0), CTEs: []*sql.CTE{ @@ -2362,7 +2750,7 @@ func TestParser_ParseStatement(t *testing.T) { Eq: pos(44), Expr: &sql.NumberLit{ValuePos: pos(46), Value: "1"}, }}, - }) + })*/ AssertParseStatementError(t, `UPDATE`, `1:6: expected table name, found 'EOF'`) AssertParseStatementError(t, `UPDATE OR`, `1:9: expected ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE, found 'EOF'`) @@ -2374,27 +2762,27 @@ func TestParser_ParseStatement(t *testing.T) { }) t.Run("Delete", func(t *testing.T) { - AssertParseStatement(t, `DELETE FROM tbl`, &sql.DeleteStatement{ + AssertParseStatement(t, `DELETE FROM tbl`, &parser.DeleteStatement{ Delete: pos(0), From: pos(7), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, }, }) - AssertParseStatement(t, `DELETE FROM tbl WHERE x = 1`, &sql.DeleteStatement{ + AssertParseStatement(t, `DELETE FROM tbl WHERE x = 1`, &parser.DeleteStatement{ Delete: pos(0), From: pos(7), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, }, Where: pos(16), - WhereExpr: &sql.BinaryExpr{ - X: &sql.Ident{NamePos: pos(22), Name: "x"}, - OpPos: pos(24), Op: sql.EQ, - Y: &sql.NumberLit{ValuePos: pos(26), Value: "1"}, + WhereExpr: &parser.BinaryExpr{ + X: &parser.Ident{NamePos: pos(22), Name: "x"}, + OpPos: pos(24), Op: parser.EQ, + Y: &parser.IntegerLit{ValuePos: pos(26), Value: "1"}, }, }) - AssertParseStatement(t, `WITH cte (x) AS (SELECT y) DELETE FROM tbl`, &sql.DeleteStatement{ + /*AssertParseStatement(t, `WITH cte (x) AS (SELECT y) DELETE FROM tbl`, &sql.DeleteStatement{ WithClause: &sql.WithClause{ With: pos(0), CTEs: []*sql.CTE{ @@ -2422,199 +2810,121 @@ func TestParser_ParseStatement(t *testing.T) { Table: &sql.QualifiedTableName{ Name: &sql.Ident{NamePos: pos(39), Name: "tbl"}, }, - }) - AssertParseStatement(t, `DELETE FROM tbl ORDER BY x, y LIMIT 1 OFFSET 2`, &sql.DeleteStatement{ + })*/ + /*AssertParseStatement(t, `DELETE FROM tbl ORDER BY x, y LIMIT 1 OFFSET 2`, &parser.DeleteStatement{ Delete: pos(0), From: pos(7), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, }, Order: pos(16), OrderBy: pos(22), - OrderingTerms: []*sql.OrderingTerm{ - {X: &sql.Ident{NamePos: pos(25), Name: "x"}}, - {X: &sql.Ident{NamePos: pos(28), Name: "y"}}, + OrderingTerms: []*parser.OrderingTerm{ + {X: &parser.Ident{NamePos: pos(25), Name: "x"}}, + {X: &parser.Ident{NamePos: pos(28), Name: "y"}}, }, Limit: pos(30), - LimitExpr: &sql.NumberLit{ValuePos: pos(36), Value: "1"}, + LimitExpr: &parser.IntegerLit{ValuePos: pos(36), Value: "1"}, Offset: pos(38), - OffsetExpr: &sql.NumberLit{ValuePos: pos(45), Value: "2"}, - }) - AssertParseStatement(t, `DELETE FROM tbl LIMIT 1`, &sql.DeleteStatement{ + OffsetExpr: &parser.IntegerLit{ValuePos: pos(45), Value: "2"}, + })*/ + /*AssertParseStatement(t, `DELETE FROM tbl LIMIT 1`, &parser.DeleteStatement{ Delete: pos(0), From: pos(7), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, }, Limit: pos(16), - LimitExpr: &sql.NumberLit{ValuePos: pos(22), Value: "1"}, - }) - AssertParseStatement(t, `DELETE FROM tbl LIMIT 1, 2`, &sql.DeleteStatement{ + LimitExpr: &parser.IntegerLit{ValuePos: pos(22), Value: "1"}, + })*/ + /*AssertParseStatement(t, `DELETE FROM tbl LIMIT 1, 2`, &parser.DeleteStatement{ Delete: pos(0), From: pos(7), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, + Table: &parser.QualifiedTableName{ + Name: &parser.Ident{NamePos: pos(12), Name: "tbl"}, }, Limit: pos(16), - LimitExpr: &sql.NumberLit{ValuePos: pos(22), Value: "1"}, + LimitExpr: &parser.IntegerLit{ValuePos: pos(22), Value: "1"}, OffsetComma: pos(23), - OffsetExpr: &sql.NumberLit{ValuePos: pos(25), Value: "2"}, - }) + OffsetExpr: &parser.IntegerLit{ValuePos: pos(25), Value: "2"}, + })*/ AssertParseStatementError(t, `DELETE`, `1:6: expected FROM, found 'EOF'`) AssertParseStatementError(t, `DELETE FROM`, `1:11: expected table name, found 'EOF'`) AssertParseStatementError(t, `DELETE FROM tbl WHERE`, `1:21: expected expression, found 'EOF'`) AssertParseStatementError(t, `DELETE FROM tbl ORDER `, `1:22: expected BY, found 'EOF'`) AssertParseStatementError(t, `DELETE FROM tbl ORDER BY`, `1:24: expected expression, found 'EOF'`) - AssertParseStatementError(t, `DELETE FROM tbl ORDER BY x`, `1:26: expected LIMIT, found 'EOF'`) - AssertParseStatementError(t, `DELETE FROM tbl LIMIT`, `1:21: expected expression, found 'EOF'`) - AssertParseStatementError(t, `DELETE FROM tbl LIMIT 1,`, `1:24: expected expression, found 'EOF'`) - AssertParseStatementError(t, `DELETE FROM tbl LIMIT 1 OFFSET`, `1:30: expected expression, found 'EOF'`) + //AssertParseStatementError(t, `DELETE FROM tbl ORDER BY x`, `1:26: expected LIMIT, found 'EOF'`) + //AssertParseStatementError(t, `DELETE FROM tbl LIMIT`, `1:21: expected expression, found 'EOF'`) + //AssertParseStatementError(t, `DELETE FROM tbl LIMIT 1,`, `1:24: expected expression, found 'EOF'`) + //AssertParseStatementError(t, `DELETE FROM tbl LIMIT 1 OFFSET`, `1:30: expected expression, found 'EOF'`) }) - t.Run("AlterTable", func(t *testing.T) { - AssertParseStatement(t, `ALTER TABLE tbl RENAME TO new_tbl`, &sql.AlterTableStatement{ - Alter: pos(0), - Table: pos(6), - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, - Rename: pos(16), - RenameTo: pos(23), - NewName: &sql.Ident{NamePos: pos(26), Name: "new_tbl"}, - }) - AssertParseStatement(t, `ALTER TABLE tbl RENAME COLUMN col TO new_col`, &sql.AlterTableStatement{ - Alter: pos(0), - Table: pos(6), - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, - Rename: pos(16), - RenameColumn: pos(23), - ColumnName: &sql.Ident{NamePos: pos(30), Name: "col"}, - To: pos(34), - NewColumnName: &sql.Ident{NamePos: pos(37), Name: "new_col"}, - }) - AssertParseStatement(t, `ALTER TABLE tbl RENAME col TO new_col`, &sql.AlterTableStatement{ - Alter: pos(0), - Table: pos(6), - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, - Rename: pos(16), - ColumnName: &sql.Ident{NamePos: pos(23), Name: "col"}, - To: pos(27), - NewColumnName: &sql.Ident{NamePos: pos(30), Name: "new_col"}, - }) - AssertParseStatement(t, `ALTER TABLE tbl ADD COLUMN col TEXT PRIMARY KEY`, &sql.AlterTableStatement{ - Alter: pos(0), - Table: pos(6), - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, - Add: pos(16), - AddColumn: pos(20), - ColumnDef: &sql.ColumnDefinition{ - Name: &sql.Ident{Name: "col", NamePos: pos(27)}, - Type: &sql.Type{ - Name: &sql.Ident{Name: "TEXT", NamePos: pos(31)}, - }, - Constraints: []sql.Constraint{ - &sql.PrimaryKeyConstraint{ - Primary: pos(36), - Key: pos(44), - }, - }, - }, - }) - AssertParseStatement(t, `ALTER TABLE tbl ADD col TEXT`, &sql.AlterTableStatement{ - Alter: pos(0), - Table: pos(6), - Name: &sql.Ident{NamePos: pos(12), Name: "tbl"}, - Add: pos(16), - ColumnDef: &sql.ColumnDefinition{ - Name: &sql.Ident{Name: "col", NamePos: pos(20)}, - Type: &sql.Type{ - Name: &sql.Ident{Name: "TEXT", NamePos: pos(24)}, - }, - }, - }) - - AssertParseStatementError(t, `ALTER`, `1:5: expected TABLE, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE`, `1:11: expected table name, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl`, `1:15: expected ADD or RENAME, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl RENAME`, `1:22: expected COLUMN keyword or column name, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl RENAME TO`, `1:25: expected new table name, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl RENAME COLUMN`, `1:29: expected column name, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl RENAME COLUMN col`, `1:33: expected TO, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl RENAME COLUMN col TO`, `1:36: expected new column name, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl ADD`, `1:19: expected COLUMN keyword or column name, found 'EOF'`) - AssertParseStatementError(t, `ALTER TABLE tbl ADD COLUMN`, `1:26: expected column name, found 'EOF'`) - }) - - t.Run("Analyze", func(t *testing.T) { - AssertParseStatement(t, `ANALYZE tbl`, &sql.AnalyzeStatement{ + /*t.Run("Analyze", func(t *testing.T) { + AssertParseStatement(t, `ANALYZE tbl`, &parser.AnalyzeStatement{ Analyze: pos(0), - Name: &sql.Ident{NamePos: pos(8), Name: "tbl"}, + Name: &parser.Ident{NamePos: pos(8), Name: "tbl"}, }) AssertParseStatementError(t, `ANALYZE`, `1:7: expected table or index name, found 'EOF'`) - }) + })*/ } func TestParser_ParseExpr(t *testing.T) { t.Run("Ident", func(t *testing.T) { - AssertParseExpr(t, `fooBAR_123'`, &sql.Ident{NamePos: pos(0), Name: `fooBAR_123`}) + AssertParseExpr(t, `fooBAR_123'`, &parser.Ident{NamePos: pos(0), Name: `fooBAR_123`}) }) t.Run("StringLit", func(t *testing.T) { - AssertParseExpr(t, `'foo bar'`, &sql.StringLit{ValuePos: pos(0), Value: `foo bar`}) - }) - t.Run("BlobLit", func(t *testing.T) { - AssertParseExpr(t, `x'0123'`, &sql.BlobLit{ValuePos: pos(0), Value: `0123`}) + AssertParseExpr(t, `'foo bar'`, &parser.StringLit{ValuePos: pos(0), Value: `foo bar`}) }) t.Run("Integer", func(t *testing.T) { - AssertParseExpr(t, `123`, &sql.NumberLit{ValuePos: pos(0), Value: `123`}) + AssertParseExpr(t, `123`, &parser.IntegerLit{ValuePos: pos(0), Value: `123`}) }) t.Run("Float", func(t *testing.T) { - AssertParseExpr(t, `123.456`, &sql.NumberLit{ValuePos: pos(0), Value: `123.456`}) + AssertParseExpr(t, `123.456`, &parser.FloatLit{ValuePos: pos(0), Value: `123.456`}) }) t.Run("Null", func(t *testing.T) { - AssertParseExpr(t, `NULL`, &sql.NullLit{Pos: pos(0)}) + AssertParseExpr(t, `NULL`, &parser.NullLit{ValuePos: pos(0)}) }) t.Run("Bool", func(t *testing.T) { - AssertParseExpr(t, `true`, &sql.BoolLit{ValuePos: pos(0), Value: true}) - AssertParseExpr(t, `false`, &sql.BoolLit{ValuePos: pos(0), Value: false}) - }) - t.Run("Bind", func(t *testing.T) { - AssertParseExpr(t, `$bar`, &sql.BindExpr{NamePos: pos(0), Name: "$bar"}) + AssertParseExpr(t, `true`, &parser.BoolLit{ValuePos: pos(0), Value: true}) + AssertParseExpr(t, `false`, &parser.BoolLit{ValuePos: pos(0), Value: false}) }) t.Run("UnaryExpr", func(t *testing.T) { - AssertParseExpr(t, `-123`, &sql.UnaryExpr{OpPos: pos(0), Op: sql.MINUS, X: &sql.NumberLit{ValuePos: pos(1), Value: `123`}}) + AssertParseExpr(t, `-123`, &parser.UnaryExpr{OpPos: pos(0), Op: parser.MINUS, X: &parser.IntegerLit{ValuePos: pos(1), Value: `123`}}) AssertParseExprError(t, `-`, `1:1: expected expression, found 'EOF'`) }) t.Run("QualifiedRef", func(t *testing.T) { - AssertParseExpr(t, `tbl.col`, &sql.QualifiedRef{ - Table: &sql.Ident{NamePos: pos(0), Name: "tbl"}, + AssertParseExpr(t, `tbl.col`, &parser.QualifiedRef{ + Table: &parser.Ident{NamePos: pos(0), Name: "tbl"}, Dot: pos(3), - Column: &sql.Ident{NamePos: pos(4), Name: "col"}, + Column: &parser.Ident{NamePos: pos(4), Name: "col"}, }) - AssertParseExpr(t, `"tbl"."col"`, &sql.QualifiedRef{ - Table: &sql.Ident{NamePos: pos(0), Name: "tbl", Quoted: true}, + AssertParseExpr(t, `"tbl"."col"`, &parser.QualifiedRef{ + Table: &parser.Ident{NamePos: pos(0), Name: "tbl", Quoted: true}, Dot: pos(5), - Column: &sql.Ident{NamePos: pos(6), Name: "col", Quoted: true}, + Column: &parser.Ident{NamePos: pos(6), Name: "col", Quoted: true}, }) AssertParseExprError(t, `tbl.`, `1:4: expected column name, found 'EOF'`) }) t.Run("Exists", func(t *testing.T) { - AssertParseExpr(t, `EXISTS (SELECT *)`, &sql.Exists{ + AssertParseExpr(t, `EXISTS (SELECT *)`, &parser.Exists{ Exists: pos(0), Lparen: pos(7), - Select: &sql.SelectStatement{ + Select: &parser.SelectStatement{ Select: pos(8), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(15)}, }, }, Rparen: pos(16), }) - AssertParseExpr(t, `NOT EXISTS (SELECT *)`, &sql.Exists{ + AssertParseExpr(t, `NOT EXISTS (SELECT *)`, &parser.Exists{ Not: pos(0), Exists: pos(4), Lparen: pos(11), - Select: &sql.SelectStatement{ + Select: &parser.SelectStatement{ Select: pos(12), - Columns: []*sql.ResultColumn{ + Columns: []*parser.ResultColumn{ {Star: pos(19)}, }, }, @@ -2622,172 +2932,172 @@ func TestParser_ParseExpr(t *testing.T) { }) AssertParseExprError(t, `NOT`, `1:3: expected EXISTS, found 'EOF'`) AssertParseExprError(t, `EXISTS`, `1:6: expected left paren, found 'EOF'`) - AssertParseExprError(t, `EXISTS (`, `1:8: expected SELECT or VALUES, found 'EOF'`) - AssertParseExprError(t, `EXISTS (SELECT`, `1:14: expected expression, found 'EOF'`) - AssertParseExprError(t, `EXISTS (SELECT *`, `1:16: expected right paren, found 'EOF'`) + //AssertParseExprError(t, `EXISTS (`, `1:8: expected SELECT or VALUES, found 'EOF'`) + //AssertParseExprError(t, `EXISTS (SELECT`, `1:14: expected expression, found 'EOF'`) + //AssertParseExprError(t, `EXISTS (SELECT *`, `1:16: expected right paren, found 'EOF'`) }) t.Run("BinaryExpr", func(t *testing.T) { - AssertParseExpr(t, `1 + 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.PLUS, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 + 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.PLUS, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 - 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.MINUS, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 - 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.MINUS, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 * 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.STAR, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 * 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.STAR, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 / 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.SLASH, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 / 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.SLASH, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 % 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.REM, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 % 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.REM, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 || 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.CONCAT, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 || 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.CONCAT, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `1 << 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.LSHIFT, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 << 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.LSHIFT, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `1 >> 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.RSHIFT, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 >> 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.RSHIFT, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `1 & 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.BITAND, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 & 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.BITAND, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 | 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.BITOR, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 | 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.BITOR, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 < 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.LT, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 < 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.LT, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 <= 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.LE, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 <= 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.LE, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `1 > 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.GT, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 > 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.GT, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 >= 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.GE, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 >= 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.GE, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `1 = 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.EQ, - Y: &sql.NumberLit{ValuePos: pos(4), Value: "2"}, + AssertParseExpr(t, `1 = 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.EQ, + Y: &parser.IntegerLit{ValuePos: pos(4), Value: "2"}, }) - AssertParseExpr(t, `1 != 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NE, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 != 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NE, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `(1 + 2)'`, &sql.ParenExpr{ + AssertParseExpr(t, `(1 + 2)'`, &parser.ParenExpr{ Lparen: pos(0), - X: &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(1), Value: "1"}, - OpPos: pos(3), Op: sql.PLUS, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + X: &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(1), Value: "1"}, + OpPos: pos(3), Op: parser.PLUS, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }, Rparen: pos(6), }) AssertParseExprError(t, `(`, `1:1: expected expression, found 'EOF'`) - AssertParseExpr(t, `1 IS 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.IS, - Y: &sql.NumberLit{ValuePos: pos(5), Value: "2"}, + AssertParseExpr(t, `1 IS 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.IS, + Y: &parser.IntegerLit{ValuePos: pos(5), Value: "2"}, }) - AssertParseExpr(t, `1 IS NOT 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.ISNOT, - Y: &sql.NumberLit{ValuePos: pos(9), Value: "2"}, + AssertParseExpr(t, `1 IS NOT 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.ISNOT, + Y: &parser.IntegerLit{ValuePos: pos(9), Value: "2"}, }) - AssertParseExpr(t, `1 LIKE 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.LIKE, - Y: &sql.NumberLit{ValuePos: pos(7), Value: "2"}, + AssertParseExpr(t, `1 LIKE 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.LIKE, + Y: &parser.IntegerLit{ValuePos: pos(7), Value: "2"}, }) - AssertParseExpr(t, `1 NOT LIKE 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NOTLIKE, - Y: &sql.NumberLit{ValuePos: pos(11), Value: "2"}, + AssertParseExpr(t, `1 NOT LIKE 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NOTLIKE, + Y: &parser.IntegerLit{ValuePos: pos(11), Value: "2"}, }) - AssertParseExpr(t, `1 GLOB 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.GLOB, - Y: &sql.NumberLit{ValuePos: pos(7), Value: "2"}, + AssertParseExpr(t, `1 GLOB 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.GLOB, + Y: &parser.IntegerLit{ValuePos: pos(7), Value: "2"}, }) - AssertParseExpr(t, `1 NOT GLOB 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NOTGLOB, - Y: &sql.NumberLit{ValuePos: pos(11), Value: "2"}, + AssertParseExpr(t, `1 NOT GLOB 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NOTGLOB, + Y: &parser.IntegerLit{ValuePos: pos(11), Value: "2"}, }) - AssertParseExpr(t, `1 REGEXP 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.REGEXP, - Y: &sql.NumberLit{ValuePos: pos(9), Value: "2"}, + AssertParseExpr(t, `1 REGEXP 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.REGEXP, + Y: &parser.IntegerLit{ValuePos: pos(9), Value: "2"}, }) - AssertParseExpr(t, `1 NOT REGEXP 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NOTREGEXP, - Y: &sql.NumberLit{ValuePos: pos(13), Value: "2"}, + AssertParseExpr(t, `1 NOT REGEXP 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NOTREGEXP, + Y: &parser.IntegerLit{ValuePos: pos(13), Value: "2"}, }) - AssertParseExpr(t, `1 MATCH 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.MATCH, - Y: &sql.NumberLit{ValuePos: pos(8), Value: "2"}, + AssertParseExpr(t, `1 MATCH 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.MATCH, + Y: &parser.IntegerLit{ValuePos: pos(8), Value: "2"}, }) - AssertParseExpr(t, `1 NOT MATCH 2'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NOTMATCH, - Y: &sql.NumberLit{ValuePos: pos(12), Value: "2"}, + AssertParseExpr(t, `1 NOT MATCH 2'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NOTMATCH, + Y: &parser.IntegerLit{ValuePos: pos(12), Value: "2"}, }) AssertParseExprError(t, `1 NOT TABLE`, `1:7: expected IN, LIKE, GLOB, REGEXP, MATCH, or BETWEEN, found 'TABLE'`) - AssertParseExpr(t, `1 IN (2, 3)'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.IN, - Y: &sql.ExprList{ + AssertParseExpr(t, `1 IN (2, 3)'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.IN, + Y: &parser.ExprList{ Lparen: pos(5), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(6), Value: "2"}, - &sql.NumberLit{ValuePos: pos(9), Value: "3"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(6), Value: "2"}, + &parser.IntegerLit{ValuePos: pos(9), Value: "3"}, }, Rparen: pos(10), }, }) - AssertParseExpr(t, `1 NOT IN (2, 3)'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NOTIN, - Y: &sql.ExprList{ + AssertParseExpr(t, `1 NOT IN (2, 3)'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NOTIN, + Y: &parser.ExprList{ Lparen: pos(9), - Exprs: []sql.Expr{ - &sql.NumberLit{ValuePos: pos(10), Value: "2"}, - &sql.NumberLit{ValuePos: pos(13), Value: "3"}, + Exprs: []parser.Expr{ + &parser.IntegerLit{ValuePos: pos(10), Value: "2"}, + &parser.IntegerLit{ValuePos: pos(13), Value: "3"}, }, Rparen: pos(14), }, @@ -2795,22 +3105,22 @@ func TestParser_ParseExpr(t *testing.T) { AssertParseExprError(t, `1 IN 2`, `1:6: expected left paren, found 2`) AssertParseExprError(t, `1 IN (`, `1:6: expected expression, found 'EOF'`) AssertParseExprError(t, `1 IN (2 3`, `1:9: expected comma or right paren, found 3`) - AssertParseExpr(t, `1 BETWEEN 2 AND 3'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.BETWEEN, - Y: &sql.Range{ - X: &sql.NumberLit{ValuePos: pos(10), Value: "2"}, + AssertParseExpr(t, `1 BETWEEN 2 AND 3'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.BETWEEN, + Y: &parser.Range{ + X: &parser.IntegerLit{ValuePos: pos(10), Value: "2"}, And: pos(12), - Y: &sql.NumberLit{ValuePos: pos(16), Value: "3"}, + Y: &parser.IntegerLit{ValuePos: pos(16), Value: "3"}, }, }) - AssertParseExpr(t, `1 NOT BETWEEN 2 AND 3'`, &sql.BinaryExpr{ - X: &sql.NumberLit{ValuePos: pos(0), Value: "1"}, - OpPos: pos(2), Op: sql.NOTBETWEEN, - Y: &sql.Range{ - X: &sql.NumberLit{ValuePos: pos(14), Value: "2"}, + AssertParseExpr(t, `1 NOT BETWEEN 2 AND 3'`, &parser.BinaryExpr{ + X: &parser.IntegerLit{ValuePos: pos(0), Value: "1"}, + OpPos: pos(2), Op: parser.NOTBETWEEN, + Y: &parser.Range{ + X: &parser.IntegerLit{ValuePos: pos(14), Value: "2"}, And: pos(16), - Y: &sql.NumberLit{ValuePos: pos(20), Value: "3"}, + Y: &parser.IntegerLit{ValuePos: pos(20), Value: "3"}, }, }) AssertParseExprError(t, `1 BETWEEN`, `1:9: expected expression, found 'EOF'`) @@ -2819,101 +3129,101 @@ func TestParser_ParseExpr(t *testing.T) { AssertParseExprError(t, `1 + `, `1:4: expected expression, found 'EOF'`) }) t.Run("Call", func(t *testing.T) { - AssertParseExpr(t, `sum()`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum()`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), }) - AssertParseExpr(t, `sum(*)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum(*)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Star: pos(4), Rparen: pos(5), }) - AssertParseExpr(t, `sum(foo, 123)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum(foo, 123)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), - Args: []sql.Expr{ - &sql.Ident{NamePos: pos(4), Name: "foo"}, - &sql.NumberLit{ValuePos: pos(9), Value: "123"}, + Args: []parser.Expr{ + &parser.Ident{NamePos: pos(4), Name: "foo"}, + &parser.IntegerLit{ValuePos: pos(9), Value: "123"}, }, Rparen: pos(12), }) - AssertParseExpr(t, `sum(distinct 'foo')`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum(distinct 'foo')`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Distinct: pos(4), - Args: []sql.Expr{ - &sql.StringLit{ValuePos: pos(13), Value: "foo"}, + Args: []parser.Expr{ + &parser.StringLit{ValuePos: pos(13), Value: "foo"}, }, Rparen: pos(18), }) - AssertParseExpr(t, `sum() filter (where true)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() filter (where true)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Filter: &sql.FilterClause{ + Filter: &parser.FilterClause{ Filter: pos(6), Lparen: pos(13), Where: pos(14), - X: &sql.BoolLit{ValuePos: pos(20), Value: true}, + X: &parser.BoolLit{ValuePos: pos(20), Value: true}, Rparen: pos(24), }, }) - AssertParseExpr(t, `sum() over win1`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over win1`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Name: &sql.Ident{NamePos: pos(11), Name: "win1"}, + Name: &parser.Ident{NamePos: pos(11), Name: "win1"}, }, }) - AssertParseExpr(t, `sum() over (win1 partition by foo, bar order by baz ASC NULLS FIRST, biz)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (win1 partition by foo, bar order by baz ASC NULLS FIRST, biz)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Base: &sql.Ident{NamePos: pos(12), Name: "win1"}, + Base: &parser.Ident{NamePos: pos(12), Name: "win1"}, Partition: pos(17), PartitionBy: pos(27), - Partitions: []sql.Expr{ - &sql.Ident{NamePos: pos(30), Name: "foo"}, - &sql.Ident{NamePos: pos(35), Name: "bar"}, + Partitions: []parser.Expr{ + &parser.Ident{NamePos: pos(30), Name: "foo"}, + &parser.Ident{NamePos: pos(35), Name: "bar"}, }, Order: pos(39), OrderBy: pos(45), - OrderingTerms: []*sql.OrderingTerm{ + OrderingTerms: []*parser.OrderingTerm{ { - X: &sql.Ident{NamePos: pos(48), Name: "baz"}, + X: &parser.Ident{NamePos: pos(48), Name: "baz"}, Asc: pos(52), Nulls: pos(56), NullsFirst: pos(62), }, { - X: &sql.Ident{NamePos: pos(69), Name: "biz"}, + X: &parser.Ident{NamePos: pos(69), Name: "biz"}, }, }, Rparen: pos(72), }, }, }) - AssertParseExpr(t, `sum() over (order by baz DESC NULLS LAST)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (order by baz DESC NULLS LAST)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), Order: pos(12), OrderBy: pos(18), - OrderingTerms: []*sql.OrderingTerm{ + OrderingTerms: []*parser.OrderingTerm{ { - X: &sql.Ident{NamePos: pos(21), Name: "baz"}, + X: &parser.Ident{NamePos: pos(21), Name: "baz"}, Desc: pos(25), Nulls: pos(30), NullsLast: pos(36), @@ -2923,74 +3233,74 @@ func TestParser_ParseExpr(t *testing.T) { }, }, }) - AssertParseExpr(t, `sum() over (range foo preceding)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (range foo preceding)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Range: pos(12), - X: &sql.Ident{NamePos: pos(18), Name: "foo"}, + X: &parser.Ident{NamePos: pos(18), Name: "foo"}, PrecedingX: pos(22), }, Rparen: pos(31), }, }, }) - AssertParseExpr(t, `sum() over (rows between foo following and bar preceding)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (rows between foo following and bar preceding)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Rows: pos(12), Between: pos(17), - X: &sql.Ident{NamePos: pos(25), Name: "foo"}, + X: &parser.Ident{NamePos: pos(25), Name: "foo"}, FollowingX: pos(29), And: pos(39), - Y: &sql.Ident{NamePos: pos(43), Name: "bar"}, + Y: &parser.Ident{NamePos: pos(43), Name: "bar"}, PrecedingY: pos(47), }, Rparen: pos(56), }, }, }) - AssertParseExpr(t, `sum() over (rows between foo following and bar following)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (rows between foo following and bar following)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Rows: pos(12), Between: pos(17), - X: &sql.Ident{NamePos: pos(25), Name: "foo"}, + X: &parser.Ident{NamePos: pos(25), Name: "foo"}, FollowingX: pos(29), And: pos(39), - Y: &sql.Ident{NamePos: pos(43), Name: "bar"}, + Y: &parser.Ident{NamePos: pos(43), Name: "bar"}, FollowingY: pos(47), }, Rparen: pos(56), }, }, }) - AssertParseExpr(t, `sum() over (groups between unbounded preceding and unbounded following)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (groups between unbounded preceding and unbounded following)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Groups: pos(12), Between: pos(19), UnboundedX: pos(27), @@ -3003,15 +3313,15 @@ func TestParser_ParseExpr(t *testing.T) { }, }, }) - AssertParseExpr(t, `sum() over (groups between current row and current row)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (groups between current row and current row)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Groups: pos(12), Between: pos(19), CurrentX: pos(27), @@ -3024,15 +3334,15 @@ func TestParser_ParseExpr(t *testing.T) { }, }, }) - AssertParseExpr(t, `sum() over (groups current row exclude no others)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (groups current row exclude no others)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Groups: pos(12), CurrentX: pos(19), CurrentRowX: pos(27), @@ -3044,15 +3354,15 @@ func TestParser_ParseExpr(t *testing.T) { }, }, }) - AssertParseExpr(t, `sum() over (groups current row exclude current row)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (groups current row exclude current row)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Groups: pos(12), CurrentX: pos(19), CurrentRowX: pos(27), @@ -3064,15 +3374,15 @@ func TestParser_ParseExpr(t *testing.T) { }, }, }) - AssertParseExpr(t, `sum() over (groups current row exclude group)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (groups current row exclude group)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Groups: pos(12), CurrentX: pos(19), CurrentRowX: pos(27), @@ -3083,15 +3393,15 @@ func TestParser_ParseExpr(t *testing.T) { }, }, }) - AssertParseExpr(t, `sum() over (groups current row exclude ties)`, &sql.Call{ - Name: &sql.Ident{NamePos: pos(0), Name: "sum"}, + AssertParseExpr(t, `sum() over (groups current row exclude ties)`, &parser.Call{ + Name: &parser.Ident{NamePos: pos(0), Name: "sum"}, Lparen: pos(3), Rparen: pos(4), - Over: &sql.OverClause{ + Over: &parser.OverClause{ Over: pos(6), - Definition: &sql.WindowDefinition{ + Definition: &parser.WindowDefinition{ Lparen: pos(11), - Frame: &sql.FrameSpec{ + Frame: &parser.FrameSpec{ Groups: pos(12), CurrentX: pos(19), CurrentRowX: pos(27), @@ -3134,12 +3444,12 @@ func TestParser_ParseExpr(t *testing.T) { }) t.Run("Cast", func(t *testing.T) { - AssertParseExpr(t, `CAST (1 AS INTEGER)`, &sql.CastExpr{ + AssertParseExpr(t, `CAST (1 AS INTEGER)`, &parser.CastExpr{ Cast: pos(0), Lparen: pos(5), - X: &sql.NumberLit{ValuePos: pos(6), Value: "1"}, + X: &parser.IntegerLit{ValuePos: pos(6), Value: "1"}, As: pos(8), - Type: &sql.Type{Name: &sql.Ident{NamePos: pos(11), Name: "INTEGER"}}, + Type: &parser.Type{Name: &parser.Ident{NamePos: pos(11), Name: "INTEGER"}}, Rparen: pos(18), }) AssertParseExprError(t, `CAST`, `1:4: expected left paren, found 'EOF'`) @@ -3150,35 +3460,35 @@ func TestParser_ParseExpr(t *testing.T) { }) t.Run("Case", func(t *testing.T) { - AssertParseExpr(t, `CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 ELSE 6 END`, &sql.CaseExpr{ + AssertParseExpr(t, `CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 ELSE 6 END`, &parser.CaseExpr{ Case: pos(0), - Operand: &sql.NumberLit{ValuePos: pos(5), Value: "1"}, - Blocks: []*sql.CaseBlock{ + Operand: &parser.IntegerLit{ValuePos: pos(5), Value: "1"}, + Blocks: []*parser.CaseBlock{ { When: pos(7), - Condition: &sql.NumberLit{ValuePos: pos(12), Value: "2"}, + Condition: &parser.IntegerLit{ValuePos: pos(12), Value: "2"}, Then: pos(14), - Body: &sql.NumberLit{ValuePos: pos(19), Value: "3"}, + Body: &parser.IntegerLit{ValuePos: pos(19), Value: "3"}, }, { When: pos(21), - Condition: &sql.NumberLit{ValuePos: pos(26), Value: "4"}, + Condition: &parser.IntegerLit{ValuePos: pos(26), Value: "4"}, Then: pos(28), - Body: &sql.NumberLit{ValuePos: pos(33), Value: "5"}, + Body: &parser.IntegerLit{ValuePos: pos(33), Value: "5"}, }, }, Else: pos(35), - ElseExpr: &sql.NumberLit{ValuePos: pos(40), Value: "6"}, + ElseExpr: &parser.IntegerLit{ValuePos: pos(40), Value: "6"}, End: pos(42), }) - AssertParseExpr(t, `CASE WHEN 1 THEN 2 END`, &sql.CaseExpr{ + AssertParseExpr(t, `CASE WHEN 1 THEN 2 END`, &parser.CaseExpr{ Case: pos(0), - Blocks: []*sql.CaseBlock{ + Blocks: []*parser.CaseBlock{ { When: pos(5), - Condition: &sql.NumberLit{ValuePos: pos(10), Value: "1"}, + Condition: &parser.IntegerLit{ValuePos: pos(10), Value: "1"}, Then: pos(12), - Body: &sql.NumberLit{ValuePos: pos(17), Value: "2"}, + Body: &parser.IntegerLit{ValuePos: pos(17), Value: "2"}, }, }, End: pos(19), @@ -3193,7 +3503,8 @@ func TestParser_ParseExpr(t *testing.T) { AssertParseExprError(t, `CASE WHEN 1 THEN 2 ELSE 3`, `1:25: expected END, found 'EOF'`) }) - t.Run("Raise", func(t *testing.T) { + /*t.Run("Raise", func(t *testing.T) { + AssertParseExpr(t, `RAISE(IGNORE)`, &sql.Raise{ Raise: pos(0), Lparen: pos(5), @@ -3229,20 +3540,20 @@ func TestParser_ParseExpr(t *testing.T) { AssertParseExprError(t, `RAISE(IGNORE`, `1:12: expected right paren, found 'EOF'`) AssertParseExprError(t, `RAISE(ROLLBACK`, `1:14: expected comma, found 'EOF'`) AssertParseExprError(t, `RAISE(ROLLBACK,`, `1:15: expected error message, found 'EOF'`) - }) + })*/ } func TestError_Error(t *testing.T) { - err := &sql.Error{Msg: "test"} + err := &parser.Error{Msg: "test"} if got, want := err.Error(), `test`; got != want { t.Fatalf("Error()=%s, want %s", got, want) } } // ParseStatementOrFail parses a statement from s. Fail on error. -func ParseStatementOrFail(tb testing.TB, s string) sql.Statement { +func ParseStatementOrFail(tb testing.TB, s string) parser.Statement { tb.Helper() - stmt, err := sql.NewParser(strings.NewReader(s)).ParseStatement() + stmt, err := parser.NewParser(strings.NewReader(s)).ParseStatement() if err != nil { tb.Fatal(err) } @@ -3250,9 +3561,9 @@ func ParseStatementOrFail(tb testing.TB, s string) sql.Statement { } // AssertParseStatement asserts the value of the first parse of s. -func AssertParseStatement(tb testing.TB, s string, want sql.Statement) { +func AssertParseStatement(tb testing.TB, s string, want parser.Statement) { tb.Helper() - stmt, err := sql.NewParser(strings.NewReader(s)).ParseStatement() + stmt, err := parser.NewParser(strings.NewReader(s)).ParseStatement() if err != nil { tb.Fatal(err) } else if diff := deep.Equal(stmt, want); diff != nil { @@ -3263,16 +3574,16 @@ func AssertParseStatement(tb testing.TB, s string, want sql.Statement) { // AssertParseStatementError asserts s parses to a given error string. func AssertParseStatementError(tb testing.TB, s string, want string) { tb.Helper() - _, err := sql.NewParser(strings.NewReader(s)).ParseStatement() + _, err := parser.NewParser(strings.NewReader(s)).ParseStatement() if err == nil || err.Error() != want { tb.Fatalf("ParseStatement()=%q, want %q", err, want) } } // AssertParseExpr asserts the value of the first parse of s. -func AssertParseExpr(tb testing.TB, s string, want sql.Expr) { +func AssertParseExpr(tb testing.TB, s string, want parser.Expr) { tb.Helper() - stmt, err := sql.NewParser(strings.NewReader(s)).ParseExpr() + stmt, err := parser.NewParser(strings.NewReader(s)).ParseExpr() if err != nil { tb.Fatal(err) } else if diff := deep.Equal(stmt, want); diff != nil { @@ -3283,15 +3594,15 @@ func AssertParseExpr(tb testing.TB, s string, want sql.Expr) { // AssertParseExprError asserts s parses to a given error string. func AssertParseExprError(tb testing.TB, s string, want string) { tb.Helper() - _, err := sql.NewParser(strings.NewReader(s)).ParseExpr() + _, err := parser.NewParser(strings.NewReader(s)).ParseExpr() if err == nil || err.Error() != want { tb.Fatalf("ParseExpr()=%q, want %q", err, want) } } // pos is a helper function for generating positions based on offset for one-line parsing. -func pos(offset int) sql.Pos { - return sql.Pos{Offset: offset, Line: 1, Column: offset + 1} +func pos(offset int) parser.Pos { + return parser.Pos{Offset: offset, Line: 1, Column: offset + 1} } func deepEqual(a, b interface{}) string { diff --git a/sql2/scanner.go b/sql3/parser/scanner.go similarity index 89% rename from sql2/scanner.go rename to sql3/parser/scanner.go index fb2d8da61..69fb912c3 100644 --- a/sql2/scanner.go +++ b/sql3/parser/scanner.go @@ -1,6 +1,5 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2 +// Copyright 2021 Molecula Corp. All rights reserved. +package parser import ( "bufio" @@ -42,8 +41,6 @@ func (s *Scanner) Scan() (pos Pos, token Token, lit string) { return s.scanQuotedIdent() } else if ch == '\'' { return s.scanString() - } else if ch == '?' || ch == ':' || ch == '@' || ch == '$' { - return s.scanBind() } switch ch, pos := s.read(); ch { @@ -53,6 +50,10 @@ func (s *Scanner) Scan() (pos Pos, token Token, lit string) { return pos, LP, "(" case ')': return pos, RP, ")" + case '[': + return pos, LB, "[" + case ']': + return pos, RB, "]" case ',': return pos, COMMA, "," case '!': @@ -174,30 +175,6 @@ func (s *Scanner) scanString() (Pos, Token, string) { } } -func (s *Scanner) scanBind() (Pos, Token, string) { - start, pos := s.read() - - s.buf.Reset() - s.buf.WriteRune(start) - - // Question mark starts a numeric bind. - if start == '?' { - for isDigit(s.peek()) { - ch, _ := s.read() - s.buf.WriteRune(ch) - } - return pos, BIND, s.buf.String() - } - - // All other characters start an alphanumeric bind. - assert(start == ':' || start == '@' || start == '$') - for isUnquotedIdent(s.peek()) { - ch, _ := s.read() - s.buf.WriteRune(ch) - } - return pos, BIND, s.buf.String() -} - func (s *Scanner) scanBlob() (Pos, Token, string) { start, pos := s.read() assert(start == 'x' || start == 'X') diff --git a/sql3/parser/scanner_test.go b/sql3/parser/scanner_test.go new file mode 100644 index 000000000..2991c6665 --- /dev/null +++ b/sql3/parser/scanner_test.go @@ -0,0 +1,160 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package parser_test + +import ( + "strings" + "testing" + + "github.com/molecula/featurebase/v3/sql3/parser" +) + +func TestScanner_Scan(t *testing.T) { + t.Run("IDENT", func(t *testing.T) { + t.Run("Unquoted", func(t *testing.T) { + AssertScan(t, `foo_BAR123`, parser.IDENT, `foo_BAR123`) + }) + t.Run("Quoted", func(t *testing.T) { + AssertScan(t, `"crazy ~!#*&# column name"" foo"`, parser.QIDENT, `crazy ~!#*&# column name" foo`) + }) + t.Run("NoEndQuote", func(t *testing.T) { + AssertScan(t, `"unfinished`, parser.ILLEGAL, `"unfinished`) + }) + t.Run("x", func(t *testing.T) { + AssertScan(t, `x`, parser.IDENT, `x`) + }) + t.Run("StartingX", func(t *testing.T) { + AssertScan(t, `xyz`, parser.IDENT, `xyz`) + }) + t.Run("WithComment", func(t *testing.T) { + AssertScan(t, "-- this is a comment\n\n-- more comments\nfoo", parser.IDENT, `foo`) + }) + }) + + t.Run("KEYWORD", func(t *testing.T) { + AssertScan(t, `BEGIN`, parser.BEGIN, `BEGIN`) + }) + + t.Run("STRING", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + AssertScan(t, `'this is ''a'' string'`, parser.STRING, `this is 'a' string`) + }) + t.Run("NoEndQuote", func(t *testing.T) { + AssertScan(t, `'unfinished`, parser.ILLEGAL, `'unfinished`) + }) + }) + t.Run("BLOB", func(t *testing.T) { + t.Run("LowerX", func(t *testing.T) { + AssertScan(t, `x'0123456789abcdef'`, parser.BLOB, `0123456789abcdef`) + }) + t.Run("UpperX", func(t *testing.T) { + AssertScan(t, `X'0123456789ABCDEF'`, parser.BLOB, `0123456789ABCDEF`) + }) + t.Run("NoEndQuote", func(t *testing.T) { + AssertScan(t, `x'0123`, parser.ILLEGAL, `x'0123`) + }) + t.Run("BadHex", func(t *testing.T) { + AssertScan(t, `x'hello`, parser.ILLEGAL, `x'h`) + }) + }) + + t.Run("INTEGER", func(t *testing.T) { + AssertScan(t, `123`, parser.INTEGER, `123`) + }) + + t.Run("FLOAT", func(t *testing.T) { + AssertScan(t, `123.456`, parser.FLOAT, `123.456`) + AssertScan(t, `.1`, parser.FLOAT, `.1`) + AssertScan(t, `123e456`, parser.FLOAT, `123e456`) + AssertScan(t, `123E456`, parser.FLOAT, `123E456`) + AssertScan(t, `123.456E78`, parser.FLOAT, `123.456E78`) + AssertScan(t, `123.E45`, parser.FLOAT, `123.E45`) + AssertScan(t, `123E+4`, parser.FLOAT, `123E+4`) + AssertScan(t, `123E-4`, parser.FLOAT, `123E-4`) + AssertScan(t, `123E`, parser.ILLEGAL, `123E`) + AssertScan(t, `123E+`, parser.ILLEGAL, `123E+`) + AssertScan(t, `123E-`, parser.ILLEGAL, `123E-`) + }) + + t.Run("EOF", func(t *testing.T) { + AssertScan(t, " \n\t\r", parser.EOF, ``) + }) + + t.Run("SEMI", func(t *testing.T) { + AssertScan(t, ";", parser.SEMI, ";") + }) + t.Run("LP", func(t *testing.T) { + AssertScan(t, "(", parser.LP, "(") + }) + t.Run("RP", func(t *testing.T) { + AssertScan(t, ")", parser.RP, ")") + }) + t.Run("COMMA", func(t *testing.T) { + AssertScan(t, ",", parser.COMMA, ",") + }) + t.Run("NE", func(t *testing.T) { + AssertScan(t, "!=", parser.NE, "!=") + }) + t.Run("BITNOT", func(t *testing.T) { + AssertScan(t, "!", parser.BITNOT, "!") + }) + t.Run("EQ", func(t *testing.T) { + AssertScan(t, "=", parser.EQ, "=") + }) + t.Run("LE", func(t *testing.T) { + AssertScan(t, "<=", parser.LE, "<=") + }) + t.Run("LSHIFT", func(t *testing.T) { + AssertScan(t, "<<", parser.LSHIFT, "<<") + }) + t.Run("LT", func(t *testing.T) { + AssertScan(t, "<", parser.LT, "<") + }) + t.Run("GE", func(t *testing.T) { + AssertScan(t, ">=", parser.GE, ">=") + }) + t.Run("RSHIFT", func(t *testing.T) { + AssertScan(t, ">>", parser.RSHIFT, ">>") + }) + t.Run("GT", func(t *testing.T) { + AssertScan(t, ">", parser.GT, ">") + }) + t.Run("BITAND", func(t *testing.T) { + AssertScan(t, "&", parser.BITAND, "&") + }) + t.Run("CONCAT", func(t *testing.T) { + AssertScan(t, "||", parser.CONCAT, "||") + }) + t.Run("BITOR", func(t *testing.T) { + AssertScan(t, "|", parser.BITOR, "|") + }) + t.Run("PLUS", func(t *testing.T) { + AssertScan(t, "+", parser.PLUS, "+") + }) + t.Run("MINUS", func(t *testing.T) { + AssertScan(t, "-", parser.MINUS, "-") + }) + t.Run("STAR", func(t *testing.T) { + AssertScan(t, "*", parser.STAR, "*") + }) + t.Run("SLASH", func(t *testing.T) { + AssertScan(t, "/", parser.SLASH, "/") + }) + t.Run("REM", func(t *testing.T) { + AssertScan(t, "%", parser.REM, "%") + }) + t.Run("DOT", func(t *testing.T) { + AssertScan(t, ".", parser.DOT, ".") + }) + t.Run("ILLEGAL", func(t *testing.T) { + AssertScan(t, "^", parser.ILLEGAL, "^") + }) +} + +// AssertScan asserts the value of the first scan to s. +func AssertScan(tb testing.TB, s string, expectedTok parser.Token, expectedLit string) { + tb.Helper() + _, tok, lit := parser.NewScanner(strings.NewReader(s)).Scan() + if tok != expectedTok || lit != expectedLit { + tb.Fatalf("Scan(%q)=<%s,%s>, want <%s,%s>", s, tok, lit, expectedTok, expectedLit) + } +} diff --git a/sql2/token.go b/sql3/parser/token.go similarity index 91% rename from sql2/token.go rename to sql3/parser/token.go index f6d472c8e..17c18022c 100644 --- a/sql2/token.go +++ b/sql3/parser/token.go @@ -1,6 +1,5 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2 +// Copyright 2021 Molecula Corp. All rights reserved. +package parser import ( "fmt" @@ -23,6 +22,7 @@ func init() { // Token is the set of lexical tokens of the Go programming language. type Token int +//TODO (pok) remove unnecessary tokens // The list of tokens. const ( // Special tokens @@ -41,13 +41,14 @@ const ( NULL // NULL TRUE // true FALSE // false - BIND //? or ?NNN or :VVV or @VVV or $VVV literal_end operator_beg SEMI // ; LP // ( RP // ) + LB // [ + RB // ] COMMA // , NE // != EQ // = @@ -89,11 +90,14 @@ const ( BEGIN BETWEEN BY + BULK + CACHETYPE CASCADE CASE CAST CHECK COLUMN + COLUMNS COLUMNKW COMMIT CONFLICT @@ -102,7 +106,6 @@ const ( CROSS CTIME_KW CURRENT - CURRENT_TIME CURRENT_DATE CURRENT_TIMESTAMP DATABASE @@ -118,6 +121,7 @@ const ( EACH ELSE END + EPOCH ESCAPE EXCEPT EXCLUDE @@ -151,14 +155,16 @@ const ( INTO IS ISNOT - ISNULL // TODO: REMOVE? JOIN KEY + KEYPARTITIONS LAST LEFT LIKE - LIMIT + LRU MATCH + MAX + MIN NATURAL NO NOT @@ -169,11 +175,9 @@ const ( NOTIN NOTLIKE NOTMATCH - NOTNULL NOTREGEXP NULLS OF - OFFSET ON OR ORDER @@ -186,8 +190,8 @@ const ( PRECEDING PRIMARY QUERY - RAISE RANGE + RANKED RECURSIVE REFERENCES REGEXP @@ -204,15 +208,24 @@ const ( SELECT SELECT_COLUMN SET + SHARDWIDTH + SIZE + SHOW SPAN TABLE + TABLES TEMP THEN TIES + TIMEUNIT + TIMEQUANTUM TO + TOP + TOPN TRANSACTION TRIGGER TRUTH + TTL UNBOUNDED UNION UNIQUE @@ -243,17 +256,17 @@ var tokens = [...]string{ IDENT: "IDENT", QIDENT: "QIDENT", STRING: "STRING", - BLOB: "BLOB", FLOAT: "FLOAT", INTEGER: "INTEGER", NULL: "NULL", TRUE: "TRUE", FALSE: "FALSE", - BIND: "BIND", SEMI: ";", LP: "(", RP: ")", + LB: "[", + RB: "]", COMMA: ",", NE: "!=", EQ: "=", @@ -293,11 +306,14 @@ var tokens = [...]string{ BEGIN: "BEGIN", BETWEEN: "BETWEEN", BY: "BY", + BULK: "BULK", + CACHETYPE: "CACHETYPE", CASCADE: "CASCADE", CASE: "CASE", CAST: "CAST", CHECK: "CHECK", COLUMN: "COLUMN", + COLUMNS: "COLUMNS", COLUMNKW: "COLUMNKW", COMMIT: "COMMIT", CONFLICT: "CONFLICT", @@ -306,7 +322,6 @@ var tokens = [...]string{ CROSS: "CROSS", CTIME_KW: "CTIME_KW", CURRENT: "CURRENT", - CURRENT_TIME: "CURRENT_TIME", CURRENT_DATE: "CURRENT_DATE", CURRENT_TIMESTAMP: "CURRENT_TIMESTAMP", DATABASE: "DATABASE", @@ -322,6 +337,7 @@ var tokens = [...]string{ EACH: "EACH", ELSE: "ELSE", END: "END", + EPOCH: "EPOCH", ESCAPE: "ESCAPE", EXCEPT: "EXCEPT", EXCLUDE: "EXCLUDE", @@ -355,14 +371,16 @@ var tokens = [...]string{ INTO: "INTO", IS: "IS", ISNOT: "ISNOT", - ISNULL: "ISNULL", JOIN: "JOIN", KEY: "KEY", + KEYPARTITIONS: "KEYPARTITIONS", LAST: "LAST", LEFT: "LEFT", LIKE: "LIKE", - LIMIT: "LIMIT", + LRU: "LRU", MATCH: "MATCH", + MAX: "MAX", + MIN: "MIN", NO: "NO", NATURAL: "NATURAL", NOT: "NOT", @@ -373,11 +391,9 @@ var tokens = [...]string{ NOTIN: "NOTIN", NOTLIKE: "NOTLIKE", NOTMATCH: "NOTMATCH", - NOTNULL: "NOTNULL", NOTREGEXP: "NOTREGEXP", NULLS: "NULLS", OF: "OF", - OFFSET: "OFFSET", ON: "ON", OR: "OR", ORDER: "ORDER", @@ -390,8 +406,8 @@ var tokens = [...]string{ PRECEDING: "PRECEDING", PRIMARY: "PRIMARY", QUERY: "QUERY", - RAISE: "RAISE", RANGE: "RANGE", + RANKED: "RANKED", RECURSIVE: "RECURSIVE", REFERENCES: "REFERENCES", REGEXP: "REGEXP", @@ -408,15 +424,24 @@ var tokens = [...]string{ SELECT: "SELECT", SELECT_COLUMN: "SELECT_COLUMN", SET: "SET", + SIZE: "SIZE", + SHARDWIDTH: "SHARDWIDTH", + SHOW: "SHOW", SPAN: "SPAN", TABLE: "TABLE", + TABLES: "TABLES", TEMP: "TEMP", THEN: "THEN", TIES: "TIES", + TIMEUNIT: "TIMEUNIT", + TIMEQUANTUM: "TIMEQUANTUM", TO: "TO", + TOP: "TOP", + TOPN: "TOPN", TRANSACTION: "TRANSACTION", TRIGGER: "TRIGGER", TRUTH: "TRUTH", + TTL: "TTL", UNBOUNDED: "UNBOUNDED", UNION: "UNION", UNIQUE: "UNIQUE", @@ -494,7 +519,7 @@ func (op Token) Precedence() int { return 2 case NOT: return 3 - case IS, MATCH, LIKE, GLOB, REGEXP, BETWEEN, IN, ISNULL, NOTNULL, NE, EQ: + case IS, MATCH, LIKE, GLOB, REGEXP, BETWEEN, IN, ISNOT, NE, EQ: return 4 case GT, LE, LT, GE: return 5 diff --git a/sql3/parser/token_test.go b/sql3/parser/token_test.go new file mode 100644 index 000000000..137f633c9 --- /dev/null +++ b/sql3/parser/token_test.go @@ -0,0 +1,14 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package parser_test + +import ( + "testing" + + "github.com/molecula/featurebase/v3/sql3/parser" +) + +func TestPos_String(t *testing.T) { + if got, want := (parser.Pos{}).String(), `-`; got != want { + t.Fatalf("String()=%q, want %q", got, want) + } +} diff --git a/sql2/walk.go b/sql3/parser/walk.go similarity index 95% rename from sql2/walk.go rename to sql3/parser/walk.go index 77e2de1b0..2104bdcf1 100644 --- a/sql2/walk.go +++ b/sql3/parser/walk.go @@ -1,6 +1,5 @@ -// Copyright 2022 Molecula Corp. (DBA FeatureBase). -// SPDX-License-Identifier: Apache-2.0 -package sql2 +// Copyright 2021 Molecula Corp. All rights reserved. +package parser // A Visitor's Visit method is invoked for each node encountered by Walk. // If the result visitor w is not nil, Walk visits each of the children @@ -87,10 +86,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { if err := walkIdent(v, &n.Name); err != nil { return node, err } - if err := walkIdent(v, &n.NewName); err != nil { - return node, err - } - if err := walkIdent(v, &n.ColumnName); err != nil { + if err := walkIdent(v, &n.OldColumnName); err != nil { return node, err } if err := walkIdent(v, &n.NewColumnName); err != nil { @@ -105,6 +101,9 @@ func walk(v Visitor, node Node) (_ Node, err error) { n.ColumnDef = nil } } + if err := walkIdent(v, &n.DropColumnName); err != nil { + return node, err + } case *AnalyzeStatement: if err := walkIdent(v, &n.Name); err != nil { @@ -195,7 +194,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { n.WithClause = nil } } - for i := range n.ValueLists { + /*for i := range n.ValueLists { if list, err := walk(v, n.ValueLists[i]); err != nil { return node, err } else if list != nil { @@ -203,7 +202,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { } else { n.ValueLists[i] = nil } - } + }*/ for i := range n.Columns { if col, err := walk(v, n.Columns[i]); err != nil { return node, err @@ -256,15 +255,9 @@ func walk(v Visitor, node Node) (_ Node, err error) { n.OrderingTerms[i] = nil } } - if err := walkExpr(v, &n.LimitExpr); err != nil { - return node, err - } - if err := walkExpr(v, &n.OffsetExpr); err != nil { - return node, err - } case *InsertStatement: - if n.WithClause != nil { + /*if n.WithClause != nil { if clause, err := walk(v, n.WithClause); err != nil { return node, err } else if clause != nil { @@ -272,7 +265,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { } else { n.WithClause = nil } - } + }*/ if err := walkIdent(v, &n.Table); err != nil { return node, err } @@ -282,16 +275,16 @@ func walk(v Visitor, node Node) (_ Node, err error) { if err := walkIdentList(v, n.Columns); err != nil { return node, err } - for i := range n.ValueLists { - if list, err := walk(v, n.ValueLists[i]); err != nil { - return node, err - } else if list != nil { - n.ValueLists[i] = list.(*ExprList) - } else { - n.ValueLists[i] = nil - } + //for i := range n.ValueLists { + if list, err := walk(v, n.ValueList); err != nil { + return node, err + } else if list != nil { + n.ValueList = list.(*ExprList) + } else { + n.ValueList = nil } - if n.Select != nil { + //} + /*if n.Select != nil { if sel, err := walk(v, n.Select); err != nil { return node, err } else if sel != nil { @@ -299,8 +292,8 @@ func walk(v Visitor, node Node) (_ Node, err error) { } else { n.Select = nil } - } - if n.UpsertClause != nil { + }*/ + /*if n.UpsertClause != nil { if clause, err := walk(v, n.UpsertClause); err != nil { return node, err } else if clause != nil { @@ -308,7 +301,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { } else { n.UpsertClause = nil } - } + }*/ case *UpdateStatement: if n.WithClause != nil { @@ -596,17 +589,6 @@ func walk(v Visitor, node Node) (_ Node, err error) { return node, err } - case *Raise: - if n.Error != nil { - if e, err := walk(v, n.Error); err != nil { - return node, err - } else if e != nil { - n.Error = e.(*StringLit) - } else { - n.Error = nil - } - } - case *Exists: if n.Select != nil { if sel, err := walk(v, n.Select); err != nil { @@ -769,7 +751,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { if p, err := walk(v, n.Precision); err != nil { return node, err } else if p != nil { - n.Precision = p.(*NumberLit) + n.Precision = p.(*IntegerLit) } else { n.Precision = nil } @@ -778,7 +760,7 @@ func walk(v Visitor, node Node) (_ Node, err error) { if scale, err := walk(v, n.Scale); err != nil { return node, err } else if scale != nil { - n.Scale = scale.(*NumberLit) + n.Scale = scale.(*IntegerLit) } else { n.Scale = nil } diff --git a/sql3/planner/compilealtertable.go b/sql3/planner/compilealtertable.go new file mode 100644 index 000000000..17d6c9026 --- /dev/null +++ b/sql3/planner/compilealtertable.go @@ -0,0 +1,77 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "strings" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +type alterOperation int64 + +const ( + alterOpAdd alterOperation = iota + alterOpDrop + alterOpRename +) + +// compileAlterTableStatement compiles an ALTER TABLE statement into a +// PlanOperator. +func (p *ExecutionPlanner) compileAlterTableStatement(stmt *parser.AlterTableStatement) (_ types.PlanOperator, err error) { + tableName := parser.IdentName(stmt.Name) + if stmt.Drop.IsValid() { + columnName := parser.IdentName(stmt.DropColumnName) + return NewPlanOpQuery(NewPlanOpAlterTable(p, tableName, alterOpDrop, columnName, "", nil), p.sql), nil + } else if stmt.Add.IsValid() { + col := stmt.ColumnDef + columnName := parser.IdentName(col.Name) + column, err := p.compileColumn(col) + if err != nil { + return nil, err + } + return NewPlanOpQuery(NewPlanOpAlterTable(p, tableName, alterOpAdd, "", columnName, column), p.sql), nil + + } else if stmt.Rename.IsValid() { + oldColumnName := parser.IdentName(stmt.OldColumnName) + newColumnName := parser.IdentName(stmt.NewColumnName) + return NewPlanOpQuery(NewPlanOpAlterTable(p, tableName, alterOpRename, oldColumnName, newColumnName, nil), p.sql), nil + } else { + return nil, sql3.NewErrInternal("unhandled alter operation") + } +} + +// analyzeAlterTableStatement analyze an ALTER TABLE statement and returns an +// error if anything is invalid. +func (p *ExecutionPlanner) analyzeAlterTableStatement(stmt *parser.AlterTableStatement) error { + if stmt.Drop.IsValid() { + //no checks for now + } else if stmt.Add.IsValid() { + col := stmt.ColumnDef + columnName := parser.IdentName(col.Name) + typeName := parser.IdentName(col.Type.Name) + if !parser.IsValidTypeName(typeName) { + return sql3.NewErrUnknownType(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column, typeName) + } + + if strings.ToLower(columnName) == "_id" { + //not allowed to add an _id column after the fact + return sql3.NewErrTableIDColumnAlter(col.Name.NamePos.Line, col.Name.NamePos.Column) + } + + err := p.analyzeColumn(typeName, col) + if err != nil { + return err + } + } else if stmt.Rename.IsValid() { + //check the new and old are not the same + oldColumnName := parser.IdentName(stmt.OldColumnName) + newColumnName := parser.IdentName(stmt.NewColumnName) + if strings.EqualFold(oldColumnName, newColumnName) { + return sql3.NewErrDuplicateColumn(stmt.NewColumnName.NamePos.Line, stmt.NewColumnName.NamePos.Column, newColumnName) + } + } + return nil +} diff --git a/sql3/planner/compilebulkinsert.go b/sql3/planner/compilebulkinsert.go new file mode 100644 index 000000000..be384fd32 --- /dev/null +++ b/sql3/planner/compilebulkinsert.go @@ -0,0 +1,46 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +// compileBulkInsertStatement compiles a BULK INSERT statement into a +// PlanOperator. +func (p *ExecutionPlanner) compileBulkInsertStatement(stmt *parser.BulkInsertStatement) (_ types.PlanOperator, err error) { + tableName := parser.IdentName(stmt.Table) + + /*table*/ + _, err = p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return nil, sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) + } + return nil, err + } + + return NewPlanOpBulkInsert(p, tableName), nil +} + +// analyzeBulkInsertStatement analyzes a BULK INSERT statement and returns an +// error if anything is invalid. +func (p *ExecutionPlanner) analyzeBulkInsertStatement(stmt *parser.BulkInsertStatement) error { + //check referred to table exists + tableName := parser.IdentName(stmt.Table) + /*table*/ _, err := p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) + } + return err + } + + return nil +} diff --git a/sql3/planner/compilecreatetable.go b/sql3/planner/compilecreatetable.go new file mode 100644 index 000000000..23d038b86 --- /dev/null +++ b/sql3/planner/compilecreatetable.go @@ -0,0 +1,424 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "strconv" + "strings" + "time" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +type createTableField struct { + planner *ExecutionPlanner + name string + typeName string + fos []pilosa.FieldOption +} + +// compileCreateTableStatement compiles a CREATE TABLE statement into a +// PlanOperator. +func (p *ExecutionPlanner) compileCreateTableStatement(stmt *parser.CreateTableStatement) (_ types.PlanOperator, err error) { + tableName := parser.IdentName(stmt.Name) + failIfExists := !stmt.IfNotExists.IsValid() + + // apply table options + keyPartitions := 0 + for _, option := range stmt.Options { + switch o := option.(type) { + case *parser.KeyPartitionsOption: + e := o.Expr.(*parser.IntegerLit) + i, err := strconv.ParseInt(e.Value, 10, 64) + if err != nil { + return nil, err + } + keyPartitions = int(i) + } + } + + isKeyed := false + + var columns = []*createTableField{} + for _, col := range stmt.Columns { + columnName := parser.IdentName(col.Name) + typeName := parser.IdentName(col.Type.Name) + + if strings.ToLower(columnName) == "_id" { + if strings.EqualFold(typeName, parser.FieldTypeString) { + isKeyed = true + } + continue + } + + column, err := p.compileColumn(col) + if err != nil { + return nil, err + } + + columns = append(columns, column) + } + return NewPlanOpQuery(NewPlanOpCreateTable(p, tableName, failIfExists, isKeyed, keyPartitions, columns), p.sql), nil +} + +// compiles a column def +func (p *ExecutionPlanner) compileColumn(col *parser.ColumnDefinition) (*createTableField, error) { + var err error + columnName := parser.IdentName(col.Name) + typeName := parser.IdentName(col.Type.Name) + + column := &createTableField{ + planner: p, + name: columnName, + typeName: typeName, + } + // Possible FieldOptions. We define these here, but the contraints below + // can set them to the values provided in the create table statement. + // And finally, the correct pilosa.FieldOption functional option will be + // created after that. + var cacheType string = pilosa.DefaultCacheType + var cacheSize uint32 = pilosa.DefaultCacheSize + var scale int64 + min, max := pql.MinMax(0) + var epoch = pilosa.DefaultEpoch + var timeUnit string = pilosa.TimeUnitSeconds + var timeQuantum pilosa.TimeQuantum + var ttl = "0" + + for _, con := range col.Constraints { + switch c := con.(type) { + case *parser.CacheTypeConstraint: + cacheType = c.CacheTypeValue + + if c.Size.IsValid() { + e := c.SizeExpr.(*parser.IntegerLit) + i, err := strconv.ParseInt(e.Value, 10, 64) + if err != nil { + return nil, err + } + cacheSize = uint32(i) + } + + case *parser.MinConstraint: + var val string + switch e := c.Expr.(type) { + case *parser.IntegerLit: + val = e.Value + + case *parser.UnaryExpr: + // Call analyzeUnaryExpression() in order to set the value on + // expr.ResultDataType so that we can rely on the DataType() + // method. There is a case where a BITNOT expression could get + // through, but that value as a string will fail in + // strconv.ParseInt() conversion. + if _, err := p.analyzeUnaryExpression(e, nil); err != nil { + return nil, err + } + + if e.IsLiteral() && typeIsInteger(e.DataType()) { + val = e.String() + } + } + + i, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, err + } + min = pql.NewDecimal(i, 0) + + case *parser.MaxConstraint: + var val string + switch e := c.Expr.(type) { + case *parser.IntegerLit: + val = e.Value + + case *parser.UnaryExpr: + // Call analyzeUnaryExpression() in order to set the value on + // expr.ResultDataType so that we can rely on the DataType() + // method. There is a case where a BITNOT expression could get + // through, but that value as a string will fail in + // strconv.ParseInt() conversion. + if _, err := p.analyzeUnaryExpression(e, nil); err != nil { + return nil, err + } + + if e.IsLiteral() && typeIsInteger(e.DataType()) { + val = e.String() + } + } + + i, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, err + } + max = pql.NewDecimal(i, 0) + + case *parser.TimeUnitConstraint: + unit := c.Expr.(*parser.StringLit) + timeUnit = unit.Value + + epochString := c.EpochExpr.(*parser.StringLit) + tm, err := time.ParseInLocation(time.RFC3339, epochString.Value, time.UTC) + if err != nil { + return nil, sql3.NewErrInvalidTimeEpoch(c.EpochExpr.Pos().Line, c.EpochExpr.Pos().Line, epochString.Value) + } + epoch = tm + + case *parser.TimeQuantumConstraint: + unit := c.Expr.(*parser.StringLit) + timeQuantum = pilosa.TimeQuantum(unit.Value) + if c.TtlExpr != nil { + e := c.TtlExpr.(*parser.StringLit) + ttl = e.Value + } + + default: + return nil, sql3.NewErrInternalf("unhandled column constraint type '%T'", c) + } + } + + switch strings.ToUpper(typeName) { + case parser.FieldTypeBool: + column.fos = append(column.fos, pilosa.OptFieldTypeBool()) + case parser.FieldTypeDecimal: + // Get the scale value. + scale, err = strconv.ParseInt(col.Type.Scale.Value, 10, 64) + if err != nil { + return nil, err + } + + // Adjust min/max to fit within the scaled min/max. + scaledMin, scaledMax := pql.MinMax(scale) + if scaledMax.LessThan(max) { + max = scaledMax + } + if scaledMin.GreaterThan(min) { + min = scaledMin + } + + column.fos = append(column.fos, pilosa.OptFieldTypeDecimal(scale, min, max)) + case parser.FieldTypeID: + column.fos = append(column.fos, pilosa.OptFieldTypeMutex(cacheType, cacheSize)) + case parser.FieldTypeIDSet: + if timeQuantum != "" { + column.fos = append(column.fos, pilosa.OptFieldTypeTime(timeQuantum, ttl)) + } else { + column.fos = append(column.fos, pilosa.OptFieldTypeSet(cacheType, cacheSize)) + } + case parser.FieldTypeInt: + column.fos = append(column.fos, pilosa.OptFieldTypeInt(min.ToInt64(0), max.ToInt64(0))) + case parser.FieldTypeString: + column.fos = append(column.fos, pilosa.OptFieldTypeMutex(cacheType, cacheSize)) + column.fos = append(column.fos, pilosa.OptFieldKeys()) + case parser.FieldTypeStringSet: + if timeQuantum != "" { + column.fos = append(column.fos, pilosa.OptFieldTypeTime(timeQuantum, ttl)) + } else { + column.fos = append(column.fos, pilosa.OptFieldTypeSet(cacheType, cacheSize)) + } + column.fos = append(column.fos, pilosa.OptFieldKeys()) + case parser.FieldTypeTimestamp: + column.fos = append(column.fos, pilosa.OptFieldTypeTimestamp(epoch, timeUnit)) + } + return column, nil +} + +// analyzeCreateTableStatement analyzes a CREATE TABLE statement and returns an +// error if anything is invalid. +func (p *ExecutionPlanner) analyzeCreateTableStatement(stmt *parser.CreateTableStatement) error { + //iterate columns, check types, check constraints, ensure we have no dupe names and make sure there is an _id column + checkedColumns := make(map[string]string) + for _, col := range stmt.Columns { + columnName := parser.IdentName(col.Name) + _, ok := checkedColumns[strings.ToLower(columnName)] + if ok { + return sql3.NewErrDuplicateColumn(col.Name.NamePos.Line, col.Name.NamePos.Column, columnName) + } + + typeName := parser.IdentName(col.Type.Name) + if !parser.IsValidTypeName(typeName) { + return sql3.NewErrUnknownType(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column, typeName) + } + + if strings.ToLower(columnName) == "_id" { + //check the type + if !(strings.EqualFold(typeName, parser.FieldTypeID) || strings.EqualFold(typeName, parser.FieldTypeString)) { + return sql3.NewErrTableIDColumnType(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column) + } + //make sure we have no constraints + if len(col.Constraints) > 0 { + return sql3.NewErrTableIDColumnConstraints(col.Type.Name.NamePos.Line, col.Type.Name.NamePos.Column) + } + } + checkedColumns[columnName] = strings.ToLower(columnName) + + err := p.analyzeColumn(typeName, col) + if err != nil { + return err + } + } + _, ok := checkedColumns["_id"] + if !ok { + return sql3.NewErrTableMustHaveIDColumn(stmt.Create.Line, stmt.Create.Column) + } + //check table options + for _, option := range stmt.Options { + + switch o := option.(type) { + case *parser.KeyPartitionsOption: + //check the type of the expression + literal, ok := o.Expr.(*parser.IntegerLit) + if !ok { + return sql3.NewErrIntegerLiteral(o.Expr.Pos().Line, o.Expr.Pos().Column) + } + //key partittions needs to be >=1 and we'll cap conservatively at 10000 + i, err := strconv.ParseInt(literal.Value, 10, 64) + if err != nil { + return err + } + if i < 1 || i > 10000 { + return sql3.NewErrInvalidKeyPartitionsValue(o.Expr.Pos().Line, o.Expr.Pos().Column, i) + } + + case *parser.ShardWidthOption: + //check the type of the expression + literal, ok := o.Expr.(*parser.IntegerLit) + if !ok { + return sql3.NewErrIntegerLiteral(o.Expr.Pos().Line, o.Expr.Pos().Column) + } + //shardwidth needs to be a power of 2 and > 2^16 + i, err := strconv.ParseInt(literal.Value, 10, 64) + if err != nil { + return err + } + isPwrOf2 := (i & (i - 1)) == 0 + if (i == 0) || !isPwrOf2 || i < (1<<16) { + return sql3.NewErrInvalidShardWidthValue(o.Expr.Pos().Line, o.Expr.Pos().Column, i) + } + + default: + return sql3.NewErrInternalf("unhandled table option type '%T'", option) + } + } + + return nil +} + +// analyze the column def for a CREATE or ALTER TABLE +func (p *ExecutionPlanner) analyzeColumn(typeName string, col *parser.ColumnDefinition) error { + // handledConstraints keeps track of the constraints which have been + // analyzed in the for loop below. This allows us to verify that two + // different, incompatible constraints aren't included. For now, that + // really only applies to TIMEQUANTUM and CACHETYPE. The other + // constraints which may be incompatible are checked against the field + // type. It may make sense, in the future, to add some logic which + // analyzes all the constraints in a more flexible way, but this + // addresses the immediate issue. + handledConstraints := make(map[parser.Token]struct{}) + + for _, con := range col.Constraints { + switch c := con.(type) { + case *parser.CacheTypeConstraint: + //make sure we have a set or mutex type + if !(strings.EqualFold(typeName, parser.FieldTypeString) || strings.EqualFold(typeName, parser.FieldTypeStringSet) || strings.EqualFold(typeName, parser.FieldTypeID) || strings.EqualFold(typeName, parser.FieldTypeIDSet)) { + return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "CACHETYPE", typeName) + } + //check the type of the expression for SIZE + if c.Size.IsValid() { + if _, ok := c.SizeExpr.(*parser.IntegerLit); !ok { + return sql3.NewErrIntegerLiteral(c.SizeExpr.Pos().Line, c.SizeExpr.Pos().Column) + } + } + // Make sure a TIMEQUANTUM constraint (which is incompatible with + // CACHETYPE) hasn't been specified. + if _, ok := handledConstraints[parser.TIMEQUANTUM]; ok { + return sql3.NewErrConflictingColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, parser.CACHETYPE, parser.TIMEQUANTUM) + } + handledConstraints[parser.CACHETYPE] = struct{}{} + + case *parser.MinConstraint: + // Make sure we have either an integer or unary type. + switch c.Expr.(type) { + case *parser.IntegerLit, *parser.UnaryExpr: + // pass + default: + return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "MIN", typeName) + } + handledConstraints[parser.MIN] = struct{}{} + + case *parser.MaxConstraint: + // Make sure we have either an integer or unary type. + switch c.Expr.(type) { + case *parser.IntegerLit, *parser.UnaryExpr: + // pass + default: + return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "MAX", typeName) + } + handledConstraints[parser.MAX] = struct{}{} + + case *parser.TimeUnitConstraint: + //make sure we have an timestamp type + if !strings.EqualFold(typeName, parser.FieldTypeTimestamp) { + return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "TIMEUNIT", typeName) + } + //check the type of the expression + unit, ok := c.Expr.(*parser.StringLit) + if !ok { + return sql3.NewErrStringLiteral(c.Expr.Pos().Line, c.Expr.Pos().Column) + } + if !pilosa.IsValidTimeUnit(unit.Value) { + return sql3.NewErrInvalidTimeUnit(c.Expr.Pos().Line, c.Expr.Pos().Column, unit.Value) + } + if c.EpochExpr != nil { + //check the type of the expression + _, ok := c.EpochExpr.(*parser.StringLit) + if !ok { + return sql3.NewErrStringLiteral(c.EpochExpr.Pos().Line, c.EpochExpr.Pos().Column) + } + } + handledConstraints[parser.TIMEUNIT] = struct{}{} + + case *parser.TimeQuantumConstraint: + //make sure we have a set type + if !(strings.EqualFold(typeName, parser.FieldTypeStringSet) || strings.EqualFold(typeName, parser.FieldTypeIDSet)) { + return sql3.NewErrBadColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, "TIMEQUANTUM", typeName) + } + //check the type of the expression + unit, ok := c.Expr.(*parser.StringLit) + if !ok { + return sql3.NewErrStringLiteral(c.Expr.Pos().Line, c.Expr.Pos().Column) + } + quantum := pilosa.TimeQuantum(strings.ToUpper(unit.Value)) + if !quantum.Valid() { + return sql3.NewErrInvalidTimeQuantum(c.Expr.Pos().Line, c.Expr.Pos().Column, unit.Value) + } + if c.TtlExpr != nil { + //check the type of the expression + ttl, ok := c.TtlExpr.(*parser.StringLit) + if !ok { + return sql3.NewErrStringLiteral(c.Expr.Pos().Line, c.Expr.Pos().Column) + } + _, err := time.ParseDuration(ttl.Value) + if err != nil { + return sql3.NewErrInvalidDuration(c.Expr.Pos().Line, c.Expr.Pos().Column, ttl.Value) + } + } + + // Make sure a CACHETYPE constraint (which is incompatible with + // TIMEQUANTUM) hasn't been specified. + if _, ok := handledConstraints[parser.CACHETYPE]; ok { + return sql3.NewErrConflictingColumnConstraint(col.Name.NamePos.Line, col.Name.NamePos.Column, parser.CACHETYPE, parser.TIMEQUANTUM) + } + + handledConstraints[parser.TIMEQUANTUM] = struct{}{} + + default: + return sql3.NewErrInternalf("unhandled column constraint type '%T'", c) + } + } + return nil +} diff --git a/sql3/planner/compiledroptable.go b/sql3/planner/compiledroptable.go new file mode 100644 index 000000000..1d3a9ad27 --- /dev/null +++ b/sql3/planner/compiledroptable.go @@ -0,0 +1,27 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +// compileDropTableStatement compiles a DROP TABLE statement into a +// PlanOperator. +func (p *ExecutionPlanner) compileDropTableStatement(stmt *parser.DropTableStatement) (_ types.PlanOperator, err error) { + tableName := parser.IdentName(stmt.Name) + index, err := p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return nil, sql3.NewErrTableNotFound(stmt.Name.NamePos.Line, stmt.Name.NamePos.Column, tableName) + } + return nil, err + } + return NewPlanOpQuery(NewPlanOpDropTable(p, index), p.sql), nil +} diff --git a/sql3/planner/compileinsert.go b/sql3/planner/compileinsert.go new file mode 100644 index 000000000..85c5d0fb2 --- /dev/null +++ b/sql3/planner/compileinsert.go @@ -0,0 +1,181 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "strings" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +// compileInsertStatement compiles an INSERT statement into a PlanOperator. +func (p *ExecutionPlanner) compileInsertStatement(stmt *parser.InsertStatement) (_ types.PlanOperator, err error) { + tableName := parser.IdentName(stmt.Table) + + targetColumns := []*qualifiedRefPlanExpression{} + insertValues := []types.PlanExpression{} + + table, err := p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return nil, sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) + } + return nil, err + } + + if len(stmt.Columns) > 0 { + for _, columnIdent := range stmt.Columns { + colName := parser.IdentName(columnIdent) + + if strings.EqualFold(colName, "_id") { + targetColumns = append(targetColumns, newQualifiedRefPlanExpression(tableName, colName, 0, parser.NewDataTypeID())) + continue + } + + for idx, field := range table.Fields { + if strings.EqualFold(colName, field.Name) { + targetColumns = append(targetColumns, newQualifiedRefPlanExpression(tableName, colName, idx, fieldSQLDataType(field))) + break + } + } + } + } else { + for idx, field := range table.Fields { + if strings.EqualFold("_exists", field.Name) { + continue + } + targetColumns = append(targetColumns, newQualifiedRefPlanExpression(tableName, field.Name, idx, fieldSQLDataType(field))) + } + } + + //add expressions from values list + for _, expr := range stmt.ValueList.Exprs { + e, err := p.compileExpr(expr) + if err != nil { + return nil, err + } + insertValues = append(insertValues, e) + } + + return NewPlanOpInsert(p, tableName, targetColumns, insertValues), nil +} + +// analyzeInsertStatement analyzes an INSERT statement and returns and error if +// anything is invalid. +func (p *ExecutionPlanner) analyzeInsertStatement(stmt *parser.InsertStatement) error { + // Check that referred table exists. + tableName := parser.IdentName(stmt.Table) + table, err := p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) + } + return err + } + + typeNames := make([]parser.ExprDataType, 0) + // If the insert statement does not provide the list of columns in which to + // insert the values, then the assumption is that the values apply to ALL + // fields in the table. + if len(stmt.Columns) == 0 { + // Generate the list of types from the FeatureBase index. + for _, field := range table.Fields { + if strings.EqualFold("_exists", field.Name) { + continue + } + typeNames = append(typeNames, fieldSQLDataType(field)) + } + // Make sure (implicit) insert list and expression list have the same + // number of items. + if len(typeNames) != len(stmt.ValueList.Exprs) { + return sql3.NewErrInsertExprTargetCountMismatch(stmt.ValueList.Lparen.Line, stmt.ValueList.Lparen.Column) + } + } else { + // Check column list refers to actual columns, and that there are no + // dupes. + columnNameMap := make(map[string]struct{}) + for _, columnIdent := range stmt.Columns { + colName := parser.IdentName(columnIdent) + var typeName parser.ExprDataType + + if strings.EqualFold(colName, "_id") { + columnNameMap["_id"] = struct{}{} + + // Determine, from the existing table, whether the _id is of + // type ID or STRING. + var idType parser.ExprDataType + if table.Options.Keys { + idType = parser.NewDataTypeString() + } else { + idType = parser.NewDataTypeID() + } + typeNames = append(typeNames, idType) + + continue + } + + // Find the column in the existing table. + columnFound := false + for _, field := range table.Fields { + if strings.EqualFold(colName, field.Name) { + typeName = fieldSQLDataType(field) + columnFound = true + break + } + } + if !columnFound { + return sql3.NewErrColumnNotFound(columnIdent.NamePos.Line, columnIdent.NamePos.Column, colName) + } + + // Ensure the column name hasn't already appeared in the list of + // columns. + if _, found := columnNameMap[colName]; found { + return sql3.NewErrDuplicateColumn(columnIdent.NamePos.Line, columnIdent.NamePos.Column, colName) + } + + typeNames = append(typeNames, typeName) + columnNameMap[colName] = struct{}{} + } + + // Ensure we have an _id column. + if _, ok := columnNameMap["_id"]; !ok { + return sql3.NewErrInsertMustHaveIDColumn(stmt.ColumnsLparen.Line, stmt.ColumnsLparen.Column) + } + + // Ensure we have at least one more than just the _id column. + if len(stmt.Columns) < 2 { + return sql3.NewErrInsertMustAtLeastOneNonIDColumn(stmt.ColumnsLparen.Line, stmt.ColumnsLparen.Column) + } + + // Make sure insert list and expression list have the same number of items. + if len(stmt.Columns) != len(stmt.ValueList.Exprs) { + return sql3.NewErrInsertExprTargetCountMismatch(stmt.ValueList.Lparen.Line, stmt.ValueList.Lparen.Column) + } + } + + // Check each of the expressions. + for i, expr := range stmt.ValueList.Exprs { + e, err := p.analyzeExpression(expr, stmt) + if err != nil { + return err + } + + if !e.IsLiteral() { + return sql3.NewErrLiteralExpected(expr.Pos().Line, expr.Pos().Column) + } + + // Type check against same ordinal position in column type list. + if !typesAreAssignmentCompatible(typeNames[i], e.DataType()) { + return sql3.NewErrTypeAssignmentIncompatible(expr.Pos().Line, expr.Pos().Column, e.DataType().TypeName(), typeNames[i].TypeName()) + } + + stmt.ValueList.Exprs[i] = e + } + + return nil +} diff --git a/sql3/planner/compileselect.go b/sql3/planner/compileselect.go new file mode 100644 index 000000000..2e2365eb4 --- /dev/null +++ b/sql3/planner/compileselect.go @@ -0,0 +1,376 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +// compileSelectStatment compiles a parser.SelectStatment AST into a PlanOperator +func (p *ExecutionPlanner) compileSelectStatement(stmt *parser.SelectStatement, isSubquery bool) (types.PlanOperator, error) { + query := NewPlanOpQuery(NewPlanOpNullTable(), p.sql) + //p.pushPlannerScope(query) + p.scopeStack.push(query) + + // handle select list + projections := make([]types.PlanExpression, 0) + for _, c := range stmt.Columns { + planExpr, err := p.compileExpr(c.Expr) + if err != nil { + return nil, errors.Wrap(err, "planning select column expression") + } + if c.Alias != nil { + planExpr = newAliasPlanExpression(c.Alias.Name, planExpr) + } + projections = append(projections, planExpr) + } + + // group by clause. + groupByExprs := make([]types.PlanExpression, 0) + for _, expr := range stmt.GroupByExprs { + switch expr := expr.(type) { + case *parser.QualifiedRef: + groupByExprs = append(groupByExprs, newQualifiedRefPlanExpression(expr.Table.Name, expr.Column.Name, expr.ColumnIndex, expr.DataType())) + default: + return nil, sql3.NewErrInternalf("unsupported expression type in GROUP BY clause: %T", expr) + } + } + var err error + + if stmt.Having.IsValid() { + query.AddWarning("HAVING is not yet supported") + } + + // handle distinct + if stmt.Distinct.IsValid() { + query.AddWarning("DISTINCT not yet implemented") + } + + // source expression last + source, err := p.compileSelectSource(query, stmt.WhereExpr, stmt.Source) + if err != nil { + return nil, err + } + + //do we have straight projection or a group by? + var compiledOp types.PlanOperator + if len(query.aggregates) > 0 { + compiledOp = NewPlanOpProjection(projections, NewPlanOpGroupBy(query.aggregates, groupByExprs, source)) + } else { + compiledOp = NewPlanOpProjection(projections, source) + } + + // handle order by + if len(stmt.OrderingTerms) > 0 { + orderByFields := make([]*OrderByExpression, 0) + for _, ot := range stmt.OrderingTerms { + otExpr, err := p.compileExpr(ot.X) + if err != nil { + return nil, err + } + f := &OrderByExpression{ + Expr: otExpr, + } + f.Order = orderByAsc + if ot.Desc.IsValid() { + f.Order = orderByDesc + } + orderByFields = append(orderByFields, f) + } + compiledOp = NewPlanOpOrderBy(orderByFields, compiledOp) + } + + //insert the top operator if it exists + if stmt.Top.IsValid() { + topExpr, err := p.compileExpr(stmt.TopExpr) + if err != nil { + return nil, err + } + compiledOp = NewPlanOpTop(topExpr, compiledOp) + } + + //pop the scope + //p.popPlannerScope() + _ = p.scopeStack.pop() + + //if it is a subquery, don't wrap in a PlanOpQuery + if isSubquery { + return compiledOp, nil + } + children := []types.PlanOperator{ + compiledOp, + } + return query.WithChildren(children...) +} + +func (p *ExecutionPlanner) compileSelectSource(scope *PlanOpQuery, whereExpr parser.Expr, source parser.Source) (types.PlanOperator, error) { + if source == nil { + return NewPlanOpNullTable(), nil + } + + switch sourceExpr := source.(type) { + case *parser.JoinClause: + topOp, err := p.compileSelectSource(scope, whereExpr, sourceExpr.X) + if err != nil { + return nil, err + } + bottomOp, err := p.compileSelectSource(scope, whereExpr, sourceExpr.Y) + if err != nil { + return nil, err + } + scope.AddWarning("🦖 here there be dragons! JOINS are experimental.") + if sourceExpr.Constraint == nil { + scope.AddWarning("⚠️ cartesian products are never a good idea - are you missing a join constraint?") + } + return NewPlanOpNestedLoops(topOp, bottomOp), nil + + case *parser.QualifiedTableName: + //get all the qualified refs that refer to this table + extractColumns := []types.PlanExpression{} + + for _, r := range scope.referenceList { + if sourceExpr.MatchesTablenameOrAlias(r.tableName) { + extractColumns = append(extractColumns, r) + } + } + + // handle the where clause + where, err := p.compileExpr(whereExpr) + if err != nil { + return nil, err + } + + //get for the table name + tableName := parser.IdentName(sourceExpr.Name) + + return NewPlanOpPQLTableScan(p, tableName, extractColumns, where), nil + + case *parser.ParenSource: + return p.compileSelectSource(scope, whereExpr, sourceExpr.X) + + case *parser.SelectStatement: + subQuery, err := p.compileSelectStatement(sourceExpr, true) + if err != nil { + return nil, err + } + return NewPlanOpSubquery(subQuery), nil + + default: + return nil, sql3.NewErrInternalf("unexpected source type: %T", source) + } +} + +func (p *ExecutionPlanner) analyzeSource(source parser.Source) error { + if source == nil { + return nil + } + switch source := source.(type) { + case *parser.JoinClause: + err := p.analyzeSource(source.X) + if err != nil { + return err + } + err = p.analyzeSource(source.Y) + if err != nil { + return err + } + return nil + + case *parser.ParenSource: + err := p.analyzeSource(source.X) + if err != nil { + return err + } + return nil + + case *parser.QualifiedTableName: + //check table exists + tableName := parser.IdentName(source.Name) + table, err := p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return sql3.NewErrTableNotFound(source.Name.NamePos.Line, source.Name.NamePos.Column, tableName) + } + return err + } + + // populate the output columns from the source + for idx, fld := range table.Fields { + soc := &parser.SourceOutputColumn{ + TableName: tableName, + ColumnName: fld.Name, + ColumnIndex: idx, + Datatype: fieldSQLDataType(fld), + } + source.OutputColumns = append(source.OutputColumns, soc) + } + + return nil + + case *parser.SelectStatement: + err := p.analyzeSelectStatement(source) + if err != nil { + return err + } + return nil + + default: + return sql3.NewErrInternalf("unexpected source type: %T", source) + } +} + +func (p *ExecutionPlanner) analyzeSelectStatement(stmt *parser.SelectStatement) error { + //analyze source first - needed for name resolution + err := p.analyzeSource(stmt.Source) + if err != nil { + return err + } + + if err := p.analyzeSelectStatementWildcards(stmt); err != nil { + return err + } + + for _, col := range stmt.Columns { + expr, err := p.analyzeExpression(col.Expr, stmt) + if err != nil { + return err + } + if expr != nil { + col.Expr = expr + } + } + + expr, err := p.analyzeExpression(stmt.TopExpr, stmt) + if err != nil { + return err + } + if expr != nil { + if !(expr.IsLiteral() && typeIsInteger(expr.DataType())) { + return sql3.NewErrIntegerLiteral(stmt.TopExpr.Pos().Line, stmt.TopExpr.Pos().Column) + } + stmt.TopExpr = expr + } + + expr, err = p.analyzeExpression(stmt.WhereExpr, stmt) + if err != nil { + return err + } + stmt.WhereExpr = expr + + for i, g := range stmt.GroupByExprs { + expr, err = p.analyzeExpression(g, stmt) + if err != nil { + return err + } + if expr != nil { + stmt.GroupByExprs[i] = expr + } + } + + expr, err = p.analyzeExpression(stmt.HavingExpr, stmt) + if err != nil { + return err + } + if expr != nil { + stmt.HavingExpr = expr + } + + for _, term := range stmt.OrderingTerms { + expr, err = p.analyzeExpression(term.X, stmt) + if err != nil { + return err + } + if expr != nil { + term.X = expr + } + } + + return nil +} + +func (p *ExecutionPlanner) analyzeSelectStatementWildcards(stmt *parser.SelectStatement) error { + if !stmt.HasWildcard() { + return nil + } + + // replace wildcards with column references + newColumns := make([]*parser.ResultColumn, 0, len(stmt.Columns)) + for _, col := range stmt.Columns { + + //handle the case of unqualified * + if col.Star.IsValid() { + + cols, err := p.columnsFromSource(stmt.Source) + if err != nil { + return err + } + newColumns = append(newColumns, cols...) + + } else { + //handle the case of a qualified ref with a * + if ref, ok := col.Expr.(*parser.QualifiedRef); ok && ref.Star.IsValid() { + refName := parser.IdentName(ref.Table) + src := stmt.Source.SourceFromAlias(refName) + if src == nil { + return sql3.NewErrTableNotFound(ref.Table.NamePos.Line, ref.Table.NamePos.Column, refName) + } + + cols, err := p.columnsFromSource(src) + if err != nil { + return err + } + newColumns = append(newColumns, cols...) + + } else { + //add the column as is... + newColumns = append(newColumns, col) + } + } + } + stmt.Columns = newColumns + + return nil +} + +func (p *ExecutionPlanner) columnsFromSource(source parser.Source) ([]*parser.ResultColumn, error) { + result := []*parser.ResultColumn{} + + switch src := source.(type) { + case *parser.JoinClause: + return nil, sql3.NewErrInternal("joins are not currently supported") + case *parser.ParenSource: + for _, oc := range src.PossibleOutputColumns() { + result = append(result, &parser.ResultColumn{ + Expr: &parser.QualifiedRef{ + Table: &parser.Ident{Name: oc.TableName}, + Column: &parser.Ident{Name: oc.ColumnName}, + ColumnIndex: oc.ColumnIndex, + }, + }) + } + return result, nil + + case *parser.QualifiedTableName: + for _, oc := range src.PossibleOutputColumns() { + result = append(result, &parser.ResultColumn{ + Expr: &parser.QualifiedRef{ + Table: &parser.Ident{Name: oc.TableName}, + Column: &parser.Ident{Name: oc.ColumnName}, + ColumnIndex: oc.ColumnIndex, + }, + }) + } + return result, nil + + case *parser.SelectStatement: + return nil, sql3.NewErrInternal("sub-selects are not currently supported") + default: + return nil, sql3.NewErrInternalf("unexpected source type: %T", source) + } +} diff --git a/sql3/planner/compileshow.go b/sql3/planner/compileshow.go new file mode 100644 index 000000000..821aae654 --- /dev/null +++ b/sql3/planner/compileshow.go @@ -0,0 +1,134 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +func (p *ExecutionPlanner) compileShowTablesStatement(stmt parser.Statement) (types.PlanOperator, error) { + indexInfo, err := p.schemaAPI.Schema(context.Background(), false) + if err != nil { + return nil, errors.Wrap(err, "getting schema") + } + + columns := []types.PlanExpression{&qualifiedRefPlanExpression{ + tableName: "fb$tables", + columnName: "name", + columnIndex: 0, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$tables", + columnName: "created_at", + columnIndex: 1, + dataType: parser.NewDataTypeTimestamp(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$tables", + columnName: "track_existence", + columnIndex: 2, + dataType: parser.NewDataTypeBool(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$tables", + columnName: "keys", + columnIndex: 3, + dataType: parser.NewDataTypeBool(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$tables", + columnName: "shard_width", + columnIndex: 4, + dataType: parser.NewDataTypeInt(), + }} + + return NewPlanOpQuery(NewPlanOpProjection(columns, NewPlanOpFeatureBaseTables(indexInfo)), p.sql), nil +} + +func (p *ExecutionPlanner) compileShowColumnsStatement(stmt *parser.ShowColumnsStatement) (_ types.PlanOperator, err error) { + tableName := parser.IdentName(stmt.TableName) + index, err := p.schemaAPI.IndexInfo(context.Background(), tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return nil, sql3.NewErrTableNotFound(stmt.TableName.NamePos.Line, stmt.TableName.NamePos.Column, tableName) + } + return nil, err + } + + columns := []types.PlanExpression{&qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "name", + columnIndex: 0, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ // the SQL3 data type description + tableName: "fb$table_columns", + columnName: "type", + columnIndex: 1, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ // the FeatureBase 'native' data type description + tableName: "fb$table_columns", + columnName: "internal_type", + columnIndex: 2, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "created_at", + columnIndex: 3, + dataType: parser.NewDataTypeTimestamp(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "keys", + columnIndex: 4, + dataType: parser.NewDataTypeBool(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "cache_type", + columnIndex: 5, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "cache_size", + columnIndex: 6, + dataType: parser.NewDataTypeInt(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "scale", + columnIndex: 7, + dataType: parser.NewDataTypeInt(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "min", + columnIndex: 8, + dataType: parser.NewDataTypeInt(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "max", + columnIndex: 9, + dataType: parser.NewDataTypeInt(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "timeunit", + columnIndex: 10, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "epoch", + columnIndex: 11, + dataType: parser.NewDataTypeInt(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "timequantum", + columnIndex: 12, + dataType: parser.NewDataTypeString(), + }, &qualifiedRefPlanExpression{ + tableName: "fb$table_columns", + columnName: "ttl", + columnIndex: 13, + dataType: parser.NewDataTypeString(), + }} + + return NewPlanOpQuery(NewPlanOpProjection(columns, NewPlanOpFeatureBaseColumns(index)), p.sql), nil +} diff --git a/sql3/planner/executionplanner.go b/sql3/planner/executionplanner.go new file mode 100644 index 000000000..63978095d --- /dev/null +++ b/sql3/planner/executionplanner.go @@ -0,0 +1,197 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "encoding/json" + "log" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlannerScope holds scope for the planner +// there is a stack of these in the ExecutionPlanner and some corresponding push/pop functions +// this allows us to do scoped operations without passing stuff down into +// every function +type PlannerScope struct { + scope types.PlanOperator +} + +// ExecutionPlanner compiles SQL text into a query plan +type ExecutionPlanner struct { + executor pilosa.Executor + schemaAPI pilosa.SchemaAPI + computeAPI pilosa.ComputeAPI + sql string + scopeStack *scopeStack +} + +func NewExecutionPlanner(executor pilosa.Executor, schemaAPI pilosa.SchemaAPI, computeAPI pilosa.ComputeAPI, sql string) *ExecutionPlanner { + return &ExecutionPlanner{ + executor: executor, + schemaAPI: schemaAPI, + computeAPI: computeAPI, + sql: sql, + scopeStack: newScopeStack(), + } +} + +// CompilePlan takes an AST (parser.Statement) and compiles into a query plan returning the root +// PlanOperator +// The act of compiling includes an analysis step that does semantic analysis of the AST, this includes +// type checking, and sometimes AST rewriting. The compile phase uses the type-checked and rewritten AST +// to produce a query plan. +func (p *ExecutionPlanner) CompilePlan(ctx context.Context, stmt parser.Statement) (types.PlanOperator, error) { + // call analyze first + err := p.analyzePlan(stmt) + if err != nil { + return nil, err + } + + var rootOperator types.PlanOperator + switch stmt := stmt.(type) { + case *parser.SelectStatement: + rootOperator, err = p.compileSelectStatement(stmt, false) + case *parser.ShowTablesStatement: + rootOperator, err = p.compileShowTablesStatement(stmt) + case *parser.ShowColumnsStatement: + rootOperator, err = p.compileShowColumnsStatement(stmt) + case *parser.CreateTableStatement: + rootOperator, err = p.compileCreateTableStatement(stmt) + case *parser.AlterTableStatement: + rootOperator, err = p.compileAlterTableStatement(stmt) + case *parser.DropTableStatement: + rootOperator, err = p.compileDropTableStatement(stmt) + case *parser.InsertStatement: + rootOperator, err = p.compileInsertStatement(stmt) + case *parser.BulkInsertStatement: + rootOperator, err = p.compileBulkInsertStatement(stmt) + default: + return nil, sql3.NewErrInternalf("cannot plan statement: %T", stmt) + } + + // Optimize the plan. + if err == nil { + rootOperator, err = p.optimizePlan(ctx, rootOperator) + } + + // Log the plan. This happens even if an error occurred. + if rootOperator != nil { + plan := rootOperator.Plan() + a, _ := json.MarshalIndent(plan, "", " ") + log.Println(string(a)) + } + + return rootOperator, err +} + +func (p *ExecutionPlanner) analyzePlan(stmt parser.Statement) error { + switch stmt := stmt.(type) { + case *parser.SelectStatement: + return p.analyzeSelectStatement(stmt) + case *parser.ShowTablesStatement: + return nil + case *parser.ShowColumnsStatement: + return nil + case *parser.CreateTableStatement: + return p.analyzeCreateTableStatement(stmt) + case *parser.AlterTableStatement: + return p.analyzeAlterTableStatement(stmt) + case *parser.DropTableStatement: + return nil + case *parser.InsertStatement: + return p.analyzeInsertStatement(stmt) + case *parser.BulkInsertStatement: + return p.analyzeBulkInsertStatement(stmt) + default: + return sql3.NewErrInternalf("cannot analyze statement: %T", stmt) + } +} + +type accessType byte + +const ( + accessTypeReadData accessType = iota + accessTypeWriteData + accessTypeCreateObject + accessTypeAlterObject + accessTypeDropObject +) + +func (p *ExecutionPlanner) checkAccess(ctx context.Context, objectName string, _ accessType) error { + return nil +} + +// convenience function that allows the planner to keep track of aggregates so we can +// use them during optimization +func (p *ExecutionPlanner) addAggregate(agg types.PlanExpression) error { + table := p.scopeStack.read() + if table == nil { + return sql3.NewErrInternalf("unexpected symbol table state") + } + + switch s := table.scope.(type) { + case *PlanOpQuery: + s.aggregates = append(s.aggregates, agg) + } + return nil +} + +// addReference is a convenience function that allows the planner to keep track +// of references so we can use them during optimization. +func (p *ExecutionPlanner) addReference(ref *qualifiedRefPlanExpression) error { + table := p.scopeStack.read() + if table == nil { + return sql3.NewErrInternalf("unexpected symbol table state") + } + + switch s := table.scope.(type) { + case *PlanOpQuery: + s.referenceList = append(s.referenceList, ref) + } + return nil +} + +// scopeStack is a stack of PlannerScope with the usual push/pop methods. +type scopeStack struct { + st []*PlannerScope +} + +// newScopeStack returns a scope stack initialized with zero elements on the +// stack. +func newScopeStack() *scopeStack { + return &scopeStack{ + st: make([]*PlannerScope, 0), + } +} + +// push adds the provided PlanOperator (as the scope of a PlannerScope) to the +// scope stack. +func (ss *scopeStack) push(scope types.PlanOperator) { + ss.st = append(ss.st, &PlannerScope{ + scope: scope, + }) +} + +// pop removes (and returns) the last scope pushed to the stack. +func (ss *scopeStack) pop() *PlannerScope { + if len(ss.st) == 0 { + return nil + } + ret := ss.st[len(ss.st)-1] + ss.st = ss.st[:len(ss.st)-1] + return ret +} + +// read returns the last scope pushed to the stack, but unlike pop, it does not +// remove it. +func (ss *scopeStack) read() *PlannerScope { + if len(ss.st) == 0 { + return nil + } + return ss.st[len(ss.st)-1] +} diff --git a/sql3/planner/executionplanner_test.go b/sql3/planner/executionplanner_test.go new file mode 100644 index 000000000..83d005bba --- /dev/null +++ b/sql3/planner/executionplanner_test.go @@ -0,0 +1,1561 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package planner_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3/parser" + planner_types "github.com/molecula/featurebase/v3/sql3/planner/types" + sql_test "github.com/molecula/featurebase/v3/sql3/test" + "github.com/molecula/featurebase/v3/test" + "github.com/stretchr/testify/assert" +) + +func TestPlanner_Show(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + index, err := c.GetHolder(0).CreateIndex("i", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := index.CreateField("f", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := index.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + index2, err := c.GetHolder(0).CreateIndex("i2", pilosa.IndexOptions{TrackExistence: false}) + if err != nil { + t.Fatal(err) + } + + if _, err := index2.CreateField("f", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := index2.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + t.Run("ShowTables", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SHOW TABLES`) + if err != nil { + t.Fatal(err) + } + if len(results) != 2 { + t.Fatal(fmt.Errorf("unexpected result set length")) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "name", Type: parser.NewDataTypeString()}, + {Name: "created_at", Type: parser.NewDataTypeTimestamp()}, + {Name: "track_existence", Type: parser.NewDataTypeBool()}, + {Name: "keys", Type: parser.NewDataTypeBool()}, + {Name: "shard_width", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ShowColumns", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SHOW COLUMNS FROM i`) + if err != nil { + t.Fatal(err) + } + if len(results) != 3 { + t.Fatal(fmt.Errorf("unexpected result set length: %d", len(results))) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "name", Type: parser.NewDataTypeString()}, + {Name: "type", Type: parser.NewDataTypeString()}, + {Name: "internal_type", Type: parser.NewDataTypeString()}, + {Name: "created_at", Type: parser.NewDataTypeTimestamp()}, + {Name: "keys", Type: parser.NewDataTypeBool()}, + {Name: "cache_type", Type: parser.NewDataTypeString()}, + {Name: "cache_size", Type: parser.NewDataTypeInt()}, + {Name: "scale", Type: parser.NewDataTypeInt()}, + {Name: "min", Type: parser.NewDataTypeInt()}, + {Name: "max", Type: parser.NewDataTypeInt()}, + {Name: "timeunit", Type: parser.NewDataTypeString()}, + {Name: "epoch", Type: parser.NewDataTypeInt()}, + {Name: "timequantum", Type: parser.NewDataTypeString()}, + {Name: "ttl", Type: parser.NewDataTypeString()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ShowColumns2", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SHOW COLUMNS FROM i2`) + if err != nil { + t.Fatal(err) + } + if len(results) != 3 { + t.Fatal(fmt.Errorf("unexpected result set length")) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "name", Type: parser.NewDataTypeString()}, + {Name: "type", Type: parser.NewDataTypeString()}, + {Name: "internal_type", Type: parser.NewDataTypeString()}, + {Name: "created_at", Type: parser.NewDataTypeTimestamp()}, + {Name: "keys", Type: parser.NewDataTypeBool()}, + {Name: "cache_type", Type: parser.NewDataTypeString()}, + {Name: "cache_size", Type: parser.NewDataTypeInt()}, + {Name: "scale", Type: parser.NewDataTypeInt()}, + {Name: "min", Type: parser.NewDataTypeInt()}, + {Name: "max", Type: parser.NewDataTypeInt()}, + {Name: "timeunit", Type: parser.NewDataTypeString()}, + {Name: "epoch", Type: parser.NewDataTypeInt()}, + {Name: "timequantum", Type: parser.NewDataTypeString()}, + {Name: "ttl", Type: parser.NewDataTypeString()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ShowColumnsFromNotATable", func(t *testing.T) { + _, _, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SHOW COLUMNS FROM foo`) + if err != nil { + if err.Error() != "[1:19] table 'foo' not found" { + t.Fatal(err) + } + } + }) +} +func TestPlanner_CoverCreateTable(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + server := c.GetNode(0).Server + + t.Run("Invalid", func(t *testing.T) { + tableName := "invalidfieldcontraints" + + fields := []struct { + name string + typ string + constraints string + expErr string + }{ + { + name: "stringsetcolq", + typ: "stringset", + constraints: "cachetype lru size 1000 timequantum 'YMD' ttl '24h'", + expErr: "[1:60] 'CACHETYPE' constraint conflicts with 'TIMEQUANTUM'", + }, + { + name: "stringsetcolq", + typ: "stringset", + constraints: "timequantum 'YMD' ttl '24h' cachetype ranked", + expErr: "[1:60] 'CACHETYPE' constraint conflicts with 'TIMEQUANTUM'", + }, + } + + for i, fld := range fields { + if fld.name == "" { + t.Fatalf("field name at slice index %d is blank", i) + } + if fld.typ == "" { + t.Fatalf("field type at slice index %d is blank", i) + } + + // Build the create table statement based on the fields slice above. + sql := "create table " + tableName + "_" + fld.name + " (_id id, " + sql += fld.name + " " + fld.typ + " " + fld.constraints + sql += `) keypartitions 12 shardwidth 1024` + + // Run the create table statement. + _, _, err := sql_test.MustQueryRows(t, server, sql) + if assert.Error(t, err) { + assert.Equal(t, fld.expErr, err.Error()) + //sql3.SQLErrConflictingColumnConstraint.Message + } + } + }) + + t.Run("Valid", func(t *testing.T) { + tableName := "validfieldcontraints" + + fields := []struct { + name string + typ string + constraints string + expOptions pilosa.FieldOptions + }{ + { + name: "_id", + typ: "id", + }, + { + name: "intcol", + typ: "int", + constraints: "min 100 max 10000", + expOptions: pilosa.FieldOptions{ + Type: "int", + Base: 100, + Min: pql.NewDecimal(100, 0), + Max: pql.NewDecimal(10000, 0), + }, + }, + { + name: "boolcol", + typ: "bool", + constraints: "", + expOptions: pilosa.FieldOptions{ + Type: "bool", + }, + }, + { + name: "timestampcol", + typ: "timestamp", + constraints: "timeunit 'ms' epoch '2021-01-01T00:00:00Z'", + expOptions: pilosa.FieldOptions{ + Base: 1609459200000, + Type: "timestamp", + TimeUnit: "ms", + Min: pql.NewDecimal(-63745055999000, 0), + Max: pql.NewDecimal(251792841599000, 0), + }, + }, + { + name: "decimalcol", + typ: "decimal(2)", + constraints: "", + expOptions: pilosa.FieldOptions{ + Type: "decimal", + Scale: 2, + Min: pql.NewDecimal(-9223372036854775808, 2), + Max: pql.NewDecimal(9223372036854775807, 2), + }, + }, + { + name: "stringcol", + typ: "string", + constraints: "cachetype ranked size 1000", + expOptions: pilosa.FieldOptions{ + Type: "mutex", + Keys: true, + CacheType: "ranked", + CacheSize: 1000, + }, + }, + { + name: "stringsetcol", + typ: "stringset", + constraints: "cachetype lru size 1000", + expOptions: pilosa.FieldOptions{ + Type: "set", + Keys: true, + CacheType: "lru", + CacheSize: 1000, + }, + }, + { + name: "stringsetcolq", + typ: "stringset", + constraints: "timequantum 'YMD' ttl '24h'", + expOptions: pilosa.FieldOptions{ + Type: "time", + Keys: true, + CacheType: "", + CacheSize: 0, + TimeQuantum: "YMD", + TTL: time.Duration(24 * time.Hour), + }, + }, + { + name: "idcol", + typ: "id", + constraints: "cachetype ranked size 1000", + expOptions: pilosa.FieldOptions{ + Type: "mutex", + Keys: false, + CacheType: "ranked", + CacheSize: 1000, + }, + }, + { + name: "idsetcol", + typ: "idset", + constraints: "cachetype lru", + expOptions: pilosa.FieldOptions{ + Type: "set", + Keys: false, + CacheType: "lru", + CacheSize: pilosa.DefaultCacheSize, + }, + }, + { + name: "idsetcolsz", + typ: "idset", + constraints: "cachetype lru size 1000", + expOptions: pilosa.FieldOptions{ + Type: "set", + Keys: false, + CacheType: "lru", + CacheSize: 1000, + }, + }, + { + name: "idsetcolq", + typ: "idset", + constraints: "timequantum 'YMD' ttl '24h'", + expOptions: pilosa.FieldOptions{ + Type: "time", + Keys: false, + CacheType: "", + CacheSize: 0, + TimeQuantum: "YMD", + TTL: time.Duration(24 * time.Hour), + }, + }, + } + + // Build the create table statement based on the fields slice above. + sql := "create table " + tableName + " (" + fieldDefs := make([]string, len(fields)) + for i, fld := range fields { + if fld.name == "" { + t.Fatalf("field name at slice index %d is blank", i) + } + if fld.typ == "" { + t.Fatalf("field type at slice index %d is blank", i) + } + fieldDefs[i] = fld.name + " " + fld.typ + if fld.constraints != "" { + fieldDefs[i] += " " + fld.constraints + } + } + sql += strings.Join(fieldDefs, ", ") + sql += `) keypartitions 12 shardwidth 65536` + + // Run the create table statement. + results, columns, err := sql_test.MustQueryRows(t, server, sql) + assert.NoError(t, err) + assert.Equal(t, [][]interface{}{}, results) + assert.Equal(t, []*planner_types.PlannerColumn{}, columns) + + // Ensure that the fields got created as expected. + t.Run("EnsureFields", func(t *testing.T) { + api := c.GetNode(0).API + ctx := context.Background() + + schema, err := api.Schema(ctx, false) + assert.NoError(t, err) + //spew.Dump(schema) + + // Get the fields from the FeatureBase schema. + // fbFields is a map of fieldName to FieldInfo. + var fbFields map[string]*pilosa.FieldInfo + var tableKeys bool + for _, idx := range schema { + if idx.Name == tableName { + tableKeys = idx.Options.Keys + fbFields = make(map[string]*pilosa.FieldInfo) + for _, fld := range idx.Fields { + fbFields[fld.Name] = fld + } + } + } + assert.NotNil(t, fbFields) + + // Ensure the schema field options match the expected options. + for _, fld := range fields { + t.Run(fmt.Sprintf("Field:%s", fld.name), func(t *testing.T) { + // Field `_id` isn't returned from FeatureBase in the schema, + // but we do want to validate that its type is used to determine + // whether or not the table is keyed. + if fld.name == "_id" { + switch fld.typ { + case "id": + assert.False(t, tableKeys) + case "string": + assert.True(t, tableKeys) + default: + t.Fatalf("invalid _id type: %s", fld.typ) + } + return + } + + fbField, ok := fbFields[fld.name] + assert.True(t, ok) + assert.Equal(t, fld.expOptions, fbField.Options) + }) + } + }) + }) +} + +func TestPlanner_CreateTable(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + server := c.GetNode(0).Server + + t.Run("CreateTableAllDataTypes", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, server, `create table allcoltypes ( + _id id, + intcol int, + boolcol bool, + timestampcol timestamp, + decimalcol decimal(2), + stringcol string, + stringsetcol stringset, + idcol id, + idsetcol idset) keypartitions 12 shardwidth 65536`) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([][]interface{}{}, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{}, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("CreateTableAllDataTypesAgain", func(t *testing.T) { + _, _, err := sql_test.MustQueryRows(t, server, `create table allcoltypes ( + _id id, + intcol int, + boolcol bool, + timestampcol timestamp, + decimalcol decimal(2), + stringcol string, + stringsetcol stringset, + idcol id, + idsetcol idset) keypartitions 12 shardwidth 65536`) + if err == nil { + t.Fatal("expected error") + } else { + if err.Error() != "creating index: index already exists" { + t.Fatal(err) + } + } + }) + + t.Run("DropTable1", func(t *testing.T) { + _, _, err := sql_test.MustQueryRows(t, server, `drop table allcoltypes`) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("CreateTableAllDataTypesAllConstraints", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, server, `create table allcoltypes ( + _id id, + intcol int min 0 max 10000, + boolcol bool, + timestampcol timestamp timeunit 'ms' epoch '2010-01-01T00:00:00Z', + decimalcol decimal(2), + stringcol string cachetype ranked size 1000, + stringsetcol stringset cachetype lru size 1000, + stringsetcolq stringset timequantum 'YMD' ttl '24h', + idcol id cachetype ranked size 1000, + idsetcol idset cachetype lru, + idsetcolsz idset cachetype lru size 1000, + idsetcolq idset timequantum 'YMD' ttl '24h') keypartitions 12 shardwidth 65536`) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([][]interface{}{}, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{}, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ShowColumns1", func(t *testing.T) { + _, columns, err := sql_test.MustQueryRows(t, server, `SHOW COLUMNS FROM allcoltypes`) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "name", Type: parser.NewDataTypeString()}, + {Name: "type", Type: parser.NewDataTypeString()}, + {Name: "internal_type", Type: parser.NewDataTypeString()}, + {Name: "created_at", Type: parser.NewDataTypeTimestamp()}, + {Name: "keys", Type: parser.NewDataTypeBool()}, + {Name: "cache_type", Type: parser.NewDataTypeString()}, + {Name: "cache_size", Type: parser.NewDataTypeInt()}, + {Name: "scale", Type: parser.NewDataTypeInt()}, + {Name: "min", Type: parser.NewDataTypeInt()}, + {Name: "max", Type: parser.NewDataTypeInt()}, + {Name: "timeunit", Type: parser.NewDataTypeString()}, + {Name: "epoch", Type: parser.NewDataTypeInt()}, + {Name: "timequantum", Type: parser.NewDataTypeString()}, + {Name: "ttl", Type: parser.NewDataTypeString()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("CreateTableDupeColumns", func(t *testing.T) { + _, _, err := sql_test.MustQueryRows(t, server, `create table dupecols ( + _id id, + _id int)`) + if err == nil { + t.Fatal("expected error") + } else { + if err.Error() != "[3:4] duplicate column '_id'" { + t.Fatal(err) + } + } + }) + + t.Run("CreateTableMissingId", func(t *testing.T) { + _, _, err := sql_test.MustQueryRows(t, server, `create table missingid ( + foo int)`) + if err == nil { + t.Fatal("expected error") + } else { + if err.Error() != "[1:1] _id column must be specified" { + t.Fatal(err) + } + } + }) +} + +func TestPlanner_AlterTable(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + index, err := c.GetHolder(0).CreateIndex("i", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := index.CreateField("f", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := index.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + server := c.GetNode(0).Server + + t.Run("AlterTableDrop", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, server, `alter table i drop column f`) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([][]interface{}{}, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{}, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("AlterTableAdd", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, server, `alter table i add column f int`) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([][]interface{}{}, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{}, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("AlterTableRename", func(t *testing.T) { + t.Skip("not yet implemented") + results, columns, err := sql_test.MustQueryRows(t, server, `alter table i rename column f to g`) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([][]interface{}{}, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{}, columns); diff != "" { + t.Fatal(diff) + } + }) + +} +func TestPlanner_DropTable(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + index, err := c.GetHolder(0).CreateIndex("i", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := index.CreateField("f", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := index.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + t.Run("DropTable", func(t *testing.T) { + _, _, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `DROP TABLE i`) + if err != nil { + t.Fatal(err) + } + }) +} + +func TestPlanner_ExpressionsInSelectListParen(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + i1, err := c.GetHolder(0).CreateIndex("i1", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i1.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i1.CreateField("y", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(1, b=100) + Set(2, a=20) + Set(2, b=200) + `}); err != nil { + t.Fatal(err) + } + + t.Run("ParenOne", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT (a != b) = false, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {bool(false), int64(1)}, + {bool(false), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeBool()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ParenTwo", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT (a != b) = (false), _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {bool(false), int64(1)}, + {bool(false), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeBool()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} + +func TestPlanner_ExpressionsInSelectListLiterals(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("d", pilosa.OptFieldTypeDecimal(2)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("ts", pilosa.OptFieldTypeTimestamp(pilosa.DefaultEpoch, "s")); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("str", pilosa.OptFieldTypeMutex(pilosa.CacheTypeLRU, pilosa.DefaultCacheSize), pilosa.OptFieldKeys()); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(1, b=100) + Set(2, a=20) + Set(2, b=200) + Set(1, d=10.3) + Set(1, ts='2022-02-22T22:22:22Z') + Set(1, str='foo') + `}); err != nil { + t.Fatal(err) + } + + t.Run("LiteralsBool", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT false = true, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {bool(false), int64(1)}, + {bool(false), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeBool()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("LiteralsInt", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT 1 + 2, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(3), int64(1)}, + {int64(3), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("LiteralsID", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT _id + 2, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(3), int64(1)}, + {int64(4), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("LiteralsDecimal", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT d + 2.0, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {float64(12.3), int64(1)}, + {nil, int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeDecimal(2)}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("LiteralsString", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT str || ' bar', _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {string("foo bar"), int64(1)}, + {nil, int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "", Type: parser.NewDataTypeString()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} + +func TestPlanner_ExpressionsInSelectListCase(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("d", pilosa.OptFieldTypeDecimal(2)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("ts", pilosa.OptFieldTypeTimestamp(pilosa.DefaultEpoch, "s")); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("str", pilosa.OptFieldTypeMutex(pilosa.CacheTypeLRU, pilosa.DefaultCacheSize), pilosa.OptFieldKeys()); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(1, b=100) + Set(2, a=20) + Set(2, b=200) + Set(1, d=10.3) + Set(1, ts='2022-02-22T22:22:22Z') + Set(1, str='foo') + `}); err != nil { + t.Fatal(err) + } + + t.Run("CaseWithBase", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT b, case b when 100 then 10 when 201 then 20 else 5 end, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(100), int64(10), int64(1)}, + {int64(200), int64(5), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("CaseWithNoBase", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT b, case when b = 100 then 10 when b = 201 then 20 else 5 end, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(100), int64(10), int64(1)}, + {int64(200), int64(5), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} + +func TestPlanner_Select(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + i1, err := c.GetHolder(0).CreateIndex("i1", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i1.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i1.CreateField("y", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(1, b=100) + Set(2, a=20) + Set(2, b=200) + `}); err != nil { + t.Fatal(err) + } + + t.Run("UnqualifiedColumns", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT a, b, _id FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(10), int64(100), int64(1)}, + {int64(20), int64(200), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("QualifiedTableRef", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT bar.a, bar.b, bar._id FROM i0 as bar`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(10), int64(100), int64(1)}, + {int64(20), int64(200), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("AliasedUnqualifiedColumns", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT a as foo, b as bar, _id as baz FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(10), int64(100), int64(1)}, + {int64(20), int64(200), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "foo", Type: parser.NewDataTypeInt()}, + {Name: "bar", Type: parser.NewDataTypeInt()}, + {Name: "baz", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("QualifiedColumns", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT i0._id, i0.a, i0.b FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1), int64(10), int64(100)}, + {int64(2), int64(20), int64(200)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("UnqualifiedStar", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT * FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1), int64(10), int64(100)}, + {int64(2), int64(20), int64(200)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("QualifiedStar", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT i0.* FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1), int64(10), int64(100)}, + {int64(2), int64(20), int64(200)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("NoIdentifier", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT a, b FROM i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(10), int64(100)}, + {int64(20), int64(200)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ErrFieldNotFound", func(t *testing.T) { + _, err := c.GetNode(0).Server.CompileExecutionPlan(context.Background(), `SELECT xyz FROM i0`) + if err == nil || !strings.Contains(err.Error(), `column 'xyz' not found`) { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestPlanner_SelectOrderBy(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(1, b=100) + Set(2, a=20) + Set(2, b=200) + `}); err != nil { + t.Fatal(err) + } + + t.Run("OrderBy", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT a, b, _id FROM i0 order by a desc`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(20), int64(200), int64(2)}, + {int64(10), int64(100), int64(1)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} + +func TestPlanner_SelectSelectSource(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(1, b=100) + Set(2, a=20) + Set(2, b=200) + `}); err != nil { + t.Fatal(err) + } + + t.Run("ParenSource", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT a, b, _id FROM (select * from i0)`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(10), int64(100), int64(1)}, + {int64(20), int64(200), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ParenSourceWithAlias", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT foo.a, b, _id FROM (select * from i0) as foo`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(10), int64(100), int64(1)}, + {int64(20), int64(200), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} + +func TestPlanner_In(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + i1, err := c.GetHolder(0).CreateIndex("i1", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i1.CreateField("parentid", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i1.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(2, a=20) + Set(3, a=30) + `}); err != nil { + t.Fatal(err) + } + + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i1", + Query: ` + Set(1, parentid=1) + Set(1, x=100) + + Set(2, parentid=1) + Set(2, x=200) + + Set(3, parentid=2) + Set(3, x=300) + `}); err != nil { + t.Fatal(err) + } + + t.Run("Count", func(t *testing.T) { + t.Skip("Need to add join conditions to get this to pass") + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT i0._id, i0.a, i1._id, i1.parentid, i1.x FROM i0 INNER JOIN i1 ON i0._id = i1.parentid`) + //results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid`) + //results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT a FROM i0 where a = 20`) // SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "count", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + /*t.Run("Count", func(t *testing.T) { + //results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid`) + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 where i0._id in (select distinct parentid from i1)`) // SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "count", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("CountWithParentCondition", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 where i0._id in (select distinct parentid from i1) and i0.a = 10`) // SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid WHERE i0.a = 10 + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "count", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("CountWithParentAndChildCondition", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT COUNT(*) FROM i0 where i0._id in (select distinct parentid from i1 where x = 200) and i0.a = 10`) // SELECT COUNT(*) FROM i0 INNER JOIN i1 ON i0._id = i1.parentid WHERE i0.a = 10 + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "count", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + })*/ +} + +func TestPlanner_Distinct(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + i1, err := c.GetHolder(0).CreateIndex("i1", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i1.CreateField("parentid", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } else if _, err := i1.CreateField("x", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(2, a=20) + Set(3, a=30) + `}); err != nil { + t.Fatal(err) + } + + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i1", + Query: ` + Set(1, parentid=1) + Set(1, x=100) + + Set(2, parentid=1) + Set(2, x=200) + + Set(3, parentid=2) + Set(3, x=300) + `}); err != nil { + t.Fatal(err) + } + + t.Run("SelectDistinct_id", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT distinct _id from i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1)}, + {int64(2)}, + {int64(3)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("SelectDistinctNonId", func(t *testing.T) { + t.Skip("WIP") + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `SELECT distinct parentid from i1`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1)}, + {int64(2)}, + {int64(3)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "parentid", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("SelectDistinctMultiple", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `select distinct _id, parentid from i1`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1), int64(1)}, + {int64(2), int64(1)}, + {int64(3), int64(2)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + {Name: "parentid", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} + +func TestPlanner_SelectTop(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + i0, err := c.GetHolder(0).CreateIndex("i0", pilosa.IndexOptions{TrackExistence: true}) + if err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("a", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + if _, err := i0.CreateField("b", pilosa.OptFieldTypeInt(0, 1000)); err != nil { + t.Fatal(err) + } + + // Populate with data. + if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{ + Index: "i0", + Query: ` + Set(1, a=10) + Set(2, a=20) + Set(3, a=30) + Set(1, b=100) + Set(2, b=200) + Set(3, b=300) + `}); err != nil { + t.Fatal(err) + } + + t.Run("SelectTopStar", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `select top(1) * from i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1), int64(10), int64(100)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("SelectTopNStar", func(t *testing.T) { + results, columns, err := sql_test.MustQueryRows(t, c.GetNode(0).Server, `select topn(1) * from i0`) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff([][]interface{}{ + {int64(1), int64(10), int64(100)}, + {int64(2), int64(20), int64(200)}, + {int64(3), int64(30), int64(300)}, + }, results); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff([]*planner_types.PlannerColumn{ + {Name: "_id", Type: parser.NewDataTypeID()}, + {Name: "a", Type: parser.NewDataTypeInt()}, + {Name: "b", Type: parser.NewDataTypeInt()}, + }, columns); diff != "" { + t.Fatal(diff) + } + }) +} diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go new file mode 100644 index 000000000..1cf1c5b8d --- /dev/null +++ b/sql3/planner/expression.go @@ -0,0 +1,2369 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" + + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// coerceValue coerces a value from a source type to a target type. If the types do not allow a conversion +// an error is produced +func coerceValue(sourceType parser.ExprDataType, targetType parser.ExprDataType, value interface{}, atPos parser.Pos) (interface{}, error) { + switch sourceType.(type) { + + case *parser.DataTypeInt: + switch t := targetType.(type) { + case *parser.DataTypeInt: + return value, nil + + case *parser.DataTypeID: + return value, nil + + case *parser.DataTypeDecimal: + val, ok := value.(int64) + if !ok { + return nil, sql3.NewErrInternalf("unexpected value type '%T'", value) + } + return pql.NewDecimal(val*int64(math.Pow(10, float64(t.Scale))), t.Scale), nil + } + + case *parser.DataTypeID: + switch t := targetType.(type) { + case *parser.DataTypeID: + return value, nil + + case *parser.DataTypeInt: + return value, nil + + case *parser.DataTypeDecimal: + val, ok := value.(int64) + if !ok { + return nil, sql3.NewErrInternalf("unexpected value type '%T'", value) + } + return pql.NewDecimal(int64(val)*int64(math.Pow(10, float64(t.Scale))), t.Scale), nil + } + + case *parser.DataTypeDecimal: + switch targetType.(type) { + case *parser.DataTypeDecimal: + return value, nil + } + + case *parser.DataTypeString: + switch targetType.(type) { + case *parser.DataTypeString: + return value, nil + case *parser.DataTypeTimestamp: + //try to coerce to a date + val, ok := value.(string) + if !ok { + return nil, sql3.NewErrInternalf("unexpected value type '%T'", value) + } + if tm, err := time.ParseInLocation(time.RFC3339Nano, val, time.UTC); err == nil { + return tm, nil + } else if tm, err := time.ParseInLocation(time.RFC3339, val, time.UTC); err == nil { + return tm, nil + } else if tm, err := time.ParseInLocation("2006-01-02", val, time.UTC); err == nil { + return tm, nil + } else { + return nil, sql3.NewErrInvalidTypeCoercion(0, 0, val, targetType.TypeName()) + } + } + + case *parser.DataTypeTimestamp: + switch targetType.(type) { + case *parser.DataTypeTimestamp: + return value, nil + } + + case *parser.DataTypeIDSet: + switch targetType.(type) { + case *parser.DataTypeIDSet: + return value, nil + } + + default: + return nil, sql3.NewErrInternalf("unhandled source type '%T'", sourceType) + } + return nil, sql3.NewErrTypeMismatch(atPos.Line, atPos.Column, targetType.TypeName(), sourceType.TypeName()) +} + +// unaryOpPlanExpression is a unary op +type unaryOpPlanExpression struct { + op parser.Token + rhs types.PlanExpression + + resultDataType parser.ExprDataType +} + +func newUnaryOpPlanExpression(op parser.Token, rhs types.PlanExpression, dataType parser.ExprDataType) *unaryOpPlanExpression { + return &unaryOpPlanExpression{ + op: op, + rhs: rhs, + resultDataType: dataType, + } +} + +func (n *unaryOpPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + evalRhs, err := n.rhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + switch n.op { + case parser.BITNOT: + return n.bitNotWithTypeCheck(evalRhs) + case parser.PLUS: + return n.plusWithTypeCheck(evalRhs) + case parser.MINUS: + return n.minusWithTypeCheck(evalRhs) + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } +} + +func (n *unaryOpPlanExpression) Type() parser.ExprDataType { + return n.resultDataType +} + +func (n *unaryOpPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["op"] = n.op + result["rhs"] = n.rhs.Plan() + return result +} + +func (n *unaryOpPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.rhs, + } +} + +func (n *unaryOpPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newUnaryOpPlanExpression(n.op, children[0], n.resultDataType), nil +} + +func (n *unaryOpPlanExpression) bitNotWithTypeCheck(rhs interface{}) (interface{}, error) { + switch n.resultDataType.(type) { + case *parser.DataTypeID: + nr, nrok := rhs.(int64) + if nrok { + return ^nr, nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + case *parser.DataTypeInt: + nr, nrok := rhs.(int64) + if nrok { + return ^nr, nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + default: + return nil, sql3.NewErrInternalf("unexpected type '%T", n.resultDataType) + } +} + +func (n *unaryOpPlanExpression) plusWithTypeCheck(rhs interface{}) (interface{}, error) { + switch n.resultDataType.(type) { + case *parser.DataTypeID: + nr, nrok := rhs.(int64) + if nrok { + return +nr, nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + case *parser.DataTypeInt: + coercedRhs, err := coerceValue(n.rhs.Type(), n.resultDataType, rhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + nr, nrok := coercedRhs.(int64) + if nrok { + return +nr, nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + case *parser.DataTypeDecimal: + nr, nrok := rhs.(pql.Decimal) + if nrok { + return +(nr.Float64()), nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + default: + return nil, sql3.NewErrInternalf("unexpected type '%T", n.resultDataType) + } +} + +func (n *unaryOpPlanExpression) minusWithTypeCheck(rhs interface{}) (interface{}, error) { + switch n.resultDataType.(type) { + case *parser.DataTypeID: + nr, nrok := rhs.(int64) + if nrok { + return -nr, nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + case *parser.DataTypeInt: + coercedRhs, err := coerceValue(n.rhs.Type(), n.resultDataType, rhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + nr, nrok := coercedRhs.(int64) + if nrok { + return -nr, nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + case *parser.DataTypeDecimal: + nr, nrok := rhs.(pql.Decimal) + if nrok { + return -(nr.Float64()), nil + } + return nil, sql3.NewErrInternalf("unexpected incompatible types '%T", rhs) + + default: + return nil, sql3.NewErrInternalf("unexpected type '%T", n.resultDataType) + } +} + +// binOpPlanExpression is a binary op +type binOpPlanExpression struct { + lhs types.PlanExpression + op parser.Token + rhs types.PlanExpression + + resultDataType parser.ExprDataType +} + +func newBinOpPlanExpression(lhs types.PlanExpression, op parser.Token, rhs types.PlanExpression, dataType parser.ExprDataType) *binOpPlanExpression { + return &binOpPlanExpression{ + lhs: lhs, + op: op, + rhs: rhs, + resultDataType: dataType, + } +} + +func (n *binOpPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + evalLhs, err := n.lhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + evalRhs, err := n.rhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + + if n.op == parser.IS || n.op == parser.ISNOT { + isNull := evalLhs == nil + if n.op == parser.ISNOT { + isNull = !isNull + } + return isNull, nil + } + + coercedDataType, err := typeCoerceType(n.lhs.Type(), n.rhs.Type(), parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + switch coercedDataType.(type) { + case *parser.DataTypeBool: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + nl, nlok := evalLhs.(bool) + nr, nrok := evalRhs.(bool) + if nlok && nrok { + switch n.op { + case parser.NE: + return nl != nr, nil + case parser.EQ: + return nl == nr, nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeInt: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + coercedLhs, err := coerceValue(n.lhs.Type(), coercedDataType, evalLhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + coercedRhs, err := coerceValue(n.rhs.Type(), coercedDataType, evalRhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + nl, nlok := coercedLhs.(int64) + nr, nrok := coercedRhs.(int64) + if nlok && nrok { + switch n.op { + case parser.NE: + return nl != nr, nil + case parser.EQ: + return nl == nr, nil + case parser.LE: + return nl <= nr, nil + case parser.GE: + return nl >= nr, nil + case parser.GT: + return nl > nr, nil + case parser.LT: + return nl < nr, nil + + case parser.BITAND: + return nl & nr, nil + case parser.BITOR: + return nl | nr, nil + + case parser.LSHIFT: + return nl << nr, nil + case parser.RSHIFT: + return nl >> nr, nil + + case parser.PLUS: + return nl + nr, nil + case parser.MINUS: + return nl - nr, nil + case parser.STAR: + return nl * nr, nil + case parser.SLASH: + return nl / nr, nil + + case parser.REM: + return nl % nr, nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeID: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + coercedLhs, err := coerceValue(n.lhs.Type(), coercedDataType, evalLhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + coercedRhs, err := coerceValue(n.rhs.Type(), coercedDataType, evalRhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + nl, nlok := coercedLhs.(int64) + nr, nrok := coercedRhs.(int64) + if nlok && nrok { + switch n.op { + case parser.NE: + return nl != nr, nil + case parser.EQ: + return nl == nr, nil + case parser.LE: + return nl <= nr, nil + case parser.GE: + return nl >= nr, nil + case parser.GT: + return nl > nr, nil + case parser.LT: + return nl < nr, nil + + case parser.BITAND: + return nl & nr, nil + case parser.BITOR: + return nl | nr, nil + + case parser.LSHIFT: + return nl << nr, nil + case parser.RSHIFT: + return nl >> nr, nil + + case parser.PLUS: + return nl + nr, nil + case parser.MINUS: + return nl - nr, nil + case parser.STAR: + return nl * nr, nil + case parser.SLASH: + return nl / nr, nil + + case parser.REM: + return nl % nr, nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeDecimal: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + var nl float64 + var nr float64 + + coercedLhs, err := coerceValue(n.lhs.Type(), coercedDataType, evalLhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + coercedRhs, err := coerceValue(n.rhs.Type(), coercedDataType, evalRhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + nld, nlok := coercedLhs.(pql.Decimal) + nrd, nrok := coercedRhs.(pql.Decimal) + + //TODO(pok) eliminate the use of float here and return pql.Decimal values for arithmetic ops + if nlok { + nl = nld.Float64() + } + if nrok { + nr = nrd.Float64() + } + if nlok && nrok { + switch n.op { + case parser.NE: + return nl != nr, nil + case parser.EQ: + return nl == nr, nil + case parser.LE: + return nl <= nr, nil + case parser.GE: + return nl >= nr, nil + case parser.GT: + return nl > nr, nil + case parser.LT: + return nl < nr, nil + + case parser.PLUS: + return nl + nr, nil + case parser.MINUS: + return nl - nr, nil + case parser.STAR: + return nl * nr, nil + case parser.SLASH: + return nl / nr, nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeTimestamp: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + coercedLhs, err := coerceValue(n.lhs.Type(), coercedDataType, evalLhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + coercedRhs, err := coerceValue(n.rhs.Type(), coercedDataType, evalRhs, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + nl, nlok := coercedLhs.(time.Time) + nr, nrok := coercedRhs.(time.Time) + + if nlok && nrok { + switch n.op { + case parser.NE: + return nl != nr, nil + case parser.EQ: + return nl == nr, nil + case parser.LE: + return nl == nr || nl.Before(nr), nil + case parser.GE: + return nl == nr || nl.After(nr), nil + case parser.GT: + return nl.After(nr), nil + case parser.LT: + return nl.Before(nr), nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeIDSet: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + nl, nlok := evalLhs.([]int64) + nr, nrok := evalRhs.([]int64) + + if nlok && nrok { + switch n.op { + case parser.NE: + return !intSetContainsAll(nl, nr), nil + case parser.EQ: + return intSetContainsAll(nl, nr), nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeString: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + nl, nlok := evalLhs.(string) + nr, nrok := evalRhs.(string) + if nlok && nrok { + switch n.op { + + case parser.NE: + return nl != nr, nil + + case parser.EQ: + return nl == nr, nil + + case parser.CONCAT: + return nl + nr, nil + + case parser.LIKE: + regexPattern := wildCardToRegexp(nr) + + matched, err := regexp.MatchString(regexPattern, nl) + if err != nil { + return nil, err + } + return matched, nil + + case parser.NOTLIKE: + regexPattern := wildCardToRegexp(nr) + matched, err := regexp.MatchString(regexPattern, nl) + if err != nil { + return nil, err + } + return !matched, nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + case *parser.DataTypeStringSet: + //if either side is nil, return nil + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + nl, nlok := evalLhs.([]string) + nr, nrok := evalRhs.([]string) + + if nlok && nrok { + switch n.op { + case parser.NE: + return !stringSetContainsAll(nl, nr), nil + case parser.EQ: + return stringSetContainsAll(nl, nr), nil + + default: + return nil, sql3.NewErrInternalf("unhandled operator %d", n.op) + } + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + + default: + return nil, sql3.NewErrInternalf("unhandled type '%s'", coercedDataType.TypeName()) + } +} + +func (n *binOpPlanExpression) Type() parser.ExprDataType { + return n.resultDataType +} + +func (n *binOpPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["op"] = n.op + result["lhs"] = n.lhs.Plan() + result["rhs"] = n.rhs.Plan() + return result +} + +func (n *binOpPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.lhs, + n.rhs, + } +} + +func (n *binOpPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 2 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newBinOpPlanExpression(children[0], n.op, children[1], n.resultDataType), nil +} + +// rangePlanExpression is a range expression +type rangePlanExpression struct { + lhs types.PlanExpression + rhs types.PlanExpression + + resultDataType parser.ExprDataType +} + +func newRangeOpPlanExpression(lhs types.PlanExpression, rhs types.PlanExpression, dataType parser.ExprDataType) *rangePlanExpression { + return &rangePlanExpression{ + lhs: lhs, + rhs: rhs, + resultDataType: dataType, + } +} + +func (n *rangePlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + evalLhs, err := n.lhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + evalRhs, err := n.rhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + + if evalLhs == nil || evalRhs == nil { + return nil, nil + } + + /*nl*/ + _, nlok := evalLhs.(int64) + /*nr*/ _, nrok := evalRhs.(int64) + if nlok && nrok { + return true, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) +} + +func (n *rangePlanExpression) Type() parser.ExprDataType { + return n.resultDataType +} + +func (n *rangePlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["lhs"] = n.lhs.Plan() + result["rhs"] = n.rhs.Plan() + return result +} + +func (n *rangePlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.lhs, + n.rhs, + } +} + +func (n *rangePlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newRangeOpPlanExpression(children[0], children[1], n.resultDataType), nil +} + +// casePlanExpression is a case expr +type casePlanExpression struct { + baseExpr types.PlanExpression + blocks []types.PlanExpression + elseExpr types.PlanExpression + + resultDataType parser.ExprDataType +} + +func newCasePlanExpression(baseExpr types.PlanExpression, blocks []types.PlanExpression, elseExpr types.PlanExpression, dataType parser.ExprDataType) *casePlanExpression { + return &casePlanExpression{ + baseExpr: baseExpr, + blocks: blocks, + elseExpr: elseExpr, + resultDataType: dataType, + } +} + +func (n *casePlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + if n.baseExpr != nil { + evalBase, err := n.baseExpr.Evaluate(currentRow) + if err != nil { + return nil, err + } + if evalBase == nil { + return nil, nil + } + for _, block := range n.blocks { + caseBlock, ok := block.(*caseBlockPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected block type '%T'", block) + } + + evalBlock, err := caseBlock.condition.Evaluate(currentRow) + if err != nil { + return nil, err + } + switch n.baseExpr.Type().(type) { + case *parser.DataTypeInt: + nl, nlok := evalBase.(int64) + nr, nrok := evalBlock.(int64) + if nlok && nrok { + if nl == nr { + evalBlockBody, err := caseBlock.body.Evaluate(currentRow) + if err != nil { + return nil, err + } + if evalBlockBody == nil { + return nil, nil + } + switch caseBlock.body.Type().(type) { + case *parser.DataTypeInt: + b, bok := evalBlockBody.(int64) + if bok { + return b, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", bok) + default: + return nil, sql3.NewErrInternalf("unhandled type '%s'", n.baseExpr.Type()) + } + } + } else { + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t'", nlok, nrok) + } + default: + return nil, sql3.NewErrInternalf("unhandled type '%s'", n.baseExpr.Type()) + } + } + //if we get to here, we're falling back to else + if n.elseExpr != nil { + evalElse, err := n.elseExpr.Evaluate(currentRow) + if err != nil { + return nil, err + } + if evalElse == nil { + return nil, nil + } + switch n.elseExpr.Type().(type) { + case *parser.DataTypeInt: + el, elok := evalElse.(int64) + if elok { + return el, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", elok) + default: + return nil, sql3.NewErrInternalf("unhandled type '%s'", n.elseExpr.Type()) + + } + } + return nil, nil + } else { + for _, block := range n.blocks { + caseBlock, ok := block.(*caseBlockPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected block type '%T'", block) + } + + evalBlock, err := caseBlock.condition.Evaluate(currentRow) + if err != nil { + return nil, err + } + bl, blok := evalBlock.(bool) + if !blok { + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", blok) + } + if bl { + evalBlockBody, err := caseBlock.body.Evaluate(currentRow) + if err != nil { + return nil, err + } + if evalBlockBody == nil { + return nil, nil + } + switch caseBlock.body.Type().(type) { + case *parser.DataTypeInt: + b, bok := evalBlockBody.(int64) + if bok { + return b, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", bok) + case *parser.DataTypeString: + s, sok := evalBlockBody.(string) + if sok { + return s, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", sok) + default: + return nil, sql3.NewErrInternalf("unhandled type '%T'", caseBlock.body.Type()) + } + } + } + //if we get to here, we're falling back to else + if n.elseExpr != nil { + evalElse, err := n.elseExpr.Evaluate(currentRow) + if err != nil { + return nil, err + } + if evalElse == nil { + return nil, nil + } + switch n.elseExpr.Type().(type) { + case *parser.DataTypeInt: + el, elok := evalElse.(int64) + if elok { + return el, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", elok) + case *parser.DataTypeString: + s, sok := evalElse.(string) + if sok { + return s, nil + } + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t'", sok) + default: + return nil, sql3.NewErrInternalf("unhandled type '%T'", n.elseExpr.Type()) + + } + } + return nil, nil + } +} + +func (n *casePlanExpression) Type() parser.ExprDataType { + return n.resultDataType +} + +func (n *casePlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + if n.baseExpr != nil { + result["baseExpr"] = n.baseExpr.Plan() + } + if n.elseExpr != nil { + result["elseExpr"] = n.elseExpr.Plan() + } + ps := make([]interface{}, 0) + for _, e := range n.blocks { + ps = append(ps, e.Plan()) + } + result["blocks"] = ps + return result +} + +func (n *casePlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *casePlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// caseBlockPlanExpression is for case blocks +type caseBlockPlanExpression struct { + condition types.PlanExpression + body types.PlanExpression +} + +func newCaseBlockPlanExpression(condition types.PlanExpression, body types.PlanExpression) *caseBlockPlanExpression { + return &caseBlockPlanExpression{ + condition: condition, + body: body, + } +} + +func (n *caseBlockPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return nil, nil +} + +func (n *caseBlockPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeBool() +} + +func (n *caseBlockPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["condition"] = n.condition.Plan() + result["body"] = n.body.Plan() + return result +} + +func (n *caseBlockPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.condition, + n.body, + } +} + +func (n *caseBlockPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// subqueryPlanExpression is a select statement (when used in an expression) +type subqueryPlanExpression struct { + op types.PlanOperator +} + +func newSubqueryPlanExpression(op types.PlanOperator) *subqueryPlanExpression { + return &subqueryPlanExpression{ + op: op, + } +} + +func (n *subqueryPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + //get an iterator + iter, err := n.op.Iterator(context.Background(), currentRow) + if err != nil { + return nil, err + } + + //get the first row + row, err := iter.Next(context.Background()) + if err != nil { + if err == types.ErrNoMoreRows { + //no rows, so return null + //TODO(pok) - check that this is the right behavior + return nil, nil + } + return nil, err + } + result := row[0] + + //make sure we don't have a next row - this is an error + _, err = iter.Next(context.Background()) + if err != nil && err == types.ErrNoMoreRows { + return result, nil + } + return nil, sql3.NewErrSingleRowExpected(0, 0) +} + +func (n *subqueryPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeBool() +} + +func (n *subqueryPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["subquery"] = n.op.Plan() + return result +} + +func (n *subqueryPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *subqueryPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// betweenOpPlanExpression is a 'between/not between' op +type betweenOpPlanExpression struct { + lhs types.PlanExpression + op parser.Token + rhs types.PlanExpression +} + +func newBetweenOpPlanExpression(lhs types.PlanExpression, op parser.Token, rhs types.PlanExpression) *betweenOpPlanExpression { + return &betweenOpPlanExpression{ + lhs: lhs, + op: op, + rhs: rhs, + } +} + +func (n *betweenOpPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + evalLhs, err := n.lhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + + exprRange, ok := n.rhs.(*rangePlanExpression) + if !ok { + return nil, sql3.NewErrInternal("range expression expected") + } + + rangeLower, err := exprRange.lhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + rangeUpper, err := exprRange.rhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + + if evalLhs == nil || rangeLower == nil || rangeUpper == nil { + return nil, nil + } + + switch rType := n.rhs.Type().(type) { + case *parser.DataTypeRange: + switch rType.SubscriptType.(type) { + case *parser.DataTypeInt: + + nl, nlok := evalLhs.(int64) + rl, rlok := rangeLower.(int64) + ru, ruok := rangeUpper.(int64) + + if !(nlok && rlok && ruok) { + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t', '%t'", nlok, rlok, ruok) + } + result := nl >= rl && nl <= ru + if n.op == parser.NOTBETWEEN { + result = !result + } + return result, nil + + case *parser.DataTypeTimestamp: + + nl, nlok := evalLhs.(time.Time) + rl, rlok := rangeLower.(time.Time) + ru, ruok := rangeUpper.(time.Time) + + if !(nlok && rlok && ruok) { + return nil, sql3.NewErrInternalf("unexpected type conversion error '%t', '%t', '%t'", nlok, rlok, ruok) + } + result := (nl == rl || nl.After(rl)) && (nl == ru || nl.Before(ru)) + if n.op == parser.NOTBETWEEN { + result = !result + } + return result, nil + + default: + return nil, sql3.NewErrInternalf("unexpected range type '%T'", rType.SubscriptType) + } + + default: + return nil, sql3.NewErrInternalf("unexpected range type '%T'", n.rhs.Type()) + } +} + +func (n *betweenOpPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeBool() +} + +func (n *betweenOpPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["lhs"] = n.lhs.Plan() + result["rhs"] = n.rhs.Plan() + return result +} + +func (n *betweenOpPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.lhs, + n.rhs, + } +} + +func (n *betweenOpPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newBetweenOpPlanExpression(children[0], n.op, children[1]), nil +} + +// inOpPlanExpression is an 'in/not in' op +type inOpPlanExpression struct { + lhs types.PlanExpression + op parser.Token + rhs types.PlanExpression +} + +func newInOpPlanExpression(lhs types.PlanExpression, op parser.Token, rhs types.PlanExpression) *inOpPlanExpression { + return &inOpPlanExpression{ + lhs: lhs, + op: op, + rhs: rhs, + } +} + +func (n *inOpPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + evalLhs, err := n.lhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + + //if lhs is nil, bail + if evalLhs == nil { + return nil, nil + } + + exprList, ok := n.rhs.(*exprListPlanExpression) + if !ok { + return nil, sql3.NewErrInternal("expression list expected") + } + + listMembers := []interface{}{} + + //evaluate all the list members + for _, lm := range exprList.exprs { + lv, err := lm.Evaluate(currentRow) + if err != nil { + return nil, err + } + //if any of the list members eval to nil, bail + if lv == nil { + return nil, nil + } + listMembers = append(listMembers, lv) + } + + result := false + + switch n.lhs.Type().(type) { + + case *parser.DataTypeInt, *parser.DataTypeID: + nl, nlok := evalLhs.(int64) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.(int64) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if nl == l { + result = true + break + } + } + + case *parser.DataTypeBool: + nl, nlok := evalLhs.(bool) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.(bool) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if nl == l { + result = true + break + } + } + + case *parser.DataTypeDecimal: + nl, nlok := evalLhs.(pql.Decimal) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.(pql.Decimal) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if nl.EqualTo(l) { + result = true + break + } + } + + case *parser.DataTypeIDSet: + nl, nlok := evalLhs.([]int64) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.([]int64) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if intSetContainsAll(nl, l) { + result = true + break + } + } + + case *parser.DataTypeString: + nl, nlok := evalLhs.(string) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.(string) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if nl == l { + result = true + break + } + } + + case *parser.DataTypeStringSet: + nl, nlok := evalLhs.([]string) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.([]string) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if stringSetContainsAll(nl, l) { + result = true + break + } + } + + case *parser.DataTypeTimestamp: + nl, nlok := evalLhs.(time.Time) + if !nlok { + return nil, sql3.NewErrInternalf("unable to convert lhs expression to type '%s'", n.lhs.Type().TypeName()) + } + + for _, lm := range listMembers { + l, lok := lm.(time.Time) + if !lok { + return nil, sql3.NewErrInternalf("unable to convert list expression to type '%s'", n.lhs.Type().TypeName()) + } + if nl == l { + result = true + break + } + } + + default: + return nil, sql3.NewErrInternalf("unhandled type '%T'", n.lhs.Type()) + } + + if n.op == parser.NOTIN { + return !result, nil + } else { + return result, nil + } +} + +func (n *inOpPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeBool() +} + +func (n *inOpPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["lhs"] = n.lhs.Plan() + result["rhs"] = n.rhs.Plan() + return result + +} + +func (n *inOpPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.lhs, + n.rhs, + } +} + +func (n *inOpPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newInOpPlanExpression(children[0], n.op, children[1]), nil +} + +// callPlanExpression is a function call +type callPlanExpression struct { + name string + args []types.PlanExpression + dataType parser.ExprDataType +} + +func newCallPlanExpression(name string, args []types.PlanExpression, dataType parser.ExprDataType) *callPlanExpression { + return &callPlanExpression{ + name: name, + args: args, + dataType: dataType, + } +} + +func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + switch strings.ToUpper(n.name) { + case "SETCONTAINS": + return n.EvaluateSetContains(currentRow) + case "SETCONTAINSANY": + return n.EvaluateSetContainsAny(currentRow) + case "SETCONTAINSALL": + return n.EvaluateSetContainsAll(currentRow) + case "DATEPART": + return n.EvaluateDatepart(currentRow) + default: + return nil, sql3.NewErrInternalf("unhandled function name '%s'", n.name) + } +} + +func (n *callPlanExpression) Type() parser.ExprDataType { + return n.dataType +} + +func (n *callPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["name"] = n.name + result["dataType"] = n.Type().TypeName() + ps := make([]interface{}, 0) + for _, e := range n.args { + ps = append(ps, e.Plan()) + } + result["args"] = ps + return result +} + +func (n *callPlanExpression) Children() []types.PlanExpression { + return n.args +} + +func (n *callPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != len(n.args) { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newCallPlanExpression(n.name, children, n.dataType), nil +} + +// aliasPlanExpression is a alias ref +type aliasPlanExpression struct { + types.SchemaIdentifiable + aliasName string + expr types.PlanExpression +} + +func newAliasPlanExpression(aliasName string, expr types.PlanExpression) *aliasPlanExpression { + return &aliasPlanExpression{ + aliasName: aliasName, + expr: expr, + } +} + +func (n *aliasPlanExpression) Name() string { + return n.aliasName +} + +//evaluates expression based on current row and column +func (n *aliasPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return n.expr.Evaluate(currentRow) +} + +//returns the type of the expression +func (n *aliasPlanExpression) Type() parser.ExprDataType { + return n.expr.Type() +} + +func (n *aliasPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["aliasName"] = n.aliasName + result["expr"] = n.expr.Plan() + return result +} + +func (n *aliasPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.expr, + } +} + +func (n *aliasPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newAliasPlanExpression(n.aliasName, children[0]), nil +} + +// qualifiedRefPlanExpression is a qualified ref +type qualifiedRefPlanExpression struct { + types.SchemaIdentifiable + tableName string + columnName string + columnIndex int + dataType parser.ExprDataType +} + +func newQualifiedRefPlanExpression(tableName string, columnName string, columnIndex int, dataType parser.ExprDataType) *qualifiedRefPlanExpression { + return &qualifiedRefPlanExpression{ + tableName: tableName, + columnName: columnName, + columnIndex: columnIndex, + dataType: dataType, + } +} + +func (n *qualifiedRefPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + if n.columnIndex < 0 || n.columnIndex >= len(currentRow) { + return nil, sql3.NewErrInternalf("unable to to find column '%d' in currentColumns", n.columnIndex) + } + + if currentRow[n.columnIndex] == nil { + return currentRow[n.columnIndex], nil + } + + switch n.dataType.(type) { + case *parser.DataTypeIDSet: + row, ok := currentRow[n.columnIndex].([]uint64) + if !ok { + return nil, sql3.NewErrInternalf("unexpected type for current row '%T'", currentRow[n.columnIndex]) + } + result := make([]int64, len(row)) + for i, v := range row { + result[i] = int64(v) + } + return result, nil + + case *parser.DataTypeID: + //TODO(pok) why are we trying two underlying types here? + iv, iok := currentRow[n.columnIndex].(int64) + if iok { + return iv, nil + } + v, ok := currentRow[n.columnIndex].(uint64) + if !ok { + return nil, sql3.NewErrInternalf("unexpected type for current row '%T'", currentRow[n.columnIndex]) + } + return int64(v), nil + + default: + return currentRow[n.columnIndex], nil + } +} + +func (n *qualifiedRefPlanExpression) Name() string { + return n.columnName +} + +func (n *qualifiedRefPlanExpression) Type() parser.ExprDataType { + return n.dataType +} + +func (n *qualifiedRefPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["tableName"] = n.tableName + result["columnName"] = n.columnName + result["columnIndex"] = n.columnIndex + result["dataType"] = n.dataType.TypeName() + return result +} + +func (n *qualifiedRefPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *qualifiedRefPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// nullLiteralPlanExpression is a null literal +type nullLiteralPlanExpression struct{} + +func newNullLiteralPlanExpression() *nullLiteralPlanExpression { + return &nullLiteralPlanExpression{} +} + +func (n *nullLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return nil, nil +} + +func (n *nullLiteralPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeVoid() +} + +func (n *nullLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + return result +} + +func (n *nullLiteralPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *nullLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// intLiteralPlanExpression is an integer literal +type intLiteralPlanExpression struct { + value string +} + +func newIntLiteralPlanExpression(value string) *intLiteralPlanExpression { + return &intLiteralPlanExpression{ + value: value, + } +} + +func (n *intLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return strconv.ParseInt(n.value, 10, 64) +} + +func (n *intLiteralPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeInt() +} + +func (n *intLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["value"] = n.value + return result +} + +func (n *intLiteralPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *intLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// floatLiteralPlanExpression is a float literal +type floatLiteralPlanExpression struct { + value string +} + +func newFloatLiteralPlanExpression(value string) *floatLiteralPlanExpression { + return &floatLiteralPlanExpression{ + value: value, + } +} + +func (n *floatLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + scale := parser.NumDecimalPlaces(n.value) + fvalue, err := strconv.ParseFloat(n.value, 64) + if err != nil { + return nil, err + } + unscaledValue := int64(fvalue * math.Pow(10, float64(scale))) + return pql.NewDecimal(unscaledValue, int64(scale)), nil +} + +func (n *floatLiteralPlanExpression) Type() parser.ExprDataType { + scale := parser.NumDecimalPlaces(n.value) + return parser.NewDataTypeDecimal(int64(scale)) +} + +func (n *floatLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["value"] = n.value + return result +} + +func (n *floatLiteralPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *floatLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// boolLiteralPlanExpression is a bool literal +type boolLiteralPlanExpression struct { + value bool +} + +func newBoolLiteralPlanExpression(value bool) *boolLiteralPlanExpression { + return &boolLiteralPlanExpression{ + value: value, + } +} + +func (n *boolLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return n.value, nil +} + +func (n *boolLiteralPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeBool() +} + +func (n *boolLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["value"] = n.value + return result +} + +func (n *boolLiteralPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *boolLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// dateLiteralPlanExpression is a date literal +type dateLiteralPlanExpression struct { + value time.Time +} + +func newDateLiteralPlanExpression(value time.Time) *dateLiteralPlanExpression { + return &dateLiteralPlanExpression{ + value: value, + } +} + +func (n *dateLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return n.value, nil +} + +func (n *dateLiteralPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeTimestamp() +} + +func (n *dateLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["value"] = n.value + return result +} + +func (n *dateLiteralPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *dateLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// stringLiteralPlanExpression is a string literal +type stringLiteralPlanExpression struct { + value string +} + +func newStringLiteralPlanExpression(value string) *stringLiteralPlanExpression { + return &stringLiteralPlanExpression{ + value: value, + } +} + +func (n *stringLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return n.value, nil +} + +func (n *stringLiteralPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeString() +} + +func (n *stringLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["value"] = n.value + return result +} + +func (n *stringLiteralPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *stringLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// castPlanExpressionis a cast op +type castPlanExpression struct { + lhs types.PlanExpression + targetType parser.ExprDataType +} + +func newCastPlanExpression(lhs types.PlanExpression, targetType parser.ExprDataType) *castPlanExpression { + return &castPlanExpression{ + lhs: lhs, + targetType: targetType, + } +} + +func (n *castPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + evalLhs, err := n.lhs.Evaluate(currentRow) + if err != nil { + return nil, err + } + switch sourceType := n.lhs.Type().(type) { + case *parser.DataTypeInt: + nl, nlok := evalLhs.(int64) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch tt := n.targetType.(type) { + case *parser.DataTypeInt, *parser.DataTypeID: + return nl, nil + case *parser.DataTypeBool: + return nl > 0, nil + case *parser.DataTypeDecimal: + return pql.NewDecimal(nl*int64(math.Pow(10, float64(tt.Scale))), tt.Scale), nil + case *parser.DataTypeString: + return fmt.Sprintf("%d", nl), nil + case *parser.DataTypeTimestamp: + tm := time.Unix(nl, 0).UTC() + return tm, nil + } + + case *parser.DataTypeID: + nl, nlok := evalLhs.(int64) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch tt := n.targetType.(type) { + case *parser.DataTypeInt, *parser.DataTypeID: + return nl, nil + case *parser.DataTypeBool: + return nl > 0, nil + case *parser.DataTypeDecimal: + return pql.NewDecimal(nl*int64(math.Pow(10, float64(tt.Scale))), tt.Scale), nil + case *parser.DataTypeString: + return fmt.Sprintf("%d", nl), nil + case *parser.DataTypeTimestamp: + tm := time.Unix(nl, 0).UTC() + return tm, nil + } + + case *parser.DataTypeBool: + nl, nlok := evalLhs.(bool) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch n.targetType.(type) { + case *parser.DataTypeInt, *parser.DataTypeID: + if nl { + return int64(1), nil + } + return int64(0), nil + case *parser.DataTypeBool: + return nl, nil + case *parser.DataTypeString: + return fmt.Sprintf("%v", nl), nil + } + + case *parser.DataTypeDecimal: + nl, nlok := evalLhs.(pql.Decimal) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch n.targetType.(type) { + case *parser.DataTypeDecimal: + return nl, nil + case *parser.DataTypeString: + return fmt.Sprintf("%v", nl), nil + } + + case *parser.DataTypeIDSet: + nl, nlok := evalLhs.([]int64) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch n.targetType.(type) { + case *parser.DataTypeIDSet: + return nl, nil + case *parser.DataTypeString: + //TODO(pok) come up with a better string representation of idset + return fmt.Sprintf("%v", nl), nil + } + + case *parser.DataTypeString: + nl, nlok := evalLhs.(string) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch tt := n.targetType.(type) { + case *parser.DataTypeInt, *parser.DataTypeID: + i, err := strconv.Atoi(nl) + if err != nil { + //TODO(pok) need to push location into here + return nil, sql3.NewErrInvalidCast(0, 0, nl, n.targetType.TypeName()) + } + return int64(i), nil + + case *parser.DataTypeBool: + i, err := strconv.ParseBool(nl) + if err != nil { + //TODO(pok) need to push location into here + return nil, sql3.NewErrInvalidCast(0, 0, nl, n.targetType.TypeName()) + } + return i, nil + + case *parser.DataTypeDecimal: + fvalue, err := strconv.ParseFloat(nl, 64) + if err != nil { + //TODO(pok) need to push location into here + return nil, sql3.NewErrInvalidCast(0, 0, nl, n.targetType.TypeName()) + } + scale := parser.NumDecimalPlaces(nl) + unscaledValue := int64(fvalue * math.Pow(10, float64(scale))) + castValue := pql.NewDecimal(unscaledValue, int64(scale)) + if tt.Scale < castValue.Scale { + return nil, sql3.NewErrInvalidCast(0, 0, nl, n.targetType.TypeName()) + } + + return castValue, nil + + case *parser.DataTypeString: + return nl, nil + + case *parser.DataTypeTimestamp: + if tm, err := time.ParseInLocation(time.RFC3339Nano, nl, time.UTC); err == nil { + return tm, nil + } else if tm, err := time.ParseInLocation(time.RFC3339, nl, time.UTC); err == nil { + return tm, nil + } else if tm, err := time.ParseInLocation("2006-01-02", nl, time.UTC); err == nil { + return tm, nil + } else { + return nil, sql3.NewErrInvalidCast(0, 0, nl, n.targetType.TypeName()) + } + } + + case *parser.DataTypeStringSet: + nl, nlok := evalLhs.([]string) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch n.targetType.(type) { + case *parser.DataTypeStringSet: + return nl, nil + case *parser.DataTypeString: + //TODO(pok) come up with a better string representation of string set + return fmt.Sprintf("%v", nl), nil + } + + case *parser.DataTypeTimestamp: + nl, nlok := evalLhs.(time.Time) + if !nlok { + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) + } + switch n.targetType.(type) { + case *parser.DataTypeTimestamp: + return nl, nil + case *parser.DataTypeInt: + return nl.Unix(), nil + case *parser.DataTypeString: + return nl.Format(time.RFC3339), nil + } + + default: + return nil, sql3.NewErrInternalf("unhandled cast type '%T'", sourceType) + } + return nil, sql3.NewErrInternalf("unable to cast expression of type '%T' to type '%T'", n.lhs.Type(), n.targetType) +} + +func (n *castPlanExpression) Type() parser.ExprDataType { + return n.targetType +} + +func (n *castPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["lhs"] = n.lhs.Plan() + return result +} + +func (n *castPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{ + n.lhs, + } +} + +func (n *castPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newCastPlanExpression(children[0], n.targetType), nil +} + +// exprListPlanExpression is an expression list +type exprListPlanExpression struct { + exprs []types.PlanExpression +} + +func newExprListExpression(exprs []types.PlanExpression) *exprListPlanExpression { + return &exprListPlanExpression{ + exprs: exprs, + } +} + +func (n *exprListPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + return nil, nil +} + +func (n *exprListPlanExpression) Type() parser.ExprDataType { + return parser.NewDataTypeVoid() +} + +func (n *exprListPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + ps := make([]interface{}, 0) + for _, e := range n.exprs { + ps = append(ps, e.Plan()) + } + result["exprs"] = ps + return result +} + +func (n *exprListPlanExpression) Children() []types.PlanExpression { + return n.exprs +} + +func (n *exprListPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// exprSetLiteralPlanExpression is a set literal +type exprSetLiteralPlanExpression struct { + members []types.PlanExpression + dataType parser.ExprDataType +} + +func newExprSetLiteralPlanExpression(members []types.PlanExpression, dataType parser.ExprDataType) *exprSetLiteralPlanExpression { + return &exprSetLiteralPlanExpression{ + members: members, + dataType: dataType, + } +} + +func (n *exprSetLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + switch typ := n.dataType.(type) { + case *parser.DataTypeIDSet: + result := []int64{} + for _, e := range n.members { + er, err := e.Evaluate(currentRow) + if err != nil { + return nil, err + } + coercedEr, err := coerceValue(e.Type(), &parser.DataTypeID{}, er, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + eri, ok := coercedEr.(int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert element result") + } + result = append(result, eri) + } + return result, nil + + case *parser.DataTypeStringSet: + result := []string{} + for _, e := range n.members { + er, err := e.Evaluate(currentRow) + if err != nil { + return nil, err + } + ers, ok := er.(string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert element result") + } + result = append(result, ers) + } + return result, nil + default: + return nil, sql3.NewErrInternalf("unexpected set literal type '%T'", typ) + } +} + +func (n *exprSetLiteralPlanExpression) Type() parser.ExprDataType { + return n.dataType +} + +func (n *exprSetLiteralPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + ps := make([]interface{}, 0) + for _, e := range n.members { + ps = append(ps, e.Plan()) + } + result["members"] = ps + return result +} + +func (n *exprSetLiteralPlanExpression) Children() []types.PlanExpression { + return n.members +} + +func (n *exprSetLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + if len(children) != len(n.members) { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return newExprSetLiteralPlanExpression(children, n.dataType), nil +} + +// compileExpr returns a types.PlanExpression tree for a given parser.Expr +func (p *ExecutionPlanner) compileExpr(expr parser.Expr) (_ types.PlanExpression, err error) { + if expr == nil { + return nil, nil + } + + switch expr := expr.(type) { + case *parser.BinaryExpr: + return p.compileBinaryExpr(expr) + + case *parser.BoolLit: + return newBoolLiteralPlanExpression(expr.Value), nil + + case *parser.Call: + return p.compileCallExpr(expr) + + case *parser.CastExpr: + castExpr, err := p.compileExpr(expr.X) + if err != nil { + return nil, err + } + dataType, err := dataTypeFromParserType(expr.Type) + if err != nil { + return nil, err + } + return newCastPlanExpression(castExpr, dataType), nil + + case *parser.Exists: + return nil, sql3.NewErrInternal("exists expressions are not supported") + + case *parser.ExprList: + exprList := []types.PlanExpression{} + for _, e := range expr.Exprs { + listExpr, err := p.compileExpr(e) + if err != nil { + return nil, err + } + exprList = append(exprList, listExpr) + } + return newExprListExpression(exprList), nil + + case *parser.SetLiteralExpr: + exprList := []types.PlanExpression{} + for _, e := range expr.Members { + listExpr, err := p.compileExpr(e) + if err != nil { + return nil, err + } + exprList = append(exprList, listExpr) + } + return newExprSetLiteralPlanExpression(exprList, expr.DataType()), nil + + case *parser.Ident: + return nil, sql3.NewErrInternal("identifiers are not supported") + + case *parser.NullLit: + return newNullLiteralPlanExpression(), nil + + case *parser.IntegerLit: + return newIntLiteralPlanExpression(expr.Value), nil + + case *parser.FloatLit: + return newFloatLiteralPlanExpression(expr.Value), nil + + case *parser.DateLit: + return newDateLiteralPlanExpression(expr.Value), nil + + case *parser.ParenExpr: + return p.compileExpr(expr.X) + + case *parser.QualifiedRef: + ref := newQualifiedRefPlanExpression(parser.IdentName(expr.Table), parser.IdentName(expr.Column), expr.ColumnIndex, expr.DataType()) + p.addReference(ref) + return ref, nil + + case *parser.Range: + lhs, err := p.compileExpr(expr.X) + if err != nil { + return nil, err + } + rhs, err := p.compileExpr(expr.Y) + if err != nil { + return nil, err + } + return newRangeOpPlanExpression(lhs, rhs, expr.ResultDataType), nil + + case *parser.StringLit: + return newStringLiteralPlanExpression(expr.Value), nil + + case *parser.UnaryExpr: + return p.compileUnaryExpr(expr) + + case *parser.CaseExpr: + operand, err := p.compileExpr(expr.Operand) + if err != nil { + return nil, err + } + blocks := []types.PlanExpression{} + for _, b := range expr.Blocks { + block, err := p.compileExpr(b) + if err != nil { + return nil, err + } + blocks = append(blocks, block) + } + + elseExpr, err := p.compileExpr(expr.ElseExpr) + if err != nil { + return nil, err + } + return newCasePlanExpression(operand, blocks, elseExpr, expr.DataType()), nil + + case *parser.CaseBlock: + + condition, err := p.compileExpr(expr.Condition) + if err != nil { + return nil, err + } + body, err := p.compileExpr(expr.Body) + if err != nil { + return nil, err + } + return newCaseBlockPlanExpression(condition, body), nil + + case *parser.SelectStatement: + selOp, err := p.compileSelectStatement(expr, true) + if err != nil { + return nil, err + } + return newSubqueryPlanExpression(selOp), nil + + default: + return nil, sql3.NewErrInternalf("unexpected SQL expression type: %T", expr) + } +} + +func (p *ExecutionPlanner) compileUnaryExpr(expr *parser.UnaryExpr) (_ types.PlanExpression, err error) { + switch op := expr.Op; op { + + //bitwise operators + case parser.BITNOT: + x, err := p.compileExpr(expr.X) + if err != nil { + return nil, err + } + return newUnaryOpPlanExpression(expr.Op, x, expr.ResultDataType), nil + + //arithmetic operators + case parser.PLUS, parser.MINUS: + x, err := p.compileExpr(expr.X) + if err != nil { + return nil, err + } + return newUnaryOpPlanExpression(expr.Op, x, expr.ResultDataType), nil + default: + return nil, sql3.NewErrInternalf("unexpected unary expression operator: %s", expr.Op) + } +} + +func (p *ExecutionPlanner) compileBinaryExpr(expr *parser.BinaryExpr) (_ types.PlanExpression, err error) { + x, err := p.compileExpr(expr.X) + if err != nil { + return nil, err + } + y, err := p.compileExpr(expr.Y) + if err != nil { + return nil, err + } + + switch op := expr.Op; op { + + //logical operators + case parser.AND, parser.OR: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + //equality operators + case parser.EQ, parser.NE: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + //comparison operators + case parser.LT, parser.LE, parser.GT, parser.GE: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + //arithmetic operators + case parser.PLUS, parser.MINUS, parser.STAR, parser.SLASH, parser.REM: + + //TODO(pok) move constant folding to optimizer + opx, okx := x.(*intLiteralPlanExpression) + opy, oky := y.(*intLiteralPlanExpression) + if okx && oky { + //both literals so we can fold + numx, err := strconv.Atoi(opx.value) + if err != nil { + return nil, err + } + numy, err := strconv.Atoi(opy.value) + if err != nil { + return nil, err + } + + switch op { + case parser.PLUS: + value := numx + numy + return newIntLiteralPlanExpression(strconv.Itoa(value)), nil + + case parser.MINUS: + value := numx - numy + return newIntLiteralPlanExpression(strconv.Itoa(value)), nil + + case parser.STAR: + value := numx * numy + return newIntLiteralPlanExpression(strconv.Itoa(value)), nil + + case parser.SLASH: + value := numx / numy + return newIntLiteralPlanExpression(strconv.Itoa(value)), nil + + case parser.REM: + value := numx % numy + return newIntLiteralPlanExpression(strconv.Itoa(value)), nil + + default: + //run home to momma + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + } + } else { + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + } + + //bitwise operators + case parser.BITAND, parser.BITOR, parser.LSHIFT, parser.RSHIFT: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + //null test + case parser.IS, parser.ISNOT: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + case parser.IN, parser.NOTIN: + return newInOpPlanExpression(x, expr.Op, y), nil + + case parser.BETWEEN, parser.NOTBETWEEN: + return newBetweenOpPlanExpression(x, expr.Op, y), nil + + case parser.CONCAT: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + case parser.LIKE, parser.NOTLIKE: + return newBinOpPlanExpression(x, expr.Op, y, expr.ResultDataType), nil + + default: + return nil, sql3.NewErrInternalf("unexpected binary expression operator: %s", expr.Op) + } +} + +func (p *ExecutionPlanner) compileCallExpr(expr *parser.Call) (_ types.PlanExpression, err error) { + args := []types.PlanExpression{} + for _, a := range expr.Args { + arg, err := p.compileExpr(a) + if err != nil { + return nil, err + } + args = append(args, arg) + } + + callName := strings.ToUpper(parser.IdentName(expr.Name)) + switch callName { + case "COUNT": + var agg types.PlanExpression + if expr.Distinct.IsValid() { + agg = newCountDistinctPlanExpression(args[0], expr.ResultDataType) + } else { + agg = newCountPlanExpression(args[0], expr.ResultDataType) + } + p.addAggregate(agg) + return agg, nil + + case "SUM": + agg := newSumPlanExpression(args[0], expr.ResultDataType) + p.addAggregate(agg) + return agg, nil + + case "AVG": + agg := newAvgPlanExpression(args[0], expr.ResultDataType) + p.addAggregate(agg) + return agg, nil + + case "PERCENTILE": + agg := newPercentilePlanExpression(args[0], args[1], expr.ResultDataType) + p.addAggregate(agg) + return agg, nil + + case "MIN": + agg := newMinPlanExpression(args[0], expr.ResultDataType) + p.addAggregate(agg) + return agg, nil + + case "MAX": + agg := newMaxPlanExpression(args[0], expr.ResultDataType) + p.addAggregate(agg) + return agg, nil + + default: + return newCallPlanExpression(parser.IdentName(expr.Name), args, expr.ResultDataType), nil + } +} + +// wildCardToRegexp converts a wildcard pattern to a regular expression pattern. +// used by the LIKE/NOT LIKE operator +func wildCardToRegexp(pattern string) string { + var result strings.Builder + result.WriteString("(?i)") + + rpattern := strings.Replace(pattern, "%", ".*", -1) + rpattern = strings.Replace(rpattern, "_", ".+", -1) + result.WriteString(rpattern) + + return result.String() +} diff --git a/sql3/planner/expressionagg.go b/sql3/planner/expressionagg.go new file mode 100644 index 000000000..c8f04dae4 --- /dev/null +++ b/sql3/planner/expressionagg.go @@ -0,0 +1,708 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "reflect" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// aggregator for the COUNT function +type aggregateCount struct { + count int64 + expr types.PlanExpression +} + +func NewAggCountBuffer(child types.PlanExpression) *aggregateCount { + return &aggregateCount{0, child} +} + +func (c *aggregateCount) Update(ctx context.Context, row types.Row) error { + var inc bool + v, err := c.expr.Evaluate(row) + if v != nil { + inc = true + } + + if err != nil { + return err + } + + if inc { + c.count += 1 + } + return nil +} + +func (c *aggregateCount) Eval(ctx context.Context) (interface{}, error) { + return c.count, nil +} + +// aggregator for the COUNT DISTINCT function +type aggregateCountDistinct struct { + valueSeen map[string]struct{} + expr types.PlanExpression +} + +func NewAggCountDistinctBuffer(child types.PlanExpression) *aggregateCountDistinct { + return &aggregateCountDistinct{make(map[string]struct{}), child} +} + +func (c *aggregateCountDistinct) Update(ctx context.Context, row types.Row) error { + var value interface{} + v, err := c.expr.Evaluate(row) + if v == nil { + return nil + } + + if err != nil { + return err + } + + value = v + + hash := fmt.Sprintf("%v", value) + c.valueSeen[hash] = struct{}{} + + return nil +} + +func (c *aggregateCountDistinct) Eval(ctx context.Context) (interface{}, error) { + return int64(len(c.valueSeen)), nil +} + +// countPlanExpression handles COUNT() +type countPlanExpression struct { + arg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*countPlanExpression)(nil) + +func newCountPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *countPlanExpression { + return &countPlanExpression{ + arg: arg, + returnDataType: returnDataType, + } +} + +func (n *countPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *countPlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggCountBuffer(n), nil +} + +func (n *countPlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_COUNT +} + +func (n *countPlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *countPlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *countPlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *countPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *countPlanExpression) Children() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *countPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// countDistinctPlanExpression handles COUNT(DISTINCT) +type countDistinctPlanExpression struct { + arg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*countDistinctPlanExpression)(nil) + +func newCountDistinctPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *countDistinctPlanExpression { + return &countDistinctPlanExpression{ + arg: arg, + returnDataType: returnDataType, + } +} + +func (n *countDistinctPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *countDistinctPlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggCountDistinctBuffer(n), nil +} + +func (n *countDistinctPlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_COUNT_DISTINCT +} + +func (n *countDistinctPlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *countDistinctPlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *countDistinctPlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *countDistinctPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *countDistinctPlanExpression) Children() []types.PlanExpression { + return nil +} + +func (n *countDistinctPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// aggregator for the SUM function +type aggregateSum struct { + isnil bool + sum float64 + expr types.PlanExpression +} + +func NewAggSumBuffer(child types.PlanExpression) *aggregateSum { + return &aggregateSum{true, float64(0), child} +} + +func (m *aggregateSum) Update(ctx context.Context, row types.Row) error { + v, err := m.expr.Evaluate(row) + if err != nil { + return err + } + + if v == nil { + return nil + } + + var val interface{} = 0 + + if m.isnil { + m.sum = 0 + m.isnil = false + } + + m.sum += val.(float64) + + //return nil + return sql3.NewErrInternalf("implement me") +} + +func (m *aggregateSum) Eval(ctx context.Context) (interface{}, error) { + if m.isnil { + return nil, nil + } + return m.sum, nil +} + +// sumPlanExpression handles SUM() +type sumPlanExpression struct { + arg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*sumPlanExpression)(nil) + +func newSumPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *sumPlanExpression { + return &sumPlanExpression{ + arg: arg, + returnDataType: returnDataType, + } +} + +func (n *sumPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *sumPlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggSumBuffer(n), nil +} + +func (n *sumPlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_SUM +} + +func (n *sumPlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *sumPlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *sumPlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *sumPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *sumPlanExpression) Children() []types.PlanExpression { + return nil +} + +func (n *sumPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// aggregator for AVG +type aggregateAvg struct { + sum float64 + rows int64 + expr types.PlanExpression +} + +func NewAggAvgBuffer(child types.PlanExpression) *aggregateAvg { + const ( + sum = float64(0) + rows = int64(0) + ) + return &aggregateAvg{sum, rows, child} +} + +func (a *aggregateAvg) Update(ctx context.Context, row types.Row) error { + v, err := a.expr.Evaluate(row) + if err != nil { + return err + } + + if v == nil { + return nil + } + a.sum += v.(float64) + a.rows += 1 + + //return nil + return sql3.NewErrInternalf("implement me") +} + +func (a *aggregateAvg) Eval(ctx context.Context) (interface{}, error) { + // This case is triggered when no rows exist. + if a.sum == 0 && a.rows == 0 { + return nil, nil + } + + if a.rows == 0 { + return float64(0), nil + } + + return a.sum / float64(a.rows), nil +} + +// avgPlanExpression handles AVG() +type avgPlanExpression struct { + arg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*avgPlanExpression)(nil) + +func newAvgPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *avgPlanExpression { + return &avgPlanExpression{ + arg: arg, + returnDataType: returnDataType, + } +} + +func (n *avgPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *avgPlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggAvgBuffer(n), nil +} + +func (n *avgPlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_AVG +} + +func (n *avgPlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *avgPlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *avgPlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *avgPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *avgPlanExpression) Children() []types.PlanExpression { + return nil +} + +func (n *avgPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// aggregator for MIN +type aggreagateMin struct { + val interface{} + expr types.PlanExpression +} + +func NewAggMinBuffer(child types.PlanExpression) *aggreagateMin { + return &aggreagateMin{nil, child} +} + +func (m *aggreagateMin) Update(ctx context.Context, row types.Row) error { + v, err := m.expr.Evaluate(row) + if err != nil { + return err + } + + if reflect.TypeOf(v) == nil { + return nil + } + + if m.val == nil { + m.val = v + return nil + } + + //return nil + return sql3.NewErrInternalf("implement me") + +} + +func (m *aggreagateMin) Eval(ctx context.Context) (interface{}, error) { + return m.val, nil +} + +// minPlanExpression handles MIN() +type minPlanExpression struct { + arg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*minPlanExpression)(nil) + +func newMinPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *minPlanExpression { + return &minPlanExpression{ + arg: arg, + returnDataType: returnDataType, + } +} + +func (n *minPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *minPlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggMinBuffer(n), nil +} + +func (n *minPlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_MIN +} + +func (n *minPlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *minPlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *minPlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *minPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *minPlanExpression) Children() []types.PlanExpression { + return nil +} + +func (n *minPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// aggregator for MAX +type aggregateMax struct { + val interface{} + expr types.PlanExpression +} + +func NewAggMaxBuffer(child types.PlanExpression) *aggregateMax { + return &aggregateMax{nil, child} +} + +func (m *aggregateMax) Update(ctx context.Context, row types.Row) error { + v, err := m.expr.Evaluate(row) + if err != nil { + return err + } + + if reflect.TypeOf(v) == nil { + return nil + } + + if m.val == nil { + m.val = v + return nil + } + + //return nil + return sql3.NewErrInternalf("implement me") + +} + +func (m *aggregateMax) Eval(ctx context.Context) (interface{}, error) { + return m.val, nil +} + +// maxPlanExpression handles MAX() +type maxPlanExpression struct { + arg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*maxPlanExpression)(nil) + +func newMaxPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *maxPlanExpression { + return &maxPlanExpression{ + arg: arg, + returnDataType: returnDataType, + } +} + +func (n *maxPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *maxPlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggMaxBuffer(n), nil +} + +func (n *maxPlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_MAX +} + +func (n *maxPlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *maxPlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{} +} + +func (n *maxPlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *maxPlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *maxPlanExpression) Children() []types.PlanExpression { + return nil +} + +func (n *maxPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +// percentilePlanExpression handles PERCENTILE() +type percentilePlanExpression struct { + arg types.PlanExpression + nthArg types.PlanExpression + returnDataType parser.ExprDataType +} + +var _ types.Aggregable = (*percentilePlanExpression)(nil) + +func newPercentilePlanExpression(arg types.PlanExpression, nthArg types.PlanExpression, returnDataType parser.ExprDataType) *percentilePlanExpression { + return &percentilePlanExpression{ + arg: arg, + nthArg: nthArg, + returnDataType: returnDataType, + } +} + +func (n *percentilePlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) { + arg, ok := n.arg.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.arg) + } + return currentRow[arg.columnIndex], nil +} + +func (n *percentilePlanExpression) NewBuffer() (types.AggregationBuffer, error) { + return NewAggCountBuffer(n), nil +} + +func (n *percentilePlanExpression) AggType() types.AggregateFunctionType { + return types.AGGREGATE_PERCENTILE +} + +func (n *percentilePlanExpression) AggExpression() types.PlanExpression { + return n.arg +} + +func (n *percentilePlanExpression) AggAdditionalExpr() []types.PlanExpression { + return []types.PlanExpression{ + n.nthArg, + } +} + +func (n *percentilePlanExpression) Type() parser.ExprDataType { + return n.returnDataType +} + +func (n *percentilePlanExpression) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_expr"] = fmt.Sprintf("%T", n) + result["dataType"] = n.Type().TypeName() + result["arg"] = n.arg.Plan() + return result +} + +func (n *percentilePlanExpression) Children() []types.PlanExpression { + return nil +} + +func (n *percentilePlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) { + return n, nil +} + +//aggregator for last +type aggregateLast struct { + val interface{} + expr types.PlanExpression +} + +func NewAggLastBuffer(child types.PlanExpression) *aggregateLast { + return &aggregateLast{nil, child} +} + +func (l *aggregateLast) Update(ctx context.Context, row types.Row) error { + v, err := l.expr.Evaluate(row) + if err != nil { + return err + } + + if v == nil { + return nil + } + + l.val = v + return nil +} + +func (l *aggregateLast) Eval(ctx context.Context) (interface{}, error) { + return l.val, nil +} + +// aggregator for first +type aggregateFirst struct { + val interface{} + expr types.PlanExpression +} + +func NewFirstBuffer(child types.PlanExpression) *aggregateFirst { + return &aggregateFirst{nil, child} +} + +func (f *aggregateFirst) Update(ctx context.Context, row types.Row) error { + if f.val != nil { + return nil + } + + v, err := f.expr.Evaluate(row) + if err != nil { + return err + } + + if v == nil { + return nil + } + + f.val = v + + return nil +} + +func (f *aggregateFirst) Eval(ctx context.Context) (interface{}, error) { + return f.val, nil +} diff --git a/sql3/planner/expressionanalyzer.go b/sql3/planner/expressionanalyzer.go new file mode 100644 index 000000000..3d3c0180c --- /dev/null +++ b/sql3/planner/expressionanalyzer.go @@ -0,0 +1,687 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +// analyze a parser.Expr. returns the analyzed parser.Expr +func (p *ExecutionPlanner) analyzeExpression(expr parser.Expr, scope parser.Statement) (parser.Expr, error) { + if expr == nil { + return nil, nil + } + + switch e := expr.(type) { + case *parser.BinaryExpr: + return p.analyzeBinaryExpression(e, scope) + + case *parser.BoolLit: + return e, nil + + case *parser.Call: + return p.analyzeCallExpression(e, scope) + + case *parser.CastExpr: + analyzedExpr, err := p.analyzeExpression(e.X, scope) + if err != nil { + return nil, err + } + + targetType, err := dataTypeFromParserType(e.Type) + if err != nil { + return nil, err + } + if !typesCanBeCast(analyzedExpr.DataType(), targetType) { + return nil, sql3.NewErrInvalidCast(analyzedExpr.Pos().Line, analyzedExpr.Pos().Column, analyzedExpr.DataType().TypeName(), targetType.TypeName()) + } + e.X = analyzedExpr + e.ResultDataType = targetType + return e, nil + + case *parser.ExprList: + for i, ex := range e.Exprs { + listExpr, err := p.analyzeExpression(ex, scope) + if err != nil { + return nil, err + } + e.Exprs[i] = listExpr + } + return e, nil + + case *parser.Ident: + switch sc := scope.(type) { + case *parser.SelectStatement: + // turn *parser.Ident into *parser.QualifiedRef + if sc.Source == nil { + return nil, sql3.NewErrColumnNotFound(e.NamePos.Line, e.NamePos.Column, e.Name) + } + + // go find the first ident in the source that matches + oc, err := sc.Source.OutputColumnNamed(e.Name) + if err != nil { + return nil, err + } else if oc == nil { + return nil, sql3.NewErrColumnNotFound(e.NamePos.Line, e.NamePos.Column, e.Name) + } + + ident := &parser.QualifiedRef{ + Table: &parser.Ident{ + Name: oc.TableName, + NamePos: e.NamePos, + }, + Column: &parser.Ident{ + Name: oc.ColumnName, + NamePos: e.NamePos, + }, + ColumnIndex: oc.ColumnIndex, + } + return p.analyzeExpression(ident, scope) + + case *parser.InsertStatement: + return nil, sql3.NewErrColumnNotFound(e.NamePos.Line, e.NamePos.Column, e.Name) + + default: + return nil, sql3.NewErrInternalf("unhandled scope type '%T'", sc) + } + + case *parser.NullLit: + return e, nil + + case *parser.IntegerLit: + return e, nil + + case *parser.FloatLit: + return e, nil + + case *parser.StringLit: + return e, nil + + case *parser.DateLit: + return e, nil + + case *parser.ParenExpr: + pexpr, err := p.analyzeExpression(e.X, scope) + if err != nil { + return nil, err + } + e.X = pexpr + return e, nil + + case *parser.SetLiteralExpr: + for i, ex := range e.Members { + listExpr, err := p.analyzeExpression(ex, scope) + if err != nil { + return nil, err + } + e.Members[i] = listExpr + } + + if len(e.Members) == 0 { + return nil, sql3.NewErrLiteralEmptySetNotAllowed(e.Lbracket.Line, e.Lbracket.Column) + } + + setDataType := e.Members[0].DataType() + switch setDataType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt: + //make sure everything else is an int + for _, mbr := range e.Members { + if !typeIsInteger(mbr.DataType()) { + return nil, sql3.NewErrIntExpressionExpected(mbr.Pos().Line, mbr.Pos().Column) + } + } + e.ResultDataType = parser.NewDataTypeIDSet() + + case *parser.DataTypeString: + //make sure everything else is a string + for _, mbr := range e.Members { + if !typeIsString(mbr.DataType()) { + return nil, sql3.NewErrStringExpressionExpected(mbr.Pos().Line, mbr.Pos().Column) + } + } + e.ResultDataType = parser.NewDataTypeStringSet() + + default: + return nil, sql3.NewErrSetLiteralMustContainIntOrString(e.Members[0].Pos().Line, e.Members[0].Pos().Column) + } + + return e, nil + + case *parser.QualifiedRef: + switch sc := scope.(type) { + case *parser.SelectStatement: + + if e.Table.Name == "" { + // there is no table or alias name in the qualifier so go look for the first matching column from any of the sources + oc, err := sc.Source.OutputColumnNamed(e.Column.Name) + if err != nil { + return nil, err + } + if oc != nil { + e.RefDataType = oc.Datatype + e.ColumnIndex = oc.ColumnIndex + return e, nil + + } + return nil, sql3.NewErrColumnNotFound(e.Column.NamePos.Line, e.Column.NamePos.Column, e.Column.Name) + + } else { + oc, err := sc.Source.OutputColumnQualifierNamed(e.Table.Name, e.Column.Name) + if err != nil { + return nil, err + } + if oc != nil { + e.RefDataType = oc.Datatype + e.ColumnIndex = oc.ColumnIndex + return e, nil + + } + return nil, sql3.NewErrColumnNotFound(e.Column.NamePos.Line, e.Column.NamePos.Column, e.Column.Name) + } + + default: + return nil, sql3.NewErrInternalf("unhandled scope type '%T'", sc) + } + + case *parser.Range: + return p.analyzeRangeExpression(e, scope) + + case *parser.CaseExpr: + operand, err := p.analyzeExpression(e.Operand, scope) + if err != nil { + return nil, err + } + e.Operand = operand + + for i, ex := range e.Blocks { + block, err := p.analyzeCaseBlockExpression(ex, e, scope) + if err != nil { + return nil, err + } + e.Blocks[i] = block + } + + elseExpr, err := p.analyzeExpression(e.ElseExpr, scope) + if err != nil { + return nil, err + } + e.ElseExpr = elseExpr + + //type checking... + if e.Operand != nil { + //we are "case expr when" form, so need to make sure that 'expr' and all block conditions are equatable + for _, blk := range e.Blocks { + if !typesAreComparable(e.Operand.DataType(), blk.Condition.DataType()) { + return nil, sql3.NewErrTypesAreNotEquatable(blk.Condition.Pos().Line, blk.Condition.Pos().Column, e.Operand.DataType().TypeName(), blk.Condition.DataType().TypeName()) + } + } + } else { + //we are "case when" form, so need to make sure that all block conditions are bool + for _, blk := range e.Blocks { + if !typeIsBool(blk.Condition.DataType()) { + return nil, sql3.NewErrBooleanExpressionExpected(blk.Condition.Pos().Line, blk.Condition.Pos().Column) + } + } + } + + if len(e.Blocks) == 0 { + return nil, sql3.NewErrInternalf("unexpected case blocks length") + } + + //set the result type for the case to the type of the first block + caseType := e.Blocks[0].DataType() + + //now check all the other blocks to make sure that each body is assignment compatible with that type + for _, blk := range e.Blocks { + if !typesAreAssignmentCompatible(caseType, blk.Body.DataType()) { + return nil, sql3.NewErrTypeAssignmentIncompatible(blk.Body.Pos().Line, blk.Body.Pos().Column, caseType.TypeName(), blk.Body.DataType().TypeName()) + } + } + + //if there is an else check that too + if e.ElseExpr != nil { + if !typesAreAssignmentCompatible(caseType, e.ElseExpr.DataType()) { + return nil, sql3.NewErrTypeAssignmentIncompatible(e.ElseExpr.Pos().Line, e.ElseExpr.Pos().Column, caseType.TypeName(), e.ElseExpr.DataType().TypeName()) + } + } + + e.ResultDataType = caseType + + return e, nil + + case *parser.UnaryExpr: + return p.analyzeUnaryExpression(e, scope) + + case *parser.SelectStatement: + err := p.analyzeSelectStatement(e) + if err != nil { + return nil, err + } + // if we return more than one column + if len(e.Columns) > 1 { + return nil, sql3.NewErrInternalf("subquery must return only one column") + } + return e, nil + + default: + return nil, sql3.NewErrInternalf("unexpected SQL expression type: %T", expr) + } +} + +func (p *ExecutionPlanner) analyzeUnaryExpression(expr *parser.UnaryExpr, scope parser.Statement) (parser.Expr, error) { + + x, err := p.analyzeExpression(expr.X, scope) + if err != nil { + return nil, err + } + expr.X = x + + switch op := expr.Op; op { + + //bitwise operators + case parser.BITNOT: + if !typeIsCompatibleWithBitwiseOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithBitwiseOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + expr.ResultDataType = x.DataType() + return expr, nil + + //arithmetic operators + case parser.PLUS, parser.MINUS: + if !typeIsCompatibleWithArithmeticOperator(x.DataType(), op) { + return nil, sql3.NewErrTypeIncompatibleWithArithmeticOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if typeIsInteger(x.DataType()) { + expr.ResultDataType = parser.NewDataTypeInt() + } else if typeIsFloat(x.DataType()) { + fd, ok := x.DataType().(*parser.DataTypeDecimal) + if !ok { + return nil, sql3.NewErrInternalf("unexpected data type") + } + expr.ResultDataType = fd + } else { + return nil, sql3.NewErrInternalf("unexpected unary expression type: %T", x.DataType()) + } + return expr, nil + + default: + return nil, sql3.NewErrInternalf("unexpected unary expression operator: %s", op) + } +} + +func (p *ExecutionPlanner) analyzeBinaryExpression(expr *parser.BinaryExpr, scope parser.Statement) (parser.Expr, error) { + + //analyze both sides first + x, err := p.analyzeExpression(expr.X, scope) + if err != nil { + return nil, err + } + expr.X = x + y, err := p.analyzeExpression(expr.Y, scope) + if err != nil { + return nil, err + } + expr.Y = y + + //handle operator + switch op := expr.Op; op { + + //logical operators + case parser.AND, parser.OR: + if !typeIsCompatibleWithLogicalOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithLogicalOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithLogicalOperator(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithLogicalOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + //logical operator so type of expr is bool + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + //equality operators + case parser.EQ, parser.NE: + if !typeIsCompatibleWithEqualityOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithEqualityOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithEqualityOperator(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithEqualityOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + if !typesAreComparable(x.DataType(), y.DataType()) { + return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeName(), y.DataType().TypeName()) + } + //equality operator so type of expr is bool + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + //comparison operators + case parser.LT, parser.LE, parser.GT, parser.GE: + if !typeIsCompatibleWithComparisonOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithComparisonOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithComparisonOperator(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithComparisonOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + if !typesAreComparable(x.DataType(), y.DataType()) { + return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeName(), y.DataType().TypeName()) + } + //comparison operator so type of expr is bool + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + //arithmetic operators + case parser.PLUS, parser.MINUS, parser.STAR, parser.SLASH, parser.REM: + if !typeIsCompatibleWithArithmeticOperator(x.DataType(), op) { + return nil, sql3.NewErrTypeIncompatibleWithArithmeticOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithArithmeticOperator(y.DataType(), op) { + return nil, sql3.NewErrTypeIncompatibleWithArithmeticOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + + coercedType, err := typesCoercedForArithmeticOperator(x.DataType(), y.DataType(), x.Pos()) + if err != nil { + return nil, err + } + expr.ResultDataType = coercedType + return expr, nil + + /* + opx, okx := x.(*NumLiteralPlanExpresssion) + opy, oky := y.(*NumLiteralPlanExpresssion) + if okx && oky { + //both literals so we can fold + numx, err := strconv.Atoi(opx.value) + if err != nil { + return nil, err + } + numy, err := strconv.Atoi(opy.value) + if err != nil { + return nil, err + } + + switch op { + case parser.PLUS: + value := numx + numy + return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil + + case parser.MINUS: + value := numx - numy + return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil + + case parser.STAR: + value := numx * numy + return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil + + case parser.SLASH: + value := numx / numy + return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil + + case parser.REM: + value := numx % numy + return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil + + default: + //run home to momma + return NewBinOpPlanExpression(p, x, expr.Op, y), nil + } + } else { + return NewBinOpPlanExpression(p, x, expr.Op, y), nil + }*/ + + //bitwise operators + case parser.BITAND, parser.BITOR, parser.LSHIFT, parser.RSHIFT: + if !typeIsCompatibleWithBitwiseOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithBitwiseOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithBitwiseOperator(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithBitwiseOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + coercedType, err := typesCoercedForBitwiseOperator(x.DataType(), y.DataType(), x.Pos()) + if err != nil { + return nil, err + } + expr.ResultDataType = coercedType + return expr, nil + + /* + opx, okx := x.(*NumLiteralPlanExpression) + opy, oky := y.(*NumLiteralPlanExpression) + if okx && oky { + //both literals so we can fold + numx, err := strconv.Atoi(opx.value) + if err != nil { + return nil, err + } + numy, err := strconv.Atoi(opy.value) + if err != nil { + return nil, err + } + + switch op { + case parser.PLUS: + value := numx + numy + return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil + + case parser.MINUS: + value := numx - numy + return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil + + case parser.STAR: + value := numx * numy + return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil + + case parser.SLASH: + value := numx / numy + return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil + + case parser.REM: + value := numx % numy + return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil + + default: + //run home to momma + return newBinOpPlanExpression(x, expr.Op, y), nil + } + } else { + return newBinOpPlanExpression(x, expr.Op, y), nil + }*/ + + //null test + case parser.IS, parser.ISNOT: + _, ok := expr.Y.(*parser.NullLit) + if !ok { + return nil, sql3.NewErrInternalf("NULL expected") + } + //no type check against null...logical operator so type of expr is bool + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + case parser.IN, parser.NOTIN: + lst, ok := y.(*parser.ExprList) + if !ok { + return nil, sql3.NewErrExpressionListExpected(y.Pos().Line, y.Pos().Column) + } + + for idx, ex := range lst.Exprs { + + //check to see if our expression is a select statement + //if it is it needs special handling + sel, ok := ex.(*parser.SelectStatement) + if ok { + //we have a select in the expression list so make sure it is the only thing in the expression list + if len(lst.Exprs) > 1 { + return nil, sql3.NewErrInternalf("expresion list should only contain one select statement") + } + //make sure select only returns one column + if len(sel.Columns) > 1 { + return nil, sql3.NewErrInternalf("select used as part of IN expression should only return one column") + } + if !typesAreComparable(x.DataType(), sel.Columns[0].Expr.DataType()) { + return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeName(), ex.DataType().TypeName()) + } + + //need to turn this into an inner join + selStmt, ok := scope.(*parser.SelectStatement) + if !ok { + return nil, sql3.NewErrInternalf("unexpected scope type '%T'", scope) + } + + operator := &parser.JoinOperator{ + Inner: expr.OpPos, + } + + constraint := &parser.OnConstraint{ + X: &parser.BinaryExpr{ + X: expr.X, + Op: parser.EQ, + Y: sel.Columns[0].Expr, + }, + } + + if lhs, ok := selStmt.Source.(*parser.JoinClause); ok { + selStmt.Source = &parser.JoinClause{ + X: lhs.X, + Operator: lhs.Operator, + Y: &parser.JoinClause{ + X: lhs.Y, + Operator: operator, + Y: sel, + Constraint: constraint, + }, + Constraint: lhs.Constraint, + } + } else { + selStmt.Source = &parser.JoinClause{ + X: selStmt.Source, + Operator: operator, + Y: sel, + Constraint: constraint, + } + } + return nil, nil + } + + //not a sql statement + + //handle the case of of tthe LHS of the expression being a timestamp, the RHS being a string literal + //if so, try to coerce to a timestamp + if typeIsTimestamp(x.DataType()) && typeIsString(ex.DataType()) && ex.IsLiteral() { + litExpr, ok := ex.(*parser.StringLit) + if ok { + tsLit := litExpr.ConvertToTimestamp() + if tsLit != nil { + ex = tsLit + lst.Exprs[idx] = ex + } + } + } + + //make sure LHS and RHS types are comparable + if !typesAreComparable(x.DataType(), ex.DataType()) { + return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeName(), ex.DataType().TypeName()) + } + } + + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + case parser.BETWEEN, parser.NOTBETWEEN: + if !typeIsRange(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithBetweenOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + + ok, err := typesAreRangeComparable(x.DataType(), y.DataType()) + if err != nil { + return nil, err + } + if !ok { + return nil, sql3.NewErrTypeIncompatibleWithBetweenOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + case parser.CONCAT: + if !typeIsCompatibleWithConcatOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithConcatOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithConcatOperator(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithConcatOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + expr.ResultDataType = parser.NewDataTypeString() + return expr, nil + + case parser.LIKE, parser.NOTLIKE: + if !typeIsCompatibleWithLikeOperator(x.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithLikeOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeName()) + } + if !typeIsCompatibleWithLikeOperator(y.DataType()) { + return nil, sql3.NewErrTypeIncompatibleWithLikeOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeName()) + } + //comparison operator so type of expr is bool + expr.ResultDataType = parser.NewDataTypeBool() + return expr, nil + + default: + return nil, sql3.NewErrInternalf("unexpected binary expression operator: %s", op) + } +} + +func (p *ExecutionPlanner) analyzeRangeExpression(expr *parser.Range, scope parser.Statement) (parser.Expr, error) { + //analyze subscripts + x, err := p.analyzeExpression(expr.X, scope) + if err != nil { + return nil, err + } + expr.X = x + y, err := p.analyzeExpression(expr.Y, scope) + if err != nil { + return nil, err + } + expr.Y = y + + //check to see if we have string literals that are actually dates + xLiteral, ok := x.(*parser.StringLit) + if ok { + tsLiteral := xLiteral.ConvertToTimestamp() + if tsLiteral != nil { + expr.X = tsLiteral + } + } + + yLiteral, ok := y.(*parser.StringLit) + if ok { + tsLiteral := yLiteral.ConvertToTimestamp() + if tsLiteral != nil { + expr.Y = tsLiteral + } + } + + if !typeCanBeUsedInRange(expr.X.DataType()) { + return nil, sql3.NewErrTypeCannotBeUsedAsRangeSubscript(expr.X.Pos().Line, expr.X.Pos().Column, expr.X.DataType().TypeName()) + } + if !typeCanBeUsedInRange(expr.Y.DataType()) { + return nil, sql3.NewErrTypeCannotBeUsedAsRangeSubscript(expr.Y.Pos().Line, expr.Y.Pos().Column, expr.Y.DataType().TypeName()) + } + if !typesOfRangeBoundsAreTheSame(expr.X.DataType(), expr.Y.DataType()) { + return nil, sql3.NewErrIncompatibleTypesForRangeSubscripts(expr.Pos().Line, expr.Pos().Column, expr.X.DataType().TypeName(), expr.Y.DataType().TypeName()) + } + + expr.ResultDataType = parser.NewDataTypeRange(expr.X.DataType()) + + return expr, nil +} + +func (p *ExecutionPlanner) analyzeCaseBlockExpression(expr *parser.CaseBlock, caseScope *parser.CaseExpr, scope parser.Statement) (*parser.CaseBlock, error) { + x, err := p.analyzeExpression(expr.Body, scope) + if err != nil { + return nil, err + } + expr.Body = x + y, err := p.analyzeExpression(expr.Condition, scope) + if err != nil { + return nil, err + } + expr.Condition = y + + return expr, nil +} diff --git a/sql3/planner/expressionanalyzercall.go b/sql3/planner/expressionanalyzercall.go new file mode 100644 index 000000000..f560a3d4b --- /dev/null +++ b/sql3/planner/expressionanalyzercall.go @@ -0,0 +1,246 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "strings" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +// analyze a *parser.Call and return the parser.Expr +func (p *ExecutionPlanner) analyzeCallExpression(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + //analyze all the args + for i, a := range call.Args { + arg, err := p.analyzeExpression(a, scope) + if err != nil { + return nil, err + } + call.Args[i] = arg + } + switch strings.ToUpper(call.Name.Name) { + case "COUNT": + //check to see if we have a star, if we do turn it into a qualified ref to _id + if call.Star.IsValid() && len(call.Args) == 0 { + newArg := &parser.Ident{ + NamePos: call.Star, + Name: "_id", + } + arg, err := p.analyzeExpression(newArg, scope) + if err != nil { + return nil, err + } + call.Args = append(call.Args, arg) + } + // one argument only + if len(call.Args) != 1 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args)) + } + //make sure it's a qualified ref + _, ok := call.Args[0].(*parser.QualifiedRef) + if !ok { + return nil, sql3.NewErrExpectedColumnReference(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + //COUNT always returns int + call.ResultDataType = parser.NewDataTypeInt() + + case "SUM": + // can't do a sum on * + if call.Star.IsValid() && len(call.Args) == 0 { + return nil, sql3.NewErrExpectedColumnReference(call.Star.Line, call.Star.Column) + } + // one argument only + if len(call.Args) != 1 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args)) + } + + //make sure it's a qualified ref + ref, ok := call.Args[0].(*parser.QualifiedRef) + if !ok { + return nil, sql3.NewErrExpectedColumnReference(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + //can't do a sum on _id + if strings.EqualFold(ref.Column.Name, "_id") { + return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Name.Name) + } + + //make sure the ref is sum-able + if !(typeIsInteger(ref.DataType()) || typeIsFloat(ref.DataType())) { + return nil, sql3.NewErrIntOrDecimalExpressionExpected(ref.Table.NamePos.Line, ref.Table.NamePos.Column) + } + + call.ResultDataType = ref.DataType() + + case "AVG": + // can't do an avg on a * + if call.Star.IsValid() && len(call.Args) == 0 { + return nil, sql3.NewErrExpectedColumnReference(call.Star.Line, call.Star.Column) + } + + // one argument only + if len(call.Args) != 1 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args)) + } + + ref, ok := call.Args[0].(*parser.QualifiedRef) + if !ok { + return nil, sql3.NewErrExpectedColumnReference(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + //can't do a avg on _id + if strings.EqualFold(ref.Column.Name, "_id") { + return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Name.Name) + } + + //make sure the ref is avg-able + if !(typeIsInteger(ref.DataType()) || typeIsFloat(ref.DataType())) { + return nil, sql3.NewErrIntOrDecimalExpressionExpected(ref.Table.NamePos.Line, ref.Table.NamePos.Column) + } + + call.ResultDataType = parser.NewDataTypeDecimal(4) + + case "PERCENTILE": + // can't do an percentile on a * + if call.Star.IsValid() && len(call.Args) == 0 { + return nil, sql3.NewErrExpectedColumnReference(call.Star.Line, call.Star.Column) + } + + if len(call.Args) != 2 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args)) + } + + //first arg should be a qualified ref + ref, ok := call.Args[0].(*parser.QualifiedRef) + if !ok { + return nil, sql3.NewErrExpectedColumnReference(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + //can't do a percentile on _id + if strings.EqualFold(ref.Column.Name, "_id") { + return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Name.Name) + } + + //make sure the ref is percentilable-able + if !(typeIsInteger(ref.DataType()) || typeIsFloat(ref.DataType()) || typeIsTimestamp(ref.DataType())) { + return nil, sql3.NewErrIntOrDecimalOrTimestampExpressionExpected(ref.Table.NamePos.Line, ref.Table.NamePos.Column) + } + + //second column is the nth value + targetType := parser.NewDataTypeDecimal(4) + if !typesAreAssignmentCompatible(targetType, call.Args[1].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[1].Pos().Line, call.Args[1].Pos().Column, targetType.TypeName(), call.Args[1].DataType().TypeName()) + } + + //make sure it's literal + if !call.Args[1].IsLiteral() { + return nil, sql3.NewErrLiteralExpected(call.Args[1].Pos().Line, call.Args[1].Pos().Column) + } + + //return the data type of the referenced column + call.ResultDataType = ref.DataType() + + case "MIN", "MAX": + // can't do an min/max on a * + if call.Star.IsValid() && len(call.Args) == 0 { + return nil, sql3.NewErrExpectedColumnReference(call.Star.Line, call.Star.Column) + } + + // one argument + if len(call.Args) != 1 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args)) + } + + // first arg should be a qualified ref + ref, ok := call.Args[0].(*parser.QualifiedRef) + if !ok { + return nil, sql3.NewErrExpectedColumnReference(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + // can't do a min/max on _id + if strings.EqualFold(ref.Column.Name, "_id") { + return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Name.Name) + } + + // make sure the ref is min/max-able + if !(typeIsInteger(ref.DataType()) || typeIsFloat(ref.DataType()) || typeIsTimestamp(ref.DataType())) { + return nil, sql3.NewErrIntOrDecimalOrTimestampExpressionExpected(ref.Table.NamePos.Line, ref.Table.NamePos.Column) + } + + // return the data type of the referenced column + call.ResultDataType = ref.DataType() + + case "SETCONTAINS": + // two arguments + if len(call.Args) != 2 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, len(call.Args)) + } + + ok, baseType := typeIsSet(call.Args[0].DataType()) + if !ok { + return nil, sql3.NewErrSetExpressionExpected(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + if !typesAreComparable(baseType, call.Args[1].DataType()) { + return nil, sql3.NewErrTypesAreNotEquatable(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Args[0].DataType().TypeName(), call.Args[1].DataType().TypeName()) + } + call.ResultDataType = parser.NewDataTypeBool() + + case "SETCONTAINSALL": + //two arguments + if len(call.Args) != 2 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, len(call.Args)) + } + + // first arg should be set + ok, baseType1 := typeIsSet(call.Args[0].DataType()) + if !ok { + return nil, sql3.NewErrSetExpressionExpected(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + // second arg should be set + ok, baseType2 := typeIsSet(call.Args[1].DataType()) + if !ok { + return nil, sql3.NewErrSetExpressionExpected(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + //types from both set should be comparable + if !typesAreComparable(baseType1, baseType2) { + return nil, sql3.NewErrTypesAreNotEquatable(call.Args[1].Pos().Line, call.Args[1].Pos().Column, baseType1.TypeName(), baseType2.TypeName()) + } + + call.ResultDataType = parser.NewDataTypeBool() + + case "SETCONTAINSANY": + if len(call.Args) != 2 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, len(call.Args)) + } + + // first arg should be set + ok, baseType1 := typeIsSet(call.Args[0].DataType()) + if !ok { + return nil, sql3.NewErrSetExpressionExpected(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + // second arg should be set + ok, baseType2 := typeIsSet(call.Args[1].DataType()) + if !ok { + return nil, sql3.NewErrSetExpressionExpected(call.Args[0].Pos().Line, call.Args[0].Pos().Column) + } + + //types from both set should be comparable + if !typesAreComparable(baseType1, baseType2) { + return nil, sql3.NewErrTypesAreNotEquatable(call.Args[1].Pos().Line, call.Args[1].Pos().Column, baseType1.TypeName(), baseType2.TypeName()) + } + + call.ResultDataType = parser.NewDataTypeBool() + + case "DATEPART": + return p.analyzeFunctionDatePart(call, scope) + + default: + return nil, sql3.NewErrCallUnknownFunction(call.Name.NamePos.Line, call.Name.NamePos.Column, call.Name.Name) + } + return call, nil +} diff --git a/sql3/planner/expressionpql.go b/sql3/planner/expressionpql.go new file mode 100644 index 000000000..1f8b4cec7 --- /dev/null +++ b/sql3/planner/expressionpql.go @@ -0,0 +1,226 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "strconv" + "strings" + + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// generatePQLCallFromExpr returns a *pql.Call tree for a given plan expression +func (p *ExecutionPlanner) generatePQLCallFromExpr(ctx context.Context, expr types.PlanExpression) (_ *pql.Call, err error) { + if expr == nil { + return nil, nil + } + + switch expr := expr.(type) { + case *binOpPlanExpression: + return p.generatePQLCallFromBinaryExpr(ctx, expr) + + case *callPlanExpression: + switch strings.ToUpper(expr.name) { + case "SETCONTAINS": + col := expr.args[0].(*qualifiedRefPlanExpression) + + pqlValue, err := planExprToValue(expr.args[1]) + if err != nil { + return nil, err + } + return &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + col.columnName: pqlValue, + }, + }, nil + + case "SETCONTAINSALL": + col := expr.args[0].(*qualifiedRefPlanExpression) + + set, ok := expr.args[1].(*exprSetLiteralPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected argument type '%T'", expr.args[1]) + } + + call := &pql.Call{ + Name: "Intersect", + Children: []*pql.Call{}, + } + + for _, m := range set.members { + pqlValue, err := planExprToValue(m) + if err != nil { + return nil, err + } + rc := &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + col.columnName: pqlValue, + }, + } + call.Children = append(call.Children, rc) + } + return call, nil + + case "SETCONTAINSANY": + col := expr.args[0].(*qualifiedRefPlanExpression) + + set, ok := expr.args[1].(*exprSetLiteralPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected argument type '%T'", expr.args[1]) + } + + call := &pql.Call{ + Name: "Union", + Children: []*pql.Call{}, + } + + for _, m := range set.members { + pqlValue, err := planExprToValue(m) + if err != nil { + return nil, err + } + rc := &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + col.columnName: pqlValue, + }, + } + call.Children = append(call.Children, rc) + } + return call, nil + + default: + return nil, sql3.NewErrInternalf("unsupported scalar function '%s'", expr.name) + } + + default: + return nil, sql3.NewErrInternalf("unexpected expression type: %T", expr) + } +} + +func (p *ExecutionPlanner) generatePQLCallFromBinaryExpr(ctx context.Context, expr *binOpPlanExpression) (_ *pql.Call, err error) { + switch op := expr.op; op { + case parser.AND, parser.OR: + name := "Intersect" + if op == parser.OR { + name = "Union" + } + + x, err := p.generatePQLCallFromExpr(ctx, expr.lhs) + if err != nil { + return nil, err + } + y, err := p.generatePQLCallFromExpr(ctx, expr.rhs) + if err != nil { + return nil, err + } + + return &pql.Call{ + Name: name, + Children: []*pql.Call{x, y}, + }, nil + + case parser.EQ, parser.NE, parser.LT, parser.LE, parser.GT, parser.GE: + lhs, ok := expr.lhs.(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected lhs %T", expr.lhs) + } + + pqlValue, err := planExprToValue(expr.rhs) + if err != nil { + return nil, err + } + + switch typ := expr.lhs.Type().(type) { + case *parser.DataTypeInt: + pqlOp, err := sqlToPQLOp(op) + if err != nil { + return nil, err + } + return &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + lhs.columnName: &pql.Condition{ + Op: pqlOp, + Value: pqlValue, + }, + }, + }, nil + + case *parser.DataTypeID: + return &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + lhs.columnName: pqlValue, + }, + }, nil + + case *parser.DataTypeString: + return &pql.Call{ + Name: "Row", + Args: map[string]interface{}{ + lhs.columnName: pqlValue, + }, + }, nil + + default: + return nil, sql3.NewErrInternalf("unsupported type for binary expression: %v (%T)", typ, typ) + } + + case parser.BITAND, parser.BITOR, parser.BITNOT, parser.LSHIFT, parser.RSHIFT: + return nil, sql3.NewErrInternal("bitwise operators are not supported here") + + case parser.PLUS, parser.MINUS, parser.STAR, parser.SLASH, parser.REM: // + + return nil, sql3.NewErrInternal("aritmetic operators are not supported here") + + case parser.CONCAT: + return nil, sql3.NewErrInternal("concatenation operator is not supported here") + + case parser.IN, parser.NOTIN: + return nil, sql3.NewErrInternal("IN operator is not supported") + + case parser.BETWEEN, parser.NOTBETWEEN: + return nil, sql3.NewErrInternal("BETWEEN operator is not supported") + + default: + return nil, sql3.NewErrInternalf("unexpected binary expression operator: %s", expr.op) + } +} + +// sqlToPQLOp converts a parser operation token to PQL. +func sqlToPQLOp(op parser.Token) (pql.Token, error) { + switch op { + case parser.EQ: + return pql.EQ, nil + case parser.NE: + return pql.NEQ, nil + case parser.LT: + return pql.LT, nil + case parser.LE: + return pql.LTE, nil + case parser.GT: + return pql.GT, nil + case parser.GE: + return pql.GTE, nil + default: + return pql.ILLEGAL, sql3.NewErrInternalf("cannot convert SQL op %q to PQL", op) + } +} + +// planExprToValue converts a literal parser expression node to a value. +func planExprToValue(expr types.PlanExpression) (interface{}, error) { + switch expr := expr.(type) { + case *intLiteralPlanExpression: + return strconv.ParseInt(expr.value, 10, 64) + case *stringLiteralPlanExpression: + return expr.value, nil + default: + return nil, sql3.NewErrInternalf("cannot convert SQL expression %T to a literal value", expr) + } +} diff --git a/sql3/planner/expressiontypes.go b/sql3/planner/expressiontypes.go new file mode 100644 index 000000000..8e0e33bce --- /dev/null +++ b/sql3/planner/expressiontypes.go @@ -0,0 +1,711 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "strconv" + "strings" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +// takes a *pilosa.FieldInfo and returns a sql data type +func fieldSQLDataType(f *pilosa.FieldInfo) parser.ExprDataType { + // This is special handling for the primary key (_id) field. The normal + // handling below was not well suited to this field because there isn't a + // `pilosa.FieldTypeID` to compare against. One option would have been to + // add that to field.go, but it seemed risky to add a FieldType which + // FeatureBase is currently not using. In the future, this logic should use + // the next generation of FieldTypes in the dax package, which does include + // a FieldTypeID. Another thing to be updated is the "_id" value itself; in + // the dax package there is a constant called `PrimaryKeyFieldName` which + // would be used here instead. + if f.Name == "_id" { + switch f.Options.Type { + case "id": + return parser.NewDataTypeID() + case "string": + return parser.NewDataTypeString() + default: + return parser.NewDataTypeVoid() + } + } + + switch f.Options.Type { + case pilosa.FieldTypeInt: + return parser.NewDataTypeInt() + + case pilosa.FieldTypeMutex: + if f.Options.Keys { + return parser.NewDataTypeString() + } else { + return parser.NewDataTypeID() + } + + case pilosa.FieldTypeSet: + if f.Options.Keys { + return parser.NewDataTypeStringSet() + } else { + return parser.NewDataTypeIDSet() + } + + case pilosa.FieldTypeBool: + return parser.NewDataTypeBool() + + case pilosa.FieldTypeDecimal: + return parser.NewDataTypeDecimal(f.Options.Scale) + + case pilosa.FieldTypeTime, pilosa.FieldTypeTimestamp: + return parser.NewDataTypeTimestamp() + + default: + return parser.NewDataTypeVoid() + } +} + +// resolves type names to type representations +func dataTypeFromParserType(typ *parser.Type) (parser.ExprDataType, error) { + typeName := parser.IdentName(typ.Name) + switch strings.ToUpper(typeName) { + case parser.FieldTypeBool: + return parser.NewDataTypeBool(), nil + + case parser.FieldTypeDecimal: + scale, err := strconv.Atoi(typ.Scale.Value) + if err != nil { + return nil, err + } + return parser.NewDataTypeDecimal(int64(scale)), nil + + case parser.FieldTypeID: + return parser.NewDataTypeID(), nil + + case parser.FieldTypeIDSet: + return parser.NewDataTypeIDSet(), nil + + case parser.FieldTypeInt: + return parser.NewDataTypeInt(), nil + + case parser.FieldTypeString: + return parser.NewDataTypeString(), nil + + case parser.FieldTypeStringSet: + return parser.NewDataTypeStringSet(), nil + + case parser.FieldTypeTimestamp: + return parser.NewDataTypeTimestamp(), nil + + default: + return nil, sql3.NewErrUnknownType(typ.Name.NamePos.Line, typ.Name.NamePos.Column, typeName) + } +} + +// returns true if type is compatible with logical operators (AND, OR) +func typeIsCompatibleWithLogicalOperator(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt, *parser.DataTypeBool: + return true + default: + return false + } +} + +// returns true if type is compatible with equality operators (=, <>) +func typeIsCompatibleWithEqualityOperator(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt, + *parser.DataTypeDecimal, *parser.DataTypeBool, + *parser.DataTypeString, *parser.DataTypeTimestamp, + *parser.DataTypeIDSet, *parser.DataTypeStringSet: + return true + default: + return false + } +} + +// returns true if type is compatible with comparison operators (<, <=, >, >=) +func typeIsCompatibleWithComparisonOperator(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt, *parser.DataTypeDecimal, *parser.DataTypeTimestamp: + return true + default: + return false + } +} + +// returns true if type is compatible with arithmetic operators (+, -, *, /, %) +func typeIsCompatibleWithArithmeticOperator(testType parser.ExprDataType, op parser.Token) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt: + return true + case *parser.DataTypeDecimal: + return op != parser.REM + default: + return false + } +} + +// returns true if type is compatible with concatenation operator (||) +func typeIsCompatibleWithConcatOperator(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeString: + return true + default: + return false + } +} + +// returns true if type is compatible using a range comparison. rhs must be a range type and the lhs must be +// compatible with the range subscript type +func typesAreRangeComparable(testTypeL parser.ExprDataType, testTypeR parser.ExprDataType) (bool, error) { + switch rhsType := testTypeR.(type) { + case *parser.DataTypeRange: + switch rhsType.SubscriptType.(type) { + case *parser.DataTypeTimestamp: + switch lhsType := testTypeL.(type) { + case *parser.DataTypeTimestamp: + return true, nil + + default: + return false, sql3.NewErrInternalf("unhandled rhs type '%T' for lhs type '%T'", rhsType, lhsType) + + } + case *parser.DataTypeInt, *parser.DataTypeID: + switch lhsType := testTypeL.(type) { + case *parser.DataTypeID: + return true, nil + case *parser.DataTypeInt: + return true, nil + + default: + return false, sql3.NewErrInternalf("unhandled rhs type '%T' for lhs type '%T'", rhsType, lhsType) + + } + } + default: + return false, sql3.NewErrInternalf("type '%T' is not a range type", rhsType) + } + return false, nil +} + +// returns true if type is compatible with the (NOT) LIKE operator +func typeIsCompatibleWithLikeOperator(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeString: + return true + default: + return false + } +} + +// returns true if type is compatible with bitwise operators (&, |, <<, >>) +func typeIsCompatibleWithBitwiseOperator(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt: + return true + default: + return false + } +} + +// returns true if type is assignemt compatible. Assignment compatibility can be either because the +// comparison types are the same or because the source type can be coerced into the the target type +func typesAreAssignmentCompatible(targetType parser.ExprDataType, sourceType parser.ExprDataType) bool { + //ok to assign null to something + _, ok := sourceType.(*parser.DataTypeVoid) + if ok { + return true + } + + switch lhs := targetType.(type) { + + case *parser.DataTypeInt: + switch sourceType.(type) { + case *parser.DataTypeInt: + return true + default: + return false + } + + case *parser.DataTypeBool: + switch sourceType.(type) { + case *parser.DataTypeBool: + return true + default: + return false + } + + case *parser.DataTypeID: + switch sourceType.(type) { + case *parser.DataTypeInt: + return true + case *parser.DataTypeID: + return true + default: + return false + } + case *parser.DataTypeStringSet: + switch sourceType.(type) { + case *parser.DataTypeStringSet: + return true + default: + return false + } + case *parser.DataTypeIDSet: + switch sourceType.(type) { + case *parser.DataTypeIDSet: + return true + default: + return false + } + case *parser.DataTypeDecimal: + switch rhs := sourceType.(type) { + case *parser.DataTypeDecimal: + //if lhs scale is >= rhs scale, we're good + return lhs.Scale >= rhs.Scale + case *parser.DataTypeInt: + return true + default: + return false + } + + case *parser.DataTypeTimestamp: + switch sourceType.(type) { + case *parser.DataTypeTimestamp: + return true + case *parser.DataTypeString: + //could be a string parseable as a date + return true + default: + return false + } + + case *parser.DataTypeString: + switch sourceType.(type) { + case *parser.DataTypeString: + return true + default: + return false + } + + default: + return false + } +} + +// returns true if the type can be used as a range subscript +func typeCanBeUsedInRange(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt, *parser.DataTypeTimestamp: + return true + default: + return false + } +} + +// returns true if the types can be considered the same when used as a range bounds +func typesOfRangeBoundsAreTheSame(testTypeL parser.ExprDataType, testTypeR parser.ExprDataType) bool { + switch testTypeL.(type) { + case *parser.DataTypeInt: + switch testTypeR.(type) { + case *parser.DataTypeInt: + return true + + case *parser.DataTypeID: + return true + + default: + return false + } + + case *parser.DataTypeID: + switch testTypeR.(type) { + case *parser.DataTypeID: + return true + + case *parser.DataTypeInt: + return true + + default: + return false + } + + case *parser.DataTypeTimestamp: + switch testTypeR.(type) { + case *parser.DataTypeTimestamp: + return true + + default: + return false + } + + default: + return false + } +} + +// returns true if the type is a range type +func typeIsRange(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeRange: + return true + default: + return false + } +} + +// returns true if the type is a set type +func typeIsSet(testType parser.ExprDataType) (bool, parser.ExprDataType) { + switch testType.(type) { + case *parser.DataTypeIDSet: + return true, parser.NewDataTypeID() + case *parser.DataTypeStringSet: + return true, parser.NewDataTypeString() + default: + return false, nil + } +} + +// returns true if the type is bool +func typeIsBool(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeBool: + return true + default: + return false + } +} + +// returns true if the type is an integer or can be treated as one +func typeIsInteger(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeID, *parser.DataTypeInt: + return true + default: + return false + } +} + +// returns true if the type is string +func typeIsString(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeString: + return true + default: + return false + } +} + +// returns true if the type is timestamp +func typeIsTimestamp(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeTimestamp: + return true + default: + return false + } +} + +// returns true if the type is a float +func typeIsFloat(testType parser.ExprDataType) bool { + switch testType.(type) { + case *parser.DataTypeDecimal: + return true + default: + return false + } +} + +// returns true if the types can be compared +func typesAreComparable(testTypeL parser.ExprDataType, testTypeR parser.ExprDataType) bool { + switch testTypeL.(type) { + case *parser.DataTypeInt: + switch testTypeR.(type) { + case *parser.DataTypeInt: + return true + case *parser.DataTypeID: + return true + case *parser.DataTypeDecimal: + return true + + } + + case *parser.DataTypeID: + switch testTypeR.(type) { + case *parser.DataTypeID: + return true + case *parser.DataTypeInt: + return true + case *parser.DataTypeDecimal: + return true + + } + + case *parser.DataTypeDecimal: + switch testTypeR.(type) { + case *parser.DataTypeID: + return true + case *parser.DataTypeInt: + return true + case *parser.DataTypeDecimal: + return true + } + + case *parser.DataTypeBool: + switch testTypeR.(type) { + case *parser.DataTypeBool: + return true + } + + case *parser.DataTypeTimestamp: + switch testTypeR.(type) { + case *parser.DataTypeTimestamp: + return true + } + + case *parser.DataTypeIDSet: + switch testTypeR.(type) { + case *parser.DataTypeIDSet: + return true + + } + + case *parser.DataTypeString: + switch testTypeR.(type) { + case *parser.DataTypeString: + return true + + } + + case *parser.DataTypeStringSet: + switch testTypeR.(type) { + case *parser.DataTypeStringSet: + return true + + } + + } + return false +} + +// returns the target type given two operand types for an artitmentic operation (or an error) +func typesCoercedForArithmeticOperator(testTypeL parser.ExprDataType, testTypeR parser.ExprDataType, atPos parser.Pos) (parser.ExprDataType, error) { + switch lhsType := testTypeL.(type) { + case *parser.DataTypeInt: + switch rhsType := testTypeR.(type) { + case *parser.DataTypeInt: + return rhsType, nil + + case *parser.DataTypeID: + return lhsType, nil + + case *parser.DataTypeDecimal: + return rhsType, nil + } + + case *parser.DataTypeID: + switch testTypeR.(type) { + case *parser.DataTypeID: + return testTypeR, nil + + case *parser.DataTypeInt: + return testTypeR, nil + } + + case *parser.DataTypeDecimal: + switch rhsType := testTypeR.(type) { + case *parser.DataTypeInt: + return testTypeL, nil + + case *parser.DataTypeID: + return testTypeL, nil + + case *parser.DataTypeDecimal: + if lhsType.Scale > rhsType.Scale { + return lhsType, nil + } + return rhsType, nil + } + + } + return nil, sql3.NewErrTypeMismatch(atPos.Line, atPos.Column, testTypeL.TypeName(), testTypeR.TypeName()) +} + +// returns the target type given two operand types for a bitwise operation (or an error) +func typesCoercedForBitwiseOperator(testTypeL parser.ExprDataType, testTypeR parser.ExprDataType, atPos parser.Pos) (parser.ExprDataType, error) { + switch testTypeL.(type) { + + case *parser.DataTypeInt: + switch rhsType := testTypeR.(type) { + case *parser.DataTypeInt: + return testTypeL, nil + + case *parser.DataTypeID: + return testTypeL, nil + + case *parser.DataTypeDecimal: + return parser.NewDataTypeDecimal(rhsType.Scale), nil + + } + + case *parser.DataTypeID: + switch rhsType := testTypeR.(type) { + case *parser.DataTypeID: + return testTypeR, nil + + case *parser.DataTypeInt: + return testTypeR, nil + + case *parser.DataTypeDecimal: + return parser.NewDataTypeDecimal(rhsType.Scale), nil + } + } + return nil, sql3.NewErrTypeMismatch(atPos.Line, atPos.Column, testTypeL.TypeName(), testTypeR.TypeName()) +} + +// returns the target type given two operand types type coercion operation (or an error) +func typeCoerceType(testTypeL parser.ExprDataType, testTypeR parser.ExprDataType, atPos parser.Pos) (parser.ExprDataType, error) { + switch testTypeL.(type) { + case *parser.DataTypeBool: + switch testTypeR.(type) { + case *parser.DataTypeBool: + return testTypeL, nil + + } + + case *parser.DataTypeInt: + switch testTypeR.(type) { + case *parser.DataTypeInt: + return testTypeL, nil + case *parser.DataTypeID: + return testTypeL, nil + case *parser.DataTypeDecimal: + return testTypeR, nil + } + + case *parser.DataTypeDecimal: + switch testTypeR.(type) { + case *parser.DataTypeDecimal: + return testTypeL, nil + case *parser.DataTypeInt: + return testTypeL, nil + case *parser.DataTypeID: + return testTypeL, nil + + } + + case *parser.DataTypeID: + switch testTypeR.(type) { + case *parser.DataTypeID: + return testTypeL, nil + case *parser.DataTypeInt: + return testTypeR, nil + + } + + case *parser.DataTypeIDSet: + switch testTypeR.(type) { + case *parser.DataTypeIDSet: + return testTypeL, nil + + } + + case *parser.DataTypeString: + switch testTypeR.(type) { + case *parser.DataTypeString: + return testTypeL, nil + + } + + case *parser.DataTypeStringSet: + switch testTypeR.(type) { + case *parser.DataTypeStringSet: + return testTypeL, nil + + } + + case *parser.DataTypeTimestamp: + switch testTypeR.(type) { + case *parser.DataTypeTimestamp: + return testTypeL, nil + + } + + default: + return nil, sql3.NewErrInternalf("unhandled lhs type '%t'", testTypeL) + } + return nil, sql3.NewErrTypeMismatch(atPos.Line, atPos.Line, testTypeL.TypeName(), testTypeR.TypeName()) +} + +// returns true if source type can be cast to target type +func typesCanBeCast(sourceType parser.ExprDataType, targetType parser.ExprDataType) bool { + switch st := sourceType.(type) { + case *parser.DataTypeInt: + switch targetType.(type) { + case *parser.DataTypeInt, + *parser.DataTypeBool, + *parser.DataTypeDecimal, + *parser.DataTypeID, + *parser.DataTypeString, + *parser.DataTypeTimestamp: + return true + } + + case *parser.DataTypeBool: + switch targetType.(type) { + case *parser.DataTypeBool, + *parser.DataTypeInt, + *parser.DataTypeString: + return true + } + + case *parser.DataTypeDecimal: + switch tt := targetType.(type) { + case *parser.DataTypeDecimal: + return tt.Scale >= st.Scale + case *parser.DataTypeString: + return true + } + + case *parser.DataTypeID: + switch targetType.(type) { + case *parser.DataTypeInt, + *parser.DataTypeBool, + *parser.DataTypeDecimal, + *parser.DataTypeID: + return true + } + + case *parser.DataTypeIDSet: + switch targetType.(type) { + case *parser.DataTypeIDSet, *parser.DataTypeString: + return true + } + + case *parser.DataTypeString: + switch targetType.(type) { + case *parser.DataTypeInt, + *parser.DataTypeBool, + *parser.DataTypeDecimal, + *parser.DataTypeID, + *parser.DataTypeString, + *parser.DataTypeTimestamp: + return true + } + case *parser.DataTypeStringSet: + switch targetType.(type) { + case *parser.DataTypeStringSet, *parser.DataTypeString: + return true + } + + case *parser.DataTypeTimestamp: + switch targetType.(type) { + case *parser.DataTypeInt, *parser.DataTypeTimestamp, *parser.DataTypeString: + return true + } + + } + return false +} diff --git a/sql3/planner/inbuiltfunctionsdate.go b/sql3/planner/inbuiltfunctionsdate.go new file mode 100644 index 000000000..ef9eb41e5 --- /dev/null +++ b/sql3/planner/inbuiltfunctionsdate.go @@ -0,0 +1,123 @@ +package planner + +import ( + "strings" + "time" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +const intervalYear = "YY" +const intervalYearDay = "YD" +const intervalMonth = "M" +const intervalDay = "D" +const intervalWeeKDay = "W" +const intervalWeek = "WK" +const intervalHour = "HH" +const intervalMinute = "MI" +const intervalSecond = "S" +const intervalMillisecond = "MS" +const intervalNanosecond = "NS" + +func (p *ExecutionPlanner) analyzeFunctionDatePart(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + + if len(call.Args) != 2 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, len(call.Args)) + } + // interval + intervalType := parser.NewDataTypeString() + if !typesAreAssignmentCompatible(intervalType, call.Args[0].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Args[0].DataType().TypeName(), intervalType.TypeName()) + } + + // date + dateType := parser.NewDataTypeTimestamp() + if !typesAreAssignmentCompatible(dateType, call.Args[1].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Args[1].DataType().TypeName(), dateType.TypeName()) + } + + //return int + call.ResultDataType = parser.NewDataTypeInt() + + return call, nil +} + +func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interface{}, error) { + intervalEval, err := n.args[0].Evaluate(currentRow) + if err != nil { + return nil, err + } + + dateEval, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + + // nil if anything is nil + if intervalEval == nil || dateEval == nil { + return nil, nil + } + + //get the date value + coercedDate, err := coerceValue(n.args[1].Type(), parser.NewDataTypeTimestamp(), dateEval, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + date, dateOk := coercedDate.(time.Time) + if !dateOk { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + //get the interval value + coercedInterval, err := coerceValue(n.args[0].Type(), parser.NewDataTypeString(), intervalEval, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + interval, intervalOk := coercedInterval.(string) + if !intervalOk { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + switch strings.ToUpper(interval) { + case intervalYear: + return int64(date.Year()), nil + + case intervalYearDay: + return int64(date.YearDay()), nil + + case intervalMonth: + return int64(date.Month()), nil + + case intervalDay: + return int64(date.Day()), nil + + case intervalWeeKDay: + return int64(date.Weekday()), nil + + case intervalWeek: + _, isoWeek := date.ISOWeek() + return int64(isoWeek), nil + + case intervalHour: + return int64(date.Hour()), nil + + case intervalMinute: + return int64(date.Minute()), nil + + case intervalSecond: + return int64(date.Second()), nil + + case intervalMillisecond: + return int64(date.Nanosecond() * 1000 * 1000), nil + + case intervalNanosecond: + return int64(date.Nanosecond()), nil + + default: + return nil, sql3.NewErrCallParameterValueInvalid(0, 0, interval, "interval") + } + +} diff --git a/sql3/planner/inbuiltfunctionsset.go b/sql3/planner/inbuiltfunctionsset.go new file mode 100644 index 000000000..6d41dc146 --- /dev/null +++ b/sql3/planner/inbuiltfunctionsset.go @@ -0,0 +1,208 @@ +package planner + +import ( + "strings" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +func (n *callPlanExpression) EvaluateSetContains(currentRow []interface{}) (interface{}, error) { + targetSetEval, err := n.args[0].Evaluate(currentRow) + if err != nil { + return nil, err + } + + testValueEval, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + + //if either term is null, then null + if testValueEval == nil || targetSetEval == nil { + return nil, nil + } + + if targetSetEval != nil { + switch typ := n.args[0].Type().(type) { + case *parser.DataTypeStringSet: + targetSet, ok := targetSetEval.([]string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + testValue, ok := testValueEval.(string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + return stringSetContains(targetSet, testValue), nil + + case *parser.DataTypeIDSet: + targetSet, ok := targetSetEval.([]int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + testValue, ok := testValueEval.(int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + return intSetContains(targetSet, int64(testValue)), nil + + default: + return nil, sql3.NewErrInternalf("unexpected data type '%T'", typ) + } + } + return nil, sql3.NewErrInternalf("unable to to find column '%s' in currentColumns", n.name) +} + +func (n *callPlanExpression) EvaluateSetContainsAny(currentRow []interface{}) (interface{}, error) { + targetSetEval, err := n.args[0].Evaluate(currentRow) + if err != nil { + return nil, err + } + + testSetEval, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + + if targetSetEval != nil { + switch typ := n.args[0].Type().(type) { + case *parser.DataTypeStringSet: + targetSet, ok := targetSetEval.([]string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + testSet, ok := testSetEval.([]string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + return stringSetContainsAny(targetSet, testSet), nil + + case *parser.DataTypeIDSet: + targetSet, ok := targetSetEval.([]int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + testSet, ok := testSetEval.([]int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + return intSetContainsAny(targetSet, testSet), nil + + default: + return nil, sql3.NewErrInternalf("unexpected data type '%T'", typ) + } + + } + return nil, sql3.NewErrInternalf("unable to to find column '%s' in currentColumns", n.name) +} + +func (n *callPlanExpression) EvaluateSetContainsAll(currentRow []interface{}) (interface{}, error) { + targetSetEval, err := n.args[0].Evaluate(currentRow) + if err != nil { + return nil, err + } + + testSetEval, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + + if targetSetEval != nil { + switch typ := n.args[0].Type().(type) { + case *parser.DataTypeStringSet: + targetSet, ok := targetSetEval.([]string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + testSet, ok := testSetEval.([]string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + return stringSetContainsAll(targetSet, testSet), nil + + case *parser.DataTypeIDSet: + targetSet, ok := targetSetEval.([]int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + testSet, ok := testSetEval.([]int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + return intSetContainsAll(targetSet, testSet), nil + + default: + return nil, sql3.NewErrInternalf("unexpected data type '%T'", typ) + } + + } + + return nil, sql3.NewErrInternalf("unable to to find column '%s' in currentColumns", n.name) +} + +func stringSetContains(set []string, val string) bool { + for _, v := range set { + if strings.EqualFold(v, val) { + return true + } + } + return false +} + +func stringSetContainsAny(targetSet []string, testSet []string) bool { + for _, test := range testSet { + if stringSetContains(targetSet, test) { + return true + } + } + return false +} + +func stringSetContainsAll(targetSet []string, testSet []string) bool { + for _, test := range testSet { + if !stringSetContains(targetSet, test) { + return false + } + } + return true +} + +func intSetContains(set []int64, val int64) bool { + for _, v := range set { + if v == val { + return true + } + } + return false +} + +func intSetContainsAny(targetSet []int64, testSet []int64) bool { + for _, test := range testSet { + if intSetContains(targetSet, test) { + return true + } + } + return false +} + +func intSetContainsAll(targetSet []int64, testSet []int64) bool { + for _, test := range testSet { + if !intSetContains(targetSet, test) { + return false + } + } + return true +} diff --git a/sql3/planner/memoryobj.go b/sql3/planner/memoryobj.go new file mode 100644 index 000000000..770c7f916 --- /dev/null +++ b/sql3/planner/memoryobj.go @@ -0,0 +1,72 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import "github.com/molecula/featurebase/v3/sql3/planner/types" + +// RowCache is a cache of rows used during row iteration +type RowCache interface { + Add(row types.Row) error + + // AllRows returns all rows. + AllRows() []types.Row +} + +// KeyedRowCache is a cache of keyed rows used during row iteration +type KeyedRowCache interface { + // Put adds row to the cache at the given key. + Put(key string, row types.Row) error + + // Get returns the rows specified by key. + Get(key string) (types.Row, error) + + // Size returns the number of rows in the cache. + Size() int +} + +// Ensure type implements interface. +var _ KeyedRowCache = (*inMemoryKeyedRowCache)(nil) + +// default implementation of KeyedRowCache (in memory) +type inMemoryKeyedRowCache struct { + store map[string][]interface{} +} + +func newinMemoryKeyedRowCache() *inMemoryKeyedRowCache { + return &inMemoryKeyedRowCache{ + store: make(map[string][]interface{}), + } +} + +func (m inMemoryKeyedRowCache) Put(u string, i types.Row) error { + m.store[u] = i + return nil +} + +func (m inMemoryKeyedRowCache) Get(u string) (types.Row, error) { + return m.store[u], nil +} + +func (m inMemoryKeyedRowCache) Size() int { + return len(m.store) +} + +// Ensure type implements interface. +var _ RowCache = (*inMemoryRowCache)(nil) + +type inMemoryRowCache struct { + rows []types.Row +} + +func newInMemoryRowCache() *inMemoryRowCache { + return &inMemoryRowCache{} +} + +func (c *inMemoryRowCache) Add(row types.Row) error { + c.rows = append(c.rows, row) + return nil +} + +func (c *inMemoryRowCache) AllRows() []types.Row { + return c.rows +} diff --git a/sql3/planner/opaltertable.go b/sql3/planner/opaltertable.go new file mode 100644 index 000000000..5ec3585b6 --- /dev/null +++ b/sql3/planner/opaltertable.go @@ -0,0 +1,105 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpAlterTable plan operator to alter a table. +type PlanOpAlterTable struct { + planner *ExecutionPlanner + tableName string + operation alterOperation + oldColumnName string + newColumnName string + columnDef *createTableField + warnings []string +} + +func NewPlanOpAlterTable(p *ExecutionPlanner, tableName string, operation alterOperation, oldColumnName string, newColumnName string, columnDef *createTableField) *PlanOpAlterTable { + return &PlanOpAlterTable{ + planner: p, + tableName: tableName, + operation: operation, + oldColumnName: oldColumnName, + newColumnName: newColumnName, + columnDef: columnDef, + } +} + +func (p *PlanOpAlterTable) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + result["tableName"] = p.tableName + return result +} + +func (p *PlanOpAlterTable) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpAlterTable) Warnings() []string { + return p.warnings +} + +func (p *PlanOpAlterTable) String() string { + return "" +} + +func (p *PlanOpAlterTable) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpAlterTable) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpAlterTable) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &alterTableRowIter{ + planner: p.planner, + operation: p.operation, + tableName: p.tableName, + columnDef: p.columnDef, + oldColumnName: p.oldColumnName, + }, nil +} + +func (p *PlanOpAlterTable) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +type alterTableRowIter struct { + planner *ExecutionPlanner + operation alterOperation + tableName string + columnDef *createTableField + oldColumnName string +} + +var _ types.RowIterator = (*alterTableRowIter)(nil) + +func (i *alterTableRowIter) Next(ctx context.Context) (types.Row, error) { + switch i.operation { + case alterOpAdd: + _, err := i.planner.schemaAPI.CreateField(ctx, i.tableName, i.columnDef.name, i.columnDef.fos...) + if err != nil { + return nil, err + } + + case alterOpDrop: + err := i.planner.schemaAPI.DeleteField(ctx, i.tableName, i.oldColumnName) + if err != nil { + return nil, err + } + + case alterOpRename: + return nil, sql3.NewErrInternal("column rename is unimplemented") + + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opbulkinsert.go b/sql3/planner/opbulkinsert.go new file mode 100644 index 000000000..64b4f9fc4 --- /dev/null +++ b/sql3/planner/opbulkinsert.go @@ -0,0 +1,79 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpBulkInsert plan operator to handle INSERT. +type PlanOpBulkInsert struct { + planner *ExecutionPlanner + tableName string + warnings []string +} + +func NewPlanOpBulkInsert(p *ExecutionPlanner, tableName string) *PlanOpBulkInsert { + return &PlanOpBulkInsert{ + planner: p, + tableName: tableName, + } +} + +func (p *PlanOpBulkInsert) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + result["tableName"] = p.tableName + return result +} + +func (p *PlanOpBulkInsert) String() string { + return "" +} + +func (p *PlanOpBulkInsert) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpBulkInsert) Warnings() []string { + return p.warnings +} + +func (p *PlanOpBulkInsert) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpBulkInsert) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpBulkInsert) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &bulkInsertRowIter{ + planner: p.planner, + tableName: p.tableName, + }, nil +} + +func (p *PlanOpBulkInsert) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return NewPlanOpBulkInsert(p.planner, p.tableName), nil +} + +type bulkInsertRowIter struct { + planner *ExecutionPlanner + tableName string +} + +var _ types.RowIterator = (*bulkInsertRowIter)(nil) + +func (i *bulkInsertRowIter) Next(ctx context.Context) (types.Row, error) { + + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opcreatetable.go b/sql3/planner/opcreatetable.go new file mode 100644 index 000000000..129291951 --- /dev/null +++ b/sql3/planner/opcreatetable.go @@ -0,0 +1,121 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +// PlanOpCreateTable plan operator that creates a table. +type PlanOpCreateTable struct { + planner *ExecutionPlanner + tableName string + failIfExists bool + isKeyed bool + keyPartitions int + columns []*createTableField + warnings []string +} + +func NewPlanOpCreateTable(p *ExecutionPlanner, tableName string, failIfExists bool, isKeyed bool, keyPartitions int, columns []*createTableField) *PlanOpCreateTable { + return &PlanOpCreateTable{ + planner: p, + tableName: tableName, + failIfExists: failIfExists, + isKeyed: isKeyed, + keyPartitions: keyPartitions, + columns: columns, + } +} + +func (p *PlanOpCreateTable) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + result["name"] = p.tableName + result["failIfExists"] = p.failIfExists + return result +} + +func (p *PlanOpCreateTable) String() string { + return "" +} + +func (p *PlanOpCreateTable) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpCreateTable) Warnings() []string { + return p.warnings +} + +func (p *PlanOpCreateTable) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpCreateTable) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpCreateTable) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &createTableRowIter{ + planner: p.planner, + tableName: p.tableName, + failIfExists: p.failIfExists, + isKeyed: p.isKeyed, + keyPartitions: p.keyPartitions, + columns: p.columns, + }, nil +} + +func (p *PlanOpCreateTable) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +type createTableRowIter struct { + planner *ExecutionPlanner + tableName string + failIfExists bool + isKeyed bool + keyPartitions int + columns []*createTableField +} + +var _ types.RowIterator = (*createTableRowIter)(nil) + +func (i *createTableRowIter) Next(ctx context.Context) (types.Row, error) { + //create the table + options := pilosa.IndexOptions{ + Keys: i.isKeyed, + TrackExistence: true, + PartitionN: i.keyPartitions, + } + + fields := make([]pilosa.CreateFieldObj, len(i.columns)) + for i, f := range i.columns { + fields[i] = pilosa.CreateFieldObj{ + Name: f.name, + Options: f.fos, + } + } + + if err := i.planner.schemaAPI.CreateIndexAndFields(ctx, i.tableName, options, fields); err != nil { + if _, ok := errors.Cause(err).(pilosa.ConflictError); ok { + if i.failIfExists { + return nil, err + } + } else { + return nil, err + } + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opdistinct.go b/sql3/planner/opdistinct.go new file mode 100644 index 000000000..2284856eb --- /dev/null +++ b/sql3/planner/opdistinct.go @@ -0,0 +1,40 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpDistinct plan operator handles DISTINCT +type PlanOpDistinct struct { + planner *ExecutionPlanner + source types.PlanOperator + warnings []string +} + +func NewPlanOpDistinct(p *ExecutionPlanner, source types.PlanOperator) *PlanOpDistinct { + return &PlanOpDistinct{ + planner: p, + source: source, + } +} + +func (n *PlanOpDistinct) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", n) + return result +} + +func (n *PlanOpDistinct) AddWarning(warning string) { + n.warnings = append(n.warnings, warning) +} + +func (n *PlanOpDistinct) Warnings() []string { + var w []string + w = append(w, n.warnings...) + w = append(w, n.source.Warnings()...) + return w +} diff --git a/sql3/planner/opdroptable.go b/sql3/planner/opdroptable.go new file mode 100644 index 000000000..f32844854 --- /dev/null +++ b/sql3/planner/opdroptable.go @@ -0,0 +1,88 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpDropTable plan operator to drop a table. +type PlanOpDropTable struct { + planner *ExecutionPlanner + index *pilosa.IndexInfo + warnings []string +} + +func NewPlanOpDropTable(p *ExecutionPlanner, index *pilosa.IndexInfo) *PlanOpDropTable { + return &PlanOpDropTable{ + planner: p, + index: index, + } +} + +func (p *PlanOpDropTable) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + result["tableName"] = p.index.Name + return result +} + +func (p *PlanOpDropTable) String() string { + return "" +} + +func (p *PlanOpDropTable) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpDropTable) Warnings() []string { + return p.warnings +} + +func (p *PlanOpDropTable) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpDropTable) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpDropTable) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &dropTableRowIter{ + planner: p.planner, + index: p.index, + }, nil +} + +func (p *PlanOpDropTable) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +type dropTableRowIter struct { + planner *ExecutionPlanner + index *pilosa.IndexInfo +} + +var _ types.RowIterator = (*dropTableRowIter)(nil) + +func (i *dropTableRowIter) Next(ctx context.Context) (types.Row, error) { + err := i.planner.checkAccess(ctx, i.index.Name, accessTypeDropObject) + if err != nil { + return nil, err + } + + err = i.planner.schemaAPI.DeleteIndex(ctx, i.index.Name) + if err != nil { + return nil, err + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opfeaturebasecolumns.go b/sql3/planner/opfeaturebasecolumns.go new file mode 100644 index 000000000..9d2690dbc --- /dev/null +++ b/sql3/planner/opfeaturebasecolumns.go @@ -0,0 +1,174 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "time" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpFeatureBaseColumns wraps an Index that is returned from schemaAPI.Schema(). +type PlanOpFeatureBaseColumns struct { + index *pilosa.IndexInfo + warnings []string +} + +func NewPlanOpFeatureBaseColumns(index *pilosa.IndexInfo) *PlanOpFeatureBaseColumns { + node := &PlanOpFeatureBaseColumns{ + index: index, + } + return node +} + +func (p *PlanOpFeatureBaseColumns) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + return result +} + +func (p *PlanOpFeatureBaseColumns) String() string { + return "" +} + +func (p *PlanOpFeatureBaseColumns) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpFeatureBaseColumns) Warnings() []string { + return p.warnings +} + +func (p *PlanOpFeatureBaseColumns) Schema() types.Schema { + return types.Schema{ + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "name", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "type", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "internal_type", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "created_at", + Type: parser.NewDataTypeTimestamp(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "keys", + Type: parser.NewDataTypeBool(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "cache_type", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "cache_size", + Type: parser.NewDataTypeInt(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "scale", + Type: parser.NewDataTypeInt(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "min", + Type: parser.NewDataTypeInt(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "max", + Type: parser.NewDataTypeInt(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "timeunit", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "epoch", + Type: parser.NewDataTypeInt(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "timequantum", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$table_columns", + Name: "ttl", + Type: parser.NewDataTypeInt(), + }, + } +} + +func (p *PlanOpFeatureBaseColumns) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpFeatureBaseColumns) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &showColumnsRowIter{ + index: p.index, + }, nil +} + +func (p *PlanOpFeatureBaseColumns) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return NewPlanOpFeatureBaseColumns(p.index), nil +} + +type showColumnsRowIter struct { + index *pilosa.IndexInfo + rowIndex int +} + +var _ types.RowIterator = (*showColumnsRowIter)(nil) + +func (i *showColumnsRowIter) Next(ctx context.Context) (types.Row, error) { + if i.rowIndex < len(i.index.Fields) { + fields := i.index.Fields + + tm := time.Unix(0, fields[i.rowIndex].CreatedAt) + + row := []interface{}{ + fields[i.rowIndex].Name, + fieldSQLDataType(fields[i.rowIndex]).TypeName(), + fields[i.rowIndex].Options.Type, + tm.Format(time.RFC3339), + fields[i.rowIndex].Options.Keys, + fields[i.rowIndex].Options.CacheType, + fields[i.rowIndex].Options.CacheSize, + fields[i.rowIndex].Options.Scale, + fields[i.rowIndex].Options.Min.ToInt64(0), + fields[i.rowIndex].Options.Max.ToInt64(0), + fields[i.rowIndex].Options.TimeUnit, + 0, //TODO(pok) get Epoch from somewhere? + fields[i.rowIndex].Options.TimeQuantum.String(), + fields[i.rowIndex].Options.TTL.String(), + } + + i.rowIndex += 1 + return row, nil + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opfeaturebasetables.go b/sql3/planner/opfeaturebasetables.go new file mode 100644 index 000000000..8bd1d67c6 --- /dev/null +++ b/sql3/planner/opfeaturebasetables.go @@ -0,0 +1,116 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "time" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpFeatureBaseTables wraps a []*IndexInfo that is returned from +// schemaAPI.Schema(). +type PlanOpFeatureBaseTables struct { + indexInfo []*pilosa.IndexInfo + warnings []string +} + +func NewPlanOpFeatureBaseTables(indexInfo []*pilosa.IndexInfo) *PlanOpFeatureBaseTables { + return &PlanOpFeatureBaseTables{ + indexInfo: indexInfo, + } +} + +func (p *PlanOpFeatureBaseTables) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + return result +} + +func (p *PlanOpFeatureBaseTables) String() string { + return "" +} + +func (p *PlanOpFeatureBaseTables) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpFeatureBaseTables) Warnings() []string { + return p.warnings +} + +func (p *PlanOpFeatureBaseTables) Schema() types.Schema { + return types.Schema{ + &types.PlannerColumn{ + Table: "fb$tables", + Name: "name", + Type: parser.NewDataTypeString(), + }, + &types.PlannerColumn{ + Table: "fb$tables", + Name: "created_at", + Type: parser.NewDataTypeTimestamp(), + }, + &types.PlannerColumn{ + Table: "fb$tables", + Name: "track_existence", + Type: parser.NewDataTypeBool(), + }, + &types.PlannerColumn{ + Table: "fb$tables", + Name: "keys", + Type: parser.NewDataTypeBool(), + }, + &types.PlannerColumn{ + Table: "fb$tables", + Name: "shard_width", + Type: parser.NewDataTypeInt(), + }, + } +} + +func (p *PlanOpFeatureBaseTables) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpFeatureBaseTables) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &showTablesRowIter{ + indexInfo: p.indexInfo, + }, nil +} + +func (p *PlanOpFeatureBaseTables) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return NewPlanOpFeatureBaseTables(p.indexInfo), nil +} + +type showTablesRowIter struct { + indexInfo []*pilosa.IndexInfo + rowIndex int +} + +var _ types.RowIterator = (*showTablesRowIter)(nil) + +func (i *showTablesRowIter) Next(ctx context.Context) (types.Row, error) { + if i.rowIndex < len(i.indexInfo) { + tm := time.Unix(0, i.indexInfo[i.rowIndex].CreatedAt) + row := []interface{}{ + i.indexInfo[i.rowIndex].Name, + tm.Format(time.RFC3339), + i.indexInfo[i.rowIndex].Options.TrackExistence, + i.indexInfo[i.rowIndex].Options.Keys, + i.indexInfo[i.rowIndex].ShardWidth, + } + i.rowIndex += 1 + return row, nil + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opgroupby.go b/sql3/planner/opgroupby.go new file mode 100644 index 000000000..843845892 --- /dev/null +++ b/sql3/planner/opgroupby.go @@ -0,0 +1,198 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpGroupBy handles the GROUP BY clause +// this is the default GROUP BY operator and may be replaced by the optimizer +// with one or more of the PQL related group by or aggregate operators +type PlanOpGroupBy struct { + ChildOp types.PlanOperator + Aggregates []types.PlanExpression + GroupByExprs []types.PlanExpression + warnings []string +} + +func NewPlanOpGroupBy(aggregates []types.PlanExpression, groupByExprs []types.PlanExpression, child types.PlanOperator) *PlanOpGroupBy { + return &PlanOpGroupBy{ + ChildOp: child, + Aggregates: aggregates, + GroupByExprs: groupByExprs, + } +} + +// Schema for GroupBy is the group by expressions followed by the aggregate expressions +func (p *PlanOpGroupBy) Schema() types.Schema { + result := make(types.Schema, len(p.GroupByExprs)+len(p.Aggregates)) + for idx, expr := range p.GroupByExprs { + ref, ok := expr.(*qualifiedRefPlanExpression) + if !ok { + continue + } + s := &types.PlannerColumn{ + Name: ref.columnName, + Table: ref.tableName, + Type: expr.Type(), + } + result[idx] = s + } + offset := len(p.GroupByExprs) + for idx, agg := range p.Aggregates { + s := &types.PlannerColumn{ + Name: "", + Table: "", + Type: agg.Type(), + } + result[idx+offset] = s + } + + return result +} + +func (p *PlanOpGroupBy) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + // TODO(pok) implement group by with group by expressions + i, err := p.ChildOp.Iterator(ctx, row) + if err != nil { + return nil, err + } + aggs := []types.PlanExpression{} + aggs = append(aggs, p.GroupByExprs...) + aggs = append(aggs, p.Aggregates...) + return newGroupByIter(ctx, aggs, i), nil +} + +func (p *PlanOpGroupBy) Children() []types.PlanOperator { + return []types.PlanOperator{ + p.ChildOp, + } +} + +func (p *PlanOpGroupBy) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpGroupBy(p.Aggregates, p.GroupByExprs, children[0]), nil +} + +func (p *PlanOpGroupBy) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + result["child"] = p.ChildOp.Plan() + ps := make([]interface{}, 0) + for _, e := range p.Aggregates { + ps = append(ps, e.Plan()) + } + result["aggregates"] = ps + ps = make([]interface{}, 0) + for _, e := range p.GroupByExprs { + ps = append(ps, e.Plan()) + } + result["groupByExprs"] = ps + return result +} + +func (p *PlanOpGroupBy) String() string { + return "" +} + +func (p *PlanOpGroupBy) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpGroupBy) Warnings() []string { + var w []string + w = append(w, p.warnings...) + w = append(w, p.ChildOp.Warnings()...) + return w +} + +type groupByIter struct { + aggregates []types.PlanExpression + child types.RowIterator + ctx context.Context + buf []types.AggregationBuffer + done bool +} + +func newGroupByIter(ctx context.Context, aggregates []types.PlanExpression, child types.RowIterator) *groupByIter { + return &groupByIter{ + aggregates: aggregates, + child: child, + ctx: ctx, + buf: make([]types.AggregationBuffer, len(aggregates)), + } +} + +func (i *groupByIter) Next(ctx context.Context) (types.Row, error) { + if i.done { + return nil, types.ErrNoMoreRows + } + + i.done = true + + var err error + for j, a := range i.aggregates { + i.buf[j], err = newAggregationBuffer(a) + if err != nil { + return nil, err + } + } + + for { + row, err := i.child.Next(ctx) + if err != nil { + if err == types.ErrNoMoreRows { + break + } + return nil, err + } + + if err := updateBuffers(ctx, i.buf, row); err != nil { + return nil, err + } + } + + return evalBuffers(ctx, i.buf) +} + +func newAggregationBuffer(expr types.PlanExpression) (types.AggregationBuffer, error) { + switch n := expr.(type) { + case types.Aggregable: + return n.NewBuffer() + default: + return NewAggLastBuffer(expr), nil + } +} + +func updateBuffers(ctx context.Context, buffers []types.AggregationBuffer, row types.Row) error { + for _, b := range buffers { + if err := b.Update(ctx, row); err != nil { + return err + } + } + return nil +} + +func evalBuffers(ctx context.Context, buffers []types.AggregationBuffer) (types.Row, error) { + var row = make(types.Row, len(buffers)) + var err error + for i, b := range buffers { + row[i], err = b.Eval(ctx) + if err != nil { + return nil, err + } + } + return row, nil +} diff --git a/sql3/planner/opinsert.go b/sql3/planner/opinsert.go new file mode 100644 index 000000000..551a100c7 --- /dev/null +++ b/sql3/planner/opinsert.go @@ -0,0 +1,364 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "strings" + "time" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpInsert plan operator to handle INSERT. +type PlanOpInsert struct { + planner *ExecutionPlanner + tableName string + targetColumns []*qualifiedRefPlanExpression + insertValues []types.PlanExpression + warnings []string +} + +func NewPlanOpInsert(p *ExecutionPlanner, tableName string, targetColumns []*qualifiedRefPlanExpression, insertValues []types.PlanExpression) *PlanOpInsert { + return &PlanOpInsert{ + planner: p, + tableName: tableName, + targetColumns: targetColumns, + insertValues: insertValues, + } +} + +func (p *PlanOpInsert) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + result["tableName"] = p.tableName + ps := make([]interface{}, 0) + for _, e := range p.targetColumns { + ps = append(ps, e.Plan()) + } + result["targetColumns"] = ps + ps = make([]interface{}, 0) + for _, e := range p.insertValues { + ps = append(ps, e.Plan()) + } + result["insertValues"] = ps + return result +} + +func (p *PlanOpInsert) String() string { + return "" +} + +func (p *PlanOpInsert) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpInsert) Warnings() []string { + return p.warnings +} + +func (p *PlanOpInsert) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpInsert) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpInsert) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &insertRowIter{ + planner: p.planner, + tableName: p.tableName, + targetColumns: p.targetColumns, + insertValues: p.insertValues, + }, nil +} + +func (p *PlanOpInsert) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return NewPlanOpInsert(p.planner, p.tableName, p.targetColumns, p.insertValues), nil +} + +type insertRowIter struct { + planner *ExecutionPlanner + tableName string + targetColumns []*qualifiedRefPlanExpression + insertValues []types.PlanExpression +} + +var _ types.RowIterator = (*insertRowIter)(nil) + +func (i *insertRowIter) Next(ctx context.Context) (types.Row, error) { + qcx := i.planner.computeAPI.Txf().NewQcx() + + colIDs := make([]uint64, 0) + colKeys := make([]string, 0) + + addColID := func(v interface{}) error { + switch id := v.(type) { + case int64: + colIDs = append(colIDs, uint64(id)) + case uint64: + colIDs = append(colIDs, id) + case string: + colKeys = append(colKeys, id) + default: + return sql3.NewErrInternalf("unhandled _id data type '%T'", id) + } + return nil + } + + //find the _id column and evaluate + var err error + var columnID interface{} + for idx, iv := range i.insertValues { + targetColumn := i.targetColumns[idx] + if strings.EqualFold(targetColumn.columnName, "_id") { + columnID, err = iv.Evaluate(nil) + if err != nil { + return nil, err + } + break + } + } + + //eval all the expressions and do the insert + for idx, iv := range i.insertValues { + colIDs = make([]uint64, 0) + colKeys = make([]string, 0) + + targetColumn := i.targetColumns[idx] + + if strings.EqualFold(targetColumn.columnName, "_id") { + continue + } + + eval, err := iv.Evaluate(nil) + if err != nil { + return nil, err + } + + //nothing to do if a value is null + if eval == nil { + continue + } + + sourceType := iv.Type() + switch targetType := i.targetColumns[idx].dataType.(type) { + case *parser.DataTypeInt: + + err = addColID(columnID) + if err != nil { + return nil, err + } + + vals := make([]int64, 1) + vals[0] = eval.(int64) + + req := &pilosa.ImportValueRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + Values: vals, + } + + err = i.planner.computeAPI.ImportValue(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeBool: + err = addColID(columnID) + if err != nil { + return nil, err + } + + val := eval.(bool) + vals := make([]uint64, 1) + if val { + vals[0] = 1 + } else { + vals[0] = 0 + } + + req := &pilosa.ImportRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowIDs: vals, + } + + err = i.planner.computeAPI.Import(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeDecimal: + err = addColID(columnID) + if err != nil { + return nil, err + } + + vals := make([]float64, 1) + vals[0] = eval.(pql.Decimal).Float64() + + req := &pilosa.ImportValueRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + FloatValues: vals, + } + + err = i.planner.computeAPI.ImportValue(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeID: + err = addColID(columnID) + if err != nil { + return nil, err + } + + coercedVal, err := coerceValue(sourceType, targetType, eval, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + vals := make([]uint64, 1) + vals[0] = coercedVal.(uint64) + + req := &pilosa.ImportRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowIDs: vals, + } + + err = i.planner.computeAPI.Import(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeIDSet: + rowIDs := make([]uint64, 0) + rowSet := eval.([]int64) + for k := range rowSet { + err = addColID(columnID) + if err != nil { + return nil, err + } + rowIDs = append(rowIDs, uint64(rowSet[k])) + } + + req := &pilosa.ImportRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowIDs: rowIDs, + } + err = i.planner.computeAPI.Import(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeString: + err = addColID(columnID) + if err != nil { + return nil, err + } + + rowKeys := make([]string, 1) + rowKeys[0] = eval.(string) + + req := &pilosa.ImportRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowKeys: rowKeys, + } + err = i.planner.computeAPI.Import(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeStringSet: + rowKeys := make([]string, 0) + rowSet := eval.([]string) + for k := range rowSet { + err = addColID(columnID) + if err != nil { + return nil, err + } + rowKeys = append(rowKeys, rowSet[k]) + } + + req := &pilosa.ImportRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowKeys: rowKeys, + } + err = i.planner.computeAPI.Import(ctx, qcx, req) + if err != nil { + return nil, err + } + + case *parser.DataTypeTimestamp: + err = addColID(columnID) + if err != nil { + return nil, err + } + + coercedVal, err := coerceValue(sourceType, targetType, eval, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + vals := make([]time.Time, 1) + vals[0] = coercedVal.(time.Time) + + req := &pilosa.ImportValueRequest{ + Index: i.tableName, + Field: targetColumn.columnName, + Shard: 0, //TODO: handle non-0 shards + ColumnIDs: colIDs, + ColumnKeys: colKeys, + TimestampValues: vals, + } + + err = i.planner.computeAPI.ImportValue(ctx, qcx, req) + if err != nil { + return nil, err + } + + default: + return nil, sql3.NewErrInternalf("unhandled data type '%T'", iv.Type()) + } + } + + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opnestedloops.go b/sql3/planner/opnestedloops.go new file mode 100644 index 000000000..5dcaecfb2 --- /dev/null +++ b/sql3/planner/opnestedloops.go @@ -0,0 +1,340 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpNestedLoops plan operator handles a join +// For each row in the top input, scan the bottom input and output matching rows +type PlanOpNestedLoops struct { + top types.PlanOperator + bottom types.PlanOperator + warnings []string +} + +func NewPlanOpNestedLoops(top, bottom types.PlanOperator) *PlanOpNestedLoops { + return &PlanOpNestedLoops{ + top: top, + bottom: bottom, + } +} + +func (p *PlanOpNestedLoops) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + result["top"] = p.top.Plan() + result["bottom"] = p.bottom.Plan() + return result +} + +func (p *PlanOpNestedLoops) String() string { + return "" +} + +func (p *PlanOpNestedLoops) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpNestedLoops) Warnings() []string { + return p.warnings +} + +func (p *PlanOpNestedLoops) Schema() types.Schema { + result := types.Schema{} + result = append(result, p.top.Schema()...) + result = append(result, p.bottom.Schema()...) + return result +} + +func (p *PlanOpNestedLoops) Children() []types.PlanOperator { + return []types.PlanOperator{ + p.top, + p.bottom, + } +} + +func (p *PlanOpNestedLoops) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + topIter, err := p.top.Iterator(ctx, row) + if err != nil { + return nil, err + } + + rowWidth := len(row) + len(p.top.Schema()) + len(p.bottom.Schema()) + return newNestedLoopsIter(ctx, joinTypeInner, topIter, p.bottom, row, nil, rowWidth, row), nil +} + +func (p *PlanOpNestedLoops) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 2 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpNestedLoops(children[0], children[1]), nil +} + +type joinType byte + +const ( + joinTypeInner joinType = iota + joinTypeLeft + joinTypeRight +) + +// joinMode defines the mode in which a join will be performed. +type joinMode byte + +const ( + // unknownMode is the default mode. It will start iterating without really + // knowing in which mode it will end up computing the join. If it + // iterates the right side fully one time and so far it fits in memory, + // then it will switch to memory mode. Otherwise, if at some point during + // this first iteration it finds that it does not fit in memory, will + // switch to multipass mode. + unknownMode joinMode = iota + // memoryMode computes all the join directly in memory iterating each + // side of the join exactly once. + //memoryMode + // multipassMode computes the join by iterating the left side once, + // and the right side one time for each row in the left side. + multipassMode +) + +type nestedLoopsIter struct { + typ joinType + + top types.RowIterator + bottom types.RowIterator + ctx context.Context + cond types.PlanExpression + + secondaryProvider types.RowIterable + + primaryRow types.Row + foundMatch bool + rowSize int + + originalRow types.Row + scopeLen int + + mode joinMode + secondaryRows RowCache +} + +func newNestedLoopsIter(ctx context.Context, jt joinType, top types.RowIterator, bottom types.RowIterable, scopeRow types.Row, joinCondition types.PlanExpression, rowWidth int, originalRow types.Row) *nestedLoopsIter { + return &nestedLoopsIter{ + typ: jt, + top: top, + secondaryProvider: bottom, + cond: joinCondition, + rowSize: rowWidth, + originalRow: originalRow, + secondaryRows: newInMemoryRowCache(), + ctx: ctx, + } +} + +func (i *nestedLoopsIter) loadPrimary(ctx context.Context) error { + // If primary has already been loaded, it's safe to no-op. + if i.primaryRow != nil { + return nil + } + + r, err := i.top.Next(ctx) + if err != nil { + return err + } + i.primaryRow = i.originalRow.Append(r) + i.foundMatch = false + + return nil +} + +/*func (i *nestedLoopsIter) loadSecondaryInMemory(ctx context.Context) error { + iter, err := i.secondaryProvider.Iterator(ctx, i.primaryRow) + if err != nil { + return err + } + + for { + row, err := iter.Next(ctx) + if err == types.ErrNoMoreRows { + break + } + if err != nil { + //iter.Close(ctx) + return err + } + + if err := i.secondaryRows.Add(row); err != nil { + //iter.Close(ctx) + return err + } + } + + //err = iter.Close(ctx) + //if err != nil { + // return err + //} + + if len(i.secondaryRows.Get()) == 0 { + return types.ErrNoMoreRows + } + + return nil +}*/ + +func (i *nestedLoopsIter) loadSecondary(ctx context.Context) (row types.Row, err error) { + /*if i.mode == memoryMode { + if len(i.secondaryRows.Get()) == 0 { + if err = i.loadSecondaryInMemory(ctx); err != nil { + if err == types.ErrNoMoreRows { + i.primaryRow = nil + i.pos = 0 + } + return nil, err + } + } + + if i.pos >= len(i.secondaryRows.Get()) { + i.primaryRow = nil + i.pos = 0 + return nil, types.ErrNoMoreRows + } + + row := i.secondaryRows.Get()[i.pos] + i.pos++ + return row, nil + }*/ + + if i.bottom == nil { + var iter types.RowIterator + iter, err = i.secondaryProvider.Iterator(ctx, i.primaryRow) + if err != nil { + return nil, err + } + + i.bottom = iter + } + + rightRow, err := i.bottom.Next(ctx) + if err != nil { + if err == types.ErrNoMoreRows { + //err = i.bottom.Close(ctx) + i.bottom = nil + //if err != nil { + // return nil, err + //} + i.primaryRow = nil + + // If we got to this point and the mode is still unknown it means + // the right side fits in memory, so the mode changes to memory + // join. + //if i.mode == unknownMode { + // i.mode = memoryMode + //} + + return nil, types.ErrNoMoreRows + } + return nil, err + } + + if i.mode == unknownMode { + var switchToMultipass bool + //if !ctx.Memory.HasAvailable() { + //switchToMultipass = true + //} else { + err := i.secondaryRows.Add(rightRow) + if err != nil { //&& !sql.ErrNoMemoryAvailable.Is(err) { + return nil, err + } + //} + + if switchToMultipass { + //i.Dispose() + i.secondaryRows = nil + i.mode = multipassMode + } + } + + return rightRow, nil +} + +func (i *nestedLoopsIter) buildRow(primary, secondary types.Row) types.Row { + toCut := len(i.originalRow) - i.scopeLen + row := make(types.Row, i.rowSize-toCut) + + scope := primary[:i.scopeLen] + primary = primary[len(i.originalRow):] + + var first, second types.Row + var secondOffset int + switch i.typ { + case joinTypeRight: + first = secondary + second = primary + secondOffset = len(row) - len(second) + default: + first = primary + second = secondary + secondOffset = i.scopeLen + len(first) + } + + copy(row, scope) + copy(row[i.scopeLen:], first) + copy(row[secondOffset:], second) + return row +} + +func conditionIsTrue(ctx context.Context, row types.Row, cond types.PlanExpression) (bool, error) { + if cond == nil { + return true, nil + } + v, err := cond.Evaluate(row) + if err != nil { + return false, err + } + return v == true, nil +} + +func (i *nestedLoopsIter) Next(ctx context.Context) (types.Row, error) { + for { + if err := i.loadPrimary(ctx); err != nil { + return nil, err + } + + primary := i.primaryRow + secondary, err := i.loadSecondary(ctx) + if err != nil { + if err == types.ErrNoMoreRows { + if !i.foundMatch && (i.typ == joinTypeLeft || i.typ == joinTypeRight) { + row := i.buildRow(primary, nil) + return row, nil + } + continue + } + return nil, err + } + + row := i.buildRow(primary, secondary) + matches, err := conditionIsTrue(ctx, row, i.cond) + if err != nil { + return nil, err + } + + if !matches { + continue + } + + i.foundMatch = true + return row, nil + } +} diff --git a/sql3/planner/opnulltable.go b/sql3/planner/opnulltable.go new file mode 100644 index 000000000..66e16f7ab --- /dev/null +++ b/sql3/planner/opnulltable.go @@ -0,0 +1,71 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpNullTable is an operator for a null table +// basically when you do select 1, you're using the null table +type PlanOpNullTable struct { + warnings []string +} + +func NewPlanOpNullTable() *PlanOpNullTable { + return &PlanOpNullTable{} +} + +func (p *PlanOpNullTable) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpNullTable) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &nullTableIterator{}, nil +} + +func (p *PlanOpNullTable) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return NewPlanOpNullTable(), nil +} + +func (p *PlanOpNullTable) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpNullTable) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + return result +} + +func (p *PlanOpNullTable) String() string { + return "" +} + +func (p *PlanOpNullTable) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpNullTable) Warnings() []string { + return p.warnings +} + +type nullTableIterator struct { + rowConsumed bool +} + +func (i *nullTableIterator) Next(ctx context.Context) (types.Row, error) { + if !i.rowConsumed { + i.rowConsumed = true + return make([]interface{}, 0), nil + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/oporderby.go b/sql3/planner/oporderby.go new file mode 100644 index 000000000..3e8b3e26c --- /dev/null +++ b/sql3/planner/oporderby.go @@ -0,0 +1,274 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// orderByOrder is the direction of the order by (ascending or descending). +type orderByOrder int + +const ( + orderByAsc orderByOrder = 1 + orderByDesc orderByOrder = 2 +) + +// nullOrdering specifies how to handle null values during order by. +type nullOrdering byte + +const ( + nullOrderingFirst nullOrdering = iota + nullOrderingLast nullOrdering = 2 +) + +// OrderByExpression is the expression on which an order by can be computed +type OrderByExpression struct { + Expr types.PlanExpression + Order orderByOrder + NullOrdering nullOrdering +} + +// PlanOpOrderBy plan operator handles ORDER BY +type PlanOpOrderBy struct { + ChildOp types.PlanOperator + orderByFields []*OrderByExpression + + warnings []string +} + +func NewPlanOpOrderBy(orderByFields []*OrderByExpression, child types.PlanOperator) *PlanOpOrderBy { + return &PlanOpOrderBy{ + ChildOp: child, + orderByFields: orderByFields, + } +} + +func (n *PlanOpOrderBy) Schema() types.Schema { + return n.ChildOp.Schema() +} + +func (n *PlanOpOrderBy) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + iter, err := n.ChildOp.Iterator(ctx, row) + if err != nil { + return nil, err + } + return newOrderByIter(ctx, n, iter), nil +} + +func (n *PlanOpOrderBy) Children() []types.PlanOperator { + return []types.PlanOperator{ + n.ChildOp, + } +} + +func (n *PlanOpOrderBy) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpOrderBy(n.orderByFields, children[0]), nil +} + +func (n *PlanOpOrderBy) String() string { + return "" +} + +func (n *PlanOpOrderBy) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", n) + sc := make([]string, 0) + for _, e := range n.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + result["child"] = n.ChildOp.Plan() + ps := make([]interface{}, 0) + for _, e := range n.orderByFields { + ps = append(ps, &map[string]interface{}{ + "expr": e.Expr.Plan(), + "order": e.Order, + "nullOrdering": e.NullOrdering, + }) + } + result["orderByFields"] = ps + return result +} + +func (n *PlanOpOrderBy) AddWarning(warning string) { + n.warnings = append(n.warnings, warning) +} + +func (n *PlanOpOrderBy) Warnings() []string { + var w []string + w = append(w, n.warnings...) + w = append(w, n.ChildOp.Warnings()...) + return w +} + +type orderByIter struct { + s *PlanOpOrderBy + childIter types.RowIterator + sortedRows []types.Row +} + +var _ types.RowIterator = (*orderByIter)(nil) + +func newOrderByIter(ctx context.Context, s *PlanOpOrderBy, child types.RowIterator) *orderByIter { + return &orderByIter{ + s: s, + childIter: child, + } +} + +func (i *orderByIter) Next(ctx context.Context) (types.Row, error) { + if i.sortedRows == nil { + err := i.computeOrderByRows(ctx) + if err != nil { + return nil, err + } + } + + if len(i.sortedRows) > 0 { + row := i.sortedRows[0] + // Move to next result element. + i.sortedRows = i.sortedRows[1:] + return row, nil + } + return nil, types.ErrNoMoreRows +} + +func (i *orderByIter) computeOrderByRows(ctx context.Context) error { + cache := newInMemoryRowCache() + + for { + row, err := i.childIter.Next(ctx) + + if err == types.ErrNoMoreRows { + break + } + if err != nil { + return err + } + + if err := cache.Add(row); err != nil { + return err + } + } + + rows := cache.AllRows() + sorter := &OrderBySorter{ + SortFields: i.s.orderByFields, + Rows: rows, + LastError: nil, + Ctx: ctx, + } + sort.Stable(sorter) + if sorter.LastError != nil { + return sorter.LastError + } + i.sortedRows = rows + return nil +} + +type OrderBySorter struct { + SortFields []*OrderByExpression + Rows []types.Row + LastError error + Ctx context.Context +} + +func (s *OrderBySorter) Len() int { + return len(s.Rows) +} + +func (s *OrderBySorter) Swap(i, j int) { + s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i] +} + +func (s *OrderBySorter) Less(i, j int) bool { + if s.LastError != nil { + return false + } + + //TODO(pok) handle multi column sort + + a := s.Rows[i] + b := s.Rows[j] + for _, sf := range s.SortFields { + av, err := sf.Expr.Evaluate(a) + if err != nil { + s.LastError = sql3.NewErrInternalf("unable to sort '%s'", err.Error()) + return false + } + + bv, err := sf.Expr.Evaluate(b) + if err != nil { + s.LastError = sql3.NewErrInternalf("unable to sort '%s'", err.Error()) + return false + } + + if sf.Order == orderByDesc { + av, bv = bv, av + } + + if av == nil && bv == nil { + continue + } else if av == nil { + return sf.NullOrdering == nullOrderingFirst + } else if bv == nil { + return sf.NullOrdering != nullOrderingFirst + } + + switch sf.Expr.Type().(type) { + case *parser.DataTypeInt, *parser.DataTypeID: + avInt, aok := av.(int64) + bvInt, bok := bv.(int64) + if !(aok && bok) { + s.LastError = sql3.NewErrInternalf("unexpected type conversion result") + return false + } + if avInt > bvInt { + return false + } + return true + + case *parser.DataTypeBool: + avBool, aok := av.(bool) + bvBool, bok := bv.(bool) + if !(aok && bok) { + s.LastError = sql3.NewErrInternalf("unexpected type conversion result") + return false + } + if avBool == bvBool { + return false + } + return true + + case *parser.DataTypeTimestamp: + avTime, aok := av.(time.Time) + bvTime, bok := bv.(time.Time) + if !(aok && bok) { + s.LastError = sql3.NewErrInternalf("unexpected type conversion result") + return false + } + if avTime.After(bvTime) { + return false + } + return true + + default: + s.LastError = sql3.NewErrInternalf("unhandled data type '%T'", sf.Expr.Type()) + return false + } + } + + return false +} diff --git a/sql3/planner/oppqlaggregate.go b/sql3/planner/oppqlaggregate.go new file mode 100644 index 000000000..2fb998460 --- /dev/null +++ b/sql3/planner/oppqlaggregate.go @@ -0,0 +1,263 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpPQLAggregate plan operator handles a single pql aggregate +type PlanOpPQLAggregate struct { + planner *ExecutionPlanner + tableName string + filter types.PlanExpression + aggregate types.Aggregable + + warnings []string +} + +func NewPlanOpPQLAggregate(p *ExecutionPlanner, tableName string, aggregate types.Aggregable, filter types.PlanExpression) *PlanOpPQLAggregate { + return &PlanOpPQLAggregate{ + planner: p, + tableName: tableName, + filter: filter, + aggregate: aggregate, + } +} + +func (p *PlanOpPQLAggregate) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + ps := make([]string, 0) + for _, e := range p.Schema() { + ps = append(ps, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = ps + result["tableName"] = p.tableName + if p.filter != nil { + result["filter"] = p.filter.Plan() + } + result["aggregate"] = p.aggregate.AggExpression().Plan() + return result + +} + +func (p *PlanOpPQLAggregate) String() string { + return "" +} + +func (p *PlanOpPQLAggregate) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpPQLAggregate) Warnings() []string { + return p.warnings +} + +func (p *PlanOpPQLAggregate) Schema() types.Schema { + result := make(types.Schema, 1) + s := &types.PlannerColumn{ + Name: "", + Table: "", + Type: p.aggregate.AggExpression().Type(), + } + result[0] = s + return result +} + +func (p *PlanOpPQLAggregate) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpPQLAggregate) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &pqlAggregateRowIter{ + planner: p.planner, + tableName: p.tableName, + filter: p.filter, + aggregate: p.aggregate, + }, nil +} + +func (p *PlanOpPQLAggregate) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return NewPlanOpPQLAggregate(p.planner, p.tableName, p.aggregate, p.filter), nil +} + +type pqlAggregateRowIter struct { + planner *ExecutionPlanner + tableName string + filter types.PlanExpression + aggregate types.Aggregable + + resultValue interface{} +} + +var _ types.RowIterator = (*pqlAggregateRowIter)(nil) + +func (i *pqlAggregateRowIter) Next(ctx context.Context) (types.Row, error) { + if i.resultValue == nil { + var call *pql.Call + var cond *pql.Call + var err error + + err = i.planner.checkAccess(ctx, i.tableName, accessTypeReadData) + if err != nil { + return nil, err + } + + cond, err = i.planner.generatePQLCallFromExpr(ctx, i.filter) + if err != nil { + return nil, err + } + + expr, ok := i.aggregate.AggExpression().(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate expression type '%T'", i.aggregate.AggExpression()) + } + + switch i.aggregate.AggType() { + case types.AGGREGATE_COUNT_DISTINCT: + //make a distinct call + distinctCond := &pql.Call{ + Name: "Distinct", + Args: map[string]interface{}{"field": expr.columnName}, + Type: pql.PrecallGlobal, + } + //add the cond to the distinct + if cond != nil { + distinctCond.Children = []*pql.Call{cond} + } + cond = distinctCond + + call = &pql.Call{Name: "Count", Children: []*pql.Call{cond}} + + case types.AGGREGATE_COUNT: + if cond == nil { + cond = &pql.Call{Name: "All"} + } + + call = &pql.Call{Name: "Count", Children: []*pql.Call{cond}} + + case types.AGGREGATE_AVG: + if cond == nil { + cond = &pql.Call{Name: "All"} + } + + call = &pql.Call{ + Name: "Sum", + Args: map[string]interface{}{"field": expr.columnName}, + Children: []*pql.Call{cond}, + } + + case types.AGGREGATE_SUM: + if cond == nil { + cond = &pql.Call{Name: "All"} + } + call = &pql.Call{ + Name: "Sum", + Args: map[string]interface{}{"field": expr.columnName}, + Children: []*pql.Call{cond}, + } + + case types.AGGREGATE_MAX: + if cond == nil { + cond = &pql.Call{Name: "All"} + } + + call = &pql.Call{ + Name: "Max", + Args: map[string]interface{}{"field": expr.columnName}, + Children: []*pql.Call{cond}, + } + + case types.AGGREGATE_MIN: + if cond == nil { + cond = &pql.Call{Name: "All"} + } + + call = &pql.Call{ + Name: "Min", + Args: map[string]interface{}{"field": expr.columnName}, + Children: []*pql.Call{cond}, + } + + case types.AGGREGATE_PERCENTILE: + + additionalExprs := i.aggregate.AggAdditionalExpr() + if len(additionalExprs) != 1 { + return nil, sql3.NewErrInternalf("unexpected AggAdditionalExpr() length (%d)", len(additionalExprs)) + } + nthExpr := additionalExprs[0] + + nthValue, err := nthExpr.Evaluate(nil) + if err != nil { + return nil, err + } + coercedNthValue, err := coerceValue(nthExpr.Type(), parser.NewDataTypeDecimal(4), nthValue, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + nth, ok := coercedNthValue.(pql.Decimal) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate nth arg type '%T'", coercedNthValue) + } + + if cond == nil { + cond = &pql.Call{Name: "All"} + } + + call = &pql.Call{ + Name: "Percentile", + Args: map[string]interface{}{ + "field": expr.columnName, + "nth": nth, + }, + Children: []*pql.Call{cond}, + } + + default: + return nil, sql3.NewErrInternalf("unhandled aggregate type '%d'", i.aggregate.AggType()) + } + + queryResponse, err := i.planner.executor.Execute(ctx, i.tableName, &pql.Query{Calls: []*pql.Call{call}}, nil, nil) + if err != nil { + return nil, err + } + + switch actualResult := queryResponse.Results[0].(type) { + case uint64: + i.resultValue = int64(actualResult) + + case pilosa.ValCount: + if actualResult.DecimalVal == nil { + if i.aggregate.AggType() == types.AGGREGATE_AVG { + average := float64(actualResult.Val) / float64(actualResult.Count) + i.resultValue = pql.NewDecimal(int64(average*10000), 4) + } else { + i.resultValue = int64(actualResult.Val) + } + } else { + if i.aggregate.AggType() == types.AGGREGATE_AVG { + average := actualResult.DecimalVal.Float64() / float64(actualResult.Count) + i.resultValue = pql.NewDecimal(int64(average*10000), 4) + } else { + i.resultValue = *actualResult.DecimalVal + } + } + default: + return nil, sql3.NewErrInternalf("unexpected result type '%T'", queryResponse.Results[0]) + } + + row := make([]interface{}, 1) + row[0] = i.resultValue + return row, nil + + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/oppqlgroupby.go b/sql3/planner/oppqlgroupby.go new file mode 100644 index 000000000..4417cc03c --- /dev/null +++ b/sql3/planner/oppqlgroupby.go @@ -0,0 +1,267 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpPQLGroupBy plan operator handles a PQL group by with a single aggregate +type PlanOpPQLGroupBy struct { + planner *ExecutionPlanner + tableName string + filter types.PlanExpression + aggregate types.Aggregable + groupByExprs []types.PlanExpression + + warnings []string +} + +func NewPlanOpPQLGroupBy(p *ExecutionPlanner, tableName string, groupByExprs []types.PlanExpression, filter types.PlanExpression, aggregate types.Aggregable) *PlanOpPQLGroupBy { + return &PlanOpPQLGroupBy{ + planner: p, + tableName: tableName, + groupByExprs: groupByExprs, + filter: filter, + aggregate: aggregate, + } +} + +func (p *PlanOpPQLGroupBy) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + result["tableName"] = p.tableName + if p.filter != nil { + result["filter"] = p.filter.Plan() + + } + result["aggregate"] = p.aggregate.AggExpression().Plan() + ps := make([]interface{}, 0) + for _, e := range p.groupByExprs { + ps = append(ps, e.Plan()) + } + result["groupByColumns"] = ps + return result +} + +func (p *PlanOpPQLGroupBy) String() string { + return "" +} + +func (p *PlanOpPQLGroupBy) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpPQLGroupBy) Warnings() []string { + return p.warnings +} + +func (p *PlanOpPQLGroupBy) Schema() types.Schema { + result := make(types.Schema, len(p.groupByExprs)+1) + for idx, expr := range p.groupByExprs { + ref, ok := expr.(*qualifiedRefPlanExpression) + if !ok { + continue + } + s := &types.PlannerColumn{ + Name: ref.columnName, + Table: ref.tableName, + Type: expr.Type(), + } + result[idx] = s + } + s := &types.PlannerColumn{ + Name: "", + Table: "", + Type: p.aggregate.AggExpression().Type(), + } + result[len(p.groupByExprs)] = s + + return result +} + +func (p *PlanOpPQLGroupBy) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpPQLGroupBy) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &pqlGroupByRowIter{ + planner: p.planner, + tableName: p.tableName, + groupByColumns: p.groupByExprs, + aggregate: p.aggregate, + filter: p.filter, + }, nil +} + +func (p *PlanOpPQLGroupBy) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +// pqlGroupByRowIter is an iterator for the PlanOpPQLGroupBy operator +// it provides rows consisting of the group by columns in the order they +// were specified and lastly the aggregate +type pqlGroupByRowIter struct { + planner *ExecutionPlanner + tableName string + groupByColumns []types.PlanExpression + filter types.PlanExpression + aggregate types.Aggregable + + result []pilosa.GroupCount +} + +var _ types.RowIterator = (*pqlGroupByRowIter)(nil) + +func (i *pqlGroupByRowIter) Next(ctx context.Context) (types.Row, error) { + if i.result == nil { + + var cond *pql.Call + var err error + + err = i.planner.checkAccess(ctx, i.tableName, accessTypeReadData) + if err != nil { + return nil, err + } + + cond, err = i.planner.generatePQLCallFromExpr(ctx, i.filter) + if err != nil { + return nil, err + } + + call := &pql.Call{ + Name: "GroupBy", + Args: map[string]interface{}{}, + } + for _, c := range i.groupByColumns { + ref, ok := c.(types.SchemaIdentifiable) + if !ok { + return nil, sql3.NewErrInternalf("unexpected expression type in group by list '%T'", c) + } + //don't ask for the _id field + if ref.Name() != "_id" { + call.Children = append(call.Children, + &pql.Call{ + Name: "Rows", + Args: map[string]interface{}{"_field": ref.Name()}, + }, + ) + } + } + + // Apply filter & aggregate, if set. + aggExpr, ok := i.aggregate.AggExpression().(*qualifiedRefPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected aggregate expression type '%T'", i.aggregate.AggExpression()) + } + + switch i.aggregate.AggType() { + case types.AGGREGATE_COUNT: + //nop + + case types.AGGREGATE_COUNT_DISTINCT: + aggregate := &pql.Call{ + Name: "Count", + Children: []*pql.Call{{ + Name: "Distinct", + Args: map[string]interface{}{"field": aggExpr.columnName}, + }}, + } + call.Args["aggregate"] = aggregate + + case types.AGGREGATE_SUM, types.AGGREGATE_AVG: + aggregate := &pql.Call{ + Name: "Sum", + Args: map[string]interface{}{"field": aggExpr.columnName}, + } + call.Args["aggregate"] = aggregate + + case types.AGGREGATE_PERCENTILE: + return nil, sql3.NewErrAggregateNotAllowedInGroupBy(0, 0, "PERCENTILE()") + + case types.AGGREGATE_MIN: + return nil, sql3.NewErrAggregateNotAllowedInGroupBy(0, 0, "MIN()") + + case types.AGGREGATE_MAX: + return nil, sql3.NewErrAggregateNotAllowedInGroupBy(0, 0, "MAX()") + + default: + return nil, sql3.NewErrInternalf("unexpected agg function type: %d", i.aggregate.AggType()) + } + if cond != nil { + call.Args["filter"] = cond + } + + queryResponse, err := i.planner.executor.Execute(ctx, i.tableName, &pql.Query{Calls: []*pql.Call{call}}, nil, nil) + if err != nil { + return nil, err + } + tbl, ok := queryResponse.Results[0].(*pilosa.GroupCounts) + if !ok { + return nil, sql3.NewErrInternalf("unexpected Extract() result type: %T", queryResponse.Results[0]) + } + i.result = tbl.Groups() + } + + if len(i.result) > 0 { + //row width is group by columns + aggregate + row := make([]interface{}, len(i.groupByColumns)+1) + + group := i.result[0] + + //populate all the group by columns + for idx, c := range i.groupByColumns { + + g := group.Group[idx] + if g.Value != nil { + row[idx] = *g.Value + } else if g.RowKey != "" { + row[idx] = g.RowKey + } else { + switch c.Type().(type) { + case *parser.DataTypeIDSet: + row[idx] = []uint64{g.RowID} + default: + row[idx] = int64(g.RowID) + } + } + } + //now populate the aggregate value + aggIdx := len(i.groupByColumns) + switch i.aggregate.AggType() { + case types.AGGREGATE_COUNT: + row[aggIdx] = int64(group.Count) + + case types.AGGREGATE_COUNT_DISTINCT, types.AGGREGATE_SUM: + row[aggIdx] = int64(group.Agg) + + case types.AGGREGATE_AVG: + if group.DecimalAgg == nil { + average := float64(group.Agg) / float64(group.Count) + row[aggIdx] = pql.NewDecimal(int64(average*10000), 4) + } else { + average := group.DecimalAgg.Float64() / float64(group.Count) + row[aggIdx] = pql.NewDecimal(int64(average*10000), 4) + } + default: + return nil, sql3.NewErrInternalf("unhandled aggregate function type '%v'", i.aggregate.AggType()) + } + + // Move to next result element. + i.result = i.result[1:] + return row, nil + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/oppqlmultiaggregate.go b/sql3/planner/oppqlmultiaggregate.go new file mode 100644 index 000000000..b43edd766 --- /dev/null +++ b/sql3/planner/oppqlmultiaggregate.go @@ -0,0 +1,115 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpPQLMultiAggregate plan operator handles executing multiple 'sibling' pql aggregate queries +type PlanOpPQLMultiAggregate struct { + planner *ExecutionPlanner + operators []*PlanOpPQLAggregate + warnings []string +} + +func NewPlanOpPQLMultiAggregate(p *ExecutionPlanner, operators []*PlanOpPQLAggregate) *PlanOpPQLMultiAggregate { + return &PlanOpPQLMultiAggregate{ + planner: p, + operators: operators, + } +} + +func (p *PlanOpPQLMultiAggregate) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + ps := make([]interface{}, 0) + for _, e := range p.operators { + ps = append(ps, e.Plan()) + } + result["operators"] = ps + return result +} + +func (p *PlanOpPQLMultiAggregate) String() string { + return "" +} + +func (p *PlanOpPQLMultiAggregate) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpPQLMultiAggregate) Warnings() []string { + return p.warnings +} + +func (p *PlanOpPQLMultiAggregate) Schema() types.Schema { + result := make(types.Schema, len(p.operators)) + for idx, aggOp := range p.operators { + s := &types.PlannerColumn{ + Name: "", + Table: "", + Type: aggOp.aggregate.AggExpression().Type(), + } + result[idx] = s + } + return result +} + +func (p *PlanOpPQLMultiAggregate) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpPQLMultiAggregate) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + iterators := make([]types.RowIterator, 0) + + for _, op := range p.operators { + iter, err := op.Iterator(ctx, row) + if err != nil { + return nil, err + } + iterators = append(iterators, iter) + } + + return &pqlMultiAggregateRowIter{ + planner: p.planner, + iterators: iterators, + }, nil +} + +func (p *PlanOpPQLMultiAggregate) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +type pqlMultiAggregateRowIter struct { + planner *ExecutionPlanner + iterators []types.RowIterator + doneLatch bool +} + +var _ types.RowIterator = (*pqlMultiAggregateRowIter)(nil) + +func (i *pqlMultiAggregateRowIter) Next(ctx context.Context) (types.Row, error) { + if !i.doneLatch { + var row = make(types.Row, len(i.iterators)) + for idx, iter := range i.iterators { + irow, err := iter.Next(ctx) + if err != nil { + return nil, err + } + row[idx] = irow[0] + } + i.doneLatch = true + return row, nil + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/oppqlmultigroupby.go b/sql3/planner/oppqlmultigroupby.go new file mode 100644 index 000000000..0f646c7ce --- /dev/null +++ b/sql3/planner/oppqlmultigroupby.go @@ -0,0 +1,229 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpPQLMultiGroupBy plan operator handles executing multiple 'sibling' pql group by queries +// it will materialize the result sets from each of its operators and then merge them. +// Its iterator will return a row consisting of all the group by columns in the order specified +// followed by all aggregates in order +type PlanOpPQLMultiGroupBy struct { + planner *ExecutionPlanner + operators []*PlanOpPQLGroupBy + groupByExprs []types.PlanExpression + warnings []string +} + +func NewPlanOpPQLMultiGroupBy(p *ExecutionPlanner, operators []*PlanOpPQLGroupBy, groupByExprs []types.PlanExpression) *PlanOpPQLMultiGroupBy { + return &PlanOpPQLMultiGroupBy{ + planner: p, + operators: operators, + groupByExprs: groupByExprs, + } +} + +func (p *PlanOpPQLMultiGroupBy) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + ps := make([]interface{}, 0) + for _, e := range p.operators { + ps = append(ps, e.Plan()) + } + result["operators"] = ps + ps = make([]interface{}, 0) + for _, e := range p.groupByExprs { + ps = append(ps, e.Plan()) + } + result["groupByColumns"] = ps + return result +} + +func (p *PlanOpPQLMultiGroupBy) String() string { + return "" +} + +func (p *PlanOpPQLMultiGroupBy) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpPQLMultiGroupBy) Warnings() []string { + return p.warnings +} + +func (p *PlanOpPQLMultiGroupBy) Schema() types.Schema { + result := make(types.Schema, len(p.groupByExprs)+len(p.operators)) + for idx, expr := range p.groupByExprs { + ref, ok := expr.(*qualifiedRefPlanExpression) + if !ok { + continue + } + s := &types.PlannerColumn{ + Name: ref.columnName, + Table: ref.tableName, + Type: expr.Type(), + } + result[idx] = s + } + offset := len(p.groupByExprs) + for idx, aggOp := range p.operators { + s := &types.PlannerColumn{ + Name: "", + Table: "", + Type: aggOp.aggregate.AggExpression().Type(), + } + result[idx+offset] = s + } + + return result +} + +func (p *PlanOpPQLMultiGroupBy) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpPQLMultiGroupBy) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + iterators := make([]types.RowIterator, 0) + + for _, op := range p.operators { + iter, err := op.Iterator(ctx, row) + if err != nil { + return nil, err + } + iterators = append(iterators, iter) + } + + return &pqlMultiGroupByRowIter{ + planner: p.planner, + groupByColumns: p.groupByExprs, + iterators: iterators, + }, nil +} + +func (p *PlanOpPQLMultiGroupBy) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +// pqlMultiGroupByRowIter is an iterator for the PlanOpPQLMultiGroupBy operator +// it provides rows consisting of the group by columns in the order they +// were specified and lastly the aggregates in the order they were specified +type pqlMultiGroupByRowIter struct { + planner *ExecutionPlanner + groupByColumns []types.PlanExpression + iterators []types.RowIterator + groupCache KeyedRowCache + + groupKeys []string +} + +var _ types.RowIterator = (*pqlMultiGroupByRowIter)(nil) + +func (i *pqlMultiGroupByRowIter) Next(ctx context.Context) (types.Row, error) { + if i.groupCache == nil { + //consume all the rows from the child iterators + i.groupCache = newinMemoryKeyedRowCache() + if err := i.computeMultiGroupBy(ctx); err != nil { + return nil, err + } + } + + if len(i.groupKeys) > 0 { + key := i.groupKeys[0] + + row, err := i.groupCache.Get(key) + if err != nil { + return nil, err + } + // Move to next result element. + i.groupKeys = i.groupKeys[1:] + return row, nil + } + return nil, types.ErrNoMoreRows +} + +func (i *pqlMultiGroupByRowIter) computeMultiGroupBy(ctx context.Context) error { + //for each operator, consume all rows + for iteratorIdx, iter := range i.iterators { + + //get the first row + irow, err := iter.Next(ctx) + if err != nil { + if err == types.ErrNoMoreRows { + continue + } + return err + } + + for { + //build a key for the group by columns for this row + key, err := groupingKey(ctx, i.groupByColumns, irow) + if err != nil { + return err + } + + // get the group from the cache + cachedRow, err := i.groupCache.Get(key) + if err != nil { + return err + } + + aggIndex := iteratorIdx + len(i.groupByColumns) + if cachedRow != nil { + // if the group exists then update the row + // NB: the aggregate for this iterator is at the end of irow + cachedRow[aggIndex] = irow[len(irow)-1] + } else { + // if the group does not exist, add a row; set length to be number of group by columns + number of aggregates + cachedRow := make([]interface{}, len(i.groupByColumns)+len(i.iterators)) + // copy the group by values into the new row + for gidx := range i.groupByColumns { + cachedRow[gidx] = irow[gidx] + } + // write the aggregate value in + cachedRow[aggIndex] = irow[len(irow)-1] + + // write the row to the cache + err = i.groupCache.Put(key, cachedRow) + if err != nil { + return err + } + + //record the new key + i.groupKeys = append(i.groupKeys, key) + } + + irow, err = iter.Next(ctx) + if err != nil { + if err == types.ErrNoMoreRows { + break + } + return err + } + } + } + + return nil +} + +func groupingKey(ctx context.Context, exprs []types.PlanExpression, row types.Row) (string, error) { + key := "" + for _, expr := range exprs { + v, err := expr.Evaluate(row) + if err != nil { + return "", err + } + key += fmt.Sprintf(":%v", v) + } + return key, nil +} diff --git a/sql3/planner/opprojection.go b/sql3/planner/opprojection.go new file mode 100644 index 000000000..4e05250cf --- /dev/null +++ b/sql3/planner/opprojection.go @@ -0,0 +1,144 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpProjection handles row projection and expression evaluation +type PlanOpProjection struct { + ChildOp types.PlanOperator + Projections []types.PlanExpression + warnings []string +} + +func NewPlanOpProjection(expressions []types.PlanExpression, child types.PlanOperator) *PlanOpProjection { + return &PlanOpProjection{ + ChildOp: child, + Projections: expressions, + } +} + +func (p *PlanOpProjection) Schema() types.Schema { + var s = make(types.Schema, len(p.Projections)) + for i, e := range p.Projections { + s[i] = ExpressionToColumn(e) + } + return s +} + +func (p *PlanOpProjection) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + i, err := p.ChildOp.Iterator(ctx, row) + if err != nil { + return nil, err + } + return &iter{ + p: p, + childIter: i, + row: row, + }, nil +} + +func (p *PlanOpProjection) Children() []types.PlanOperator { + return []types.PlanOperator{ + p.ChildOp, + } +} + +func (p *PlanOpProjection) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpProjection(p.Projections, children[0]), nil +} + +func (p *PlanOpProjection) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + result["child"] = p.ChildOp.Plan() + + ps := make([]interface{}, 0) + for _, e := range p.Projections { + ps = append(ps, e.Plan()) + } + result["projections"] = ps + + return result +} + +func (p *PlanOpProjection) String() string { + return "" +} + +func (p *PlanOpProjection) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpProjection) Warnings() []string { + var w []string + w = append(w, p.warnings...) + if p.ChildOp != nil { + w = append(w, p.ChildOp.Warnings()...) + } + return w +} + +func ExpressionToColumn(e types.PlanExpression) *types.PlannerColumn { + var name string + if n, ok := e.(types.SchemaIdentifiable); ok { + name = n.Name() + } else { + //TODO(pok) - work out what this should be + name = "" //e.String() + } + + var table string + if t, ok := e.(types.SchemaObject); ok { + table = t.ObjectName() + } + + return &types.PlannerColumn{ + Name: name, + Type: e.Type(), + Table: table, + } +} + +type iter struct { + p *PlanOpProjection + childIter types.RowIterator + row types.Row +} + +func (i *iter) Next(ctx context.Context) (types.Row, error) { + childRow, err := i.childIter.Next(ctx) + if err != nil { + return nil, err + } + + return ProjectRow(ctx, i.p.Projections, childRow) +} + +// ProjectRow evaluates a set of projections. +func ProjectRow(ctx context.Context, projections []types.PlanExpression, row types.Row) (types.Row, error) { + var fields types.Row + for _, expr := range projections { + f, fErr := expr.Evaluate(row) + if fErr != nil { + return nil, fErr + } + fields = append(fields, f) + } + return fields, nil +} diff --git a/sql3/planner/opquery.go b/sql3/planner/opquery.go new file mode 100644 index 000000000..fb4196ae0 --- /dev/null +++ b/sql3/planner/opquery.go @@ -0,0 +1,88 @@ +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpQuery is a query - this is the root node of an execution plan +type PlanOpQuery struct { + ChildOp types.PlanOperator + + // the list of aggregate terms + aggregates []types.PlanExpression + + // all the identifiers that are referenced + referenceList []*qualifiedRefPlanExpression + + sql string + warnings []string +} + +var _ types.PlanOperator = (*PlanOpQuery)(nil) + +func NewPlanOpQuery(child types.PlanOperator, sql string) *PlanOpQuery { + return &PlanOpQuery{ + ChildOp: child, + } +} + +func (p *PlanOpQuery) Schema() types.Schema { + return p.ChildOp.Schema() +} + +func (p *PlanOpQuery) Child() types.PlanOperator { + return p.ChildOp +} + +func (p *PlanOpQuery) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return p.Child().Iterator(ctx, row) +} + +func (p *PlanOpQuery) Children() []types.PlanOperator { + return []types.PlanOperator{ + p.ChildOp, + } +} + +func (p *PlanOpQuery) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 1 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpQuery(children[0], p.sql), nil +} + +func (p *PlanOpQuery) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + result["sql"] = p.sql + result["warnings"] = p.warnings + result["child"] = p.ChildOp.Plan() + return result +} + +func (p *PlanOpQuery) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpQuery) Warnings() []string { + var w []string + w = append(w, p.warnings...) + if p.ChildOp != nil { + w = append(w, p.ChildOp.Warnings()...) + } + return w +} + +func (p *PlanOpQuery) String() string { + return "" +} diff --git a/sql3/planner/opsubquery.go b/sql3/planner/opsubquery.go new file mode 100644 index 000000000..aec295751 --- /dev/null +++ b/sql3/planner/opsubquery.go @@ -0,0 +1,71 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpSubquery is an operator for a subquery +type PlanOpSubquery struct { + ChildOp types.PlanOperator + warnings []string +} + +func NewPlanOpSubquery(child types.PlanOperator) *PlanOpSubquery { + return &PlanOpSubquery{ + ChildOp: child, + } +} + +func (p *PlanOpSubquery) Schema() types.Schema { + return p.ChildOp.Schema() +} + +func (p *PlanOpSubquery) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return p.ChildOp.Iterator(ctx, row) +} + +func (p *PlanOpSubquery) Children() []types.PlanOperator { + return []types.PlanOperator{ + p.ChildOp, + } +} + +func (p *PlanOpSubquery) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +func (p *PlanOpSubquery) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + result["child"] = p.ChildOp.Plan() + return result +} + +func (p *PlanOpSubquery) String() string { + return "" +} + +func (p *PlanOpSubquery) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpSubquery) Warnings() []string { + var w []string + w = append(w, p.warnings...) + if p.ChildOp != nil { + w = append(w, p.ChildOp.Warnings()...) + } + return w + +} diff --git a/sql3/planner/optablescan.go b/sql3/planner/optablescan.go new file mode 100644 index 000000000..a8848475b --- /dev/null +++ b/sql3/planner/optablescan.go @@ -0,0 +1,271 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" + "github.com/pkg/errors" +) + +// PlanOpPQLTableScan plan operator handles a PQL table scan +type PlanOpPQLTableScan struct { + planner *ExecutionPlanner + tableName string + columns []types.PlanExpression + filter types.PlanExpression + topExpr types.PlanExpression + warnings []string +} + +func NewPlanOpPQLTableScan(p *ExecutionPlanner, tableName string, columns []types.PlanExpression, filter types.PlanExpression) *PlanOpPQLTableScan { + return &PlanOpPQLTableScan{ + planner: p, + tableName: tableName, + columns: columns, + filter: filter, + } +} + +func (p *PlanOpPQLTableScan) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + result["tableName"] = p.tableName + + if p.topExpr != nil { + result["topExpr"] = p.topExpr.Plan() + } + if p.filter != nil { + result["filter"] = p.filter.Plan() + } + + ps := make([]interface{}, 0) + for _, c := range p.columns { + ps = append(ps, c.Plan()) + } + result["columns"] = ps + return result +} + +func (p *PlanOpPQLTableScan) String() string { + return "" +} + +func (p *PlanOpPQLTableScan) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpPQLTableScan) Warnings() []string { + return p.warnings +} + +func (p *PlanOpPQLTableScan) Schema() types.Schema { + result := make(types.Schema, 0) + for _, col := range p.columns { + si, ok := col.(types.SchemaIdentifiable) + if ok { + result = append(result, &types.PlannerColumn{ + Name: si.Name(), + Table: p.tableName, + Type: col.Type(), + }) + } + } + return result +} + +func (p *PlanOpPQLTableScan) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpPQLTableScan) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &tableScanRowIter{ + planner: p.planner, + tableName: p.tableName, + columns: p.columns, + predicate: p.filter, + topExpr: p.topExpr, + }, nil +} + +func (p *PlanOpPQLTableScan) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +// TODO(pok) remove the name mapping here and do it by ordinal position + +type tableScanRowIter struct { + planner *ExecutionPlanner + tableName string + columns []types.PlanExpression + predicate types.PlanExpression + topExpr types.PlanExpression + + result []pilosa.ExtractedTableColumn + rowWidth int + sourceColumnMap map[string]int + targetColumnMap map[string]int +} + +var _ types.RowIterator = (*tableScanRowIter)(nil) + +func (i *tableScanRowIter) Next(ctx context.Context) (types.Row, error) { + if i.result == nil { + err := i.planner.checkAccess(ctx, i.tableName, accessTypeReadData) + if err != nil { + return nil, err + } + + //go get the schema def and map names to indexes in the resultant row + table, err := i.planner.schemaAPI.IndexInfo(context.Background(), i.tableName) + if err != nil { + if errors.Is(err, pilosa.ErrIndexNotFound) { + return nil, sql3.NewErrInternalf("table not found '%s'", i.tableName) + } + return nil, err + } + i.rowWidth = len(table.Fields) + + i.targetColumnMap = make(map[string]int) + for idx, fld := range table.Fields { + i.targetColumnMap[fld.Name] = idx + } + + var cond *pql.Call + + cond, err = i.planner.generatePQLCallFromExpr(ctx, i.predicate) + if err != nil { + return nil, err + } + if cond == nil { + cond = &pql.Call{Name: "All"} + } + + if i.topExpr != nil { + _, ok := i.topExpr.(*intLiteralPlanExpression) + if !ok { + return nil, sql3.NewErrInternalf("unexpected top expression type: %T", i.topExpr) + } + pqlValue, err := planExprToValue(i.topExpr) + if err != nil { + return nil, err + } + cond = &pql.Call{ + Name: "Limit", + Children: []*pql.Call{cond}, + Args: map[string]interface{}{"limit": pqlValue}, + Type: pql.PrecallGlobal, + } + } + + call := &pql.Call{Name: "Extract", Children: []*pql.Call{cond}} + for _, c := range i.columns { + col, ok := c.(types.SchemaIdentifiable) + if !ok { + return nil, sql3.NewErrInternalf("unexpected column type '%T'", c) + } + + // Skip the _id field. + if col.Name() == "_id" { + continue + } + call.Children = append(call.Children, + &pql.Call{ + Name: "Rows", + Args: map[string]interface{}{"field": col.Name()}, + }, + ) + } + + queryResponse, err := i.planner.executor.Execute(ctx, i.tableName, &pql.Query{Calls: []*pql.Call{call}}, nil, nil) + if err != nil { + return nil, err + } + tbl, ok := queryResponse.Results[0].(pilosa.ExtractedTable) + if !ok { + return nil, sql3.NewErrInternalf("unexpected Extract() result type: %T", queryResponse.Results[0]) + } + i.result = tbl.Columns + i.sourceColumnMap = make(map[string]int) + for idx, fld := range tbl.Fields { + i.sourceColumnMap[fld.Name] = idx + } + } + + if len(i.result) > 0 { + row := make([]interface{}, i.rowWidth) + + for _, c := range i.columns { + result := i.result[0] + + col, ok := c.(types.SchemaIdentifiable) + if !ok { + return nil, sql3.NewErrInternalf("unexpected column type '%T'", c) + } + + targetColIdx, ok := i.targetColumnMap[col.Name()] + if !ok { + return nil, sql3.NewErrInternalf("target index not found for column named %s", col.Name()) + } + + if col.Name() == "_id" { + if result.Column.Keyed { + row[targetColIdx] = result.Column.Key + } else { + row[targetColIdx] = int64(result.Column.ID) + } + } else { + + sourceColIdx, ok := i.sourceColumnMap[col.Name()] + if !ok { + return nil, sql3.NewErrInternalf("source index not found for column named %s", col.Name()) + } + switch c.Type().(type) { + case *parser.DataTypeIDSet: + //empty sets are null + val, ok := result.Rows[sourceColIdx].([]uint64) + if !ok { + return nil, sql3.NewErrInternalf("unexpected type for column value '%T'", result.Rows[sourceColIdx]) + } + if len(val) == 0 { + row[targetColIdx] = nil + } else { + row[targetColIdx] = val + } + + case *parser.DataTypeStringSet: + //empty sets are null + val, ok := result.Rows[sourceColIdx].([]string) + if !ok { + return nil, sql3.NewErrInternalf("unexpected type for column value '%T'", result.Rows[sourceColIdx]) + } + if len(val) == 0 { + row[targetColIdx] = nil + } else { + row[targetColIdx] = val + } + + default: + row[targetColIdx] = result.Rows[sourceColIdx] + } + } + } + + // Move to next result element. + i.result = i.result[1:] + return row, nil + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/optop.go b/sql3/planner/optop.go new file mode 100644 index 000000000..06b6d010e --- /dev/null +++ b/sql3/planner/optop.go @@ -0,0 +1,71 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpTop implements the TOP operator +type PlanOpTop struct { + ChildOp types.PlanOperator + expr types.PlanExpression + warnings []string +} + +func NewPlanOpTop(expr types.PlanExpression, child types.PlanOperator) *PlanOpTop { + return &PlanOpTop{ + ChildOp: child, + expr: expr, + } +} + +func (p *PlanOpTop) Schema() types.Schema { + return p.ChildOp.Schema() +} + +func (p *PlanOpTop) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return p.ChildOp.Iterator(ctx, row) +} + +func (p *PlanOpTop) Children() []types.PlanOperator { + return []types.PlanOperator{ + p.ChildOp, + } +} + +func (p *PlanOpTop) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +func (p *PlanOpTop) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + sc := make([]string, 0) + for _, e := range p.Schema() { + sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.Name, e.Table, e.Type.TypeName())) + } + result["_schema"] = sc + + result["expr"] = p.expr + result["child"] = p.ChildOp.Plan() + return result +} + +func (p *PlanOpTop) String() string { + return "" +} + +func (p *PlanOpTop) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpTop) Warnings() []string { + var w []string + w = append(w, p.warnings...) + w = append(w, p.ChildOp.Warnings()...) + return w +} diff --git a/sql3/planner/planner.go b/sql3/planner/planner.go new file mode 100644 index 000000000..80a97bd88 --- /dev/null +++ b/sql3/planner/planner.go @@ -0,0 +1,3 @@ +// Package planner contains everything required to build a query plan from a SQL +// statement. +package planner diff --git a/sql3/planner/planoptimizer.go b/sql3/planner/planoptimizer.go new file mode 100644 index 000000000..145187b78 --- /dev/null +++ b/sql3/planner/planoptimizer.go @@ -0,0 +1,503 @@ +// Copyright 2021 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +//TODO(pok) push order by down as far as possible +//TODO(pok) handle the case of the order by expressions not being in a projection list +//TODO(pok) you can't group by _id in PQL, so we need to not use a PQL group by operator here +//TODO(pok) move constant folding to the here + +var optimizerFunctions = []OptimizerFunc{ + // if we have a group by that has one TableScanOperator, + // no Top or TopN or Distincts, try to use a PQL(multi) + // groupby operator instead + tryToReplaceGroupByWithPQLGroupBy, + + // if we have a group by with no group by exprs that has + // one TableScanOperator, no Top or TopN or Distincts, try + // to use a PQL aggregate operators instead + tryToReplaceGroupByWithPQLAggregate, + + // update the columnIdx for all the references in the projections + // based on the child operator for a projection + fixGroupByProjections, + + // update the columnIdx for all the references in the projections + // based on the child operator for a projection + fixJoinProjections, + + // if the query has one TableScanOperator then push the top + // expression down into that operator + pushdownPQLTop, +} + +type OptimizerScope struct { +} + +type OptimizerFunc func(context.Context, *ExecutionPlanner, types.PlanOperator, *OptimizerScope) (types.PlanOperator, bool, error) + +// optimizePlan takes a plan from the compiler and executes a series of transforms on it to optimize it +func (p *ExecutionPlanner) optimizePlan(ctx context.Context, plan types.PlanOperator) (types.PlanOperator, error) { + var err error + var result = plan + for _, ofunc := range optimizerFunctions { + result, err = p.optimizeNode(ctx, result, ofunc) + if err != nil { + return nil, err + } + } + return result, nil +} + +func (p *ExecutionPlanner) optimizeNode(ctx context.Context, node types.PlanOperator, ofunc OptimizerFunc) (types.PlanOperator, error) { + op, same, err := ofunc(ctx, p, node, nil) + if err != nil { + return nil, err + } + if !same { + return op, nil + } + return node, nil +} + +func tryToReplaceGroupByWithPQLAggregate(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) { + //bail if there are any joins + scans, err := hasOnlyTableScans(ctx, a, n, scope) + if err != nil { + return nil, false, err + } + if !scans { + return n, true, nil + } + //bail if there is a top + top, err := hasTop(ctx, a, n, scope) + if err != nil { + return nil, false, err + } + if top { + return n, true, nil + } + + //go find the table scan operators + tables := getTableScanOperators(ctx, a, n, scope) + + //only do this if we have one TableScanOperator + if len(tables) == 1 { + return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) { + switch n := node.(type) { + case *PlanOpGroupBy: + //only do this if there are no group by expressions + if len(n.GroupByExprs) == 0 { + + //table scan + table := tables[0] + ops := make([]*PlanOpPQLAggregate, 0) + + for _, agg := range n.Aggregates { + aggregable, ok := agg.(types.Aggregable) + if !ok { + return n, false, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", agg) + } + + ops = append(ops, NewPlanOpPQLAggregate(a, table.tableName, aggregable, table.filter)) + } + newOp := NewPlanOpPQLMultiAggregate(a, ops) + lenOps := len(ops) + if lenOps > 1 { + newOp.AddWarning(fmt.Sprintf("Multiple (%d) aggregates referenced in select list will result in multiple aggregate queries being executed.", lenOps)) + } + return newOp, false, nil + } + return n, true, nil + default: + return n, true, nil + } + }) + } + return n, true, nil +} + +func tryToReplaceGroupByWithPQLGroupBy(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) { + //bail if there are any joins + scans, err := hasOnlyTableScans(ctx, a, n, scope) + if err != nil { + return nil, false, err + } + if !scans { + return n, true, nil + } + //bail if there is a top + top, err := hasTop(ctx, a, n, scope) + if err != nil { + return nil, false, err + } + if top { + return n, true, nil + } + + //go find the table scan operators + tables := getTableScanOperators(ctx, a, n, scope) + + //only do this if we have one TableScanOperator + if len(tables) == 1 { + return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) { + switch n := node.(type) { + case *PlanOpGroupBy: + //table scan + table := tables[0] + //only do this if we have group by expressions + if len(n.GroupByExprs) > 0 { + //use a multi group by if more than 1 aggregate + if len(n.Aggregates) > 1 { + ops := make([]*PlanOpPQLGroupBy, 0) + for _, agg := range n.Aggregates { + + aggregable, ok := agg.(types.Aggregable) + if !ok { + return n, false, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", agg) + } + ops = append(ops, NewPlanOpPQLGroupBy(a, table.tableName, n.GroupByExprs, table.filter, aggregable)) + } + newOp := NewPlanOpPQLMultiGroupBy(a, ops, n.GroupByExprs) + newOp.AddWarning(fmt.Sprintf("Multiple (%d) aggregates referenced in select list will result in multiple group by aggregate queries being executed.", len(ops))) + return newOp, false, nil + } + //only one aggregate + aggregable, ok := n.Aggregates[0].(types.Aggregable) + if !ok { + return n, false, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", n.Aggregates[0]) + } + newOp := NewPlanOpPQLGroupBy(a, table.tableName, n.GroupByExprs, table.filter, aggregable) + return newOp, false, nil + } + return n, true, nil + default: + return n, true, nil + } + }) + } + return n, true, nil +} + +func pushdownPQLTop(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) { + //bail if there are any joins + hasOnlyScans, err := hasOnlyTableScans(ctx, a, n, scope) + if err != nil { + return nil, false, err + } + if !hasOnlyScans { + return n, true, nil + } + + //go find the table scan operators + tables := getTableScanOperators(ctx, a, n, scope) + + //only do this if we have one TableScanOperator + if len(tables) == 1 { + return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) { + switch n := node.(type) { + case *PlanOpTop: + table := tables[0] + //set the topExpr for the PlanOpTableScan + table.topExpr = n.expr + //return the child of the top node to eliminate it + return n.ChildOp, false, nil + default: + return n, true, nil + } + }) + } + return n, true, nil +} + +func areAggregablesEqual(lhs types.Aggregable, rhs types.Aggregable) bool { + if reflect.TypeOf(lhs) == reflect.TypeOf(rhs) { + lhsRef, lhsok := lhs.AggExpression().(*qualifiedRefPlanExpression) + rhsRef, rhsok := rhs.AggExpression().(*qualifiedRefPlanExpression) + if lhsok && rhsok { + return strings.EqualFold(lhsRef.columnName, rhsRef.columnName) + } + } + return false +} + +func fixGroupByProjections(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) { + return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) { + switch n := node.(type) { + case *PlanOpProjection: + switch childOp := n.ChildOp.(type) { + case *PlanOpGroupBy: + //PlanOpGroupBy's iterator returns group by exprs, then aggregates in the order they appear + + for idx, pj := range n.Projections { + expr, _, err := TransformExpr(pj, func(e types.PlanExpression) (types.PlanExpression, bool, error) { + switch e.(type) { + case types.Aggregable: + // if we have a Aggregable, the AggExpression() will be a qualified ref + // given we are in the context of a PlanOpProjection with a PlanOpGroupBy + // we can use the ordinal position of the projection as the column index + ae := newQualifiedRefPlanExpression("", "", idx, e.Type()) + return ae, false, nil + default: + return e, true, nil + } + }) + if err != nil { + return n, true, err + } + n.Projections[idx] = expr + } + return n, false, nil + + case *PlanOpPQLGroupBy: + // PlanOpGroupBy's iterator returns group by exprs, then the single aggregate in the order they appear + + // make a map of the names of the group by columns + groupByColumnsNameMap := make(map[string]int) + for gidx, gbe := range childOp.groupByExprs { + gbeRef, ok := gbe.(*qualifiedRefPlanExpression) + if !ok { + return nil, false, sql3.NewErrInternalf("unexpected group by expression type '%T'", gbe) + } + gbeRef.columnIndex = gidx + groupByColumnsNameMap[gbeRef.columnName] = gidx + } + //set the index for the aggregate to be the length of the group by list + aggregateIndex := len(childOp.groupByExprs) + + //loop projections: + //1. looking for the Aggregable and replace it with a qualifiedRefPlanExpression pointing to + // the offset in the child iterator + //2. looking for the qualified refs replace it with a qualifiedRefPlanExpression pointing to + // the offset in the child iterator + for idx, pj := range n.Projections { + expr, _, err := TransformExpr(pj, func(e types.PlanExpression) (types.PlanExpression, bool, error) { + switch thisExpr := e.(type) { + case types.Aggregable: + ae := newQualifiedRefPlanExpression(fmt.Sprintf("$PlanOpPQLGroupBy:%d", aggregateIndex), "", aggregateIndex, e.Type()) + return ae, false, nil + case *qualifiedRefPlanExpression: + colIdx, ok := groupByColumnsNameMap[thisExpr.columnName] + if ok { + ae := newQualifiedRefPlanExpression(fmt.Sprintf("$PlanOpPQLGroupBy.%s:%d", thisExpr.columnName, colIdx), thisExpr.columnName, colIdx, e.Type()) + return ae, false, nil + } + return e, true, nil + default: + return e, true, nil + } + }) + if err != nil { + return n, true, err + } + n.Projections[idx] = expr + } + return n, false, nil + + case *PlanOpPQLMultiAggregate: + //PlanOpGroupBy's iterator returns aggregates in the order they appear + + // make a list of the aggregables + aggregableList := make([]types.Aggregable, 0) + for _, op := range childOp.operators { + aggregableList = append(aggregableList, op.aggregate) + } + + //loop projections: + //1. looking for the Aggregable and replace it with a qualifiedRefPlanExpression pointing to + // the offset in the child iterator + for idx, pj := range n.Projections { + expr, _, err := TransformExpr(pj, func(e types.PlanExpression) (types.PlanExpression, bool, error) { + switch thisExpr := e.(type) { + case types.Aggregable: + for idx, a := range aggregableList { + if areAggregablesEqual(thisExpr, a) { + ae := newQualifiedRefPlanExpression(fmt.Sprintf("$PlanOpPQLMultiAggregate:%d", idx), "", idx, e.Type()) + return ae, false, nil + } + } + return e, true, nil + default: + return e, true, nil + } + }) + if err != nil { + return n, true, err + } + n.Projections[idx] = expr + } + return n, false, nil + + case *PlanOpPQLMultiGroupBy: + //PlanOpPQLMultiGroupBy's iterator returns group by exprs, then the aggregates in the order they appear + + // make a map of the names of the group by columns and update the indexes + groupByColumnsNameMap := make(map[string]int) + for gidx, gbe := range childOp.groupByExprs { + gbeRef, ok := gbe.(*qualifiedRefPlanExpression) + if !ok { + return nil, false, sql3.NewErrInternalf("unexpected group by expression type '%T'", gbe) + } + gbeRef.columnIndex = gidx + groupByColumnsNameMap[gbeRef.columnName] = gidx + } + //set the index for the start of the aggregates to be the length of the group by list + aggregateStartIndex := len(childOp.groupByExprs) + + // make a list of the aggregables + aggregableList := make([]types.Aggregable, 0) + for _, op := range childOp.operators { + aggregableList = append(aggregableList, op.aggregate) + } + + //loop projections: + //1. looking for the Aggregable and replace it with a qualifiedRefPlanExpression pointing to + // the offset in the child iterator + //2. looking for the qualified refs replace it with a qualifiedRefPlanExpression pointing to + // the offset in the child iterator + for idx, pj := range n.Projections { + expr, _, err := TransformExpr(pj, func(e types.PlanExpression) (types.PlanExpression, bool, error) { + switch thisExpr := e.(type) { + case types.Aggregable: + for idx, a := range aggregableList { + if areAggregablesEqual(thisExpr, a) { + ae := newQualifiedRefPlanExpression(fmt.Sprintf("$PlanOpPQLMultiGroupBy:%d", idx+aggregateStartIndex), "", idx+aggregateStartIndex, e.Type()) + return ae, false, nil + } + } + return e, true, nil + case *qualifiedRefPlanExpression: + colIdx, ok := groupByColumnsNameMap[thisExpr.columnName] + if ok { + ae := newQualifiedRefPlanExpression(fmt.Sprintf("$PlanOpPQLMultiGroupBy.%s:%d", thisExpr.columnName, colIdx), thisExpr.columnName, colIdx, e.Type()) + return ae, false, nil + } + return e, true, nil + + default: + return e, true, nil + } + }) + if err != nil { + return n, true, err + } + n.Projections[idx] = expr + } + return n, false, nil + } + return n, true, nil + default: + return n, true, nil + } + }) +} + +func fixJoinProjections(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) { + return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) { + switch n := node.(type) { + case *PlanOpProjection: + switch childOp := n.ChildOp.(type) { + case *PlanOpNestedLoops: + //PlanOpNestedLoops iterator returns columns from top iterator and then columns from bottom iterator + + //make a map of names from the schema + schemaNameMap := make(map[string]int) + schema := childOp.Schema() + for idx, s := range schema { + key := fmt.Sprintf("%s.%s", s.Table, s.Name) + schemaNameMap[key] = idx + } + + for idx, pj := range n.Projections { + expr, _, err := TransformExpr(pj, func(e types.PlanExpression) (types.PlanExpression, bool, error) { + switch thisExpr := e.(type) { + case *qualifiedRefPlanExpression: + key := fmt.Sprintf("%s.%s", thisExpr.tableName, thisExpr.columnName) + colIdx, ok := schemaNameMap[key] + if ok { + ae := newQualifiedRefPlanExpression(fmt.Sprintf("$PlanOpNestedLoops.%s.%s:%d", thisExpr.tableName, thisExpr.columnName, colIdx), thisExpr.columnName, colIdx, e.Type()) + return ae, false, nil + } + return e, true, nil + + default: + return e, true, nil + } + }) + if err != nil { + return n, true, err + } + n.Projections[idx] = expr + } + return n, false, nil + } + return n, true, nil + default: + return n, true, nil + } + }) +} + +// hasTop inspects a plan op tree and returns true (or error) if there are Top +// operators. +func hasTop(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (bool, error) { + result := false + InspectPlan(n, func(node types.PlanOperator) bool { + switch node.(type) { + case *PlanOpTop: + result = true + return false + } + return true + }) + return result, nil +} + +// hasTopN inspects a plan op tree and returns true (or error) if there are TopN +// operators. +func hasTopN(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (bool, error) { + //TODO(pok) implement this + return false, nil +} + +// inspects a plan op tree and returns false (or error) if there are read operators other +// than table scans +func hasOnlyTableScans(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (bool, error) { + //assume true + result := true + InspectPlan(n, func(node types.PlanOperator) bool { + // if we find a nested loops, nope to only table scans + switch node.(type) { + case *PlanOpNestedLoops: + result = false + return false + } + return true + }) + return result, nil +} + +// inspects a plan op tree and returns a list (or error) of all the PlanOpTableScan operators +func getTableScanOperators(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) []*PlanOpPQLTableScan { + var tables []*PlanOpPQLTableScan + //go find the table scan operators + InspectPlan(n, func(node types.PlanOperator) bool { + switch nd := node.(type) { + case *PlanOpPQLTableScan: + tables = append(tables, nd) + return false + } + return true + }) + return tables +} diff --git a/sql3/planner/planwalker.go b/sql3/planner/planwalker.go new file mode 100644 index 000000000..49052d189 --- /dev/null +++ b/sql3/planner/planwalker.go @@ -0,0 +1,385 @@ +package planner + +import ( + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanVisitor visits nodes in the plan. +type PlanVisitor interface { + // VisitOperator method is invoked for each node during PlanWalk. If the + // resulting PlanVisitor is not nil, PlanWalk visits each of the children of + // the node with that visitor, followed by a call of VisitOperator(nil) to + // the returned visitor. + VisitOperator(op types.PlanOperator) PlanVisitor +} + +// PlanWalk traverses the plan depth-first. It starts by calling +// v.VisitOperator; node must not be nil. If the result returned by +// v.VisitOperator is not nil, PlanWalk is invoked recursively with the returned +// result for each of the children of the plan operator, followed by a call of +// v.VisitOperator(nil) to the returned result. If v.VisitOperator(op) returns +// non-nil, then all children are walked, even if one of them returns nil. +func PlanWalk(v PlanVisitor, op types.PlanOperator) { + if v = v.VisitOperator(op); v == nil { + return + } + + for _, child := range op.Children() { + PlanWalk(v, child) + } + + v.VisitOperator(nil) +} + +type planInspector func(types.PlanOperator) bool + +func (f planInspector) VisitOperator(op types.PlanOperator) PlanVisitor { + if f(op) { + return f + } + return nil +} + +// InspectPlan traverses the plan op graph depth-first order +// if f(op) returns true, InspectPlan invokes f recursively for each of the children of op, +// followed by a call of f(nil). +func InspectPlan(op types.PlanOperator, f planInspector) { + PlanWalk(f, op) +} + +//----------------------------------------------------------------------------- + +// ExprVisitor visits expressions in an expression tree. +type ExprVisitor interface { + // VisitExpr method is invoked for each expr encountered by ExprWalk. + // If the result is not nil, ExprWalk visits each of the children + // of the expr, followed by a call of VisitExpr(nil) to the returned result. + VisitExpr(expr types.PlanExpression) ExprVisitor +} + +func ExprWalk(v ExprVisitor, expr types.PlanExpression) { + if v = v.VisitExpr(expr); v == nil { + return + } + + for _, child := range expr.Children() { + ExprWalk(v, child) + } + + v.VisitExpr(nil) +} + +type exprInspector func(types.PlanExpression) bool + +func (f exprInspector) VisitExpr(e types.PlanExpression) ExprVisitor { + if f(e) { + return f + } + return nil +} + +// WalkExpressions traverses the plan and calls sql.Walk on any expression it finds. +func WalkExpressions(v ExprVisitor, node types.PlanOperator) { + InspectPlan(node, func(node types.PlanOperator) bool { + if n, ok := node.(types.ContainsExpressions); ok { + for _, e := range n.Expressions() { + ExprWalk(v, e) + } + } + return true + }) +} + +// InspectExpressions traverses the plan and calls WalkExpressions on any +// expression it finds. +func InspectExpressions(node types.PlanOperator, f exprInspector) { + WalkExpressions(f, node) +} + +//----------------------------------------------------------------------------- + +// PlanOpExprVisitor visits expressions in an expression tree. Like ExprVisitor, but with the added context of the plan op in which +// an expression is embedded. +type PlanOpExprVisitor interface { + // VisitPlanOpExpr method is invoked for each expr encountered by Walk. If the result Visitor is not nil, Walk visits each of + // the children of the expr with that visitor, followed by a call of VisitPlanOpExpr(nil, nil) to the returned visitor. + VisitPlanOpExpr(node types.PlanOperator, expression types.PlanExpression) PlanOpExprVisitor +} + +// ExprWithPlanOpWalk traverses the expression tree in depth-first order. It starts by calling v.VisitPlanOpExpr(op, expr); expr must +// not be nil. If the visitor returned by v.VisitPlanOpExpr(op, expr) is not nil, Walk is invoked recursively with the returned +// visitor for each children of the expr, followed by a call of v.VisitPlanOpExpr(nil, nil) to the returned visitor. +func ExprWithPlanOpWalk(v PlanOpExprVisitor, n types.PlanOperator, expr types.PlanExpression) { + if v = v.VisitPlanOpExpr(n, expr); v == nil { + return + } + + for _, child := range expr.Children() { + ExprWithPlanOpWalk(v, n, child) + } + + v.VisitPlanOpExpr(nil, nil) +} + +type exprWithNodeInspector func(types.PlanOperator, types.PlanExpression) bool + +func (f exprWithNodeInspector) VisitPlanOpExpr(n types.PlanOperator, e types.PlanExpression) PlanOpExprVisitor { + if f(n, e) { + return f + } + return nil +} + +// WalkExpressionsWithPlanOp traverses the plan and calls ExprWithPlanOpWalk on any expression it finds. +func WalkExpressionsWithPlanOp(v PlanOpExprVisitor, n types.PlanOperator) { + InspectPlan(n, func(n types.PlanOperator) bool { + if expressioner, ok := n.(types.ContainsExpressions); ok { + for _, e := range expressioner.Expressions() { + ExprWithPlanOpWalk(v, n, e) + } + } + return true + }) +} + +// InspectExpressionsWithPlanOp traverses the plan and calls f on any expression it finds. +func InspectExpressionsWithPlanOp(node types.PlanOperator, f exprWithNodeInspector) { + WalkExpressionsWithPlanOp(f, node) +} + +// PlanOpFunc is a function that given a plan op will return either a transformed plan op or the original plan op. +// If there was a transformation, the bool will be true, and an error if there was an error +type PlanOpFunc func(n types.PlanOperator) (types.PlanOperator, bool, error) + +// TransformPlanOp applies a transformation function to the given plan op graph +func TransformPlanOp(op types.PlanOperator, f PlanOpFunc) (types.PlanOperator, bool, error) { + + children := op.Children() + if len(children) == 0 { + return f(op) + } + + var ( + newChildren []types.PlanOperator + ) + + for i := range children { + child := children[i] + child, same, err := TransformPlanOp(child, f) + if err != nil { + return nil, true, err + } + if !same { + if newChildren == nil { + newChildren = make([]types.PlanOperator, len(children)) + copy(newChildren, children) + } + newChildren[i] = child + } + } + + var err error + sameC := true + if len(newChildren) > 0 { + sameC = false + op, err = op.WithChildren(newChildren...) + if err != nil { + return nil, true, err + } + } + + op, sameN, err := f(op) + if err != nil { + return nil, true, err + } + return op, sameC && sameN, nil +} + +// ExprWithPlanOpFunc is a function that given an expression and the node +// that contains it, will return that expression as is or transformed +// along with an error, if any. +type ExprWithPlanOpFunc func(types.PlanOperator, types.PlanExpression) (types.PlanExpression, bool, error) + +// ExprFunc is a function that given an expression will return that +// expression as is or transformed, or bool to indicate +// whether the expression was modified, and an error or nil. +type ExprFunc func(e types.PlanExpression) (types.PlanExpression, bool, error) + +// TransformPlanOpExprsWithPlanOp applies a transformation function to all expressions on the given plan operator from the bottom up in the context of the plan operator +func TransformPlanOpExprsWithPlanOp(op types.PlanOperator, f ExprWithPlanOpFunc) (types.PlanOperator, bool, error) { + return TransformPlanOp(op, func(n types.PlanOperator) (types.PlanOperator, bool, error) { + return SinglePlanOpExprsWithPlanOp(n, f) + }) +} + +// TransformPlanOpExprs applies a transformation function to all expressions on the given plan operator from the bottom up +func TransformPlanOpExprs(op types.PlanOperator, f ExprFunc) (types.PlanOperator, bool, error) { + return TransformPlanOpExprsWithPlanOp(op, func(n types.PlanOperator, e types.PlanExpression) (types.PlanExpression, bool, error) { + return f(e) + }) +} + +// SinglePlanOpExprsWithPlanOp applies a transformation function to all expressions on a given plan operator in the context of that plan operator +func SinglePlanOpExprsWithPlanOp(n types.PlanOperator, f ExprWithPlanOpFunc) (types.PlanOperator, bool, error) { + ne, ok := n.(types.ContainsExpressions) + if !ok { + return n, true, nil + } + + exprs := ne.Expressions() + if len(exprs) == 0 { + return n, true, nil + } + + var ( + newExprs []types.PlanExpression + err error + ) + + for i := range exprs { + e := exprs[i] + e, same, err := TransformExprWithPlanOp(n, e, f) + if err != nil { + return nil, true, err + } + if !same { + if newExprs == nil { + newExprs = make([]types.PlanExpression, len(exprs)) + copy(newExprs, exprs) + } + newExprs[i] = e + } + } + + if len(newExprs) > 0 { + n, err = ne.WithExpressions(newExprs...) + if err != nil { + return nil, true, err + } + return n, false, nil + } + return n, true, nil +} + +// TransformSinglePlanOpExpressions applies a transformation function to all expressions on the given plan operator +func TransformSinglePlanOpExpressions(o types.PlanOperator, f ExprFunc) (types.PlanOperator, bool, error) { + e, ok := o.(types.ContainsExpressions) + if !ok { + return o, true, nil + } + + exprs := e.Expressions() + if len(exprs) == 0 { + return o, true, nil + } + + var newExprs []types.PlanExpression + for i := range exprs { + expr := exprs[i] + expr, same, err := TransformExpr(expr, f) + if err != nil { + return nil, true, err + } + if !same { + if newExprs == nil { + newExprs = make([]types.PlanExpression, len(exprs)) + copy(newExprs, exprs) + } + newExprs[i] = expr + } + } + if len(newExprs) > 0 { + n, err := e.WithExpressions(newExprs...) + if err != nil { + return nil, true, err + } + return n, false, nil + } + return o, true, nil +} + +// TransformExpr applies a transformation function to an expression +func TransformExpr(e types.PlanExpression, f ExprFunc) (types.PlanExpression, bool, error) { + children := e.Children() + if len(children) == 0 { + return f(e) + } + + var ( + newChildren []types.PlanExpression + err error + ) + + for i := 0; i < len(children); i++ { + c := children[i] + c, same, err := TransformExpr(c, f) + if err != nil { + return nil, true, err + } + if !same { + if newChildren == nil { + newChildren = make([]types.PlanExpression, len(children)) + copy(newChildren, children) + } + newChildren[i] = c + } + } + + sameC := true + if len(newChildren) > 0 { + sameC = false + e, err = e.WithChildren(newChildren...) + if err != nil { + return nil, true, err + } + } + + e, sameN, err := f(e) + if err != nil { + return nil, true, err + } + return e, sameC && sameN, nil +} + +// TransformExprWithPlanOp applies a transformation function to an expression in the context of a plan operator +func TransformExprWithPlanOp(n types.PlanOperator, e types.PlanExpression, f ExprWithPlanOpFunc) (types.PlanExpression, bool, error) { + children := e.Children() + if len(children) == 0 { + return f(n, e) + } + + var ( + newChildren []types.PlanExpression + err error + ) + + for i := 0; i < len(children); i++ { + c := children[i] + c, sameC, err := TransformExprWithPlanOp(n, c, f) + if err != nil { + return nil, true, err + } + if !sameC { + if newChildren == nil { + newChildren = make([]types.PlanExpression, len(children)) + copy(newChildren, children) + } + newChildren[i] = c + } + } + + sameC := true + if len(newChildren) > 0 { + sameC = false + e, err = e.WithChildren(newChildren...) + if err != nil { + return nil, true, err + } + } + + e, sameN, err := f(n, e) + if err != nil { + return nil, true, err + } + return e, sameC && sameN, nil +} diff --git a/sql3/planner/types/operator.go b/sql3/planner/types/operator.go new file mode 100644 index 000000000..9b491d9a5 --- /dev/null +++ b/sql3/planner/types/operator.go @@ -0,0 +1,88 @@ +package types + +import ( + "context" + "errors" + "fmt" + + "github.com/molecula/featurebase/v3/sql3/parser" +) + +// PlanOperator is a node in an execution plan. +type PlanOperator interface { + fmt.Stringer + + // Children of this operator. + Children() []PlanOperator + + // Schema of this operator. + Schema() Schema + + // Iterator produces an iterator from this node. The current row being + // evaluated is provided, as well as the context of the query. + Iterator(ctx context.Context, row Row) (RowIterator, error) + + // WithChildren creates a new node with the children passed + WithChildren(children ...PlanOperator) (PlanOperator, error) + + // Plan returns a map containing a rich description of this operator; + // intended to be marshalled into json. + Plan() map[string]interface{} + + // AddWarning adds a warning to the plan. + AddWarning(warning string) + + // Warnings returns a list of warnings as strings. + Warnings() []string +} + +// ContainsExpressions exposes expressions in plan operators +type ContainsExpressions interface { + // returns the list of expressions contained by the plan operator + Expressions() []PlanExpression + + // WithExpressions returns a new operator with expressions replaced + WithExpressions(...PlanExpression) (PlanOperator, error) +} + +// SchemaObject exposes an ObjectName() for operators that iterate on schema objects +type SchemaObject interface { + ObjectName() string +} + +// PlannerColumn is the definition of a column returned as a set from each operator +type PlannerColumn struct { + Name string + Table string + Type parser.ExprDataType +} + +// Schema is the definition a set of columns from each operator +type Schema []*PlannerColumn + +// Row is a tuple of values +type Row []interface{} + +// Append appends all the values in r2 to this row and returns the result +func (r Row) Append(r2 Row) Row { + row := make(Row, len(r)+len(r2)) + for i := range r { + row[i] = r[i] + } + for i := range r2 { + row[i+len(r)] = r2[i] + } + return row +} + +// ErrNoMoreRows is a 'special' error returned to signify no more rows +var ErrNoMoreRows = errors.New("ErrNoMoreRows") + +// RowIterator is an iterator that produces rows (or an error) +type RowIterator interface { + Next(ctx context.Context) (Row, error) +} + +type RowIterable interface { + Iterator(ctx context.Context, row Row) (RowIterator, error) +} diff --git a/sql3/planner/types/planexpression.go b/sql3/planner/types/planexpression.go new file mode 100644 index 000000000..6cfc0b871 --- /dev/null +++ b/sql3/planner/types/planexpression.go @@ -0,0 +1,62 @@ +package types + +import ( + "context" + + "github.com/molecula/featurebase/v3/sql3/parser" +) + +//TODO(pok) we can get rid of this - we have expression types for all of these now... +type AggregateFunctionType int + +// The list of AggregateFunction. +const ( + // Special tokens + AGGREGATE_ILLEGAL AggregateFunctionType = iota + AGGREGATE_COUNT + AGGREGATE_COUNT_DISTINCT + AGGREGATE_SUM + AGGREGATE_AVG + AGGREGATE_PERCENTILE + AGGREGATE_MIN + AGGREGATE_MAX +) + +// PlanExpression is an expression node for an execution plan +type PlanExpression interface { + // evaluates expression based on current row + Evaluate(currentRow []interface{}) (interface{}, error) + + // returns the type of the expression + Type() parser.ExprDataType + + // returns the child expressions for this expression + Children() []PlanExpression + + // creates a new expression node with the children replaced + WithChildren(children ...PlanExpression) (PlanExpression, error) + + // returns a map containing a rich description of this expression; intended to be + // marshalled into json + Plan() map[string]interface{} +} + +// Aggregattion buffer is an interface to something that maintains an aggregate during query +// execution +type AggregationBuffer interface { + Eval(ctx context.Context) (interface{}, error) + Update(ctx context.Context, row Row) error +} + +// Interface to an expression that is a an aggregate +type Aggregable interface { + NewBuffer() (AggregationBuffer, error) + AggType() AggregateFunctionType + AggExpression() PlanExpression + AggAdditionalExpr() []PlanExpression +} + +// Interface to an expression that is a reference to a schema object +type SchemaIdentifiable interface { + Name() string +} diff --git a/sql3/sql3.ebnf b/sql3/sql3.ebnf new file mode 100644 index 000000000..802e2f1ae --- /dev/null +++ b/sql3/sql3.ebnf @@ -0,0 +1,264 @@ +(* + + sql3 + ========================== + + Document the SQL language support in FeatureBase + +*) + +sql3 = statement, [ ";" ] ; + +statement = show_tables + | show_columns + | drop_table + | create_table_stmt + | alter_table_stmt + | select_stmt + | insert_stmt + | delete_stmt ; + +(* + + SHOW TABLES + ---------------- + Shows the tables within a FeatureBase instance. + +*) +show_tables = "SHOW", "TABLES" ; + +(* + + SHOW COLUMNS + ---------------- + Shows the columns on a FeatureBase table. + +*) +show_columns = "SHOW", "COLUMNS", "FROM", identifier ; + +(* + + DROP TABLE + ---------------- + Drops a FeatureBase table. + +*) +drop_table = "DROP", "TABLE", [ "IF", "EXISTS" ], identifier ; + + +(* + + CREATE TABLE + ---------------- + Creates a FeatureBase table. + +*) +create_table_stmt = "CREATE", "TABLE", [ "IF", "NOT", "EXISTS" ], identifier, "(", column_def, { ",", column_def }, ")", { table_options } ; + +column_def = identifier, type_name, { column_constraint } ; + +(* + + Column Constraint + ---------------- + Constraints for a column: + + MIN, MAX - min/max for int types + + TIMEUNIT - 's', 'ms' etc. + + TIMEQUANTUM - 'YMD' etc. + +*) +column_constraint = + "MIN", integer_literal + | "MAX", integer_literal + | "TIMEUNIT", string_literal, [ "EPOCH", date_literal ] + | "TIMEQUANTUM", string_literal, [ "TTL", string_literal ] + | "CACHETYPE", ( "RANKED" | "LRU" ), [ "SIZE", integer_literal ] ; + +table_options = "KEYPARTITIONS", integer_literal + | "SHARDWIDTH", integer_literal ; + +type_name = "INT" + | "BOOL" + | "TIMESTAMP" + | "DECIMAL" + | "STRING" + | "STRINGSET" + | "ID" + | "IDSET" ; + +(* + + ALTER TABLE + ---------------- + Alters a FeatureBase table. + +*) +alter_table_stmt = "ALTER", "TABLE", ( add_column | drop_column | rename_column ) ; + +add_column = "ADD", [ "COLUMN" ], column_def ; + +drop_column = "DROP", [ "COLUMN" ], identifier ; + +rename_column = "RENAME", [ "COLUMN" ], identifier, "TO", identifier ; + +(* + + INSERT + ---------------- + Inserts data into a FeatureBase table. + +*) +insert_stmt = "INSERT", "INTO", identifier, "(", identifier, { identifier, "," }, ")", "VALUES", "(", expr, { expr, "," }, ")" ; + +(* + + DELETE + ---------------- + Deletes data from a FeatureBase table. + +*) +delete_stmt = "DELETE" ; + +(* + + SELECT + ---------------- + Queries a FeatureBase table. + +*) +select_stmt = "SELECT", [ top_clause ], [ "DISTINCT" ], result_column, { ",", result_column }, [ from_clause ], [ where_clause ], [group_by_clause] ; + +top_clause = ( "TOP" | "TOPN" ), "(", expr, ")" ; + +result_column = expr, [ [ "AS" ], column_alias ] + | "*" + | identifier, ".", "*" ; + +column_alias = identifier ; + +from_clause = "FROM", table_or_subquery, { ",", table_or_subquery } ; + +table_or_subquery = identifier, [ [ "AS" ], table_alias ], [ table_option ] + | "(", select_stmt, ")", [ [ "AS" ], table_alias ] ; + +table_alias = identifier ; + +table_option = "SHARDS", "(", integer_literal, { ",", integer_literal }, ")"; + +where_clause = "WHERE", expr ; + +group_by_clause = "GROUP", "BY", expr, { ",", expr }, [ "HAVING", expr ] ; + +(* + + Expressions + ---------------- + + timequantums - modelling + + * let stringsetcolq refer to a set with a timequantum column + * the expression stringsetcolq is an array/table? of tuple of (timestamp, set) + * when you refer to stringsetcolq by name, this is shorthand for + "the set value for the latest timestamp" + * if we were to use dotted notation to refer to the components of the tuple, + we could refer to the components of the tuple e.g. + - stringsetcolq.timestamp + - stringsetcolq.value + * but stringsetcolq is an array of tuples, so assuming some array type notation + - stringsetcolq[subscript].timestamp + - stringsetcolq[subscript].value + * this is dumb + * what if, when you refer to stringsetcolq by name, this is shorthand for + "the set value for the latest timestamp" - the tuple not the array + * the underlying 'table' could be modelled as (_id, timestamp, set) + * if you want access to that table, we could use a table valued function that would enable us to use + cross apply etc. + + + * you want to do range queries on the timestamp + * you want to be able to see what timestamps you have + * you want to be able to see what sets and set values you have + * do we ever want to be able to do this with any arbitary range queryable type, right + now we have one additional dimension, would we ever want more? would we want additional values? + +*) +expr = integer_literal + | string_literal + | decimal_literal + | set_literal + | date_literal + | [ table_name, "." ], column_name + | unary_op, expr + | expr, binary_op, expr + | function_call + | "(", expr, ")" + | "CAST", "(", expr, "AS", type_name, ")" + | expr, [ "NOT" ], "LIKE", expr + | expr, "IS", [ "NOT" ], "NULL" + | expr, [ "NOT" ], "BETWEEN", expr, "AND", expr + | expr, [ "NOT" ], "IN", "(", ( select_stmt | expr, { ",", expr } ), ")" + | paren_select_stmt + | case_expr ; + +paren_select_stmt = "(", select_stmt, ")" ; + +case_expr = "CASE", [ expr ], { "WHEN", expr, "THEN", expr }, [ "ELSE", expr ], "END" ; + +unary_op = "-" + | "+" + | "!" ; + +binary_op = "=" + | "!=" + | "<=" + | ">=" + | "&" + | "|" + | "<<" + | ">>" + | "+" + | "-" + | "*" + | "/" + | "%" + | "||" ; + +set_literal = "[", expr, { ",", expr }, "]" ; + +date_literal = rfc_3339 + | "CURRENT_DATE" + | "CURRENT_TIMESTAMP" ; + +table_name = identifier ; + +column_name = identifier ; + +function_call = agg_function + | non_agg_function ; + +agg_function = ( "AVG" | "COUNT" | "MAX" | "MIN" | "SUM" ), "(", ( ( [ "DISTINCT" ], expr ) | "*" ), ")" + | "PERCENTILE", "(", expr, ",", expr, ")" ; + +non_agg_function = + "SETCONTAINS" , "(", expr, ",", expr, ")" + | "SETCONTAINSALL" , "(", expr, ",", expr, ")" + | "SETCONTAINSANY" , "(", expr, ",", expr, ")" + | "DATEPART" , "(", expr, ",", expr, ")" ; + +identifier = letter , { letter | digit | "_" } ; + +letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" + | "H" | "I" | "J" | "K" | "L" | "M" | "N" + | "O" | "P" | "Q" | "R" | "S" | "T" | "U" + | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" + | "c" | "d" | "e" | "f" | "g" | "h" | "i" + | "j" | "k" | "l" | "m" | "n" | "o" | "p" + | "q" | "r" | "s" | "t" | "u" | "v" | "w" + | "x" | "y" | "z" ; + +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; + + diff --git a/sql3/sql_definitions_test.go b/sql3/sql_definitions_test.go new file mode 100644 index 000000000..a2a18d828 --- /dev/null +++ b/sql3/sql_definitions_test.go @@ -0,0 +1,517 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package sql3_test + +import ( + "time" + + "github.com/molecula/featurebase/v3/pql" +) + +// tableTests is the list of tests which get run by TestSQL_Execute in +// sql_test.go. They're defined here just to keep the test definitions separate +// from the test execution logic. +var tableTests []tableTest = []tableTest{ + { + name: "minmaxnegatives", + table: tbl( + "minmaxnegatives", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("positive_int", fldTypeInt, "min 10", "max 100"), + srcHdr("negative_int", fldTypeInt, "min -100", "max -10"), + ), + srcRows( + srcRow(int64(1), int64(11), int64(-11)), + srcRow(int64(2), int64(22), int64(-22)), + srcRow(int64(3), int64(33), int64(-33)), + ), + ), + sqlTests: []sqlTest{}, + }, + { + name: "unkeyed", + table: tbl( + "unkeyed", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("an_int", fldTypeInt, "min 0", "max 100"), + srcHdr("an_id_set", fldTypeIDSet), + srcHdr("an_id", fldTypeID), + srcHdr("a_string", fldTypeString), + srcHdr("a_string_set", fldTypeStringSet), + srcHdr("a_decimal", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}, float64(123.45)), + srcRow(int64(2), int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}, float64(234.56)), + srcRow(int64(3), int64(33), []int64{31, 32, 33}, int64(301), "str3", []string{"a3", "b3", "c3"}, float64(345.67)), + srcRow(int64(4), int64(44), []int64{41, 42, 43}, int64(401), "str4", []string{"a4", "b4", "c4"}, float64(456.78)), + ), + ), + sqlTests: []sqlTest{ + { + // Select all. + name: "select-all", + sqls: sqls( + "select * from unkeyed", + "select _id, an_int, an_id_set, an_id, a_string, a_string_set, a_decimal from unkeyed", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("an_int", fldTypeInt), + hdr("an_id_set", fldTypeIDSet), + hdr("an_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_string_set", fldTypeStringSet), + hdr("a_decimal", fldTypeDecimal2), + ), + expRows: rows( + row(int64(1), int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}, pql.NewDecimal(12345, 2)), + row(int64(2), int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}, pql.NewDecimal(23456, 2)), + row(int64(3), int64(33), []int64{31, 32, 33}, int64(301), "str3", []string{"a3", "b3", "c3"}, pql.NewDecimal(34567, 2)), + row(int64(4), int64(44), []int64{41, 42, 43}, int64(401), "str4", []string{"a4", "b4", "c4"}, pql.NewDecimal(45678, 2)), + ), + compare: compareExactUnordered, + }, + { + // Select all with top. + name: "select-all-with-top", + sqls: sqls( + "select top(2) * from unkeyed", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("an_int", fldTypeInt), + hdr("an_id_set", fldTypeIDSet), + hdr("an_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_string_set", fldTypeStringSet), + hdr("a_decimal", fldTypeDecimal2), + ), + expRows: rows( + row(int64(1), int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}, pql.NewDecimal(12345, 2)), + row(int64(2), int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}, pql.NewDecimal(23456, 2)), + ), + compare: compareExactUnordered, + }, + { + // Select all with where on each field. + name: "select-all-with-where", + sqls: sqls( + "select * from unkeyed where an_int = 22", + "select * from unkeyed where a_string = 'str2'", + "select * from unkeyed where an_id = 201", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("an_int", fldTypeInt), + hdr("an_id_set", fldTypeIDSet), + hdr("an_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_string_set", fldTypeStringSet), + hdr("a_decimal", fldTypeDecimal2), + ), + expRows: rows( + row(int64(2), int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}, pql.NewDecimal(23456, 2)), + ), + compare: compareExactOrdered, + }, + }, + }, + { + table: tbl( + "keyed", + srcHdrs( + srcHdr("_id", fldTypeString), + srcHdr("an_int", fldTypeInt, "min 0", "max 100"), + srcHdr("an_id_set", fldTypeIDSet), + srcHdr("an_id", fldTypeID), + srcHdr("a_string", fldTypeString), + srcHdr("a_string_set", fldTypeStringSet), + ), + srcRows( + srcRow("one", int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}), + srcRow("two", int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}), + srcRow("three", int64(33), []int64{31, 32, 33}, int64(301), "str3", []string{"a3", "b3", "c3"}), + srcRow("four", int64(44), []int64{41, 42, 43}, int64(401), "str4", []string{"a4", "b4", "c4"}), + ), + ), + sqlTests: []sqlTest{ + { + // Select all. + name: "select-all", + sqls: sqls( + "select * from keyed", + "select _id, an_int, an_id_set, an_id, a_string, a_string_set from keyed", + ), + expHdrs: hdrs( + hdr("_id", fldTypeString), + hdr("an_int", fldTypeInt), + hdr("an_id_set", fldTypeIDSet), + hdr("an_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_string_set", fldTypeStringSet), + ), + expRows: rows( + row("one", int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}), + row("two", int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}), + row("three", int64(33), []int64{31, 32, 33}, int64(301), "str3", []string{"a3", "b3", "c3"}), + row("four", int64(44), []int64{41, 42, 43}, int64(401), "str4", []string{"a4", "b4", "c4"}), + ), + compare: compareExactUnordered, + }, + { + // Select all with top. + name: "select-all-with-top", + sqls: sqls( + "select top(2) * from keyed", + ), + expHdrs: hdrs( + hdr("_id", fldTypeString), + hdr("an_int", fldTypeInt), + hdr("an_id_set", fldTypeIDSet), + hdr("an_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_string_set", fldTypeStringSet), + ), + expRows: rows( + row("one", int64(11), []int64{11, 12, 13}, int64(101), "str1", []string{"a1", "b1", "c1"}), + row("two", int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}), + row("three", int64(33), []int64{31, 32, 33}, int64(301), "str3", []string{"a3", "b3", "c3"}), + row("four", int64(44), []int64{41, 42, 43}, int64(401), "str4", []string{"a4", "b4", "c4"}), + ), + compare: compareIncludedIn, + expRowCount: 2, + }, + { + // Select all with where on int field. + name: "select-all-with-where", + sqls: sqls( + "select * from keyed where an_int = 22", + "select * from keyed where a_string = 'str2'", + "select * from keyed where an_id = 201", + ), + expHdrs: hdrs( + hdr("_id", fldTypeString), + hdr("an_int", fldTypeInt), + hdr("an_id_set", fldTypeIDSet), + hdr("an_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_string_set", fldTypeStringSet), + ), + expRows: rows( + row("two", int64(22), []int64{21, 22, 23}, int64(201), "str2", []string{"a2", "b2", "c2"}), + ), + compare: compareExactUnordered, + }, + }, + }, + + setLiteralTests, + setFunctionTests, + setParameterTests, + datePartTests, + + insertTest, + keyedInsertTest, + timestampLiterals, + unaryOpExprWithInt, + unaryOpExprWithID, + unaryOpExprWithBool, + unaryOpExprWithDecimal, + unaryOpExprWithTimestamp, + unaryOpExprWithIDSet, + unaryOpExprWithString, + unaryOpExprWithStringSet, + + binOpExprWithIntInt, + binOpExprWithIntBool, + binOpExprWithIntID, + binOpExprWithIntDecimal, + binOpExprWithIntTimestamp, + binOpExprWithIntIDSet, + binOpExprWithIntString, + binOpExprWithIntStringSet, + + binOpExprWithBoolInt, + binOpExprWithBoolBool, + binOpExprWithBoolID, + binOpExprWithBoolDecimal, + binOpExprWithBoolTimestamp, + binOpExprWithBoolIDSet, + binOpExprWithBoolString, + binOpExprWithBoolStringSet, + + binOpExprWithIDInt, + binOpExprWithIDBool, + binOpExprWithIDID, + binOpExprWithIDDecimal, + binOpExprWithIDTimestamp, + binOpExprWithIDIDSet, + binOpExprWithIDString, + binOpExprWithIDStringSet, + + binOpExprWithDecInt, + binOpExprWithDecBool, + binOpExprWithDecID, + binOpExprWithDecDecimal, + binOpExprWithDecTimestamp, + binOpExprWithDecIDSet, + binOpExprWithDecString, + binOpExprWithDecStringSet, + + binOpExprWithTSInt, + binOpExprWithTSBool, + binOpExprWithTSID, + binOpExprWithTSDecimal, + binOpExprWithTSTimestamp, + binOpExprWithTSIDSet, + binOpExprWithTSString, + binOpExprWithTSStringSet, + + binOpExprWithIDSetInt, + binOpExprWithIDSetBool, + binOpExprWithIDSetID, + binOpExprWithIDSetDecimal, + binOpExprWithIDSetTimestamp, + binOpExprWithIDSetIDSet, + binOpExprWithIDSetString, + binOpExprWithIDSetStringSet, + + binOpExprWithStringInt, + binOpExprWithStringBool, + binOpExprWithStringID, + binOpExprWithStringDecimal, + binOpExprWithStringTimestamp, + binOpExprWithStringIDSet, + binOpExprWithStringString, + binOpExprWithStringStringSet, + + binOpExprWithStringSetInt, + binOpExprWithStringSetBool, + binOpExprWithStringSetID, + binOpExprWithStringSetDecimal, + binOpExprWithStringSetTimestamp, + binOpExprWithStringSetIDSet, + binOpExprWithStringSetString, + binOpExprWithStringSetStringSet, + + //cast tests + castIntLiteral, + castInt, + + castBool, + castDecimal, + castID, + castIDSet, + castString, + castStringSet, + castTimestamp, + + //like tests + likeTests, + notLikeTests, + + //null tests + nullTests, + notNullTests, + + //between tests + betweenTests, + notBetweenTests, + + //in tests + inTests, + notInTests, + + //aggregate tests + countTests, + countDistinctTests, + sumTests, + avgTests, + percentileTests, + minmaxTests, + + //groupby tests + groupByTests, + + //create table tests + createTable, + + //time quantums + // Skip for now - timeQuantumInsertTest, +} + +var insertTest = tableTest{ + table: tbl( + "testinsert", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("s", fldTypeString), + srcHdr("bl", fldTypeBool), + srcHdr("d", fldTypeDecimal2), + srcHdr("event", fldTypeStringSet), + srcHdr("ievent", fldTypeIDSet), + ), + nil, + ), + sqlTests: []sqlTest{ + { + // Insert + sqls: sqls( + "insert into testinsert (_id, a, b, s, bl, d, event, ievent) values (4, 40, 400, 'foo', false, 10.12, ['A', 'B', 'C'], [1, 2, 3])", + ), + expHdrs: hdrs(), + expRows: rows(), + compare: compareExactUnordered, + }, + { + // Insert with nulls + sqls: sqls( + "insert into testinsert (_id, a, b, s, bl, d, event, ievent) values (5, null, null, null, null, null, null, null)", + "insert into testinsert (_id, a, b, s, bl, d, event, ievent) values (6, 1, null, null, null, null, null, null)", + ), + expHdrs: hdrs(), + expRows: rows(), + compare: compareExactUnordered, + }, + { + // InsertBadTable + sqls: sqls( + "insert into ifoo (a, b) values (1, 2)", + ), + expErr: "table 'ifoo' not found", + }, + { + // InsertBadColumn + sqls: sqls( + "insert into testinsert (c, b) values (1, 2)", + ), + expErr: "column 'c' not found", + }, + { + // InsertDupeColumn + sqls: sqls( + "insert into testinsert (a, a, b) values (1, 2)", + ), + expErr: "duplicate column 'a'", + }, + { + // InsertMismatchColumnValues + sqls: sqls( + "insert into testinsert (_id, a, b) values (1)", + ), + expErr: "mismatch in the count of expressions and target columns", + }, + { + // InsertHandleMissingColumns + sqls: sqls( + "insert into testinsert values (4, 40, 400)", + ), + expErr: "mismatch in the count of expressions and target columns", + }, + { + // InsertHandleMissingId + sqls: sqls( + "insert into testinsert (a, b) values (1, 2)", + ), + expErr: "insert column list must have '_id' column specified", + }, + { + // InsertHandleMissingIdPlusOneOther + sqls: sqls( + "insert into testinsert (_id) values (1)", + ), + expErr: "insert column list must have at least one non '_id' column specified", + }, + { + // InsertSetsTypeError + sqls: sqls( + "insert into testinsert (_id, a, event) values (4, 40, [101, 150])", + ), + expErr: "an expression of type 'IDSET' cannot be assigned to type 'STRINGSET'", + }, + { + // InsertSetsTypeError2 + sqls: sqls( + "insert into testinsert (_id, a, ievent) values (4, 40, ['POST', 'GET'])", + ), + expErr: "an expression of type 'STRINGSET' cannot be assigned to type 'IDSET'", + }, + }, +} + +var keyedInsertTest = tableTest{ + name: "keyedinsert", + table: tbl( + "testkeyedinsert", + srcHdrs( + srcHdr("_id", fldTypeString), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("s", fldTypeString), + srcHdr("bl", fldTypeBool), + srcHdr("d", fldTypeDecimal2), + srcHdr("event", fldTypeStringSet), + srcHdr("ievent", fldTypeIDSet), + ), + nil, + ), + sqlTests: []sqlTest{ + { + // Insert + sqls: sqls( + "insert into testkeyedinsert (_id, a, b, s, bl, d, event, ievent) values ('four', 40, 400, 'foo', false, 10.12, ['A', 'B', 'C'], [1, 2, 3])", + ), + expHdrs: hdrs(), + expRows: rows(), + compare: compareExactUnordered, + }, + }, +} + +func knownTimestamp() time.Time { + tm, err := time.ParseInLocation(time.RFC3339, "2012-11-01T22:08:41+00:00", time.UTC) + if err != nil { + panic(err.Error()) + } + return tm +} + +var timestampLiterals = tableTest{ + table: tbl( + "testtimestampliterals", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("d", fldTypeDecimal2), + srcHdr("ts", fldTypeTimestamp), + srcHdr("event", fldTypeStringSet), + srcHdr("ievent", fldTypeIDSet), + ), + srcRows(), + ), + sqlTests: []sqlTest{ + { + // InsertWithCurrentTimestamp + sqls: sqls( + "insert into testtimestampliterals (_id, a, b, d, ts, event, ievent) values (4, 40, 400, 10.12, current_timestamp, ['A', 'B', 'C'], [1, 2, 3])", + ), + expHdrs: hdrs(), + expRows: rows(), + compare: compareExactUnordered, + }, + { + // InsertWithCurrentDate + sqls: sqls( + "insert into testtimestampliterals (_id, a, b, d, ts, event, ievent) values (4, 40, 400, 10.12, current_date, ['A', 'B', 'C'], [1, 2, 3])", + ), + expHdrs: hdrs(), + expRows: rows(), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_aggregate_test.go b/sql3/sql_defs_aggregate_test.go new file mode 100644 index 000000000..d52b11007 --- /dev/null +++ b/sql3/sql_defs_aggregate_test.go @@ -0,0 +1,507 @@ +package sql3_test + +import ( + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +//aggregate function tests +var countTests = tableTest{ + table: tbl( + "count_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("d1", fldTypeDecimal2), + srcHdr("i2", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), int64(10), float64(10), int64(100)), + srcRow(int64(2), int64(10), float64(10), int64(200)), + srcRow(int64(3), int64(11), float64(11), nil), + srcRow(int64(4), int64(12), float64(12), nil), + srcRow(int64(5), int64(12), float64(12), nil), + srcRow(int64(6), int64(13), float64(13), nil), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT COUNT(i1, d1) AS count_rows FROM count_test", + ), + expErr: "count of formal parameters (1) does not match count of actual parameters (2)", + }, + { + sqls: sqls( + "SELECT COUNT(1) AS count_rows FROM count_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test", + "SELECT COUNT(_id) AS count_rows FROM count_test", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(6)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) + 10 - 11 * 2 AS count_rows FROM count_test", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(-6)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test WHERE i1 = 10", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test WHERE i1 != 10", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(4)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test WHERE i1 < 12", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(3)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test WHERE i1 > 12", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test WHERE i1 = 10 AND i2 = 100", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) AS count_rows FROM count_test WHERE i1 = 10 OR i1 = 200 OR i1 = 12", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(4)), + ), + compare: compareExactUnordered, + }, + }, +} + +var countDistinctTests = tableTest{ + table: tbl( + "count_d_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("i2", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), int64(10), int64(100)), + srcRow(int64(2), int64(10), int64(200)), + srcRow(int64(3), int64(11), nil), + srcRow(int64(4), int64(12), nil), + srcRow(int64(5), int64(12), nil), + srcRow(int64(6), int64(13), nil), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT COUNT(distinct i1) AS count_rows FROM count_d_test", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(4)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(distinct i1) AS count_rows FROM count_d_test where i1 > 11", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + ), + expRows: rows( + row(int64(2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(distinct i1) AS count_rows, sum(i1) as sum_rows FROM count_d_test where i1 > 11", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + hdr("sum_rows", fldTypeInt), + ), + expRows: rows( + row(int64(2), int64(37)), + ), + compare: compareExactUnordered, + }, + }, +} + +var sumTests = tableTest{ + table: tbl( + "sum_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("d1", fldTypeDecimal2), + srcHdr("i2", fldTypeInt, "min 0", "max 1000"), + srcHdr("s1", fldTypeString), + ), + srcRows( + srcRow(int64(1), int64(10), float64(10), int64(100), string("foo")), + srcRow(int64(2), int64(10), float64(10), int64(200), string("foo")), + srcRow(int64(3), int64(11), float64(11), nil, string("foo")), + srcRow(int64(4), int64(12), float64(12), nil, string("foo")), + srcRow(int64(5), int64(12), float64(12), nil, string("foo")), + srcRow(int64(6), int64(13), float64(13), nil, string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT sum(*) AS sum_rows FROM sum_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT sum(_id) AS sum_rows FROM sum_test", + ), + expErr: "_id column cannot be used in aggregate function 'sum'", + }, + { + sqls: sqls( + "SELECT sum(1) AS sum_rows FROM sum_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT sum(i1, d1) AS sum_rows FROM sum_test", + ), + expErr: "count of formal parameters (1) does not match count of actual parameters (2)", + }, + { + sqls: sqls( + "SELECT sum(i1) AS sum_rows FROM sum_test", + ), + expHdrs: hdrs( + hdr("sum_rows", fldTypeInt), + ), + expRows: rows( + row(int64(68)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT sum(d1) AS sum_rows FROM sum_test", + ), + expHdrs: hdrs( + hdr("sum_rows", fldTypeDecimal2), + ), + expRows: rows( + row(pql.NewDecimal(6800, 2)), + ), + compare: compareExactUnordered, + }, + }, +} + +var avgTests = tableTest{ + table: tbl( + "avg_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("d1", fldTypeDecimal2), + srcHdr("s1", fldTypeString), + ), + srcRows( + srcRow(int64(1), int64(10), float64(10), string("foo")), + srcRow(int64(2), int64(10), float64(10), string("foo")), + srcRow(int64(3), int64(11), float64(11), string("foo")), + srcRow(int64(4), int64(12), float64(12), string("foo")), + srcRow(int64(5), int64(12), float64(12), string("foo")), + srcRow(int64(6), int64(13), float64(13), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT avg(*) AS avg_rows FROM avg_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT avg(_id) AS avg_rows FROM avg_test", + ), + expErr: "_id column cannot be used in aggregate function 'avg'", + }, + { + sqls: sqls( + "SELECT avg(i1, d1) AS avg_rows FROM avg_test", + ), + expErr: "count of formal parameters (1) does not match count of actual parameters (2)", + }, + { + sqls: sqls( + "SELECT avg(s1) AS avg_rows FROM avg_test", + ), + expErr: "integer or decimal expression expected", + }, + { + sqls: sqls( + "SELECT avg(i1) AS avg_rows FROM avg_test", + "SELECT avg(d1) AS avg_rows FROM avg_test", + ), + expHdrs: hdrs( + hdr("avg_rows", parser.NewDataTypeDecimal(4)), + ), + expRows: rows( + row(pql.NewDecimal(113333, 4)), + ), + compare: compareExactUnordered, + }, + }, +} + +var percentileTests = tableTest{ + table: tbl( + "percentile_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("d1", fldTypeDecimal2), + srcHdr("s1", fldTypeString), + ), + srcRows( + srcRow(int64(1), int64(10), float64(10), string("foo")), + srcRow(int64(2), int64(10), float64(10), string("foo")), + srcRow(int64(3), int64(11), float64(11), string("foo")), + srcRow(int64(4), int64(12), float64(12), string("foo")), + srcRow(int64(5), int64(12), float64(12), string("foo")), + srcRow(int64(6), int64(13), float64(13), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT percentile(*) AS avg_rows FROM percentile_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT percentile(10, i1) AS avg_rows FROM percentile_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT percentile(_id, 50) AS avg_rows FROM percentile_test", + ), + expErr: "_id column cannot be used in aggregate function 'percentile'", + }, + { + sqls: sqls( + "SELECT percentile(i1, d1) AS avg_rows FROM percentile_test", + ), + expErr: "literal expression expected", + }, + { + sqls: sqls( + "SELECT percentile(s1, 50) AS avg_rows FROM percentile_test", + ), + expErr: "integer, decimal or timestamp expression expected", + }, + { + sqls: sqls( + "SELECT percentile(i1, 50) AS p_rows FROM percentile_test", + ), + expHdrs: hdrs( + hdr("p_rows", fldTypeInt), + ), + expRows: rows( + row(int64(12)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT percentile(d1, 50) AS p_rows FROM percentile_test", + ), + expHdrs: hdrs( + hdr("p_rows", fldTypeDecimal2), + ), + expRows: rows( + row(pql.NewDecimal(1000, 2)), + ), + compare: compareExactUnordered, + }, + }, +} + +var minmaxTests = tableTest{ + table: tbl( + "minmax_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("d1", fldTypeDecimal2), + srcHdr("s1", fldTypeString), + ), + srcRows( + srcRow(int64(1), int64(10), float64(10), string("foo")), + srcRow(int64(2), int64(10), float64(10), string("foo")), + srcRow(int64(3), int64(11), float64(11), string("foo")), + srcRow(int64(4), int64(12), float64(12), string("foo")), + srcRow(int64(5), int64(12), float64(12), string("foo")), + srcRow(int64(6), int64(13), float64(13), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT min(*) AS p_rows FROM minmax_test", + "SELECT max(*) AS p_rows FROM minmax_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT min(i1, d1) AS p_rows FROM minmax_test", + "SELECT max(i1, d1) AS p_rows FROM minmax_test", + ), + expErr: "count of formal parameters (1) does not match count of actual parameters (2)", + }, + { + sqls: sqls( + "SELECT min(1) AS p_rows FROM minmax_test", + "SELECT max(1) AS p_rows FROM minmax_test", + ), + expErr: "column reference expected", + }, + { + sqls: sqls( + "SELECT min(_id) AS p_rows FROM minmax_test", + "SELECT max(_id) AS p_rows FROM minmax_test", + ), + expErr: "_id column cannot be used in aggregate function", + }, + { + sqls: sqls( + "SELECT min(s1) AS p_rows FROM minmax_test", + "SELECT max(s1) AS p_rows FROM minmax_test", + ), + expErr: "integer, decimal or timestamp expression expected", + }, + { + sqls: sqls( + "SELECT min(i1) AS p_rows FROM minmax_test", + ), + expHdrs: hdrs( + hdr("p_rows", fldTypeInt), + ), + expRows: rows( + row(int64(10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT max(i1) AS p_rows FROM minmax_test", + ), + expHdrs: hdrs( + hdr("p_rows", fldTypeInt), + ), + expRows: rows( + row(int64(13)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT min(d1) AS p_rows FROM minmax_test", + ), + expHdrs: hdrs( + hdr("p_rows", fldTypeDecimal2), + ), + expRows: rows( + row(pql.NewDecimal(1000, 2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT max(d1) AS p_rows FROM minmax_test", + ), + expHdrs: hdrs( + hdr("p_rows", fldTypeDecimal2), + ), + expRows: rows( + row(pql.NewDecimal(1300, 2)), + ), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_between_test.go b/sql3/sql_defs_between_test.go new file mode 100644 index 000000000..eb67e4d80 --- /dev/null +++ b/sql3/sql_defs_between_test.go @@ -0,0 +1,203 @@ +package sql3_test + +//BETWEEN tests +var betweenTests = tableTest{ + table: tbl( + "between_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id between 1 and 10 from between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i1 between 1 and 10 from between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b1 between true and false from between_all_types", + ), + expErr: "type 'BOOL' cannot be used a range subscript", + }, + { + sqls: sqls( + "select d1 between 1.23 and 4.56 from between_all_types", + ), + expErr: "type 'DECIMAL(2)' cannot be used a range subscript", + }, + { + sqls: sqls( + "select id1 between 3 and 7 from between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ids1 between [100, 102] and [456, 789] from between_all_types", + ), + expErr: "type 'IDSET' cannot be used a range subscript", + }, + { + sqls: sqls( + "select s1 between 'foo' and 'bar' from between_all_types", + ), + expErr: "type 'STRING' cannot be used a range subscript", + }, + { + sqls: sqls( + "select ss1 between ['a', 'b'] and ['c', 'd'] from between_all_types", + ), + expErr: "type 'STRINGSET' cannot be used a range subscript", + }, + { + sqls: sqls( + "select t1 between '2010-11-01T22:08:41+00:00' and '2013-11-01T22:08:41+00:00' from between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + }, +} + +//NOT BETWEEN tests +var notBetweenTests = tableTest{ + table: tbl( + "not_between_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id not between 1 and 10 from not_between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i1 not between 1 and 10 from not_between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b1 not between true and false from not_between_all_types", + ), + expErr: "type 'BOOL' cannot be used a range subscript", + }, + { + sqls: sqls( + "select d1 not between 1.23 and 4.56 from not_between_all_types", + ), + expErr: "type 'DECIMAL(2)' cannot be used a range subscript", + }, + { + sqls: sqls( + "select id1 between 3 and 7 from not_between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ids1 not between [100, 102] and [456, 789] from not_between_all_types", + ), + expErr: "type 'IDSET' cannot be used a range subscript", + }, + { + sqls: sqls( + "select s1 not between 'foo' and 'bar' from not_between_all_types", + ), + expErr: "type 'STRING' cannot be used a range subscript", + }, + { + sqls: sqls( + "select ss1 not between ['a', 'b'] and ['c', 'd'] from not_between_all_types", + ), + expErr: "type 'STRINGSET' cannot be used a range subscript", + }, + { + sqls: sqls( + "select t1 not between '2010-11-01T22:08:41+00:00' and '2013-11-01T22:08:41+00:00' from not_between_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_binops_test.go b/sql3/sql_defs_binops_test.go new file mode 100644 index 000000000..4b3eeedce --- /dev/null +++ b/sql3/sql_defs_binops_test.go @@ -0,0 +1,7922 @@ +package sql3_test + +import "time" + +//INT bin op tests +var binOpExprWithIntInt = tableTest{ + table: tbl( + "binoptesti_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), int64(10), int64(20)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a >= b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a < b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a > b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a & b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a | b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a << b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(10485760)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a >> b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a + b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a - b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(-10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a * b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(200)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a / b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a % b from binoptesti_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a || b from binoptesti_i;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntBool = tableTest{ + table: tbl( + "binoptesti_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeBool), + ), + srcRows( + srcRow(int64(1), int64(10), bool(true)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_b;", + ), + expErr: "types 'INT' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_b;", + ), + expErr: "types 'INT' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_b;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_b;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < b from binoptesti_b;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > b from binoptesti_b;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & b from binoptesti_b;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | b from binoptesti_b;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << b from binoptesti_b;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_b;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + b from binoptesti_b;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - b from binoptesti_b;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * b from binoptesti_b;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / b from binoptesti_b;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % b from binoptesti_b;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || b from binoptesti_b;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntID = tableTest{ + table: tbl( + "binoptesti_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(10), int64(20)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select b != _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b = _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b <= _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b >= _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b < _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b > _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b & _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b | _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b << _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(20480)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b >> _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b + _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b - _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b * _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(200)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b / _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b % _id from binoptesti_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b || _id from binoptesti_id;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntDecimal = tableTest{ + table: tbl( + "binoptesti_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), int64(20), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a >= d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a < d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a > d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a & d from binoptesti_d;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | d from binoptesti_d;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << d from binoptesti_d;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> d from binoptesti_d;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(32.34)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a - d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(7.66)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a * d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(246.8)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a / d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + //TODO(pok) this float64 thing is for the birds + row(float64(1.6207455429497568)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a % d from binoptesti_d;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || d from binoptesti_d;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntTimestamp = tableTest{ + table: tbl( + "binoptesti_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(20), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a >= ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a < ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a > ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a & ts from binoptesti_ts;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | ts from binoptesti_ts;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << ts from binoptesti_ts;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> ts from binoptesti_ts;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + ts from binoptesti_ts;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - ts from binoptesti_ts;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * ts from binoptesti_ts;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / ts from binoptesti_ts;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % ts from binoptesti_ts;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || ts from binoptesti_ts;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntIDSet = tableTest{ + table: tbl( + "binoptesti_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), int64(20), []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_ids;", + ), + expErr: "types 'INT' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_ids;", + ), + expErr: "types 'INT' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_ids;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_ids;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptesti_ids;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptesti_ids;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptesti_ids;", + ), + expErr: " operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | b from binoptesti_ids;", + ), + expErr: " operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << b from binoptesti_ids;", + ), + expErr: " operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_ids;", + ), + expErr: " operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + b from binoptesti_ids;", + ), + expErr: " operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - b from binoptesti_ids;", + ), + expErr: " operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * b from binoptesti_ids;", + ), + expErr: " operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / b from binoptesti_ids;", + ), + expErr: " operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % b from binoptesti_ids;", + ), + expErr: " operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || b from binoptesti_ids;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntString = tableTest{ + table: tbl( + "binoptesti_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), int64(20), string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_s;", + ), + expErr: "types 'INT' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_s;", + ), + expErr: "types 'INT' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_s;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_s;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptesti_s;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptesti_s;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptesti_s;", + ), + expErr: " operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | b from binoptesti_s;", + ), + expErr: " operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << b from binoptesti_s;", + ), + expErr: " operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_s;", + ), + expErr: " operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + b from binoptesti_s;", + ), + expErr: " operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - b from binoptesti_s;", + ), + expErr: " operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * b from binoptesti_s;", + ), + expErr: " operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / b from binoptesti_s;", + ), + expErr: " operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % b from binoptesti_s;", + ), + expErr: " operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || b from binoptesti_s;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIntStringSet = tableTest{ + table: tbl( + "binoptesti_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), int64(20), []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_ss;", + ), + expErr: "types 'INT' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_ss;", + ), + expErr: "types 'INT' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_ss;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_ss;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptesti_ss;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptesti_ss;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptesti_ss;", + ), + expErr: " operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | b from binoptesti_ss;", + ), + expErr: " operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << b from binoptesti_ss;", + ), + expErr: " operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_ss;", + ), + expErr: " operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + b from binoptesti_ss;", + ), + expErr: " operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - b from binoptesti_ss;", + ), + expErr: " operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * b from binoptesti_ss;", + ), + expErr: " operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / b from binoptesti_ss;", + ), + expErr: " operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % b from binoptesti_ss;", + ), + expErr: " operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || b from binoptesti_ss;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +//BOOL bin op tests +var binOpExprWithBoolInt = tableTest{ + table: tbl( + "binoptestb_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), bool(true), int64(20)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestb_i;", + ), + expErr: "types 'BOOL' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestb_i;", + ), + expErr: "types 'BOOL' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestb_i;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= b from binoptestb_i;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < b from binoptestb_i;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > b from binoptestb_i;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & b from binoptestb_i;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | b from binoptestb_i;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << b from binoptestb_i;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> b from binoptestb_i;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + b from binoptestb_i;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - b from binoptestb_i;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * b from binoptestb_i;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / b from binoptestb_i;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % b from binoptestb_i;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || b from binoptestb_i;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolBool = tableTest{ + table: tbl( + "binoptestb_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("b", fldTypeBool), + ), + srcRows( + srcRow(int64(1), bool(true), bool(true)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestb_b;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = b from binoptestb_b;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= b from binoptestb_b;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= b from binoptestb_b;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < b from binoptestb_b;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > b from binoptestb_b;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & b from binoptestb_b;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | b from binoptestb_b;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << b from binoptestb_b;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> b from binoptestb_b;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + b from binoptestb_b;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - b from binoptestb_b;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * b from binoptestb_b;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / b from binoptestb_b;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % b from binoptestb_b;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || b from binoptestb_b;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolID = tableTest{ + table: tbl( + "binoptestb_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + ), + srcRows( + srcRow(int64(10), bool(true)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select b != _id from binoptestb_id;", + ), + expErr: "types 'BOOL' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select b = _id from binoptestb_id;", + ), + expErr: "types 'BOOL' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select b <= _id from binoptestb_id;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b >= _id from binoptestb_id;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b < _id from binoptestb_id;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b > _id from binoptestb_id;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b & _id from binoptestb_id;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b | _id from binoptestb_id;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b << _id from binoptestb_id;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b >> _id from binoptestb_id;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b + _id from binoptestb_id;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b - _id from binoptestb_id;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b * _id from binoptestb_id;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b / _id from binoptestb_id;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b % _id from binoptestb_id;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select b || _id from binoptestb_id;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolDecimal = tableTest{ + table: tbl( + "binoptestb_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), bool(true), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptestb_d;", + ), + expErr: "types 'BOOL' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a = d from binoptestb_d;", + ), + expErr: "types 'BOOL' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a <= d from binoptestb_d;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= d from binoptestb_d;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < d from binoptestb_d;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > d from binoptestb_d;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & d from binoptestb_d;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | d from binoptestb_d;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << d from binoptestb_d;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> d from binoptestb_d;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + d from binoptestb_d;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - d from binoptestb_d;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * d from binoptestb_d;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / d from binoptestb_d;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % d from binoptestb_d;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || d from binoptestb_d;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolTimestamp = tableTest{ + table: tbl( + "binoptestb_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), bool(true), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptestb_ts;", + ), + expErr: "types 'BOOL' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptestb_ts;", + ), + expErr: "types 'BOOL' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptestb_ts;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= ts from binoptestb_ts;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < ts from binoptestb_ts;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > ts from binoptestb_ts;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & ts from binoptestb_ts;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | ts from binoptestb_ts;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << ts from binoptestb_ts;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> ts from binoptestb_ts;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + ts from binoptestb_ts;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - ts from binoptestb_ts;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * ts from binoptestb_ts;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / ts from binoptestb_ts;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % ts from binoptestb_ts;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || ts from binoptestb_ts;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolIDSet = tableTest{ + table: tbl( + "binoptestb_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), bool(true), []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestb_ids;", + ), + expErr: "types 'BOOL' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestb_ids;", + ), + expErr: "types 'BOOL' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestb_ids;", + ), + expErr: " operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= b from binoptestb_ids;", + ), + expErr: " operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < b from binoptestb_ids;", + ), + expErr: " operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > b from binoptestb_ids;", + ), + expErr: " operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & b from binoptestb_ids;", + ), + expErr: " operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | b from binoptestb_ids;", + ), + expErr: " operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << b from binoptestb_ids;", + ), + expErr: " operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> b from binoptestb_ids;", + ), + expErr: " operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + b from binoptestb_ids;", + ), + expErr: " operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - b from binoptestb_ids;", + ), + expErr: " operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * b from binoptestb_ids;", + ), + expErr: " operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / b from binoptestb_ids;", + ), + expErr: " operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % b from binoptestb_ids;", + ), + expErr: " operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || b from binoptestb_ids;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolString = tableTest{ + table: tbl( + "binoptestb_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), bool(true), string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestb_s;", + ), + expErr: "types 'BOOL' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestb_s;", + ), + expErr: "types 'BOOL' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestb_s;", + ), + expErr: " operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= b from binoptestb_s;", + ), + expErr: " operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < b from binoptestb_s;", + ), + expErr: " operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > b from binoptestb_s;", + ), + expErr: " operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & b from binoptestb_s;", + ), + expErr: " operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | b from binoptestb_s;", + ), + expErr: " operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << b from binoptestb_s;", + ), + expErr: " operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> b from binoptestb_s;", + ), + expErr: " operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + b from binoptestb_s;", + ), + expErr: " operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - b from binoptestb_s;", + ), + expErr: " operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * b from binoptestb_s;", + ), + expErr: " operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / b from binoptestb_s;", + ), + expErr: " operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % b from binoptestb_s;", + ), + expErr: " operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || b from binoptestb_s;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithBoolStringSet = tableTest{ + table: tbl( + "binoptestb_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeBool), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), bool(true), []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestb_ss;", + ), + expErr: "types 'BOOL' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestb_ss;", + ), + expErr: "types 'BOOL' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestb_ss;", + ), + expErr: " operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >= b from binoptestb_ss;", + ), + expErr: " operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a < b from binoptestb_ss;", + ), + expErr: " operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a > b from binoptestb_ss;", + ), + expErr: " operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a & b from binoptestb_ss;", + ), + expErr: " operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a | b from binoptestb_ss;", + ), + expErr: " operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a << b from binoptestb_ss;", + ), + expErr: " operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a >> b from binoptestb_ss;", + ), + expErr: " operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a + b from binoptestb_ss;", + ), + expErr: " operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a - b from binoptestb_ss;", + ), + expErr: " operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a * b from binoptestb_ss;", + ), + expErr: " operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a / b from binoptestb_ss;", + ), + expErr: " operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a % b from binoptestb_ss;", + ), + expErr: " operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select a || b from binoptestb_ss;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +//ID bin op tests +var binOpExprWithIDInt = tableTest{ + table: tbl( + "binoptestid_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(10), int64(20)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id != b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id = b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id <= b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id >= b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id < b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id > b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id & b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id | b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id << b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(10485760)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id >> b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id + b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id - b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(-10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id * b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(200)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id / b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id % b from binoptestid_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id || b from binoptestid_i;", + ), + expErr: "operator '||' incompatible with type 'ID'", + }, + }, +} + +var binOpExprWithIDBool = tableTest{ + table: tbl( + "binoptestid_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + ), + srcRows( + srcRow(int64(10), bool(true)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id != b from binoptestid_b;", + ), + expErr: "types 'ID' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select _id = b from binoptestid_b;", + ), + expErr: "types 'ID' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select _id <= b from binoptestid_b;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id >= b from binoptestid_b;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id < b from binoptestid_b;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id > b from binoptestid_b;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id & b from binoptestid_b;", + ), + expErr: "operator '&' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id | b from binoptestid_b;", + ), + expErr: "operator '|' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id << b from binoptestid_b;", + ), + expErr: "operator '<<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id >> b from binoptestid_b;", + ), + expErr: "operator '>>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id + b from binoptestid_b;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id - b from binoptestid_b;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id * b from binoptestid_b;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id / b from binoptestid_b;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id % b from binoptestid_b;", + ), + expErr: "operator '%' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select _id || b from binoptestid_b;", + ), + expErr: "operator '||' incompatible with type 'ID'", + }, + }, +} + +var binOpExprWithIDID = tableTest{ + table: tbl( + "binoptestid_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeID), + ), + srcRows( + srcRow(int64(10), int64(20)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id != b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id = b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id <= b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id >= b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id < b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id > b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id & b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id | b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id << b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(10485760)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id >> b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id + b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(30)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id - b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(-10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id * b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(200)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id / b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id % b from binoptestid_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id || b from binoptestid_id;", + ), + expErr: "operator '||' incompatible with type 'ID'", + }, + }, +} + +var binOpExprWithIDDecimal = tableTest{ + table: tbl( + "binoptestid_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeID), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), int64(20), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a >= d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a < d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a > d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a & d from binoptesti_d;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | d from binoptesti_d;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << d from binoptesti_d;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> d from binoptesti_d;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(32.34)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a - d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(7.66)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a * d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(246.8)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a / d from binoptesti_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + //TODO(pok) this float64 thing is for the birds + row(float64(1.6207455429497568)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a % d from binoptesti_d;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || d from binoptesti_d;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIDTimestamp = tableTest{ + table: tbl( + "binoptestid_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeID), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(20), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a >= ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a < ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a > ts from binoptesti_ts;", + ), + expErr: "types 'INT' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a & ts from binoptesti_ts;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | ts from binoptesti_ts;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << ts from binoptesti_ts;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> ts from binoptesti_ts;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + ts from binoptesti_ts;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - ts from binoptesti_ts;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * ts from binoptesti_ts;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / ts from binoptesti_ts;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % ts from binoptesti_ts;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || ts from binoptesti_ts;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIDIDSet = tableTest{ + table: tbl( + "binoptestid_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeID), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), int64(20), []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_ids;", + ), + expErr: "types 'INT' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_ids;", + ), + expErr: "types 'INT' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_ids;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_ids;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptesti_ids;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptesti_ids;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptesti_ids;", + ), + expErr: " operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | b from binoptesti_ids;", + ), + expErr: " operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << b from binoptesti_ids;", + ), + expErr: " operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_ids;", + ), + expErr: " operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + b from binoptesti_ids;", + ), + expErr: " operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - b from binoptesti_ids;", + ), + expErr: " operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * b from binoptesti_ids;", + ), + expErr: " operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / b from binoptesti_ids;", + ), + expErr: " operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % b from binoptesti_ids;", + ), + expErr: " operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || b from binoptesti_ids;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIDString = tableTest{ + table: tbl( + "binoptestid_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeID), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), int64(20), string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_s;", + ), + expErr: "types 'INT' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_s;", + ), + expErr: "types 'INT' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_s;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_s;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptesti_s;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptesti_s;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptesti_s;", + ), + expErr: " operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | b from binoptesti_s;", + ), + expErr: " operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << b from binoptesti_s;", + ), + expErr: " operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_s;", + ), + expErr: " operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + b from binoptesti_s;", + ), + expErr: " operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - b from binoptesti_s;", + ), + expErr: " operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * b from binoptesti_s;", + ), + expErr: " operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / b from binoptesti_s;", + ), + expErr: " operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % b from binoptesti_s;", + ), + expErr: " operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || b from binoptesti_s;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithIDStringSet = tableTest{ + table: tbl( + "binoptestid_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeID), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), int64(20), []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptesti_ss;", + ), + expErr: "types 'INT' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptesti_ss;", + ), + expErr: "types 'INT' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptesti_ss;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptesti_ss;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptesti_ss;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptesti_ss;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptesti_ss;", + ), + expErr: " operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | b from binoptesti_ss;", + ), + expErr: " operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << b from binoptesti_ss;", + ), + expErr: " operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> b from binoptesti_ss;", + ), + expErr: " operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + b from binoptesti_ss;", + ), + expErr: " operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - b from binoptesti_ss;", + ), + expErr: " operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * b from binoptesti_ss;", + ), + expErr: " operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / b from binoptesti_ss;", + ), + expErr: " operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % b from binoptesti_ss;", + ), + expErr: " operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || b from binoptesti_ss;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +//DECIMAL bin op tests +var binOpExprWithDecInt = tableTest{ + table: tbl( + "binoptestdec_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(10), int64(20), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d = b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d <= b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d >= b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d < b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d > b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d & b from binoptestdec_i;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d | b from binoptestdec_i;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d << b from binoptestdec_i;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d >> b from binoptestdec_i;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d + b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(32.34)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d - b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(-7.66)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d * b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(246.8)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d / b from binoptestdec_i;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(0.617)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d % b from binoptestdec_i;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d || b from binoptestdec_i;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecBool = tableTest{ + table: tbl( + "binoptestdec_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(10), bool(true), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestdec_b;", + ), + expErr: "types 'DECIMAL(2)' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestdec_b;", + ), + expErr: "types 'DECIMAL(2)' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestdec_b;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d >= b from binoptestdec_b;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d < b from binoptestdec_b;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d > b from binoptestdec_b;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d & b from binoptestdec_b;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d | b from binoptestdec_b;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d << b from binoptestdec_b;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d >> b from binoptestdec_b;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d + b from binoptestdec_b;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d - b from binoptestdec_b;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d * b from binoptestdec_b;", + ), + expErr: "operator '*' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d / b from binoptestdec_b;", + ), + expErr: "operator '/' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d % b from binoptestdec_b;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d || b from binoptestdec_b;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecID = tableTest{ + table: tbl( + "binoptestdec_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeID), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(10), int64(20), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d = b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d <= b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d >= b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d < b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d > b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d & b from binoptestdec_id;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d | b from binoptestdec_id;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d << b from binoptestdec_id;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d >> b from binoptestdec_id;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d + b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(32.34)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d - b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(-7.66)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d * b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(246.8)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d / b from binoptestdec_id;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(0.617)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d % b from binoptestdec_id;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select d || b from binoptestdec_id;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecDecimal = tableTest{ + table: tbl( + "binoptestdec_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeDecimal2), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), float64(20.00), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a >= d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a < d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a > d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a & d from binoptestdec_d;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | d from binoptestdec_d;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << d from binoptestdec_d;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> d from binoptestdec_d;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(32.34)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a - d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(7.66)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a * d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(246.8)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a / d from binoptestdec_d;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + //TODO(pok) this float64 thing is for the birds + row(float64(1.6207455429497568)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a % d from binoptestdec_d;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || d from binoptestdec_d;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecTimestamp = tableTest{ + table: tbl( + "binoptestdec_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeDecimal2), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), float64(20.00), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptestdec_ts;", + ), + expErr: "types 'DECIMAL(2)' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptestdec_ts;", + ), + expErr: "types 'DECIMAL(2)' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptestdec_ts;", + ), + expErr: "types 'DECIMAL(2)' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a >= ts from binoptestdec_ts;", + ), + expErr: "types 'DECIMAL(2)' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a < ts from binoptestdec_ts;", + ), + expErr: "types 'DECIMAL(2)' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a > ts from binoptestdec_ts;", + ), + expErr: "types 'DECIMAL(2)' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a & ts from binoptestdec_ts;", + ), + expErr: "operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | ts from binoptestdec_ts;", + ), + expErr: "operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << ts from binoptestdec_ts;", + ), + expErr: "operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> ts from binoptestdec_ts;", + ), + expErr: "operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + ts from binoptestdec_ts;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - ts from binoptestdec_ts;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * ts from binoptestdec_ts;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / ts from binoptestdec_ts;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % ts from binoptestdec_ts;", + ), + expErr: "operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || ts from binoptestdec_ts;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecIDSet = tableTest{ + table: tbl( + "binoptestdec_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeDecimal2), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), float64(20.00), []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestdec_ids;", + ), + expErr: "types 'DECIMAL(2)' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestdec_ids;", + ), + expErr: "types 'DECIMAL(2)' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestdec_ids;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestdec_ids;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptestdec_ids;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptestdec_ids;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptestdec_ids;", + ), + expErr: " operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | b from binoptestdec_ids;", + ), + expErr: " operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << b from binoptestdec_ids;", + ), + expErr: " operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> b from binoptestdec_ids;", + ), + expErr: " operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + b from binoptestdec_ids;", + ), + expErr: " operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - b from binoptestdec_ids;", + ), + expErr: " operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * b from binoptestdec_ids;", + ), + expErr: " operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / b from binoptestdec_ids;", + ), + expErr: " operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % b from binoptestdec_ids;", + ), + expErr: " operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || b from binoptestdec_ids;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecString = tableTest{ + table: tbl( + "binoptestdec_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeDecimal2), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), float64(20.00), string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestdec_s;", + ), + expErr: "types 'DECIMAL(2)' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestdec_s;", + ), + expErr: "types 'DECIMAL(2)' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestdec_s;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptestdec_s;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptestdec_s;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptestdec_s;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptestdec_s;", + ), + expErr: " operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | b from binoptestdec_s;", + ), + expErr: " operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << b from binoptestdec_s;", + ), + expErr: " operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> b from binoptestdec_s;", + ), + expErr: " operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + b from binoptestdec_s;", + ), + expErr: " operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - b from binoptestdec_s;", + ), + expErr: " operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * b from binoptestdec_s;", + ), + expErr: " operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / b from binoptestdec_s;", + ), + expErr: " operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % b from binoptestdec_s;", + ), + expErr: " operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || b from binoptestdec_s;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithDecStringSet = tableTest{ + table: tbl( + "binoptestdec_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeDecimal2), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), float64(20.00), []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestdec_ss;", + ), + expErr: "types 'DECIMAL(2)' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestdec_ss;", + ), + expErr: "types 'DECIMAL(2)' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestdec_ss;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestdec_ss;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptestdec_ss;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptestdec_ss;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptestdec_ss;", + ), + expErr: " operator '&' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a | b from binoptestdec_ss;", + ), + expErr: " operator '|' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a << b from binoptestdec_ss;", + ), + expErr: " operator '<<' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a >> b from binoptestdec_ss;", + ), + expErr: " operator '>>' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a + b from binoptestdec_ss;", + ), + expErr: " operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - b from binoptestdec_ss;", + ), + expErr: " operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * b from binoptestdec_ss;", + ), + expErr: " operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / b from binoptestdec_ss;", + ), + expErr: " operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % b from binoptestdec_ss;", + ), + expErr: " operator '%' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select a || b from binoptestdec_ss;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +//TIMESTAMP bin op tests +var binOpExprWithTSInt = tableTest{ + table: tbl( + "binoptestts_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("d", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(10), int64(20), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestts_i;", + ), + expErr: "types 'TIMESTAMP' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestts_i;", + ), + expErr: "types 'TIMESTAMP' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestts_i;", + ), + expErr: "types 'TIMESTAMP' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d >= b from binoptestts_i;", + ), + expErr: "types 'TIMESTAMP' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d < b from binoptestts_i;", + ), + expErr: "types 'TIMESTAMP' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d > b from binoptestts_i;", + ), + expErr: "types 'TIMESTAMP' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d & b from binoptestts_i;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d | b from binoptestts_i;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d << b from binoptestts_i;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d >> b from binoptestts_i;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d + b from binoptestts_i;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d - b from binoptestts_i;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d * b from binoptestts_i;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d / b from binoptestts_i;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d % b from binoptestts_i;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d || b from binoptestts_i;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSBool = tableTest{ + table: tbl( + "binoptestts_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + srcHdr("d", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(10), bool(true), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestts_b;", + ), + expErr: "types 'TIMESTAMP' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestts_b;", + ), + expErr: "types 'TIMESTAMP' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestts_b;", + ), + expErr: "operator '<=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d >= b from binoptestts_b;", + ), + expErr: "operator '>=' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d < b from binoptestts_b;", + ), + expErr: "operator '<' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d > b from binoptestts_b;", + ), + expErr: "operator '>' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d & b from binoptestts_b;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d | b from binoptestts_b;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d << b from binoptestts_b;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d >> b from binoptestts_b;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d + b from binoptestts_b;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d - b from binoptestts_b;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d * b from binoptestts_b;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d / b from binoptestts_b;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d % b from binoptestts_b;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d || b from binoptestts_b;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSID = tableTest{ + table: tbl( + "binoptestts_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeID), + srcHdr("d", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(10), int64(20), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestts_id;", + ), + expErr: "types 'TIMESTAMP' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestts_id;", + ), + expErr: "types 'TIMESTAMP' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestts_id;", + ), + expErr: "types 'TIMESTAMP' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d >= b from binoptestts_id;", + ), + expErr: "types 'TIMESTAMP' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d < b from binoptestts_id;", + ), + expErr: "types 'TIMESTAMP' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d > b from binoptestts_id;", + ), + expErr: "types 'TIMESTAMP' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d & b from binoptestts_id;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d | b from binoptestts_id;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d << b from binoptestts_id;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d >> b from binoptestts_id;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d + b from binoptestts_id;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d - b from binoptestts_id;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d * b from binoptestts_id;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d / b from binoptestts_id;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d % b from binoptestts_id;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select d || b from binoptestts_id;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSDecimal = tableTest{ + table: tbl( + "binoptestts_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeTimestamp), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), time.Time(knownTimestamp()), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptestts_d;", + ), + expErr: "types 'TIMESTAMP' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a = d from binoptestts_d;", + ), + expErr: "types 'TIMESTAMP' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a <= d from binoptestts_d;", + ), + expErr: "types 'TIMESTAMP' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a >= d from binoptestts_d;", + ), + expErr: "types 'TIMESTAMP' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a < d from binoptestts_d;", + ), + expErr: "types 'TIMESTAMP' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a > d from binoptestts_d;", + ), + expErr: "types 'TIMESTAMP' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a & d from binoptestts_d;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | d from binoptestts_d;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << d from binoptestts_d;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> d from binoptestts_d;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + d from binoptestts_d;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - d from binoptestts_d;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * d from binoptestts_d;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / d from binoptestts_d;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % d from binoptestts_d;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || d from binoptestts_d;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSTimestamp = tableTest{ + table: tbl( + "binoptestts_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeTimestamp), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), time.Time(knownTimestamp()), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptestts_ts;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered}, + { + sqls: sqls( + "select a = ts from binoptestts_ts;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered}, + { + sqls: sqls( + "select a <= ts from binoptestts_ts;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a >= ts from binoptestts_ts;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered}, + { + sqls: sqls( + "select a < ts from binoptestts_ts;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered}, + { + sqls: sqls( + "select a > ts from binoptestts_ts;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered}, + { + sqls: sqls( + "select a & ts from binoptestts_ts;", + ), + expErr: "operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | ts from binoptestts_ts;", + ), + expErr: "operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << ts from binoptestts_ts;", + ), + expErr: "operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> ts from binoptestts_ts;", + ), + expErr: "operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + ts from binoptestts_ts;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - ts from binoptestts_ts;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * ts from binoptestts_ts;", + ), + expErr: "operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / ts from binoptestts_ts;", + ), + expErr: "operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % ts from binoptestts_ts;", + ), + expErr: "operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || ts from binoptestts_ts;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSIDSet = tableTest{ + table: tbl( + "binoptestts_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeTimestamp), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), time.Time(knownTimestamp()), []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestts_ids;", + ), + expErr: "types 'TIMESTAMP' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestts_ids;", + ), + expErr: "types 'TIMESTAMP' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestts_ids;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestts_ids;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptestts_ids;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptestts_ids;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptestts_ids;", + ), + expErr: " operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | b from binoptestts_ids;", + ), + expErr: " operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << b from binoptestts_ids;", + ), + expErr: " operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> b from binoptestts_ids;", + ), + expErr: " operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + b from binoptestts_ids;", + ), + expErr: " operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - b from binoptestts_ids;", + ), + expErr: " operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * b from binoptestts_ids;", + ), + expErr: " operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / b from binoptestts_ids;", + ), + expErr: " operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % b from binoptestts_ids;", + ), + expErr: " operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || b from binoptestts_ids;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSString = tableTest{ + table: tbl( + "binoptestts_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeTimestamp), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), time.Time(knownTimestamp()), string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestts_s;", + ), + expErr: "types 'TIMESTAMP' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestts_s;", + ), + expErr: "types 'TIMESTAMP' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestts_s;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptestts_s;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptestts_s;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptestts_s;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptestts_s;", + ), + expErr: " operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | b from binoptestts_s;", + ), + expErr: " operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << b from binoptestts_s;", + ), + expErr: " operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> b from binoptestts_s;", + ), + expErr: " operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + b from binoptestts_s;", + ), + expErr: " operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - b from binoptestts_s;", + ), + expErr: " operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * b from binoptestts_s;", + ), + expErr: " operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / b from binoptestts_s;", + ), + expErr: " operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % b from binoptestts_s;", + ), + expErr: " operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || b from binoptestts_s;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithTSStringSet = tableTest{ + table: tbl( + "binoptestts_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeTimestamp), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), time.Time(knownTimestamp()), []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestts_ss;", + ), + expErr: "types 'TIMESTAMP' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestts_ss;", + ), + expErr: "types 'TIMESTAMP' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestts_ss;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestts_ss;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptestts_ss;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptestts_ss;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptestts_ss;", + ), + expErr: " operator '&' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a | b from binoptestts_ss;", + ), + expErr: " operator '|' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a << b from binoptestts_ss;", + ), + expErr: " operator '<<' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a >> b from binoptestts_ss;", + ), + expErr: " operator '>>' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a + b from binoptestts_ss;", + ), + expErr: " operator '+' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a - b from binoptestts_ss;", + ), + expErr: " operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a * b from binoptestts_ss;", + ), + expErr: " operator '*' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a / b from binoptestts_ss;", + ), + expErr: " operator '/' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a % b from binoptestts_ss;", + ), + expErr: " operator '%' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select a || b from binoptestts_ss;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +//IDSET bin op tests +var binOpExprWithIDSetInt = tableTest{ + table: tbl( + "binoptestids_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt), + srcHdr("d", fldTypeIDSet), + ), + srcRows( + srcRow(int64(10), int64(20), []int64{20, 21}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestids_i;", + ), + expErr: "types 'IDSET' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestids_i;", + ), + expErr: "types 'IDSET' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestids_i;", + ), + expErr: "operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d >= b from binoptestids_i;", + ), + expErr: "operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d < b from binoptestids_i;", + ), + expErr: "operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d > b from binoptestids_i;", + ), + expErr: "operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d & b from binoptestids_i;", + ), + expErr: "operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d | b from binoptestids_i;", + ), + expErr: "operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d << b from binoptestids_i;", + ), + expErr: "operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d >> b from binoptestids_i;", + ), + expErr: "operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d + b from binoptestids_i;", + ), + expErr: "operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d - b from binoptestids_i;", + ), + expErr: "operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d * b from binoptestids_i;", + ), + expErr: "operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d / b from binoptestids_i;", + ), + expErr: "operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d % b from binoptestids_i;", + ), + expErr: "operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d || b from binoptestids_i;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetBool = tableTest{ + table: tbl( + "binoptestids_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + srcHdr("d", fldTypeIDSet), + ), + srcRows( + srcRow(int64(10), bool(true), []int64{20, 21}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestids_b;", + ), + expErr: "types 'IDSET' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestids_b;", + ), + expErr: "types 'IDSET' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestids_b;", + ), + expErr: "operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d >= b from binoptestids_b;", + ), + expErr: "operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d < b from binoptestids_b;", + ), + expErr: "operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d > b from binoptestids_b;", + ), + expErr: "operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d & b from binoptestids_b;", + ), + expErr: "operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d | b from binoptestids_b;", + ), + expErr: "operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d << b from binoptestids_b;", + ), + expErr: "operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d >> b from binoptestids_b;", + ), + expErr: "operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d + b from binoptestids_b;", + ), + expErr: "operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d - b from binoptestids_b;", + ), + expErr: "operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d * b from binoptestids_b;", + ), + expErr: "operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d / b from binoptestids_b;", + ), + expErr: "operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d % b from binoptestids_b;", + ), + expErr: "operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d || b from binoptestids_b;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetID = tableTest{ + table: tbl( + "binoptestids_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeID), + srcHdr("d", fldTypeIDSet), + ), + srcRows( + srcRow(int64(10), int64(20), []int64{20, 21}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestids_id;", + ), + expErr: "types 'IDSET' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestids_id;", + ), + expErr: "types 'IDSET' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestids_id;", + ), + expErr: "operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d >= b from binoptestids_id;", + ), + expErr: "operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d < b from binoptestids_id;", + ), + expErr: "operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d > b from binoptestids_id;", + ), + expErr: "operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d & b from binoptestids_id;", + ), + expErr: "operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d | b from binoptestids_id;", + ), + expErr: "operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d << b from binoptestids_id;", + ), + expErr: "operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d >> b from binoptestids_id;", + ), + expErr: "operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d + b from binoptestids_id;", + ), + expErr: "operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d - b from binoptestids_id;", + ), + expErr: "operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d * b from binoptestids_id;", + ), + expErr: "operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d / b from binoptestids_id;", + ), + expErr: "operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d % b from binoptestids_id;", + ), + expErr: "operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select d || b from binoptestids_id;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetDecimal = tableTest{ + table: tbl( + "binoptestids_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeIDSet), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), []int64{20, 21}, float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptestids_d;", + ), + expErr: "types 'IDSET' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a = d from binoptestids_d;", + ), + expErr: "types 'IDSET' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a <= d from binoptestids_d;", + ), + expErr: "operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= d from binoptestids_d;", + ), + expErr: "operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < d from binoptestids_d;", + ), + expErr: "operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > d from binoptestids_d;", + ), + expErr: "operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & d from binoptestids_d;", + ), + expErr: "operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | d from binoptestids_d;", + ), + expErr: "operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << d from binoptestids_d;", + ), + expErr: "operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> d from binoptestids_d;", + ), + expErr: "operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + d from binoptestids_d;", + ), + expErr: "operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - d from binoptestids_d;", + ), + expErr: "operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * d from binoptestids_d;", + ), + expErr: "operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / d from binoptestids_d;", + ), + expErr: "operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % d from binoptestids_d;", + ), + expErr: "operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || d from binoptestids_d;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetTimestamp = tableTest{ + table: tbl( + "binoptestids_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeIDSet), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), []int64{20, 21}, time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptestids_ts;", + ), + expErr: "types 'IDSET' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptestids_ts;", + ), + expErr: "types 'IDSET' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptestids_ts;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= ts from binoptestids_ts;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < ts from binoptestids_ts;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > ts from binoptestids_ts;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & ts from binoptestids_ts;", + ), + expErr: "operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | ts from binoptestids_ts;", + ), + expErr: "operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << ts from binoptestids_ts;", + ), + expErr: "operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> ts from binoptestids_ts;", + ), + expErr: "operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + ts from binoptestids_ts;", + ), + expErr: "operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - ts from binoptestids_ts;", + ), + expErr: "operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * ts from binoptestids_ts;", + ), + expErr: "operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / ts from binoptestids_ts;", + ), + expErr: "operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % ts from binoptestids_ts;", + ), + expErr: "operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || ts from binoptestids_ts;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetIDSet = tableTest{ + table: tbl( + "binoptestids_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeIDSet), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), []int64{101, 103}, []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestids_ids;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = b from binoptestids_ids;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= b from binoptestids_ids;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestids_ids;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptestids_ids;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptestids_ids;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptestids_ids;", + ), + expErr: " operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | b from binoptestids_ids;", + ), + expErr: " operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << b from binoptestids_ids;", + ), + expErr: " operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> b from binoptestids_ids;", + ), + expErr: " operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + b from binoptestids_ids;", + ), + expErr: " operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - b from binoptestids_ids;", + ), + expErr: " operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * b from binoptestids_ids;", + ), + expErr: " operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / b from binoptestids_ids;", + ), + expErr: " operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % b from binoptestids_ids;", + ), + expErr: " operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || b from binoptestids_ids;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetString = tableTest{ + table: tbl( + "binoptestids_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeIDSet), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), []int64{101, 102}, string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestids_s;", + ), + expErr: "types 'IDSET' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestids_s;", + ), + expErr: "types 'IDSET' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestids_s;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestids_s;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptestids_s;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptestids_s;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptestids_s;", + ), + expErr: " operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | b from binoptestids_s;", + ), + expErr: " operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << b from binoptestids_s;", + ), + expErr: " operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> b from binoptestids_s;", + ), + expErr: " operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + b from binoptestids_s;", + ), + expErr: " operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - b from binoptestids_s;", + ), + expErr: " operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * b from binoptestids_s;", + ), + expErr: " operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / b from binoptestids_s;", + ), + expErr: " operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % b from binoptestids_s;", + ), + expErr: " operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || b from binoptestids_s;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithIDSetStringSet = tableTest{ + table: tbl( + "binoptestids_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeIDSet), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), []int64{102, 103}, []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestids_ss;", + ), + expErr: "types 'IDSET' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestids_ss;", + ), + expErr: "types 'IDSET' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestids_ss;", + ), + expErr: " operator '<=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestids_ss;", + ), + expErr: " operator '>=' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a < b from binoptestids_ss;", + ), + expErr: " operator '<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a > b from binoptestids_ss;", + ), + expErr: " operator '>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a & b from binoptestids_ss;", + ), + expErr: " operator '&' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a | b from binoptestids_ss;", + ), + expErr: " operator '|' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a << b from binoptestids_ss;", + ), + expErr: " operator '<<' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a >> b from binoptestids_ss;", + ), + expErr: " operator '>>' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a + b from binoptestids_ss;", + ), + expErr: " operator '+' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a - b from binoptestids_ss;", + ), + expErr: " operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a * b from binoptestids_ss;", + ), + expErr: " operator '*' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a / b from binoptestids_ss;", + ), + expErr: " operator '/' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a % b from binoptestids_ss;", + ), + expErr: " operator '%' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select a || b from binoptestids_ss;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +//STRING bin op tests +var binOpExprWithStringInt = tableTest{ + table: tbl( + "binoptests_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt), + srcHdr("d", fldTypeString), + ), + srcRows( + srcRow(int64(10), int64(20), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptests_i;", + ), + expErr: "types 'STRING' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptests_i;", + ), + expErr: "types 'STRING' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptests_i;", + ), + expErr: "operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d >= b from binoptests_i;", + ), + expErr: "operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d < b from binoptests_i;", + ), + expErr: "operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d > b from binoptests_i;", + ), + expErr: "operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d & b from binoptests_i;", + ), + expErr: "operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d | b from binoptests_i;", + ), + expErr: "operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d << b from binoptests_i;", + ), + expErr: "operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d >> b from binoptests_i;", + ), + expErr: "operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d + b from binoptests_i;", + ), + expErr: "operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d - b from binoptests_i;", + ), + expErr: "operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d * b from binoptests_i;", + ), + expErr: "operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d / b from binoptests_i;", + ), + expErr: "operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d % b from binoptests_i;", + ), + expErr: "operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d || b from binoptests_i;", + ), + expErr: "operator '||' incompatible with type 'INT'", + }, + }, +} + +var binOpExprWithStringBool = tableTest{ + table: tbl( + "binoptests_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + srcHdr("d", fldTypeString), + ), + srcRows( + srcRow(int64(10), bool(true), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptests_b;", + ), + expErr: "types 'STRING' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptests_b;", + ), + expErr: "types 'STRING' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptests_b;", + ), + expErr: "operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d >= b from binoptests_b;", + ), + expErr: "operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d < b from binoptests_b;", + ), + expErr: "operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d > b from binoptests_b;", + ), + expErr: "operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d & b from binoptests_b;", + ), + expErr: "operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d | b from binoptests_b;", + ), + expErr: "operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d << b from binoptests_b;", + ), + expErr: "operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d >> b from binoptests_b;", + ), + expErr: "operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d + b from binoptests_b;", + ), + expErr: "operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d - b from binoptests_b;", + ), + expErr: "operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d * b from binoptests_b;", + ), + expErr: "operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d / b from binoptests_b;", + ), + expErr: "operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d % b from binoptests_b;", + ), + expErr: "operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d || b from binoptests_b;", + ), + expErr: "operator '||' incompatible with type 'BOOL'", + }, + }, +} + +var binOpExprWithStringID = tableTest{ + table: tbl( + "binoptests_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeID), + srcHdr("d", fldTypeString), + ), + srcRows( + srcRow(int64(10), int64(20), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptests_id;", + ), + expErr: "types 'STRING' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptests_id;", + ), + expErr: "types 'STRING' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptests_id;", + ), + expErr: "operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d >= b from binoptests_id;", + ), + expErr: "operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d < b from binoptests_id;", + ), + expErr: "operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d > b from binoptests_id;", + ), + expErr: "operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d & b from binoptests_id;", + ), + expErr: "operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d | b from binoptests_id;", + ), + expErr: "operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d << b from binoptests_id;", + ), + expErr: "operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d >> b from binoptests_id;", + ), + expErr: "operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d + b from binoptests_id;", + ), + expErr: "operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d - b from binoptests_id;", + ), + expErr: "operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d * b from binoptests_id;", + ), + expErr: "operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d / b from binoptests_id;", + ), + expErr: "operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d % b from binoptests_id;", + ), + expErr: "operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select d || b from binoptests_id;", + ), + expErr: "operator '||' incompatible with type 'ID'", + }, + }, +} + +var binOpExprWithStringDecimal = tableTest{ + table: tbl( + "binoptests_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeString), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), string("foo"), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptests_d;", + ), + expErr: "types 'STRING' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a = d from binoptests_d;", + ), + expErr: "types 'STRING' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a <= d from binoptests_d;", + ), + expErr: "operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= d from binoptests_d;", + ), + expErr: "operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < d from binoptests_d;", + ), + expErr: "operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > d from binoptests_d;", + ), + expErr: "operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & d from binoptests_d;", + ), + expErr: "operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | d from binoptests_d;", + ), + expErr: "operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << d from binoptests_d;", + ), + expErr: "operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> d from binoptests_d;", + ), + expErr: "operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + d from binoptests_d;", + ), + expErr: "operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - d from binoptests_d;", + ), + expErr: "operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * d from binoptests_d;", + ), + expErr: "operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / d from binoptests_d;", + ), + expErr: "operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % d from binoptests_d;", + ), + expErr: "operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || d from binoptests_d;", + ), + expErr: "operator '||' incompatible with type 'DECIMAL(2)'", + }, + }, +} + +var binOpExprWithStringTimestamp = tableTest{ + table: tbl( + "binoptests_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeString), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), string("foo"), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptests_ts;", + ), + expErr: "types 'STRING' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptests_ts;", + ), + expErr: "types 'STRING' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptests_ts;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= ts from binoptests_ts;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < ts from binoptests_ts;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > ts from binoptests_ts;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & ts from binoptests_ts;", + ), + expErr: "operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | ts from binoptests_ts;", + ), + expErr: "operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << ts from binoptests_ts;", + ), + expErr: "operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> ts from binoptests_ts;", + ), + expErr: "operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + ts from binoptests_ts;", + ), + expErr: "operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - ts from binoptests_ts;", + ), + expErr: "operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * ts from binoptests_ts;", + ), + expErr: "operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / ts from binoptests_ts;", + ), + expErr: "operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % ts from binoptests_ts;", + ), + expErr: "operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || ts from binoptests_ts;", + ), + expErr: "operator '||' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var binOpExprWithStringIDSet = tableTest{ + table: tbl( + "binoptests_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeString), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), string("foo"), []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptests_ids;", + ), + expErr: "types 'STRING' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptests_ids;", + ), + expErr: "types 'STRING' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptests_ids;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptests_ids;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptests_ids;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptests_ids;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptests_ids;", + ), + expErr: " operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | b from binoptests_ids;", + ), + expErr: " operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << b from binoptests_ids;", + ), + expErr: " operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> b from binoptests_ids;", + ), + expErr: " operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + b from binoptests_ids;", + ), + expErr: " operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - b from binoptests_ids;", + ), + expErr: " operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * b from binoptests_ids;", + ), + expErr: " operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / b from binoptests_ids;", + ), + expErr: " operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % b from binoptests_ids;", + ), + expErr: " operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || b from binoptests_ids;", + ), + expErr: "operator '||' incompatible with type 'IDSET'", + }, + }, +} + +var binOpExprWithStringString = tableTest{ + table: tbl( + "binoptests_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeString), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), string("foo"), string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptests_s;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = b from binoptests_s;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= b from binoptests_s;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptests_s;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptests_s;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptests_s;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptests_s;", + ), + expErr: " operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | b from binoptests_s;", + ), + expErr: " operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << b from binoptests_s;", + ), + expErr: " operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> b from binoptests_s;", + ), + expErr: " operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + b from binoptests_s;", + ), + expErr: " operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - b from binoptests_s;", + ), + expErr: " operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * b from binoptests_s;", + ), + expErr: " operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / b from binoptests_s;", + ), + expErr: " operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % b from binoptests_s;", + ), + expErr: " operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || b from binoptests_s;", + ), + expHdrs: hdrs( + hdr("", fldTypeString), + ), + expRows: rows( + row(string("foo101")), + ), + compare: compareExactUnordered, + }, + }, +} + +var binOpExprWithStringStringSet = tableTest{ + table: tbl( + "binoptests_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeString), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), string("foo"), []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptests_ss;", + ), + expErr: "types 'STRING' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptests_ss;", + ), + expErr: "types 'STRING' and 'STRINGSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptests_ss;", + ), + expErr: " operator '<=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >= b from binoptests_ss;", + ), + expErr: " operator '>=' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a < b from binoptests_ss;", + ), + expErr: " operator '<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a > b from binoptests_ss;", + ), + expErr: " operator '>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a & b from binoptests_ss;", + ), + expErr: " operator '&' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a | b from binoptests_ss;", + ), + expErr: " operator '|' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a << b from binoptests_ss;", + ), + expErr: " operator '<<' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a >> b from binoptests_ss;", + ), + expErr: " operator '>>' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a + b from binoptests_ss;", + ), + expErr: " operator '+' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a - b from binoptests_ss;", + ), + expErr: " operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a * b from binoptests_ss;", + ), + expErr: " operator '*' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a / b from binoptests_ss;", + ), + expErr: " operator '/' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a % b from binoptests_ss;", + ), + expErr: " operator '%' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select a || b from binoptests_ss;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +//STRINGSET bin op tests +var binOpExprWithStringSetInt = tableTest{ + table: tbl( + "binoptestss_i", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeInt), + srcHdr("d", fldTypeStringSet), + ), + srcRows( + srcRow(int64(10), int64(20), []string{"20", "21"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestss_i;", + ), + expErr: "types 'STRINGSET' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestss_i;", + ), + expErr: "types 'STRINGSET' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestss_i;", + ), + expErr: "operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d >= b from binoptestss_i;", + ), + expErr: "operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d < b from binoptestss_i;", + ), + expErr: "operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d > b from binoptestss_i;", + ), + expErr: "operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d & b from binoptestss_i;", + ), + expErr: "operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d | b from binoptestss_i;", + ), + expErr: "operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d << b from binoptestss_i;", + ), + expErr: "operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d >> b from binoptestss_i;", + ), + expErr: "operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d + b from binoptestss_i;", + ), + expErr: "operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d - b from binoptestss_i;", + ), + expErr: "operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d * b from binoptestss_i;", + ), + expErr: "operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d / b from binoptestss_i;", + ), + expErr: "operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d % b from binoptestss_i;", + ), + expErr: "operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d || b from binoptestss_i;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetBool = tableTest{ + table: tbl( + "binoptestss_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeBool), + srcHdr("d", fldTypeStringSet), + ), + srcRows( + srcRow(int64(10), bool(true), []string{"20", "21"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestss_b;", + ), + expErr: "types 'STRINGSET' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestss_b;", + ), + expErr: "types 'STRINGSET' and 'BOOL' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestss_b;", + ), + expErr: "operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d >= b from binoptestss_b;", + ), + expErr: "operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d < b from binoptestss_b;", + ), + expErr: "operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d > b from binoptestss_b;", + ), + expErr: "operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d & b from binoptestss_b;", + ), + expErr: "operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d | b from binoptestss_b;", + ), + expErr: "operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d << b from binoptestss_b;", + ), + expErr: "operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d >> b from binoptestss_b;", + ), + expErr: "operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d + b from binoptestss_b;", + ), + expErr: "operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d - b from binoptestss_b;", + ), + expErr: "operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d * b from binoptestss_b;", + ), + expErr: "operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d / b from binoptestss_b;", + ), + expErr: "operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d % b from binoptestss_b;", + ), + expErr: "operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d || b from binoptestss_b;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetID = tableTest{ + table: tbl( + "binoptestss_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("b", fldTypeID), + srcHdr("d", fldTypeStringSet), + ), + srcRows( + srcRow(int64(10), int64(20), []string{"20", "21"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select d != b from binoptestss_id;", + ), + expErr: "types 'STRINGSET' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d = b from binoptestss_id;", + ), + expErr: "types 'STRINGSET' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select d <= b from binoptestss_id;", + ), + expErr: "operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d >= b from binoptestss_id;", + ), + expErr: "operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d < b from binoptestss_id;", + ), + expErr: "operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d > b from binoptestss_id;", + ), + expErr: "operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d & b from binoptestss_id;", + ), + expErr: "operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d | b from binoptestss_id;", + ), + expErr: "operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d << b from binoptestss_id;", + ), + expErr: "operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d >> b from binoptestss_id;", + ), + expErr: "operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d + b from binoptestss_id;", + ), + expErr: "operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d - b from binoptestss_id;", + ), + expErr: "operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d * b from binoptestss_id;", + ), + expErr: "operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d / b from binoptestss_id;", + ), + expErr: "operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d % b from binoptestss_id;", + ), + expErr: "operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select d || b from binoptestss_id;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetDecimal = tableTest{ + table: tbl( + "binoptestss_d", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeStringSet), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), []string{"20", "21"}, float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != d from binoptestss_d;", + ), + expErr: "types 'STRINGSET' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a = d from binoptestss_d;", + ), + expErr: "types 'STRINGSET' and 'DECIMAL(2)' are not equatable", + }, + { + sqls: sqls( + "select a <= d from binoptestss_d;", + ), + expErr: "operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= d from binoptestss_d;", + ), + expErr: "operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < d from binoptestss_d;", + ), + expErr: "operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > d from binoptestss_d;", + ), + expErr: "operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & d from binoptestss_d;", + ), + expErr: "operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | d from binoptestss_d;", + ), + expErr: "operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << d from binoptestss_d;", + ), + expErr: "operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> d from binoptestss_d;", + ), + expErr: "operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + d from binoptestss_d;", + ), + expErr: "operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - d from binoptestss_d;", + ), + expErr: "operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * d from binoptestss_d;", + ), + expErr: "operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / d from binoptestss_d;", + ), + expErr: "operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % d from binoptestss_d;", + ), + expErr: "operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || d from binoptestss_d;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetTimestamp = tableTest{ + table: tbl( + "binoptestss_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeStringSet), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), []string{"20", "21"}, time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != ts from binoptestss_ts;", + ), + expErr: "types 'STRINGSET' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a = ts from binoptestss_ts;", + ), + expErr: "types 'STRINGSET' and 'TIMESTAMP' are not equatable", + }, + { + sqls: sqls( + "select a <= ts from binoptestss_ts;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= ts from binoptestss_ts;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < ts from binoptestss_ts;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > ts from binoptestss_ts;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & ts from binoptestss_ts;", + ), + expErr: "operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | ts from binoptestss_ts;", + ), + expErr: "operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << ts from binoptestss_ts;", + ), + expErr: "operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> ts from binoptestss_ts;", + ), + expErr: "operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + ts from binoptestss_ts;", + ), + expErr: "operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - ts from binoptestss_ts;", + ), + expErr: "operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * ts from binoptestss_ts;", + ), + expErr: "operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / ts from binoptestss_ts;", + ), + expErr: "operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % ts from binoptestss_ts;", + ), + expErr: "operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || ts from binoptestss_ts;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetIDSet = tableTest{ + table: tbl( + "binoptestss_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeStringSet), + srcHdr("b", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), []string{"101", "103"}, []int64{101, 102}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestss_ids;", + ), + expErr: "types 'STRINGSET' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestss_ids;", + ), + expErr: "types 'STRINGSET' and 'IDSET' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestss_ids;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestss_ids;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptestss_ids;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptestss_ids;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptestss_ids;", + ), + expErr: " operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | b from binoptestss_ids;", + ), + expErr: " operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << b from binoptestss_ids;", + ), + expErr: " operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> b from binoptestss_ids;", + ), + expErr: " operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + b from binoptestss_ids;", + ), + expErr: " operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - b from binoptestss_ids;", + ), + expErr: " operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * b from binoptestss_ids;", + ), + expErr: " operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / b from binoptestss_ids;", + ), + expErr: " operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % b from binoptestss_ids;", + ), + expErr: " operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || b from binoptestss_ids;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetString = tableTest{ + table: tbl( + "binoptestss_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeStringSet), + srcHdr("b", fldTypeString), + ), + srcRows( + srcRow(int64(1), []string{"101", "102"}, string("101")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestss_s;", + ), + expErr: "types 'STRINGSET' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a = b from binoptestss_s;", + ), + expErr: "types 'STRINGSET' and 'STRING' are not equatable", + }, + { + sqls: sqls( + "select a <= b from binoptestss_s;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestss_s;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptestss_s;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptestss_s;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptestss_s;", + ), + expErr: " operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | b from binoptestss_s;", + ), + expErr: " operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << b from binoptestss_s;", + ), + expErr: " operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> b from binoptestss_s;", + ), + expErr: " operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + b from binoptestss_s;", + ), + expErr: " operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - b from binoptestss_s;", + ), + expErr: " operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * b from binoptestss_s;", + ), + expErr: " operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / b from binoptestss_s;", + ), + expErr: " operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % b from binoptestss_s;", + ), + expErr: " operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || b from binoptestss_s;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} + +var binOpExprWithStringSetStringSet = tableTest{ + table: tbl( + "binoptestss_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeStringSet), + srcHdr("b", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), []string{"102", "103"}, []string{"101", "102"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select a != b from binoptestss_ss;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a = b from binoptestss_ss;", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select a <= b from binoptestss_ss;", + ), + expErr: " operator '<=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >= b from binoptestss_ss;", + ), + expErr: " operator '>=' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a < b from binoptestss_ss;", + ), + expErr: " operator '<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a > b from binoptestss_ss;", + ), + expErr: " operator '>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a & b from binoptestss_ss;", + ), + expErr: " operator '&' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a | b from binoptestss_ss;", + ), + expErr: " operator '|' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a << b from binoptestss_ss;", + ), + expErr: " operator '<<' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a >> b from binoptestss_ss;", + ), + expErr: " operator '>>' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a + b from binoptestss_ss;", + ), + expErr: " operator '+' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a - b from binoptestss_ss;", + ), + expErr: " operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a * b from binoptestss_ss;", + ), + expErr: " operator '*' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a / b from binoptestss_ss;", + ), + expErr: " operator '/' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a % b from binoptestss_ss;", + ), + expErr: " operator '%' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select a || b from binoptestss_ss;", + ), + expErr: "operator '||' incompatible with type 'STRINGSET'", + }, + }, +} diff --git a/sql3/sql_defs_cast_test.go b/sql3/sql_defs_cast_test.go new file mode 100644 index 000000000..d144c9f41 --- /dev/null +++ b/sql3/sql_defs_cast_test.go @@ -0,0 +1,903 @@ +package sql3_test + +import ( + "time" + + "github.com/molecula/featurebase/v3/pql" +) + +func expectedCastTime() time.Time { + return time.Unix(1000, 0).UTC() +} + +var castIntLiteral = tableTest{ + table: tbl( + "", + nil, + nil, + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select cast(1 as int)", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select cast(1 as bool)", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select cast(0 as bool)", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select cast(1 as decimal(2))", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(pql.NewDecimal(100, 2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select cast(1 as id)", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select cast(1 as idset)", + ), + expErr: "'INT' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select cast(1 as string)", + ), + expHdrs: hdrs( + hdr("", fldTypeString), + ), + expRows: rows( + row(string("1")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select cast(1 as stringset)", + ), + expErr: "'INT' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select cast(1000 as timestamp)", + ), + expHdrs: hdrs( + hdr("", fldTypeTimestamp), + ), + expRows: rows( + row(time.Time(expectedCastTime())), + ), + compare: compareExactUnordered, + }, + }, +} + +var castInt = tableTest{ + table: tbl( + "cast_int", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(i1 as int) from cast_int", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(1000)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(i1 as bool) from cast_int", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(i1 as decimal(2)) from cast_int", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(int64(1), pql.NewDecimal(100000, 2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(i1 as id) from cast_int", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(1), int64(1000)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(i1 as idset) from cast_int", + ), + expErr: "'INT' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(i1 as string) from cast_int", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("1000")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(i1 as stringset) from cast_int", + ), + expErr: "'INT' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(i1 as timestamp) from cast_int", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeTimestamp), + ), + expRows: rows( + row(int64(1), time.Time(expectedCastTime())), + ), + compare: compareExactUnordered, + }, + }, +} + +var castBool = tableTest{ + table: tbl( + "cast_bool", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(b1 as int) from cast_bool", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(b1 as bool) from cast_bool", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(b1 as decimal(2)) from cast_bool", + ), + expErr: "'BOOL' cannot be cast to 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select _id, cast(b1 as id) from cast_bool", + ), + expErr: "'BOOL' cannot be cast to 'ID'", + }, + { + sqls: sqls( + "select _id, cast(b1 as idset) from cast_bool", + ), + expErr: "'BOOL' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(b1 as string) from cast_bool", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("true")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(b1 as stringset) from cast_bool", + ), + expErr: "'BOOL' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(b1 as timestamp) from cast_bool", + ), + expErr: "'BOOL' cannot be cast to 'TIMESTAMP'", + }, + }, +} + +var castDecimal = tableTest{ + table: tbl( + "cast_dec", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(d1 as int) from cast_dec", + ), + expErr: "'DECIMAL(2)' cannot be cast to 'INT'", + }, + { + sqls: sqls( + "select _id, cast(d1 as bool) from cast_dec", + ), + expErr: "'DECIMAL(2)' cannot be cast to 'BOOL'", + }, + { + sqls: sqls( + "select _id, cast(d1 as decimal(2)) from cast_dec", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(int64(1), pql.NewDecimal(1234, 2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(d1 as id) from cast_dec", + ), + expErr: "'DECIMAL(2)' cannot be cast to 'ID'", + }, + { + sqls: sqls( + "select _id, cast(d1 as idset) from cast_dec", + ), + expErr: "'DECIMAL(2)' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(d1 as string) from cast_dec", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("12.34")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(d1 as stringset) from cast_dec", + ), + expErr: "'DECIMAL(2)' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(d1 as timestamp) from cast_dec", + ), + expErr: "'DECIMAL(2)' cannot be cast to 'TIMESTAMP'", + }, + }, +} + +var castID = tableTest{ + table: tbl( + "cast_id", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(id1 as int) from cast_id", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(20)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(id1 as bool) from cast_id", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(id1 as decimal(2)) from cast_id", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(int64(1), pql.NewDecimal(2000, 2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(id1 as id) from cast_id", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(1), int64(20)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(id1 as idset) from cast_id", + ), + expErr: "'ID' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(id1 as string) from cast_id", + ), + expErr: "'ID' cannot be cast to 'STRING'", + }, + { + sqls: sqls( + "select _id, cast(id1 as stringset) from cast_id", + ), + expErr: "'ID' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(id1 as timestamp) from cast_id", + ), + expErr: "'ID' cannot be cast to 'TIMESTAMP'", + }, + }, +} + +var castIDSet = tableTest{ + table: tbl( + "cast_ids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(ids1 as int) from cast_ids", + ), + expErr: "'IDSET' cannot be cast to 'INT'", + }, + { + sqls: sqls( + "select _id, cast(ids1 as bool) from cast_ids", + ), + expErr: "'IDSET' cannot be cast to 'BOOL'", + }, + { + sqls: sqls( + "select _id, cast(ids1 as decimal(2)) from cast_ids", + ), + expErr: "'IDSET' cannot be cast to 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select _id, cast(ids1 as id) from cast_ids", + ), + expErr: "'IDSET' cannot be cast to 'ID'", + }, + { + sqls: sqls( + "select _id, cast(ids1 as idset) from cast_ids", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeIDSet), + ), + expRows: rows( + row(int64(1), []int64{101, 102}), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(ids1 as string) from cast_ids", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("[101 102]")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(ids1 as stringset) from cast_ids", + ), + expErr: "'IDSET' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(ids1 as timestamp) from cast_ids", + ), + expErr: "'IDSET' cannot be cast to 'TIMESTAMP'", + }, + }, +} + +var castString = tableTest{ + table: tbl( + "cast_string", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(s1 as int) from cast_string", + ), + expErr: "'foo' cannot be cast to 'INT'", + }, + { + sqls: sqls( + "select _id, cast('11' as int) from cast_string", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(11)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(s1 as bool) from cast_string", + ), + expErr: "'foo' cannot be cast to 'BOOL'", + }, + { + sqls: sqls( + "select _id, cast('true' as bool) from cast_string", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(s1 as decimal(2)) from cast_string", + ), + expErr: "'foo' cannot be cast to 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select _id, cast('12.34' as decimal(2)) from cast_string", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(int64(1), pql.NewDecimal(1234, 2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(s1 as id) from cast_string", + ), + expErr: "'foo' cannot be cast to 'ID'", + }, + { + sqls: sqls( + "select _id, cast('11' as id) from cast_string", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(1), int64(11)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(s1 as idset) from cast_string", + ), + expErr: "'STRING' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(s1 as string) from cast_string", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("foo")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(s1 as stringset) from cast_string", + ), + expErr: "'STRING' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(s1 as timestamp) from cast_string", + ), + expErr: "'foo' cannot be cast to 'TIMESTAMP'", + }, + { + sqls: sqls( + "select _id, cast('1970-01-01T00:16:40Z' as timestamp) from cast_string", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeTimestamp), + ), + expRows: rows( + row(int64(1), time.Time(expectedCastTime())), + ), + compare: compareExactUnordered, + }, + }, +} + +var castStringSet = tableTest{ + table: tbl( + "cast_ss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(ss1 as int) from cast_ss", + ), + expErr: "'STRINGSET' cannot be cast to 'INT'", + }, + { + sqls: sqls( + "select _id, cast(ss1 as bool) from cast_ss", + ), + expErr: "'STRINGSET' cannot be cast to 'BOOL'", + }, + { + sqls: sqls( + "select _id, cast(ss1 as decimal(2)) from cast_ss", + ), + expErr: "'STRINGSET' cannot be cast to 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select _id, cast(ss1 as id) from cast_ss", + ), + expErr: "'STRINGSET' cannot be cast to 'ID'", + }, + { + sqls: sqls( + "select _id, cast(ss1 as idset) from cast_ss", + ), + expErr: "'STRINGSET' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(ss1 as string) from cast_ss", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("[101 102]")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(ss1 as stringset) from cast_ss", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeStringSet), + ), + expRows: rows( + row(int64(1), []string{"101", "102"}), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(ss1 as timestamp) from cast_ss", + ), + expErr: "'STRINGSET' cannot be cast to 'TIMESTAMP'", + }, + }, +} + +var castTimestamp = tableTest{ + table: tbl( + "cast_ts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id, cast(t1 as int) from cast_ts", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(1351807721)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(t1 as bool) from cast_ts", + ), + expErr: "'TIMESTAMP' cannot be cast to 'BOOL'", + }, + { + sqls: sqls( + "select _id, cast(t1 as decimal(2)) from cast_ts", + ), + expErr: "'TIMESTAMP' cannot be cast to 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select _id, cast(t1 as id) from cast_ts", + ), + expErr: "'TIMESTAMP' cannot be cast to 'ID'", + }, + { + sqls: sqls( + "select _id, cast(t1 as idset) from cast_ts", + ), + expErr: "'TIMESTAMP' cannot be cast to 'IDSET'", + }, + { + sqls: sqls( + "select _id, cast(t1 as string) from cast_ts", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + expRows: rows( + row(int64(1), string("2012-11-01T22:08:41Z")), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, cast(t1 as stringset) from cast_ts", + ), + expErr: "'TIMESTAMP' cannot be cast to 'STRINGSET'", + }, + { + sqls: sqls( + "select _id, cast(t1 as timestamp) from cast_ts", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeTimestamp), + ), + expRows: rows( + row(int64(1), knownTimestamp()), + ), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_create_table_test.go b/sql3/sql_defs_create_table_test.go new file mode 100644 index 000000000..dceded5a7 --- /dev/null +++ b/sql3/sql_defs_create_table_test.go @@ -0,0 +1,48 @@ +package sql3_test + +var createTable = tableTest{ + name: "createTable", + sqlTests: []sqlTest{ + { + name: "keyPartitionsSetTo0", + sqls: sqls( + "create table foo (_id id, i1 int) keypartitions 0", + ), + expErr: "invalid value '0' for key partitions (should be a number between 1-10000)", + }, + { + name: "keyPartitionsSetTo10001", + sqls: sqls( + "create table foo (_id id, i1 int) keypartitions 10001", + ), + expErr: "invalid value '10001' for key partitions (should be a number between 1-10000)", + }, + { + name: "shardWidthSetTo0", + sqls: sqls( + "create table foo (_id id, i1 int) shardwidth 0", + ), + expErr: "invalid value '0' for shardwidth (should be a number that is a power of 2 and greater or equal to 2^16)", + }, + { + name: "shardWidthSetTo11", + sqls: sqls( + "create table foo (_id id, i1 int) shardwidth 11", + ), + expErr: "invalid value '11' for shardwidth (should be a number that is a power of 2 and greater or equal to 2^16)", + }, + { + name: "shardWidthSetTo11", + sqls: sqls( + "create table foo (_id id, i1 int) shardwidth 32", + ), + expErr: "invalid value '32' for shardwidth (should be a number that is a power of 2 and greater or equal to 2^16)", + }, + { + name: "shardWidthSetTo131072", + sqls: sqls( + "create table foo (_id id, i1 int) shardwidth 131072", + ), + }, + }, +} diff --git a/sql3/sql_defs_date_functions_test.go b/sql3/sql_defs_date_functions_test.go new file mode 100644 index 000000000..359f4a8b4 --- /dev/null +++ b/sql3/sql_defs_date_functions_test.go @@ -0,0 +1,187 @@ +package sql3_test + +// datepart tests +var datePartTests = tableTest{ + + table: tbl( + "dateparttests", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(10), int64(100), knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select datepart()", + ), + expErr: "count of formal parameters (2) does not match count of actual parameters (0)", + }, + { + sqls: sqls( + "select datepart(1, 2)", + ), + expErr: "an expression of type 'INT' cannot be passed to a parameter of type 'STRING'", + }, + { + sqls: sqls( + "select datepart('1', 2)", + ), + expErr: "an expression of type 'INT' cannot be passed to a parameter of type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select datepart('1', current_timestamp)", + ), + expErr: "invalid value '1' for parameter 'interval'", + }, + { + sqls: sqls( + "select _id, datepart('yy', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(2012)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('yd', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(306)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('m', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(11)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('d', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('w', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(4)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('wk', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(44)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('hh', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(22)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('mi', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(8)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('s', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(41)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('ms', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(0)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select _id, datepart('ns', ts) from dateparttests", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1), int64(0)), + ), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_groupby_test.go b/sql3/sql_defs_groupby_test.go new file mode 100644 index 000000000..1f4ad4d2f --- /dev/null +++ b/sql3/sql_defs_groupby_test.go @@ -0,0 +1,158 @@ +package sql3_test + +import ( + "github.com/molecula/featurebase/v3/pql" + "github.com/molecula/featurebase/v3/sql3/parser" +) + +//groupby tests +var groupByTests = tableTest{ + table: tbl( + "groupby_test", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("d1", fldTypeDecimal2), + srcHdr("s1", fldTypeString), + srcHdr("i2", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), int64(10), float64(10), string("10"), int64(100)), + srcRow(int64(2), int64(10), float64(10), string("10"), int64(200)), + srcRow(int64(3), int64(11), float64(11), string("11"), nil), + srcRow(int64(4), int64(12), float64(12), string("12"), nil), + srcRow(int64(5), int64(12), float64(12), string("12"), nil), + srcRow(int64(6), int64(13), float64(13), string("13"), nil), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "SELECT COUNT(*), i1 FROM groupby_test group by i1", + "SELECT COUNT(_id), i1 FROM groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + hdr("i1", fldTypeInt), + ), + expRows: rows( + row(int64(2), int64(10)), + row(int64(1), int64(11)), + row(int64(2), int64(12)), + row(int64(1), int64(13)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(distinct i2) AS count_rows, i1 FROM groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + hdr("i1", fldTypeInt), + ), + expRows: rows( + row(int64(2), int64(10)), + row(int64(0), int64(11)), + row(int64(0), int64(12)), + row(int64(0), int64(13)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT sum(i2) AS sum_rows, i1 FROM groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("sum_rows", fldTypeInt), + hdr("i1", fldTypeInt), + ), + expRows: rows( + row(int64(300), int64(10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "SELECT COUNT(*) FROM groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(2)), + row(int64(1)), + row(int64(2)), + row(int64(1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select count(distinct i2) AS count_rows, sum(i2) as sum_rows, i1 from groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("count_rows", fldTypeInt), + hdr("sum_rows", fldTypeInt), + hdr("i1", fldTypeInt), + ), + expRows: rows( + row(int64(2), int64(300), int64(10)), + row(int64(0), nil, int64(11)), + row(int64(0), nil, int64(12)), + row(int64(0), nil, int64(13)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select avg(i1) as avg_rows, i1 from groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("avg_rows", parser.NewDataTypeDecimal(4)), + hdr("i1", fldTypeInt), + ), + expRows: rows( + row(pql.NewDecimal(100000, 4), int64(10)), + row(pql.NewDecimal(110000, 4), int64(11)), + row(pql.NewDecimal(120000, 4), int64(12)), + row(pql.NewDecimal(130000, 4), int64(13)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select avg(d1) as avg_rows, i1 from groupby_test group by i1", + ), + expHdrs: hdrs( + hdr("avg_rows", parser.NewDataTypeDecimal(4)), + hdr("i1", fldTypeInt), + ), + expRows: rows( + row(pql.NewDecimal(100000, 4), int64(10)), + row(pql.NewDecimal(110000, 4), int64(11)), + row(pql.NewDecimal(120000, 4), int64(12)), + row(pql.NewDecimal(130000, 4), int64(13)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select percentile(i1, 0) as p_rows, i1 from groupby_test group by i1", + ), + expErr: "aggregate 'PERCENTILE()' not allowed in GROUP BY", + }, + { + sqls: sqls( + "select min(i1) as p_rows, i1 from groupby_test group by i1", + ), + expErr: "aggregate 'MIN()' not allowed in GROUP BY", + }, + { + sqls: sqls( + "select max(i1) as p_rows, i1 from groupby_test group by i1", + ), + expErr: "aggregate 'MAX()' not allowed in GROUP BY", + }, + }, +} diff --git a/sql3/sql_defs_in_test.go b/sql3/sql_defs_in_test.go new file mode 100644 index 000000000..e07f05f4c --- /dev/null +++ b/sql3/sql_defs_in_test.go @@ -0,0 +1,263 @@ +package sql3_test + +//IN tests +var inTests = tableTest{ + table: tbl( + "in_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id in (1, 10) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i1 in (1, 1000) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b1 in (true, false) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d1 in (1.23, 4.56) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select id1 in (3, 7) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ids1 in ([101, 102], [456, 789]) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select s1 in ('foo', 'bar') from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ss1 in (['a', 'b'], ['101', '102']) from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select t1 in ('2010-11-01T22:08:41+00:00', '2013-11-01T22:08:41+00:00', '2012-11-01T22:08:41+00:00') from in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + }, +} + +//NOT IN tests +var notInTests = tableTest{ + table: tbl( + "not_in_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id not in (1, 10) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i1 not in (1, 1000) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b1 not in (true, false) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d1 not in (1.23, 4.56) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select id1 not in (3, 7) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ids1 not in ([101, 102], [456, 789]) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select s1 not in ('foo', 'bar') from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ss1 not in (['a', 'b'], ['101', '102']) from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select t1 not in ('2010-11-01T22:08:41+00:00', '2013-11-01T22:08:41+00:00', '2012-11-01T22:08:41+00:00') from not_in_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_like_test.go b/sql3/sql_defs_like_test.go new file mode 100644 index 000000000..c9bdc15a8 --- /dev/null +++ b/sql3/sql_defs_like_test.go @@ -0,0 +1,167 @@ +package sql3_test + +//LIKE tests +var likeTests = tableTest{ + table: tbl( + "like_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'ID'", + }, + { + sqls: sqls( + "select i1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'INT'", + }, + { + sqls: sqls( + "select b1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select id1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'ID'", + }, + { + sqls: sqls( + "select ids1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select s1 like '%f_' from like_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ss1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select t1 like '%f_' from like_all_types", + ), + expErr: "operator 'LIKE' incompatible with type 'TIMESTAMP'", + }, + }, +} + +//NOT LIKE tests +var notLikeTests = tableTest{ + table: tbl( + "not_like_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'ID'", + }, + { + sqls: sqls( + "select i1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'INT'", + }, + { + sqls: sqls( + "select b1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select d1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select id1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'ID'", + }, + { + sqls: sqls( + "select ids1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select s1 not like '%f_' from not_like_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ss1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select t1 not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'TIMESTAMP'", + }, + }, +} diff --git a/sql3/sql_defs_null_test.go b/sql3/sql_defs_null_test.go new file mode 100644 index 000000000..89a4c4476 --- /dev/null +++ b/sql3/sql_defs_null_test.go @@ -0,0 +1,277 @@ +package sql3_test + +//NULL tests +var nullTests = tableTest{ + table: tbl( + "null_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i", fldTypeInt, "min 0", "max 1000"), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1), nil, nil, nil, nil, nil, nil, nil, nil), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select id1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ids1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select s1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ss1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select t1 is null from null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + }, +} + +//NOT NULL tests +var notNullTests = tableTest{ + table: tbl( + "not_null_all_types", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i", fldTypeInt, "min 0", "max 1000"), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1), nil, nil, nil, nil, nil, nil, nil, nil), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(true)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select i1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select b1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select d1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select id1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ids1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select s1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select ss1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select t1 is not null from not_null_all_types", + ), + expHdrs: hdrs( + hdr("", fldTypeBool), + ), + expRows: rows( + row(bool(false)), + ), + compare: compareExactUnordered, + }, + }, +} diff --git a/sql3/sql_defs_set_functions_test.go b/sql3/sql_defs_set_functions_test.go new file mode 100644 index 000000000..39c05a6ac --- /dev/null +++ b/sql3/sql_defs_set_functions_test.go @@ -0,0 +1,302 @@ +package sql3_test + +// set literal tests +var setLiteralTests = tableTest{ + name: "selectwithsetliterals", + table: tbl( + "selectwithsetliterals", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("event", fldTypeStringSet), + srcHdr("ievent", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), int64(10), int64(100), []string{"POST"}, nil), + srcRow(int64(2), int64(20), int64(200), []string{"GET"}, nil), + srcRow(int64(3), int64(30), int64(300), []string{"GET", "POST"}, []int64{101}), + ), + ), + sqlTests: []sqlTest{ + { + // SetContainsSelectList + name: "set-contains-select-list", + sqls: sqls( + "select _id, setcontains(event, 'POST') from selectwithsetliterals", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), true), + row(int64(2), false), + row(int64(3), true), + ), + compare: compareExactUnordered, + }, + { + // SetContainsSelectListInt + name: "set-contains-select-list-int", + sqls: sqls( + "select _id, setcontains(ievent, 101) from selectwithsetliterals", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), nil), + row(int64(2), nil), + row(int64(3), true), + ), + compare: compareExactUnordered, + }, + { + // SetContainsWithLiteral + // SetContainsWithLiteralInt + // SetContainsWithLiteralAny + // SetContainsWithLiteralAnyInt + // SetContainsWithLiteralAll + // SetContainsWithLiteralAllInt + name: "set-contains-with-literal", + sqls: sqls( + "select _id, setcontains(['POST'], 'POST') from selectwithsetliterals", + "select _id, setcontains([101], 101) from selectwithsetliterals", + "select _id, setcontainsany(['POST'], ['POST']) from selectwithsetliterals", + "select _id, setcontainsany([101], [101]) from selectwithsetliterals", + "select _id, setcontainsall(['POST'], ['POST']) from selectwithsetliterals", + "select _id, setcontainsall([101], [101]) from selectwithsetliterals", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeBool), + ), + expRows: rows( + row(int64(1), true), + row(int64(2), true), + row(int64(3), true), + ), + compare: compareExactUnordered, + }, + }, +} + +// set function tests +var setFunctionTests = tableTest{ + name: "selectwithset", + table: tbl( + "selectwithset", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("event", fldTypeStringSet), + srcHdr("ievent", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), int64(10), int64(100), []string{"POST"}, []int64{101}), + srcRow(int64(2), int64(20), int64(200), []string{"GET"}, nil), + srcRow(int64(3), int64(30), int64(300), []string{"GET", "POST"}, nil), + ), + ), + sqlTests: []sqlTest{ + { + name: "set-contains", + sqls: sqls( + "select * from selectwithset where setcontains(event, 'POST')", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a", fldTypeInt), + hdr("b", fldTypeInt), + hdr("event", fldTypeStringSet), + hdr("ievent", fldTypeIDSet), + ), + expRows: rows( + row(int64(1), int64(10), int64(100), []string{"POST"}, []int64{101}), + row(int64(3), int64(30), int64(300), []string{"POST", "GET"}, nil), + ), + compare: compareExactUnordered, + }, + { + // SetContains + + sqls: sqls( + "select * from selectwithset where setcontains(event, 'POST')", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a", fldTypeInt), + hdr("b", fldTypeInt), + hdr("event", fldTypeStringSet), + hdr("ievent", fldTypeIDSet), + ), + expRows: rows( + row(int64(1), int64(10), int64(100), []string{"POST"}, []int64{101}), + row(int64(3), int64(30), int64(300), []string{"POST", "GET"}, nil), + ), + compare: compareExactUnordered, + }, + { + // SetContainsInt + name: "set-contains-int", + sqls: sqls( + "select * from selectwithset where setcontains(ievent, 101)", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a", fldTypeInt), + hdr("b", fldTypeInt), + hdr("event", fldTypeStringSet), + hdr("ievent", fldTypeIDSet), + ), + expRows: rows( + row(int64(1), int64(10), int64(100), []string{"POST"}, []int64{101}), + ), + compare: compareExactUnordered, + }, + { + // SetContainsOrSetContains + // SetContainsAny + name: "set-contains-or-set-contains", + sqls: sqls( + "select * from selectwithset where setcontains(event, 'POST') or setcontains(event, 'GET')", + "select * from selectwithset where setcontainsany(event, ['POST', 'GET'])", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a", fldTypeInt), + hdr("b", fldTypeInt), + hdr("event", fldTypeStringSet), + hdr("ievent", fldTypeIDSet), + ), + expRows: rows( + row(int64(1), int64(10), int64(100), []string{"POST"}, []int64{101}), + row(int64(2), int64(20), int64(200), []string{"GET"}, nil), + row(int64(3), int64(30), int64(300), []string{"POST", "GET"}, nil), + ), + compare: compareExactUnordered, + }, + { + // SetContainsAndSetContains + // SetContainsAll + name: "set-contains-and-set-contains", + sqls: sqls( + "select * from selectwithset where setcontains(event, 'POST') and setcontains(event, 'GET')", + "select * from selectwithset where setcontainsall(event, ['POST', 'GET'])", + ), + expHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a", fldTypeInt), + hdr("b", fldTypeInt), + hdr("event", fldTypeStringSet), + hdr("ievent", fldTypeIDSet), + ), + expRows: rows( + row(int64(3), int64(30), int64(300), []string{"POST", "GET"}, nil), + ), + compare: compareExactUnordered, + }, + { + // SetContainsWrongType + name: "set-contains-wrong-type", + sqls: sqls( + "select * from selectwithset where setcontains(event, 1)", + ), + expErr: "types 'STRINGSET' and 'INT' are not equatable", + }, + { + // SetContainsWrongTypeInt + name: "set-contains-wrong-type-int", + sqls: sqls( + "select * from selectwithset where setcontains(ievent, 'foo')", + ), + expErr: "types 'IDSET' and 'STRING' are not equatable", + }, + { + // SetContainsWrongTypeSet + name: "set-contains-wrong-type-set", + sqls: sqls( + "select * from selectwithset where setcontains(event, ['foo'])", + ), + expErr: "types 'STRINGSET' and 'STRINGSET' are not equatable", + }, + }, +} + +// set parameter tests +var setParameterTests = tableTest{ + + table: tbl( + "selectwithsetparams", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + srcHdr("b", fldTypeInt, "min 0", "max 1000"), + srcHdr("event", fldTypeStringSet), + srcHdr("ievent", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), int64(10), int64(100), []string{"POST"}, []int64{101}), + srcRow(int64(2), int64(20), int64(200), []string{"GET"}, nil), + srcRow(int64(3), int64(30), int64(300), []string{"GET", "POST"}, nil), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select setcontains(['POST', 'GET'])", + ), + expErr: "count of formal parameters (2) does not match count of actual parameters (1)", + }, + { + sqls: sqls( + "select setcontains(1, 2)", + ), + expErr: "set expression expected", + }, + { + sqls: sqls( + "select setcontains(['POST', 'GET'], 1)", + ), + expErr: "types 'STRINGSET' and 'INT' are not equatable", + }, + { + sqls: sqls( + "select setcontains([1, 2], '1')", + ), + expErr: "types 'IDSET' and 'STRING' are not equatable", + }, + + { + sqls: sqls( + "select setcontainsall(['POST', 'GET'])", + "select setcontainsany(['POST', 'GET'])", + ), + expErr: "count of formal parameters (2) does not match count of actual parameters (1)", + }, + { + sqls: sqls( + "select setcontainsall(1, 2)", + "select setcontainsany(1, 2)", + ), + expErr: "set expression expected", + }, + { + sqls: sqls( + "select setcontainsall(['POST', 'GET'], [1, 2])", + "select setcontainsany(['POST', 'GET'], [1, 2])", + ), + expErr: "types 'STRING' and 'ID' are not equatable", + }, + { + sqls: sqls( + "select setcontainsall([1, 2], ['1', '2'])", + "select setcontainsany([1, 2], ['1', '2'])", + ), + expErr: "types 'ID' and 'STRING' are not equatable", + }, + }, +} diff --git a/sql3/sql_defs_timequantum_test.go b/sql3/sql_defs_timequantum_test.go new file mode 100644 index 000000000..c2f0f48de --- /dev/null +++ b/sql3/sql_defs_timequantum_test.go @@ -0,0 +1,53 @@ +package sql3_test + +//time quantum insert tests +var timeQuantumInsertTest = tableTest{ + table: tbl( + "time_quantum_insert", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("ids1", fldTypeIDSet, "timequantum 'YMD'"), + ), + srcRows(), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "insert into time_quantum_insert (_id, i1, ids1) values (1, 1, [1])", + ), + expHdrs: hdrs(), + expRows: rows(), + compare: compareExactUnordered, + }, + }, +} + +//time quantum query tests +var timeQuantumQueryTest = tableTest{ + table: tbl( + "timeQuantumQueryTest", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i1", fldTypeInt, "min 0", "max 1000"), + srcHdr("b1", fldTypeBool), + srcHdr("d1", fldTypeDecimal2), + srcHdr("id1", fldTypeID), + srcHdr("ids1", fldTypeIDSet), + srcHdr("s1", fldTypeString), + srcHdr("ss1", fldTypeStringSet), + srcHdr("t1", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), int64(1000), bool(true), float64(12.34), int64(20), []int64{101, 102}, string("foo"), []string{"101", "102"}, knownTimestamp()), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select _id not like '%f_' from not_like_all_types", + ), + expErr: "operator 'NOTLIKE' incompatible with type 'ID'", + }, + }, +} diff --git a/sql3/sql_defs_unops_test.go b/sql3/sql_defs_unops_test.go new file mode 100644 index 000000000..a38c35afc --- /dev/null +++ b/sql3/sql_defs_unops_test.go @@ -0,0 +1,315 @@ +package sql3_test + +import "time" + +var unaryOpExprWithInt = tableTest{ + table: tbl( + "unoptesti", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), int64(10)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -i from unoptesti;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(-10)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select !i from unoptesti;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(-11)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select +i from unoptesti;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(10)), + ), + compare: compareExactUnordered, + }, + }, +} + +var unaryOpExprWithBool = tableTest{ + table: tbl( + "unoptest_b", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("i", fldTypeBool), + ), + srcRows( + srcRow(int64(1), bool(false)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -i from unoptest_b;", + ), + expErr: "operator '-' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select !i from unoptest_b;", + ), + expErr: "operator '!' incompatible with type 'BOOL'", + }, + { + sqls: sqls( + "select +i from unoptest_b;", + ), + expErr: "operator '+' incompatible with type 'BOOL'", + }, + }, +} + +var unaryOpExprWithID = tableTest{ + table: tbl( + "unoptestid", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a", fldTypeInt, "min 0", "max 1000"), + ), + srcRows( + srcRow(int64(1), int64(10)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -_id from unoptestid;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(-1)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select !_id from unoptestid;", + ), + expHdrs: hdrs( + hdr("", fldTypeID), + ), + expRows: rows( + row(int64(-2)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select +_id from unoptestid;", + ), + expHdrs: hdrs( + hdr("", fldTypeInt), + ), + expRows: rows( + row(int64(1)), + ), + compare: compareExactUnordered, + }, + }, +} + +var unaryOpExprWithDecimal = tableTest{ + table: tbl( + "unoptestd", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("d", fldTypeDecimal2), + ), + srcRows( + srcRow(int64(1), float64(12.34)), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -d from unoptestd;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(-12.34)), + ), + compare: compareExactUnordered, + }, + { + sqls: sqls( + "select !d from unoptestd;", + ), + expErr: "operator '!' incompatible with type 'DECIMAL(2)'", + }, + { + sqls: sqls( + "select +d from unoptestd;", + ), + expHdrs: hdrs( + hdr("", fldTypeDecimal2), + ), + expRows: rows( + row(float64(12.34)), + ), + compare: compareExactUnordered, + }, + }, +} + +var unaryOpExprWithTimestamp = tableTest{ + table: tbl( + "unoptestts", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), time.Time(knownTimestamp())), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -ts from unoptestts;", + ), + expErr: "operator '-' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select !ts from unoptestts;", + ), + expErr: "operator '!' incompatible with type 'TIMESTAMP'", + }, + { + sqls: sqls( + "select +ts from unoptestts;", + ), + expErr: "operator '+' incompatible with type 'TIMESTAMP'", + }, + }, +} + +var unaryOpExprWithIDSet = tableTest{ + table: tbl( + "unoptestids", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("ids", fldTypeIDSet), + ), + srcRows( + srcRow(int64(1), []int64{11, 12, 13}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -ids from unoptestids;", + ), + expErr: "operator '-' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select !ids from unoptestids;", + ), + expErr: "operator '!' incompatible with type 'IDSET'", + }, + { + sqls: sqls( + "select +ids from unoptestids;", + ), + expErr: "operator '+' incompatible with type 'IDSET'", + }, + }, +} + +var unaryOpExprWithString = tableTest{ + table: tbl( + "unoptest_s", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("s", fldTypeString), + ), + srcRows( + srcRow(int64(1), string("foo")), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -s from unoptest_s;", + ), + expErr: "operator '-' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select !s from unoptest_s;", + ), + expErr: "operator '!' incompatible with type 'STRING'", + }, + { + sqls: sqls( + "select +s from unoptest_s;", + ), + expErr: "operator '+' incompatible with type 'STRING'", + }, + }, +} + +var unaryOpExprWithStringSet = tableTest{ + table: tbl( + "unoptestss", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("s", fldTypeStringSet), + ), + srcRows( + srcRow(int64(1), []string{"11", "12", "13"}), + ), + ), + sqlTests: []sqlTest{ + { + sqls: sqls( + "select -s from unoptestss;", + ), + expErr: "operator '-' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select !s from unoptestss;", + ), + expErr: "operator '!' incompatible with type 'STRINGSET'", + }, + { + sqls: sqls( + "select +s from unoptestss;", + ), + expErr: "operator '+' incompatible with type 'STRINGSET'", + }, + }, +} diff --git a/sql3/sql_test.go b/sql3/sql_test.go new file mode 100644 index 000000000..a8d638213 --- /dev/null +++ b/sql3/sql_test.go @@ -0,0 +1,472 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package sql3_test + +import ( + "context" + "fmt" + "log" + "strings" + "testing" + "time" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/sql3/parser" + planner_types "github.com/molecula/featurebase/v3/sql3/planner/types" + sql_test "github.com/molecula/featurebase/v3/sql3/test" + "github.com/molecula/featurebase/v3/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSQL_Execute(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + + ctx := context.Background() + api := c.GetNode(0).API + svr := c.GetNode(0).Server + + for i, test := range tableTests { + tableTestName := fmt.Sprintf("table-%d", i) + if test.name != "" { + tableTestName = test.name + } + t.Run(tableTestName, func(t *testing.T) { + + var err error + // Create a table with all field types. + if test.table.columns != nil { + _, _, err := sql_test.MustQueryRows(t, svr, test.table.createTable()) + assert.NoError(t, err) + } + + if len(test.table.rows) > 0 { + + // Populate fields with data. + qcx := api.Txf().NewQcx() + + // idIdx is the index position of the _id column. If a source provides the + // _id somewhere other than column 0, then we need to add logic here to find + // its index. + idIdx := 0 + for i, col := range test.table.columns { + if col.name == "_id" { + continue + } + + colIDs := make([]uint64, 0) + colKeys := make([]string, 0) + + addColID := func(v interface{}) { + switch id := v.(type) { + case uint64: + colIDs = append(colIDs, id) + case int64: + colIDs = append(colIDs, uint64(id)) + case string: + colKeys = append(colKeys, id) + default: + t.Fatalf("unexpected type for colid '%T'", v) + } + } + + switch col.typ.(type) { + case *parser.DataTypeInt: + vals := make([]int64, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + addColID(row[idIdx]) + vals = append(vals, row[i].(int64)) + } + if len(vals) == 0 { + continue + } + req := &pilosa.ImportValueRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + Values: vals, + } + + err = api.ImportValue(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeBool: + vals := make([]uint64, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + addColID(row[idIdx]) + if row[i].(bool) { + vals = append(vals, 1) + } else { + vals = append(vals, 0) + } + } + if len(vals) == 0 { + continue + } + req := &pilosa.ImportRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowIDs: vals, + } + + err = api.Import(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeDecimal: + vals := make([]float64, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + addColID(row[idIdx]) + vals = append(vals, row[i].(float64)) + } + if len(vals) == 0 { + continue + } + req := &pilosa.ImportValueRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + FloatValues: vals, + } + + err = api.ImportValue(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeIDSet: + rowIDs := make([]uint64, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + rowSet := row[i].([]int64) + for k := range rowSet { + addColID(row[idIdx]) + rowIDs = append(rowIDs, uint64(rowSet[k])) + } + } + if len(rowIDs) == 0 { + continue + } + req := &pilosa.ImportRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowIDs: rowIDs, + } + err = api.Import(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeID: + rowIDs := make([]uint64, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + addColID(row[idIdx]) + rowIDs = append(rowIDs, uint64(row[i].(int64))) + } + + if len(rowIDs) == 0 { + continue + } + req := &pilosa.ImportRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowIDs: rowIDs, + } + err = api.Import(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeString: + rowKeys := make([]string, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + addColID(row[idIdx]) + rowKeys = append(rowKeys, row[i].(string)) + } + + if len(rowKeys) == 0 { + continue + } + req := &pilosa.ImportRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowKeys: rowKeys, + } + err = api.Import(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeStringSet: + rowKeys := make([]string, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + rowSet := row[i].([]string) + for k := range rowSet { + addColID(row[idIdx]) + rowKeys = append(rowKeys, rowSet[k]) + } + } + + if len(rowKeys) == 0 { + continue + } + req := &pilosa.ImportRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + RowKeys: rowKeys, + } + err = api.Import(ctx, qcx, req) + assert.NoError(t, err) + + case *parser.DataTypeTimestamp: + vals := make([]time.Time, 0) + for _, row := range test.table.rows { + if row[i] == nil { + continue + } + addColID(row[idIdx]) + vals = append(vals, row[i].(time.Time)) + } + if len(vals) == 0 { + continue + } + req := &pilosa.ImportValueRequest{ + Index: test.table.name, + Field: col.name, + Shard: 0, + ColumnIDs: colIDs, + ColumnKeys: colKeys, + TimestampValues: vals, + } + + err = api.ImportValue(ctx, qcx, req) + assert.NoError(t, err) + + default: + t.Fatalf("column type not supported: %s", col.typ) + } + } + } + + for i, sqltest := range test.sqlTests { + sqlTestName := fmt.Sprintf("test-%d", i) + if sqltest.name != "" { + sqlTestName = sqltest.name + } + t.Run(sqlTestName, func(t *testing.T) { + for _, sql := range sqltest.sqls { + t.Run(fmt.Sprintf("sql-%s", sql), func(t *testing.T) { + log.Printf("SQL: %s", sql) + rows, headers, err := sql_test.MustQueryRows(t, svr, sql) + + // Check expected error instead of results. + if sqltest.expErr != "" { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), sqltest.expErr) + } + return + } + + require.NoError(t, err) + + // Check headers. + assert.ElementsMatch(t, sqltest.expHdrs, headers) + + // make a map of column name to header index + m := make(map[string]int) + for i := range headers { + m[headers[i].Name] = i + } + + // Put the expRows in the same column order as the headers returned + // by the query. + exp := make([][]interface{}, len(sqltest.expRows)) + for i := range sqltest.expRows { + exp[i] = make([]interface{}, len(headers)) + for j := range sqltest.expHdrs { + targetIdx := m[sqltest.expHdrs[j].Name] + if !assert.GreaterOrEqual(t, len(sqltest.expRows[i]), len(headers)) { + t.Fatalf("expected row set has fewer columns than returned headers") + } + exp[i][targetIdx] = sqltest.expRows[i][j] + } + } + + switch sqltest.compare { + case compareExactOrdered: + assert.EqualValues(t, len(sqltest.expRows), len(rows)) + assert.EqualValues(t, exp, rows) + case compareExactUnordered: + assert.EqualValues(t, len(sqltest.expRows), len(rows)) + assert.ElementsMatch(t, exp, rows) + case compareIncludedIn: + assert.EqualValues(t, sqltest.expRowCount, len(rows)) + for _, row := range rows { + assert.Contains(t, exp, row) + } + } + }) + } + }) + } + }) + } +} + +////////////////////////////////////////////////////////////////////// + +type fldType parser.ExprDataType + +// fldType constants are providing a map of a defined test type to the +// parser.ExprDataType +var ( + fldTypeID fldType = parser.NewDataTypeID() + fldTypeBool fldType = parser.NewDataTypeBool() + fldTypeIDSet fldType = parser.NewDataTypeIDSet() + fldTypeInt fldType = parser.NewDataTypeInt() + fldTypeDecimal2 fldType = parser.NewDataTypeDecimal(2) + fldTypeString fldType = parser.NewDataTypeString() + fldTypeStringSet fldType = parser.NewDataTypeStringSet() + fldTypeTimestamp fldType = parser.NewDataTypeTimestamp() +) + +type compareMethod string + +const ( + compareExactOrdered compareMethod = "exactOrdered" + compareExactUnordered compareMethod = "exactUnordered" + compareIncludedIn compareMethod = "includedIn" +) + +type tableTest struct { + name string + table source + sqlTests []sqlTest +} + +type sqlTest struct { + name string + sqls []string + expHdrs []*planner_types.PlannerColumn + expRows [][]interface{} + expErr string + compare compareMethod + expRowCount int +} + +// The following "source" types are helpers for creating a test table. +type sourceColumn struct { + name string + typ fldType + options string +} + +func tbl(name string, columns []sourceColumn, rows []sourceRow) source { + return source{ + name: name, + columns: columns, + rows: rows, + } +} + +func srcHdrs(hdrs ...sourceColumn) []sourceColumn { + return hdrs +} + +func srcHdr(name string, typ fldType, opts ...string) sourceColumn { + return sourceColumn{ + name: name, + typ: typ, + options: strings.Join(opts, " "), + } +} + +func srcRows(rows ...sourceRow) []sourceRow { + return rows +} +func srcRow(cells ...interface{}) sourceRow { + return cells +} + +type sourceRow []interface{} + +type source struct { + name string + columns []sourceColumn + rows []sourceRow +} + +func (s source) createTable() string { + ct := "CREATE TABLE " + s.name + " (" + + cols := []string{} + for _, col := range s.columns { + f := col.name + " " + col.typ.TypeName() + if col.options != "" { + f += " " + col.options + } + cols = append(cols, f) + } + ct += strings.Join(cols, ",") + + ct += `)` + + log.Printf("CREATE: %s", ct) + return ct +} + +// hdrs is just a helper function to make the test definition look cleaner. +func hdrs(hdrs ...*planner_types.PlannerColumn) []*planner_types.PlannerColumn { + return hdrs +} + +// hdr is just a helper function to make the test definition look cleaner. +func hdr(name string, typ fldType) *planner_types.PlannerColumn { + return &planner_types.PlannerColumn{ + Name: name, + Type: typ, + } +} + +// row helpers for expected results +func rows(rows ...[]interface{}) [][]interface{} { + return rows +} + +func row(cells ...interface{}) []interface{} { + return cells +} + +func sqls(sqls ...string) []string { + return sqls +} diff --git a/sql3/test/helpers.go b/sql3/test/helpers.go new file mode 100644 index 000000000..e1a9b6bf7 --- /dev/null +++ b/sql3/test/helpers.go @@ -0,0 +1,55 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package test + +import ( + "context" + "testing" + + pilosa "github.com/molecula/featurebase/v3" + planner_types "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// MustQueryRows returns the row results as a slice of []interface{}, along with the columns. +func MustQueryRows(tb testing.TB, svr *pilosa.Server, q string) ([][]interface{}, []*planner_types.PlannerColumn, error) { + tb.Helper() + + ctx := context.Background() + + stmt, err := svr.CompileExecutionPlan(ctx, q) + if err != nil { + return nil, nil, err + } + + ocolumns := stmt.Schema() + + rowIter, err := stmt.Iterator(ctx, nil) + if err != nil { + return nil, nil, err + } + results := make([][]interface{}, 0) + + next, err := rowIter.Next(ctx) + if err != nil && err != planner_types.ErrNoMoreRows { + return nil, nil, err + } + for err != planner_types.ErrNoMoreRows { + result := make([]interface{}, len(ocolumns)) + for i := range result { + result[i] = next[i] + } + results = append(results, result) + next, err = rowIter.Next(ctx) + if err != nil && err != planner_types.ErrNoMoreRows { + return nil, nil, err + } + } + //temporarily transform to Columns() + cols := make([]*planner_types.PlannerColumn, 0) + for _, oc := range ocolumns { + cols = append(cols, &planner_types.PlannerColumn{ + Name: oc.Name, + Type: oc.Type, + }) + } + return results, cols, nil +}