|
14 | 14 |
|
15 | 15 | import pytest |
16 | 16 |
|
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 |
18 | 20 | from conftest import is_iceberg_remote_catalog |
19 | 21 | from data_gen import * |
20 | 22 | from iceberg import get_full_table_name, iceberg_unsupported_mark, _build_tblprops, \ |
21 | 23 | _BASE_TBLPROPS_SQL, create_iceberg_table |
22 | 24 | 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, \ |
24 | 26 | with_gpu_session |
25 | 27 |
|
26 | 28 | iceberg_map_gens = [MapGen(f(nullable=False), f()) for f in [ |
|
47 | 49 |
|
48 | 50 | pytestmark = iceberg_unsupported_mark |
49 | 51 |
|
| 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 | + |
50 | 108 | @allow_non_gpu("BatchScanExec") |
51 | 109 | @iceberg |
52 | 110 | @ignore_order(local=True) # Iceberg plans with a thread pool and is not deterministic in file ordering |
|
0 commit comments