Skip to content

Conversation

@spiridonov
Copy link
Contributor

@spiridonov spiridonov commented Oct 7, 2025

What this PR does / why we need it:

In order to add support for rate() aggregation function, which will be implemented as count_over_time/$interval, I had to add support for math expressions. Math expressions are implemented as Projections, where Expand expression is evaluated from other columns and added to the result. Binary expressions with only single input are supported for now. Things like sum_over_time/count_over_time will be implemented later. Two inputs will be implemented as inner joins. For now I only added some plumbing to the planner for adding Join node before math expressions with two inputs. Joins themselves are not implemented yet, and the logical planner will throw a NotImplementer error if such a query is passed into it, but the plumbing code is there for better picture of overall design.

  • I restricted Projections to a single expand math expression because it is unclear how to evaluate multiple expressions each of which adds value column as the result.
  • I also refactored Planner.process() to return a single Node for simplicity. I do not see a case where it can return several children at once, however I see examples of "complex" children (MakeTable or Projection) where a subtree can be returned instead of a single node.

Minor:

  • fixed some typos

Which issue(s) this PR fixes:
Fixes #

Special notes for your reviewer:

Diff for pkg/engine/internal/planner/logical/planner.go is ugly here, it is better to view the new file as the whole to understand it better. I basically split one large function into 3 pieces without changing much in that logic.

Checklist

  • Reviewed the CONTRIBUTING.md guide (required)
  • Documentation added
  • Tests updated
  • Title matches the required conventional commits format, see here
    • Note that Promtail is considered to be feature complete, and future development for logs collection will be in Grafana Alloy. As such, feat PRs are unlikely to be accepted unless a case can be made for the feature actually being a bug fix to existing behavior.
  • Changes that require user attention or interaction to upgrade are documented in docs/sources/setup/upgrade/_index.md
  • If the change is deprecating or removing a configuration option, update the deprecated-config.yaml and deleted-config.yaml files respectively in the tools/deprecated-config-checker directory. Example PR

@spiridonov spiridonov requested a review from a team as a code owner October 7, 2025 18:43
@spiridonov spiridonov enabled auto-merge (squash) October 7, 2025 18:44
@spiridonov spiridonov disabled auto-merge October 7, 2025 21:07
@spiridonov spiridonov marked this pull request as draft October 8, 2025 18:44
@spiridonov spiridonov marked this pull request as ready for review October 9, 2025 16:09
@spiridonov spiridonov enabled auto-merge (squash) October 9, 2025 16:09
@spiridonov spiridonov requested a review from rfratto October 16, 2025 20:08
@rfratto
Copy link
Member

rfratto commented Oct 16, 2025

Thanks, that makes sense if you're trying to prepare for supporting math over two vectors.

I suspect computations over two vectors (in a way that's compatible with LogQL/PromQL) is going to be trickier than it seems. I believe, in relational algebra terms, they're expressed as a combination of an inner join and a projection.

So, given a query like

(
  sum by (job) (rate({namespace="dev"}[$__auto])
) + (
  sum by (job) (rate({namespace="qa"}[$__auto])
)

A physical plan mapping literally to the algebra could be

Projection expressions=[
  left.timestamp as timestamp,
  left.job AS job, 
  left.value + right.value as value,
] 
  InnerJoin left_prefix="left" right_prefix="right" on=[
    left.timestamp = right.timestamp && left.job = right.job 
  ] 
    VectorAggregate op=sum groupings=[job] # left-hand side 
      RangeAggregate op=rate  
        DataObjScan 
    VectorAggregate op=sum groupings=[job] # right-hand side 
      RangeAggregate op=rate 
        DataObjScan 

This needs to explicitly be an inner join, since LogQL's metric queries require the sample to exist on both side of the expression (this matches PromQL's behaviour).

Example

For example, the two inputs of OuterJoin

ts job value
0 loki 5
10 mimir 10

and

ts job value
0 loki 15
10 tempo 10

is joined into

left.ts left.job left.value right.ts right.job right.value
0 loki 5 0 loki 15

and is projected to

unix_ts job value
0 loki 20

I think it's probably okay if we wanted to have a node which combines the work of projection and inner joins, though I do think it's possible to represent these operations using projections, which we will have a separate node for, and separating them out may be easier to understand in the plans.

All that said, I do wonder if math on two vectors is going to require a lot more thought. Would we be able to simplify the logic here if we descoped that from our consideration?

Copy link
Contributor

@ashwanthgoli ashwanthgoli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

@chaudum chaudum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add some test cases to pkg/engine/internal/planner/planner_test.go

builder = builder.Cast(unwrapIdentifier, unwrapOperation)
op := convertBinaryArithmeticOp(e.Op)
if op == types.BinaryOpInvalid {
return nil, errUnimplemented
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An invalid op type should return an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have an inconsistent pattern for function like convertBinaryArithmeticOp. Some panic, and some return Ivalid enum value. Here I return Invalid that indicates that this op is not implemented. I don't want to panic here, and I don't want to make the function more complex by returning error just for that.

Comment on lines 403 to 404
// both left and right expressions have obj scans, replace with join
if leftInput != nil && rightInput != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftInput and rightInput are nil only in case they are literals, arent't they?
So checking for !- nil does not mean the node is a scan node? This assumption seems error prone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftInput and rightInput are nil if they are literals, bin ops or unary ops.

Column: "value_right",
Type: types.ColumnTypeGenerated,
}
join := &Join{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the Join used for? I cannot see a usage in the executor. Should the executor return an error in case sees a join in the plan?

I assume this is the plumbing you mentioned in the PR description

Two inputs will be implemented as inner joins. For now I only added some plumbing to the planner for adding Join node before math expressions with two inputs. Joins themselves are not implemented yet, and the logical planner will throw a NotImplementer error if such a query is passed into it, but the plumbing code is there for better picture of overall design.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is that plumbing from the description. Join is not implemented in the executor yet. But this code is not actually reachable because the logical planner will throw nonImplemented error when there are math expressions with two inputs.

return nil, nil, nil, err
}

columnRef := newColumnExpr(types.ColumnNameGeneratedValue, types.ColumnTypeGenerated)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there be a problem with colliding column names if there are nested math expressions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be something I am not aware of. But there are couple unit tests that test nested math expressions and they behave as expected.

@spiridonov spiridonov marked this pull request as draft October 24, 2025 20:44
auto-merge was automatically disabled October 24, 2025 20:44

Pull request was converted to draft

@spiridonov spiridonov marked this pull request as ready for review October 24, 2025 20:44
@spiridonov spiridonov merged commit 7eda674 into main Oct 24, 2025
63 checks passed
@spiridonov spiridonov deleted the spiridonov-rate-aggregate branch October 24, 2025 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants