From 204558b46f627c45916a56cd23e26347fda3307f Mon Sep 17 00:00:00 2001 From: pokeeffe-molecula <85502298+pokeeffe-molecula@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:01:05 -0500 Subject: [PATCH] added /sql endpoint; implemented SHOW TABLES (#1935) * squashed 45 commits into one :) * tlt/sql experiment (#2035) * Move PlanOperator to sql3/planner/types package includes: type PlanOperatorColumn struct type PlanOperator interface * Remove planner dependencies from pilosa package The goal after this is to prevent the planner package (which doesn't exist yet) from being imported by the pilosa package; we just want it injected into the server in server/server.go. This is because the planner package uses pilosa types, so we need to avoid circular dependencies. Added ExecutionPlannerFn Make public: pilosa.ExecOptions Added a pilosa.Executor interface Added a planner.types.CompilePlanner interface Isolated the planner calls to: - Executor.Execute() - *API.[method]() * Move executionplanner files into the sql3/planner package. This required a bit of gymnastics, and there are some things around FieldOptions which need to be addressed soon. * Remove the hacky FieldOptions stuff I added earlier This implementation just uses the pilosa.FieldOption functional options provided by the API (as opposed to trying to build a FieldOptions object. It also changes field types to constants. These are private for now, but if we need to make them public, we should put them in the planner/types package. * Implement the "scale" value from Decimal(scale) Also, precision and scale were currently reversed in the parser. This fixes that. * Modify the parser to handle CACHETYPE SIZE It's a little odd to me that the cache type values are Tokens, but I guess it's ok. One thing to keep in mind is that FeatureBase expects lowercase values, so this commit changes the parser to set the value to the lowercase version of the type. * Fix the /sql2 tests This entailed a combination of commenting out or t.Skip()-ing tests which covered code in the parser that has been commented out or removed as not currently supported in sql3. It also adds some coverage for the sql.Contraint stringers. * Prevent JSON sql results from containing closing commas This commit just re-works the existing output code to avoid inserting closing commas (which results in invalid JSON). * Enhance the CREATE TABLE test coverage. In particular, ensure that the fields which get created in FeatureBase are what we expect based on the fields defined in the CREATE TABLE statement. This also ensures that the TIMEQUANTUM and CACHETYPE contraints are not provided for the same field (since those constraints are not supported together). * Adjust the EBNF file to indicate SIZE contraint is optional A CACHETYPE can be provided without a SIZE. This change indicates that SIZE is optional. * Remove `executionplanner_` from file names (#2040) * implementation of ALTER TABLE (sans column RENAME) * refactored expression analysis; added more robust type checking; all unary and bin ops function on ints * added type support for expressions; full bin/unary op support; added cast; more literal support * cast int to all other types * all literals (except idset, stringset & timestamp) make it thru; cast to all types with int as source now works * implemented LIKE/NOT LIKE * Implemented IS [NOT] NULL * Move sql2 files into sql3/parser package (#2045) * Move sql2 files into sql3/parser package This also removes the sql2 package. * Fix tests which were typing _id fields as INT intead of ID * implemented BETWEEN, NOT BETWEEN * Add featurebase/error package (#2046) * Add featurebase/error package I copied the `dax/errors` package which I am starting to use in the DAX prototype into `featurebase/errors` in order to start using it with the sql3 package. It's basically a wrapper around `github.com/pkg/errors`, but it uses a customer coded error. The sql package can define its own errors based on the `featurebase/errors` types. Then do things like `Wrap()` and `Is()`. * Address the linter complaints: shadowed variables, unreachable code * implemented IN & NOT IN with expression lists * first cut of CASE * Fixed some errors from rebase * updated bnf; removed unused code; tightened up error handling * first crack at basic CLI for SQL3 Use: `featurebase cli` Still lots to do here, but for example: > select count(*) from tremor +--------------+ | COUNT | +--------------+ | 1.158321e+06 | +--------------+ * Iterate on the CLI (#2057) Handle the errors. Add an "exit" command. Add some general formatting and white space. Add termination character: ";" (semicolon) This commit allows a user to provide multiple or partial SQL statements. Example of multiple statements: ``` show tables; select * from foo; ``` Example of partial (multi-line) statements: ``` select * from foo; ``` Don't uppercase the header values * error refactoring; first cut of TOP; remove unused code; use log.Printf instead of fmt.Printf * fixed a bug with QualifiedRef from refactoring; added bones of INSERT; removal of unused code; tightened up errors more; fixed failing tests * single value list for INSERT * Update bnf per discussion with Travis; INSERT now doing the requisite stuff * Pat's eyes went square - nothing wrong with TOP, Pat needed to learn arrays again. * improved some errors; fixed tests to suit * send warnings back in the api; update CLI to display warnings * start warning on stuff not implemented so we don't get bugged about it * Tlt/sql experiment (#2063) * Expresssion -> Expression * Add SQL planner test - adds a test to which it is easier to add tables and SQL statments - un-exports all of the expression types - removes the planner pointer from the expression types (it can be added back later if need be) * Fix where clause on a string field Prior to this commit, the binary expression for a where clause on a string field was building the call by providing a range operator which is typically used for BSI fields. This changes it to use the call.Args for string values. * Update planner tests to handle multiple sql for the same results * Reorganize SQL tests Introduce a test/helpers package and move shared MustQueryRows into that package. * Add a compatibility map for field types. (#2064) This is primarily to address the fact that ID fields were previously incompatible with INT literals. We should probably consider introducing a custom type for FieldType which can be used to define compatibilities. * significantly refactored type checking * Handle nil (NULL) values in the sql CLI. (#2067) go-pretty panics if the interface{} field value is nil. This replaces nil values with a "NULL" string. * Squash some commits fixed a still failing test added line, col to all error messages refactored source handling to enable table aliases fixed some copypasta per review warnings for order by & topn; implemented select as a source starting to handle in (select...); added stub for optimizer JSON-encode the sql error and warning strings (#2069) Error strings with unencoded characters (like double quotes) were resulting in invalid json. got insert working; added symbol table; added concrete optimizer; added nascent NestedLoopsOperator; rewrite "where foo in (select..." as inner join * all about the sets (#2085) * implemented setcontains() * implemented set literal; insert set column values; setcontains/all/any both in expr eval and pql filters * Convert test to use latest framework. (#2086) * fixed some comments * removed refactored tests Co-authored-by: Travis Turner * Add support for Decimal fields to the sql test. (#2090) * dates (#2094) * return dates as strings in output; tightened up decimal type checking * return dates as strings in output; tightened up decimal type checking * fixed failing tests after decimal changes * can now insert decimal values * implemented insert for timestamp data type; implemented current_date, current_timestamp constants * fixed some failing tests * handle date literals from strings in insert statements * changes from feedback * Fix pointer method error * sql3 API interface (#2110) * Introduce API-related interfaces: SchemaAPI, ComputeAPI The sql3 code was relying on the pointer: *pilosa.API in order to call API methods directly on the local node. If we want to import and use the sql3 package in another service (the DAX queryer, for example), we need to be able to use an implementation of an interface for those API method calls. This commit introduces two interfaces, both automatically implemented by pilosa.API: - SchemaAPI - ComputeAPI * Convert sql3 code to use IndexInfo instead of Index The sql3 code was relying on a *pilosa.Index and its methods to get general information like index and field name, type, etc. This commit converts everything to use a *pilosa.IndexInfo instead. This allows us to modify the SchemaAPI interface to also return IndexInfo instead of Index, which will be a lot easier to implement in a non-pilosa package (like DAX); creating a *pilosa.Index requires providing things like data directory paths and holders, which are not necessary for these use cases. * Unary and Binary Ops R US plus CAST (#2111) * implemented string literal for timestamp epoch * fixed failing test * fixed the failing test again * refactored tests; implemented unary op tests for all datatypes; implemented binop tests for int/int, int/id, int/decimal & ID/int * implemented all binary ops for INT & all other types, ID & all other types * implemented binary ops for DECIMAL types & all other types * added STRING & BOOL to various tests; implemented all remaining binOp tests * fix up some stuff after rebasing * refactored test defs into multiple files; implemented CAST for every datatype * added tests for like/not like * addressed review feedback * addressed type review feedback * tightened up IS [NOT] NULL behavior plus tests (#2118) * tightened up IS [NOT] NULL behavior plus tests * BETWEEN/NOT BETWEEN with all data types * addressed review feedback * Handle negative integers in column min/max constraints (#2120) This commit parses the min/max contraint as an expression, as opposed to an int literal, so that negative values are treated as Unary expressions. There currently isn't support for min/max constraints on `decimal` fiels, so for now this change only expects +/- integer values. * Implement the CREATE TABLE keypartitions logic (#2123) * Execution time, IN/NOT IN & multiple aggregates (#2124) * added display of execution time * IN/NOT IN tests for all data types * fixed date parsing * removed duplicative tests * refactoring aggregates * suport multiple aggregates * Address review feedback * final round of feedback * Add method SchemaAPI.CreateIndexAndFields() (#2127) In order to support a CREATE TABLE statement as a single command, this commit alters the SchemaAPI interface to contain a single method which handles both the index and its fields. It also updates the sql3 code to use this interface instead of CreateIndex() and CreateField() indepedently. * Symbol Handling (Again) (#2129) * Refactored symbol handling in the planner; re-instated the select as source tests * removed commented out code * addressing review feedback * Move hard-coded _id field out of planner and into interface implementation (#2130) This commit moves the hard-coded addition of the `_id` field from the planner to the SchemaAPI.IndexInfo() implementation method. NOTE: If anything was expecting SchemaAPI.Schema() to also return the `_id` field as part of its field list in each table, then it would not be there because the `_id` field is only added in the IndexInfo() method for now. Currently that's not a problem because nothing is expecting the `_id` field for `Schema()`. * Multiple aggregates, all aggregates stand alone and in GROUP BY (#2132) * handle multiple aggregates in group by queries * added handling for avg() aggregate both stand alone and in group by * tightened up sum & avg outside of group by * added min, max & percentile * added warnings * Make MaterializedRowSet implement the PlanOperator interface. (#2133) This commit refactors the PQLMultiGroupByOperator to have a PlanOperator as its output. Then, when it initializes, it sets up a MaterializedRowSet and populates that with the values from the multiple group by operations. * added explicit min/max pql operators * saved a file I forgot to save * per review * Un-indent some if/else nesting (#2136) Co-authored-by: Travis Turner * Add optional `name` argument to test structs. This commit adds the `name` argument to `tableTest` and `sqlTest` so that a test can be optionally named. This allows a developer to more easily run/identify a particular test by name. * Inbuilt functions (redux) (#2141) * set functions type parameter type checking * implemented datepart * include SQL3 type in SHOW COLUMNS output * fixed select as source; failing SHOW COLUMNS test * select in select list * dump output columns; handle optimization for select list subqueries * make it an error to return multiple rows for a select list subquery * added description * contants and test coverage for datepart function * SQL3 Refactor-palooza (#2182) * removed unneeded IsAggregate() * first cut of working nested loops operator aka INNER JOIN * remove selectListItemPlanExpression * added some warnings * all the tests are passing again! * addressed some linter complaints * added basic order by * bug fixes; added 'or replace'/'replace' to insert * for insert references should return appropriately * added back ability to use subquery singleton expressions * removed dead code; fixed test * json-able plan, Schema() plus refactoring * fixed dumb code * add some tests for time quantum behavior * Code cleanup during review. Also fixed INSERT to keyed table bug. This commit contains a lot of minor adjustments made during code review. It also contains a bug fix that was preventing INSERT into a keyed table (i.e. _id type STRING) from working. Co-authored-by: Travis Turner * Fix expected min/max on timestamp column test (decimal field) I don't know why this changed, but presumably something to do with decimal related work that happened on master. * Fix compile problem after rebase * review feedback Co-authored-by: Matthew Jaffee Co-authored-by: Travis Turner Co-authored-by: Travis Turner Co-authored-by: Fletcher Haynes --- .gitignore | 66 + Makefile | 3 + api.go | 135 +- cmd/cli.go | 31 + cmd/featurebase-parse-sql/main.go | 4 +- cmd/root.go | 1 + ctl/cli.go | 246 + ctl/server.go | 10 +- errors/errors.go | 85 + errors/errors_test.go | 81 + executor.go | 198 +- go.mod | 15 +- go.sum | 26 +- http_handler.go | 183 + index.go | 12 + pg/cancel.go | 125 - pg/cancel_test.go | 66 - pg/io.go | 237 - pg/lookerToFeaturebase.md | 652 -- pg/lookerToPostgres.md | 677 -- pg/message/io.go | 112 - pg/message/message.go | 532 -- pg/pgtest/handler.go | 113 - pg/pgtest/memnet.go | 59 - pg/pgtest/server.go | 118 - pg/pgtest/tls.go | 89 - pg/protocol.go | 1101 ---- pg/query.go | 120 - pg/server.go | 133 - pg/server_test.go | 283 - pg/type.go | 63 - planner.go | 1330 ---- planner_test.go | 531 -- server.go | 42 +- server/config.go | 33 +- server/pg.go | 665 -- server/pg_internal_test.go | 54 - server/pg_test.go | 314 - server/server.go | 69 +- sql2/ast_test.go | 1149 ---- sql2/scanner_test.go | 168 - sql2/token_test.go | 15 - sql3/errors.go | 501 ++ sql3/interfaces.go | 27 + {sql2 => sql3/parser}/ast.go | 1416 +++-- sql3/parser/ast_test.go | 1246 ++++ sql3/parser/astdatatype.go | 182 + {sql2 => sql3/parser}/parser.go | 885 ++- {sql2 => sql3/parser}/parser_test.go | 2135 ++++--- {sql2 => sql3/parser}/scanner.go | 35 +- sql3/parser/scanner_test.go | 160 + {sql2 => sql3/parser}/token.go | 63 +- sql3/parser/token_test.go | 14 + {sql2 => sql3/parser}/walk.go | 66 +- sql3/planner/compilealtertable.go | 77 + sql3/planner/compilebulkinsert.go | 46 + sql3/planner/compilecreatetable.go | 424 ++ sql3/planner/compiledroptable.go | 27 + sql3/planner/compileinsert.go | 181 + sql3/planner/compileselect.go | 376 ++ sql3/planner/compileshow.go | 134 + sql3/planner/executionplanner.go | 197 + sql3/planner/executionplanner_test.go | 1561 +++++ sql3/planner/expression.go | 2369 +++++++ sql3/planner/expressionagg.go | 708 +++ sql3/planner/expressionanalyzer.go | 687 ++ sql3/planner/expressionanalyzercall.go | 246 + sql3/planner/expressionpql.go | 226 + sql3/planner/expressiontypes.go | 711 +++ sql3/planner/inbuiltfunctionsdate.go | 123 + sql3/planner/inbuiltfunctionsset.go | 208 + sql3/planner/memoryobj.go | 72 + sql3/planner/opaltertable.go | 105 + sql3/planner/opbulkinsert.go | 79 + sql3/planner/opcreatetable.go | 121 + sql3/planner/opdistinct.go | 40 + sql3/planner/opdroptable.go | 88 + sql3/planner/opfeaturebasecolumns.go | 174 + sql3/planner/opfeaturebasetables.go | 116 + sql3/planner/opgroupby.go | 198 + sql3/planner/opinsert.go | 364 ++ sql3/planner/opnestedloops.go | 340 + sql3/planner/opnulltable.go | 71 + sql3/planner/oporderby.go | 274 + sql3/planner/oppqlaggregate.go | 263 + sql3/planner/oppqlgroupby.go | 267 + sql3/planner/oppqlmultiaggregate.go | 115 + sql3/planner/oppqlmultigroupby.go | 229 + sql3/planner/opprojection.go | 144 + sql3/planner/opquery.go | 88 + sql3/planner/opsubquery.go | 71 + sql3/planner/optablescan.go | 271 + sql3/planner/optop.go | 71 + sql3/planner/planner.go | 3 + sql3/planner/planoptimizer.go | 503 ++ sql3/planner/planwalker.go | 385 ++ sql3/planner/types/operator.go | 88 + sql3/planner/types/planexpression.go | 62 + sql3/sql3.ebnf | 264 + sql3/sql_definitions_test.go | 517 ++ sql3/sql_defs_aggregate_test.go | 507 ++ sql3/sql_defs_between_test.go | 203 + sql3/sql_defs_binops_test.go | 7922 ++++++++++++++++++++++++ sql3/sql_defs_cast_test.go | 903 +++ sql3/sql_defs_create_table_test.go | 48 + sql3/sql_defs_date_functions_test.go | 187 + sql3/sql_defs_groupby_test.go | 158 + sql3/sql_defs_in_test.go | 263 + sql3/sql_defs_like_test.go | 167 + sql3/sql_defs_null_test.go | 277 + sql3/sql_defs_set_functions_test.go | 302 + sql3/sql_defs_timequantum_test.go | 53 + sql3/sql_defs_unops_test.go | 315 + sql3/sql_test.go | 472 ++ sql3/test/helpers.go | 55 + 115 files changed, 31408 insertions(+), 10784 deletions(-) create mode 100644 cmd/cli.go create mode 100644 ctl/cli.go create mode 100644 errors/errors.go create mode 100644 errors/errors_test.go delete mode 100644 pg/cancel.go delete mode 100644 pg/cancel_test.go delete mode 100644 pg/io.go delete mode 100644 pg/lookerToFeaturebase.md delete mode 100644 pg/lookerToPostgres.md delete mode 100644 pg/message/io.go delete mode 100644 pg/message/message.go delete mode 100644 pg/pgtest/handler.go delete mode 100644 pg/pgtest/memnet.go delete mode 100644 pg/pgtest/server.go delete mode 100644 pg/pgtest/tls.go delete mode 100644 pg/protocol.go delete mode 100644 pg/query.go delete mode 100644 pg/server.go delete mode 100644 pg/server_test.go delete mode 100644 pg/type.go delete mode 100644 planner.go delete mode 100644 planner_test.go delete mode 100644 server/pg.go delete mode 100644 server/pg_internal_test.go delete mode 100644 server/pg_test.go delete mode 100644 sql2/ast_test.go delete mode 100644 sql2/scanner_test.go delete mode 100644 sql2/token_test.go create mode 100644 sql3/errors.go create mode 100644 sql3/interfaces.go rename {sql2 => sql3/parser}/ast.go (77%) create mode 100644 sql3/parser/ast_test.go create mode 100644 sql3/parser/astdatatype.go rename {sql2 => sql3/parser}/parser.go (79%) rename {sql2 => sql3/parser}/parser_test.go (62%) rename {sql2 => sql3/parser}/scanner.go (89%) create mode 100644 sql3/parser/scanner_test.go rename {sql2 => sql3/parser}/token.go (91%) create mode 100644 sql3/parser/token_test.go rename {sql2 => sql3/parser}/walk.go (95%) create mode 100644 sql3/planner/compilealtertable.go create mode 100644 sql3/planner/compilebulkinsert.go create mode 100644 sql3/planner/compilecreatetable.go create mode 100644 sql3/planner/compiledroptable.go create mode 100644 sql3/planner/compileinsert.go create mode 100644 sql3/planner/compileselect.go create mode 100644 sql3/planner/compileshow.go create mode 100644 sql3/planner/executionplanner.go create mode 100644 sql3/planner/executionplanner_test.go create mode 100644 sql3/planner/expression.go create mode 100644 sql3/planner/expressionagg.go create mode 100644 sql3/planner/expressionanalyzer.go create mode 100644 sql3/planner/expressionanalyzercall.go create mode 100644 sql3/planner/expressionpql.go create mode 100644 sql3/planner/expressiontypes.go create mode 100644 sql3/planner/inbuiltfunctionsdate.go create mode 100644 sql3/planner/inbuiltfunctionsset.go create mode 100644 sql3/planner/memoryobj.go create mode 100644 sql3/planner/opaltertable.go create mode 100644 sql3/planner/opbulkinsert.go create mode 100644 sql3/planner/opcreatetable.go create mode 100644 sql3/planner/opdistinct.go create mode 100644 sql3/planner/opdroptable.go create mode 100644 sql3/planner/opfeaturebasecolumns.go create mode 100644 sql3/planner/opfeaturebasetables.go create mode 100644 sql3/planner/opgroupby.go create mode 100644 sql3/planner/opinsert.go create mode 100644 sql3/planner/opnestedloops.go create mode 100644 sql3/planner/opnulltable.go create mode 100644 sql3/planner/oporderby.go create mode 100644 sql3/planner/oppqlaggregate.go create mode 100644 sql3/planner/oppqlgroupby.go create mode 100644 sql3/planner/oppqlmultiaggregate.go create mode 100644 sql3/planner/oppqlmultigroupby.go create mode 100644 sql3/planner/opprojection.go create mode 100644 sql3/planner/opquery.go create mode 100644 sql3/planner/opsubquery.go create mode 100644 sql3/planner/optablescan.go create mode 100644 sql3/planner/optop.go create mode 100644 sql3/planner/planner.go create mode 100644 sql3/planner/planoptimizer.go create mode 100644 sql3/planner/planwalker.go create mode 100644 sql3/planner/types/operator.go create mode 100644 sql3/planner/types/planexpression.go create mode 100644 sql3/sql3.ebnf create mode 100644 sql3/sql_definitions_test.go create mode 100644 sql3/sql_defs_aggregate_test.go create mode 100644 sql3/sql_defs_between_test.go create mode 100644 sql3/sql_defs_binops_test.go create mode 100644 sql3/sql_defs_cast_test.go create mode 100644 sql3/sql_defs_create_table_test.go create mode 100644 sql3/sql_defs_date_functions_test.go create mode 100644 sql3/sql_defs_groupby_test.go create mode 100644 sql3/sql_defs_in_test.go create mode 100644 sql3/sql_defs_like_test.go create mode 100644 sql3/sql_defs_null_test.go create mode 100644 sql3/sql_defs_set_functions_test.go create mode 100644 sql3/sql_defs_timequantum_test.go create mode 100644 sql3/sql_defs_unops_test.go create mode 100644 sql3/sql_test.go create mode 100644 sql3/test/helpers.go 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 +}