huan233usc commented on code in PR #17149:
URL: https://github.com/apache/iceberg/pull/17149#discussion_r3566679504
##########
spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkGeospatial.java:
##########
@@ -92,6 +94,159 @@ public void testGeospatialWkbReadBack(boolean vectorized) {
TABLE));
}
+ @ParameterizedTest
+ @ValueSource(strings = {"copy-on-write", "merge-on-read"})
+ public void testDeleteGeospatial(String mode) {
Review Comment:
Good call — done in 3698f8c73. 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.
##########
spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkGeospatial.java:
##########
@@ -92,6 +94,159 @@ public void testGeospatialWkbReadBack(boolean vectorized) {
TABLE));
}
+ @ParameterizedTest
+ @ValueSource(strings = {"copy-on-write", "merge-on-read"})
+ public void testDeleteGeospatial(String mode) {
+ // Exercise both row-level modes: copy-on-write (the default) rewrites the
surviving rows into a
+ // new data file, while merge-on-read writes a deletion vector. Both must
read the geo values
+ // back out correctly. Write both rows into one data file (COALESCE(1)) so
the merge-on-read
+ // delete leaves a survivor in that file, forcing a deletion vector rather
than a whole-file
+ // removal. Filter on id since Spark has no spatial predicate.
+ String deleteTable = CATALOG + ".default.geo_delete";
+ sql("DROP TABLE IF EXISTS %s", deleteTable);
+ sql(
+ "CREATE TABLE %s (id BIGINT, geom GEOMETRY(4326), geog
GEOGRAPHY(4326)) USING iceberg "
+ + "TBLPROPERTIES ('format-version'='3', 'write.delete.mode'='%s')",
+ deleteTable, mode);
+ sql(
+ "INSERT INTO %s SELECT /*+ COALESCE(1) */ id, "
+ + "CASE WHEN geom_wkb IS NULL THEN NULL "
+ + "ELSE st_setsrid(st_geomfromwkb(geom_wkb), 4326) END, "
+ + "CASE WHEN geog_wkb IS NULL THEN NULL "
+ + "ELSE st_setsrid(st_geogfromwkb(geog_wkb), 4326) END "
+ + "FROM VALUES (1, X'%s', X'%s'), (2, CAST(NULL AS BINARY),
CAST(NULL AS BINARY)) "
+ + "AS v(id, geom_wkb, geog_wkb)",
+ deleteTable, GEOM_WKB, GEOG_WKB);
+
+ sql("DELETE FROM %s WHERE id = 1", deleteTable);
+
+ assertEquals(
+ "Only the null-geo row should remain after the delete",
+ ImmutableList.of(row(2L, null, null)),
+ sql(
+ "SELECT id, hex(st_asbinary(geom)), hex(st_asbinary(geog)) FROM %s
ORDER BY id",
+ deleteTable));
+
+ // A merge-on-read delete of a row from a multi-row file writes a deletion
vector (format v3),
+ // recorded on the 'delete' snapshot; copy-on-write instead rewrites the
file under an
+ // 'overwrite' snapshot and adds no deletion vector. Select the
merge-on-read delete snapshot by
+ // operation so the assertion does not depend on committed_at ordering,
whose millisecond
+ // granularity could tie with the insert snapshot.
+ if (mode.equals("merge-on-read")) {
+ assertThat(
+ scalarSql(
+ "SELECT summary['added-dvs'] FROM %s.snapshots WHERE
operation = 'delete'",
Review Comment:
I'd like to keep the SQL form here: this test extends `TestBase` (not
`TestBaseWithCatalog`), so `validationCatalog`/`tableIdent` aren't available,
and loading the table through the catalog by name would add setup that the
pure-SQL assertion avoids. The `operation = 'delete'` filter keeps it
deterministic. Happy to switch if you'd prefer the class move to
`TestBaseWithCatalog`.
##########
spark/v4.1/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkGeospatial.java:
##########
@@ -92,6 +94,159 @@ public void testGeospatialWkbReadBack(boolean vectorized) {
TABLE));
}
+ @ParameterizedTest
+ @ValueSource(strings = {"copy-on-write", "merge-on-read"})
+ public void testDeleteGeospatial(String mode) {
+ // Exercise both row-level modes: copy-on-write (the default) rewrites the
surviving rows into a
+ // new data file, while merge-on-read writes a deletion vector. Both must
read the geo values
+ // back out correctly. Write both rows into one data file (COALESCE(1)) so
the merge-on-read
+ // delete leaves a survivor in that file, forcing a deletion vector rather
than a whole-file
+ // removal. Filter on id since Spark has no spatial predicate.
+ String deleteTable = CATALOG + ".default.geo_delete";
+ sql("DROP TABLE IF EXISTS %s", deleteTable);
+ sql(
+ "CREATE TABLE %s (id BIGINT, geom GEOMETRY(4326), geog
GEOGRAPHY(4326)) USING iceberg "
+ + "TBLPROPERTIES ('format-version'='3', 'write.delete.mode'='%s')",
+ deleteTable, mode);
+ sql(
+ "INSERT INTO %s SELECT /*+ COALESCE(1) */ id, "
+ + "CASE WHEN geom_wkb IS NULL THEN NULL "
+ + "ELSE st_setsrid(st_geomfromwkb(geom_wkb), 4326) END, "
+ + "CASE WHEN geog_wkb IS NULL THEN NULL "
+ + "ELSE st_setsrid(st_geogfromwkb(geog_wkb), 4326) END "
+ + "FROM VALUES (1, X'%s', X'%s'), (2, CAST(NULL AS BINARY),
CAST(NULL AS BINARY)) "
+ + "AS v(id, geom_wkb, geog_wkb)",
+ deleteTable, GEOM_WKB, GEOG_WKB);
+
+ sql("DELETE FROM %s WHERE id = 1", deleteTable);
+
+ assertEquals(
+ "Only the null-geo row should remain after the delete",
+ ImmutableList.of(row(2L, null, null)),
+ sql(
+ "SELECT id, hex(st_asbinary(geom)), hex(st_asbinary(geog)) FROM %s
ORDER BY id",
+ deleteTable));
+
+ // A merge-on-read delete of a row from a multi-row file writes a deletion
vector (format v3),
+ // recorded on the 'delete' snapshot; copy-on-write instead rewrites the
file under an
+ // 'overwrite' snapshot and adds no deletion vector. Select the
merge-on-read delete snapshot by
+ // operation so the assertion does not depend on committed_at ordering,
whose millisecond
+ // granularity could tie with the insert snapshot.
+ if (mode.equals("merge-on-read")) {
+ assertThat(
+ scalarSql(
+ "SELECT summary['added-dvs'] FROM %s.snapshots WHERE
operation = 'delete'",
+ deleteTable))
+ .as("Merge-on-read delete should add a deletion vector")
+ .isEqualTo("1");
+ }
+
+ sql("DROP TABLE IF EXISTS %s", deleteTable);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"copy-on-write", "merge-on-read"})
+ public void testUpdateGeospatial(String mode) {
+ // Update the first row's geometry; the untouched row must round-trip
unchanged. Copy-on-write
+ // rewrites the untouched row, merge-on-read writes a deletion vector plus
the new row.
+ String updateTable = CATALOG + ".default.geo_update";
+ sql("DROP TABLE IF EXISTS %s", updateTable);
+ sql(
+ "CREATE TABLE %s (id BIGINT, geom GEOMETRY(4326), geog
GEOGRAPHY(4326)) USING iceberg "
+ + "TBLPROPERTIES ('format-version'='3', 'write.update.mode'='%s')",
+ updateTable, mode);
+ sql(
+ "INSERT INTO %s VALUES "
+ + "(1, st_setsrid(st_geomfromwkb(X'%s'), 4326),
st_setsrid(st_geogfromwkb(X'%s'), 4326)), "
+ + "(2, st_setsrid(st_geomfromwkb(X'%s'), 4326), NULL)",
+ updateTable, GEOM_WKB, GEOG_WKB, GEOM_WKB);
+
+ sql(
+ "UPDATE %s SET geom = st_setsrid(st_geomfromwkb(X'%s'), 4326) WHERE id
= 1",
+ updateTable, OTHER_WKB);
+
+ assertEquals(
+ "The updated row changes and the untouched row round-trips unchanged",
+ ImmutableList.of(
+ row(1L, OTHER_WKB.toUpperCase(Locale.ROOT),
GEOG_WKB.toUpperCase(Locale.ROOT)),
+ row(2L, GEOM_WKB.toUpperCase(Locale.ROOT), null)),
+ sql(
+ "SELECT id, hex(st_asbinary(geom)), hex(st_asbinary(geog)) FROM %s
ORDER BY id",
+ updateTable));
+
+ sql("DROP TABLE IF EXISTS %s", updateTable);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"copy-on-write", "merge-on-read"})
+ public void testMergeGeospatial(String mode) {
+ // Source updates id=1 (matched) and inserts id=3 (not matched); geo
values flow through both.
+ String mergeTable = CATALOG + ".default.geo_merge";
+ sql("DROP TABLE IF EXISTS %s", mergeTable);
+ sql(
+ "CREATE TABLE %s (id BIGINT, geom GEOMETRY(4326), geog
GEOGRAPHY(4326)) USING iceberg "
+ + "TBLPROPERTIES ('format-version'='3', 'write.merge.mode'='%s')",
+ mergeTable, mode);
+ sql(
+ "INSERT INTO %s VALUES "
+ + "(1, st_setsrid(st_geomfromwkb(X'%s'), 4326),
st_setsrid(st_geogfromwkb(X'%s'), 4326)), "
+ + "(2, NULL, NULL)",
+ mergeTable, GEOM_WKB, GEOG_WKB);
+
+ sql(
+ "MERGE INTO %s AS t USING ("
+ + "SELECT 1 AS id, st_setsrid(st_geomfromwkb(X'%s'), 4326) AS
geom, "
+ + "st_setsrid(st_geogfromwkb(X'%s'), 4326) AS geog "
+ + "UNION ALL "
+ + "SELECT 3 AS id, st_setsrid(st_geomfromwkb(X'%s'), 4326) AS
geom, "
+ + "st_setsrid(st_geogfromwkb(X'%s'), 4326) AS geog) AS s "
+ + "ON t.id = s.id "
+ + "WHEN MATCHED THEN UPDATE SET t.geom = s.geom, t.geog = s.geog "
+ + "WHEN NOT MATCHED THEN INSERT *",
+ mergeTable, OTHER_WKB, GEOG_WKB, GEOM_WKB, GEOG_WKB);
+
+ assertEquals(
+ "Matched row is updated and unmatched row is inserted",
+ ImmutableList.of(
+ row(1L, OTHER_WKB.toUpperCase(Locale.ROOT),
GEOG_WKB.toUpperCase(Locale.ROOT)),
+ row(2L, null, null),
+ row(3L, GEOM_WKB.toUpperCase(Locale.ROOT),
GEOG_WKB.toUpperCase(Locale.ROOT))),
+ sql(
+ "SELECT id, hex(st_asbinary(geom)), hex(st_asbinary(geog)) FROM %s
ORDER BY id",
+ mergeTable));
+
+ sql("DROP TABLE IF EXISTS %s", mergeTable);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"copy-on-write", "merge-on-read"})
+ public void testDeleteNestedGeometry(String mode) {
Review Comment:
Good catch — added in 3698f8c73. 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.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]