This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 8b330ca3be [spark] Push down array predicates to multivalue indexes
(#9306)
8b330ca3be is described below
commit 8b330ca3be9ab517f37ee12bdabce2d9082354e2
Author: Jingsong Lee <[email protected]>
AuthorDate: Thu Aug 20 08:53:03 2026 +0800
[spark] Push down array predicates to multivalue indexes (#9306)
---
.../sql/ArrayPredicatePushDownVersionTest.scala | 24 +++++
.../sql/ArrayPredicatePushDownVersionTest.scala | 24 +++++
.../paimon/spark/SparkV2FilterConverter.scala | 21 ++++-
.../optimizer/PushDownArrayPredicates.scala | 102 +++++++++++++++++++++
.../extensions/PaimonSparkSessionExtensions.scala | 7 +-
.../spark/util/SparkExpressionConverter.scala | 28 +++++-
.../ArrayPredicatePushDownVersionTestBase.scala | 77 ++++++++++++++++
.../spark/sql/PrimaryKeySortedIndexTest.scala | 70 +++++++++++++-
8 files changed, 346 insertions(+), 7 deletions(-)
diff --git
a/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTest.scala
b/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTest.scala
new file mode 100644
index 0000000000..63decd9230
--- /dev/null
+++
b/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTest.scala
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+class ArrayPredicatePushDownVersionTest extends
ArrayPredicatePushDownVersionTestBase {
+
+ override protected def supportsArrayPredicatePushDown: Boolean = false
+}
diff --git
a/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTest.scala
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTest.scala
new file mode 100644
index 0000000000..ca9049335a
--- /dev/null
+++
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTest.scala
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+class ArrayPredicatePushDownVersionTest extends
ArrayPredicatePushDownVersionTestBase {
+
+ override protected def supportsArrayPredicatePushDown: Boolean = true
+}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/SparkV2FilterConverter.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/SparkV2FilterConverter.scala
index edece9340b..168c518cd3 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/SparkV2FilterConverter.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/SparkV2FilterConverter.scala
@@ -19,12 +19,13 @@
package org.apache.paimon.spark
import org.apache.paimon.predicate.{Predicate, PredicateBuilder, Transform}
-import org.apache.paimon.spark.util.SparkExpressionConverter.{toPaimonLiteral,
toPaimonTransform}
-import org.apache.paimon.types.RowType
+import
org.apache.paimon.spark.util.SparkExpressionConverter.{toPaimonArrayLiteral,
toPaimonLiteral, toPaimonTransform}
+import org.apache.paimon.types.{ArrayType, RowType}
import org.apache.spark.internal.Logging
import org.apache.spark.sql.connector.expressions.{Expression, Literal}
import org.apache.spark.sql.connector.expressions.filter.{And, Not, Or,
Predicate => SparkPredicate}
+import org.apache.spark.sql.types.{ArrayType => SparkArrayType}
import scala.collection.JavaConverters._
@@ -161,6 +162,11 @@ case class SparkV2FilterConverter(rowType: RowType)
extends Logging {
case STRING_CONTAINS =>
sparkPredicate match {
+ case ArrayLiteralPredicate(transform, literals) =>
+ builder.arraysOverlap(transform, literals.asJava)
+ case BinaryPredicate(transform, literal)
+ if transform.outputType().isInstanceOf[ArrayType] =>
+ builder.arrayContains(transform, literal)
case BinaryPredicate(transform, literal) =>
builder.contains(transform, literal)
case _ =>
@@ -217,6 +223,17 @@ case class SparkV2FilterConverter(rowType: RowType)
extends Logging {
}
}
}
+
+ private object ArrayLiteralPredicate {
+ def unapply(sparkPredicate: SparkPredicate): Option[(Transform,
Seq[Object])] = {
+ sparkPredicate.children() match {
+ case Array(e: Expression, literal: Literal[_])
+ if literal.dataType().isInstanceOf[SparkArrayType] =>
+ toPaimonTransform(e, rowType).map((_, toPaimonArrayLiteral(literal)))
+ case _ => None
+ }
+ }
+ }
}
object SparkV2FilterConverter extends Logging {
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownArrayPredicates.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownArrayPredicates.scala
new file mode 100644
index 0000000000..093d2df736
--- /dev/null
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownArrayPredicates.scala
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.catalyst.optimizer
+
+import org.apache.paimon.spark.catalyst.analysis.PaimonRelation
+
+import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
+import org.apache.spark.sql.catalyst.expressions.{ArrayContains,
ArraysOverlap, BinaryComparison, Expression, Literal}
+import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext,
ExprCode}
+import org.apache.spark.sql.catalyst.plans.logical.{Filter, LogicalPlan}
+import org.apache.spark.sql.catalyst.rules.Rule
+
+/** Makes literal array predicates visible to Spark's V2 predicate translator
for Paimon scans. */
+object PushDownArrayPredicates extends Rule[LogicalPlan] {
+
+ override def apply(plan: LogicalPlan): LogicalPlan = plan.transformUp {
+ case filter @ Filter(condition, child) if
PaimonRelation.isPaimonTable(child) =>
+ filter.copy(condition = condition.transformDown {
+ case ArrayContains(array, literal: Literal)
+ if array.references.nonEmpty &&
+ array.references.subsetOf(child.outputSet) &&
+ literal.value != null =>
+ PaimonArrayContains(array, literal)
+
+ case ArraysOverlap(array, literal: Literal)
+ if array.references.nonEmpty &&
array.references.subsetOf(child.outputSet) =>
+ PaimonArraysOverlap(array, literal)
+
+ case ArraysOverlap(literal: Literal, array)
+ if array.references.nonEmpty &&
array.references.subsetOf(child.outputSet) =>
+ PaimonArraysOverlap(array, literal)
+ })
+ }
+}
+
+/**
+ * Spark's V2 expression builder translates [[BinaryComparison]] generically,
but does not know
+ * [[ArrayContains]]. This comparison-shaped wrapper keeps Spark's original
evaluation semantics for
+ * the residual filter while exposing an array membership marker to the
connector.
+ */
+private[spark] case class PaimonArrayContains(left: Expression, right:
Expression)
+ extends BinaryComparison {
+
+ private def original: ArrayContains = ArrayContains(left, right)
+
+ // Use a V2 predicate name understood by Spark's SQL renderer. The Paimon
converter
+ // distinguishes string and array CONTAINS predicates from the transform
output type.
+ override def symbol: String = "CONTAINS"
+
+ override def nullable: Boolean = original.nullable
+
+ override def checkInputDataTypes(): TypeCheckResult =
original.checkInputDataTypes()
+
+ override protected def nullSafeEval(leftValue: Any, rightValue: Any): Any =
+ original.nullSafeEval(leftValue, rightValue)
+
+ override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode =
+ original.doGenCode(ctx, ev)
+
+ override protected def withNewChildrenInternal(
+ newLeft: Expression,
+ newRight: Expression): PaimonArrayContains = copy(left = newLeft, right
= newRight)
+}
+
+/** Comparison-shaped V2 bridge for Spark's [[ArraysOverlap]] expression. */
+private[spark] case class PaimonArraysOverlap(left: Expression, right:
Expression)
+ extends BinaryComparison {
+
+ private def original: ArraysOverlap = ArraysOverlap(left, right)
+
+ override def symbol: String = "CONTAINS"
+
+ override def nullable: Boolean = original.nullable
+
+ override def checkInputDataTypes(): TypeCheckResult =
original.checkInputDataTypes()
+
+ override protected def nullSafeEval(leftValue: Any, rightValue: Any): Any =
+ original.nullSafeEval(leftValue, rightValue)
+
+ override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode =
+ original.doGenCode(ctx, ev)
+
+ override protected def withNewChildrenInternal(
+ newLeft: Expression,
+ newRight: Expression): PaimonArraysOverlap = copy(left = newLeft, right
= newRight)
+}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
index f856d348a4..b216454ba4 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
@@ -19,7 +19,7 @@
package org.apache.paimon.spark.extensions
import org.apache.paimon.spark.catalyst.analysis.{PaimonAnalysis,
PaimonDeleteTable, PaimonFunctionResolver, PaimonIncompatibleResolutionRules,
PaimonMergeInto, PaimonPostHocResolutionRules, PaimonProcedureResolver,
PaimonUpdateTable, PaimonViewResolver, ReplacePaimonFunctions}
-import
org.apache.paimon.spark.catalyst.optimizer.{MergePaimonScalarSubqueries,
OptimizeMetadataOnlyDeleteFromPaimonTable, PushDownLateralVectorSearchFilter,
RepartitionLateralVectorSearchInput}
+import
org.apache.paimon.spark.catalyst.optimizer.{MergePaimonScalarSubqueries,
OptimizeMetadataOnlyDeleteFromPaimonTable, PushDownArrayPredicates,
PushDownLateralVectorSearchFilter, RepartitionLateralVectorSearchInput}
import
org.apache.paimon.spark.catalyst.plans.logical.PaimonTableValuedFunctions
import org.apache.paimon.spark.commands.BucketExpression
import org.apache.paimon.spark.execution.{OldCompatibleStrategy,
PaimonStrategy}
@@ -100,6 +100,11 @@ class PaimonSparkSessionExtensions extends
(SparkSessionExtensions => Unit) {
extensions.injectOptimizerRule(spark => ReplacePaimonFunctions(spark))
extensions.injectOptimizerRule(spark =>
OptimizeMetadataOnlyDeleteFromPaimonTable(spark))
extensions.injectOptimizerRule(_ => MergePaimonScalarSubqueries)
+ // Spark 3.2 uses the V1 filter translation path, which cannot translate
the
+ // comparison-shaped array predicate bridge used by this rule.
+ if (org.apache.spark.SPARK_VERSION >= "3.3") {
+ extensions.injectOptimizerRule(_ => PushDownArrayPredicates)
+ }
extensions.injectOptimizerRule(_ => RepartitionLateralVectorSearchInput)
extensions.injectOptimizerRule(_ => PushDownLateralVectorSearchFilter)
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/util/SparkExpressionConverter.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/util/SparkExpressionConverter.scala
index a5ff3598fb..00898c98ca 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/util/SparkExpressionConverter.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/util/SparkExpressionConverter.scala
@@ -25,8 +25,9 @@ import
org.apache.paimon.spark.util.shim.TypeUtils.treatPaimonTimestampTypeAsSpa
import org.apache.paimon.types.{DecimalType, RowType}
import org.apache.paimon.types.DataTypeRoot._
-import org.apache.spark.sql.catalyst.util.DateTimeUtils
+import org.apache.spark.sql.catalyst.util.{ArrayData, DateTimeUtils}
import org.apache.spark.sql.connector.expressions.{Cast, Expression,
GeneralScalarExpression, Literal, NamedReference}
+import org.apache.spark.sql.types.{ArrayType => SparkArrayType, DataType =>
SparkDataType}
import scala.collection.JavaConverters._
@@ -98,8 +99,29 @@ object SparkExpressionConverter {
throw new UnsupportedOperationException(s"Convert value: $literal is
unsupported.")
}
- val dataType = SparkTypeUtils.toPaimonType(literal.dataType())
- val value = literal.value()
+ toPaimonLiteral(literal.value(), literal.dataType())
+ }
+
+ /** Convert a Spark ARRAY [[Literal]] to Paimon element literals. */
+ def toPaimonArrayLiteral(literal: Literal[_]): Seq[Object] = {
+ literal.dataType() match {
+ case SparkArrayType(elementType, _) =>
+ val array = literal.value().asInstanceOf[ArrayData]
+ (0 until array.numElements()).map {
+ i =>
+ if (array.isNullAt(i)) {
+ null
+ } else {
+ toPaimonLiteral(array.get(i, elementType), elementType)
+ }
+ }
+ case _ =>
+ throw new UnsupportedOperationException(s"Convert value: $literal is
unsupported.")
+ }
+ }
+
+ private def toPaimonLiteral(value: Any, sparkDataType: SparkDataType):
Object = {
+ val dataType = SparkTypeUtils.toPaimonType(sparkDataType)
dataType.getTypeRoot match {
case BOOLEAN | BIGINT | DOUBLE | TINYINT | SMALLINT | INTEGER | FLOAT |
DATE =>
value.asInstanceOf[AnyRef]
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTestBase.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTestBase.scala
new file mode 100644
index 0000000000..2c6b3e9f13
--- /dev/null
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/ArrayPredicatePushDownVersionTestBase.scala
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+import org.apache.paimon.spark.PaimonSparkTestBase
+import org.apache.paimon.spark.catalyst.optimizer.{PaimonArrayContains,
PaimonArraysOverlap, PushDownArrayPredicates}
+
+import org.apache.spark.sql.catalyst.expressions.{ArrayContains,
ArraysOverlap, Expression}
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
+
+abstract class ArrayPredicatePushDownVersionTestBase extends
PaimonSparkTestBase {
+
+ protected def supportsArrayPredicatePushDown: Boolean
+
+ test("register array predicate pushdown only for supported Spark versions") {
+ val registered = spark.sessionState.optimizer.batches
+ .flatMap(_.rules)
+ .exists(_.getClass == PushDownArrayPredicates.getClass)
+
+ assert(registered == supportsArrayPredicatePushDown)
+ }
+
+ test("rewrite array predicates only for supported Spark versions") {
+ withTable("t") {
+ sql("CREATE TABLE t (id INT, tags ARRAY<STRING>)")
+
+ Seq(
+ "SELECT * FROM t WHERE array_contains(tags, 'red')",
+ "SELECT * FROM t WHERE arrays_overlap(tags, array('red', 'blue'))"
+ ).foreach {
+ query =>
+ val optimizedPlan = sql(query).queryExecution.optimizedPlan
+ assert(
+ containsPaimonArrayPredicate(optimizedPlan) ==
supportsArrayPredicatePushDown,
+ optimizedPlan.toString())
+ assert(
+ containsSparkArrayPredicate(optimizedPlan) !=
supportsArrayPredicatePushDown,
+ optimizedPlan.toString())
+ }
+ }
+ }
+
+ private def containsPaimonArrayPredicate(plan: LogicalPlan): Boolean =
+ containsExpression(
+ plan,
+ {
+ case _: PaimonArrayContains | _: PaimonArraysOverlap => true
+ case _ => false
+ })
+
+ private def containsSparkArrayPredicate(plan: LogicalPlan): Boolean =
+ containsExpression(
+ plan,
+ {
+ case _: ArrayContains | _: ArraysOverlap => true
+ case _ => false
+ })
+
+ private def containsExpression(plan: LogicalPlan, predicate: Expression =>
Boolean): Boolean =
+ plan.find(_.expressions.exists(_.find(predicate).isDefined)).isDefined
+}
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PrimaryKeySortedIndexTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PrimaryKeySortedIndexTest.scala
index 4a09bfc18b..823b099582 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PrimaryKeySortedIndexTest.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PrimaryKeySortedIndexTest.scala
@@ -18,7 +18,9 @@
package org.apache.paimon.spark.sql
+import org.apache.paimon.data.BinaryString
import org.apache.paimon.globalindex.IndexedSplit
+import org.apache.paimon.predicate.{ArrayContains, ArrayContainsAll,
ArraysOverlap, LeafPredicate, PredicateBuilder}
import org.apache.paimon.spark.PaimonSparkTestBase
import org.apache.paimon.table.source.DataSplit
@@ -26,9 +28,75 @@ import org.apache.spark.sql.Row
import scala.collection.JavaConverters._
-/** End-to-end Spark SQL tests for source-backed primary-key BTree and Bitmap
indexes. */
+/** End-to-end Spark SQL tests for source-backed primary-key sorted indexes. */
class PrimaryKeySortedIndexTest extends PaimonSparkTestBase {
+ test("Spark array predicates use multivalue index") {
+ assume(gteqSpark3_3)
+
+ withTable("t") {
+ spark.sql("""
+ |CREATE TABLE t (id INT, tags ARRAY<STRING>, probe STRING)
+ |TBLPROPERTIES (
+ | 'primary-key' = 'id',
+ | 'bucket' = '1',
+ | 'deletion-vectors.enabled' = 'true',
+ | 'pk-multivalue.index.columns' = 'tags'
+ |)
+ |""".stripMargin)
+ spark.sql("""
+ |INSERT INTO t VALUES
+ | (1, array('red', 'blue'), 'red'),
+ | (2, array('green'), 'red'),
+ | (3, array('red'), 'blue')
+ |""".stripMargin)
+ spark.sql("""
+ |INSERT INTO t VALUES
+ | (4, array('yellow'), 'yellow'),
+ | (5, array(CAST(NULL AS STRING)), 'red'),
+ | (6, CAST(NULL AS ARRAY<STRING>), 'red')
+ |""".stripMargin)
+ spark.sql("CALL sys.compact(table => 't')")
+
+ val sourceIndexes =
loadTable("t").store.newIndexFileHandler.scanEntries.asScala
+ .map(_.indexFile)
+ .filter(meta => meta.globalIndexMeta != null &&
meta.globalIndexMeta.sourceMeta != null)
+ assert(sourceIndexes.map(_.indexType).toSet == Set("multivalue"))
+
+ val predicateBuilder = new PredicateBuilder(loadTable("t").rowType())
+ val red = BinaryString.fromString("red")
+ val blue = BinaryString.fromString("blue")
+ val green = BinaryString.fromString("green")
+
+ val containsQuery = "SELECT id FROM t WHERE array_contains(tags, 'red')"
+ val containsScan = getPaimonScan(containsQuery)
+
assert(containsScan.pushedDataFilters.contains(predicateBuilder.arrayContains(1,
red)))
+ assert(containsScan.inputSplits.exists(_.isInstanceOf[IndexedSplit]))
+ checkAnswer(spark.sql(containsQuery), Seq(Row(1), Row(3)))
+
+ val overlapsQuery =
+ "SELECT id FROM t WHERE arrays_overlap(tags, array('blue', 'green'))"
+ val overlapsScan = getPaimonScan(overlapsQuery)
+ assert(
+ overlapsScan.pushedDataFilters.contains(
+ predicateBuilder.arraysOverlap(1, Seq(blue, green).asJava)))
+ assert(overlapsScan.inputSplits.exists(_.isInstanceOf[IndexedSplit]))
+ checkAnswer(spark.sql(overlapsQuery), Seq(Row(1), Row(2)))
+
+ val dynamicQuery = "SELECT id FROM t WHERE array_contains(tags, probe)"
+ val dynamicScan = getPaimonScan(dynamicQuery)
+ assert(!dynamicScan.pushedDataFilters.exists {
+ case leaf: LeafPredicate =>
+ leaf.function() == ArrayContains.INSTANCE ||
+ leaf.function() == ArraysOverlap.INSTANCE ||
+ leaf.function() == ArrayContainsAll.INSTANCE
+ case _ => false
+ })
+ assert(!dynamicScan.inputSplits.exists(_.isInstanceOf[IndexedSplit]))
+ checkAnswer(spark.sql(dynamicQuery), Seq(Row(1), Row(4)))
+ }
+ }
+
test("postpone bucket builds and applies sorted indexes during compact") {
withTable("t") {
spark.sql("""