-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-20680][SQL] Spark-sql do not support for creating table with void column datatype #28833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
fda353f
1e86674
3b8ddec
cf0db98
479901d
5aa4c1a
fdf57bf
de08967
d6f1a4b
98d12fe
31ba0bf
9ad57d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ import org.apache.spark.sql.catalyst.parser.CatalystSqlParser | |
| import org.apache.spark.sql.catalyst.plans.logical.AlterTable | ||
| import org.apache.spark.sql.connector.catalog.TableChange._ | ||
| import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation | ||
| import org.apache.spark.sql.types.{ArrayType, DataType, HIVE_TYPE_STRING, HiveStringType, MapType, StructField, StructType} | ||
| import org.apache.spark.sql.types.{ArrayType, DataType, HIVE_TYPE_STRING, HiveStringType, MapType, NullType, StructField, StructType} | ||
| import org.apache.spark.sql.util.CaseInsensitiveStringMap | ||
| import org.apache.spark.util.Utils | ||
|
|
||
|
|
@@ -346,4 +346,23 @@ private[sql] object CatalogV2Util { | |
| } | ||
| } | ||
| } | ||
|
|
||
| def failNullType(dt: DataType): Unit = { | ||
| def containsNullType(dt: DataType): Boolean = dt match { | ||
| case ArrayType(et, _) => containsNullType(et) | ||
| case MapType(kt, vt, _) => containsNullType(kt) || containsNullType(vt) | ||
| case StructType(fields) => fields.exists(f => containsNullType(f.dataType)) | ||
| case _ => dt.isInstanceOf[NullType] | ||
| } | ||
| if (containsNullType(dt)) { | ||
| throw new AnalysisException( | ||
| "Cannot create tables with VOID type.") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ur, are we going to expose
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cloud-fan ask me to use VOID from NULL. Personally I agree to use VOID even through NullType is a data type class in Spark, but the symbol "null" has a literal syntax. Every time when we see a "null", we need to finger out it's a data type or literal. It's a little ambiguity. So @cloud-fan also suggests me to change the simpleString of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But I am not sure the behaviour in other database managements. Maybe
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will check some database system to see their behaviours, wait a minute.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for checking, @LantaoJin . This is also confusing to me. From Hive side,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So basically, looks like current change allows to parse One question, if we don't allow void/unknown type, is there good reason to accept in parser? Because we don't have real
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. We throw exception in command like creating table.
VOID is a legacy Hive table type which may be read by Spark. So in #28935, I map
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's go with |
||
| } | ||
| } | ||
|
|
||
| def assertNoNullTypeInSchema(schema: StructType): Unit = { | ||
| schema.foreach { f => | ||
| failNullType(f.dataType) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ class ResolveSessionCatalog( | |
| override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperatorsUp { | ||
| case AlterTableAddColumnsStatement( | ||
| nameParts @ SessionCatalogAndTable(catalog, tbl), cols) => | ||
| cols.foreach(c => failNullType(c.dataType)) | ||
| loadTable(catalog, tbl.asIdentifier).collect { | ||
| case v1Table: V1Table => | ||
| if (!DDLUtils.isHiveTable(v1Table.v1Table)) { | ||
|
|
@@ -76,6 +77,7 @@ class ResolveSessionCatalog( | |
|
|
||
| case AlterTableReplaceColumnsStatement( | ||
| nameParts @ SessionCatalogAndTable(catalog, tbl), cols) => | ||
| cols.foreach(c => failNullType(c.dataType)) | ||
| val changes: Seq[TableChange] = loadTable(catalog, tbl.asIdentifier) match { | ||
| case Some(_: V1Table) => | ||
| throw new AnalysisException("REPLACE COLUMNS is only supported with v2 tables.") | ||
|
|
@@ -100,6 +102,7 @@ class ResolveSessionCatalog( | |
|
|
||
| case a @ AlterTableAlterColumnStatement( | ||
| nameParts @ SessionCatalogAndTable(catalog, tbl), _, _, _, _, _) => | ||
| a.dataType.foreach(failNullType) | ||
| loadTable(catalog, tbl.asIdentifier).collect { | ||
| case v1Table: V1Table => | ||
| if (!DDLUtils.isHiveTable(v1Table.v1Table)) { | ||
|
|
@@ -268,6 +271,7 @@ class ResolveSessionCatalog( | |
| // session catalog and the table provider is not v2. | ||
| case c @ CreateTableStatement( | ||
| SessionCatalogAndTable(catalog, tbl), _, _, _, _, _, _, _, _, _) => | ||
| assertNoNullTypeInSchema(c.tableSchema) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be great if we could add a legacy flag for such behavior change in future. This changes the behavior for both v1 and v2 Catalogs in order to fix a compatibility issue with Hive Metastore. But Hive Metastore is not the only Catalog Spark supports since we have opened the Catalog APIs in DSv2.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know any database that supports creating tables with null/void type column, so this change is not for hive compatibility but for reasonable SQL semantic. I agree this is a breaking change that should be at least put in the migration guide. A legacy config can also be added but I can't find a reasonable use case for a null type column. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the main reason why you would want to support it is when people are using tables / views / temp tables to structure existing workloads. We support NullType type in CTEs, but in the case where people want to reuse the same CTE in multiple queries (i.e., multi-output workloads), they have no choice but to use views or temporary tables. (With DataFrames they'd still be able to reuse the same dataframe for multiple outputs, but in SQL that doesn't work.) One typical use case where you use CTEs to structure your code is if you have multiple sources with different structures that you then UNION ALL together into a single dataset. It is not uncommon for each of the sources to have certain columns that don't apply, and then you write explicit NULLs there. It would be pretty annoying if you had to write explicit casts of those NULLs to the right type in all of those cases.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bart-samwel this makes sense, shall we also support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @LantaoJin do you have time to fix it? I think we can simply remove the null type check and add a few tests with both in-memory and hive catalog. |
||
| val provider = c.provider.getOrElse(conf.defaultDataSourceName) | ||
| if (!isV2Provider(provider)) { | ||
| if (!DDLUtils.isHiveTable(Some(provider))) { | ||
|
|
@@ -292,6 +296,9 @@ class ResolveSessionCatalog( | |
|
|
||
| case c @ CreateTableAsSelectStatement( | ||
| SessionCatalogAndTable(catalog, tbl), _, _, _, _, _, _, _, _, _, _) => | ||
| if (c.asSelect.resolved) { | ||
| assertNoNullTypeInSchema(c.asSelect.schema) | ||
| } | ||
| val provider = c.provider.getOrElse(conf.defaultDataSourceName) | ||
| if (!isV2Provider(provider)) { | ||
| val tableDesc = buildCatalogTable(tbl.asTableIdentifier, new StructType, | ||
|
|
@@ -319,6 +326,7 @@ class ResolveSessionCatalog( | |
| // session catalog and the table provider is not v2. | ||
| case c @ ReplaceTableStatement( | ||
| SessionCatalogAndTable(catalog, tbl), _, _, _, _, _, _, _, _, _) => | ||
| assertNoNullTypeInSchema(c.tableSchema) | ||
|
LantaoJin marked this conversation as resolved.
|
||
| val provider = c.provider.getOrElse(conf.defaultDataSourceName) | ||
| if (!isV2Provider(provider)) { | ||
| throw new AnalysisException("REPLACE TABLE is only supported with v2 tables.") | ||
|
|
@@ -336,6 +344,9 @@ class ResolveSessionCatalog( | |
|
|
||
| case c @ ReplaceTableAsSelectStatement( | ||
| SessionCatalogAndTable(catalog, tbl), _, _, _, _, _, _, _, _, _, _) => | ||
| if (c.asSelect.resolved) { | ||
| assertNoNullTypeInSchema(c.asSelect.schema) | ||
| } | ||
| val provider = c.provider.getOrElse(conf.defaultDataSourceName) | ||
| if (!isV2Provider(provider)) { | ||
| throw new AnalysisException("REPLACE TABLE AS SELECT is only supported with v2 tables.") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import org.apache.spark.sql.catalyst.catalog._ | |
| import org.apache.spark.sql.catalyst.expressions.{Expression, InputFileBlockLength, InputFileBlockStart, InputFileName, RowOrdering} | ||
| import org.apache.spark.sql.catalyst.plans.logical._ | ||
| import org.apache.spark.sql.catalyst.rules.Rule | ||
| import org.apache.spark.sql.connector.catalog.CatalogV2Util.assertNoNullTypeInSchema | ||
| import org.apache.spark.sql.connector.expressions.{FieldReference, RewritableTransform} | ||
| import org.apache.spark.sql.execution.command.DDLUtils | ||
| import org.apache.spark.sql.execution.datasources.v2.FileDataSourceV2 | ||
|
|
@@ -292,6 +293,8 @@ case class PreprocessTableCreation(sparkSession: SparkSession) extends Rule[Logi | |
| "in the table definition of " + table.identifier, | ||
| sparkSession.sessionState.conf.caseSensitiveAnalysis) | ||
|
|
||
| assertNoNullTypeInSchema(schema) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this needed? I think the changes in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without this, "CREATE TABLE t1 USING PARQUET AS SELECT null as null_col" in Spark will throw Comparing the error message from Hive
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sorry, above description is incorrect. Without this, CTAS for Hive table Seems Hive table (non-parquet/orc format) doesn't go through |
||
|
|
||
| val normalizedPartCols = normalizePartitionColumns(schema, table) | ||
| val normalizedBucketSpec = normalizeBucketSpec(schema, table) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ import org.apache.spark.sql.catalyst.TableIdentifier | |
| import org.apache.spark.sql.catalyst.analysis.{NoSuchPartitionException, TableAlreadyExistsException} | ||
| import org.apache.spark.sql.catalyst.catalog._ | ||
| import org.apache.spark.sql.catalyst.parser.ParseException | ||
| import org.apache.spark.sql.connector.FakeV2Provider | ||
| import org.apache.spark.sql.connector.catalog.CatalogManager | ||
| import org.apache.spark.sql.connector.catalog.SupportsNamespaces.PROP_OWNER | ||
| import org.apache.spark.sql.execution.command.{DDLSuite, DDLUtils} | ||
|
|
@@ -2309,6 +2310,126 @@ class HiveDDLSuite | |
| } | ||
| } | ||
|
|
||
| test("SPARK-20680: Spark-sql do not support for void column datatype") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you adjust this test case according to the last commit? Specifically, for the following?
|
||
| withTable("t") { | ||
| withView("tabVoidType") { | ||
| hiveClient.runSqlHive("CREATE TABLE t (t1 int)") | ||
| hiveClient.runSqlHive("INSERT INTO t VALUES (3)") | ||
| hiveClient.runSqlHive("CREATE VIEW tabVoidType AS SELECT NULL AS col FROM t") | ||
| checkAnswer(spark.table("tabVoidType"), Row(null)) | ||
| // No exception shows | ||
| val desc = spark.sql("DESC tabVoidType").collect().toSeq | ||
| assert(desc.contains(Row("col", "null", null))) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean I think it looks better if DESC TABLE returns
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
| } | ||
| } | ||
|
|
||
| // Forbid CTAS with null type | ||
| withTable("t1", "t2", "t3") { | ||
| val e1 = intercept[AnalysisException] { | ||
| spark.sql("CREATE TABLE t1 USING PARQUET AS SELECT null as null_col") | ||
| }.getMessage | ||
| assert(e1.contains("Cannot create tables with VOID type")) | ||
|
|
||
| val e2 = intercept[AnalysisException] { | ||
| spark.sql("CREATE TABLE t2 AS SELECT null as null_col") | ||
| }.getMessage | ||
| assert(e2.contains("Cannot create tables with VOID type")) | ||
|
|
||
| val e3 = intercept[AnalysisException] { | ||
| spark.sql("CREATE TABLE t3 STORED AS PARQUET AS SELECT null as null_col") | ||
| }.getMessage | ||
| assert(e3.contains("Cannot create tables with VOID type")) | ||
| } | ||
|
|
||
| // Forbid Replace table AS SELECT with null type | ||
| withTable("t") { | ||
| val v2Source = classOf[FakeV2Provider].getName | ||
| val e = intercept[AnalysisException] { | ||
| spark.sql(s"CREATE OR REPLACE TABLE t USING $v2Source AS SELECT null as null_col") | ||
| }.getMessage | ||
| assert(e.contains("Cannot create tables with VOID type")) | ||
| } | ||
|
|
||
| // Forbid creating table with VOID type in Spark | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this line and the following, it looks correct because we parse |
||
| withTable("t1", "t2", "t3", "t4") { | ||
| val e1 = intercept[AnalysisException] { | ||
| spark.sql(s"CREATE TABLE t1 (v VOID) USING PARQUET") | ||
| }.getMessage | ||
| assert(e1.contains("Cannot create tables with VOID type")) | ||
| val e2 = intercept[AnalysisException] { | ||
| spark.sql(s"CREATE TABLE t2 (v VOID) USING hive") | ||
| }.getMessage | ||
| assert(e2.contains("Cannot create tables with VOID type")) | ||
| val e3 = intercept[AnalysisException] { | ||
| spark.sql(s"CREATE TABLE t3 (v VOID)") | ||
| }.getMessage | ||
| assert(e3.contains("Cannot create tables with VOID type")) | ||
| val e4 = intercept[AnalysisException] { | ||
| spark.sql(s"CREATE TABLE t4 (v VOID) STORED AS PARQUET") | ||
| }.getMessage | ||
| assert(e4.contains("Cannot create tables with VOID type")) | ||
| } | ||
|
|
||
| // Forbid Replace table with VOID type | ||
| withTable("t") { | ||
| val v2Source = classOf[FakeV2Provider].getName | ||
| val e = intercept[AnalysisException] { | ||
| spark.sql(s"CREATE OR REPLACE TABLE t (v VOID) USING $v2Source") | ||
| }.getMessage | ||
| assert(e.contains("Cannot create tables with VOID type")) | ||
| } | ||
|
|
||
| // Make sure spark.catalog.createTable with null type will fail | ||
| val schema1 = new StructType().add("c", NullType) | ||
| assertHiveTableNullType(schema1) | ||
| assertDSTableNullType(schema1) | ||
|
|
||
| val schema2 = new StructType() | ||
| .add("c", StructType(Seq(StructField.apply("c1", NullType)))) | ||
| assertHiveTableNullType(schema2) | ||
| assertDSTableNullType(schema2) | ||
|
|
||
| val schema3 = new StructType().add("c", ArrayType(NullType)) | ||
| assertHiveTableNullType(schema3) | ||
| assertDSTableNullType(schema3) | ||
|
|
||
| val schema4 = new StructType() | ||
| .add("c", MapType(StringType, NullType)) | ||
| assertHiveTableNullType(schema4) | ||
| assertDSTableNullType(schema4) | ||
|
|
||
| val schema5 = new StructType() | ||
| .add("c", MapType(NullType, StringType)) | ||
| assertHiveTableNullType(schema5) | ||
| assertDSTableNullType(schema5) | ||
| } | ||
|
|
||
| private def assertHiveTableNullType(schema: StructType): Unit = { | ||
| withTable("t") { | ||
| val e = intercept[AnalysisException] { | ||
| spark.catalog.createTable( | ||
| tableName = "t", | ||
| source = "hive", | ||
| schema = schema, | ||
| options = Map("fileFormat" -> "parquet")) | ||
| }.getMessage | ||
| assert(e.contains("Cannot create tables with VOID type")) | ||
| } | ||
| } | ||
|
|
||
| private def assertDSTableNullType(schema: StructType): Unit = { | ||
| withTable("t") { | ||
| val e = intercept[AnalysisException] { | ||
| spark.catalog.createTable( | ||
| tableName = "t", | ||
| source = "json", | ||
| schema = schema, | ||
| options = Map.empty[String, String]) | ||
| }.getMessage | ||
| assert(e.contains("Cannot create tables with VOID type")) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-21216: join with a streaming DataFrame") { | ||
| import org.apache.spark.sql.execution.streaming.MemoryStream | ||
| import testImplicits._ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.