// Plan entry points: desugar + split a query into UNION branches and// projection-boundary segments (port of the Rust plan.rs top half). The// planner is cost-based only: the Rust engine's rank/cost mode switch and// thread-local override do not exist here -- the cost branch is the one// and only strategy, layered on the same rank tiers so plan shapes match// the Rust cost mode exactly.
package planimport ()// planCtx carries adaptive-anchor state across a planning pass. On the// primary pass `ties` collects the patterns whose anchor orientation was a// both-endpoints-unbound-param-seek tie the planner could only break with// value-independent average degree -- exactly the auto-parameterization// hazard. When exactly one such tie exists, a second pass with// `forceReverse` set to that pattern re-plans it the other way, yielding the// sibling Plan the cached executor chooses between per execution.type planCtx struct { ties []*ast.Pattern forceReverse *ast.Pattern}// Build desugars and plans a query against g's statistics.func ( *ast.Query, graph.Graph) (*Plan, error) {// Normalize before planning: non-literal inline pattern properties // lower to WHERE equalities, reaching nested CALL {} bodies, so the // recursive BuildWithInCols below need not re-run it.if := semantics.Desugar(); != nil {returnnil, } := &planCtx{} , := buildWithInColsCtx(, nil, , )if != nil {returnnil, }// One or more qualifying auto-param anchor ties -> build the sibling // with the FIRST tie's orientation flipped, leaving any further ties // static. A full 2^n sibling set is out of the question, but one // sibling for the earliest-planned tie strictly dominates giving up: // the query with MORE value-blind decisions used to get LESS help. // The adaptive chooser scores primary vs sibling by real first-hop // fan-out and falls back to the primary whenever it cannot score, so // extra unflipped ties cost nothing. .Ties = len(.ties)iflen(.ties) >= 1 { , := buildWithInColsCtx(, nil, , &planCtx{forceReverse: .ties[0]})if == nil && != nil { .Alt = } }return , nil}// BuildWithInCols plans a query whose branches each begin with inCols// already bound, carried in from an outer scope (a CALL {} subquery plans// its body with inCols = its import list). Adaptive anchoring is scoped to// the top-level Build pass BY DECISION, not accident: a subquery body// executes once per outer row, so a per-execution anchor probe there// would run per row rather than per statement -- the probe's O(1)-ish// budget assumes statement frequency. Subquery bodies stay on the static// fallback until a per-row-amortized scoring exists.func ( *ast.Query, []string, graph.Graph) (*Plan, error) {returnbuildWithInColsCtx(, , , &planCtx{})}func buildWithInColsCtx( *ast.Query, []string, graph.Graph, *planCtx) (*Plan, error) { := make([][]*Segment, 0, len(.Parts))var []stringfor := range .Parts { , , := planPart(&.Parts[], , , )if != nil {returnnil, }// Every UNION branch must project the same column names in order.if == nil { = } elseif !stringsEqual(, ) {returnnil, bindErrf("all UNION branches must return the same columns: [%s] vs [%s]",joinComma(), joinComma()) }fuseSortLimit() = append(, ) }return &Plan{Branches: , Union: .Union, Columns: }, nil}// planPart plans one UNION branch into its segment pipeline, returning the// segments and the branch's output column names.func planPart( *ast.QueryPart, []string, graph.Graph, *planCtx) ([]*Segment, []string, error) {var []*Segmentvar []stageSpec := // Each MATCH clause is one relationship-uniqueness scope: its comma // patterns (and any planner splits of them) must bind pairwise // distinct relationships (ISO GQL's DIFFERENT EDGES default). := uint32(0) , := fuseTrailingProjectionIntoRet(fuseProjectionBeforeAggregate(.Clauses), .Ret)for , := range {switch c := .(type) {case *ast.Match:// Comma-separated patterns bind sequentially; the clause WHERE // applies once, after all of them (attached to the last). ++ := len(.Patterns) - 1for := range .Patterns {varast.Exprif == { = .Where } = append(, stageSpec{kind: specMatch, pattern: &.Patterns[], where: , optional: .Optional, acyclic: .Acyclic, walk: .Repeatable, scope: }) }case *ast.PathBind: ++ = append(, stageSpec{kind: specMatch, pattern: &.Pattern, where: .Where, optional: .Optional, pathVar: .PathVar, acyclic: .Acyclic, scope: })case *ast.ShortestPath: = append(, stageSpec{kind: specShortest, pattern: &.Pattern, where: .Where, optional: .Optional, pathVar: .PathVar, all: .All, weight: .Weight})case *ast.CallProc: = append(, stageSpec{kind: specCall, proc: .Proc, args: .Args, yields: .Yields})case *ast.Unwind: = append(, stageSpec{kind: specUnwind, list: .Expr, varName: .Var})case *ast.CallSubquery: = append(, stageSpec{kind: specCallSubquery, query: &.Query, imports: .Imports})case *ast.With: , := buildSegment(, .Proj, .Where, , , )if != nil {returnnil, nil, } = nil = .Proj.Columns = append(, ) } } , := buildSegment(, , nil, , , )if != nil {returnnil, nil, } = append(, )// The monotonic pushdown runs once over the fully built segment graph: // it sees every stage WHERE (inline form), the same segment's boundary // WHERE (derived form), and a LET/FILTER split across segments // (cross-segment form) uniformly.pushMonoPreds()// Early shortest-path row gating runs after every other segment // rewrite so it sees the final stage and boundary shapes.injectSPGates()lowerColumnarAggs()return , .Proj.Columns, nil}// pathCost estimates the fan-out of traversing p from its start: the// product of each hop's average degree. Used only to break anchor ties, so// an unknown/any-type hop counts as a neutral 1.func pathCost( *ast.Pattern, graph.Graph) float64 { := 1.0for := range .Hops { := &.Hops[].Rel := mapDir(.Dir) := 1.0iflen(.Types) > 0 { = 0.0for , := range .Types { += .AvgDegree(, ) } }if > 0 { *= } }return}func stringsEqual(, []string) bool {iflen() != len() {returnfalse }for := range {if [] != [] {returnfalse } }returntrue}func joinComma( []string) string { := ""for , := range {if > 0 { += ", " } += }return}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.