diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/ExpressionEncoder.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/ExpressionEncoder.scala index 74d7a5e7a675..654f39393636 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/ExpressionEncoder.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/ExpressionEncoder.scala @@ -325,11 +325,19 @@ case class ExpressionEncoder[T]( assert(serializer.forall(_.references.isEmpty), "serializer cannot reference any attributes.") assert(serializer.flatMap { ser => val boundRefs = ser.collect { case b: BoundReference => b } - assert(boundRefs.nonEmpty, - "each serializer expression should contain at least one `BoundReference`") + assert(boundRefs.nonEmpty || isEmptyStruct(ser), + "each serializer expression should contain at least one `BoundReference` or it " + + "should be an empty struct. This is required to ensure that there is a reference point " + + "for the serialized object or that the serialized object is intentionally left empty." + ) boundRefs }.distinct.length <= 1, "all serializer expressions must use the same BoundReference.") + private def isEmptyStruct(expr: NamedExpression): Boolean = expr.dataType match { + case struct: StructType => struct.isEmpty + case _ => false + } + /** * Returns a new copy of this encoder, where the `deserializer` is resolved and bound to the * given schema. diff --git a/sql/core/src/test/resources/sql-tests/analyzer-results/selectExcept.sql.out b/sql/core/src/test/resources/sql-tests/analyzer-results/selectExcept.sql.out index 8643d40b886b..48c7ad10ed78 100644 --- a/sql/core/src/test/resources/sql-tests/analyzer-results/selectExcept.sql.out +++ b/sql/core/src/test/resources/sql-tests/analyzer-results/selectExcept.sql.out @@ -121,6 +121,18 @@ Project [id#x, name#x, named_struct(f1, data#x.f1, s2, named_struct(f3, data#x.s +- LocalRelation [id#x, name#x, data#x] +-- !query +SELECT * EXCEPT (data.f1, data.s2) FROM tbl_view +-- !query analysis +Project [id#x, name#x, named_struct() AS data#x] ++- SubqueryAlias tbl_view + +- View (`tbl_view`, [id#x,name#x,data#x]) + +- Project [cast(id#x as int) AS id#x, cast(name#x as string) AS name#x, cast(data#x as struct>) AS data#x] + +- Project [id#x, name#x, data#x] + +- SubqueryAlias tbl_view + +- LocalRelation [id#x, name#x, data#x] + + -- !query SELECT * EXCEPT (id, name, data) FROM tbl_view -- !query analysis diff --git a/sql/core/src/test/resources/sql-tests/inputs/selectExcept.sql b/sql/core/src/test/resources/sql-tests/inputs/selectExcept.sql index e07e4f1117c2..08d56aeda0a8 100644 --- a/sql/core/src/test/resources/sql-tests/inputs/selectExcept.sql +++ b/sql/core/src/test/resources/sql-tests/inputs/selectExcept.sql @@ -20,6 +20,7 @@ SELECT * EXCEPT (data) FROM tbl_view; SELECT * EXCEPT (data.f1) FROM tbl_view; SELECT * EXCEPT (data.s2) FROM tbl_view; SELECT * EXCEPT (data.s2.f2) FROM tbl_view; +SELECT * EXCEPT (data.f1, data.s2) FROM tbl_view; -- EXCEPT all columns SELECT * EXCEPT (id, name, data) FROM tbl_view; -- EXCEPT special character names diff --git a/sql/core/src/test/resources/sql-tests/results/selectExcept.sql.out b/sql/core/src/test/resources/sql-tests/results/selectExcept.sql.out index 6f6ba9097342..2621782342cc 100644 --- a/sql/core/src/test/resources/sql-tests/results/selectExcept.sql.out +++ b/sql/core/src/test/resources/sql-tests/results/selectExcept.sql.out @@ -121,6 +121,20 @@ struct>> 70 name7 {"f1":7,"s2":{"f3":"g"}} +-- !query +SELECT * EXCEPT (data.f1, data.s2) FROM tbl_view +-- !query schema +struct> +-- !query output +10 name1 {} +20 name2 {} +30 name3 {} +40 name4 {} +50 name5 {} +60 name6 {} +70 name7 {} + + -- !query SELECT * EXCEPT (id, name, data) FROM tbl_view -- !query schema