Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,12 @@ object LikeSimplification extends Rule[LogicalPlan] with PredicateHelper {
Some(EndsWith(input, Literal.create(postfix, input.dataType)))
// 'a%a' pattern is basically same with 'a%' && '%a'.
// However, the additional length condition is required to prevent 'a' match 'a%a'.
case startsAndEndsWith(prefix, postfix) =>
// This rewrite references `input` three times (length guard, StartsWith, EndsWith), so
// gate it on `input` being cheap to duplicate -- mirroring the SPARK-40228 gate on the
// multiLike rules below. A non-cheap child (e.g. `sha2(col)`, or a nondeterministic
// `uuid()` that would otherwise draw a different value per reference) is left as `Like`;
// the single-reference shapes are unaffected.
case startsAndEndsWith(prefix, postfix) if CollapseProject.isCheap(input) =>
// The length guard only rejects inputs too short to hold both the prefix and the
// suffix. When the collation matches raw bytes (supportsBinaryEquality),
// StartsWith/EndsWith pin the literal bytes of the prefix and suffix, so a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,32 @@ class LikeSimplificationSuite extends PlanTest {
comparePlans(Optimize.execute(originalQuery), originalQuery)
}

test("SPARK-59371: do not simplify startsAndEndsWith LIKE for a non-cheap child") {
// 'a%b' rewrites to a length guard + StartsWith + EndsWith, referencing the child three
// times. Duplicating a non-cheap child would re-evaluate it (and yield different values for a
// nondeterministic one), so this shape must be left as Like -- cf. the SPARK-40228 gate above.
val originalQuery = testRelation.where($"a".substring(1, 5) like "a%b").analyze
comparePlans(Optimize.execute(originalQuery), originalQuery)
}

test("SPARK-59371: still simplify single-reference LIKE shapes for a non-cheap child") {
// The single-reference shapes evaluate the child once, exactly like the original LIKE, so they
// stay enabled for a non-cheap child; only the duplicating startsAndEndsWith shape is gated.
val child = $"a".substring(1, 5)
comparePlans(
Optimize.execute(testRelation.where(child like "a%").analyze),
testRelation.where(StartsWith(child, "a")).analyze)
comparePlans(
Optimize.execute(testRelation.where(child like "%b").analyze),
testRelation.where(EndsWith(child, "b")).analyze)
comparePlans(
Optimize.execute(testRelation.where(child like "%ab%").analyze),
testRelation.where(Contains(child, "ab")).analyze)
comparePlans(
Optimize.execute(testRelation.where(child like "abc").analyze),
testRelation.where(EqualTo(child, "abc")).analyze)
}

// scalastyle:off nonascii
test("SPARK-59063: LikeSimplification preserves LIKE semantics under non-binary collation") {
// Under UTF8_LCASE, StartsWith/EndsWith are collation-aware, so a single code point
Expand Down