Commit Graph
873 Commits
Author SHA1 Message Date
discord9 b20e22b426 refactor(series): unify raw and evaluated SeriesFrame codec
SeriesFrame is the internal transport of the Narthex native PromQL path, and
it could only carry raw samples: an evaluated result had to fall back to a
plain row table. This makes one self-describing Arrow format carry both, so a
receiver decodes a frame from the frame alone, without the scanner, region, or
planning context its producer had.

What is unified is the physical representation and its codec, not the runtime
data structures. RawSeriesChunk, Series, and EvaluatedChunk stay as they are;
only the adapter boundaries branch on the sample shape.

Three combinations are legal and every other one is refused in both
directions:

  raw + key + timestamps          a storage raw-series scan
  raw + explicit + timestamps     a raw input whose labels are decoded
  evaluated + explicit + grid_spans   a compute node's output

An evaluated frame cannot claim a storage key, because the computation may
rewrite its labels; raw samples have no grid to be addressed on. The schema
builder and the declaration parser both enforce that, so a frame cannot be
written or read in a shape no producer could mean.

An evaluated frame declares its GridSpec once in schema metadata and carries
absolute grid positions per row as spans, so a sparse grid costs its runs and
not its holes: nothing is expanded, and no position a chunk does not hold is
filled in. A chunk with no position is legal exactly when it closes its chain,
and it is kept as one frame row, because dropping it would drop the finality
it exists to carry.

Layout is v2 and the writer and reader of the reserved greptime.series.*
metadata live in one place each, so a producer and a consumer cannot spell one
frame two ways. The unreleased v1 format is not read: the in-tree producers,
consumers, tests, and the two frozen wire fixtures all move to v2 together.

Signed-off-by: discord9 <discord9@users.noreply.github.com>
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
(cherry picked from commit 176f6f6479)
2026-10-26 12:57:11 +08:00
discord9 9a70220974 test(promql): cover the multi-value timestamp refusal
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-26 11:25:44 +08:00
discord9 24e214b7d4 feat(promql): bind instant anchors and execute native timestamps
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-26 11:25:43 +08:00
discord9 3c409f91de test(promql): align admission expectations with literal entry
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-26 00:23:44 +08:00
discord9 5bcd96cead feat(promql): execute zero-input scalar roots through fused children
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-26 00:13:13 +08:00
discord9 9720e1bf88 style(promql): attach native predicate rustdoc directly
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 21:59:49 +08:00
discord9 1df0cb6af7 test(promql): admit complete aggregate in native shape coverage
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 21:46:57 +08:00
discord9 463d4a5c8f fix(promql): align aggregate admission with boxed parser inputs
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 21:35:20 +08:00
discord9 73b9a77932 feat(promql): adapt complete aggregate wiring to recursive native runtime
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 21:25:05 +08:00
discord9 8dbf08d252 feat(promql): execute independent selector scans in fused binary trees
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 20:10:32 +08:00
discord9 b4c94cbd3a test(promql): align native display and shared-source scan expectations
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 13:58:20 +08:00
discord9 4748081579 feat(promql): fan out a shared raw scan to bound binary inputs
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 13:47:42 +08:00
discord9 2f967ceb45 test(promql): correct distinct-input arithmetic expectations
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 13:15:15 +08:00
discord9 39fcdbdbcf feat(promql): lower independently bound vector binary operands
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 13:14:35 +08:00
discord9 6b38352079 test(promql): avoid cloning a selector binding for validation
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 13:07:56 +08:00
discord9 b1e3b38a28 test(promql): cover selector bounds and recursive binding contracts
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 13:05:09 +08:00
discord9 3790693dd2 feat(promql): bind selector occurrences without node paths
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-25 12:58:22 +08:00
discord9 6157479d9a chore: integrate series-label naming with the recursive native POC
Apply only the type rename to the surviving recursive runtime and tests; preserve the removal of global final-label occupancy and speculative split-series admission guards.

Signed-off-by: discord9 <discord9@163.com>
2026-10-25 03:54:27 +08:00
discord9 7d127f6830 chore: integrate recursive runtime with the minimal native POC
Preserve the POC removal of global output-label occupancy and speculative split-series guards while adopting InputId leaf routing and execution-owned metric handles. Let NativeExecutionState own its recursive root directly.

Signed-off-by: discord9 <discord9@163.com>
2026-10-25 03:50:08 +08:00
discord9 9f1bf087eb test(promql): use an instant-vector subquery boundary case
Signed-off-by: discord9 <discord9@163.com>
2026-10-25 03:39:52 +08:00
discord9 aa4a381f71 refactor(promql): keep fused POC execution partition-local
Remove plan-global label-set occupancy and the speculative split-series tree guard. Keep the native evaluator, finality, memory budgets, errors and ordinary RecordBatch output local to each scan partition.

Narrow native admission to selectors and the small unary POC subset. Document that cross-partition final-label conflicts and semantic merging are outside this POC, and cover real rate/abs(rate) execution across dense and sparse storage spanning SST and memtable data.

Signed-off-by: discord9 <discord9@163.com>
2026-10-25 03:33:49 +08:00
discord9 49558985ba chore: sync narthex before native POC closure
Signed-off-by: discord9 <discord9@163.com>
2026-10-25 03:16:42 +08:00
discord9 ebfbd2ae5d fix(query): preserve legacy semantics at native admission
Signed-off-by: discord9 <discord9@163.com>
2026-10-24 18:58:51 +08:00
discord9 8388c09d5c chore: merge narthex into refactor/promql-series-labels
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
2026-10-24 18:55:47 +08:00
discord9 da9c63dc0e refactor(promql): remove NodePath and own the runtime tree recursively
A node was identified by the path that reached it, so every execution and
every metric export walked that path again, and a node's own state lived in
tables keyed by it. This replaces that with one recursive runtime tree: each
node owns its children, its operator state, its metrics and, once settled,
nothing it no longer needs.

An `InputId` is assigned only to the leaves that actually receive a feed, by a
depth-first walk of the built tree, so a scalar leaf takes no id and no node is
addressed by position. Pulling, settling, flushing after a subtree closes,
EOF, errors and drop all recurse through the same tree.


Signed-off-by: discord9 <discord9@163.com>
2026-10-24 18:52:05 +08:00
Dennis Zhuang 9de4dcb075 chore: merge narthex into feat/narthex-range-modifiers
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

# Conflicts:
#	src/promql/src/native/display/tests.rs
2026-10-23 23:13:17 +08:00
discord9 990d932198 fix(query): keep label rewrites on the existing plan
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 20:06:13 +08:00
discord9 c6881447c1 fix(promql): bind native input to common scan ordering
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:39 +08:00
discord9 d62051e12a fix(query): preserve logical metric field order in native scans
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:39 +08:00
discord9 27cd09ec55 fix(query): keep TQL views serializable and expose native diagnostics
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:39 +08:00
discord9 340ea2d71e fix(query): retain metric table restriction in native scans
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 29ef60f6e8 test(promql): restore scan assignment coverage and update native guide
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 51da078dc8 chore(query): clear the two lints clippy reports on this branch
A doc comment on the categorizer was followed by a blank line, and the test
table builder takes eight arguments. Both are what CI's `-D warnings` turns
into errors; neither changes behaviour.

Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 ca804689eb chore(query): share the offset conversion and correct a column-order note
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 8e55a48952 refactor(promql): keep one cross-series predicate for both callers
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 ff1288f178 fix(query): match the raw scan's own field and engine rules
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 de97d86800 fix(query): only offer the native route to an engine that serves frames
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 638d485024 fix(query): decline the native route where it cannot reach a region
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 1ced0f7fdb chore(query): share the range assignment with every caller
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 8efec61743 fix(promql): keep the fused scan's parallelism
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 5527c751d2 fix(query): decline the native route where no raw scan exists
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 129faa4e49 chore(query): drop a needless borrow on the scan schema
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 0d29dc98e8 fix(query): emit the fused output in the existing column order
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 67a3d22c5b fix(query): keep a fused route to series a region owns
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 64f039eaf4 test(query): run the encoding checks for both transports
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 6121600789 fix(query): keep ordinary plans on the encoding path
Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 da27d115ca feat(query): let a region input travel as a plan when regions are in process
A plan holding a node with no Substrait serializer was always cut at that node,
because the analyzer asks whether a plan encodes before it asks whether a node
commutes with the region split. That is right for a region reached over the wire:
the plan is encoded into the request and has to decode on the other side.

Standalone does not use that path. Its region server is the one built in the same
process, its query handler passes the request straight to `RegionServer::handle_read`,
and the plan stays a `LogicalPlan` from the frontend to the region. Encoding it is
work nobody reads, and refusing to place a node because it cannot be encoded
refuses a plan that would have travelled intact.

So the engine now knows whether its regions are reached in process, and the
encodability gate applies only when they are not. The capability is set where it
can be stated truthfully - the standalone bootstrap, which creates both the
frontend and the region server it reads - and it defaults to false everywhere
else, so an ordinary build and every other deployment keeps today's behaviour
byte for byte.

