Skip to content

feat: enable Calcite engine for Prometheus datasources with filter pushdown - #5706

Open
robertpaschedag wants to merge 9 commits into
opensearch-project:mainfrom
robertpaschedag:feature/prometheus-calcite-integration
Open

feat: enable Calcite engine for Prometheus datasources with filter pushdown#5706
robertpaschedag wants to merge 9 commits into
opensearch-project:mainfrom
robertpaschedag:feature/prometheus-calcite-integration

Conversation

@robertpaschedag

Copy link
Copy Markdown

Description

Enables all Calcite-only PPL commands (join, lookup, flatten, expand, eventstats, etc.) to work with Prometheus datasources by making PrometheusMetricTable implement Calcite's TranslatableTable interface with a custom logical/physical scan operator and filter pushdown.

Key changes:

  • PrometheusMetricTable now extends AbstractTable and implements both TranslatableTable (Calcite) and Table (V2), preserving backward compatibility
  • CalciteLogicalPrometheusScan — custom logical scan node (Convention.NONE) with filter pushdown support for label matchers and time ranges
  • CalciteEnumerablePrometheusScan — physical scan (EnumerableConvention) that executes PromQL via PrometheusClient.queryRange()
  • PrometheusFilterPushDownRule — Calcite optimizer rule that pushes label equality and time range filters into the scan
  • OpenSearchSchema — dynamic DataSourceSubSchema resolution for multi-part table references (e.g., source = prometheus.metric_name)
  • CalciteRelNodeVisitor.visitRelation() — relaxed datasource guard to allow non-default datasources when their table implements Calcite's Table interface

Filter pushdown behavior:

Filter Pushed? Target
service = 'frontend' Yes PromQL label selector {service="frontend"}
@timestamp >= X AND @timestamp <= Y Yes Query range start/end parameters
@value > 0.5 No In-memory EnumerableCalc
service = 'frontend' AND @value > 0.5 Partial Label pushed, @value remains in-memory

Explain output examples:

Full pushdown:
CalciteEnumerablePrometheusScan(table=[OpenSearch, prometheus, request], PushDownContext=[LABELS->{service=frontend}])

Partial pushdown:
EnumerableCalc(condition=>($t4, 0.5))
CalciteEnumerablePrometheusScan(table=[OpenSearch, prometheus, request], PushDownContext=[LABELS->{service=frontend}])

Related Issues

Resolves #5705

Check List

  • New functionality includes testing.
  • New functionality has been documented.
    • New functionality has javadoc added.
    • New functionality has a user manual doc added.
  • New PPL command checklist all confirmed.
  • API changes companion pull request created.
  • Commits are signed per the DCO using --signoff or -s.
  • Public documentation issue/PR created.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Yes..... the code got generated with assistence of AI. I was able to do some example PPL queries in local dev environment, using otel-demo sendings logs to opensearch, while sending metrics to prometheus (victoriametrics).

Screenshots

Example 1:
image

Example 2:
image

Example 3:
image

Example 4:
image

Example 5:
image

I generally hope, that this might be useful.

Implement ScannableTable interface for PrometheusMetricTable so that
Prometheus metrics can participate in Calcite query plans. This enables
PPL commands like join, lookup, and other Calcite-only commands to work
with Prometheus data sources.

Changes:
- PrometheusMetricTable now extends AbstractTable and implements both
  ScannableTable (Calcite) and Table (V2), providing dual-engine support
- OpenSearchSchema.registerTable() uses instanceof check instead of
  blind cast, with descriptive error for non-Calcite tables
- CalciteRelNodeVisitor.visitRelation() relaxed to allow non-default
  datasources when their table implements org.apache.calcite.schema.Table
- Added unit tests for ScannableTable integration (getRowType, scan,
  empty results, error handling)

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
…lcite

When a PPL query references a non-default datasource (e.g., source =
prometheus.up), Calcite's RelBuilder.scan() resolves the multi-part
name as schema + table. Previously, only the flat OpenSearchSchema
was registered, causing 'Table not found' errors for external datasources.

This fix adds a DataSourceSubSchema inner class that is lazily created
for any known datasource. The sub-schema resolves tables from the
datasource's storage engine, enabling proper schema-qualified resolution:
  scan(["prometheus", "up"]) -> OpenSearchSchema sub-schema "prometheus" -> table "up"

This works generically for any datasource name (prometheus, vmetrics,
my-metric-backend, etc.) as long as the datasource's tables implement
org.apache.calcite.schema.Table.

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
…h filter pushdown

Replaces the simple ScannableTable implementation with a full
TranslatableTable + pushdown architecture:

- CalciteLogicalPrometheusScan: logical scan node (Convention.NONE)
  with filter pushdown support for time range and label matchers
- CalciteEnumerablePrometheusScan: physical scan node implementing
  Scannable + EnumerableRel, executes PromQL via PrometheusClient
