Skip to content

Commit ff6777e

Browse files
committed
tests: Sharpen the RQG test framework
1 parent b652e7c commit ff6777e

7 files changed

Lines changed: 387 additions & 116 deletions

File tree

ci/nightly/pipeline.template.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,18 @@ steps:
20452045
composition: rqg
20462046
args: ["banking", "--seed=$BUILDKITE_JOB_ID"]
20472047

2048+
- id: rqg-char-varchar
2049+
topics: [postgres]
2050+
label: "RQG char-varchar workload"
2051+
depends_on: build-aarch64
2052+
timeout_in_minutes: 45
2053+
agents:
2054+
queue: hetzner-aarch64-4cpu-8gb
2055+
plugins:
2056+
- ./ci/plugins/mzcompose:
2057+
composition: rqg
2058+
args: ["char-varchar", "--seed=$BUILDKITE_JOB_ID"]
2059+
20482060
- id: rqg-left-join-stacks
20492061
topics: [postgres]
20502062
label: "RQG left join stacks workload"

misc/python/materialize/mzcompose/services/rqg.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@
88
# by the Apache License, Version 2.0.
99

1010

11+
import os
12+
1113
from materialize.mzcompose.service import (
1214
Service,
1315
)
1416

1517

1618
class RQG(Service):
1719
def __init__(self, name: str = "rqg") -> None:
20+
volumes = [".:/workdir"]
21+
# RQG_CHECKOUT points at a local clone of MaterializeInc/RQG and
22+
# mounts it over the pinned checkout baked into the image, so that
23+
# changes to grammars or to the RQG library itself can be tested
24+
# without rebuilding the image. CI never sets this; it always runs
25+
# the commit pinned in test/rqg/Dockerfile.
26+
if checkout := os.environ.get("RQG_CHECKOUT"):
27+
volumes.append(f"{checkout}:/RQG")
1828
super().__init__(
1929
name=name,
20-
config={"mzbuild": "rqg", "volumes": [".:/workdir"]},
30+
config={"mzbuild": "rqg", "volumes": volumes},
2131
)

test/rqg/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ RUN perl -MCPAN -e 'install DBIx::MyParsePP'
2828

2929
RUN git clone --single-branch https://github.com/MaterializeInc/RQG.git \
3030
&& cd RQG \
31-
&& git checkout 4fe4e6f09f9599ce9f8ffd3f75d593b20518cc50 \
31+
&& git checkout 9b7a6af386f691d9ea40e4790185617cc164c92c \
3232
&& rm -rf .git
3333

3434
ENTRYPOINT ["/usr/bin/eatmydata", "/usr/bin/perl"]

