peter-toth commented on code in PR #59999:
URL: https://github.com/apache/spark/pull/59999#discussion_r4436667256
##########
common/utils/src/main/resources/error/error-conditions.json:
##########
@@ -8383,6 +8383,12 @@
],
"sqlState" : "22023"
},
+ "SUBQUERY_IN_CHECK_CONSTRAINT" : {
Review Comment:
**Finding 1.** This error rejects DDL that `master` accepts. I ran these on
the base commit with a V2 catalog, and each one succeeds and stores the
constraint:
- `CREATE TABLE t (id INT, CONSTRAINT c1 CHECK (id > (SELECT 0 AS a)))`
- `REPLACE TABLE t (id INT, CONSTRAINT c1 CHECK (id > (SELECT 0 AS a)))`
- `ALTER TABLE t ADD CONSTRAINT c1 CHECK (id > (SELECT 0 AS a))`
`CREATE TABLE` also accepts `CHECK (EXISTS (SELECT 1 AS a))`. SPARK-60777
added a migration guide entry for the same kind of change, so this one needs
one too. Something like this, after the SPARK-60777 entries:
```
- Since Spark 4.4, `CREATE TABLE`, `REPLACE TABLE`, and `ALTER TABLE ... ADD
CONSTRAINT` on a Data Source V2 table fail with `SUBQUERY_IN_CHECK_CONSTRAINT`
when the condition of a CHECK constraint contains a subquery. Previously, most
of these statements failed with `INTERNAL_ERROR`, but a subquery that needed no
resolution, such as `id > (SELECT 0 AS a)`, was accepted and every write
evaluated it again (SPARK-60781). Existing constraints that contain a subquery
are not rejected.
```
That entry would also replace the "Only `master` was run." sentence in the
description, which is hard to follow.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveTableSpec.scala:
##########
@@ -61,6 +61,14 @@ object ResolveTableSpec extends Rule[LogicalPlan] {
input: LogicalPlan,
tableSpec: TableSpecBase,
withNewSpec: TableSpecBase => LogicalPlan): LogicalPlan = tableSpec
match {
+ // Check this before `childrenResolved`: `ResolveSubquery` skips these
commands, so the subquery
+ // may never resolve, and `toV2Constraint` below would drop one that needs
no resolution.
+ case u: UnresolvedTableSpec if
u.constraints.exists(SubqueryExpression.hasSubquery) =>
Review Comment:
**Finding 3.** Every CHECK constraint is built by
`AstBuilder.visitCheckConstraint` (`AstBuilder.scala:7189`). That covers
table-level and column-level CHECK in `CREATE TABLE`, `REPLACE TABLE` and
`ALTER TABLE`. Finding a subquery needs no resolution, so the check could run
there:
```scala
override def visitCheckConstraint(ctx: CheckConstraintContext):
CheckConstraint =
withOrigin(ctx) {
val condition = getOriginalText(ctx.expr)
val check = CheckConstraint(
child = expression(ctx.booleanExpression()),
condition = condition)
check.failIfContainsSubquery()
check
}
```
I tried it with this case and the `CheckAnalysis` case removed.
`CheckConstraintSuite` passes, including both new tests with their query
context. What it changes:
- One call replaces both hooks. Neither this ordering comment nor the one
about the child `Filter` is needed.
- A subquery in a lambda gets the same error in every statement. Today
`CHECK (exists(array(id), v -> v > (SELECT 0)))` fails with
`SUBQUERY_IN_CHECK_CONSTRAINT` in `CREATE TABLE`, but with
`UNSUPPORTED_SUBQUERY_EXPRESSION_CATEGORY.HIGHER_ORDER_FUNCTION` in `ALTER
TABLE`.
The cost is a semantic check in the parser, which wins over every analysis
error. That includes a missing table (finding 2) and
`UNSUPPORTED_FEATURE.TABLE_OPERATION` for `ALTER TABLE` on a V1 table. There is
a precedent, though. `CheckConstraint.withUserProvidedCharacteristic` already
throws a `ParseException` from the parser for `NOT ENFORCED`.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/constraints.scala:
##########
@@ -167,6 +167,15 @@ case class CheckConstraint(
.build()
}
+ /** Fails the analysis if the condition contains a subquery, which `Check`
does not allow. */
+ def failIfContainsSubquery(): Unit = {
Review Comment:
**Finding 4.** Minor: this check sits after `toV2Constraint`, while its
sibling `failIfCallsUserDefinedFunction` sits above it. Moving it up next to
that one keeps the two `failIf*` checks together.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala:
##########
@@ -417,6 +417,11 @@ trait CheckAnalysis extends LookupCatalog with
QueryErrorsBase with PlanToString
// Here we perform additional checks for them.
checkIfSelfReferenceIsPlacedCorrectly(unionLoop, unionLoop.id)
+ // The child `Filter` holds the same condition. Check it first so that
an error of the
+ // subquery in the `Filter` does not hide that the constraint cannot
have a subquery.
+ case a: AddCheckConstraint =>
+ a.checkConstraint.failIfContainsSubquery()
Review Comment:
**Finding 2.** This traversal exists to report a missing INSERT target
before the errors in its query (the comment at `CheckAnalysis.scala:393`). For
`ALTER TABLE`, this case now does the opposite. I ran `ALTER TABLE
non_part_test_catalog.ns.missing ADD CONSTRAINT c CHECK (id > (SELECT 0))`:
- base: `TABLE_OR_VIEW_NOT_FOUND`
- this PR: `SUBQUERY_IN_CHECK_CONSTRAINT`
Matching on the relation under the `Filter` keeps the old order. With the
change below, the missing table reports `TABLE_OR_VIEW_NOT_FOUND` again, and
both new tests still pass. The `FROM missing` case of the test still gets the
new error, because that relation is inside the subquery, not under the `Filter`.
```suggestion
// The child `Filter` holds the same condition. Check it first so that
an error of the
// subquery in the `Filter` does not hide that the constraint cannot
have a subquery. A
// missing table is still reported first, by the bottom-up traversal.
case AddCheckConstraint(Filter(_, relation), check) if
relation.resolved =>
check.failIfContainsSubquery()
```
A test case with a missing table would pin the order. It fails without the
guard. The sentence about a missing table in the description would go too.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]