Skip to content

test(integration): broaden nodata function parity coverage - #1203

Merged
james-willis merged 6 commits into
mainfrom
jw/spark-parity-nodata-coverage
Sep 1, 2026
Merged

test(integration): broaden nodata function parity coverage#1203
james-willis merged 6 commits into
mainfrom
jw/spark-parity-nodata-coverage

Conversation

@james-willis

Copy link
Copy Markdown
Contributor

Follow-up to #1159: broaden the parity coverage for the two functions the suite already exercises, RS_BandNoDataValue and RS_SetBandNoDataValue. The suite grows from 6 tests to 34 (21 pass, 13 xfail).

Every case was first probed against both engines (SedonaDB at this branch, Sedona Spark 1.9.1 on pyspark 4.0.4), so each xfail reason states the observed behavior of each engine rather than a guess.

New coverage that passes (the engines agree)

  • RS_BandNoDataValue across all eight GDAL-writable band dtypes (adds int8, uint16, int16, uint32, float32)
  • the nodata sentinel planted into the pixels — metadata reads/writes don't mask pixel content
  • setter on band 2 leaves band 1 alone; overwriting an existing nodata; idempotent re-set
  • 2-arg setter form on a single-band raster
  • both engines refuse: nodata the band dtype cannot hold (300.5 on uint8, 0.5 on int32, NaN on int32), out-of-range setter band indexes, and the 4-arg replace form (Sedona's Java layer ships the overload, but the 1.9.1 SQL binding fails to evaluate it)
  • NULL band index or NULL nodata into the setter yields a NULL raster on both

Divergences cataloged as xfails

case SedonaDB Sedona Spark
NaN file nodata, read back (float32/float64) NaN NULL
NaN set as nodata, read back NaN NULL
fractional file nodata on int band (0.5) packs into band dtype → 0.0 GDAL metadata verbatim → 0.5
getter with out-of-range band (0, 3, -1) NULL (deliberate, per rs_band_accessors.rs) raises
getter/setter with NULL band index getter coalesces to band 1 (unwrap_or(1)) NULL
NULL raster typed via CASE loses the raster extension type; no kernel matches NULL
setter with -1.0 on uint8 rejects (not a valid UInt8) accepts, reports -1.0
2-arg setter on multi-band raster rejects (deliberate) defaults to band 1

Per the suite's convention these xfails double as the fix catalog — each flips to xpass when the gap closes on either side. Which side should move is left open per case (several SedonaDB behaviors are documented as deliberate).

This suite is not wired into CI; the counts above are from a local run (21 passed, 13 xfailed in 13s on a warm JVM).

Extend the SedonaDB vs Sedona Spark parity suite for RS_BandNoDataValue
and RS_SetBandNoDataValue: all eight GDAL-writable band dtypes, NaN and
dtype-unrepresentable nodata values, out-of-range band indexes, NULL
argument propagation, the 2-arg and 4-arg setter forms, overwrite and
idempotent re-set, and nodata sentinels planted into the pixels.

Known divergences are cataloged as xfails with the observed behavior of
each engine in the reason.
@github-actions
github-actions Bot requested a review from paleolimbot August 31, 2026 23:46
Both test modules carried an identical copy of the helper and the fixture
grid constants; they now import them from a shared sibling module.

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not mandatory, but I tend to like the style of writing a test that asserts a hard-coded result (like the postgis integration tests) as a sort of secondary guard on accidental meaningless assertions...I think we've gotten the Raster constructor to a point where eng.assert_query_result("sql stuff", reference) could work for these.

I have a few comments trying to avoid too many test helpers scattered in too many places, but your call (this integration harness is better than no integration harness!).

Did you want to run these in CI? / Are they running in CI now?

Comment on lines +64 to +76
def _outcome(engine, sql):
"""`sql`'s result on `engine`, normalized so outcomes compare with `==`:
`("value", x)` with a NaN result replaced by the string "NaN" (bare `==`
fails NaN == NaN even when the engines agree), or `("error",)` when
execution raises — the engines' error types and messages are incomparable,
so error parity is parity on refusal."""
try:
value = _one(engine, sql)
except Exception:
return ("error",)
if isinstance(value, float) and math.isnan(value):
return ("value", "NaN")
return ("value", value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a squirrely way to stick these types of comparisons...there are more straightforward ways to assert that the expected result is NaN or that an error should occur in both cases. If we need helpers for either of those, you have a setup that can handle utilities (and test them if needed). Putting them in a common file also lets future developers find them more easily instead of repeating them.

For example, in SedonaDB's testing this might be eng.assert_query_result("SELECT isnan(foofy)", True) or eng.assert_query_result("SELECT the_raster", expected_raster). You'd have to modify the query result checker to handle raster equality but that will help you write other tests, too. This could be a thin wrapper around numpy's equality assertion.

Comment thread integration/spark-parity/parity.py Outdated
…lain comparisons

Move the write-and-register helper onto DBEngine as
create_random_raster_view (next to create_raster_view, mirroring the
geoparquet view methods) and delete parity.py; write_random_geotiff
gains a plants passthrough so the pixel-planting tests need no custom
writer. Drop the _outcome normalization: divergence tests now compare
engine results directly (a raised error is what trips the xfail), and
the NaN tests ask the question through SQL isnan so the comparison is a
plain boolean.
@james-willis

Copy link
Copy Markdown
Contributor Author

I tend to like the style of writing a test that asserts a hard-coded result

I am ambivalent on this. It would be good coverage but add complexity to the tests. I feel it is somewhat a mixing of concerns.

too many test helpers scattered in too many places

Looking

Did you want to run these in CI? / Are they running in CI now?

Probably will but not in this PR. I wanted to let this bake a little bit before doing so.

Each scalar test now asserts SedonaDB's result against Sedona Spark's
live output via sedona.assert_query_result(sql, _tuples(spark, sql)).
result_to_tuples is the cross-engine form the checker already documents:
Arrow stringifies values identically on both sides, NULL stays None, and
NaN becomes the string 'nan' — so the isnan detour and any bespoke
comparison helpers are gone.
@james-willis

Copy link
Copy Markdown
Contributor Author

moving to draft as I screw around with how to perform assertions

…assertion

One entry point in sedonadb.testing for the cross-engine parity claim:
the first engine is the subject, every further engine an expected side.
Table results compare in result_to_tuples form; a sedona.raster result
dispatches to decode_raster_result + assert_decoded_equal, with all
engines agreeing on a NULL raster accepted. Every non-error parity test
in both suite modules is now compare(sql, sedona, spark).
compare() previously sniffed the subject's collected Arrow schema for
the sedona.raster extension, which silently assumed the subject was
SedonaDB. The dispatch now asks a new DBEngine.result_has_raster(sql)
hook answered from each engine's lazy result schema — SedonaDB via the
Arrow C schema of the unexecuted plan, Sedona Spark via RasterType in
the analyzed DataFrame schema — so either engine can be the subject and
nothing executes just to pick the comparison path.
@james-willis
james-willis marked this pull request as ready for review September 1, 2026 20:16
@james-willis

Copy link
Copy Markdown
Contributor Author

merging this with some small tweaks after @paleolimbot 's review. LMK if you have issues and I will raise a follow up

@james-willis
james-willis merged commit 95da422 into main Sep 1, 2026
5 checks passed

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

I still think the ability to assert that two rasters are equal (and assert an exact result when it's easy to compute) will result in fewer accidental bogus tests (e.g., our recent discovery that some spatial bench results were just the table repeated three times and sorted, because nobody looked at the result). You could add an optional parameter to compare() that lets the reference value be specified (sometimes this is hard, but sometimes it is not).

bands=2,
height=6,
width=7,
gdal_transform=(100.0, 2.0, 0.0, 500.0, 0.0, -3.0),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a bbox instead of a transform? Or can you allow the option of a bbox and use it for plausibly readable tests?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants