diff --git a/cascades/memo/implementation_rule.go b/cascades/memo/implementation_rule.go deleted file mode 100644 index e74409e3f..000000000 --- a/cascades/memo/implementation_rule.go +++ /dev/null @@ -1,7 +0,0 @@ -package memo - -import "github.com/thanos-io/promql-engine/cascades/physicalplan" - -type ImplementationRule interface { - ListImplementations(expr *GroupExpr) []physicalplan.Implementation // List all implementation for the expression -} diff --git a/cascades/memo/transformation_rule.go b/cascades/memo/transformation_rule.go deleted file mode 100644 index f2d51190e..000000000 --- a/cascades/memo/transformation_rule.go +++ /dev/null @@ -1,11 +0,0 @@ -package memo - -import ( - "github.com/thanos-io/promql-engine/cascades/utils" -) - -type TransformationRule interface { - utils.Hashable - Match(expr *GroupExpr) bool // Check if the transformation can be applied to the expression - Transform(expr *GroupExpr) *GroupExpr // Transform the expression -} diff --git a/cascades/physicalplan/plan.go b/cascades/physicalplan/plan.go deleted file mode 100644 index 3896bad10..000000000 --- a/cascades/physicalplan/plan.go +++ /dev/null @@ -1,13 +0,0 @@ -package physicalplan - -import ( - "github.com/thanos-io/promql-engine/cascades/cost" - "github.com/thanos-io/promql-engine/execution/model" -) - -type Implementation interface { - CalculateCost(children []Implementation) cost.Cost // Calculate cost based on provided child implementations, also update the actual implementation, cost, and children list - Operator() model.VectorOperator // Return the saved physical operator set from the last CalculateCost - Cost() cost.Cost // Return the saved cost from the last CalculateCost call. - Children() []Implementation // Return the saved child implementations from the last CalculateCost call. -} diff --git a/planner/README.md b/planner/README.md new file mode 100644 index 000000000..f42ab18be --- /dev/null +++ b/planner/README.md @@ -0,0 +1,184 @@ +Proposal: separate between AST & execution, refactor query planner +=== + +## Background + +Quotes from https://github.com/thanos-io/promql-engine/issues/5 + +``` +We currently translate the AST directly to a physical plan. Having an in-between logical plan will allow us to run optimizers before the query is executed. + +The logical plan would have a one to one mapping with the AST and will contain the parameters of each AST node. +Query optimizers can then transform the logical plan based on predefined heuristics. One example would be optimizing series selects in binary operations so that we do as few network calls as possible. + +Finally, we would build the physical plan from the optimized logical plan instead doing it from the AST directly. +``` + +Here's our current query lifecycle + +```mermaid +flowchart TD +Query["query string"] +AST["parser.Expr"] +Plan1["logicalplan.Plan"] +Plan2["logicalplan.Plan"] +Operator["model.VectorOperator"] + +Query -->|parsring| AST +AST -->|logicalplan.New| Plan1 +Plan1 -->|optimize| Plan2 +Plan2 -->|execution.New| Operator +``` + +The `logicalplan.Plan` is just a wrapper of `parser.Expr`, and the conversion from `logicalplan.Plan` to `model.VectorOperator` is actually direct conversion from `parser.Expr` to `model.VectorOperator`. + +Another point, is our optimizers are heuristic optimizers, hence it's could not optimize for some complex queries, and could not use the data statistic to perform the optimization + +## Proposal + +We will implement the 2-stages planner according to Volcano planner + +```mermaid +flowchart TD +Query["query string"] +AST["parser.Expr"] +LogicalPlan1["LogicalPlan"] +Operator["model.VectorOperator"] + +subgraph plan["Planner"] + subgraph explore["Exploration Phase"] + LogicalPlan2["LogicalPlan"] + end + subgraph implement["Implementation Phase"] + PhysicalPlan + end +end + +Query -->|parsring| AST +AST -->|ast2plan| LogicalPlan1 +LogicalPlan1 --> LogicalPlan2 +LogicalPlan2 --> PhysicalPlan +PhysicalPlan --> Operator +``` + +### Exploration phase + +The exploration phase, used to explore all possible transformation of the original logical plan + +```mermaid +flowchart TD +LogicalPlan1["LogicalPlan"] +LogicalPlan2["LogicalPlan"] + +LogicalPlan1 -->|fire transformation rules| LogicalPlan2 +``` + +**Define**: +- `Group`: The `Equivalent Group`, or the `Equivalent Set`, is a group of multiple equivalent logical plans +- `GroupExpr`: representing a logical plan node (basically it's just wrap around the logical plan, with some additional information) + +```go +type Group struct { + // logical + Equivalents map[ID]*GroupExpr // The equivalent expressions. + ExplorationMark +} + +type GroupExpr struct { + Expr logicalplan.LogicalPlan // The logical plan bind to the expression. + Children []*Group // The children group of the expression, noted that it must be in the same order with LogicalPlan.Children(). + AppliedTransformations utils.Set[TransformationRule] + ExplorationMark +} +``` + +Here is the interface of transformation rule + +```go +type TransformationRule interface { + Match(expr *GroupExpr) bool // Check if the transformation can be applied to the expression + Transform(expr *GroupExpr) *GroupExpr // Transform the expression +} +``` + +```go +for _, rule := range rules { + if rule.Match(equivalentExpr) { + if !equivalentExpr.AppliedTransformations.Contains(rule) { + transformedExpr := rule.Transform(o.memo, equivalentExpr) + // add new equivalent expr to group + group.Equivalents[transformedExpr.ID] = transformedExpr + equivalentExpr.AppliedTransformations.Add(rule) + // reset group exploration state + transformedExpr.SetExplore(round, false) + group.SetExplore(round, false) + } + } +} +``` + +### Implementation phase + +After exploration phase, we have the expanded logical plan (including the original plan and the transformed plans) + +Then we will find the implementation which has the lowest implementation cost + + +```mermaid +flowchart TD +LogicalPlan2["LogicalPlan"] +PhysicalPlan +Operator["model.VectorOperator"] + +LogicalPlan2 -->|find best implementation| PhysicalPlan +PhysicalPlan -->|get the actual implementation| Operator +``` + +The physical plan represent the actual implementation of a logical plan (the `Children` property in `PhysicalPlan` is used for cost calculation) + +```go +type PhysicalPlan interface { + SetChildren(children []PhysicalPlan) // set child implementations, also update the operator and cost. + Children() []PhysicalPlan // Return the saved child implementations from the last CalculateCost call. + Operator() model.VectorOperator // Return the saved physical operator set from the last CalculateCost. + Cost() cost.Cost // Return the saved cost from the last CalculateCost call. +} +``` + +For each logical plan, we will have several implementations + +```go +type ImplementationRule interface { + ListImplementations(expr *GroupExpr) []physicalplan.PhysicalPlan // List all implementation for the expression +} +``` + +And we will find the best implementation, via simple dynamic programming + +```go +var possibleImpls []physicalplan.PhysicalPlan +for _, rule := range rules { + possibleImpls = append(possibleImpls, rule.ListImplementations(expr)...) +} +``` + +```go +var currentBest *memo.GroupImplementation +for _, impl := range possibleImpls { + impl.SetChildren(childImpls) + calculatedCost := impl.Cost() + if groupImpl != nil { + if costModel.IsBetter(currentBest.Cost, calculatedCost) { + currentBest.SelectedExpr = expr + currentBest.Implementation = impl + currentBest.Cost = calculatedCost + } + } else { + currentBest = &memo.GroupImplementation{ + SelectedExpr: expr, + Cost: calculatedCost, + Implementation: impl, + } + } +} +``` \ No newline at end of file diff --git a/cascades/cost/cost.go b/planner/cost/cost.go similarity index 59% rename from cascades/cost/cost.go rename to planner/cost/cost.go index 93bc6d7f7..add8757c4 100644 --- a/cascades/cost/cost.go +++ b/planner/cost/cost.go @@ -1,6 +1,9 @@ package cost -type Cost interface{} +type Cost struct { + CpuCost float64 + MemoryCost float64 +} type CostModel interface { IsBetter(currentCost Cost, newCost Cost) bool diff --git a/cascades/logicalplan/ast2plan.go b/planner/logicalplan/ast2plan.go similarity index 100% rename from cascades/logicalplan/ast2plan.go rename to planner/logicalplan/ast2plan.go diff --git a/cascades/logicalplan/ast2plan_test.go b/planner/logicalplan/ast2plan_test.go similarity index 100% rename from cascades/logicalplan/ast2plan_test.go rename to planner/logicalplan/ast2plan_test.go diff --git a/cascades/logicalplan/plan.go b/planner/logicalplan/plan.go similarity index 100% rename from cascades/logicalplan/plan.go rename to planner/logicalplan/plan.go diff --git a/cascades/memo/explore.go b/planner/memo/explore.go similarity index 100% rename from cascades/memo/explore.go rename to planner/memo/explore.go diff --git a/cascades/memo/explore_test.go b/planner/memo/explore_test.go similarity index 100% rename from cascades/memo/explore_test.go rename to planner/memo/explore_test.go diff --git a/cascades/memo/group.go b/planner/memo/group.go similarity index 77% rename from cascades/memo/group.go rename to planner/memo/group.go index a53a2b30b..d0167aedd 100644 --- a/cascades/memo/group.go +++ b/planner/memo/group.go @@ -1,10 +1,10 @@ package memo import ( - "github.com/thanos-io/promql-engine/cascades/cost" - "github.com/thanos-io/promql-engine/cascades/logicalplan" - "github.com/thanos-io/promql-engine/cascades/physicalplan" - "github.com/thanos-io/promql-engine/cascades/utils" + "github.com/thanos-io/promql-engine/planner/cost" + "github.com/thanos-io/promql-engine/planner/logicalplan" + "github.com/thanos-io/promql-engine/planner/physicalplan" + "github.com/thanos-io/promql-engine/planner/utils" "sync/atomic" ) @@ -38,7 +38,7 @@ type Group struct { type GroupImplementation struct { SelectedExpr *GroupExpr Cost cost.Cost - Implementation physicalplan.Implementation + Implementation physicalplan.PhysicalPlan } type GroupExpr struct { diff --git a/planner/memo/implementation_rule.go b/planner/memo/implementation_rule.go new file mode 100644 index 000000000..402a840fc --- /dev/null +++ b/planner/memo/implementation_rule.go @@ -0,0 +1,7 @@ +package memo + +import "github.com/thanos-io/promql-engine/planner/physicalplan" + +type ImplementationRule interface { + ListImplementations(expr *GroupExpr) []physicalplan.PhysicalPlan // List all implementation for the expression +} diff --git a/cascades/memo/memo.go b/planner/memo/memo.go similarity index 87% rename from cascades/memo/memo.go rename to planner/memo/memo.go index b1ba97dff..20c9cb32e 100644 --- a/cascades/memo/memo.go +++ b/planner/memo/memo.go @@ -1,8 +1,8 @@ package memo import ( - "github.com/thanos-io/promql-engine/cascades/logicalplan" - "github.com/thanos-io/promql-engine/cascades/utils" + "github.com/thanos-io/promql-engine/planner/logicalplan" + "github.com/thanos-io/promql-engine/planner/utils" ) type Memo interface { @@ -40,9 +40,10 @@ func (m *memo) GetOrCreateGroupExpr(node logicalplan.LogicalPlan) *GroupExpr { } else { id := m.groupExprIDGenerator.Generate() expr := &GroupExpr{ - ID: id, - Expr: node, - Children: childGroups, + ID: id, + Expr: node, + Children: childGroups, + AppliedTransformations: make(utils.Set[TransformationRule]), } m.GroupExprs[node] = expr return expr diff --git a/cascades/memo/memo_test.go b/planner/memo/memo_test.go similarity index 98% rename from cascades/memo/memo_test.go rename to planner/memo/memo_test.go index c58a06da4..e2d9ff689 100644 --- a/cascades/memo/memo_test.go +++ b/planner/memo/memo_test.go @@ -2,8 +2,8 @@ package memo import ( "github.com/stretchr/testify/assert" - "github.com/thanos-io/promql-engine/cascades/logicalplan" "github.com/thanos-io/promql-engine/parser" + "github.com/thanos-io/promql-engine/planner/logicalplan" "golang.org/x/exp/maps" "reflect" "testing" diff --git a/planner/memo/transformation_rule.go b/planner/memo/transformation_rule.go new file mode 100644 index 000000000..456d35ab9 --- /dev/null +++ b/planner/memo/transformation_rule.go @@ -0,0 +1,11 @@ +package memo + +import ( + "github.com/thanos-io/promql-engine/planner/utils" +) + +type TransformationRule interface { + utils.Hashable + Match(expr *GroupExpr) bool // Check if the transformation can be applied to the expression + Transform(memo Memo, expr *GroupExpr) *GroupExpr // Transform the expression +} diff --git a/planner/physicalplan/plan.go b/planner/physicalplan/plan.go new file mode 100644 index 000000000..310af0fa0 --- /dev/null +++ b/planner/physicalplan/plan.go @@ -0,0 +1,13 @@ +package physicalplan + +import ( + "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/planner/cost" +) + +type PhysicalPlan interface { + SetChildren(children []PhysicalPlan) // set child implementations, also update the operator and cost. + Children() []PhysicalPlan // Return the saved child implementations + Operator() model.VectorOperator // Return the saved physical operator + Cost() cost.Cost // Return the saved cost +} diff --git a/cascades/optimize.go b/planner/planner.go similarity index 66% rename from cascades/optimize.go rename to planner/planner.go index d210ae0f4..a10b6fd45 100644 --- a/cascades/optimize.go +++ b/planner/planner.go @@ -1,15 +1,15 @@ -package cascades +package planner import ( - "github.com/thanos-io/promql-engine/cascades/cost" - "github.com/thanos-io/promql-engine/cascades/logicalplan" - "github.com/thanos-io/promql-engine/cascades/memo" - "github.com/thanos-io/promql-engine/cascades/physicalplan" "github.com/thanos-io/promql-engine/parser" + "github.com/thanos-io/promql-engine/planner/cost" + "github.com/thanos-io/promql-engine/planner/logicalplan" + "github.com/thanos-io/promql-engine/planner/memo" + "github.com/thanos-io/promql-engine/planner/physicalplan" "golang.org/x/exp/maps" ) -type Optimize struct { +type Planner struct { expr parser.Expr memo memo.Memo // root @@ -17,19 +17,19 @@ type Optimize struct { rootGroup *memo.Group } -func New(expr parser.Expr) *Optimize { - return &Optimize{ - expr: expr, +func New() *Planner { + return &Planner{ memo: memo.NewMemo(), } } -func (o *Optimize) SetRoot(root logicalplan.LogicalPlan) { - o.root = root - o.rootGroup = o.memo.GetOrCreateGroup(root) +func (o *Planner) MakeRoot(expr parser.Expr) { + o.expr = expr + o.root = logicalplan.NewLogicalPlan(&expr) + o.rootGroup = o.memo.GetOrCreateGroup(o.root) } -func (o *Optimize) exploreGroup(rules []memo.TransformationRule, group *memo.Group, round memo.ExplorationRound) { +func (o *Planner) exploreGroup(rules []memo.TransformationRule, group *memo.Group, round memo.ExplorationRound) { for { if group.IsExplored(round) { break @@ -50,9 +50,9 @@ func (o *Optimize) exploreGroup(rules []memo.TransformationRule, group *memo.Gro } // fire rules for more equivalent expressions for _, rule := range rules { - if !equivalentExpr.AppliedTransformations.Contains(rule) { - if rule.Match(equivalentExpr) { - transformedExpr := rule.Transform(equivalentExpr) + if rule.Match(equivalentExpr) { + if !equivalentExpr.AppliedTransformations.Contains(rule) { + transformedExpr := rule.Transform(o.memo, equivalentExpr) group.Equivalents[transformedExpr.ID] = transformedExpr equivalentExpr.AppliedTransformations.Add(rule) // reset group exploration state @@ -70,23 +70,23 @@ func (o *Optimize) exploreGroup(rules []memo.TransformationRule, group *memo.Gro } } -func (o *Optimize) Explore(rules []memo.TransformationRule, round memo.ExplorationRound) { +func (o *Planner) Explore(rules []memo.TransformationRule, round memo.ExplorationRound) { o.exploreGroup(rules, o.rootGroup, round) } -func (o *Optimize) findBestImpl(costModel cost.CostModel, rules []memo.ImplementationRule, group *memo.Group) *memo.GroupImplementation { +func (o *Planner) findBestImpl(costModel cost.CostModel, rules []memo.ImplementationRule, group *memo.Group) *memo.GroupImplementation { if group.Implementation != nil { return group.Implementation } else { var groupImpl *memo.GroupImplementation for _, expr := range group.Equivalents { // fire rules to find implementations for each equiv expr, returning un-calculated implementations - var possibleImpls []physicalplan.Implementation + var possibleImpls []physicalplan.PhysicalPlan for _, rule := range rules { possibleImpls = append(possibleImpls, rule.ListImplementations(expr)...) } // get the implementation of child groups - var childImpls []physicalplan.Implementation + var childImpls []physicalplan.PhysicalPlan for _, child := range expr.Children { childImpl := o.findBestImpl(costModel, rules, child) child.Implementation = childImpl @@ -94,9 +94,10 @@ func (o *Optimize) findBestImpl(costModel cost.CostModel, rules []memo.Implement } // calculate the implementation, and update the best cost for group for _, impl := range possibleImpls { - calculatedCost := impl.CalculateCost(childImpls) + impl.SetChildren(childImpls) + calculatedCost := impl.Cost() if groupImpl != nil { - if costModel.IsBetter(groupImpl, calculatedCost) { + if costModel.IsBetter(groupImpl.Cost, calculatedCost) { groupImpl.SelectedExpr = expr groupImpl.Implementation = impl groupImpl.Cost = calculatedCost @@ -114,6 +115,6 @@ func (o *Optimize) findBestImpl(costModel cost.CostModel, rules []memo.Implement } } -func (o *Optimize) FindBestImplementation(costModel cost.CostModel, rules []memo.ImplementationRule) { +func (o *Planner) FindBestImplementation(costModel cost.CostModel, rules []memo.ImplementationRule) { o.rootGroup.Implementation = o.findBestImpl(costModel, rules, o.rootGroup) } diff --git a/planner/planner_test.go b/planner/planner_test.go new file mode 100644 index 000000000..5dd33dd3f --- /dev/null +++ b/planner/planner_test.go @@ -0,0 +1,237 @@ +package planner + +import ( + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" + "github.com/stretchr/testify/require" + model2 "github.com/thanos-io/promql-engine/execution/model" + "github.com/thanos-io/promql-engine/parser" + "github.com/thanos-io/promql-engine/planner/cost" + "github.com/thanos-io/promql-engine/planner/logicalplan" + "github.com/thanos-io/promql-engine/planner/memo" + "github.com/thanos-io/promql-engine/planner/physicalplan" + "testing" +) + +// transformation rules + +/* +a simple transformation rule to inject injection_foo to label matchers of VectorSelector +The transformation result might be invalid, but it's only mean to test the planner, so it's okay +*/ +type dummyMatcherInjection struct{} + +func (s *dummyMatcherInjection) HashCode() uint64 { + return 1 +} + +func (s *dummyMatcherInjection) Match(expr *memo.GroupExpr) bool { + _, ok := (expr.Expr).(*logicalplan.VectorSelector) + return ok +} + +func (s *dummyMatcherInjection) Transform(m memo.Memo, expr *memo.GroupExpr) *memo.GroupExpr { + node, _ := (expr.Expr).(*logicalplan.VectorSelector) + var newLabelMatchers []*labels.Matcher + existingVals := make(map[string]bool) + for _, matcher := range node.LabelMatchers { + newLabelMatchers = append(newLabelMatchers, matcher) + existingVals[matcher.Value] = true + } + toBeInjected := "injection_foo" + if _, ok := existingVals[toBeInjected]; !ok { + newLabelMatchers = append(newLabelMatchers, parser.MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "injection_foo")) + newNode := &logicalplan.VectorSelector{ + Name: node.Name, + OriginalOffset: node.OriginalOffset, + Offset: node.Offset, + Timestamp: node.Timestamp, + StartOrEnd: node.StartOrEnd, + LabelMatchers: newLabelMatchers, + } + newExpr := m.GetOrCreateGroupExpr(newNode) + return newExpr + } else { + return expr // return the original + } +} + +// implementation rules + +var implementationRules = []memo.ImplementationRule{ // implementation rule must cover all possible logical plan + &mockVectorSelectorImplRule{}, + &mockBinaryExprImplRule{}, +} + +type mockVectorSelectorImplRule struct{} + +func (m *mockVectorSelectorImplRule) ListImplementations(expr *memo.GroupExpr) []physicalplan.PhysicalPlan { + if e, ok := (expr.Expr).(*logicalplan.VectorSelector); ok { + return []physicalplan.PhysicalPlan{&mockVectorSelectorImpl{ + plan: e, + parent: expr, + }} + } else { + return []physicalplan.PhysicalPlan{} + } +} + +type mockBinaryExprImplRule struct{} + +func (m *mockBinaryExprImplRule) ListImplementations(expr *memo.GroupExpr) []physicalplan.PhysicalPlan { + if e, ok := (expr.Expr).(*logicalplan.BinaryExpr); ok { + return []physicalplan.PhysicalPlan{&mockBinaryExprImpl{ + plan: e, + parent: expr, + }} + } else { + return []physicalplan.PhysicalPlan{} + } +} + +// the implementations + +type mockVectorSelectorImpl struct { + plan *logicalplan.VectorSelector + parent *memo.GroupExpr + children []physicalplan.PhysicalPlan + cost cost.Cost +} + +func (m *mockVectorSelectorImpl) ParentExpr() *memo.GroupExpr { + return m.parent +} + +func (m *mockVectorSelectorImpl) SetChildren(children []physicalplan.PhysicalPlan) { + m.children = children + matcherValues := make(map[string]bool) + for _, matcher := range m.plan.LabelMatchers { + matcherValues[matcher.Value] = true + } + // we will bias the cost if there's a label with value "injection_foo" (because our transformation rule above) + if _, ok := matcherValues["injection_foo"]; ok { + m.cost = cost.Cost{ + CpuCost: 1, + MemoryCost: 2, + } + } else { + m.cost = cost.Cost{ + CpuCost: 4, + MemoryCost: 3, + } + } +} + +func (m *mockVectorSelectorImpl) Children() []physicalplan.PhysicalPlan { + return m.children +} + +func (m *mockVectorSelectorImpl) Operator() model2.VectorOperator { + // FIXME this is just a test code to demo, not the real implementation, so I will return nil instead + return nil +} + +func (m *mockVectorSelectorImpl) Cost() cost.Cost { + return m.cost +} + +type mockBinaryExprImpl struct { + plan *logicalplan.BinaryExpr + parent *memo.GroupExpr + children []physicalplan.PhysicalPlan + cost cost.Cost +} + +func (m *mockBinaryExprImpl) SetChildren(children []physicalplan.PhysicalPlan) { + m.children = children + // this node always have 2 children + leftChild := children[0] + rightChild := children[1] + // for simplicity, just combine their cost together (might be it's not true in real scenario, but it's just a demo) + m.cost = cost.Cost{ + CpuCost: leftChild.Cost().CpuCost + rightChild.Cost().CpuCost, + MemoryCost: leftChild.Cost().MemoryCost + rightChild.Cost().MemoryCost, + } +} + +func (m *mockBinaryExprImpl) Children() []physicalplan.PhysicalPlan { + return m.children +} + +func (m *mockBinaryExprImpl) Operator() model2.VectorOperator { + // FIXME this is just a test code to demo, not the real implementation, so I will return nil instead + return nil +} + +func (m *mockBinaryExprImpl) Cost() cost.Cost { + return m.cost +} + +// cost model + +type mockCostModel struct{} + +func (m *mockCostModel) IsBetter(currentCost cost.Cost, newCost cost.Cost) bool { + if currentCost.CpuCost == newCost.CpuCost { + return currentCost.MemoryCost > newCost.MemoryCost + } else { + return currentCost.CpuCost > newCost.CpuCost + } +} + +func TestPlanner0(t *testing.T) { // demo the new planner + input := "foo / on(test,blub) group_left(bar) bar" // copied from parse_test.go + /* + &parser.BinaryExpr{ + Op: parser.DIV, + LHS: &parser.VectorSelector{ + Name: "foo", + LabelMatchers: []*labels.Matcher{ + parser.MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"), + }, + PosRange: parser.PositionRange{ + Start: 0, + End: 3, + }, + }, + RHS: &parser.VectorSelector{ + Name: "bar", + LabelMatchers: []*labels.Matcher{ + parser.MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "bar"), + }, + PosRange: parser.PositionRange{ + Start: 43, + End: 46, + }, + }, + VectorMatching: &parser.VectorMatching{ + Card: parser.CardManyToOne, + MatchingLabels: []string{"test", "blub"}, + Include: []string{"blub"}, + }, + } + */ + expr, err := parser.ParseExpr(input) + require.NoError(t, err) + planner := New() + planner.MakeRoot(expr) + planner.Explore([]memo.TransformationRule{&dummyMatcherInjection{}}, 0) // now we have a dummy + planner.FindBestImplementation(&mockCostModel{}, implementationRules) + root := planner.rootGroup + /* + In the parsed expression, we have 2 VectorSelector expression, + hence the logical plan also includes 2 VectorSelector nodes. + + Since we have a transformation rule `dummyMatcherInjection` to inject the "injection_foo" value into the matchers, + and we have the implementation rules to bias the cost if "injection_foo" is presented + ({cpu_cost=1, mem_cost=2} if "injection_foo" is presented, {cpu_cost=4, mem_cost=3} otherwise). + + And the binary implementation rule will just sum up all of its child implementation cost. + + So the final cost is {cpu_cost=2, mem_cost=4} + */ + require.Equal(t, root.Implementation.Cost, cost.Cost{ + CpuCost: 2, + MemoryCost: 4, + }) +} diff --git a/cascades/utils/id_gen.go b/planner/utils/id_gen.go similarity index 100% rename from cascades/utils/id_gen.go rename to planner/utils/id_gen.go diff --git a/cascades/utils/set.go b/planner/utils/set.go similarity index 100% rename from cascades/utils/set.go rename to planner/utils/set.go