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

fix: explain query plan formatting #147

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
19 changes: 12 additions & 7 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ type StatementResult struct {
ColumnNames []string
RowCh chan rowResult
Err error
Query string
}

func newStatementResult(columnNames []string, rowCh chan rowResult) *StatementResult {
return &StatementResult{ColumnNames: columnNames, RowCh: rowCh}
func newStatementResult(columnNames []string, rowCh chan rowResult, query string) *StatementResult {
return &StatementResult{ColumnNames: columnNames, RowCh: rowCh, Query: query}
}

func newStatementResultWithError(err error) *StatementResult {
Expand Down Expand Up @@ -172,7 +173,7 @@ func (db *Db) executeQuery(query string, statementResultCh chan StatementResult)

defer rows.Close()

return readQueryResults(rows, statementResultCh)
return readQueryResults(rows, statementResultCh, query)
}

func (db *Db) prepareStatementsIntoQueries(statementsString string) []string {
Expand Down Expand Up @@ -220,14 +221,18 @@ func getColumnTypes(rows *sql.Rows) ([]reflect.Type, error) {
return types, nil
}

func readQueryResults(queryRows *sql.Rows, statementResultCh chan StatementResult) (shouldContinue bool) {
func readQueryResults(queryRows *sql.Rows, statementResultCh chan StatementResult, query string) (shouldContinue bool) {
queries, _ := sqliteparserutils.SplitStatement(query)
queryIndex := 0
hasResultSetToRead := true
for hasResultSetToRead {
if shouldContinue := readQueryResultSet(queryRows, statementResultCh); !shouldContinue {
query := queries[queryIndex]
if shouldContinue := readQueryResultSet(queryRows, statementResultCh, query); !shouldContinue {
return false
}

hasResultSetToRead = queryRows.NextResultSet()
queryIndex++
}

if err := queryRows.Err(); err != nil {
Expand All @@ -238,7 +243,7 @@ func readQueryResults(queryRows *sql.Rows, statementResultCh chan StatementResul
return true
}

func readQueryResultSet(queryRows *sql.Rows, statementResultCh chan StatementResult) (shouldContinue bool) {
func readQueryResultSet(queryRows *sql.Rows, statementResultCh chan StatementResult, query string) (shouldContinue bool) {
columnNames, err := getColumnNames(queryRows)
if err != nil {
statementResultCh <- *newStatementResultWithError(err)
Expand All @@ -264,7 +269,7 @@ func readQueryResultSet(queryRows *sql.Rows, statementResultCh chan StatementRes
rowCh := make(chan rowResult)
defer close(rowCh)

statementResultCh <- *newStatementResult(columnNames, rowCh)
statementResultCh <- *newStatementResult(columnNames, rowCh, query)

for queryRows.Next() {
err = queryRows.Scan(columnPointers...)
Expand Down
54 changes: 54 additions & 0 deletions internal/db/explainTreeBuilder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package db

import (
"fmt"
)

type QueryPlanNode struct {
ID string
ParentID string
NotUsed string
Detail string
Children []*QueryPlanNode
}

func BuildQueryPlanTree(rows [][]string) (*QueryPlanNode, error) {
var nodes []*QueryPlanNode
nodeMap := make(map[string]*QueryPlanNode)

for _, row := range rows {
id := row[0]
parentId := row[1]
notUsed := row[2]
detail := row[3]

node := &QueryPlanNode{
ID: id,
ParentID: parentId,
NotUsed: notUsed,
Detail: detail,
}

nodes = append(nodes, node)
nodeMap[id] = node
}

root := &QueryPlanNode{}
for _, node := range nodes {
if node.ParentID == "0" {
root = node
} else {
parent := nodeMap[node.ParentID]
parent.Children = append(parent.Children, node)
}
}

return root, nil
}

func PrintQueryPlanTree(node *QueryPlanNode, indent string) {
fmt.Printf("%s%s\n", indent, node.Detail)
for _, child := range node.Children {
PrintQueryPlanTree(child, indent+" ")
}
}
30 changes: 28 additions & 2 deletions internal/db/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ type Printer interface {
print(statementResult StatementResult, outF io.Writer) error
}

type ExplainQueryPrinter struct{}

func (eqp ExplainQueryPrinter) print(statementResult StatementResult, outF io.Writer) error {
data := [][]string{}

tableData, err := appendData(statementResult, data, TABLE)
if err != nil {
return err
}

root, err := BuildQueryPlanTree(tableData)
if err != nil {
return err
}
println("QUERY PLAN")
PrintQueryPlanTree(root, "")

return nil
}

type TablePrinter struct {
withoutHeader bool
}
Expand All @@ -32,6 +52,7 @@ func (t TablePrinter) print(statementResult StatementResult, outF io.Writer) err

table.AppendBulk(tableData)
table.Render()

return nil
}

Expand Down Expand Up @@ -100,10 +121,14 @@ func appendData(statementResult StatementResult, data [][]string, mode FormatTyp
}
data = append(data, formattedRow)
}

return data, nil
}

func getPrinter(mode enums.PrintMode, withoutHeader bool) (Printer, error) {
func getPrinter(mode enums.PrintMode, withoutHeader bool, isExplainQueryPlan bool) (Printer, error) {
if isExplainQueryPlan {
return &ExplainQueryPrinter{}, nil
}
switch mode {
case enums.TABLE_MODE:
return &TablePrinter{
Expand Down Expand Up @@ -143,7 +168,8 @@ func PrintStatementResult(statementResult StatementResult, outF io.Writer, witho
return &UnableToPrintStatementResult{}
}

printer, err := getPrinter(mode, withoutHeader)
isExplainQueryPlan := IsResultComingFromExplainQueryPlan(statementResult)
printer, err := getPrinter(mode, withoutHeader, isExplainQueryPlan)
if err != nil {
return err
}
Expand Down
23 changes: 23 additions & 0 deletions internal/db/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package db

import (
"net/url"
"reflect"
"strings"
"unicode"
)
Expand Down Expand Up @@ -45,3 +46,25 @@ func NeedsEscaping(name string) bool {
}
return false
}

var explainQueryPlanStatement = "EXPLAIN QUERY PLAN"
var explainQueryPlanColumnNames = []string{"id", "parent", "notused", "detail"}

func queryContainsExplainQueryPlanStatement(query string) bool {
return strings.HasPrefix(
strings.ToLower(query),
strings.ToLower(explainQueryPlanStatement),
)
}

func columnNamesMatchExplainQueryPlan(colNames []string) bool {
return reflect.DeepEqual(colNames, explainQueryPlanColumnNames)
}

// "query" can be a string containing multiple queries separated by ";" or a single query
func IsResultComingFromExplainQueryPlan(statementResult StatementResult) bool {
query := statementResult.Query
columnNames := statementResult.ColumnNames
return queryContainsExplainQueryPlanStatement(query) &&
columnNamesMatchExplainQueryPlan(columnNames)
}
13 changes: 13 additions & 0 deletions test/db_root_command_shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ func (s *DBRootCommandShellSuite) Test_GivenATableNameWithSpecialCharacters_When
s.tc.AssertSqlEquals(outS, expected)
}

func (s *DBRootCommandShellSuite) Test_GivenATableNameWithTheSameSignatureAsExpainQueryPlan_WhenQueryingIt_ExpectNotToBeTreatedAsExplainQueryPlan() {
_, _, err := s.tc.Execute("CREATE TABLE fake_explain (ID INTEGER PRIMARY KEY, PARENT INTEGER, NOTUSED INTEGER, DETAIL TEXT);")
s.tc.Assert(err, qt.IsNil)

outS, errS, err := s.tc.ExecuteShell([]string{"SELECT * FROM fake_explain;"})
Copy link
Contributor

@WilsonNet WilsonNet Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you be testing the EXPLAI QUERY PLAN command?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is testing that a table with the same column names as the EQP table will still be treated as a normal table (which it should).

s.tc.Assert(err, qt.IsNil)
s.tc.Assert(errS, qt.Equals, "")

expected := "id parent notused detail"

s.tc.AssertSqlEquals(outS, expected)
}

func (s *DBRootCommandShellSuite) Test_GivenATableWithRecordsWithSingleQuote_WhenCalllSelectAllFromTable_ExpectSingleQuoteScape() {
s.tc.CreateEmptySimpleTable("t")
_, errS, err := s.tc.Execute("INSERT INTO t VALUES (0, \"x'x\", 0)")
Expand Down
Loading