You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a3bd763
Browse filesBrowse the repository at this point in the historyBrowse files
test(fuzz): fuzz unions and writes on every dialect (#122)
* test(fuzz): fuzz unions and writes on every dialect
Unions: `unionAll` of two or three row plans that select the same
aliases (each alias one sort in every branch), each branch with its own
join, filter, DISTINCT, ORDER BY and LIMIT, under the union's ORDER BY,
LIMIT and OFFSET. The model concatenates the branches' rows and pages
them; the union pages only when it orders by every alias, so ties are
equal rows. New properties `unions match the model (<target>, …)`.
Writes: a new suite. Each input gets a fresh scratch table on the
dialect's first target and runs a sequence of builder writes: multi-row
inserts leaving defaulted or nullable columns out, INSERT … SELECT,
ON CONFLICT DO NOTHING / DO UPDATE (with the existing and the excluded
row), UPDATE and DELETE with a filter or allRows(), and RETURNING. After
every write the table is read back and compared with a JS model of its
rows, and RETURNING with the rows the write touched. A write the dialect
does not support is still generated and must be refused at compile time.
`FuzzDialect.writes` declares the capabilities and the scratch table
(query `Table`, create/drop DDL, `exec`); `DialectModule` gains
unionAll, compileUnion, insertInto, update and deleteFrom. ClickHouse
targets pass mutations_sync=2 and lightweight_deletes_sync=2 so an
ALTER TABLE UPDATE or DELETE is visible to the next read.
Found: a ClickHouse 26 UNION ALL column whose branches have numeric
types with no common supertype (UInt64 and Int64, Int64 and Float64)
becomes a Variant, and ORDER BY on it fails. Recorded in known-issues
(clickhouse-union-variant-order-by) and pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(clickhouse): cast a union's mixed numeric column to Float64
A UNION ALL column of numbers whose branches have no common ClickHouse
type (`Id + 1` is UInt64 and `Id - 1` Int64; Int64 and Float64) becomes a
Variant, and an ORDER BY on it fails ("Variant/Dynamic are not allowed in
ORDER BY keys"); with use_variant_as_common_type = 0 the union itself is
refused. The union fuzzer found it.
On a dialect with `unionNumberCast` (ClickHouse), a column is selected as
toFloat64(…) in every branch when every branch selects a number and their
types may differ: different declared types, or different computations.
Every numeric ClickHouse type decodes to a JS number, so Float64 loses
nothing the decode keeps. The known-issues entry is gone; the pinned
counterexample now passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(fuzz): a query pinned to one org by its own filter is single-tenant for that org
The tenant oracle took "single-tenant" to mean the orgId param's tenant.
`OrgId IN ('org_2')` pins a query to org_2, which the builder rightly
reports as single-tenant; the oracle called reading org_2 a leak. Where
the plan's WHERE filters OrgId itself, any one org is right; otherwise
it must still be the param's. Found by a 20x soak on D1; pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(clickhouse): cast a union's numeric column around each branch, not inside it
Inside a branch the cast ran before that branch's DISTINCT, so two UInt64
values past 2^53 (one Float64) became one row. The cast now wraps each
branch, SELECT toFloat64(x) AS x, … FROM (<branch>), so DISTINCT, GROUP BY
and LIMIT see exact values (CodeRabbit). The writes suite now accepts only
a QueryBuilderError or a QueryBuilderDefect as the refusal of a clause a
dialect lacks, not any defect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(clickhouse): order a union's mixed numeric column by exact keys, never cast its values
The previous fix cast such a column to Float64, which an independent
review showed corrupts integers past 2^53 wherever the values stay on the
server: INSERT ... SELECT from the union stored 9007199254740992 for
9007199254740993, and an IN over it matched nothing. Only the union's
ORDER BY was ever broken (a Variant is not a valid sort key), so only the
sort key changes now: toFloat64(x), then accurateCastOrNull(x, 'Int128'),
which keeps integers past 2^53 in exact order. Checked on ClickHouse 26.8
with UInt64/Int64 values around 2^53 and 2^64 - 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(clickhouse): order union numbers with CAST, which ClickHouse 26.2 accepts on a Variant
toFloat64 refuses a Variant on ClickHouse 26.2 (CI); CAST(x AS
Nullable(Float64)) works on 26.2 and 26.8, Variant or not. Checked both
versions with the full suite and every fuzz property at 20x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(fuzz): rerun ClickHouse's one-argument and() bug without the rewrite, rows still checked
The known-issue matcher for this ClickHouse server bug covered two
shapes; a 20x soak on 26.2 found a third (`(a OR 0 OR a) AND (a OR 0)`).
Rather than chase shapes, a ClickHouse target that gets exactly this
error runs the query again with optimize_extract_common_expressions = 0,
and its rows are compared with the model as usual: these inputs are now
checked, not skipped. The matcher and its helpers are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(fuzz): pin the and() server-bug shape ClickHouse 26.2 hit
Fails with TOO_FEW_ARGUMENTS_FOR_FUNCTION if the rerun without the rewrite
is removed; passes, rows checked, with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
// CAST, not toFloat64: ClickHouse 26.2's toFloat64 refuses a Variant. Float64 orders every number; the Int128 tiebreak keeps integers past 2^53 (one Float64) in exact order.
186
+
unionNumberOrderKeys: (column)=>[`CAST(${column} AS Nullable(Float64))`,`accurateCastOrNull(${column}, 'Int128')`],
176
187
}
177
188
178
189
// The dialect of the enclosing compile, beside the syntax installed for the
Copy file name to clipboardExpand all lines: tests/fuzz/README.md
+10-1Lines changed: 10 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,7 +17,8 @@ and are skipped otherwise. Postgres runs in-process on PGlite.
17
17
18
18
| File | Property | Oracle |
19
19
| --- | --- | --- |
20
-
|`queries.fuzz.test.ts`| random plans return the rows `model.ts` computes | a JS model of SQL, per target |
20
+
|`queries.fuzz.test.ts`| random plans, and `unionAll`s of them (branch and union ORDER BY / LIMIT / OFFSET), return the rows `model.ts` computes | a JS model of SQL, per target |
21
+
|`writes.fuzz.test.ts`| random sequences of inserts (multi-row, defaulted cells, INSERT … SELECT, ON CONFLICT), updates and deletes, with RETURNING, leave a scratch table as the model says; unsupported writes are refused | a JS model of the table's rows |
21
22
|`literals.fuzz.test.ts`| every column type's values survive as a literal and as a param | the database's own JSON parser |
22
23
|`compile.fuzz.test.ts`| plans compile deterministically, with no leaked JS or misaligned placeholders; hostile values are refused with a typed error | invariants |
23
24
|`registry.fuzz.test.ts`| every `Dialect` in `src/` is registered here | — |
@@ -36,6 +37,14 @@ A `FuzzDialect` gives:
36
37
-`strings` and `ilike`: what the generators may produce for it.
37
38
-`literals`: its column types, each with a way to build the reference row
38
39
*without* the builder (usually the database parsing JSON).
40
+
-`writes`: which writes its builder compiles (`insertSelect`, `update`, `delete`,
41
+
`onConflict`, `returning`, `defaultCells` for a multi-row VALUES that leaves a
42
+
defaulted column out of some rows), and a `scratch` table: its query `Table`,
43
+
the DDL that creates and drops it, and `exec` to run that DDL on the first
44
+
target. Set a capability from what the builder does, not what the database
45
+
could: the suite still generates an unsupported write and requires the builder
46
+
to refuse it. A database whose writes are asynchronous (ClickHouse mutations)
47
+
must make them visible to the next read (`mutations_sync=2`).
39
48
-`targets`: the databases it runs on. Each target states its `Semantics`, the
40
49
points where databases legitimately disagree (NULL ordering, integer division,
41
50
what an unmatched LEFT JOIN column holds, LIKE case rules, …). The model reads
create: (name)=>[`CREATE TABLE "${name}" ("Id" int8 PRIMARY KEY, "OrgId" text NOT NULL, "Name" text NOT NULL, "Note" text, "Qty" int8 NOT NULL DEFAULT 5)`],
0 commit comments