Skip to content

Commit e683bf1

Browse files
author
Chong Gao
committed
Add partial clustering SPJ regression test
Signed-off-by: Chong Gao <res_life@163.com>
1 parent f1566c2 commit e683bf1

1 file changed

Lines changed: 60 additions & 2 deletions

File tree

integration_tests/src/main/python/iceberg/iceberg_test.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
import pytest
1616

17-
from asserts import assert_equal_with_local_sort, assert_gpu_and_cpu_are_equal_collect, assert_gpu_and_cpu_row_counts_equal, assert_gpu_fallback_collect, assert_spark_exception
17+
from asserts import assert_cpu_and_gpu_are_equal_collect_with_capture, \
18+
assert_equal_with_local_sort, assert_gpu_and_cpu_are_equal_collect, \
19+
assert_gpu_and_cpu_row_counts_equal, assert_gpu_fallback_collect, assert_spark_exception
1820
from conftest import is_iceberg_remote_catalog
1921
from data_gen import *
2022
from iceberg import get_full_table_name, iceberg_unsupported_mark, _build_tblprops, \
2123
_BASE_TBLPROPS_SQL, create_iceberg_table
2224
from marks import allow_non_gpu, iceberg, ignore_order
23-
from spark_session import is_databricks_runtime, with_cpu_session, \
25+
from spark_session import is_databricks_runtime, is_spark_359, with_cpu_session, \
2426
with_gpu_session
2527

2628
iceberg_map_gens = [MapGen(f(nullable=False), f()) for f in [
@@ -47,6 +49,62 @@
4749

4850
pytestmark = iceberg_unsupported_mark
4951

52+
53+
@iceberg
54+
@ignore_order(local=True)
55+
@pytest.mark.skipif(not is_spark_359(),
56+
reason="Partial-clustering marker was added in Apache Spark 3.5.9")
57+
def test_iceberg_spj_partial_clustering_distinct(spark_tmp_table_factory):
58+
left_table = get_full_table_name(spark_tmp_table_factory)
59+
right_table = get_full_table_name(spark_tmp_table_factory)
60+
table_props = _build_tblprops({
61+
# Keep separate INSERTs as separate scan splits so that id=1 is partially clustered.
62+
"read.split.target-size": "1",
63+
"read.split.open-file-cost": "1",
64+
})
65+
table_props_sql = ", ".join(f"'{k}' = '{v}'" for k, v in table_props.items())
66+
67+
def setup_iceberg_tables(spark):
68+
spark.sql(
69+
f"CREATE TABLE {left_table} (id INT, price DOUBLE) USING ICEBERG "
70+
f"PARTITIONED BY (id) TBLPROPERTIES ({table_props_sql})")
71+
spark.sql(
72+
f"CREATE TABLE {right_table} (id INT, value STRING) USING ICEBERG "
73+
f"PARTITIONED BY (id) TBLPROPERTIES ({table_props_sql})")
74+
75+
# The two id=1 rows land in different files. Partial clustering assigns them to
76+
# different join tasks and replicates the matching row from the other side.
77+
spark.sql(f"INSERT INTO {left_table} VALUES (1, 40.0), (2, 10.0), (3, 15.5)")
78+
spark.sql(f"INSERT INTO {left_table} VALUES (1, 41.0)")
79+
spark.sql(f"INSERT INTO {right_table} VALUES (1, 'a'), (2, 'b'), (3, 'c')")
80+
81+
with_cpu_session(setup_iceberg_tables)
82+
83+
conf = {
84+
"spark.sql.adaptive.enabled": "false",
85+
"spark.sql.autoBroadcastJoinThreshold": "-1",
86+
"spark.sql.sources.v2.bucketing.enabled": "true",
87+
"spark.sql.sources.v2.bucketing.pushPartValues.enabled": "true",
88+
"spark.sql.sources.v2.bucketing.partiallyClusteredDistribution.enabled": "true",
89+
}
90+
91+
def distinct_after_spj(spark):
92+
return spark.sql(
93+
f"""
94+
SELECT DISTINCT l.id
95+
FROM {left_table} l
96+
JOIN {right_table} r ON l.id = r.id
97+
""")
98+
99+
# The SPJ itself is shuffle-free, so this exchange is the required post-join distinct
100+
# shuffle. It also verifies that GpuBatchScanExec preserved Spark's partial-clustering marker.
101+
assert_cpu_and_gpu_are_equal_collect_with_capture(
102+
distinct_after_spj,
103+
exist_classes="GpuBatchScanExec,GpuShuffleExchangeExec",
104+
conf=conf,
105+
require_non_empty=True)
106+
107+
50108
@allow_non_gpu("BatchScanExec")
51109
@iceberg
52110
@ignore_order(local=True) # Iceberg plans with a thread pool and is not deterministic in file ordering

0 commit comments

Comments
 (0)