Skip to content

Commit

Permalink
Introduce logical query plan with EXPLAIN QUERY PLAN (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
chirst authored Jul 21, 2024
1 parent 0fe34b1 commit b8ff8a8
Show file tree
Hide file tree
Showing 18 changed files with 782 additions and 185 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ a subset of SQL described below.
graph LR
begin(( ))
explain([EXPLAIN])
queryPlan([QUERY PLAN])
select([SELECT])
all[*]
from([FROM])
begin --> explain
explain --> queryPlan
queryPlan --> select
begin --> select
explain --> select
select --> all
Expand All @@ -33,6 +36,7 @@ Create supports the `PRIMARY KEY` column constraint for a single integer column.
graph LR
begin(( ))
explain([EXPLAIN])
queryPlan([QUERY PLAN])
create([CREATE])
table([TABLE])
colTypeInt([INTEGER])
Expand All @@ -45,6 +49,8 @@ colIdent["Column Identifier"]
pkConstraint["PRIMARY KEY"]
begin --> explain
explain --> queryPlan
queryPlan --> create
begin --> create
explain --> create
create --> table
Expand All @@ -69,6 +75,7 @@ colSep --> colIdent
graph LR
begin(( ))
explain([EXPLAIN])
queryPlan([QUERY PLAN])
insert([INSERT])
into([INTO])
tableIdent["Table Identifier"]
Expand All @@ -83,6 +90,8 @@ literal["literal"]
valSep[","]
begin --> explain
explain --> queryPlan
queryPlan --> insert
begin --> insert
explain --> insert
insert --> into
Expand Down
32 changes: 31 additions & 1 deletion compiler/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ package compiler
type Stmt interface{}

type StmtBase struct {
Explain bool
Explain bool
ExplainQueryPlan bool
}

type SelectStmt struct {
Expand Down Expand Up @@ -43,3 +44,32 @@ type InsertStmt struct {
ColNames []string
ColValues []string
}

// type Expr interface {
// Type() string
// }

// type BinaryExpr struct {
// Left Expr
// Operator string
// Right Expr
// }

// type UnaryExpr struct {
// Operator string
// Operand Expr
// }

// type ColumnRef struct {
// Table string
// Column string
// }

// type IntLit struct {
// Value int
// }

// type FunctionExpr struct {
// Name string
// Args []Expr
// }
4 changes: 4 additions & 0 deletions compiler/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
// Keywords where kw is keyword
const (
kwExplain = "EXPLAIN"
kwQuery = "QUERY"
kwPlan = "PLAN"
kwSelect = "SELECT"
kwCount = "COUNT"
kwFrom = "FROM"
Expand All @@ -57,6 +59,8 @@ const (

var keywords = []string{
kwExplain,
kwQuery,
kwPlan,
kwSelect,
kwCount,
kwFrom,
Expand Down
44 changes: 32 additions & 12 deletions compiler/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ func TestLexSelect(t *testing.T) {
{tkNumeric, "1"},
},
},
{
sql: "EXPLAIN QUERY PLAN SELECT 1",
expected: []token{
{tkKeyword, "EXPLAIN"},
{tkWhitespace, " "},
{tkKeyword, "QUERY"},
{tkWhitespace, " "},
{tkKeyword, "PLAN"},
{tkWhitespace, " "},
{tkKeyword, "SELECT"},
{tkWhitespace, " "},
{tkNumeric, "1"},
},
},
{
sql: "SELECT 12",
expected: []token{
Expand All @@ -95,10 +109,12 @@ func TestLexSelect(t *testing.T) {
},
}
for _, c := range cases {
ret := NewLexer(c.sql).Lex()
if !reflect.DeepEqual(ret, c.expected) {
t.Errorf("expected %#v got %#v", c.expected, ret)
}
t.Run(c.sql, func(t *testing.T) {
ret := NewLexer(c.sql).Lex()
if !reflect.DeepEqual(ret, c.expected) {
t.Errorf("expected %#v got %#v", c.expected, ret)
}
})
}
}

Expand Down Expand Up @@ -141,10 +157,12 @@ func TestLexCreate(t *testing.T) {
},
}
for _, c := range cases {
ret := NewLexer(c.sql).Lex()
if !reflect.DeepEqual(ret, c.expected) {
t.Errorf("expected %#v got %#v", c.expected, ret)
}
t.Run(c.sql, func(t *testing.T) {
ret := NewLexer(c.sql).Lex()
if !reflect.DeepEqual(ret, c.expected) {
t.Errorf("expected %#v got %#v", c.expected, ret)
}
})
}
}

Expand Down Expand Up @@ -195,9 +213,11 @@ func TestLexInsert(t *testing.T) {
},
}
for _, c := range cases {
ret := NewLexer(c.sql).Lex()
if !reflect.DeepEqual(ret, c.expected) {
t.Errorf("expected %#v got %#v", c.expected, ret)
}
t.Run(c.sql, func(t *testing.T) {
ret := NewLexer(c.sql).Lex()
if !reflect.DeepEqual(ret, c.expected) {
t.Errorf("expected %#v got %#v", c.expected, ret)
}
})
}
}
13 changes: 12 additions & 1 deletion compiler/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,19 @@ func (p *parser) parseStmt() (Stmt, error) {
t := p.tokens[p.start]
sb := &StmtBase{}
if t.value == kwExplain {
sb.Explain = true
t = p.nextNonSpace()
nv := p.peekNextNonSpace().value
if nv == kwQuery {
tp := p.nextNonSpace()
if tp.value == kwPlan {
sb.ExplainQueryPlan = true
t = p.nextNonSpace()
} else {
return nil, fmt.Errorf(tokenErr, p.tokens[p.end].value)
}
} else {
sb.Explain = true
}
}
switch t.value {
case kwSelect:
Expand Down
49 changes: 42 additions & 7 deletions compiler/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
)

type selectTestCase struct {
name string
tokens []token
expect Stmt
}

func TestParseSelect(t *testing.T) {
cases := []selectTestCase{
{
name: "with explain",
tokens: []token{
{tkKeyword, "EXPLAIN"},
{tkWhitespace, " "},
Expand All @@ -37,6 +39,37 @@ func TestParseSelect(t *testing.T) {
},
},
{
name: "with explain query plan",
tokens: []token{
{tkKeyword, "EXPLAIN"},
{tkWhitespace, " "},
{tkKeyword, "QUERY"},
{tkWhitespace, " "},
{tkKeyword, "PLAN"},
{tkWhitespace, " "},
{tkKeyword, "SELECT"},
{tkWhitespace, " "},
{tkPunctuator, "*"},
{tkWhitespace, " "},
{tkKeyword, "FROM"},
{tkWhitespace, " "},
{tkIdentifier, "foo"},
},
expect: &SelectStmt{
StmtBase: &StmtBase{
Explain: false,
ExplainQueryPlan: true,
},
From: &From{
TableName: "foo",
},
ResultColumn: ResultColumn{
All: true,
},
},
},
{
name: "with count",
tokens: []token{
{tkKeyword, "SELECT"},
{tkWhitespace, " "},
Expand Down Expand Up @@ -64,13 +97,15 @@ func TestParseSelect(t *testing.T) {
},
}
for _, c := range cases {
ret, err := NewParser(c.tokens).Parse()
if err != nil {
t.Errorf("want no err got err %s", err.Error())
}
if !reflect.DeepEqual(ret, c.expect) {
t.Errorf("got %#v want %#v", ret, c.expect)
}
t.Run(c.name, func(t *testing.T) {
ret, err := NewParser(c.tokens).Parse()
if err != nil {
t.Errorf("want no err got err %s", err.Error())
}
if !reflect.DeepEqual(ret, c.expect) {
t.Errorf("got %#v want %#v", ret, c.expect)
}
})
}
}

Expand Down
30 changes: 23 additions & 7 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package db

import (
"errors"
"fmt"

"github.com/chirst/cdb/compiler"
"github.com/chirst/cdb/kv"
Expand All @@ -17,6 +16,11 @@ type executor interface {
Execute(*vm.ExecutionPlan) *vm.ExecuteResult
}

type statementPlanner interface {
ExecutionPlan() (*vm.ExecutionPlan, error)
QueryPlan() (*planner.QueryPlan, error)
}

type dbCatalog interface {
GetColumns(tableOrIndexName string) ([]string, error)
GetRootPageNumber(tableOrIndexName string) (int, error)
Expand Down Expand Up @@ -49,9 +53,21 @@ func (db *DB) Execute(sql string) vm.ExecuteResult {
if err != nil {
return vm.ExecuteResult{Err: err}
}

planner := db.getPlannerFor(statement)
qp, err := planner.QueryPlan()
if err != nil {
return vm.ExecuteResult{Err: err}
}
if qp.ExplainQueryPlan {
return vm.ExecuteResult{
Text: qp.ToString(),
}
}

var executeResult vm.ExecuteResult
for {
executionPlan, err := db.getExecutionPlanFor(statement)
executionPlan, err := planner.ExecutionPlan()
if err != nil {
return vm.ExecuteResult{Err: err}
}
Expand All @@ -63,14 +79,14 @@ func (db *DB) Execute(sql string) vm.ExecuteResult {
return executeResult
}

func (db *DB) getExecutionPlanFor(statement compiler.Stmt) (*vm.ExecutionPlan, error) {
func (db *DB) getPlannerFor(statement compiler.Stmt) statementPlanner {
switch s := statement.(type) {
case *compiler.SelectStmt:
return planner.NewSelect(db.catalog).GetPlan(s)
return planner.NewSelect(db.catalog, s)
case *compiler.CreateStmt:
return planner.NewCreate(db.catalog).GetPlan(s)
return planner.NewCreate(db.catalog, s)
case *compiler.InsertStmt:
return planner.NewInsert(db.catalog).GetPlan(s)
return planner.NewInsert(db.catalog, s)
}
return nil, fmt.Errorf("statement not supported")
panic("statement not supported")
}
Loading

0 comments on commit b8ff8a8

Please sign in to comment.