|
22 | 22 | from parquet_write_test import parquet_datetime_gen_simple, parquet_nested_datetime_gen, parquet_ts_write_options |
23 | 23 | from marks import * |
24 | 24 | import pyarrow as pa |
| 25 | +import pyarrow.parquet as pq |
25 | 26 | from parquet_test_utils import parquet_row_group_midpoints |
26 | 27 | from pyspark.sql.types import * |
27 | 28 | from pyspark.sql.functions import * |
@@ -1991,3 +1992,185 @@ def setup_table(spark): |
1991 | 1992 | with_cpu_session(lambda spark: setup_table(spark)) |
1992 | 1993 | assert_gpu_and_cpu_are_equal_collect(lambda spark: spark.read.parquet(data_path).select("p"), |
1993 | 1994 | conf={"spark.rapids.sql.columnSizeBytes": "100"}) |
| 1995 | + |
| 1996 | + |
| 1997 | +def _write_parquet_unknown_null_table( |
| 1998 | + data_path, with_list=False, with_map=False, field_id=None): |
| 1999 | + """Write INT32 physical + UNKNOWN/Null logical annotation (Spark void_in_parquet shape).""" |
| 2000 | + if with_list: |
| 2001 | + table = pa.table({ |
| 2002 | + 'list_void': pa.array([[None, None], [None], None], type=pa.list_(pa.null())), |
| 2003 | + }) |
| 2004 | + elif with_map: |
| 2005 | + table = pa.table({ |
| 2006 | + 'map_void': pa.array( |
| 2007 | + [{1: None, 2: None}, {3: None}, None], |
| 2008 | + type=pa.map_(pa.int32(), pa.null())), |
| 2009 | + }) |
| 2010 | + elif field_id is not None: |
| 2011 | + arrow_schema = pa.schema([ |
| 2012 | + pa.field('void_col', pa.null(), metadata={b'PARQUET:field_id': str(field_id).encode()}), |
| 2013 | + ]) |
| 2014 | + table = pa.Table.from_arrays( |
| 2015 | + [pa.array([None, None, None], type=pa.null())], schema=arrow_schema) |
| 2016 | + else: |
| 2017 | + table = pa.table({ |
| 2018 | + 'id': pa.array([1, 2, 3], type=pa.int32()), |
| 2019 | + 'void_col': pa.array([None, None, None], type=pa.null()), |
| 2020 | + }) |
| 2021 | + pq.write_table(table, data_path) |
| 2022 | + |
| 2023 | + |
| 2024 | +# SPARK-56045 / SPARK-54220: Parquet UNKNOWN logical type annotation. PyArrow null columns are |
| 2025 | +# written as INT32 physical + UNKNOWN/Null logical annotation. |
| 2026 | + |
| 2027 | +@pytest.mark.skipif(is_spark_411_or_later(), |
| 2028 | + reason='pre-SPARK-54220 physical-type behavior') |
| 2029 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2030 | +def test_parquet_unknown_type_annotation_pre_411_physical(spark_tmp_path, reader_confs): |
| 2031 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN_PRE_411' |
| 2032 | + _write_parquet_unknown_null_table(data_path) |
| 2033 | + |
| 2034 | + def read_and_check_schema(spark): |
| 2035 | + df = spark.read.parquet(data_path) |
| 2036 | + assert df.schema['void_col'].dataType == IntegerType(), \ |
| 2037 | + f"expected void_col=IntegerType, got {df.schema['void_col'].dataType}" |
| 2038 | + return df |
| 2039 | + |
| 2040 | + assert_gpu_and_cpu_are_equal_collect(read_and_check_schema, conf=reader_confs) |
| 2041 | + |
| 2042 | + |
| 2043 | +@pytest.mark.skipif(not is_spark_412_or_later(), |
| 2044 | + reason='SPARK-56045 requires Spark 4.1.2+') |
| 2045 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2046 | +def test_parquet_unknown_type_annotation_default_physical(spark_tmp_path, reader_confs): |
| 2047 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN' |
| 2048 | + _write_parquet_unknown_null_table(data_path) |
| 2049 | + |
| 2050 | + conf = copy_and_update(reader_confs, { |
| 2051 | + 'spark.sql.parquet.reader.respectUnknownTypeAnnotation.enabled': 'false', |
| 2052 | + }) |
| 2053 | + |
| 2054 | + def read_and_check_schema(spark): |
| 2055 | + df = spark.read.parquet(data_path) |
| 2056 | + assert df.schema['void_col'].dataType == IntegerType(), \ |
| 2057 | + f"expected void_col=IntegerType, got {df.schema['void_col'].dataType}" |
| 2058 | + return df |
| 2059 | + |
| 2060 | + assert_gpu_and_cpu_are_equal_collect(read_and_check_schema, conf=conf) |
| 2061 | + |
| 2062 | + |
| 2063 | +@pytest.mark.skipif(not is_spark_411_or_later(), |
| 2064 | + reason='SPARK-54220 requires Spark 4.1.1+') |
| 2065 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2066 | +@allow_non_gpu('FileSourceScanExec', 'ColumnarToRowExec') |
| 2067 | +def test_parquet_unknown_type_annotation_respect_nulltype(spark_tmp_path, reader_confs): |
| 2068 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN' |
| 2069 | + _write_parquet_unknown_null_table(data_path) |
| 2070 | + |
| 2071 | + # Spark 4.1.1 always maps UNKNOWN to NullType; 4.1.2+ needs the conf enabled. |
| 2072 | + conf = reader_confs |
| 2073 | + if is_spark_412_or_later(): |
| 2074 | + conf = copy_and_update(reader_confs, { |
| 2075 | + 'spark.sql.parquet.reader.respectUnknownTypeAnnotation.enabled': 'true', |
| 2076 | + }) |
| 2077 | + |
| 2078 | + def read_and_check_schema(spark): |
| 2079 | + df = spark.read.parquet(data_path) |
| 2080 | + assert df.schema['void_col'].dataType == NullType(), \ |
| 2081 | + f"expected void_col=NullType, got {df.schema['void_col'].dataType}" |
| 2082 | + return df |
| 2083 | + |
| 2084 | + # GPU Parquet scan does not support NullType yet; expect CPU fallback. |
| 2085 | + assert_gpu_fallback_collect(read_and_check_schema, 'FileSourceScanExec', conf=conf) |
| 2086 | + |
| 2087 | + |
| 2088 | +@pytest.mark.skipif(not is_spark_412_or_later(), |
| 2089 | + reason='SPARK-56045 requires Spark 4.1.2+') |
| 2090 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2091 | +def test_parquet_unknown_type_annotation_explicit_int_schema(spark_tmp_path, reader_confs): |
| 2092 | + """Explicit non-Null schema should strip UNKNOWN even when respect conf is true.""" |
| 2093 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN_EXPLICIT' |
| 2094 | + _write_parquet_unknown_null_table(data_path) |
| 2095 | + |
| 2096 | + read_schema = StructType([ |
| 2097 | + StructField('id', IntegerType(), True), |
| 2098 | + StructField('void_col', IntegerType(), True), |
| 2099 | + ]) |
| 2100 | + conf = copy_and_update(reader_confs, { |
| 2101 | + 'spark.sql.parquet.reader.respectUnknownTypeAnnotation.enabled': 'true', |
| 2102 | + }) |
| 2103 | + |
| 2104 | + def read_and_check_schema(spark): |
| 2105 | + df = spark.read.schema(read_schema).parquet(data_path) |
| 2106 | + assert df.schema['void_col'].dataType == IntegerType(), \ |
| 2107 | + f"expected void_col=IntegerType, got {df.schema['void_col'].dataType}" |
| 2108 | + return df |
| 2109 | + |
| 2110 | + assert_gpu_and_cpu_are_equal_collect(read_and_check_schema, conf=conf) |
| 2111 | + |
| 2112 | + |
| 2113 | +@pytest.mark.skipif(not is_spark_412_or_later(), |
| 2114 | + reason='SPARK-56045 requires Spark 4.1.2+') |
| 2115 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2116 | +def test_parquet_unknown_type_annotation_preserves_field_id(spark_tmp_path, reader_confs): |
| 2117 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN_FIELD_ID' |
| 2118 | + _write_parquet_unknown_null_table(data_path, field_id=7) |
| 2119 | + |
| 2120 | + read_schema = StructType([ |
| 2121 | + StructField('renamed_void', IntegerType(), True, metadata=with_id(7)), |
| 2122 | + ]) |
| 2123 | + conf = copy_and_update( |
| 2124 | + reader_confs, |
| 2125 | + enable_parquet_field_id_read, |
| 2126 | + {'spark.sql.parquet.reader.respectUnknownTypeAnnotation.enabled': 'false'}) |
| 2127 | + |
| 2128 | + def read_and_check_schema(spark): |
| 2129 | + df = spark.read.schema(read_schema).parquet(data_path) |
| 2130 | + assert df.schema['renamed_void'].dataType == IntegerType(), \ |
| 2131 | + f"expected renamed_void=IntegerType, got {df.schema['renamed_void'].dataType}" |
| 2132 | + return df |
| 2133 | + |
| 2134 | + assert_gpu_and_cpu_are_equal_collect(read_and_check_schema, conf=conf) |
| 2135 | + |
| 2136 | + |
| 2137 | +@pytest.mark.skipif(not is_spark_412_or_later(), |
| 2138 | + reason='SPARK-56045 requires Spark 4.1.2+') |
| 2139 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2140 | +def test_parquet_unknown_type_annotation_list_physical(spark_tmp_path, reader_confs): |
| 2141 | + """Primitive-element lists bypass structural clipping; UNKNOWN must still be stripped.""" |
| 2142 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN_LIST' |
| 2143 | + _write_parquet_unknown_null_table(data_path, with_list=True) |
| 2144 | + |
| 2145 | + conf = copy_and_update(reader_confs, { |
| 2146 | + 'spark.sql.parquet.reader.respectUnknownTypeAnnotation.enabled': 'false', |
| 2147 | + }) |
| 2148 | + |
| 2149 | + def read_and_check_schema(spark): |
| 2150 | + df = spark.read.parquet(data_path) |
| 2151 | + assert df.schema['list_void'].dataType == ArrayType(IntegerType()), \ |
| 2152 | + f"expected ArrayType(IntegerType), got {df.schema['list_void'].dataType}" |
| 2153 | + return df |
| 2154 | + |
| 2155 | + assert_gpu_and_cpu_are_equal_collect(read_and_check_schema, conf=conf) |
| 2156 | + |
| 2157 | + |
| 2158 | +@pytest.mark.skipif(not is_spark_412_or_later(), |
| 2159 | + reason='SPARK-56045 requires Spark 4.1.2+') |
| 2160 | +@pytest.mark.parametrize('reader_confs', reader_opt_confs) |
| 2161 | +def test_parquet_unknown_type_annotation_map_physical(spark_tmp_path, reader_confs): |
| 2162 | + """Primitive-value maps bypass structural clipping; UNKNOWN must still be stripped.""" |
| 2163 | + data_path = spark_tmp_path + '/PARQUET_UNKNOWN_MAP' |
| 2164 | + _write_parquet_unknown_null_table(data_path, with_map=True) |
| 2165 | + |
| 2166 | + conf = copy_and_update(reader_confs, { |
| 2167 | + 'spark.sql.parquet.reader.respectUnknownTypeAnnotation.enabled': 'false', |
| 2168 | + }) |
| 2169 | + |
| 2170 | + def read_and_check_schema(spark): |
| 2171 | + df = spark.read.parquet(data_path) |
| 2172 | + assert df.schema['map_void'].dataType == MapType(IntegerType(), IntegerType()), \ |
| 2173 | + f"expected MapType(IntegerType, IntegerType), got {df.schema['map_void'].dataType}" |
| 2174 | + return df |
| 2175 | + |
| 2176 | + assert_gpu_and_cpu_are_equal_collect(read_and_check_schema, conf=conf) |
0 commit comments