Files
greptimedb/tests-integration
discord9 72bee4ae9c feat(promql): answer whole-vector and multi-metric statements natively (#10025)
* feat(promql): name a fused tree's tags from the existing plan

A fused node declares exactly the tags the planner gives it, and reads each
one back off the series, so a label set this route cannot derive is a label
set the answer would lose. The walk over the tree derives labels for a
match, a set operation and the point-wise wrappers over them, and names
none for an aggregate's groups or a whole-vector operation - which today
declines every such statement.

Take those columns from the existing planner instead, the way the aggregate
split already does: its tag columns are what it projects for the same
statement, and they are now shared through one helper rather than written
out twice. Only the tag columns are copied, because the node names its
field columns after its own tree while the existing planner can name a
computed column after its expression.

A label rewrite is a shape the walk cannot derive at all: label_replace
adds its destination label and an empty replacement removes it, so passing
the input's labels through is an inference the rewrite contradicts. Report
no label set there. The rewrite itself keeps the existing plan, and this
does not change that: the native runtime refuses two series a rewrite gives
one label set, where that plan answers them, so admitting the shape would
turn an answer into an error. The test that holds this now covers the
two-occurrence shape as well.

An aggregate or whole-vector root does not reach this branch today: both
read across series, so they are planned by the aggregate split or fall
back. It is the prerequisite for the placement that runs such a tree in one
execution, and reaches the branch when that lands.

Signed-off-by: discord9 <discord9@163.com>

* feat(promql): run a classic histogram function above the aggregate cut

The split accepts a suffix that runs once over the fully merged series,
which is why TopK/BottomK were already admitted: the merge has folded every
partition's contribution to each group, so the series the suffix reads are
the ones the statement's input would have produced in one execution.

A classic histogram function is that kind of operation. It reads each
histogram off the bucket series that share its other labels and drops le,
and the merge has already folded each (..., le) group into one complete
series on the evaluated grid, so the buckets it reads are exactly the ones
the input would have had. Reading le off those merged series is the same
computation, not an approximation. LimitK still declines: it depends on the
storage input order, which the merge does not preserve.

This is what the o11ybench query
`histogram_quantile(0.99, sum(rate(...[5m])) by (instance, le))` needs: it
was planned by the existing path only because the split refused a
whole-vector operation above the cut.

The acceptance test runs the shape over real data and compares the rows two
executions report: the fixture spreads six bucket series over several files
and a memtable range, so the partial sums genuinely have partitions to
merge, and the native answer must equal the complete input's for quantiles
at both ends of the interpolation and for histogram_fraction. It also
asserts the native run really merged state, so a silent fall back cannot
pass it.

Signed-off-by: discord9 <discord9@163.com>

* feat(promql): read two metric logical tables as two inputs

A two-selector statement whose two sides are aggregates kept the existing
plan, even where both sides read a metric-engine logical table. The
o11ybench query `sum(max by (...) (a)) by (namespace) / sum(b) by
(namespace)` is that shape, and it fell back for three separate reasons.

First, the engine gate refused every multi-selector metric-engine
statement. The clause after it already requires each such selector to be a
recognized logical rewrite, so the removed clause refused only the case
this change handles.

Second, a metric logical table is rewritten to a scan of the physical table
restricted by `__table_id = <id>`, so two logical tables over one physical
table prepare two scans of the same table. The check deciding whether the
node binds one metric or one input per occurrence compared table names
only, so the two metrics read as one and both inputs would carry the first
one's name - a wrong answer, not a fall back. The id the rewrite already
requires is now part of that identity.

Third, a tree over two aggregates reads across series, so it is not
answered per partition, and the branch for such trees was the aggregate
split, which takes one child. A node with more than one input needs no
split: it declares one partition and reads all of every input's partitions
in one execution, so every group is folded in one place. Such a tree now
falls through to the node.

The one-selector path is unchanged: it still requires `can_split` and still
declines a tree the split cannot cut. So is the multi-selector case whose
inputs stay direct scans of one table - such a node is not region-local, as
`Categorizer::check_extension_plan` refuses a cross-series plan over more
than one scan - which is why the condition is the metric-logical-table one.

The acceptance test runs both shapes over two logical tables of one
physical table, with values that make a swapped input visibly wrong, and
compares the rows the two routes report.

Signed-off-by: discord9 <discord9@163.com>

* test(promql): log the metric pair's rows through telemetry, not stdout

The acceptance test printed its evidence with println!, which the crate
lints as an error under -D warnings, and echoed a Display value through
to_string inside the format arguments. Every other test in the file logs
through common_telemetry::info!, so this one does too.

Signed-off-by: discord9 <discord9@163.com>

* fix(promql): keep the existing plan for a tree the runtime cannot execute

The multi-input route skipped the aggregate split, and the split was what
refused a tree the runtime cannot execute: `check_cut` declines a
`quantile` and a grouping by a value field. Skipping it turned queries the
existing plan answers into errors, because the node is chosen while
planning the logical plan and a refusal the physical layer raises later has
nowhere to fall back to.

`quantile(0.5, a) / sum(b)` over two metric logical tables reported
`NotImplemented: the native runtime does not execute the Quantile { .. }
aggregate yet` where the existing plan answers it, and
`count(count by (val) (a)) / sum(b)` failed on the grouping by a value
field, which the outer `count` had hidden from the column check.

Ask the runtime before taking the route, with the same construction the
physical layer validates by, and keep the existing plan for whatever it
refuses. Asking it rather than listing the unsupported kinds is what makes
the check cover refusals this planner does not know about.

Signed-off-by: discord9 <discord9@163.com>

* fix(promql): keep the existing plan for a grouping and a selection the node cannot answer

A multi-input node skips the aggregate split, so the refusals the split
makes for reasons that are not about what the runtime computes were skipped
with it. Two of them matter here.

A grouping by the metric name keeps a label no column of the existing plan
can name. The native aggregate folds its groups by `__name__` as by any
other label, but the label set the node declares comes from the existing
planner's columns, and `__name__` is the name a raw frame carries rather
than a column of any scan - so the existing planner's `by(__name__)` finds
nothing and reports no label. The schema would then report one label set for
two groups the tree keeps apart: `sum by (__name__) (a or b)` over two
metrics would answer two rows a reader cannot tell apart, which is a series
identity lost rather than an answer given.

A selection that depends on the order its input arrives in is the other.
`limitk` keeps the first `k` series of each group in input order, while a
node with more than one input reads its inputs' partitions through a gather
that hands them over in the order they became ready: the schedule would
decide which series a query keeps. Prometheus defines the selection over a
series set. The split declines `LimitK` for the same reason. Every other
vector operation orders its input itself - `topk` by label, `sort` by value,
`limit_ratio` by a label hash - which is why only this one is asked about.

Both shapes keep the existing plan. `limitk` is not answerable by either
route today, since the existing planner has no kernel for the operator; the
point is that the native route is not the first to answer a selection the
schedule would decide.

Each guard is checked with the guard removed, so both tests are known to
fail without it.

Signed-off-by: discord9 <discord9@163.com>

* fix(promql): decline unsupported count_values groupings before execution

Signed-off-by: discord9 <discord9@163.com>

* docs(promql): scope the native capability probe guarantee

Signed-off-by: discord9 <discord9@163.com>

* test(promql): pin the same-table pair the multi-selector exclusion used to keep out

State that a Mito input joins a multi-input tree only when a second
metric logical id makes the tree multi-input, and that only LimitK's
selection depends on arrival order.

Signed-off-by: discord9 <discord9@163.com>

* fix(promql): decline ranking grouped by a value field before native execution

topk, bottomk and limitk key their groups by series labels, while the
existing plan also accepts a value field there and forms one group per
sample value. Admitted multi-input trees could therefore keep one row
where the existing plan keeps one per group. Validate the grouping when
the vector runtime is built so the statement keeps the existing plan.

Signed-off-by: discord9 <discord9@163.com>

---------

Signed-off-by: discord9 <discord9@163.com>
2026-10-29 20:16:44 +08:00
..

Setup tests for multiple storage backend

To run the integration test, please copy .env.example to .env in the project root folder and change the values on need.

Take s3 for example. You need to set your S3 bucket, access key id and secret key:

# Settings for s3 test
GT_S3_BUCKET=S3 bucket
GT_S3_REGION=S3 region
GT_S3_ACCESS_KEY_ID=S3 access key id
GT_S3_ACCESS_KEY=S3 secret access key

Run

Execute the following command in the project root folder:

cargo test integration

Test s3 storage:

cargo test s3

Test oss storage:

cargo test oss

Test azblob storage:

cargo test azblob

Setup tests with Kafka wal

To run the integration test, please copy .env.example to .env in the project root folder and change the values on need.

GT_KAFKA_ENDPOINTS = localhost:9092

Setup kafka standalone

cd tests-integration/fixtures

docker compose -f docker-compose.yml up kafka

Setup tests with etcd TLS

This guide explains how to set up and test TLS-enabled etcd connections in GreptimeDB integration tests.

Quick Start

TLS certificates are already at tests-integration/fixtures/etcd-tls-certs/.

  1. Start TLS-enabled etcd:

    cd tests-integration/fixtures
    docker compose up etcd-tls -d
    
  2. Start all services (including etcd-tls):

    cd tests-integration/fixtures
    docker compose up -d --wait
    

Certificate Details

The checked-in certificates include:

  • ca.crt - Certificate Authority certificate
  • server.crt / server-key.pem - Server certificate for etcd-tls service
  • client.crt / client-key.pem - Client certificate for connecting to etcd-tls

The server certificate includes SANs for localhost, etcd-tls, 127.0.0.1, and ::1.

Regenerating Certificates (Optional)

If you need to regenerate the etcd certificates:

# Regenerate certificates (overwrites existing ones)
./scripts/generate-etcd-tls-certs.sh

# Or generate in custom location
./scripts/generate-etcd-tls-certs.sh /path/to/cert/directory

If you need to regenerate the mysql and postgres certificates:

# Regenerate certificates (overwrites existing ones)
./scripts/generate_certs.sh

# Or generate in custom location
./scripts/generate_certs.sh /path/to/cert/directory

Note: The checked-in certificates are for testing purposes only and should never be used in production.