Add RapidCheck property-based tests that find real DuckDB bugs - #4
Add RapidCheck property-based tests that find real DuckDB bugs#4peterxcli wants to merge 2 commits into
Conversation
Standalone property-based test harness using RapidCheck, in the spirit of proptest harnesses (compare apache/arrow-rs#10352). Generates random LogicalTypes (incl. nested STRUCT/LIST/ARRAY/MAP/UNION/ENUM) and Values with adversarial special cases, and checks properties against independent oracles: text/SQL/JSON round trips, LIKE vs a reference matcher, string and list functions, integer/HUGEINT/DECIMAL arithmetic vs __int128, date parts vs civil-calendar algorithms, and persistent storage round trips across force_compression settings. Bugs found by these tests are documented with minimal reproductions in test/property/FINDINGS.md; the corresponding properties skip them via RC_PRE guards marked KNOWN ISSUE so they keep hunting for new ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ba6c729d14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| PROP_ASSERT_VALUES_EQUAL(mat.GetValue(0, 0), Value::BIGINT(int64_t(range_count))); | ||
| if (range_count > 0) { | ||
| auto expected_min = step > 0 ? start : start + (int64_t(range_count) - 1) * step; | ||
| auto expected_max = step > 0 ? start + (int64_t(range_count) - 1) * step : start; |
There was a problem hiding this comment.
Compute generated range endpoints in int128
When large steps cross zero, the mathematical endpoint fits in BIGINT even though the intermediate multiplication does not. For example, start = INT64_MIN, stop = INT64_MAX, and step = INT64_MAX produce range_count == 3, but this line evaluates 2 * INT64_MAX in int64_t; the default UBSan build therefore aborts inside the oracle, while an unsanitized build can report a false endpoint mismatch. Keep the endpoint calculation in __int128 until after the addition and checked conversion.
Useful? React with 👍 / 👎.
| include(FetchContent) | ||
| FetchContent_Declare(rapidcheck | ||
| GIT_REPOSITORY https://github.com/emil-e/rapidcheck.git | ||
| GIT_TAG master |
There was a problem hiding this comment.
Pin RapidCheck to an immutable revision
When RAPIDCHECK_SOURCE_DIR is not supplied, every clean configuration fetches whatever commit master references at that moment. An upstream API change, force-push, or regression can consequently make the same DuckDB commit stop building or change its property-generation behavior without any repository change. Use a specific commit or release tag so the test harness remains reproducible.
Useful? React with 👍 / 👎.
What is this?
A property-based test harness for DuckDB using RapidCheck (the C++ equivalent of Rust's
proptest), inspired by apache/arrow-rs#10352.Instead of fixed inputs, each test states a property and RapidCheck runs it against hundreds of randomly generated inputs, shrinking any failure to a minimal counterexample:
CAST(v AS VARCHAR) -> CAST(back AS T),Value::ToSQLString()-> re-parse,to_json-> cast back, INSERT -> SELECT, and write -> CHECKPOINT -> reopen for everyforce_compressionsettingLIKE/ILIKEvs a 40-line reference matcher; substring/split/pad/trim/translate/levenshtein vs reference implementations; integer/HUGEINT/DECIMAL arithmetic must error exactly when the__int128result is out of range; date parts vs independent civil-calendar algorithmslist_sortvsORDER BY,list_containsvslist_positionThe core piece is
test/property/generators.cpp: randomLogicalTypes (including nested STRUCT/LIST/ARRAY/MAP/UNION/ENUM) and randomValues of any type, seeded with adversarial special cases (NaN/-0.0/denormals, NUL bytes, combining marks, quotes/braces in strings, extreme dates/timestamps/intervals, decimal width/scale limits).How to build
How to run / how to find bugs
When a property fails you get the shrunk counterexample plus the exact value/SQL/error, e.g.:
…which is how the TIMETZ formatting bug below was found (two different offsets printing identically).
Bugs found so far
Full write-up with minimal reproductions in
test/property/FINDINGS.md. Verified onv1.6.0-dev13151:SELECT last_day(DATE '5881580-07-10')throws an INTERNAL error that invalidates the whole database —last_daycomputes first-of-next-month (out of range) and is not markedSetFallible()date_trunc('week'/'isoyear', DATE '5877642-06-25 (BC)')— signed integer overflow (UB) indate_t::operator-(date.hpp:58); aborts under UBSan'12:00:00-05:00:59'::TIMETZprints12:00:00-05:59(a different offset)list_sort([INTERVAL '31 days', INTERVAL '1 month'])returns[31 days, 1 month]although31 days > 1 monthistrueandORDER BYsorts the other way —create_sort_keyskips interval normalizationValue::ToSQLString()doesn't escape quotes in STRUCT keys, renders BIT values bare (re-parse as INTEGER), and loses UNION member typesto_microseconds(9223372036854775807)::VARCHARprints 10-digit hours that the interval parser (9-digit limit) cannot read backlist_contains([false,true,NULL], NULL)returns NULL butlist_position(...)returns 3 — the twin functions disagree on NULL semanticsweekofyear/isoyear/date_truncfail with "Date out of range" on minimum-year dates whose result is representableSort::Sort->DecodeSortKeyBind), which breaks for API-created types with unparseable string forms (e.g. ENUM values containing NUL, possible via Arrow dictionaries)'883406386745030.3'::JSON::DECIMAL(16,1)->883406386745030.2(JSON -> DECIMAL goes through a double)(-32768)::SMALLINT % (-1)::SMALLINTerrors ("Overflow in division") where PostgreSQL returns 0levenshtein/damerau_levenshtein/hammingcount bytes, not characters, for multi-byte UTF-8Each known bug is fenced off in the tests with an
RC_PREguard markedKNOWN ISSUE, so the suite is fully green (12 test cases / 35 properties at 1000 iterations each) and keeps hunting for new bugs rather than rediscovering these.Files
test/property/README.mdtest/property/FINDINGS.mdtest/property/CMakeLists.txt-DDUCKDB_BUILD_DIR=...test/property/include/property_test.hppPropDBhelpers, value comparison, assertion macrostest/property/generators.cpptest/property/test_*.cpp🤖 Generated with Claude Code