* perf(promql): push down last row for instant queries Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test: guard instant last row correctness Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test: update instant query explain results Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix: apply last row after source deduplication Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix: scope post-merge last row selection Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(perf): cover instant PromQL last row Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(perf): sort generated SST rows before writing Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): cover instant last row selection in sqlness Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(promql): avoid last row hints for lossy timestamp casts Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): preserve stale marker semantics across flushes Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): decode dictionary labels in stale regression Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): avoid reserved column name in stale fixture Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): exercise LastRow hints and filtered results Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * refactor: keep after-merge mode in LastRow selector Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix: reject instant LastRow across residual filters Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test: expect after-merge selector in instant vector guards Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs: explain instant LastRow filter eligibility Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix: restrict instant LastRow to safe selector nodes Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * refactor: show LastRow merge mode directly in diagnostics Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test: refresh LastRow display in explain expectations Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
5.2 KiB
Direct-SST fixture format
The phase-1 generator should be a reusable fixture generator, not a collection of issue-specific data loaders.
Inputs
Each case describes:
[case]
name = "example_metric"
description = "example direct-readable-SST regression"
[scenario]
kind = "direct_readable_sst"
seed = 12345
[[scenario.tables]]
database = "public"
name = "example_metric"
engine = "mito"
append_mode = true
sst_format = "flat"
primary_key = ["host", "instance"]
time_index = "ts"
[[scenario.tables.columns]]
name = "host"
type = "STRING"
semantic = "tag"
distribution = { kind = "cardinality", values = 100, prefix = "host" }
[[scenario.tables.columns]]
name = "value"
type = "DOUBLE"
semantic = "field"
distribution = { kind = "deterministic_wave", min = 0.0, max = 100.0 }
[[scenario.tables.columns]]
name = "ts"
type = "TIMESTAMP(9)"
semantic = "timestamp"
[scenario.layout]
regions = 1
sst_count = 1024
rows_per_sst = 4096
row_group_size = 512
time_range_layout = "non_overlapping_per_sst"
series_layout = "round_robin"
[[scenario.queries]]
name = "count_all"
kind = "sql"
query = "SELECT count(*) FROM example_metric"
warmup = 0
iterations = 1
Query kinds
kind = "prom_http" runs a Prometheus range query by POSTing form fields to
/v1/prometheus/api/v1/query_range. query, start, end, and step are
sent as form fields; start and end define the range, and step defines its
evaluation interval. start, end, and step are optional in the case model
and default to empty form values when omitted. The table database is sent as the
db URL query parameter, rather than as a form field.
The measurement starts before the HTTP request is sent and ends after the full response body is read and parsed as JSON. It therefore includes request send, server-side Prometheus JSON response building, and response-body read and JSON parsing; it is not server-side-only latency.
series_layout = "round_robin" advances the timestamp once per generated row and
cycles series labels across rows. series_layout = "timestamp_major" writes all
series for one timestamp before advancing to the next timestamp; use it for
Prometheus-like high-cardinality scrape fixtures where short query windows should
still contain many raw samples. timestamp_major requires rows_per_sst to be
divisible by series_count. per_sst uses one series selected from the SST
index. These describe logical generation order. Before either flat or primary-key
SST format is written, each completed generated batch is physically sorted by
encoded primary key ascending, timestamp ascending, and sequence descending, as
required by the Mito Parquet writer; sorting preserves every generated column and
row, including deterministic-wave values.
[scenario] is required. Other scenario variants are intentionally unsupported
for now, but scenario.kind leaves room for future write_then_query and
cache_warm_query configuration.
The generator should use these declarations to produce:
- object-store SST files written through the real Mito SST writer
- manifest checkpoint and
_last_checkpoint - fixture summary with file IDs, row counts, time ranges, and generated schema
Why this is generic
The same fixture format should support different query-regression families:
- PromQL/TQL time-index pushdown
- SQL predicate pruning
- projection and row-group pruning
- series scan behavior
- joins or aggregation over controlled layouts
Issue-specific behavior belongs in case configs and thresholds, not in the generator implementation.
Direct generation vs realism
Direct SST generation is phase-1 because it is fast and reproducible:
- SST count, file time ranges, row groups, and label distributions are fixed by the case config.
- The same fixture can be used for base and candidate builds.
- Large pruning-sensitive datasets can be created without spending CI time on ingestion, memtable flush, or compaction.
It is less realistic than ingestion-path data because it bypasses writes, memtables, flush scheduling, and compaction. That tradeoff is intentional for PR-level query regression. A later nightly/release suite can add ingestion-based cases for end-to-end realism.
Multi-table cases are supported by generating one fixture directory per table.
Each table is still limited to one region, and the runner passes --table, the
discovered --region-id, and the discovered --table-dir for each table before
materializing all generated region subtrees into the same datanode data home.
This supports JOIN regression cases without changing existing single-table case
files.
Multi-table cases must use unique table names and unique (database, name)
pairs. The runner derives each fixture subdirectory from table index, database,
and table name, sanitizing path-unsafe characters to avoid collisions and unsafe
paths.
The preferred compatibility path is:
- create an empty table with the target build to seed catalog/table metadata;
- stop the process;
- generate readable SSTs and replacement manifest checkpoints offline using the seeded region metadata;
- restart and query the fixture.
Fully synthetic metadata is useful for generator smoke tests, but seeded metadata is safer for end-to-end query performance cases.