@@ -2,7 +2,10 @@ package plan
22
33import (
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.
415464func 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).
425547func 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