test/rqg/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# RQG: grammar-based random query testing
2+
3+
Runs the [MaterializeInc/RQG](https://github.com/MaterializeInc/RQG) fork of
4+
the Random Query Generator against Materialize. Depending on the workload,
5+
generated queries are compared against Postgres, against another Materialize
6+
version, or checked against invariants embedded in the grammar. This finds
7+
query errors, panics, and (for the comparison workloads) correctness issues.
8+
9+
## Running
10+
11+
```shell
12+
# All workloads that are not disabled
13+
bin/mzcompose --find rqg run default
14+
15+
# One workload, with a fixed seed and shorter duration
16+
bin/mzcompose --find rqg run default subqueries --seed=12345 --duration=300
17+
18+
# Compare against another Materialize version instead of Postgres
19+
bin/mzcompose --find rqg run default wmr --other-tag=common-ancestor
20+
```
21+
22+
Workloads are defined in `mzcompose.py`. Each pairs a grammar with a dataset
23+
and a validator:
24+
25+
* `ResultsetComparatorSimplify` executes every query on both servers,
26+
compares the (sorted) result sets, and on a mismatch automatically shrinks
27+
the query to a minimal reproducer, printed between
28+
`RESULT COMPARISON ISSUE START/END` markers.
29+
* `QueryProperties,RepeatableRead` (banking workload) checks invariant
30+
assertions embedded in the grammar as `/* RESULTSET_... */` comments and
31+
re-executes SELECTs inside transactions to check snapshot stability.
32+
33+
## Reproducing a failure
34+
35+
Every run prints its effective integer seed and a ready-to-run repro command
36+
(`--- Reproduce with: ...`). Re-running with the same seed regenerates the
37+
same query stream. The seed must reach gentest.pl as a real integer; the
38+
harness hashes non-integer `--seed` values (such as `$BUILDKITE_JOB_ID`)
39+
because Perl would otherwise silently numify the string and collapse the
40+
seed space.
41+
42+
To shrink a reproducer further, `util/simplify-psql.pl` in the RQG repository
43+
delta-debugs a SQL file against a running Materialize:
44+
45+
```shell
46+
perl util/simplify-psql.pl --input-file=repro.sql --expected-output='internal error'
47+
```
48+
49+
Note that `--sqltrace` is currently only implemented in RQG's MySQL executor,
50+
so it has no effect here; use the seed for reproduction.
51+
52+
## Where grammars and datasets live
53+
54+
* Grammars and their datasets live in the
55+
[MaterializeInc/RQG](https://github.com/MaterializeInc/RQG) repository
56+
under `conf/mz/`, together with the RQG engine, pinned by commit in
57+
`Dockerfile`. Landing a change there means pushing to that repository and
58+
bumping the pin. For local iteration, mount your checkout with
59+
`RQG_CHECKOUT` (see below), which bypasses the pin.
60+
* `grammars/` and `datasets/` in this directory (the left-join-stacks
61+
workload) are mounted into the rqg container at `/workdir` and can be
62+
changed like any other test file, with no image rebuild.
63+
64+
Dataset files are loaded with `psql -v ON_ERROR_STOP=1` into every
65+
participating server, so they must be idempotent and valid in both the
66+
Materialize and Postgres dialects (dialect-specific files can be marked
67+
`Target.POSTGRES_ONLY` in `mzcompose.py`). After loading, the harness asserts
68+
row-count parity across both servers, so a load that silently diverged fails
69+
immediately instead of surfacing as a bogus result mismatch later.
70+
71+
Grammar rules that create or drop tables mid-run are a known trap: an init
72+
rule executes while other workers are already issuing queries, and that race
73+
produces spurious result differences (this is why the wmr and banking DDL
74+
lives in dataset files). Also keep result comparison deterministic: total
75+
ORDER BY when a query's row order matters (the comparator sorts rows, so
76+
plain ORDER BY correctness is not checked), no float accumulation whose
77+
result depends on evaluation order, and ORDER BY the aggregated expression
78+
itself inside STRING_AGG and friends.
79+
80+
## Developing against a local RQG checkout
81+
82+
Set `RQG_CHECKOUT` to mount a local clone of MaterializeInc/RQG over the
83+
pinned checkout in the image, e.g. to test changes to the RQG library or to
84+
the `conf/mz/` grammars without rebuilding the image:
85+
86+
```shell
87+
RQG_CHECKOUT=~/git/rqg bin/mzcompose --find rqg run default banking --duration=60
88+
```
89+
90+
CI always runs the commit pinned in `Dockerfile`; after changing the RQG
91+
repository, push there and update the pin.

test/rqg/datasets/star_schema.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ INSERT INTO d3 VALUES
104104
-- PK materialized views
105105
-- ---------------------
106106

107+
-- ft_pk carries all fact-table columns so the grammar can swap it in for ft.
108+
-- k is unique in the data above, which keeps DISTINCT ON deterministic.
107109
CREATE MATERIALIZED VIEW ft_pk AS
108-
SELECT DISTINCT ON (k) k, v FROM ft;
110+
SELECT DISTINCT ON (k) k, v, fk1, fk2 FROM ft;
109111

110112
CREATE MATERIALIZED VIEW d1_pk AS
111113
SELECT DISTINCT ON (pk1, pk2) pk1, pk2, v FROM d1;

0 commit comments

Comments
 (0)