// Package explain renders a plan as text for EXPLAIN/PROFILE (port of the // Rust explain.rs, trimmed of the recognizer-kernel operators and rank-mode // gating -- estimates always render). PROFILE per-operator counts arrive // via the Profile seam, populated by the executor in a later milestone.
package explain import ( ) // Profile carries PROFILE's actual per-operator row counts, in the same // branch-major segment order the renderer walks. type Profile struct { Segs []SegProf } // SegProf profiles one segment. type SegProf struct { Stages []StageProf ProjRows uint64 PostWhereRows *uint64 } // StageProf profiles one stage: Match holds one count per op plus a // trailing one for the stage WHERE; Single is a one-output stage. type StageProf struct { Match []uint64 Single *uint64 } // Render renders the plan tree with estimates (always) and PROFILE counts // (when prof is non-nil), as one line per operator. func ( *plan.Plan, *Profile, time.Duration, *plan.Estimates) []string { var []string if != nil { = append(, "PROFILE") } else { = append(, "EXPLAIN") } = append(, fmt.Sprintf("Planning: %.3f ms", .Seconds()*1000)) if != nil { if != nil { = append(, fmt.Sprintf("%-54s %10s %10s", "", "est", "rows")) } else { = append(, fmt.Sprintf("%-54s %16s", "", "est")) } } render(&, , , , true) return } // Canonical renders a plan as a stable shape string for golden // plan-regression snapshots: operator order and indentation, scan sources, // hop patterns, the recognizers that fired (hash join, SP gate, mono // var-expand, decorrelation, distinct, aggregate) and the chosen anchor note // -- but NOT the wall-clock planning time, the "est" header, or the per-operator // numeric estimates, which drift with the fixture while the shape and the anchor // choice should not. est supplies the anchor notes only; its numbers are omitted. // The result is result-independent (it never executes) and deterministic across // plannings, so a diff is a review prompt that a planner change moved a plan. func ( *plan.Plan, *plan.Estimates) []string { var []string render(&, , nil, , false) return } func render( *[]string, *plan.Plan, *Profile, *plan.Estimates, bool) { := len(.Branches) > 1 := 0 for , := range .Branches { := "" if { if > 0 { if .Union[-1] == ast.UnionAll { * = append(*, "UNION ALL") } else { * = append(*, "UNION") } } * = append(*, fmt.Sprintf("Branch %d", )) = " " } := len() > 1 for , := range { var *SegProf if != nil && < len(.Segs) { = &.Segs[] } var *plan.SegEst if != nil && < len(.Segs) { = &.Segs[] } ++ := if { * = append(*, fmt.Sprintf("%sSegment %d", , )) = + " " } renderSegment(, , , , , ) } } } func renderSegment( *[]string, *plan.Segment, string, *SegProf, *plan.SegEst, bool) { // slot -> variable name (anonymous slots render as _). := make([]string, .RowWidth) for , := range .Slots { if < len() { [] = } } // The canonical (numbers=false) form pins plan shape and the anchor // decision but omits the per-operator estimate/profile columns; numse/numsp // gate those numeric lookups while se still supplies the anchor notes. , := , if ! { , = nil, nil } for , := range .Stages { switch s := .(type) { case *plan.MatchStage: var , []uint64 if != nil && < len(.Stages) { = .Stages[].Match } if != nil && < len(.Stages) { = .Stages[].Match } := "" if != nil && < len(.AnchorNotes) { = .AnchorNotes[] } for := range .Ops { line(, , opLabel(&.Ops[], ), at(, ), at(, )) if == 0 && != "" { := if ! { = stripNoteMagnitudes() } * = append(*, +" "+) } } if .Where != nil { line(, , "Filter ("+fmtExpr(.Where)+")", at(, len(.Ops)), at(, len(.Ops))) } case *plan.HashJoinStage: var string if .KeyProbe != nil { = fmt.Sprintf("ValueHashJoin (key %s = %s)", fmtExpr(.KeyBuild), fmtExpr(.KeyProbe)) } else { = fmt.Sprintf("HashJoin (key=%s, probe %s)", nameOf(.KeySlot, ), fmtHop(nameOf(.Probe.From, ), .Probe.Dir, .Probe.Types, nameOf(.Probe.To, ), "")) } line(, , , singleEst(, ), singleCount(, )) for , := range .Build { for := range .Ops { line(, +" build: ", opLabel(&.Ops[], ), nil, nil) } if .Where != nil { line(, +" build: ", "Filter ("+fmtExpr(.Where)+")", nil, nil) } } if .Where != nil { line(, , "Filter ("+fmtExpr(.Where)+")", nil, nil) } case *plan.SpStage: := "ShortestPath" if .All { = "AllShortestPaths" } else if .Weight != nil { = "WeightedShortestPath" } := fmt.Sprintf("%s (%s)-[*]-(%s)", , nameOf(.From, ), nameOf(.To, )) line(, , , singleEst(, ), singleCount(, )) case *plan.GateStage: := "ShortestPath" if .Sp.Weight != nil { = "WeightedShortestPath" } := fmt.Sprintf("Gate (%s (%s)-[*]-(%s), %s)", , nameOf(.Sp.From, ), nameOf(.Sp.To, ), fmtExpr(.Where)) line(, , , singleEst(, ), singleCount(, )) case *plan.CallStage: := callLabel(&.Proc) if .ProcName != "" { = fmt.Sprintf("%s(…) [correlated, %d args]", .ProcName, len(.ArgExprs)) } line(, , "Call "+, singleEst(, ), singleCount(, )) case *plan.UnwindStage: := fmt.Sprintf("Unwind (%s AS %s)", fmtExpr(.List), nameOf(.OutSlot, )) line(, , , singleEst(, ), singleCount(, )) case *plan.CallSubqueryStage: line(, , "CallSubquery ["+strings.Join(.Sub.Columns, ", ")+"]", singleEst(, ), singleCount(, )) // Render the nested sub-plan indented under the operator, // dropping the sub-render's own header lines. := Render(.Sub, nil, 0, nil) for , := range [2:] { * = append(*, +" "+) } case *plan.GroupJoinStage: := make([]string, len(.KeySlots)) for , := range .KeySlots { [] = nameOf(, ) } line(, , "GroupJoin (key="+strings.Join(, ", ")+") ["+strings.Join(.Sub.Columns, ", ")+"]", singleEst(, ), singleCount(, )) // The standalone inner renders indented like a CALL subquery's. := Render(.Sub, nil, 0, nil) for , := range [2:] { * = append(*, +" "+) } } } := &.Proj var , *uint64 if != nil { = &.ProjRows } if != nil { = .ProjRows } if .Aggregated { var []string for , := range .GroupIdx { = append(, .Returns[].Name) } var []string for := range .Aggs { = append(, fmtAgg(&.Aggs[])) } line(, , fmt.Sprintf("Aggregate (group=[%s]; %s)", strings.Join(, ", "), strings.Join(, ", ")), , ) } else { := "" if .Distinct { = "Distinct " } line(, , fmt.Sprintf("%sProject [%s]", , strings.Join(.Columns, ", ")), , ) } if len(.OrderBy) > 0 { var []string for , := range .OrderBy { = append(, fmtSort()) } line(, , "OrderBy ["+strings.Join(, ", ")+"]", nil, nil) } if .Skip != nil { line(, , fmt.Sprintf("Offset %d", *.Skip), nil, nil) } if .Limit != nil { line(, , fmt.Sprintf("Limit %d", *.Limit), nil, nil) } if .PostWhere != nil { var , *uint64 if != nil { = .PostWhereRows } if != nil { = .PostWhereRows } line(, , "Filter ("+fmtExpr(.PostWhere)+")", , ) } } // stripNoteMagnitudes collapses each maximal digit run in an anchor note to a // single 'N', so the canonical form pins the anchor CHOICE and its tie-break // REASON (both text -- e.g. which variable anchors, "smaller leaf cardinality") // without the card / fan-out magnitudes, which are estimates that drift with // the fixture while the decision does not. func stripNoteMagnitudes( string) string { var strings.Builder .Grow(len()) := false for , := range { if >= '0' && <= '9' { if ! { .WriteByte('N') = true } continue } = false .WriteRune() } return .String() } // at is a bounds-checked element pointer into a count slice. func at( []uint64, int) *uint64 { if < 0 || >= len() { return nil } return &[] } func singleCount( *SegProf, int) *uint64 { if == nil || >= len(.Stages) { return nil } return .Stages[].Single } func singleEst( *plan.SegEst, int) *uint64 { if == nil || >= len(.Stages) { return nil } return .Stages[].Single } // line appends one operator line with up to two right-aligned numeric // columns (est, then the PROFILE actual). func line( *[]string, , string, , *uint64) { switch { case == nil && == nil: * = append(*, +) case != nil && != nil: * = append(*, fmt.Sprintf("%s%-54s %10s %10s", , , plan.GroupDigits(*), plan.GroupDigits(*))) case != nil: * = append(*, fmt.Sprintf("%s%-54s %16s", , , plan.GroupDigits(*))) default: * = append(*, fmt.Sprintf("%s%-54s %16s", , , plan.GroupDigits(*))) } } func nameOf( int, []string) string { if >= 0 && < len() && [] != "" { return [] } return "_" } func opLabel( *plan.BindOp, []string) string { switch .Kind { case plan.OpScan: := nameOf(.Slot, ) := &.Source switch .Kind { case plan.ScanProperty: return fmt.Sprintf("NodeByProperty (%s:%s {%s = %s})", , .Label, .Key, fmtLit(.Value)) case plan.ScanLabel: return fmt.Sprintf("NodeScan (%s:%s%s)", , .Label, fmtProps(.Props)) case plan.ScanNodeID: return fmt.Sprintf("NodeBySeek (%s = id %s)", , fmtLit(.Value)) case plan.ScanNodeIDVar: return fmt.Sprintf("NodeBySeek (%s = id %s)", , nameOf(.Slot, )) case plan.ScanPropertyIn: := make([]string, len(.Values)) for , := range .Values { [] = fmtLit() } return fmt.Sprintf("NodeByPropertyIn (%s:%s {%s IN [%s]})", , .Label, .Key, strings.Join(, ", ")) case plan.ScanTextMatch: return fmt.Sprintf("NodeByTextIndex (%s:%s {%s %s %s})", , .Label, .Field, binopStr(.Mode), fmtLit(.Value)) case plan.ScanExistsSeed: := .Label if == "" { = "*" } return fmt.Sprintf("NodeByExistsSeed (%s:%s, %d chain(s))", , , len(.Seeds)) case plan.ScanArg: return fmt.Sprintf("Argument (%s%s%s)", , fmtLabels(.Labels), fmtProps(.Props)) default: return fmt.Sprintf("AllNodesScan (%s)", ) } case plan.OpExpand: := fmtHop(nameOf(.From, ), .Dir, .Types, nameOf(.To, ), "") if .Rebind { return "Expand " + + " [into bound]" } return "Expand " + default: // OpVarExpand := fmt.Sprintf("*%d..", .Min) if .Max != nil { = fmt.Sprintf("*%d..%d", .Min, *.Max) } := "" if .MonoHop != nil { := "desc" if .MonoHop.Ascending { = "asc" } := "" if .MonoHop.NullsPass { = " nullspass" } = fmt.Sprintf(" [mono %s %s%s]", .MonoHop.RelKey, , ) } return "VarExpand " + fmtHop(nameOf(.From, ), .Dir, .Types, nameOf(.To, ), ) + } } func fmtHop( string, graph.Direction, []string, , string) string { := "" if len() > 0 { = ":" + strings.Join(, "|") } := "[" + + + "]" switch { case graph.Outgoing: return fmt.Sprintf("(%s)-%s->(%s)", , , ) case graph.Incoming: return fmt.Sprintf("(%s)<-%s-(%s)", , , ) } return fmt.Sprintf("(%s)-%s-(%s)", , , ) } func fmtLabels( []string) string { if len() == 0 { return "" } return ":" + strings.Join(, ":") } func fmtProps( []ast.PropEntry) string { if len() == 0 { return "" } := make([]string, len()) for , := range { [] = .Key + ": " + fmtLit(.Val) } return " {" + strings.Join(, ", ") + "}" } // callLabel is a short human-readable label for a CALL procedure. func callLabel( *plan.CallProc) string { switch .Kind { case plan.ProcWcc: return fmt.Sprintf("wcc('%s')", .RelType) case plan.ProcFtsSearch: return fmt.Sprintf("fts.search('%s', '%s', '%s')", .Label, .Field, .Query) case plan.ProcGeoWithinRadius: return fmt.Sprintf("geo.withinRadius('%s', …)", .Label) case plan.ProcGeoWithinBBox: return fmt.Sprintf("geo.withinBBox('%s', …)", .Label) case plan.ProcBfs: return fmt.Sprintf("algo.bfs(%d, …)", .Source) case plan.ProcPageRank: return "algo.pagerank(…)" case plan.ProcWccAll: return "algo.wcc()" case plan.ProcCdlp: return "algo.cdlp(…)" case plan.ProcLcc: return "algo.lcc(…)" case plan.ProcPropagate: return fmt.Sprintf("algo.propagate(%d seeds, %s, depth %d, …)", len(.Seeds), strings.Join(.RelTypes, "|"), .MaxDepth) default: return fmt.Sprintf("algo.sssp(%d, …)", .Source) } }