// CALL-procedure planning (port of the Rust plan/call.rs, grown for // expression arguments): resolve the procedure kind by name, fold constant // arguments and validate them at bind time, and map the YIELD fields to // output slots. A call with any non-constant argument is correlated: its // arguments are checked against the in-scope variables here and evaluated // per input row at exec, where a row whose arguments fail validation // yields no rows (total-eval semantics; static mistakes still error here).
package plan import ( ) func buildCallStage( string, []ast.Expr, []ast.YieldItem, map[string]int, map[int]bool, *int) (*CallStage, error) { , := procKindOf() if ! { return nil, planErrf("unknown procedure `%s` (supported: wcc, algo.bfs, algo.pagerank, algo.wcc, algo.cdlp, algo.lcc, algo.sssp, algo.propagate, fts.search, geo.withinRadius, geo.withinBBox)", ) } := &CallStage{} , := constArgs() if { , := ResolveCallProc(, ) if != nil { return nil, } .Proc = } else { for , := range { if semantics.ExprHasAgg() { return nil, planErrf("aggregates are not allowed in procedure arguments") } if := semantics.CheckRefs(, ); != nil { return nil, } } .Proc = CallProc{Kind: } .ProcName = .ArgExprs = } := valueFieldName() , , := NoSlot, NoSlot, NoSlot for , := range { := .Alias if == "" { = .Field } := * *++ [] = [] = true switch { case .Field == "node": = case != "" && .Field == : = case == ProcPropagate && .Field == "depth": = default: return nil, planErrf("procedure `%s` does not yield `%s`", , .Field) } } .NodeSlot, .ValueSlot, .DepthSlot = , , return , nil } // procKindOf resolves a case-insensitive procedure name to its kind. func procKindOf( string) (ProcKind, bool) { switch lower() { case "wcc": return ProcWcc, true case "fts.search": return ProcFtsSearch, true case "geo.withinradius": return ProcGeoWithinRadius, true case "geo.withinbbox": return ProcGeoWithinBBox, true case "algo.bfs": return ProcBfs, true case "algo.pagerank": return ProcPageRank, true case "algo.wcc": return ProcWccAll, true case "algo.cdlp": return ProcCdlp, true case "algo.lcc": return ProcLcc, true case "algo.sssp": return ProcSssp, true case "algo.propagate": return ProcPropagate, true } return 0, false } // valueFieldName is the per-node scalar column a procedure yields // alongside node ("" for the search procedures, which yield only node). func valueFieldName( ProcKind) string { switch { case ProcWcc: return "component" case ProcBfs, ProcPageRank, ProcWccAll, ProcCdlp, ProcLcc, ProcSssp, ProcPropagate: return "value" } return "" } // constArgs folds every argument to a constant value; ok is false when any // argument needs row context (making the call correlated). func constArgs( []ast.Expr) ([]value.Value, bool) { := make([]value.Value, len()) for , := range { , := constArg() if ! { return nil, false } [] = } return , true } // constArg folds one constant argument: a literal, a negated numeric // literal, or a list of constants. Parameters are not constants (they // resolve per run through the correlated path). func constArg( ast.Expr) (value.Value, bool) { switch n := .(type) { case *ast.Lit: if .Value.Kind == ast.LitParam || .Value.Kind == ast.LitNamedParam { return value.Value{}, false } return semantics.LitValue(.Value), true case *ast.Unary: if .Op != ast.Neg { return value.Value{}, false } , := (.Expr) if ! { return value.Value{}, false } if , := .AsInt(); { return value.Int(-), true } if , := .AsFloat(); { return value.Float(-), true } case *ast.ListExpr: := make([]value.Value, len(.Elems)) for , := range .Elems { , := () if ! { return value.Value{}, false } [] = } return value.List(), true } return value.Value{}, false } // ResolveCallProc validates resolved argument values into a concrete // CallProc -- at bind time for constant arguments, per input row from exec // for correlated ones. func ( string, []value.Value) (CallProc, error) { switch lower() { case "wcc": , := strArg(, , 0, "relType") if != nil { return CallProc{}, } := graph.Both if len() > 1 { if , = dirArg(, , 1); != nil { return CallProc{}, } } return CallProc{Kind: ProcWcc, RelType: , Direction: }, nil case "fts.search": if := expectArgCount(, , 3, "label, field, searchTerm"); != nil { return CallProc{}, } , := strArg(, , 0, "label") if != nil { return CallProc{}, } , := strArg(, , 1, "field") if != nil { return CallProc{}, } , := strArg(, , 2, "searchTerm") if != nil { return CallProc{}, } return CallProc{Kind: ProcFtsSearch, Label: , Field: , Query: }, nil case "geo.withinradius": if := expectArgCount(, , 6, "label, latField, longField, lat, lon, km"); != nil { return CallProc{}, } := CallProc{Kind: ProcGeoWithinRadius} var error if .Label, = strArg(, , 0, "label"); != nil { return CallProc{}, } if .LatField, = strArg(, , 1, "latField"); != nil { return CallProc{}, } if .LonField, = strArg(, , 2, "longField"); != nil { return CallProc{}, } if .Lat, = f64Arg(, , 3, "lat"); != nil { return CallProc{}, } if .Lon, = f64Arg(, , 4, "lon"); != nil { return CallProc{}, } if .Km, = f64Arg(, , 5, "km"); != nil { return CallProc{}, } return , nil case "geo.withinbbox": if := expectArgCount(, , 7, "label, latField, longField, minLat, minLon, maxLat, maxLon"); != nil { return CallProc{}, } := CallProc{Kind: ProcGeoWithinBBox} var error if .Label, = strArg(, , 0, "label"); != nil { return CallProc{}, } if .LatField, = strArg(, , 1, "latField"); != nil { return CallProc{}, } if .LonField, = strArg(, , 2, "longField"); != nil { return CallProc{}, } if .MinLat, = f64Arg(, , 3, "minLat"); != nil { return CallProc{}, } if .MinLon, = f64Arg(, , 4, "minLon"); != nil { return CallProc{}, } if .MaxLat, = f64Arg(, , 5, "maxLat"); != nil { return CallProc{}, } if .MaxLon, = f64Arg(, , 6, "maxLon"); != nil { return CallProc{}, } return , nil case "algo.bfs": if := maxArgs(, , 2, "source[, directed]"); != nil { return CallProc{}, } , := nodeArg(, , 0, "source") if != nil { return CallProc{}, } , := boolArgOr(, , 1, "directed", false) if != nil { return CallProc{}, } return CallProc{Kind: ProcBfs, Source: , Directed: }, nil case "algo.pagerank": if := maxArgs(, , 3, "[directed][, damping][, iterations]"); != nil { return CallProc{}, } , := boolArgOr(, , 0, "directed", false) if != nil { return CallProc{}, } , := f64ArgOr(, , 1, "damping", 0.85) if != nil { return CallProc{}, } , := u32ArgOr(, , 2, "iterations", 20) if != nil { return CallProc{}, } return CallProc{Kind: ProcPageRank, Directed: , Damping: , Iters: }, nil case "algo.wcc": if := maxArgs(, , 0, ""); != nil { return CallProc{}, } return CallProc{Kind: ProcWccAll}, nil case "algo.cdlp": if := maxArgs(, , 3, "[directed][, iterations][, seedProp]"); != nil { return CallProc{}, } , := boolArgOr(, , 0, "directed", false) if != nil { return CallProc{}, } , := u32ArgOr(, , 1, "iterations", 10) if != nil { return CallProc{}, } := "" if len() > 2 { if , = strArg(, , 2, "seedProp"); != nil { return CallProc{}, } } return CallProc{Kind: ProcCdlp, Directed: , Iters: , SeedProp: }, nil case "algo.lcc": if := maxArgs(, , 1, "[directed]"); != nil { return CallProc{}, } , := boolArgOr(, , 0, "directed", false) if != nil { return CallProc{}, } return CallProc{Kind: ProcLcc, Directed: }, nil case "algo.sssp": if := maxArgs(, , 3, "source[, directed][, weighted]"); != nil { return CallProc{}, } , := nodeArg(, , 0, "source") if != nil { return CallProc{}, } , := boolArgOr(, , 1, "directed", false) if != nil { return CallProc{}, } , := boolArgOr(, , 2, "weighted", false) if != nil { return CallProc{}, } return CallProc{Kind: ProcSssp, Source: , Directed: , Weighted: }, nil case "algo.propagate": return resolvePropagate(, ) } return CallProc{}, planErrf("unknown procedure `%s` (supported: wcc, algo.bfs, algo.pagerank, algo.wcc, algo.cdlp, algo.lcc, algo.sssp, algo.propagate, fts.search, geo.withinRadius, geo.withinBBox)", ) } // resolvePropagate validates algo.propagate(seeds, values, relTypes, // direction, maxDepth, valueProp, order, truncLimit[, minValue[, // filterProp, filterMin, filterMax]]) -- first-claim value propagation // (Snapshot.PropagateBFS), yielding node, value, depth per reached node. func resolvePropagate( string, []value.Value) (CallProc, error) { const = "seeds, values, relTypes, direction, maxDepth, valueProp, order, truncLimit[, minValue[, filterProp, filterMin, filterMax]]" if len() < 8 { return CallProc{}, planErrf("%s(%s) expects at least 8 arguments, got %d", , , len()) } if := maxArgs(, , 12, ); != nil { return CallProc{}, } := CallProc{Kind: ProcPropagate} var error if .Seeds, = nodesArg(, , 0, "seeds"); != nil { return CallProc{}, } if .SeedVals, = f64sArg(, , 1, "values"); != nil { return CallProc{}, } if len(.Seeds) != len(.SeedVals) { return CallProc{}, planErrf("%s seeds and values must be the same length (got %d and %d)", , len(.Seeds), len(.SeedVals)) } if .RelTypes, = strsArg(, , 2, "relTypes"); != nil { return CallProc{}, } if .Direction, = dirArg(, , 3); != nil { return CallProc{}, } , := u32ArgOr(, , 4, "maxDepth", 0) if != nil { return CallProc{}, } .MaxDepth = if .ValueProp, = strArg(, , 5, "valueProp"); != nil { return CallProc{}, } , := strArg(, , 6, "order") if != nil { return CallProc{}, } switch lower() { case "asc": case "desc": .Desc = true default: return CallProc{}, planErrf("%s order must be 'asc' or 'desc', got `%s`", , ) } , := u32ArgOr(, , 7, "truncLimit", 0) if != nil { return CallProc{}, } .TruncLimit = int() if .MinValue, = f64ArgOr(, , 8, "minValue", 0); != nil { return CallProc{}, } if len() > 9 { if len() != 12 { return CallProc{}, planErrf("%s filter takes filterProp, filterMin, filterMax together", ) } if .FilterProp, = strArg(, , 9, "filterProp"); != nil { return CallProc{}, } if .FilterMin, = i64Arg(, , 10, "filterMin"); != nil { return CallProc{}, } if .FilterMax, = i64Arg(, , 11, "filterMax"); != nil { return CallProc{}, } } return , nil }