|
5 | 5 |
|
6 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; |
7 | 7 | import static org.junit.jupiter.api.Assertions.assertFalse; |
| 8 | +import static org.junit.jupiter.api.Assertions.assertThrows; |
8 | 9 | import static org.junit.jupiter.api.Assertions.assertTrue; |
9 | 10 |
|
10 | 11 | import java.nio.file.Path; |
11 | 12 | import java.util.List; |
12 | 13 | import org.apache.spark.sql.Dataset; |
13 | 14 | import org.apache.spark.sql.Row; |
| 15 | +import org.apache.spark.sql.RowFactory; |
14 | 16 | import org.apache.spark.sql.SaveMode; |
15 | 17 | import org.apache.spark.sql.SparkSession; |
| 18 | +import org.apache.spark.sql.types.DataTypes; |
| 19 | +import org.apache.spark.sql.types.StructType; |
16 | 20 | import org.junit.jupiter.api.AfterAll; |
| 21 | +import org.junit.jupiter.api.AfterEach; |
17 | 22 | import org.junit.jupiter.api.BeforeAll; |
18 | 23 | import org.junit.jupiter.api.Test; |
19 | 24 | import org.junit.jupiter.api.TestInstance; |
20 | 25 | import org.junit.jupiter.api.io.TempDir; |
| 26 | +import org.junit.jupiter.params.ParameterizedTest; |
| 27 | +import org.junit.jupiter.params.provider.CsvSource; |
21 | 28 |
|
22 | 29 | /** |
23 | | - * Reads that need no data column at all. |
| 30 | + * Column pruning and resolution of user-specified read schemas. |
24 | 31 | * |
25 | 32 | * <p>A query over partition columns alone, and a count Spark declines to push down, both leave the read data schema |
26 | 33 | * empty. The scan must still push an empty projection, or it pulls every column off storage to answer them. |
@@ -50,6 +57,59 @@ public void tearDown() { |
50 | 57 | } |
51 | 58 | } |
52 | 59 |
|
| 60 | + @AfterEach |
| 61 | + void restoreActiveSession() { |
| 62 | + SparkSession.setActiveSession(spark); |
| 63 | + } |
| 64 | + |
| 65 | + @ParameterizedTest |
| 66 | + @CsvSource({"v2,false", "v2,true", "v1,false", "v1,true", "v1_rows,false", "v1_rows,true"}) |
| 67 | + void explicitSchemaRespectsCaseSensitivity(String mode, boolean caseSensitive) { |
| 68 | + SparkSession session = newActiveSession(); |
| 69 | + session.conf().set("spark.sql.sources.useV1SourceList", mode.startsWith("v1") ? "vortex" : ""); |
| 70 | + session.conf().set("spark.sql.codegen.wholeStage", !mode.equals("v1_rows")); |
| 71 | + session.conf().set("spark.sql.caseSensitive", caseSensitive); |
| 72 | + Path output = tempDir.resolve("schema_case_" + mode + "_" + caseSensitive); |
| 73 | + session.range(1, 3).coalesce(1).write().format("vortex").save(output.toString()); |
| 74 | + |
| 75 | + Dataset<Row> data = session.read() |
| 76 | + .format("vortex") |
| 77 | + .schema(new StructType().add("ID", DataTypes.LongType, true)) |
| 78 | + .load(output.toString()); |
| 79 | + List<Row> expected = caseSensitive |
| 80 | + ? List.of(RowFactory.create((Object) null), RowFactory.create((Object) null)) |
| 81 | + : List.of(RowFactory.create(1L), RowFactory.create(2L)); |
| 82 | + assertEquals(expected, data.orderBy("ID").collectAsList()); |
| 83 | + assertEquals( |
| 84 | + caseSensitive ? List.of() : List.of(RowFactory.create(2L)), |
| 85 | + data.where("ID > 1").collectAsList()); |
| 86 | + } |
| 87 | + |
| 88 | + @Test |
| 89 | + void explicitSchemaRejectsAmbiguousColumnNames() { |
| 90 | + SparkSession session = newActiveSession(); |
| 91 | + session.conf().set("spark.sql.sources.useV1SourceList", ""); |
| 92 | + session.conf().set("spark.sql.caseSensitive", true); |
| 93 | + Path output = tempDir.resolve("ambiguous_schema"); |
| 94 | + session.range(1) |
| 95 | + .selectExpr("id", "id + 1 AS ID") |
| 96 | + .coalesce(1) |
| 97 | + .write() |
| 98 | + .format("vortex") |
| 99 | + .save(output.toString()); |
| 100 | + session.conf().set("spark.sql.caseSensitive", false); |
| 101 | + Dataset<Row> data = session.read() |
| 102 | + .format("vortex") |
| 103 | + .schema(new StructType().add("id", DataTypes.LongType, true)) |
| 104 | + .load(output.toString()); |
| 105 | + |
| 106 | + Throwable failure = assertThrows(Exception.class, data::collectAsList); |
| 107 | + while (failure.getCause() != null) { |
| 108 | + failure = failure.getCause(); |
| 109 | + } |
| 110 | + assertTrue(failure.getMessage().contains("Ambiguous columns id and ID"), failure.getMessage()); |
| 111 | + } |
| 112 | + |
53 | 113 | @Test |
54 | 114 | void selectingOnlyAPartitionColumnReadsNoDataColumn() { |
55 | 115 | Dataset<Row> data = writeAndRead("partition_only", true).select("group"); |
@@ -89,6 +149,13 @@ private String plan(Dataset<Row> data) { |
89 | 149 | return data.queryExecution().executedPlan().toString(); |
90 | 150 | } |
91 | 151 |
|
| 152 | + private SparkSession newActiveSession() { |
| 153 | + SparkSession session = spark.newSession(); |
| 154 | + // Spark 3.5's FileDataSourceV2 resolves its session through SparkSession.active. |
| 155 | + SparkSession.setActiveSession(session); |
| 156 | + return session; |
| 157 | + } |
| 158 | + |
92 | 159 | private Dataset<Row> writeAndRead(String name, boolean partitioned) { |
93 | 160 | Path output = tempDir.resolve(name); |
94 | 161 | write(output, partitioned); |
|
0 commit comments