Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors #5

Merged
merged 3 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 59 additions & 19 deletions planner/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"github.com/chirst/cdb/vm"
)

var errInvalidPKColumnType = errors.New("primary key must be INTEGER type")
var errTableExists = errors.New("table exists")
var errMoreThanOnePK = errors.New("more than one primary key specified")
var (
errInvalidPKColumnType = errors.New("primary key must be INTEGER type")
errTableExists = errors.New("table exists")
errMoreThanOnePK = errors.New("more than one primary key specified")
)

// createCatalog defines the catalog methods needed by the create planner
type createCatalog interface {
Expand All @@ -21,29 +23,52 @@ type createCatalog interface {
GetVersion() string
}

// createPlanner is capable of generating a logical query plan and a physical
// executionPlan for a create statement. The planners within are separated by
// their responsibility.
type createPlanner struct {
qp *createQueryPlanner
ep *createExecutionPlanner
// queryPlanner is responsible for transforming the AST to a logical query
// plan tree. This tree is made up of nodes similar to a relational algebra
// tree. The query planner also performs binding and validation.
queryPlanner *createQueryPlanner
// executionPlanner is responsible for converting the logical query plan
// tree to a bytecode execution plan capable of being run by the virtual
// machine.
executionPlanner *createExecutionPlanner
}

// createQueryPlanner converts the AST to a logical query plan. Along the way it
// validates the statement makes sense with the catalog a process known as
// binding.
type createQueryPlanner struct {
catalog createCatalog
stmt *compiler.CreateStmt
// catalog contains the schema
catalog createCatalog
// stmt contains the AST
stmt *compiler.CreateStmt
// queryPlan contains the query plan being constructed. The root node must
// be createNode.
queryPlan *createNode
}

// createExecutionPlanner converts logical nodes to a bytecode execution plan
// that can be run by the vm.
type createExecutionPlanner struct {
queryPlan *createNode
// queryPlan contains the logical query plan. The is populated by calling
// QueryPlan.
queryPlan *createNode
// executionPlan contains the bytecode execution plan being constructed.
// This is populated by calling ExecutionPlan.
executionPlan *vm.ExecutionPlan
}

// NewCreate creates a planner for the given create statement.
func NewCreate(catalog createCatalog, stmt *compiler.CreateStmt) *createPlanner {
return &createPlanner{
qp: &createQueryPlanner{
queryPlanner: &createQueryPlanner{
catalog: catalog,
stmt: stmt,
},
ep: &createExecutionPlanner{
executionPlanner: &createExecutionPlanner{
executionPlan: vm.NewExecutionPlan(
catalog.GetVersion(),
stmt.Explain,
Expand All @@ -52,12 +77,22 @@ func NewCreate(catalog createCatalog, stmt *compiler.CreateStmt) *createPlanner
}
}

// QueryPlan generates the query plan for the planner.
func (p *createPlanner) QueryPlan() (*QueryPlan, error) {
tableName, err := p.qp.ensureTableDoesNotExist()
qp, err := p.queryPlanner.getQueryPlan()
if err != nil {
return nil, err
}
jSchema, err := p.qp.getSchemaString()
p.executionPlanner.queryPlan = p.queryPlanner.queryPlan
return qp, err
}

func (p *createQueryPlanner) getQueryPlan() (*QueryPlan, error) {
tableName, err := p.ensureTableDoesNotExist()
if err != nil {
return nil, err
}
jSchema, err := p.getSchemaString()
if err != nil {
return nil, err
}
Expand All @@ -67,9 +102,8 @@ func (p *createPlanner) QueryPlan() (*QueryPlan, error) {
tableName: tableName,
schema: jSchema,
}
qp := newQueryPlan(createNode, p.qp.stmt.ExplainQueryPlan)
p.ep.queryPlan = createNode
return qp, nil
p.queryPlan = createNode
return newQueryPlan(createNode, p.stmt.ExplainQueryPlan), nil
}

func (p *createQueryPlanner) ensureTableDoesNotExist() (string, error) {
Expand Down Expand Up @@ -141,14 +175,20 @@ func (p *createQueryPlanner) schemaFrom() *kv.TableSchema {
return &schema
}

func (cp *createPlanner) ExecutionPlan() (*vm.ExecutionPlan, error) {
if cp.qp.queryPlan == nil {
_, err := cp.QueryPlan()
// ExecutionPlan returns the bytecode execution plan for the planner. Calling
// QueryPlan is not a prerequisite to this method as it will be called by
// ExecutionPlan if needed.
func (p *createPlanner) ExecutionPlan() (*vm.ExecutionPlan, error) {
if p.queryPlanner.queryPlan == nil {
_, err := p.QueryPlan()
if err != nil {
return nil, err
}
}
p := cp.ep
return p.executionPlanner.getExecutionPlan()
}

func (p *createExecutionPlanner) getExecutionPlan() (*vm.ExecutionPlan, error) {
p.executionPlan.Append(&vm.InitCmd{P2: 1})
p.executionPlan.Append(&vm.TransactionCmd{P2: 1})
p.executionPlan.Append(&vm.CreateBTreeCmd{P2: 1})
Expand Down
180 changes: 124 additions & 56 deletions planner/insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import (
"github.com/chirst/cdb/vm"
)

var errTableNotExist = errors.New("table does not exist")
var errValuesNotMatch = errors.New("values list did not match columns list")
var errMissingColumnName = errors.New("missing column")
var (
errTableNotExist = errors.New("table does not exist")
errValuesNotMatch = errors.New("values list did not match columns list")
errMissingColumnName = errors.New("missing column")
)

// pkConstraint is the error message displayed when a primary key constraint is
// violated.
const pkConstraint = "pk unique constraint violated"

// insertCatalog defines the catalog methods needed by the insert planner
type insertCatalog interface {
Expand All @@ -22,29 +28,50 @@ type insertCatalog interface {
GetPrimaryKeyColumn(tableName string) (string, error)
}

// insertPlanner consists of planners capable of generating a logical query plan
// tree and bytecode execution plan for a insert statement.
type insertPlanner struct {
qp *insertQueryPlanner
ep *insertExecutionPlanner
// The query planner generates a logical query plan tree made up of nodes
// similar to relational algebra operators. The query planner performs
// validation while building the tree. Otherwise known as binding.
queryPlanner *insertQueryPlanner
// The executionPlanner transforms the logical query plan tree to a bytecode
// execution plan that can be ran by the virtual machine.
executionPlanner *insertExecutionPlanner
}

// insertQueryPlanner converts the AST generated by the compiler to a logical
// query plan tree. It is also responsible for validating the AST against the
// system catalog.
type insertQueryPlanner struct {
catalog insertCatalog
stmt *compiler.InsertStmt
// catalog contains the schema.
catalog insertCatalog
// stmt contains the AST.
stmt *compiler.InsertStmt
// queryPlan contains the query plan being constructed. For an insert, the
// root node must be an insertNode.
queryPlan *insertNode
}

// insertExecutionPlanner converts the logical query plan to a bytecode routine
// to be ran by the vm.
type insertExecutionPlanner struct {
queryPlan *insertNode
// queryPlan contains the query plan generated by the query planner's
// QueryPlan method.
queryPlan *insertNode
// executionPlan contains the execution plan generated by calling
// ExecutionPlan.
executionPlan *vm.ExecutionPlan
}

// NewInsert returns an instance of an insert planner for the given AST.
func NewInsert(catalog insertCatalog, stmt *compiler.InsertStmt) *insertPlanner {
return &insertPlanner{
qp: &insertQueryPlanner{
queryPlanner: &insertQueryPlanner{
catalog: catalog,
stmt: stmt,
},
ep: &insertExecutionPlanner{
executionPlanner: &insertExecutionPlanner{
executionPlan: vm.NewExecutionPlan(
catalog.GetVersion(),
stmt.Explain,
Expand All @@ -53,8 +80,17 @@ func NewInsert(catalog insertCatalog, stmt *compiler.InsertStmt) *insertPlanner
}
}

func (ip *insertPlanner) QueryPlan() (*QueryPlan, error) {
p := ip.qp
// QueryPlan generates the query plan tree for the planner.
func (p *insertPlanner) QueryPlan() (*QueryPlan, error) {
qp, err := p.queryPlanner.getQueryPlan()
if err != nil {
return nil, err
}
p.executionPlanner.queryPlan = p.queryPlanner.queryPlan
return qp, err
}

func (p *insertQueryPlanner) getQueryPlan() (*QueryPlan, error) {
rootPage, err := p.catalog.GetRootPageNumber(p.stmt.TableName)
if err != nil {
return nil, errTableNotExist
Expand All @@ -78,66 +114,98 @@ func (ip *insertPlanner) QueryPlan() (*QueryPlan, error) {
colValues: p.stmt.ColValues,
}
p.queryPlan = insertNode
ip.ep.queryPlan = insertNode
return newQueryPlan(insertNode, p.stmt.ExplainQueryPlan), nil
}

func (ip *insertPlanner) ExecutionPlan() (*vm.ExecutionPlan, error) {
if ip.qp.queryPlan == nil {
_, err := ip.QueryPlan()
// ExecutionPlan returns the bytecode routine for the planner. Calling QueryPlan
// is not prerequisite to calling ExecutionPlan as ExecutionPlan will be called
// as needed.
func (p *insertPlanner) ExecutionPlan() (*vm.ExecutionPlan, error) {
if p.queryPlanner.queryPlan == nil {
_, err := p.QueryPlan()
if err != nil {
return nil, err
}
}
ep := ip.ep
cursorId := 1
ep.executionPlan.Append(&vm.InitCmd{P2: 1})
ep.executionPlan.Append(&vm.TransactionCmd{P2: 1})
ep.executionPlan.Append(&vm.OpenWriteCmd{P1: cursorId, P2: ep.queryPlan.rootPage})

for valueIdx := range len(ep.queryPlan.colValues) / len(ep.queryPlan.colNames) {
keyRegister := 1
statementIDIdx := -1
if ep.queryPlan.pkColumn != "" {
statementIDIdx = slices.IndexFunc(ep.queryPlan.colNames, func(s string) bool {
return s == ep.queryPlan.pkColumn
})
}
if statementIDIdx == -1 {
ep.executionPlan.Append(&vm.NewRowIdCmd{P1: ep.queryPlan.rootPage, P2: keyRegister})
} else {
rowId, err := strconv.Atoi(ep.queryPlan.colValues[statementIDIdx+valueIdx*len(ep.queryPlan.colNames)])
if err != nil {
return nil, err
}
integerCmdIdx := len(ep.executionPlan.Commands) + 2
ep.executionPlan.Append(&vm.NotExistsCmd{P1: ep.queryPlan.rootPage, P2: integerCmdIdx, P3: rowId})
ep.executionPlan.Append(&vm.HaltCmd{P1: 1, P4: "pk unique constraint violated"})
ep.executionPlan.Append(&vm.IntegerCmd{P1: rowId, P2: keyRegister})
return p.executionPlanner.getExecutionPlan()
}

func (p *insertExecutionPlanner) getExecutionPlan() (*vm.ExecutionPlan, error) {
p.buildInit()
p.openWrite()
for valueIdx := range len(p.queryPlan.colValues) / len(p.queryPlan.colNames) {
// For simplicity, the primary key is in the first register.
const keyRegister = 1
if err := p.buildPrimaryKey(keyRegister, valueIdx); err != nil {
return nil, err
}
registerIdx := keyRegister
for _, catalogColumnName := range ep.queryPlan.catalogColumnNames {
if catalogColumnName != "" && catalogColumnName == ep.queryPlan.pkColumn {
for _, catalogColumnName := range p.queryPlan.catalogColumnNames {
if catalogColumnName != "" && catalogColumnName == p.queryPlan.pkColumn {
// Skip the primary key column since it is handled before.
continue
}
registerIdx += 1
vIdx := -1
for i, statementColumnName := range ep.queryPlan.colNames {
if statementColumnName == catalogColumnName {
vIdx = i + (valueIdx * len(ep.queryPlan.colNames))
}
}
if vIdx == -1 {
return nil, fmt.Errorf("%w %s", errMissingColumnName, catalogColumnName)
if err := p.buildNonPkValue(valueIdx, registerIdx, catalogColumnName); err != nil {
return nil, err
}
ep.executionPlan.Append(&vm.StringCmd{P1: registerIdx, P4: ep.queryPlan.colValues[vIdx]})
}
ep.executionPlan.Append(&vm.MakeRecordCmd{P1: 2, P2: registerIdx - 1, P3: registerIdx + 1})
ep.executionPlan.Append(&vm.InsertCmd{P1: ep.queryPlan.rootPage, P2: registerIdx + 1, P3: keyRegister})
p.executionPlan.Append(&vm.MakeRecordCmd{P1: 2, P2: registerIdx - 1, P3: registerIdx + 1})
p.executionPlan.Append(&vm.InsertCmd{P1: p.queryPlan.rootPage, P2: registerIdx + 1, P3: keyRegister})
}
p.executionPlan.Append(&vm.HaltCmd{})
return p.executionPlan, nil
}

func (p *insertExecutionPlanner) buildInit() {
p.executionPlan.Append(&vm.InitCmd{P2: 1})
p.executionPlan.Append(&vm.TransactionCmd{P2: 1})
}

ep.executionPlan.Append(&vm.HaltCmd{})
return ep.executionPlan, nil
func (p *insertExecutionPlanner) openWrite() {
const cursorId = 1
p.executionPlan.Append(&vm.OpenWriteCmd{P1: cursorId, P2: p.queryPlan.rootPage})
}

func (p *insertExecutionPlanner) buildPrimaryKey(keyRegister int, valueIdx int) error {
// If the table has a user defined pk column it needs to be looked up in the
// user defined column list. If the user has defined the pk column the
// execution plan will involve checking the uniqueness of the pk during
// execution. Otherwise the system guarantees a unique key.
statementPkIdx := -1
if p.queryPlan.pkColumn != "" {
statementPkIdx = slices.IndexFunc(p.queryPlan.colNames, func(s string) bool {
return s == p.queryPlan.pkColumn
})
}
if statementPkIdx == -1 {
p.executionPlan.Append(&vm.NewRowIdCmd{P1: p.queryPlan.rootPage, P2: keyRegister})
return nil
}
rowId, err := strconv.Atoi(p.queryPlan.colValues[statementPkIdx+valueIdx*len(p.queryPlan.colNames)])
if err != nil {
return err
}
integerCmdIdx := len(p.executionPlan.Commands) + 2
p.executionPlan.Append(&vm.NotExistsCmd{P1: p.queryPlan.rootPage, P2: integerCmdIdx, P3: rowId})
p.executionPlan.Append(&vm.HaltCmd{P1: 1, P4: pkConstraint})
p.executionPlan.Append(&vm.IntegerCmd{P1: rowId, P2: keyRegister})
return nil
}

func (p *insertExecutionPlanner) buildNonPkValue(valueIdx, registerIdx int, catalogColumnName string) error {
// Get the statement index of the column name. Because the name positions
// can mismatch the table column positions.
stmtColIdx := slices.IndexFunc(p.queryPlan.colNames, func(stmtColName string) bool {
return stmtColName == catalogColumnName
})
// Requires the statement to define a value for each column in the table.
if stmtColIdx == -1 {
return fmt.Errorf("%w %s", errMissingColumnName, catalogColumnName)
}
valuesListIdx := stmtColIdx + (valueIdx * len(p.queryPlan.colNames))
p.executionPlan.Append(&vm.StringCmd{P1: registerIdx, P4: p.queryPlan.colValues[valuesListIdx]})
return nil
}

func checkValuesMatchColumns(s *compiler.InsertStmt) error {
Expand Down
Loading
Loading