Skip to content

Spark 4.1: Test geometry and geography DML and fall back from vectorized reads - #17149

Merged
szehon-ho merged 5 commits into
apache:mainfrom
huan233usc:geo-spark-dml
Jul 14, 2026
Merged

Spark 4.1: Test geometry and geography DML and fall back from vectorized reads#17149
szehon-ho merged 5 commits into
apache:mainfrom
huan233usc:geo-spark-dml

Conversation

@huan233usc

Copy link
Copy Markdown
Contributor

Adds end-to-end Spark SQL DML tests for geometry and geography columns, and
fixes a vectorized-read bug those tests surfaced. This completes the Phase-1
geo end-to-end item (row-level DML with deletion vectors / nested geo / nulls)
and builds on #17073 (Spark geo value path) and #16982 (Parquet geo WKB values).

Tests

TestSparkGeospatial previously covered only INSERTSELECT read-back. This
adds row-level DML on a geo table under merge-on-read + format version 3
(deletion vectors):

  • testDeleteGeospatialMergeOnReadDELETE leaves a survivor in a single data
    file, and the snapshot summary confirms a deletion vector was written
    (added-dvs = 1); the surviving null-geo row round-trips.
  • testUpdateGeospatialMergeOnReadUPDATE swaps a row's geometry; others
    unchanged.
  • testMergeGeospatialMergeOnReadMERGE updates a matched row and inserts a
    not-matched row, both carrying geo values.
  • testDeleteNestedGeometryMergeOnRead — geometry nested in a STRUCT; delete by
    a nested non-geo field, surviving nested geometry (and a null nested geometry)
    still round-trip.

Geo values are asserted via hex(st_asbinary(...)) round-trips and DML filters on
non-geo columns, since Spark 4.1 has no spatial predicates.

Vectorization fallback (production fix)

geometry/geography are primitive types, so they passed
SparkBatch.supportsParquetBatchReads(NestedField) and were routed to the
vectorized Parquet reader — but there is no Arrow geo vector yet, so scanning a geo
column with vectorization enabled threw
UnsupportedOperationException: Unsupported primitive type: geometry. This excludes
the two geo types from vectorized reads so they fall back to the row-based reader.

With this fix, testGeospatialWkbReadBack now runs (no longer skipped) with
vectorization both off and on, proving the fallback. A dedicated Arrow geo
vector remains future work.

…zed reads

Add end-to-end DELETE/UPDATE/MERGE tests for geometry and geography columns
under merge-on-read at format version 3, covering deletion vectors, nested geo
in a struct, and null values.

Geometry and geography are primitive types, so they slipped through the
vectorized Parquet read gate in SparkBatch even though there is no Arrow geo
vector yet, which threw when scanning geo columns with vectorization enabled.
Exclude the two geo types so they fall back to the non-vectorized reader.
@github-actions github-actions Bot added the spark label Jul 9, 2026
Xin Huang added 3 commits July 9, 2026 14:47
Select the delete snapshot by operation instead of the latest committed_at,
whose millisecond granularity could tie with the insert snapshot, and give the
test its own table so there is exactly one delete snapshot to inspect.
Copy-on-write is the default row-level mode, and its rewrite path reads geo
values back out of the surviving data files, so add DELETE and UPDATE tests
that exercise it (the merge-on-read tests only cover deletion vectors).
Fold the duplicated merge-on-read and copy-on-write DELETE and UPDATE variants
into single tests parametrized over the write mode, and run MERGE and the nested
delete over both modes too. The deletion-vector assertion applies only to
merge-on-read.

@szehon-ho szehon-ho 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.

