A Nix flake for running a local GreptimeDB cluster with process-compose.
All processes are disabled by default. Start what you need:
nix developprocess-compose up garageprocess-compose up standalone| Protocol | Address |
|---|---|
| HTTP | http://127.0.0.1:11040 |
| gRPC | 127.0.0.1:11041 |
| MySQL | 127.0.0.1:11042 |
| PostgreSQL | 127.0.0.1:11043 |
These are the client-facing ports, shared by standalone, standalone-fs, and the distributed cluster's haproxy — they are never run at the same time, so the same client code works unchanged across every mode.
process-compose up standalone-fsSingle-node GreptimeDB using local disk instead of Garage S3. No garage/etcd dependency — fastest mode to start. Data lives under .greptimedb/standalone-fs/. Same connection details as standalone above.
process-compose up haproxy-standbyTwo enterprise standalone instances form an active/standby pair sharing Garage S3 (main data store) and a Postgres table (shared metadata + leader election); each keeps its own dedicated WAL. Only the elected leader accepts writes; the follower rejects writes and serves read-refreshed queries. haproxy-standby routes the client ports to whichever node is currently leader.
- Requires an enterprise
greptimebinary provided in place as./greptime(or viaGREPTIME_BIN) — the same path every other mode uses. The OSS binary cannot run this mode. - Election backend is Postgres (the shared
postgresprocess on port 11080) — the enterprise active/standby election is built on the external RDS metadata store. - Clients use the same ports 11040-11043 as every other mode; traffic always reaches the active leader.
| Protocol | Address |
|---|---|
| HTTP | http://127.0.0.1:11040 |
| gRPC | 127.0.0.1:11041 |
| MySQL | 127.0.0.1:11042 |
| PostgreSQL | 127.0.0.1:11043 |
Test failover with process-compose process stop standby-a (stop the leader); standby-b is elected and haproxy reroutes automatically. Inspect roles via curl http://127.0.0.1:11070/status/standalone/role (and :11074).
process-compose up haproxyClients connect to haproxy on the same ports as standalone (11040-11043) — haproxy load-balances the internal frontend instance(s), which are not exposed directly. So client code written for standalone works here unchanged.
| Protocol | Address |
|---|---|
| HTTP | http://127.0.0.1:11040 |
| gRPC | 127.0.0.1:11041 |
| MySQL | 127.0.0.1:11042 |
| PostgreSQL | 127.0.0.1:11043 |
Place a greptime binary in the project root before starting. Process-compose runs on port 11099.
A utility script for common operations against the running cluster:
./testbedctl psql # PostgreSQL CLI
./testbedctl mysql # MySQL CLI
./testbedctl s3 ls # List S3 buckets
./testbedctl s3 ls s3://test-bucket/ # List objects in bucket
./testbedctl s3 ls s3://test-bucket/ --recursive # List all objects
./testbedctl telemetrygen # Ingest OTel traces
./testbedctl telemetrygen down # Stop trace ingestion
./testbedctl telemetrygen metrics up # Ingest continuous OTel metrics (gauge/sum/histogram)
./testbedctl telemetrygen metrics down # Stop metrics ingestion
./testbedctl metrics-partition # Partition greptime_physical_table into 4 ranges on 'timebox'
./testbedctl flush <table> # Flush a table's memtable (admin flush_table)
./testbedctl compact <table> [type] [opts] # Trigger compaction (admin compact_table); optional twcs/swcs + parallelism=N
./testbedctl gc <table> [--full] # Trigger garbage collection (admin gc_table); --full = full file listing
./testbedctl gc-regions <id>... [--full] # Trigger GC for specific regions (admin gc_regions); region ids are u64
./testbedctl purge <table> [-y] # Permanently purge a DROPPED table's data (admin purge_table); -y skips prompt
./testbedctl clean # Remove .greptimedb
./testbedctl duckdb # DuckDB shell with the Iceberg REST catalog attached
./testbedctl pyiceberg [table] [-local] # Query Iceberg tables via pyiceberg (-local = standalone-fs)
./testbedctl spark # Apache Spark SQL (local mode) on the Iceberg REST catalog
./testbedctl spark demo # create a TIMESTAMP(6) table + run sample queries
./testbedctl spark -e "SELECT ..." # one-shot Spark SQL query
./testbedctl trino # Trino SQL on the Iceberg REST catalog (interactive)
./testbedctl trino demo # create a TIMESTAMP(6) table + run sample queries
./testbedctl trino --execute "SELECT ..." # one-shot Trino SQL query
./testbedctl trino stop # stop the Trino server containertestbedctl spark runs Apache Spark 4.1.3 + Iceberg 1.11.0 in local mode
(single JVM, no master/worker) against GreptimeDB's Iceberg REST catalog
(catalog name greptime, via Garage S3). On first run it pulls the Spark image
and the Iceberg/AWS jars automatically (cached in spark/jars/, gitignored).
Two modes — interactive and predefined:
./testbedctl spark # interactive spark-sql shell (Ctrl+D to exit)
./testbedctl spark demo # create a TIMESTAMP(6) demo table + sample-query battery
./testbedctl spark test # automated TIMESTAMP(6) compatibility suite (PASS/FAIL, exit 1 on failure)
./testbedctl spark -e "SQL" # one-shot query
./testbedctl spark -f q.sql # run a SQL scriptRequirements & caveats (see spark/README if present, and issue.md):
- Cluster must be up (
process-compose up standaloneorup haproxy) and the table flushed so Iceberg manifests exist. - Declare time-index columns as
TIMESTAMP(6)so the parquet is microseconds (matching the Icebergtimestamptzschema); plainTIMESTAMPwrites milliseconds and breaks time-range predicate pushdown. - Keep the default (vectorized) reader — GreptimeDB writes an internal
__sequenceUINT_64column that breaks the non-vectorized reader on every table. - Unsupported GreptimeDB types (fail when projected): unsigned ints
(
* UNSIGNED) andDECIMAL. Everything else — bool, signed ints, float, double, string, binary, date,TIMESTAMP(6)— works for SELECT / WHERE / GROUP BY / ORDER BY / aggregations / joins / windows.
./testbedctl spark test (source: scripts/spark-compat-test) is an automated,
self-verifying regression suite: it builds a TIMESTAMP(6) fixture covering every
Spark-supported type plus µs edge cases (epoch, 1900, 2262-04-11 max,
.000001/.999999, adjacent-µs rows, NULLs, numeric extremes), a join table,
and one table per known-bad type — then runs ~80 assertions with exact expected
output (precision round-trips, time predicates, µs-granularity predicate
pushdown (EXPLAIN), date/time functions, windows, joins, aggregations, NULL
semantics, binary/date columns, casts, incremental append visibility, Iceberg
.snapshots metadata, and time travel). It drops and recreates its own
spark_ts6_* / spark_bad_* tables, so it is safe to re-run any time; use it
after swapping in a new greptime binary. Raw Spark output lands in
.spark/compat/{battery,append}.out for debugging.
Verified against GreptimeDB 26.05.2.0 — 78/78 checks pass. Known issues it asserts as current behavior (fail loudly if they change):
| Issue | Behavior |
|---|---|
WHERE on an unsigned column |
silently returns wrong results (0 rows; predicate answered from manifest stats) — never filter on unsigned columns |
TIMESTAMP(9) columns |
no longer crash: read back shifted 1000× (ns parquet read as µs), e.g. 2024-01-01 00:00:00.123456789 → +55969-09-28 00:02:03.456789 |
| Iceberg snapshot history | only the current snapshot is retained — each flush expires previous snapshots, so VERSION AS OF <older> fails (Cannot find snapshot with ID); time travel to history is unavailable |
Also note: good columns on a table that also has an unsigned/decimal column
remain readable (SELECT ts, count(*) work) — only projecting the bad column
fails.
testbedctl trino runs Trino 471 (server on port 18080, single node) with its
Iceberg connector pointed at the same REST catalog.
./testbedctl trino # interactive shell (trino CLI)
./testbedctl trino demo # demo table + sample-query battery
./testbedctl trino --execute "SELECT ..." # one-shot query
./testbedctl trino stop # stop the server containerTrino caveat (fixed in GreptimeDB 26.05.2.0, 2026-08-15 build) — earlier
builds wrote manifest split_offsets as row-group start offsets, making Trino
silently return 0 rows; the fix landed and no rebuild is needed.
Two remaining operational notes: use
ts < CAST(TIMESTAMP '…' AS timestamp(6) WITH TIME ZONE) for time-predicate
pushdown (tz-less literals don't prune), and testbedctl trino restart after
testbedctl clean (regenerated S3 keys).
Trino type matrix (after rebuild): all Spark-supported types work the same;
unsigned ints read without error but wrap (u32=4294967295 → -1, u64 max →
-1) — better than Spark's hard error, but silently wrong above INT32/INT64 max;
DECIMAL and TIMESTAMP(9) (nanos) fail with hard errors (same schema/physical
mismatches as Spark).
process-compose process start metasrv-1
process-compose process start frontend-1
process-compose process start flownodeprocess-compose down
./testbedctl clean