test: fix vacuous signed-zero coverage in SQL file tests - #5393
Conversation
Replace bare -0.0 literals and CAST(-0.0 AS DOUBLE/FLOAT) with
double('-0.0') / float('-0.0') so the sign bit is actually preserved
when values are inserted into parquet columns. Also document the
pitfall in the SQL file test contributor guide.
Fixes apache#5271
revert accidental string_replace.sql change
712a3dc to
1318003
Compare
sunchao
left a comment
There was a problem hiding this comment.
Summary
This PR strengthens the signed-zero SQL coverage for #5271 by making the fixtures create real negative zero. I reviewed the full diff at 1318003d78af1dbc0a91a2a51591b670b2c6a4c2. One P2 remains: the unseeded shuffle fixture now has a nondeterministic exact-result comparison.
Prior state and problem
Spark parses a bare -0.0 as a decimal, which cannot retain the zero sign. Casting that decimal to float or double still produces positive zero, so several existing edge-case rows did not exercise the behavior their spelling suggested.
Design approach
The fixtures now construct floating-point negative zero through string casts. The PR also separates known array_min and descending sort_array incompatibilities into ignored queries, leaving the other cases active, and documents the literal convention for future tests.
Correctness / compatibility analysis
The string-cast approach matches Spark's floating-point semantics, and the SQL runner already disables constant folding and distinguishes signed-zero bits in ordinary result comparisons. The newly exposed array-minimum and descending-sort differences are pre-existing native compatibility differences. However, sort_array(shuffle(arr)) is not a canonical result for a nullable array containing both zero signs, so the changed unseeded fixture can fail even when both executions are correct.
The review covered all 32 changed files with five independent scopes. A Spark 4.1.1 probe parsed all 877 records in the 31 changed SQL files and reproduced both signed-zero orders for the shuffle query. A separate Spark-only run of the 11 scalar fixtures passed all 13 declared configuration runs, including 118 successful queries and 38 expected errors. git diff --check passed. I did not run the full exact-head native Comet suite, and GitHub reported no checks or commit statuses for this head.
Key design decisions
Keeping the existing float and double schemas preserves the intended expression paths. Isolating known divergences also avoids disabling the surrounding NaN, infinity, null, and ordinary-value coverage. The unseeded shuffle check needs one further distinction: preserving an element multiset does not guarantee a unique ordering when Spark considers the two zero signs equal.
Implementation sketch
The patch replaces decimal-based negative-zero literals across the aggregate, array, cast, conditional, and math fixtures. It adds dedicated signed-zero tables and ignored queries for the known array ordering differences, while retaining active ordinary and strict-floating-point tests. No production execution code changes.
Behavioral changes worth calling out
The affected fixtures now pass genuine negative-zero bits through their expressions, so they can expose differences that the previous data concealed. This also changes the assumptions of the existing shuffle assertion: sorting its independently shuffled nullable arrays no longer removes every observable permutation.
Suggested improvements
Please make the unseeded shuffle comparison deterministic while keeping genuine signed-zero coverage. A canonical sign-aware multiset representation would preserve the stronger check, or the generic unseeded projection can normalize zero signs while the seeded fixture retains the exact signed-zero permutation checks. The inline P2 identifies the affected row and the reproduction.
| (array(1.1, 2.2, 3.3, 4.4, 5.5)), | ||
| (NULL), | ||
| (array(CAST('NaN' AS DOUBLE), CAST('Infinity' AS DOUBLE), CAST('-Infinity' AS DOUBLE), 0.0, -0.0)) | ||
| (array(CAST('NaN' AS DOUBLE), CAST('Infinity' AS DOUBLE), CAST('-Infinity' AS DOUBLE), 0.0, double('-0.0'))) |
There was a problem hiding this comment.
[P2] Keep the unseeded permutation check deterministic
Could this mixed signed-zero case use a comparison that does not depend on the order of equal zeros? The query below compares sort_array(shuffle(arr)) exactly, but Spark's nullable-array comparator treats 0.0 and -0.0 as equal. Its stable sort therefore preserves their randomly shuffled order. The Spark and Comet runs resolve independent seeds, and the result comparator distinguishes the zero bits. With the new row, repeated Spark 4.1.1 executions produce both orders, while the baseline's two positive zeros were stable. This introduces a flaky SQL test. Sorting the string representations, for example sort_array(transform(shuffle(arr), x -> cast(x AS string))), preserved both zero signs and produced one result across seeds 0 through 11 in a Spark-only probe. Could we use that kind of canonical multiset comparison here?
There was a problem hiding this comment.
Thanks, that was a real flake. Spark's stable sort keeps the shuffled order of +0.0 and -0.0, while the SQL test comparator distinguishes the bits, so sort_array(shuffle(arr)) is not unique once the fixture actually contains both signs.
I switched the unseeded double projection to sort_array(transform(shuffle(arr), x -> cast(x AS string))) so the comparison is a canonical multiset that still preserves both zero signs. The seeded fixture is unchanged and still checks the exact permutation.
I also noted the sort_array pitfall next to the signed-zero guidance in sql-file-tests.md.
Spark treats +0.0 and -0.0 as equal, so sort_array(shuffle(arr)) is not unique across seeds. Sort the string forms instead so both signs are preserved and the result is stable.
sunchao
left a comment
There was a problem hiding this comment.
One remaining signed-zero coverage gap in array_repeat.
| (NULL, NULL, NULL, NULL, CAST(0.0 AS FLOAT), double('-0.0'), NULL, NULL, '日本', X'00', NULL, NULL, NULL, 0), | ||
| (CAST(1 AS TINYINT), CAST(1 AS SMALLINT), 1, 1, float('-0.0'), CAST(0.0 AS DOUBLE), CAST(1 AS DECIMAL(38, 0)), true, NULL, NULL, DATE '2000-01-01', TIMESTAMP '2000-01-01 00:00:00', array(7), -1), |
There was a problem hiding this comment.
[P2] Exercise signed zero with a positive repeat count
Could we also run array_repeat(float_v, 2) and array_repeat(double_v, 2) against this table? Both corrected negative-zero cells currently have nonpositive counts, so their results are empty arrays. An implementation that loses the sign would still pass these tests, leaving this part of #5271 unverified. Please retain the existing zero- and negative-count cases.
Which issue does this PR close?
Closes #5271.
Rationale for this change
Bare
-0.0literals in SQL file tests are parsed by Spark asdecimal(1,1), which has no signed zero. Coercion todoubleorfloatyields+0.0, so rows that appear to test signed-zero behaviour are actually inserting+0.0on both sides — the coverage is vacuous.CAST(-0.0 AS DOUBLE)andCAST(-0.0 AS FLOAT)have the same problem because the cast source is still a decimal literal.What changes are included in this PR?
-0.0andCAST(-0.0 AS DOUBLE/FLOAT)literals across 32 SQL test files withdouble('-0.0')/float('-0.0'), which parse through the string path and preserve the sign bit.query ignore(...)annotations for two known Spark/Comet divergences that became visible once the literals were fixed:array_min: Spark returns+0.0when+0.0and-0.0are both present; Comet returns-0.0.sort_arraydescending: Spark keeps-0.0before+0.0; Comet reverses them.docs/source/contributor-guide/sql-file-tests.mdso future contributors know to usedouble('-0.0')/float('-0.0')when writing float edge-case fixtures.How are these changes tested?
All modified SQL test files were run against both Spark 4.1 (default profile, JDK 17) and Spark 3.4 (
-Pspark-3.4) and passed.Spotless format check also passed on both Spark 4.1 (default profile) and Spark 3.4 (
-Pspark-3.4).