Skipping the gate skips one early return and adds no reason to expand:
`Categorizer::check_plan` still runs and still decides, and a plan that does not
encode for any other reason still expands at the node that caused it. Two tests
hold those two halves apart.

Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 ba9f36fdd0 docs(query): say what the fused placement does not prove
The arm above answers that a fused node commutes with the region split, and it
listed "no series is split across regions" among its reasons. That is not
established anywhere. A region evaluates the whole tree over the series its own
scan returns, so a series whose samples reach two regions is evaluated per
fragment, and a range function over a fragment is not the range function over the
series.

What the placement does have is three proofs about the node - its input is the
scan, it binds a metric name, and no node of its tree folds across series. What it
does not have is any check that the series stays in one region: the storage
partition key is whatever `partition_column_names()` names and may be a column
that varies within one series, and this categorizer runs before region selection,
so there is no selected-region vector here to consult either.

So the comment now says that plainly, names the deployment the placement is
correct under, and says what would make it unconditional. A single-region
deployment satisfies it; a partitioned table over several regions would answer
wrongly rather than fail.

Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 ff6c5db307 feat(query): place a fused node region-side, and ask for per-series scans
A `FusedSeries` node is built for one metric's series, and a region holds a subset
of those series with none split across regions, so the node commutes with the
region split when what it reads is per-series. It says so now, and the analyzer
may place it inside the merge scan.

Three conditions, each drawn from the conversion that has to succeed there:

- The node's input is exactly the table scan. The region builds the node from the
  scan's own raw-series entrypoint and cannot look through a filter, a projection
  or a sort in between, so a node over anything else has nothing to read where it
  would be placed.
- The node binds a metric name, because the region resolves the series of one
  named metric.
- No node of its tree folds across series. A region holds part of a metric's
  series, so an aggregate or a binary operation evaluated there would consume a
  partial input and return a wrong answer without saying so. Every operation the
  compiler emits today - input, instant, window, map - reads one series at a
  time, so this refuses nothing buildable; it is the conservative answer for a
  tree built some other way.

A scan below such a node also asks for the per-series distribution now. That is
what makes a region prepare its series scan, and only that path packs a series'
chunks into the frame the node reads.

Both are inert without a `FusedSeries` node, so an ordinary query keeps its plan
and its scan request.

Not yet reached: the analyzer asks whether a plan encodes before it asks whether
a node commutes, and this node has no Substrait serializer, so the plan is
expanded at the node and the region still receives the bare scan. A test pins
that boundary so it fails the day the node can cross it.

Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00
discord9 4beb5c7490 feat(query): offer a PromQL statement to the native fused-series route
Nothing called `native_compile::compile` or built a `FusedSeries`, so the native
tree and its node were library code with tests and no query behind them. This
adds the route, behind a per-query flag.

The statement is offered to the compiler at the root, where the expression the
reader wrote still exists. A shape the compiler refuses - `NotImplemented` - is
this route declining the query, and planning continues exactly as it would with
the flag off. Anything else is a real error.

The physical half is an arm in `PromExtensionPlanner`. Its children are already
built by then, so a metric scan arrives as the region scan, and the node asks that
scan for its named raw-series binding: `try_to_named_series_scan` resolves the
label source and ordering from the scanner itself, which is the only thing that
can name them, and returns the leaf the node reads. A scanner that cannot serve a
raw frame is reported, not silently re-planned.

Two things this needed that the route does not get for free:

- The region scan has to be prepared as one partition. `resolve_named_series_binding`
  refuses a scan whose partition count is not one, and a fresh scan has not set
  its partitions at all: `ParallelizeScan` runs after extension planning and would
  spread it, so the node would run one evaluator per partition and split a logical
  series. Preparing the ranges as one partition is what keeps `FusedSeriesExec`
  over one series stream, and it is why this route does not scan in parallel yet.
- The scan needs the per-series distribution hint. That is what makes a region
  prepare its series scan, and only that path packs a series' chunks into the frame
  this node reads. The existing planner asks for it through a sort that
  `ScanHintRule` turns into the hint; this node has no such sort below it, so the
  hint is attached to the scan directly.

A query the route cannot bind faithfully keeps the existing plan rather than a
wrong one: more than one selector, a label matcher that would survive
preprocessing, an `__field__` matcher, a native histogram field, a missing table,
or a shape whose grid the compiler's statement cannot express.

Signed-off-by: discord9 <discord9@163.com>
2026-10-23 19:57:38 +08:00