Reviewed the geo DML tests and the vectorization fallback fix. Overall this looks good: the production change is minimal and correct (geometry/geography are PrimitiveTypes so they wrongly passed the batch-read gate and hit ArrowSchemaUtil's Unsupported primitive type throw; excluding them at the gate is the right layer, mirroring how VARIANT stays non-vectorized). The tests are thorough — DELETE/UPDATE/MERGE parametrized over copy-on-write and merge-on-read, COALESCE(1) to force a DV in MOR, null round-trips, and testGeospatialWkbReadBack un-skipped for vectorization on. CI is green. A few non-blocking nits and one question below.

(Review drafted with AI assistance and reviewed before posting.)

Type type = field.type();
// Geometry and geography are primitive types but have no Arrow vector yet, so they must be
// read through the non-vectorized reader.
if (type.typeId() == Type.TypeID.GEOMETRY || type.typeId() == Type.TypeID.GEOGRAPHY) {

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.

Correct fix and the right layer. Nit: the method-level comment above useParquetBatchReads() still reads // - only primitives or metadata columns are projected, which is now slightly stale since geometry/geography are primitives that are intentionally excluded here. A one-line tweak there would keep the two in sync.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3698f8c — updated the comment above useParquetBatchReads() to note geometry and geography are primitives that are intentionally excluded.


@ParameterizedTest
@ValueSource(strings = {"copy-on-write", "merge-on-read"})
public void testDeleteGeospatial(String mode) {

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.

Nit: these DML tests rely on the default read.parquet.vectorization.enabled=true to exercise the fallback path. That's fine today, but parametrizing vectorization on/off (like testGeospatialWkbReadBack does) would make the intent explicit and guard against a future default change. Optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — done in 3698f8c. The DELETE/UPDATE/MERGE/nested tests now parametrize over (mode, vectorized) via @CsvSource, setting read.parquet.vectorization.enabled per table, so the fallback is exercised explicitly with vectorization both on and off rather than relying on the default.

if (mode.equals("merge-on-read")) {
assertThat(
scalarSql(
"SELECT summary['added-dvs'] FROM %s.snapshots WHERE operation = 'delete'",

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.

Nit: the DV assertion queries the snapshots metadata view via SQL. That works, and the operation = 'delete' filter nicely avoids the committed_at tie. Elsewhere in Spark 4.1 tests (e.g. TestDeleteFrom.truncateWithDVs) the convention is to load the table via the catalog and assert on snapshot().summary() with SnapshotSummary.ADDED_DVS_PROP, which is a bit more direct and avoids depending on the snapshots view. Either is acceptable.

@huan233usc huan233usc Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, that's a fair point. I looked at how other tests in this package handle it: TestSelect (same spark/.../sql/ package) also reads the .snapshots metadata view via SQL, whereas the currentSnapshot().summary() + SnapshotSummary.ADDED_DVS_PROP convention you mention is used by the CatalogTestBase tests (e.g. TestDeleteFrom), which have validationCatalog/tableIdent available. This test extends TestBase and creates its table via SQL in a hadoop catalog (Hive doesn't support geo yet), so there's no validationCatalog to load from, and the SQL form keeps it consistent with the sibling tests here. I've kept the SQL assertion with the operation = 'delete' filter for determinism — but happy to switch to the catalog form if you'd prefer it.


@ParameterizedTest
@ValueSource(strings = {"copy-on-write", "merge-on-read"})
public void testDeleteNestedGeometry(String mode) {

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.

Question: this nested case covers GEOMETRY inside a struct but not nested GEOGRAPHY. Intentional scope for Phase 1, or worth a symmetric geography case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — added in 3698f8c. The nested test (now testDeleteNestedGeospatial) uses STRUCT<c1, geom, geog> and asserts that after the delete the surviving row's non-null nested geometry and null nested geography both round-trip.

Parametrize the DELETE/UPDATE/MERGE/nested tests over vectorization on and off (in
addition to the row-level mode) so the fallback path is exercised explicitly rather
than relying on the default. Extend the nested-delete test to a struct that also
holds a geography field, asserting a non-null nested geometry and a null nested
geography both round-trip. Sync the useParquetBatchReads comment to note geometry
and geography are excluded.
@huan233usc

Copy link
Copy Markdown
Contributor Author

Thanks for the review @szehon-ho! Addressed in 3698f8c:

  • Synced the useParquetBatchReads comment to note geometry/geography are excluded.
  • Parametrized DELETE/UPDATE/MERGE/nested over (mode, vectorized) so the fallback runs with vectorization both on and off.
  • Extended the nested-delete test to a struct with both a geometry and a geography field, asserting a non-null nested geometry and a null nested geography round-trip.

On the DV assertion: kept the SQL form since this test extends TestBase (no validationCatalog), with the operation = 'delete' filter for determinism — replied inline, happy to revisit.

@huan233usc
huan233usc requested a review from szehon-ho July 12, 2026 19:41
@szehon-ho
szehon-ho merged commit 8b26043 into apache:main Jul 14, 2026
31 checks passed
@szehon-ho

Copy link
Copy Markdown
Member

Merged, thanks @huan233usc !

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants