Skip to content

Commit fbe3def

Browse files
committed
test: increase coverage from 57% to 73% across all packages
1 parent 3bdd985 commit fbe3def

13 files changed

Lines changed: 2709 additions & 15 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
run: go mod download
5353

5454
- name: Run tests with coverage
55-
run: go test -v -race -coverprofile=coverage.out ./...
55+
run: go test -v -race -coverprofile=coverage.out -coverpkg=./... ./...
5656

5757
- name: Generate coverage report
5858
run: go tool cover -html=coverage.out -o coverage.html

pkg/plan/analyzer.go

Lines changed: 234 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package plan
22

33
import (
44
"encoding/json"
5+
"encoding/xml"
56
"fmt"
7+
"strconv"
8+
"strings"
69
)
710

811
// PlanAnalyzer analyzes execution plans and provides optimization suggestions
@@ -411,22 +414,239 @@ func parsePostgreSQLNode(data map[string]interface{}) *PlanNode {
411414
return node
412415
}
413416

414-
// parseSQLServerXMLPlan parses SQL Server XML execution plan format
417+
// sqlServerXMLPlan is the top-level XML structure for SQL Server execution plans.
418+
// SQL Server EXPLAIN FOR XML produces a ShowPlanXML document.
419+
type sqlServerXMLPlan struct {
420+
XMLName xml.Name `xml:"ShowPlanXML"`
421+
Statements []sqlServerXMLStmt `xml:"BatchSequence>Batch>Statements>StmtSimple"`
422+
}
423+
424+
type sqlServerXMLStmt struct {
425+
StatementCost float64 `xml:"StatementSubTreeCost,attr"`
426+
StatementRows float64 `xml:"StatementEstRows,attr"`
427+
QueryPlan sqlServerQueryPlan `xml:"QueryPlan"`
428+
}
429+
430+
type sqlServerQueryPlan struct {
431+
RelOp sqlServerRelOp `xml:"RelOp"`
432+
}
433+
434+
type sqlServerRelOp struct {
435+
Operation string `xml:"PhysicalOp,attr"`
436+
LogicalOp string `xml:"LogicalOp,attr"`
437+
EstimatedRows float64 `xml:"EstimateRows,attr"`
438+
EstimatedCost float64 `xml:"EstimatedTotalSubtreeCost,attr"`
439+
Children []sqlServerRelOp `xml:"RelOp"`
440+
IndexScan *sqlServerIndex `xml:"IndexScan"`
441+
NestedLoops *sqlServerNested `xml:"NestedLoops"`
442+
}
443+
444+
type sqlServerIndex struct {
445+
Object []sqlServerObject `xml:"Object"`
446+
}
447+
448+
type sqlServerObject struct {
449+
Table string `xml:"Table,attr"`
450+
Index string `xml:"Index,attr"`
451+
Schema string `xml:"Schema,attr"`
452+
}
453+
454+
type sqlServerNested struct {
455+
Predicate *sqlServerPredicate `xml:"Predicate"`
456+
}
457+
458+
type sqlServerPredicate struct {
459+
ScalarOperator string `xml:",innerxml"`
460+
}
461+
462+
// parseSQLServerXMLPlan parses SQL Server XML execution plan format.
463+
// SQL Server outputs ShowPlanXML when using SET SHOWPLAN_XML ON or EXPLAIN FOR XML.
415464
func parseSQLServerXMLPlan(xmlData []byte) (*ExecutionPlan, error) {
416-
// TODO: Implement SQL Server XML plan parsing
417-
// SQL Server uses XML format which requires XML parsing
418-
return &ExecutionPlan{
419-
Dialect: "sqlserver",
420-
Warnings: []string{"SQL Server XML plan parsing not yet implemented"},
421-
}, nil
465+
var root sqlServerXMLPlan
466+
if err := xml.Unmarshal(xmlData, &root); err != nil {
467+
return nil, fmt.Errorf("failed to parse SQL Server XML plan: %w", err)
468+
}
469+
470+
plan := &ExecutionPlan{Dialect: "sqlserver"}
471+
472+
if len(root.Statements) == 0 {
473+
return plan, nil
474+
}
475+
476+
stmt := root.Statements[0]
477+
plan.TotalCost = stmt.StatementCost
478+
plan.EstimatedRows = int64(stmt.StatementRows)
479+
plan.RootNode = parseSQLServerRelOp(&stmt.QueryPlan.RelOp)
480+
481+
return plan, nil
482+
}
483+
484+
func parseSQLServerRelOp(op *sqlServerRelOp) *PlanNode {
485+
if op == nil {
486+
return nil
487+
}
488+
489+
node := &PlanNode{
490+
Operation: op.Operation,
491+
Extra: make(map[string]any),
492+
}
493+
494+
if op.LogicalOp != "" {
495+
node.Extra["logical_op"] = op.LogicalOp
496+
}
497+
498+
node.Cost = &Cost{TotalCost: op.EstimatedCost}
499+
node.Rows = &RowEstimate{Estimated: int64(op.EstimatedRows)}
500+
501+
// Map SQL Server operation names to NodeType
502+
switch strings.ToUpper(op.Operation) {
503+
case "CLUSTERED INDEX SCAN":
504+
node.NodeType = NodeTypeClusteredIndexScan
505+
case "NONCLUSTERED INDEX SCAN", "INDEX SCAN":
506+
node.NodeType = NodeTypeNonClusteredIndexScan
507+
case "TABLE SCAN":
508+
node.NodeType = NodeTypeTableScan
509+
case "CLUSTERED INDEX SEEK", "INDEX SEEK":
510+
node.NodeType = NodeTypeIndexScan
511+
case "NESTED LOOPS":
512+
node.NodeType = NodeTypeNestedLoop
513+
case "HASH MATCH":
514+
node.NodeType = NodeTypeHashJoin
515+
case "MERGE JOIN":
516+
node.NodeType = NodeTypeMergeJoin
517+
case "SORT":
518+
node.NodeType = NodeTypeSort
519+
case "STREAM AGGREGATE", "HASH AGGREGATE":
520+
node.NodeType = NodeTypeAggregate
521+
case "FILTER":
522+
node.NodeType = NodeTypeFilter
523+
case "TOP":
524+
node.NodeType = NodeTypeLimit
525+
}
526+
527+
// Extract table name from IndexScan object
528+
if op.IndexScan != nil && len(op.IndexScan.Object) > 0 {
529+
obj := op.IndexScan.Object[0]
530+
node.Table = strings.Trim(obj.Table, "[]")
531+
node.Index = strings.Trim(obj.Index, "[]")
532+
}
533+
534+
// Recursively parse children
535+
for i := range op.Children {
536+
child := parseSQLServerRelOp(&op.Children[i])
537+
if child != nil {
538+
node.Children = append(node.Children, child)
539+
}
540+
}
541+
542+
return node
422543
}
423544

424-
// parseSQLiteTextPlan parses SQLite text execution plan format
545+
// parseSQLiteTextPlan parses SQLite EXPLAIN QUERY PLAN text output.
546+
// Format: "id|parent|notused|detail" per line (pipe-separated).
425547
func parseSQLiteTextPlan(textData []byte) (*ExecutionPlan, error) {
426-
// TODO: Implement SQLite text plan parsing
427-
// SQLite uses a simple text format
428-
return &ExecutionPlan{
429-
Dialect: "sqlite",
430-
Warnings: []string{"SQLite text plan parsing not yet implemented"},
431-
}, nil
548+
plan := &ExecutionPlan{Dialect: "sqlite"}
549+
lines := strings.Split(strings.TrimSpace(string(textData)), "\n")
550+
551+
type row struct {
552+
id int
553+
parent int
554+
detail string
555+
}
556+
557+
var rows []row
558+
for _, line := range lines {
559+
line = strings.TrimSpace(line)
560+
if line == "" {
561+
continue
562+
}
563+
parts := strings.SplitN(line, "|", 4)
564+
if len(parts) < 4 {
565+
// Try space-separated (older SQLite)
566+
parts = strings.Fields(line)
567+
if len(parts) < 4 {
568+
continue
569+
}
570+
}
571+
id, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
572+
parent, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
573+
if err1 != nil || err2 != nil {
574+
continue
575+
}
576+
rows = append(rows, row{id: id, parent: parent, detail: strings.TrimSpace(parts[3])})
577+
}
578+
579+
if len(rows) == 0 {
580+
return plan, nil
581+
}
582+
583+
// Build tree: map id -> node
584+
nodes := make(map[int]*PlanNode, len(rows))
585+
for _, r := range rows {
586+
node := parseSQLiteDetail(r.detail)
587+
nodes[r.id] = node
588+
}
589+
590+
// Link children to parents; root has parent == 0
591+
var root *PlanNode
592+
for _, r := range rows {
593+
node := nodes[r.id]
594+
if r.parent == 0 {
595+
root = node
596+
} else if parent, ok := nodes[r.parent]; ok {
597+
parent.Children = append(parent.Children, node)
598+
}
599+
}
600+
601+
plan.RootNode = root
602+
return plan, nil
603+
}
604+
605+
func parseSQLiteDetail(detail string) *PlanNode {
606+
node := &PlanNode{
607+
Operation: detail,
608+
Extra: make(map[string]any),
609+
}
610+
611+
upper := strings.ToUpper(detail)
612+
switch {
613+
case strings.Contains(upper, "SCAN TABLE"):
614+
node.NodeType = NodeTypeFullTableScan
615+
node.Table = extractSQLiteTable(detail, "SCAN TABLE")
616+
case strings.Contains(upper, "SEARCH TABLE"):
617+
node.NodeType = NodeTypeIndexScan
618+
node.Table = extractSQLiteTable(detail, "SEARCH TABLE")
619+
case strings.Contains(upper, "SCAN SUBQUERY"):
620+
node.NodeType = NodeTypeSubquery
621+
case strings.Contains(upper, "USE TEMP B-TREE FOR ORDER BY"):
622+
node.NodeType = NodeTypeSort
623+
case strings.Contains(upper, "USE TEMP B-TREE"):
624+
node.NodeType = NodeTypeAggregate
625+
case strings.Contains(upper, "COMPOUND SUBQUERIES"):
626+
node.NodeType = NodeTypeUnion
627+
}
628+
629+
// Extract USING INDEX
630+
if idx := strings.Index(upper, "USING INDEX "); idx >= 0 {
631+
rest := detail[idx+len("USING INDEX "):]
632+
fields := strings.Fields(rest)
633+
if len(fields) > 0 {
634+
node.Index = fields[0]
635+
}
636+
}
637+
638+
return node
639+
}
640+
641+
func extractSQLiteTable(detail, keyword string) string {
642+
idx := strings.Index(strings.ToUpper(detail), strings.ToUpper(keyword))
643+
if idx < 0 {
644+
return ""
645+
}
646+
rest := strings.TrimSpace(detail[idx+len(keyword):])
647+
fields := strings.Fields(rest)
648+
if len(fields) > 0 {
649+
return fields[0]
650+
}
651+
return ""
432652
}

0 commit comments

Comments
 (0)