LessDB Test-Suite Research

LessDB Test-Suite Research

How mature database projects structure their test suites, and what LessDB should copy. Researched against the reference projects' GitHub repos and docs; mapped to LessDB's actual architecture (docs/ARCHITECTURE.md, docs/DESIGN-DECISIONS.md, docs/ROADMAP.md).

Implementation status (2026-09)

The recommendations below are being implemented incrementally:


1. Executive summary

The reference projects converge on the same shape, which LessDB should adopt as-is:

  1. Golden SQL tests are the backbone. ClickHouse, DuckDB, DataFusion, and
  2. SQLite all drive SQL correctness through sqllogictest-style input/expected files. This is the single highest-leverage harness LessDB can add, and a Rust crate (sqllogictest) already exists.

  1. Unit tests live next to the code (gtest/catch2 for C++, #[cfg(test)]
  2. mods + tests/ dirs for Rust). Storage/format/index logic is unit-tested exhaustively before any integration layer.

  1. Fuzzing is a first-class CI stage, not an afterthought: libFuzzer for
  2. ClickHouse, SQLSmith for DuckDB, seeded equivalence fuzz for DataFusion, dbsqlfuzz for SQLite.

  1. Differential testing against an independent engine is how the best
  2. projects catch semantic bugs (SQLite vs Postgres/MySQL; DuckDB re-runs SQLite's entire sqllogictest corpus). LessDB has a natural oracle in DuckDB via the duckdb Rust crate.

  1. Crash-safety and concurrency are validated with fault injection
  2. (kill -9, corrupt files) and isolation/restart tests, not just happy-path integration.

  1. Vector search is tested with recall@k regression (index vs. brute-force
  2. ground truth), index rebuild, and persistence roundtrips — exactly what LanceDB/Qdrant do.

  1. Slow/cloud/chaos tests are tiered: fast unit+SQL tests on normal CI,
  2. S3/MinIO + LDAP + multi-node + fuzz + chaos on dedicated/self-hosted runners.


2. ClickHouse

Repo: https://github.com/ClickHouse/ClickHouse

What they test, at what level

LevelHarnessLocation
UnitGoogleTest (gtest), unit_tests_dbms binary (-DENABLE_TESTS)src/**/tests/ (e.g. src/Storages/MergeTree/tests/, src/IO/tests/, src/Common/tests/)
Functional (golden SQL)tests/clickhouse-test Python runner (~7.3k lines) + .reference filestests/queries/0_stateless/
Integration (real services)pytest + pytest-xdist + docker composetests/integration/ (~700 test_* dirs)
FuzzinglibFuzzer targets (OSS-Fuzz style)tests/fuzz/
Stress / chaosrun-all-concurrently + Jepsen + libfiutests/stress/, tests/jepsen.clickhouse/, contrib/libfiu/
Performance.xml specs + comparison vs referencetests/performance/

Stateless tests (tests/queries/0_stateless/) — the workhorse

A test is a directory containing a .sql (or .sh) file plus a .reference file of expected output; the tests/clickhouse-test runner executes it against a running server and diffs stdout against .reference. ~15,000 tests cover SQL semantics, functions, type casts, edge cases, and engine behaviour.

Conventions worth copying verbatim:

Representative cases worth copying:

Note: tests/queries/1_stateful/ was removed from master (it needed non-public Yandex.Metrica data); "stateful" now survives only as a runner tag/suite concept (--no-stateful). LessDB's equivalent is a committed fixture dataset (a generated lineitem/hits parquet) shared by the golden and differential suites.

Integration tests (tests/integration/)

pytest-based (~700 test_* dirs), each spinning up real services via docker compose; entry point is now python -m ci.praktika run "<JOB>". Covers replication/distributed, LDAP/ACL/SSL, S3/object storage, external dictionaries, Keeper (Raft), Kafka, MySQL/Postgres and more. This is the exact model for LessDB's LDAP (glauth/OpenLDAP container) and S3 (MinIO container) tests — notably tests/integration/test_ldap/ and tests/integration/test_storage_s3/.

Fuzzing (tests/fuzz/)

OSS-Fuzz style (tests/fuzz/build.sh + .options/.dict/corpora + runner.py). Targets include the SQL lexer/select_parser/create_parser/ execute_query, format_fuzzer, data_type_deserialization_fuzzer, codec decompress fuzzers, and mergetree_checksum_fuzzer (corrupt part bytes → checksum must catch it). Query fuzzing is layered: 00746_sql_fuzzy.pl, the AST fuzzer (CI ast_fuzzer_*), BuzzHouse, and SQLancer (nightly). Data fuzzing via format/deserialization/column fuzzers. Crashes are reported as GitHub issues automatically.

Stress / chaos

Performance

tests/performance/.xml specs (settings/substitutions/create/fill/query/ drop) run by scripts/perf.py, compared against a master reference with "backward-incompatible query" reporting (CI performance_comparison_).

CI (Praktika orchestrator)

ClickHouse uses its own Praktika orchestrator (ci/defs/job_configs.py, python -m ci.praktika run "<JOB>"). Job families: unit_tests_{asan_ubsan, msan,tsan}, stateless_tests_ (many flavors), integration_tests_, stress_test_, ast_fuzzer_, buzzhouse_, performance_comparison_, clickbench_*, keeper_stress_tests_pr, fast_test. Sanitizers (ASan/UBSan/ MSan/TSan) run per-commit; a flaky-check runs new tests 100× (functional) or 10× (integration) to catch flakiness before merge.


3. DuckDB

Repo: https://github.com/duckdb/duckdb

Test directory structure (test/)

test/
  sql/           sqllogictest .test files, split by component (~4,692 files)
  sqlite/        sqllogictest runner + SQLite's own select1-4.test_slow corpus
  api/           C API + binding tests (also test/common, test/...)
  fuzzer/        SQLSmith repros (duckfuzz/, pedro/, sqlsmith/, public/)
  ossfuzz/       OSS-Fuzz harnesses
  optimizer/     plan/optimizer correctness
  parquet/       parquet reader tests (data + golden files)
  extension/     loadable-extension tests
  ...

Unit and SQL-level tests run through a single Catch2 unittest binary (test/unittest.cpp). The split is: C++ unit tests colocated in src/ (and test/api, test/common, …), while test/sql/*.test sqllogictest files are each registered as a Catch2 test case by test/sqlite/test_sqllogictest.cpp.

sqllogictest usage

DuckDB adopted SQLite's sqllogictest format (query, query I, statement ok, ---- expected results). Files in test/sql/ are organized by feature (test/sql/aggregate/, test/sql/window/, test/sql/join/). The runner is built into the unittest/shell binaries. This is the format LessDB should use via the Rust sqllogictest crate.

The SQLite-corpus suite (test/sqlite/)

DuckDB runs SQLite's entire public sqllogictest corpus against itself as a differential compatibility check — the select1.testselect5.test SQLite files (plus slow variants like select4.test_slow) — millions of queries, caught as wrong-answer diffs. This is the archetype for LessDB's "compare query results vs DuckDB on shared data" requirement: embed DuckDB (the duckdb crate), run the same SQL over the same parquet/CSV data, and diff sorted results.

Fuzzing (test/fuzzer/)

SQLSmith (the CockroachDB/Postgres random-query generator) is adapted into a DuckDB extension; duckdb-fuzzer drives random SQL and checks for crashes/wrong answers, and there is an ossfuzz/ harness for OSS-Fuzz. LessDB's equivalent: an arbitrary-driven SQL generator feeding both the DataFusion parser and the engine, checking for panics and for differential agreement with DuckDB.

CI

GitHub Actions (.github/workflows/Main.yml) runs fmt/clippy/unit tests, then the sqllogictest, SQLite-corpus, and extension suites as separate jobs.


4. Apache DataFusion

Repo: https://github.com/apache/datafusion

This is the single most important reference because LessDB's query layer is DataFusion — LessDB inherits DataFusion's SQL semantics and can reuse its test harness patterns verbatim.

sqllogictest harness (datafusion/sqllogictest/)

Fuzzing (seeded equivalence fuzz, not libFuzzer)

The historical datafusion/fuzz-utils was a rand-based helper crate, and it was removed in PR #2081 (Feb 2023). There are no cargo-fuzz/libFuzzer targets in the tree today. Current fuzzing = seeded, in-process equivalence/differential harnesses at datafusion/core/tests/fuzz_cases/, gated behind the extended_tests cargo feature: a fixed seed generates random inputs and asserts a specialized operator implementation matches a naive reference (or that results are stable). LessDB should adopt this pattern for "optimized == reference" checks and additionally run cargo-fuzz + arbitrary for panic-free SQL/parse/data decode (bloom filter, meta.json, parquet part reads).

Benchmarks (datafusion/benchmarks/)

ClickBench and TPC-H harnesses (cargo run --release --bin tpch -- ...) that also serve as semantic regression tests: CI job verify-benchmark-results runs benchmark-plan + benchmark-sqllogic and fails on any git diff in the generated plans/results. LessDB's roadmap already lists less bench vs ClickHouse/DuckDB; the TPC-H/ClickBench corpus doubles as a correctness harness and should produce plan files that are committed and diffed in CI.

Unit tests

CI

.github/workflows/rust.yml runs fmt/clippy/test, then the sqllogictest suite and extended_tests (fuzz cases). Benchmarks and their verify-benchmark-results diff gate are separate jobs.


5. SQLite

Repos/sources: https://sqlite.org/testing.html, https://github.com/sqlite/sqlite

SQLite is the gold standard for database testing and the origin of the sqllogictest format LessDB will use.

The TCL test suite (test/*.test)

~1,400 TCL files (test/alter.test, test/select1.test, test/where.test, test/join.test, …) yielding ~51,000 distinct cases, driven by a TCL harness (test/tester.tcl) with do_test/execsql helpers and do_catchsql_test for asserting an error is raised — the model for LessDB's "expected error" cases. Runs in modes quick, veryquick, all, and permutations (the latter re-runs the suite under different compile-time options).

TH3 (Test Harness #3)

A commercial-grade test generator: hundreds of millions of tests generated from templates that emit SQL + expected results + crash injection, checking wrong answers, leaks, and crashes. The harness source is public domain (available to SQLite Consortium members), the test data is private. The copyable idea is not TH3 itself but its shape: generate parameterized SQL over schema/data templates, and check correctness + crash-resistance in one loop.

Other vectors

Why it's famously thorough

100% branch and 100% MC/DC coverage claimed, enforced with gcov -b, plus testcase()/ALWAYS()/NEVER() macros that make every branch reachable, mutation testing, and OOM/IO-error/crash fault injection. The process lesson for LessDB: every bug fix must land with a sqllogictest/proptest regression case in the same PR. Two directly-copyable sqllogictest idioms: rowsort /valuesort for order-independent result comparison, and running the same query optimized vs. unoptimized to catch optimizer bugs.


6. PostgreSQL (brief)

Repo: https://github.com/postgres/postgres


7. LanceDB (vector search)

Repos: https://github.com/lancedb/lance (now lance-format/lance; Rust core), https://github.com/lancedb/lancedb (Python + server).

Layout (Rust core is a workspace under rust/)

Recall@k — the pattern to copy (rust/lance/src/index/vector/ivf/v2.rs)

All IVF-family recall/regression tests live here, structured as:

Canonical templates worth naming directly:

Property-based tests & data generation

Fault injection

No cargo-fuzz/libFuzzer — fault injection is a store proxy: rust/lance/src/utils/test/failing_store.rs (FailingProxyStore.fail_after_n (method, path_substr, skip, error)) exercises partial-write cleanup paths; throttle_store.rs injects latency. Copy this for LessDB's object-store write/cleanup fault tests.

Persistence / versioning / compaction

Integration vs. unit split

Rust core: inline unit tests + rust/lance/tests/ binary; Python surface: python/python/tests/ (test_index.py, test_query.py, test_table.py). Key point: the Python layer does not assert recall@k — the Rust core owns recall; Python asserts API surface and plan shape (e.g. assert "ANN" in query.explain_plan(verbose=True), test_search_after_merge). Server/cloud is tested separately (test_remote_db.py, test_e2e_remote_db.py). LessDB should mirror this: recall owned by less-vector Rust tests; SDKs assert API + plan shape only.

Qdrant (brief)

Repo: https://github.com/qdrant/qdrant

Milvus (brief)

Repo: https://github.com/milvus-io/milvus


8. What LessDB should copy, mapped to our feature set

Direct mapping from the user's requested categories to concrete harnesses and representative test names.

8.1 SQL correctness — sqllogictest-style golden tests

Example tests/sqllogictest/test_files/select.slt:

statement ok
CREATE TABLE t (id BIGINT UNIQUE, ts TIMESTAMP, v DOUBLE) ENGINE = MergeTree
  ORDER BY id

statement ok
INSERT INTO t VALUES (1, '2024-01-01 00:00:00', 1.5), (2, '2024-01-02 00:00:00', 2.5)

query IIR
SELECT id, id, v FROM t ORDER BY id
----
1 1 1.5
2 2 2.5

# pruning must skip the part entirely when the bloom filter proves absence
query I
SELECT count(*) FROM t WHERE id = 999
----
0

statement error
SELECT * FROM t WHERE nonexistent_col = 1

8.2 Storage engine — crash safety, WAL, merges, pruning

8.3 Vector search — recall@k, index rebuild, persistence

8.4 Graph/context store — traversal invariants, deletion cascades

8.5 MCP protocol — conformance, error handling

8.6 Auth — LDAP flows, fail-closed

8.7 Concurrency — multi-writer, parallel queries

8.8 Fault injection — corrupt files, kill -9 mid-write

8.9 Differential testing — vs DuckDB

8.10 Fuzzing — SQL parser fuzz, data fuzz

8.11 Upgrade / migration

8.12 CI integration


9. Recommended test-suite architecture for LessDB

9.1 Directory layout

crates/<crate>/
  src/...          #[cfg(test)] unit tests colocated with code
  tests/           crate-level integration tests
  benches/         per-crate criterion benches (optional)

tests/                     (workspace-level integration harnesses)
  sqllogictest/            .slt golden files + runner binary
    test_files/
      select.slt
      aggregate.slt
      join.slt
      window.slt
      explain.slt
      pruning.slt
      vector_search.slt
  differential/            LessDB vs DuckDB harness + corpus
  integration/
    ldap/                  glauth/OpenLDAP via testcontainers
    s3/                    MinIO via testcontainers
    multinode/             multi-node SharedMergeTree
    fault/                 kill -9, corrupt-file injection
    upgrade/               old-format fixture migration
  fixtures/                committed parquet parts, old meta.json, recall dataset

fuzz/                      cargo-fuzz targets (top-level, cargo fuzz discovers it)
  fuzz_targets/
    sql_parser.rs          DataFusion SQL parser fuzz (no panic)
    data_roundtrip.rs      Arrow->parquet->Arrow roundtrip fuzz
    meta_json.rs           malformed meta.json decode fuzz

9.2 Harness choices (Rust crates)

ConcernCrateNotes
Golden SQL testssqllogictest (risinglightdb/sqllogictest-rs)Same crate DataFusion uses; supports async, expected-error, sorted output
Property testingproptestCodec roundtrips, pruning soundness, merge dedup invariants
Fuzzingarbitrary + cargo-fuzz (libfuzzer-sys)SQL parser fuzz, data fuzz, bloom/meta decode fuzz
Parametrized testsrstest + test-case#[case(nlist, metric, recall)] for recall thresholds, codec/type matrices (LanceDB/DataFusion pattern)
Snapshot testinginstameta.json, EXPLAIN plans, MCP JSON responses
Differential oracleduckdb (duckdb-rs)Embed DuckDB for result diffing
Temp files/dirstempfileAll engine tests
Serializing global stateserial_testTests touching the singleton runtime/config
CLI testingassert_cmd + predicates + assert_fsless CLI smoke/e2e
HTTP mockinghttpmock / mockitoless-server route/auth tests (if not using a real dir)
LDAPglauth (Go binary) in a container + ldap3 clientReal bind/rebind flows
S3MinIO container + object_storeAlso object_store's memory:///file:// for fast tests
Containerstestcontainers / testcontainers-modulesLDAP + MinIO lifecycle

9.3 What to build ourselves

  1. less-sqllogictest runner — a thin binary that builds a
  2. SessionContext, registers LessTableProvider + the vector_search table function, and hands control to the sqllogictest crate. ~150 lines. The sqllogictest crate API is minimal: implement the AsyncDB trait (async fn run(&mut self, sql: &str) -> Result<DBOutput> returning DBOutput::Rows, DBOutput::StatementComplete(_), or DBOutput::StatementError(_)), then drive it with sqllogictest::Runner::new(|| async { test_ctx }).run_file(path).await. Use its record mode to generate .slt files from a running engine and validate mode in CI; expected-error assertions map to statement error lines in the .slt file. DataFusion ships this exact pattern as its datafusion-sqllogictest crate — copy that, but backed by LessDB's own session setup.

  1. Differential harness (tests/differential/) — a binary that loads the
  2. same parquet into LessDB and DuckDB, runs a shared SQL corpus, and diffs sorted results with a divergence allowlist.

  1. Fault-injection helpers — a process-level test harness that can
  2. kill -9 the child mid-write, corrupt part bytes, and assert recovery (Postgres TAP model, but in Rust).

  1. Recall harness (tests/recall/) — brute-force flat ground truth +
  2. threshold assertions + a tiny curated seed dataset committed as a fixture.

  1. Part/manifest golden helpers — write a part, assert meta.json schema
    • stats exactly (via insta), reload and diff.

9.4 Phased rollout

Phase 1 — foundation (now, runs on GitHub Actions, fast)

Phase 2 — engine correctness (mostly fast, some slow)

Phase 3 — differential + fuzz (self-hosted runner)

Phase 4 — cloud, multi-node, chaos, perf (self-hosted runner only)

9.5 Which tests need real S3/MinIO, and where they run


10. Citations / consulted URLs