- PrometheusPushDownContext: accumulates pushed-down state (time range,
  step, label matchers) and builds PromQL query strings
- PrometheusFilterPushDownRule: planner rule that pushes LogicalFilter
  conditions into the logical scan
- EnumerablePrometheusScanRule: converter rule (logical -> physical)
- PrometheusRules: registry of all Prometheus planner rules

PrometheusMetricTable now implements TranslatableTable and returns
CalciteLogicalPrometheusScan from toRel(). Time range filters on
@timestamp and label equality filters are pushed down to PromQL,
reducing data transfer from Prometheus.

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
…er pushdown

The VolcanoPlanner was not selecting the partially-pushed filter plan
because CalciteEnumerablePrometheusScan did not override computeSelfCost().
Both pushed and unpushed physical scans reported identical cost, so the
planner picked whichever it found first (the unpushed path).

Adding cost reduction factors (0.7x for label pushdown, 0.5x for time
range pushdown) makes the pushed scan cheaper, ensuring the planner
prefers partial pushdown when available.

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
…lanner digest

The VolcanoPlanner uses explainTerms() to compute node digests for
equivalence detection. Without this override, pushed and unpushed
Prometheus scans had identical digests (only table name), causing the
planner to treat them as the same node and ignore filter pushdown
transformations.

Adding explainTerms() that includes the PushDownContext state ensures
pushed scans have distinct digests, enabling the VolcanoPlanner to
correctly register partial filter pushdowns as alternative plans.

Also adds toString() to PrometheusPushDownContext for explain output
visibility.

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
…selection

Add PlanUtils.tryPruneRelNodes(call) after call.transformTo() in
PrometheusFilterPushDownRule, matching the pattern used by all OpenSearch
pushdown rules (FilterIndexScanRule, ProjectIndexScanRule, etc.).

Without pruning, the VolcanoPlanner retains both the original (unpushed)
and pushed alternatives and may choose the unpushed path for partial
pushdown cases (e.g., 'where service=frontend AND @value > 0.5'). Pruning
the original nodes forces the planner to use the pushed path, ensuring
label filters are correctly pushed to PromQL while unsupported conditions
remain as EnumerableCalc.

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
…nt and filter.copy()

- Add estimateRowCount() override to both CalciteLogicalPrometheusScan and
  CalciteEnumerablePrometheusScan so pushed scans report fewer rows,
  propagating cost savings to all ancestor nodes in the VolcanoPlanner.
- Change pushDownFilter() to accept the Filter RelNode and use
  filter.copy() for partial pushdown (matching OpenSearch's pattern),
  ensuring proper equivalence registration in the VolcanoPlanner.
- Update PrometheusFilterPushDownRule.onMatch() to pass the Filter object
  instead of just the condition RexNode.

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 0f0796a.

PathLineSeverityDescription
prometheus/build.gradle21highNew dependency added: 'org.immutables:value-annotations:2.8.8'. Per mandatory supply chain review policy, all dependency additions must be flagged for maintainer verification regardless of apparent legitimacy. Namespace hijacking and typosquatting are common attack techniques.
prometheus/src/main/java/org/opensearch/sql/prometheus/planner/logical/rules/PrometheusPushDownContext.java115mediumIn buildPromQL(), the metricName parameter is interpolated directly into the PromQL string without any escaping (only label values are escaped via escapePromQLLabelValue). A metric name containing PromQL special characters such as '{', '}', or '#' could result in malformed or injected PromQL queries if the metric name originates from user-controlled input.
core/src/main/java/org/opensearch/sql/calcite/OpenSearchSchema.java103lowDataSourceSubSchema.resolveTable() hardcodes 'default' as the schema name when calling getTable(new DataSourceSchemaName(dataSourceName, "default"), tableName). For datasources with multiple schemas, this bypasses schema-level resolution and could cause tables to be resolved in the wrong schema context, potentially sidestepping schema-based authorization checks depending on downstream storage engine implementations.

The table above displays the top 10 most important findings.

Total: 3 | Critical: 0 | High: 1 | Medium: 1 | Low: 1


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@robertpaschedag

Copy link
Copy Markdown
Author

I will try to tackle the code analyzer comments....

- Add escapePromQLLabelValue() to prevent PromQL injection via special
  characters (backslash, double quote, newline) in label values
- Replace wildcard jacoco exclusion for planner.logical.rules.* with
  specific exclusions for rule classes that require integration testing
- Add comprehensive unit tests for PrometheusPushDownContext covering
  buildPromQL escaping, copy independence, time defaults, and toString
- Add toRel() coverage test for PrometheusMetricTable

Signed-off-by: Robert Paschedag <robert.paschedag@sap.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Enable Calcite-only PPL commands (join, lookup, etc.) for Prometheus datasources

1 participant