diff --git a/dax/test/dax/dax_test.go b/dax/test/dax/dax_test.go index a5f026412..87b510a3b 100644 --- a/dax/test/dax/dax_test.go +++ b/dax/test/dax/dax_test.go @@ -112,6 +112,9 @@ func TestDAXIntegration(t *testing.T) { "alterTable/alterTableBadTable", // looks like table does not exist is a different error in DAX "top-tests/test-1", // don't know why this is failing at all "delete_tests", + "viewtests/drop-view", // drop view does a delete + "viewtests/drop-view-if-exists-after-drop", + "viewtests/select-view-after-drop", } doSkip := func(name string) bool { diff --git a/schema.go b/schema.go index 9c9a4a426..36fa67613 100644 --- a/schema.go +++ b/schema.go @@ -28,6 +28,9 @@ func NewOnPremSchema(api *API) *onPremSchema { func (s *onPremSchema) TableByName(ctx context.Context, tname dax.TableName) (*dax.Table, error) { idx, err := s.api.IndexInfo(context.Background(), string(tname)) if err != nil { + if err == ErrIndexNotFound { + return nil, dax.NewErrTableNameDoesNotExist(tname) + } return nil, errors.Wrapf(err, "getting index info for table name: %s", tname) } diff --git a/sql3/errors.go b/sql3/errors.go index 10c5ceff3..aab3a4218 100644 --- a/sql3/errors.go +++ b/sql3/errors.go @@ -79,6 +79,11 @@ const ( ErrTableColumnNotFound errors.Code = "ErrTableColumnNotFound" ErrInvalidKeyPartitionsValue errors.Code = "ErrInvalidKeyPartitionsValue" + ErrTableOrViewNotFound errors.Code = "ErrTableOrViewNotFound" + + ErrViewExists errors.Code = "ErrViewExists" + ErrViewNotFound errors.Code = "ErrViewNotFound" + ErrBadColumnConstraint errors.Code = "ErrBadColumnConstraint" ErrConflictingColumnConstraint errors.Code = "ErrConflictingColumnConstraint" @@ -507,6 +512,13 @@ func NewErrTableNotFound(line, col int, tableName string) error { ) } +func NewErrTableOrViewNotFound(line, col int, tableName string) error { + return errors.New( + ErrTableOrViewNotFound, + fmt.Sprintf("[%d:%d] table or view '%s' not found", line, col, tableName), + ) +} + func NewErrTableExists(line, col int, tableName string) error { return errors.New( ErrTableExists, @@ -535,6 +547,20 @@ func NewErrInvalidKeyPartitionsValue(line, col int, keypartitions int64) error { ) } +func NewErrViewNotFound(line, col int, viewName string) error { + return errors.New( + ErrViewNotFound, + fmt.Sprintf("[%d:%d] view '%s' not found", line, col, viewName), + ) +} + +func NewErrViewExists(line, col int, viewName string) error { + return errors.New( + ErrViewExists, + fmt.Sprintf("[%d:%d] view '%s' already exists", line, col, viewName), + ) +} + func NewErrBadColumnConstraint(line, col int, constraint, columnType string) error { return errors.New( ErrBadColumnConstraint, diff --git a/sql3/parser/ast.go b/sql3/parser/ast.go index 0c90d7c78..c071164c1 100644 --- a/sql3/parser/ast.go +++ b/sql3/parser/ast.go @@ -36,6 +36,7 @@ func (*CreateIndexStatement) node() {} func (*CreateTableStatement) node() {} func (*CreateFunctionStatement) node() {} func (*CreateViewStatement) node() {} +func (*AlterViewStatement) node() {} func (*DateLit) node() {} func (*DefaultConstraint) node() {} func (*DeleteStatement) node() {} @@ -111,6 +112,7 @@ func (*CreateIndexStatement) stmt() {} func (*CreateTableStatement) stmt() {} func (*CreateFunctionStatement) stmt() {} func (*CreateViewStatement) stmt() {} +func (*AlterViewStatement) stmt() {} func (*DeleteStatement) stmt() {} func (*DropIndexStatement) stmt() {} func (*DropTableStatement) stmt() {} @@ -2487,17 +2489,18 @@ func (s *DropTableStatement) String() string { } type CreateViewStatement struct { - Create Pos // position of CREATE keyword - View Pos // position of VIEW keyword - If Pos // position of IF keyword - IfNot Pos // position of NOT keyword after IF - IfNotExists Pos // position of EXISTS keyword after IF NOT - Name *Ident // view name - Lparen Pos // position of column list left paren - Columns []*Ident // column list - Rparen Pos // position of column list right paren - As Pos // position of AS keyword - Select *SelectStatement // source statement + Create Pos // position of CREATE keyword + View Pos // position of VIEW keyword + If Pos // position of IF keyword + IfNot Pos // position of NOT keyword after IF + IfNotExists Pos // position of EXISTS keyword after IF NOT + Name *Ident // view name + // TODO(pok) - we'll do this later - see note in parseCompileView() + // Lparen Pos // position of column list left paren + // Columns []*Ident // column list + // Rparen Pos // position of column list right paren + As Pos // position of AS keyword + Select *SelectStatement // source statement } // Clone returns a deep copy of s. @@ -2507,7 +2510,7 @@ func (s *CreateViewStatement) Clone() *CreateViewStatement { } other := *s other.Name = s.Name.Clone() - other.Columns = cloneIdents(s.Columns) + // other.Columns = cloneIdents(s.Columns) other.Select = s.Select.Clone() return &other } @@ -2521,22 +2524,67 @@ func (s *CreateViewStatement) String() string { } fmt.Fprintf(&buf, " %s", s.Name.String()) - if len(s.Columns) > 0 { - buf.WriteString(" (") - for i, col := range s.Columns { - if i != 0 { - buf.WriteString(", ") - } - buf.WriteString(col.String()) - } - buf.WriteString(")") - } + // if len(s.Columns) > 0 { + // buf.WriteString(" (") + // for i, col := range s.Columns { + // if i != 0 { + // buf.WriteString(", ") + // } + // buf.WriteString(col.String()) + // } + // buf.WriteString(")") + // } fmt.Fprintf(&buf, " AS %s", s.Select.String()) return buf.String() } +type AlterViewStatement struct { + Alter Pos // position of CREATE keyword + View Pos // position of VIEW keyword + Name *Ident // view name + + // TODO(pok) - we'll do this later - see note in parseCompileView() + // Lparen Pos // position of column list left paren + // Columns []*Ident // column list + // Rparen Pos // position of column list right paren + As Pos // position of AS keyword + Select *SelectStatement // source statement +} + +// Clone returns a deep copy of s. +func (s *AlterViewStatement) Clone() *AlterViewStatement { + if s == nil { + return nil + } + other := *s + other.Name = s.Name.Clone() + // other.Columns = cloneIdents(s.Columns) + other.Select = s.Select.Clone() + return &other +} + +// String returns the string representation of the statement. +func (s *AlterViewStatement) String() string { + var buf bytes.Buffer + buf.WriteString("ALTER VIEW") + fmt.Fprintf(&buf, " %s", s.Name.String()) + + // if len(s.Columns) > 0 { + // buf.WriteString(" (") + // for i, col := range s.Columns { + // if i != 0 { + // buf.WriteString(", ") + // } + // buf.WriteString(col.String()) + // } + // buf.WriteString(")") + // } + fmt.Fprintf(&buf, " AS %s", s.Select.String()) + return buf.String() +} + type DropViewStatement struct { Drop Pos // position of DROP keyword View Pos // position of VIEW keyword diff --git a/sql3/parser/ast_test.go b/sql3/parser/ast_test.go index 092b21e6c..c326e055b 100644 --- a/sql3/parser/ast_test.go +++ b/sql3/parser/ast_test.go @@ -395,14 +395,15 @@ func TestCreateFunctionStatement_String(t *testing.T) { func TestCreateViewStatement_String(t *testing.T) { AssertStatementStringer(t, &parser.CreateViewStatement{ Name: &parser.Ident{Name: "vw"}, - Columns: []*parser.Ident{ - {Name: "x"}, - {Name: "y"}, - }, + // Columns: []*parser.Ident{ + // {Name: "x"}, + // {Name: "y"}, + // }, Select: &parser.SelectStatement{ Columns: []*parser.ResultColumn{{Star: pos(0)}}, }, - }, `CREATE VIEW vw (x, y) AS SELECT *`) + //}, `CREATE VIEW vw (x, y) AS SELECT *`) + }, `CREATE VIEW vw AS SELECT *`) AssertStatementStringer(t, &parser.CreateViewStatement{ IfNotExists: pos(0), @@ -413,6 +414,20 @@ func TestCreateViewStatement_String(t *testing.T) { }, `CREATE VIEW IF NOT EXISTS vw AS SELECT *`) } +func TestAlterViewStatement_String(t *testing.T) { + AssertStatementStringer(t, &parser.AlterViewStatement{ + 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 *`) + }, `ALTER VIEW vw AS SELECT *`) +} + func TestDeleteStatement_String(t *testing.T) { AssertStatementStringer(t, &parser.DeleteStatement{ TableName: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}, Alias: &parser.Ident{Name: "tbl2"}}, @@ -487,15 +502,14 @@ func TestDropTriggerStatement_String(t *testing.T) { } 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"`) + }, `DROP VIEW vw`) AssertStatementStringer(t, &parser.DropViewStatement{ IfExists: pos(0), Name: &parser.Ident{Name: "vw"}, - }, `DROP VIEW IF EXISTS "vw"`) + }, `DROP VIEW IF EXISTS vw`) } func TestExplainStatement_String(t *testing.T) { diff --git a/sql3/parser/parser.go b/sql3/parser/parser.go index 3f19647c8..af27ab6e0 100644 --- a/sql3/parser/parser.go +++ b/sql3/parser/parser.go @@ -98,17 +98,7 @@ func (p *Parser) parseNonExplainStatement() (Statement, error) { //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() + return p.parseAlterStatement() case BULK: return p.parseBulkInsertStatement() case CREATE: @@ -337,6 +327,20 @@ func (p *Parser) parseCreateStatement() (Statement, error) { } } +func (p *Parser) parseAlterStatement() (Statement, error) { + assert(p.peek() == ALTER) + pos, tok, _ := p.scan() + + switch p.peek() { + case TABLE: + return p.parseAlterTableStatement(pos) + case VIEW: + return p.parseAlterViewStatement(pos) + default: + return nil, p.errorExpected(pos, tok, "TABLE or VIEW") + } +} + func (p *Parser) parseDropStatement() (Statement, error) { assert(p.peek() == DROP) pos, tok, _ := p.scan() @@ -1054,25 +1058,73 @@ func (p *Parser) parseCreateViewStatement(createPos Pos) (_ *CreateViewStatement return &stmt, err } + // TODO(pok) - we'll do this later - right now views are implemented as + // jit compiled from text, which makes implementing these columns a pain + // when we can pre-compile a plan op subgraph and stored it, we can put this + // back in // Parse optional column list. - if p.peek() == LP { - stmt.Lparen, _, _ = p.scan() - for { - col, err := p.parseIdent("column name") - if err != nil { - return &stmt, err - } - stmt.Columns = append(stmt.Columns, col) + // if p.peek() == LP { + // stmt.Lparen, _, _ = p.scan() + // for { + // col, err := p.parseIdent("column name") + // if err != nil { + // return &stmt, err + // } + // stmt.Columns = append(stmt.Columns, col) - if p.peek() == RP { - break - } else if p.peek() != COMMA { - return &stmt, p.errorExpected(p.pos, p.tok, "comma or right paren") - } - p.scan() - } - stmt.Rparen, _, _ = p.scan() + // if p.peek() == RP { + // break + // } else if p.peek() != COMMA { + // return &stmt, p.errorExpected(p.pos, p.tok, "comma or right paren") + // } + // p.scan() + // } + // stmt.Rparen, _, _ = p.scan() + // } + + // Parse "AS select-stmt" + if p.peek() != AS { + return &stmt, p.errorExpected(p.pos, p.tok, "AS") } + stmt.As, _, _ = p.scan() + if stmt.Select, err = p.parseSelectStatement(false, nil); err != nil { + return &stmt, err + } + return &stmt, nil +} + +func (p *Parser) parseAlterViewStatement(alterPos Pos) (_ *AlterViewStatement, err error) { + var stmt AlterViewStatement + stmt.Alter = alterPos + if p.peek() != VIEW { + return &stmt, p.errorExpected(p.pos, p.tok, "VIEW") + } + stmt.View, _, _ = p.scan() + + if stmt.Name, err = p.parseIdent("view name"); err != nil { + return &stmt, err + } + + // TODO(pok) - we'll do this later - see note in parseCompileView() + // Parse optional column list. + // if p.peek() == LP { + // stmt.Lparen, _, _ = p.scan() + // for { + // col, err := p.parseIdent("column name") + // if err != nil { + // return &stmt, err + // } + // stmt.Columns = append(stmt.Columns, col) + + // if p.peek() == RP { + // break + // } else if p.peek() != COMMA { + // return &stmt, p.errorExpected(p.pos, p.tok, "comma or right paren") + // } + // p.scan() + // } + // stmt.Rparen, _, _ = p.scan() + // } // Parse "AS select-stmt" if p.peek() != AS { @@ -3225,11 +3277,9 @@ func (p *Parser) parseIntegerLiteral(desc string) (*IntegerLit, error) { } } -func (p *Parser) parseAlterTableStatement() (_ *AlterTableStatement, err error) { - assert(p.peek() == ALTER) - +func (p *Parser) parseAlterTableStatement(alterPos Pos) (_ *AlterTableStatement, err error) { var stmt AlterTableStatement - stmt.Alter, _, _ = p.scan() + stmt.Alter = alterPos if p.peek() != TABLE { return &stmt, p.errorExpected(p.pos, p.tok, "TABLE") } diff --git a/sql3/parser/parser_test.go b/sql3/parser/parser_test.go index 74ecaf725..748ddf0f4 100644 --- a/sql3/parser/parser_test.go +++ b/sql3/parser/parser_test.go @@ -427,7 +427,7 @@ func TestParser_ParseAlterStatement(t *testing.T) { DropColumnName: &parser.Ident{NamePos: pos(28), Name: "col"}, }) - AssertParseStatementError(t, `ALTER`, `1:5: expected TABLE, found 'EOF'`) + AssertParseStatementError(t, `ALTER`, `1:1: expected TABLE or VIEW`) 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'`) @@ -1566,22 +1566,23 @@ func TestParser_ParseStatement(t *testing.T) { }) t.Run("CreateView", func(t *testing.T) { - AssertParseStatement(t, `CREATE VIEW vw (col1, col2) AS SELECT x, y`, &parser.CreateViewStatement{ + //AssertParseStatement(t, `CREATE VIEW vw (col1, col2) AS SELECT x, y`, &parser.CreateViewStatement{ + AssertParseStatement(t, `CREATE VIEW vw AS SELECT x, y`, &parser.CreateViewStatement{ Create: pos(0), View: pos(7), Name: &parser.Ident{NamePos: pos(12), Name: "vw"}, - Lparen: pos(15), - Columns: []*parser.Ident{ - {NamePos: pos(16), Name: "col1"}, - {NamePos: pos(22), Name: "col2"}, - }, - Rparen: pos(26), - As: pos(28), + // Lparen: pos(15), + // Columns: []*parser.Ident{ + // {NamePos: pos(16), Name: "col1"}, + // {NamePos: pos(22), Name: "col2"}, + // }, + // Rparen: pos(26), + As: pos(15), Select: &parser.SelectStatement{ - Select: pos(31), + Select: pos(18), Columns: []*parser.ResultColumn{ - {Expr: &parser.Ident{NamePos: pos(38), Name: "x"}}, - {Expr: &parser.Ident{NamePos: pos(41), Name: "y"}}, + {Expr: &parser.Ident{NamePos: pos(25), Name: "x"}}, + {Expr: &parser.Ident{NamePos: pos(28), Name: "y"}}, }, }, }) @@ -1616,8 +1617,8 @@ func TestParser_ParseStatement(t *testing.T) { AssertParseStatementError(t, `CREATE VIEW IF`, `1:14: expected NOT, found 'EOF'`) AssertParseStatementError(t, `CREATE VIEW IF NOT`, `1:18: expected EXISTS, found 'EOF'`) AssertParseStatementError(t, `CREATE VIEW vw`, `1:14: expected AS, found 'EOF'`) - AssertParseStatementError(t, `CREATE VIEW vw (`, `1:16: expected column name, found 'EOF'`) - AssertParseStatementError(t, `CREATE VIEW vw (x`, `1:17: expected comma or right paren, found 'EOF'`) + //AssertParseStatementError(t, `CREATE VIEW vw (`, `1:16: expected column name, found 'EOF'`) + //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, found 'EOF'`) AssertParseStatementError(t, `CREATE VIEW vw AS SELECT`, `1:24: expected expression, found 'EOF'`) }) diff --git a/sql3/parser/walk.go b/sql3/parser/walk.go index 7db4c35a4..def04823d 100644 --- a/sql3/parser/walk.go +++ b/sql3/parser/walk.go @@ -114,9 +114,9 @@ func walk(v Visitor, node Node) (_ Node, err error) { if err := walkIdent(v, &n.Name); err != nil { return node, err } - if err := walkIdentList(v, n.Columns); err != nil { - return node, err - } + // if err := walkIdentList(v, n.Columns); err != nil { + // return node, err + // } if n.Select != nil { if sel, err := walk(v, n.Select); err != nil { return node, err diff --git a/sql3/planner/compilealtertable.go b/sql3/planner/compilealtertable.go index 3031242b7..30767a1f6 100644 --- a/sql3/planner/compilealtertable.go +++ b/sql3/planner/compilealtertable.go @@ -31,7 +31,7 @@ func (p *ExecutionPlanner) compileAlterTableStatement(stmt *parser.AlterTableSta tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.Name.NamePos.Line, stmt.Name.NamePos.Column, tableName) } return nil, err diff --git a/sql3/planner/compilebulkinsert.go b/sql3/planner/compilebulkinsert.go index d934dc61b..466df5040 100644 --- a/sql3/planner/compilebulkinsert.go +++ b/sql3/planner/compilebulkinsert.go @@ -25,7 +25,7 @@ func (p *ExecutionPlanner) compileBulkInsertStatement(stmt *parser.BulkInsertSta tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) } return nil, err @@ -63,7 +63,7 @@ func (p *ExecutionPlanner) compileBulkInsertStatement(stmt *parser.BulkInsertSta switch strings.ToUpper(options.input) { case "FILE": // file should exist - if _, err := os.Stat(options.sourceData); errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(options.sourceData); goerrors.Is(err, os.ErrNotExist) { return nil, sql3.NewErrReadingDatasource(stmt.DataSource.Pos().Line, stmt.DataSource.Pos().Column, options.sourceData, fmt.Sprintf("file '%s' does not exist", options.sourceData)) } case "URL", "STREAM": @@ -163,7 +163,7 @@ func (p *ExecutionPlanner) analyzeBulkInsertStatement(stmt *parser.BulkInsertSta tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) } return err diff --git a/sql3/planner/compilecreateview.go b/sql3/planner/compilecreateview.go new file mode 100644 index 000000000..d3a554ee7 --- /dev/null +++ b/sql3/planner/compilecreateview.go @@ -0,0 +1,64 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// compileCreateViewStatement compiles a parser.CreateViewStatement AST into a PlanOperator +func (p *ExecutionPlanner) compileCreateViewStatement(stmt *parser.CreateViewStatement) (types.PlanOperator, error) { + viewName := parser.IdentName(stmt.Name) + view := &viewSystemObject{ + name: viewName, + } + + // compile select + _, err := p.compileSelectStatement(stmt.Select, true) + if err != nil { + return nil, err + } + view.statement = stmt.Select.String() + + query := NewPlanOpQuery(p, NewPlanOpCreateView(p, stmt.IfNotExists.IsValid(), view), p.sql) + return query, nil +} + +// compileAlterViewStatement compiles a parser.AlterViewStatement AST into a PlanOperator +func (p *ExecutionPlanner) compileAlterViewStatement(stmt *parser.AlterViewStatement) (types.PlanOperator, error) { + viewName := parser.IdentName(stmt.Name) + view := &viewSystemObject{ + name: viewName, + } + + // compile select + _, err := p.compileSelectStatement(stmt.Select, true) + if err != nil { + return nil, err + } + view.statement = stmt.Select.String() + + query := NewPlanOpQuery(p, NewPlanOpAlterView(p, view), p.sql) + return query, nil +} + +func (p *ExecutionPlanner) analyzeCreateViewStatement(stmt *parser.CreateViewStatement) error { + //analyze the select + _, err := p.analyzeSelectStatement(stmt.Select) + if err != nil { + return err + } + + return nil +} + +func (p *ExecutionPlanner) analyzeAlterViewStatement(stmt *parser.AlterViewStatement) error { + //analyze the select + _, err := p.analyzeSelectStatement(stmt.Select) + if err != nil { + return err + } + + return nil +} diff --git a/sql3/planner/compiledelete.go b/sql3/planner/compiledelete.go index 2e253d4aa..97624b943 100644 --- a/sql3/planner/compiledelete.go +++ b/sql3/planner/compiledelete.go @@ -54,7 +54,7 @@ func (p *ExecutionPlanner) compileDeleteStatement(stmt *parser.DeleteStatement) func (p *ExecutionPlanner) analyzeDeleteStatement(stmt *parser.DeleteStatement) error { - err := p.analyzeSource(stmt.Source, stmt) + _, err := p.analyzeSource(stmt.Source, stmt) if err != nil { return err } diff --git a/sql3/planner/compiledroptable.go b/sql3/planner/compiledroptable.go index 48a147c33..2d70b8cfd 100644 --- a/sql3/planner/compiledroptable.go +++ b/sql3/planner/compiledroptable.go @@ -20,7 +20,7 @@ func (p *ExecutionPlanner) compileDropTableStatement(stmt *parser.DropTableState tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.Name.NamePos.Line, stmt.Name.NamePos.Column, tableName) } return nil, err diff --git a/sql3/planner/compiledropview.go b/sql3/planner/compiledropview.go new file mode 100644 index 000000000..819c84fac --- /dev/null +++ b/sql3/planner/compiledropview.go @@ -0,0 +1,23 @@ +// Copyright 2023 Molecula Corp. All rights reserved. + +package planner + +import ( + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// compileDropViewStatement compiles a DROP VIEW statement into a PlanOperator. +func (p *ExecutionPlanner) compileDropViewStatement(stmt *parser.DropViewStatement) (_ types.PlanOperator, err error) { + viewName := parser.IdentName(stmt.Name) + v, err := p.getViewByName(viewName) + if err != nil { + return nil, err + } + if v == nil && !stmt.IfExists.IsValid() { + return nil, sql3.NewErrViewNotFound(0, 0, viewName) + } + + return NewPlanOpQuery(p, NewPlanOpDropView(p, stmt.IfExists.IsValid(), viewName), p.sql), nil +} diff --git a/sql3/planner/compileinsert.go b/sql3/planner/compileinsert.go index 4815e746c..06eaf11af 100644 --- a/sql3/planner/compileinsert.go +++ b/sql3/planner/compileinsert.go @@ -24,7 +24,7 @@ func (p *ExecutionPlanner) compileInsertStatement(stmt *parser.InsertStatement) tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) } return nil, err @@ -79,7 +79,7 @@ func (p *ExecutionPlanner) analyzeInsertStatement(stmt *parser.InsertStatement) tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return sql3.NewErrTableNotFound(stmt.Table.NamePos.Line, stmt.Table.NamePos.Column, tableName) } return err diff --git a/sql3/planner/compileselect.go b/sql3/planner/compileselect.go index 02d5efdec..0a112bed1 100644 --- a/sql3/planner/compileselect.go +++ b/sql3/planner/compileselect.go @@ -366,57 +366,97 @@ func (p *ExecutionPlanner) compileSource(scope *PlanOpQuery, source parser.Sourc } } -func (p *ExecutionPlanner) analyzeSource(source parser.Source, scope parser.Statement) error { +func (p *ExecutionPlanner) analyzeSource(source parser.Source, scope parser.Statement) (parser.Source, error) { if source == nil { - return nil + return nil, nil } switch source := source.(type) { case *parser.JoinClause: - err := p.analyzeSource(source.X, scope) + x, err := p.analyzeSource(source.X, scope) if err != nil { - return err + return nil, err } - err = p.analyzeSource(source.Y, scope) + y, err := p.analyzeSource(source.Y, scope) if err != nil { - return err + return nil, err } if source.Constraint != nil { switch join := source.Constraint.(type) { case *parser.OnConstraint: ex, err := p.analyzeExpression(join.X, scope) if err != nil { - return err + return nil, err } join.X = ex default: - return sql3.NewErrInternalf("unexpected constraint type '%T'", join) + return nil, sql3.NewErrInternalf("unexpected constraint type '%T'", join) } } - return nil + source.X = x + source.Y = y + return source, nil case *parser.ParenSource: - err := p.analyzeSource(source.X, scope) + x, err := p.analyzeSource(source.X, scope) if err != nil { - return err + return nil, err } - return nil + source.X = x + return source, nil case *parser.QualifiedTableName: + + objectName := parser.IdentName(source.Name) + + // check views first + view, err := p.getViewByName(objectName) + if err != nil { + return nil, err + } + + // if view is not null, it exists + if view != nil { + // parse the select statement + ast, err := parser.NewParser(strings.NewReader(view.statement)).ParseStatement() + if err != nil { + return nil, err + } + sel, ok := ast.(*parser.SelectStatement) + if !ok { + return nil, sql3.NewErrInternalf("unexpected ast type") + } + // analyze the select statement + expr, err := p.analyzeSelectStatement(sel) + if err != nil { + return nil, err + } + selExpr, ok := expr.(*parser.SelectStatement) + if !ok { + return nil, sql3.NewErrInternalf("unexpected analyzed type") + } + + // rewrite as a paren source with the select + paren := &parser.ParenSource{ + X: selExpr, + Alias: source.Alias, + } + return paren, nil + } + // check table exists - tableName := parser.IdentName(source.Name) - tname := dax.TableName(tableName) + tname := dax.TableName(objectName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { - return sql3.NewErrTableNotFound(source.Name.NamePos.Line, source.Name.NamePos.Column, tableName) + if isTableNotFoundError(err) { + return nil, sql3.NewErrTableOrViewNotFound(source.Name.NamePos.Line, source.Name.NamePos.Column, objectName) } - return err + return nil, err } // populate the output columns from the source for i, fld := range tbl.Fields { soc := &parser.SourceOutputColumn{ - TableName: tableName, + TableName: objectName, ColumnName: string(fld.Name), ColumnIndex: i, Datatype: fieldSQLDataType(pilosa.FieldToFieldInfo(fld)), @@ -424,7 +464,7 @@ func (p *ExecutionPlanner) analyzeSource(source parser.Source, scope parser.Stat source.OutputColumns = append(source.OutputColumns, soc) } - return nil + return source, nil case *parser.TableValuedFunction: // check it actually is a table valued function - we only support one right now; subtable() @@ -432,12 +472,12 @@ func (p *ExecutionPlanner) analyzeSource(source parser.Source, scope parser.Stat case "SUBTABLE": _, err := p.analyzeCallExpression(source.Call, scope) if err != nil { - return err + return nil, err } tvfResultType, ok := source.Call.ResultDataType.(*parser.DataTypeSubtable) if !ok { - return sql3.NewErrInternalf("unexepected tvf return type") + return nil, sql3.NewErrInternalf("unexepected tvf return type") } // populate the output columns from the source @@ -452,29 +492,34 @@ func (p *ExecutionPlanner) analyzeSource(source parser.Source, scope parser.Stat } default: - return sql3.NewErrInternalf("table valued function expected") + return nil, sql3.NewErrInternalf("table valued function expected") } - return nil + return source, nil case *parser.SelectStatement: - _, err := p.analyzeSelectStatement(source) + expr, err := p.analyzeSelectStatement(source) if err != nil { - return err + return nil, err } - return nil + selExpr, ok := expr.(*parser.SelectStatement) + if !ok { + return nil, sql3.NewErrInternalf("unexpected analyzed type") + } + return selExpr, nil default: - return sql3.NewErrInternalf("unexpected source type: %T", source) + return nil, sql3.NewErrInternalf("unexpected source type: %T", source) } } func (p *ExecutionPlanner) analyzeSelectStatement(stmt *parser.SelectStatement) (parser.Expr, error) { // analyze source first - needed for name resolution - err := p.analyzeSource(stmt.Source, stmt) + source, err := p.analyzeSource(stmt.Source, stmt) if err != nil { return nil, err } + stmt.Source = source if err := p.analyzeSelectStatementWildcards(stmt); err != nil { return nil, err @@ -617,7 +662,17 @@ func (p *ExecutionPlanner) columnsFromSource(source parser.Source) ([]*parser.Re return result, nil case *parser.SelectStatement: - return nil, sql3.NewErrInternal("sub-selects are not currently supported") + 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 + default: return nil, sql3.NewErrInternalf("unexpected source type: %T", source) } diff --git a/sql3/planner/compileshow.go b/sql3/planner/compileshow.go index 6b9fed8c8..d30a302f3 100644 --- a/sql3/planner/compileshow.go +++ b/sql3/planner/compileshow.go @@ -84,7 +84,7 @@ func (p *ExecutionPlanner) compileShowColumnsStatement(stmt *parser.ShowColumnsS tname := dax.TableName(tableName) tbl, err := p.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.TableName.NamePos.Line, stmt.TableName.NamePos.Column, tableName) } return nil, err @@ -174,7 +174,7 @@ func (p *ExecutionPlanner) compileShowCreateTableStatement(stmt *parser.ShowCrea tableName := parser.IdentName(stmt.TableName) tname := dax.TableName(tableName) if _, err := p.schemaAPI.TableByName(context.Background(), tname); err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrTableNotFound(stmt.TableName.NamePos.Line, stmt.TableName.NamePos.Column, tableName) } return nil, err diff --git a/sql3/planner/executionplanner.go b/sql3/planner/executionplanner.go index dbec61628..265203d5e 100644 --- a/sql3/planner/executionplanner.go +++ b/sql3/planner/executionplanner.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "strconv" + "strings" pilosa "github.com/featurebasedb/featurebase/v3" "github.com/featurebasedb/featurebase/v3/logger" @@ -17,6 +18,11 @@ import ( "github.com/featurebasedb/featurebase/v3/sql3/planner/types" ) +func isTableNotFoundError(err error) bool { + // TODO (pok) take out the second part of this check once we return correct error types across network boundaries + return errors.Is(err, dax.ErrTableNameDoesNotExist) || strings.Contains(err.Error(), "does not exist") +} + // ExecutionPlanner compiles SQL text into a query plan type ExecutionPlanner struct { executor pilosa.Executor @@ -64,10 +70,16 @@ func (p *ExecutionPlanner) CompilePlan(ctx context.Context, stmt parser.Statemen rootOperator, err = p.compileShowCreateTableStatement(stmt) case *parser.CreateTableStatement: rootOperator, err = p.compileCreateTableStatement(stmt) + case *parser.CreateViewStatement: + rootOperator, err = p.compileCreateViewStatement(stmt) case *parser.AlterTableStatement: rootOperator, err = p.compileAlterTableStatement(stmt) + case *parser.AlterViewStatement: + rootOperator, err = p.compileAlterViewStatement(stmt) case *parser.DropTableStatement: rootOperator, err = p.compileDropTableStatement(stmt) + case *parser.DropViewStatement: + rootOperator, err = p.compileDropViewStatement(stmt) case *parser.InsertStatement: rootOperator, err = p.compileInsertStatement(stmt) case *parser.BulkInsertStatement: @@ -111,10 +123,16 @@ func (p *ExecutionPlanner) analyzePlan(stmt parser.Statement) error { return nil case *parser.CreateTableStatement: return p.analyzeCreateTableStatement(stmt) + case *parser.CreateViewStatement: + return p.analyzeCreateViewStatement(stmt) case *parser.AlterTableStatement: return p.analyzeAlterTableStatement(stmt) + case *parser.AlterViewStatement: + return p.analyzeAlterViewStatement(stmt) case *parser.DropTableStatement: return nil + case *parser.DropViewStatement: + return nil case *parser.InsertStatement: return p.analyzeInsertStatement(stmt) case *parser.BulkInsertStatement: diff --git a/sql3/planner/executionplannersystemtables.go b/sql3/planner/executionplannersystemtables.go index a4a5843bc..039da5c3a 100644 --- a/sql3/planner/executionplannersystemtables.go +++ b/sql3/planner/executionplannersystemtables.go @@ -28,10 +28,10 @@ func newSystemTableDefintionsWrapper(api pilosa.SchemaAPI) *systemTableDefintion func (s *systemTableDefintionsWrapper) TableByName(ctx context.Context, tname dax.TableName) (*dax.Table, error) { tbl, err := s.schemaAPI.TableByName(ctx, tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { st, ok := systemTables[string(tname)] if !ok { - return nil, pilosa.ErrIndexNotFound + return nil, dax.NewErrTableNameDoesNotExist(tname) } return indexInfoFromSystemTableB(st) diff --git a/sql3/planner/opalterview.go b/sql3/planner/opalterview.go new file mode 100644 index 000000000..70789acb1 --- /dev/null +++ b/sql3/planner/opalterview.go @@ -0,0 +1,97 @@ +// 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" +) + +// PlanOpAlterView implements the ALTER VIEW operator +type PlanOpAlterView struct { + planner *ExecutionPlanner + view *viewSystemObject + warnings []string +} + +func NewPlanOpAlterView(planner *ExecutionPlanner, view *viewSystemObject) *PlanOpAlterView { + return &PlanOpAlterView{ + planner: planner, + view: view, + warnings: make([]string, 0), + } +} + +func (p *PlanOpAlterView) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpAlterView) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return newAlterViewIter(p.planner, p.view), nil +} + +func (p *PlanOpAlterView) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpAlterView) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 0 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpAlterView(p.planner, p.view), nil +} + +func (p *PlanOpAlterView) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + result["_schema"] = p.Schema().Plan() + result["model"] = p.view.name + return result +} + +func (p *PlanOpAlterView) String() string { + return "" +} + +func (p *PlanOpAlterView) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpAlterView) Warnings() []string { + var w []string + w = append(w, p.warnings...) + return w +} + +type alterViewIter struct { + planner *ExecutionPlanner + view *viewSystemObject +} + +func newAlterViewIter(planner *ExecutionPlanner, view *viewSystemObject) *alterViewIter { + return &alterViewIter{ + planner: planner, + view: view, + } +} + +func (i *alterViewIter) Next(ctx context.Context) (types.Row, error) { + // now check in the views table to see if it exists + v, err := i.planner.getViewByName(i.view.name) + if err != nil { + return nil, err + } + if v == nil { + return nil, sql3.NewErrViewNotFound(0, 0, i.view.name) + } + + // now store the view into fb_views + err = i.planner.updateView(i.view) + if err != nil { + return nil, err + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opcreateview.go b/sql3/planner/opcreateview.go new file mode 100644 index 000000000..310a6bdfa --- /dev/null +++ b/sql3/planner/opcreateview.go @@ -0,0 +1,120 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/dax" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpCreateView implements the CREATE VIEW operator +type PlanOpCreateView struct { + planner *ExecutionPlanner + view *viewSystemObject + ifNotExists bool + warnings []string +} + +func NewPlanOpCreateView(planner *ExecutionPlanner, ifNotExists bool, view *viewSystemObject) *PlanOpCreateView { + return &PlanOpCreateView{ + planner: planner, + view: view, + ifNotExists: ifNotExists, + warnings: make([]string, 0), + } +} + +func (p *PlanOpCreateView) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpCreateView) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return newCreateViewIter(p.planner, p.ifNotExists, p.view), nil +} + +func (p *PlanOpCreateView) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpCreateView) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + if len(children) != 0 { + return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children)) + } + return NewPlanOpCreateView(p.planner, p.ifNotExists, p.view), nil +} + +func (p *PlanOpCreateView) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + result["_schema"] = p.Schema().Plan() + result["model"] = p.view.name + return result +} + +func (p *PlanOpCreateView) String() string { + return "" +} + +func (p *PlanOpCreateView) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpCreateView) Warnings() []string { + var w []string + w = append(w, p.warnings...) + return w +} + +type createViewIter struct { + planner *ExecutionPlanner + view *viewSystemObject + ifNotExists bool +} + +func newCreateViewIter(planner *ExecutionPlanner, ifNotExists bool, view *viewSystemObject) *createViewIter { + return &createViewIter{ + planner: planner, + view: view, + ifNotExists: ifNotExists, + } +} + +func (i *createViewIter) Next(ctx context.Context) (types.Row, error) { + // make sure we have no existing table named the same as our view + viewName := dax.TableName(i.view.name) + tbl, err := i.planner.schemaAPI.TableByName(context.Background(), viewName) + if err != nil { + if !isTableNotFoundError(err) { + return nil, err + } + } + if tbl != nil { + if i.ifNotExists { + return nil, types.ErrNoMoreRows + } + return nil, sql3.NewErrViewExists(0, 0, i.view.name) + } + + // now check in the views table to see if it is exists + v, err := i.planner.getViewByName(i.view.name) + if err != nil { + return nil, err + } + if v != nil { + if i.ifNotExists { + return nil, types.ErrNoMoreRows + } + return nil, sql3.NewErrViewExists(0, 0, i.view.name) + } + + // now store the view into fb_views + err = i.planner.insertView(i.view) + if err != nil { + return nil, err + } + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/opdropview.go b/sql3/planner/opdropview.go new file mode 100644 index 000000000..c90b50469 --- /dev/null +++ b/sql3/planner/opdropview.go @@ -0,0 +1,102 @@ +// Copyright 2023 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "fmt" + + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +// PlanOpDropView plan operator to drop a view. +type PlanOpDropView struct { + planner *ExecutionPlanner + viewName string + ifExists bool + warnings []string +} + +func NewPlanOpDropView(p *ExecutionPlanner, ifExists bool, viewName string) *PlanOpDropView { + return &PlanOpDropView{ + planner: p, + viewName: viewName, + ifExists: ifExists, + warnings: make([]string, 0), + } +} + +func (p *PlanOpDropView) Plan() map[string]interface{} { + result := make(map[string]interface{}) + result["_op"] = fmt.Sprintf("%T", p) + result["viewName"] = p.viewName + result["isExists"] = p.ifExists + return result +} + +func (p *PlanOpDropView) String() string { + return "" +} + +func (p *PlanOpDropView) AddWarning(warning string) { + p.warnings = append(p.warnings, warning) +} + +func (p *PlanOpDropView) Warnings() []string { + return p.warnings +} + +func (p *PlanOpDropView) Schema() types.Schema { + return types.Schema{} +} + +func (p *PlanOpDropView) Children() []types.PlanOperator { + return []types.PlanOperator{} +} + +func (p *PlanOpDropView) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) { + return &dropViewRowIter{ + planner: p.planner, + ifExists: p.ifExists, + viewName: p.viewName, + }, nil +} + +func (p *PlanOpDropView) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) { + return nil, nil +} + +type dropViewRowIter struct { + planner *ExecutionPlanner + ifExists bool + viewName string +} + +var _ types.RowIterator = (*dropViewRowIter)(nil) + +func (i *dropViewRowIter) Next(ctx context.Context) (types.Row, error) { + err := i.planner.checkAccess(ctx, i.viewName, accessTypeDropObject) + if err != nil { + return nil, err + } + + // check in the views table to see if it exists + v, err := i.planner.getViewByName(i.viewName) + if err != nil { + return nil, err + } + if v == nil { + if i.ifExists { + return nil, types.ErrNoMoreRows + } + return nil, sql3.NewErrViewNotFound(0, 0, i.viewName) + } + + err = i.planner.deleteView(i.viewName) + if err != nil { + return nil, err + } + + return nil, types.ErrNoMoreRows +} diff --git a/sql3/planner/oppqldistinctscan.go b/sql3/planner/oppqldistinctscan.go index ef4299cb4..3ff84d0c6 100644 --- a/sql3/planner/oppqldistinctscan.go +++ b/sql3/planner/oppqldistinctscan.go @@ -142,7 +142,7 @@ func (i *distinctScanRowIter) Next(ctx context.Context) (types.Row, error) { tname := dax.TableName(i.tableName) table, err := i.planner.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrInternalf("table not found '%s'", i.tableName) } return nil, err diff --git a/sql3/planner/oppqltablescan.go b/sql3/planner/oppqltablescan.go index 542b513a2..a56261871 100644 --- a/sql3/planner/oppqltablescan.go +++ b/sql3/planner/oppqltablescan.go @@ -146,7 +146,7 @@ func (i *tableScanRowIter) Next(ctx context.Context) (types.Row, error) { tname := dax.TableName(i.tableName) table, err := i.planner.schemaAPI.TableByName(context.Background(), tname) if err != nil { - if errors.Is(err, pilosa.ErrIndexNotFound) { + if isTableNotFoundError(err) { return nil, sql3.NewErrInternalf("table not found '%s'", i.tableName) } return nil, err diff --git a/sql3/planner/planoptimizer.go b/sql3/planner/planoptimizer.go index 7b8d44b3b..9368515d7 100644 --- a/sql3/planner/planoptimizer.go +++ b/sql3/planner/planoptimizer.go @@ -18,6 +18,8 @@ import ( //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 in here +//TODO(pok) push down filters thru subqueries with aliases + // a function prototype for all optimizer rules type OptimizerFunc func(context.Context, *ExecutionPlanner, types.PlanOperator, *OptimizerScope) (types.PlanOperator, bool, error) diff --git a/sql3/planner/systemobjects.go b/sql3/planner/systemobjects.go new file mode 100644 index 000000000..d806d7359 --- /dev/null +++ b/sql3/planner/systemobjects.go @@ -0,0 +1,247 @@ +// Copyright 2022 Molecula Corp. All rights reserved. + +package planner + +import ( + "context" + "time" + + pilosa "github.com/molecula/featurebase/v3" + "github.com/molecula/featurebase/v3/dax" + "github.com/molecula/featurebase/v3/sql3" + "github.com/molecula/featurebase/v3/sql3/parser" + "github.com/molecula/featurebase/v3/sql3/planner/types" +) + +type viewSystemObject struct { + name string + statement string +} + +func (p *ExecutionPlanner) ensureViewsSystemTableExists() error { + _, err := p.schemaAPI.TableByName(context.Background(), "fb_views") + if err != nil { + if !isTableNotFoundError(err) { + return err + } + + // create table fb_views ( + // _id string + // name string + // statement string + // owner string + // updated_by string + // created_at timestamp + // updated_at timestamp + // ); + + // if it doesn't, create it by making the appropriate iterator + iter := &createTableRowIter{ + planner: p, + tableName: "fb_views", + failIfExists: false, + isKeyed: true, + keyPartitions: 0, + columns: []*createTableField{ + { + planner: p, + name: "name", + typeName: dax.BaseTypeString, + fos: []pilosa.FieldOption{ + pilosa.OptFieldTypeMutex(pilosa.DefaultCacheType, pilosa.DefaultCacheSize), + pilosa.OptFieldKeys(), + }, + }, + { + planner: p, + name: "statement", + typeName: dax.BaseTypeString, + fos: []pilosa.FieldOption{ + pilosa.OptFieldTypeMutex(pilosa.DefaultCacheType, pilosa.DefaultCacheSize), + pilosa.OptFieldKeys(), + }, + }, + { + planner: p, + name: "owner", + typeName: dax.BaseTypeString, + fos: []pilosa.FieldOption{ + pilosa.OptFieldTypeMutex(pilosa.DefaultCacheType, pilosa.DefaultCacheSize), + pilosa.OptFieldKeys(), + }, + }, + { + planner: p, + name: "updated_by", + typeName: dax.BaseTypeString, + fos: []pilosa.FieldOption{ + pilosa.OptFieldTypeMutex(pilosa.DefaultCacheType, pilosa.DefaultCacheSize), + pilosa.OptFieldKeys(), + }, + }, + { + planner: p, + name: "created_at", + typeName: dax.BaseTypeTimestamp, + fos: []pilosa.FieldOption{ + pilosa.OptFieldTypeTimestamp(pilosa.DefaultEpoch, pilosa.TimeUnitSeconds), + }, + }, + { + planner: p, + name: "updated_at", + typeName: dax.BaseTypeTimestamp, + fos: []pilosa.FieldOption{ + pilosa.OptFieldTypeTimestamp(pilosa.DefaultEpoch, pilosa.TimeUnitSeconds), + }, + }, + }, + description: "system table for views", + } + // call next on our iterator to create the table + _, err := iter.Next(context.Background()) + if err != nil && err != types.ErrNoMoreRows { + return err + } + } + return nil +} + +func (p *ExecutionPlanner) getViewByName(name string) (*viewSystemObject, error) { + err := p.ensureViewsSystemTableExists() + if err != nil { + return nil, err + } + + tbl, err := p.schemaAPI.TableByName(context.Background(), "fb_views") + if err != nil { + return nil, sql3.NewErrTableNotFound(0, 0, "fb_views") + } + + cols := make([]string, len(tbl.Fields)) + for i, c := range tbl.Fields { + cols[i] = string(c.Name) + } + + iter := &tableScanRowIter{ + planner: p, + tableName: "fb_views", + columns: cols, + predicate: newBinOpPlanExpression( + newQualifiedRefPlanExpression("fb_views", "_id", 0, parser.NewDataTypeString()), + parser.EQ, + newStringLiteralPlanExpression(name), + parser.NewDataTypeBool(), + ), + topExpr: nil, + } + + row, err := iter.Next(context.Background()) + if err != nil { + if err == types.ErrNoMoreRows { + // view does not exist + return nil, nil + } + return nil, err + } + + return &viewSystemObject{ + name: row[1].(string), + statement: row[2].(string), + }, nil +} + +func (p *ExecutionPlanner) insertView(view *viewSystemObject) error { + err := p.ensureViewsSystemTableExists() + if err != nil { + return err + } + + createTime := time.Now().UTC() + + iter := &insertRowIter{ + planner: p, + tableName: "fb_views", + targetColumns: []*qualifiedRefPlanExpression{ + newQualifiedRefPlanExpression("fb_views", "_id", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "name", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "statement", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "owner", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "updated_by", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "created_at", 0, parser.NewDataTypeTimestamp()), + newQualifiedRefPlanExpression("fb_views", "updated_at", 0, parser.NewDataTypeTimestamp()), + }, + insertValues: [][]types.PlanExpression{ + { + newStringLiteralPlanExpression(view.name), + newStringLiteralPlanExpression(view.name), + newStringLiteralPlanExpression(view.statement), + newStringLiteralPlanExpression(""), + newStringLiteralPlanExpression(""), + newDateLiteralPlanExpression(createTime), + newDateLiteralPlanExpression(createTime), + }, + }, + } + _, err = iter.Next(context.Background()) + if err != nil && err != types.ErrNoMoreRows { + return err + } + return nil +} + +func (p *ExecutionPlanner) updateView(view *viewSystemObject) error { + err := p.ensureViewsSystemTableExists() + if err != nil { + return err + } + + updateTime := time.Now().UTC() + + iter := &insertRowIter{ + planner: p, + tableName: "fb_views", + targetColumns: []*qualifiedRefPlanExpression{ + newQualifiedRefPlanExpression("fb_views", "_id", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "statement", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "updated_by", 0, parser.NewDataTypeString()), + newQualifiedRefPlanExpression("fb_views", "updated_at", 0, parser.NewDataTypeTimestamp()), + }, + insertValues: [][]types.PlanExpression{ + { + newStringLiteralPlanExpression(view.name), + newStringLiteralPlanExpression(view.statement), + newStringLiteralPlanExpression(""), + newDateLiteralPlanExpression(updateTime), + }, + }, + } + _, err = iter.Next(context.Background()) + if err != nil && err != types.ErrNoMoreRows { + return err + } + return nil +} + +func (p *ExecutionPlanner) deleteView(viewName string) error { + err := p.ensureViewsSystemTableExists() + if err != nil { + return err + } + + iter := &filteredDeleteRowIter{ + planner: p, + tableName: "fb_views", + filter: newBinOpPlanExpression( + newQualifiedRefPlanExpression("fb_views", "_id", 0, parser.NewDataTypeString()), + parser.EQ, + newStringLiteralPlanExpression(viewName), + parser.NewDataTypeBool(), + ), + } + _, err = iter.Next(context.Background()) + if err != nil && err != types.ErrNoMoreRows { + return err + } + return nil +} diff --git a/sql3/sql_complex_test.go b/sql3/sql_complex_test.go index 55c0a26c3..d780314ba 100644 --- a/sql3/sql_complex_test.go +++ b/sql3/sql_complex_test.go @@ -193,7 +193,10 @@ func TestPlanner_Show(t *testing.T) { if err != nil { t.Fatal(err) } - if len(results) != 7 { + + // we keep adding system tables on the fly so as + // long as we get more than 0 tables, we're good + if len(results) == 0 { t.Fatal(fmt.Errorf("unexpected result set length")) } diff --git a/sql3/test/defs/defs.go b/sql3/test/defs/defs.go index e25e14510..42dcd6494 100644 --- a/sql3/test/defs/defs.go +++ b/sql3/test/defs/defs.go @@ -20,6 +20,7 @@ var TableTests []TableTest = []TableTest{ distinctTests, subqueryTests, + viewTests, topTests, diff --git a/sql3/test/defs/defs_views.go b/sql3/test/defs/defs_views.go new file mode 100644 index 000000000..bd894bbb6 --- /dev/null +++ b/sql3/test/defs/defs_views.go @@ -0,0 +1,126 @@ +package defs + +var viewTests = TableTest{ + name: "viewtests", + Table: tbl( + "viewtable", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("a_string", fldTypeString), + srcHdr("a_int", fldTypeInt), + ), + srcRows( + srcRow(int64(1), "str1", int64(10)), + srcRow(int64(2), "str1", int64(20)), + srcRow(int64(3), "str2", int64(30)), + srcRow(int64(4), "str2", int64(40)), + srcRow(int64(5), "str3", int64(50)), + ), + ), + SQLTests: []SQLTest{ + { + name: "create-view", + SQLs: sqls( + "create view viewonviewtable as select _id, a_string, a_int from viewtable;", + ), + ExpHdrs: hdrs(), + ExpRows: rows(), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "create-view-should-fail", + SQLs: sqls( + "create view viewonviewtable as select _id, a_string, a_int from viewtable;", + ), + ExpErr: "view 'viewonviewtable' already exists", + }, + { + name: "create-view-should-not-fail", + SQLs: sqls( + "create view if not exists viewonviewtable as select _id, a_string, a_int from viewtable;", + ), + ExpHdrs: hdrs(), + ExpRows: rows(), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "select-view", + SQLs: sqls( + "select * from viewonviewtable;", + "select _id, a_string, a_int from viewonviewtable;", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_int", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), "str1", int64(10)), + row(int64(2), "str1", int64(20)), + row(int64(3), "str2", int64(30)), + row(int64(4), "str2", int64(40)), + row(int64(5), "str3", int64(50)), + ), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "alter-view", + SQLs: sqls( + "alter view viewonviewtable as select _id, a_string, a_int from viewtable where a_int > 20;", + ), + ExpHdrs: hdrs(), + ExpRows: rows(), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "select-alter-view", + SQLs: sqls( + "select * from viewonviewtable;", + "select _id, a_string, a_int from viewonviewtable;", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a_string", fldTypeString), + hdr("a_int", fldTypeInt), + ), + ExpRows: rows( + row(int64(3), "str2", int64(30)), + row(int64(4), "str2", int64(40)), + row(int64(5), "str3", int64(50)), + ), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "drop-view", + SQLs: sqls( + "drop view viewonviewtable;", + ), + ExpHdrs: hdrs(), + ExpRows: rows(), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "drop-view-if-exists-after-drop", + SQLs: sqls( + "drop view if exists viewonviewtable;", + ), + ExpHdrs: hdrs(), + ExpRows: rows(), + Compare: CompareExactUnordered, + SortStringKeys: true, + }, + { + name: "select-view-after-drop", + SQLs: sqls( + "select * from viewonviewtable;", + ), + ExpErr: "table or view 'viewonviewtable' not found", + }, + }, +}