Skip to content

Commit bcd086b

Browse files
Sigma-Maslfan1989
andauthored
[AURON #2511] Prune full-data-file Iceberg changelog delete tasks (#2523)
# Which issue does this PR close? Closes #2511 # Rationale for this change Native Iceberg changelog scans retain full-data-file DELETE tasks even when supported metadata predicates exclude them. Task pruning currently evaluates these predicates only for INSERT tasks. # What changes are included in this PR? - Apply the existing metadata predicate evaluator to all supported native changelog data-file tasks, including DELETE tasks with no existing delete files. - Add coverage for mixed INSERT/DELETE ranges, snapshot and ordinal predicates, and empty scans. - Verify explicit expected rows against Spark execution and assert retained scan task counts and file metrics. - Preserve task eligibility checks, post-scan filters, and operator boundaries. The existing changelog-view test continues to retain its task because its filter cannot safely reach the scan. # Are there any user-facing changes? Eligible native Iceberg changelog scans can read fewer DELETE tasks. No public API or configuration changes. # How was this patch tested? Passed locally on Spark 3.5.8 / Iceberg 1.10.1 / JDK 17: - Module compilation and Spotless check. - Three focused tests covering full-data-file DELETE scans, metadata pruning, and mixed INSERT/DELETE scans. - `./dev/reformat --check`. - Full `AuronIcebergIntegrationSuite` on Spark 3.4, 3.5, and 4.0. # Was this patch authored or co-authored using generative AI tooling? - [x] Yes - [ ] No Generated-by: OpenAI Codex (GPT-6) Co-authored-by: Shilun Fan <slfan1989@apache.org> Signed-off-by: Shilun Fan <slfan1989@apache.org>
1 parent e3d7361 commit bcd086b

2 files changed

Lines changed: 112 additions & 13 deletions

File tree

thirdparty/auron-iceberg/src/main/scala/org/apache/spark/sql/auron/iceberg/IcebergScanSupport.scala

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -618,17 +618,12 @@ object IcebergScanSupport extends Logging {
618618
changelogTaskPredicate(condition, partitionSchema)
619619
.map(predicate =>
620620
tasks.filter { task =>
621-
task.changelogTask match {
622-
case _: AddedRowsScanTask =>
623-
val values = metadataPartitionValues(
624-
task.file.location(),
625-
task.file.specId(),
626-
Some(task.changelogTask),
627-
partitionSchema)
628-
predicate(values)
629-
case _ =>
630-
true
631-
}
621+
val values = metadataPartitionValues(
622+
task.file.location(),
623+
task.file.specId(),
624+
Some(task.changelogTask),
625+
partitionSchema)
626+
predicate(values)
632627
})
633628
.getOrElse(tasks)
634629
}

thirdparty/auron-iceberg/src/test/scala/org/apache/auron/iceberg/AuronIcebergIntegrationSuite.scala

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ import java.util.concurrent.TimeUnit
2424
import scala.collection.JavaConverters._
2525
import scala.util.control.NonFatal
2626

27-
import org.apache.iceberg.{AddedRowsScanTask, ChangelogScanTask, FileFormat, FileScanTask, MetadataColumns, ScanTask}
27+
import org.apache.iceberg.{AddedRowsScanTask, ChangelogScanTask, DeletedDataFileScanTask, FileFormat, FileScanTask, MetadataColumns, ScanTask}
2828
import org.apache.iceberg.data.{GenericAppenderFactory, Record}
2929
import org.apache.iceberg.deletes.PositionDelete
3030
import org.apache.iceberg.spark.Spark3Util
3131
import org.apache.spark.scheduler.{SparkListener, SparkListenerEvent}
3232
import org.apache.spark.sql.{DataFrame, Row}
3333
import org.apache.spark.sql.auron.AuronColumnarOverrides
3434
import org.apache.spark.sql.auron.iceberg.{IcebergConvertProvider, IcebergScanSupport}
35-
import org.apache.spark.sql.catalyst.expressions.{And, Attribute, EqualTo, In, Literal, Not, Or}
35+
import org.apache.spark.sql.catalyst.CatalystTypeConverters
36+
import org.apache.spark.sql.catalyst.expressions.{And, Attribute, EqualTo, Expression, In, Literal, Not, Or}
3637
import org.apache.spark.sql.catalyst.trees.TreeNodeTag
3738
import org.apache.spark.sql.execution.{FilterExec, FormattedMode, SparkPlan}
3839
import org.apache.spark.sql.execution.ExplainUtils.collectFirst
@@ -818,12 +819,115 @@ class AuronIcebergIntegrationSuite
818819
withSQLConf("spark.auron.enable" -> "true", "spark.auron.enable.iceberg.scan" -> "true") {
819820
val df = sql(metadataFilterQuery)
820821
checkAnswer(df, Seq.empty)
822+
// The changelog view's row processing prevents pushing this filter down to the scan.
821823
assert(executedNativeIcebergTableScanExec(df).metrics("numFiles").value == 1L)
822824
}
823825
}
824826
}
825827
}
826828

829+
test(
830+
"iceberg native changelog scan prunes full-data-file delete tasks by metadata predicates") {
831+
val tableName = "local.db.t_changelog_delete_pruning"
832+
withTable(tableName) {
833+
sql(s"""
834+
|create table $tableName (id int, p int)
835+
|using iceberg
836+
|partitioned by (p)
837+
|tblproperties ('format-version' = '2')
838+
|""".stripMargin)
839+
sql(s"insert into $tableName values (1, 1), (2, 2)")
840+
val startSnapshotId = currentSnapshotId(tableName)
841+
sql(s"delete from $tableName where p = 1")
842+
val firstDeleteSnapshotId = currentSnapshotId(tableName)
843+
sql(s"insert into $tableName values (3, 3)")
844+
val insertSnapshotId = currentSnapshotId(tableName)
845+
sql(s"delete from $tableName where p = 2")
846+
val endSnapshotId = currentSnapshotId(tableName)
847+
848+
val scan = rawChangelogScan(tableName, startSnapshotId, endSnapshotId)
849+
val tasks = scan.scan.toBatch.planInputPartitions().toSeq.flatMap(icebergScanTasks)
850+
assert(tasks.size == 3)
851+
assert(tasks.count(_.isInstanceOf[AddedRowsScanTask]) == 1)
852+
val deleteTasks = tasks.collect { case task: DeletedDataFileScanTask => task }
853+
assert(deleteTasks.size == 2)
854+
assert(deleteTasks.forall(_.existingDeletes().isEmpty))
855+
856+
val firstDelete = Row(1, 1, "DELETE", 0, firstDeleteSnapshotId)
857+
val insert = Row(3, 3, "INSERT", 1, insertSnapshotId)
858+
val secondDelete = Row(2, 2, "DELETE", 2, endSnapshotId)
859+
val cases: Seq[(Map[String, Attribute] => Expression, Seq[Row])] = Seq(
860+
(
861+
attributes => EqualTo(attributes("_change_type"), Literal("DELETE")),
862+
Seq(firstDelete, secondDelete)),
863+
(attributes => EqualTo(attributes("_change_type"), Literal("INSERT")), Seq(insert)),
864+
(
865+
attributes => EqualTo(attributes("_commit_snapshot_id"), Literal(endSnapshotId)),
866+
Seq(secondDelete)),
867+
(
868+
attributes =>
869+
In(
870+
attributes("_commit_snapshot_id"),
871+
Seq(Literal(firstDeleteSnapshotId), Literal(insertSnapshotId))),
872+
Seq(firstDelete, insert)),
873+
(attributes => EqualTo(attributes("_change_ordinal"), Literal(0)), Seq(firstDelete)),
874+
(
875+
attributes => In(attributes("_change_ordinal"), Seq(Literal(1), Literal(2))),
876+
Seq(insert, secondDelete)),
877+
(
878+
attributes =>
879+
And(
880+
EqualTo(attributes("_change_type"), Literal("DELETE")),
881+
EqualTo(attributes("_commit_snapshot_id"), Literal(firstDeleteSnapshotId))),
882+
Seq(firstDelete)),
883+
(
884+
attributes => EqualTo(attributes("_commit_snapshot_id"), Literal(startSnapshotId)),
885+
Seq.empty),
886+
(
887+
attributes =>
888+
And(
889+
EqualTo(attributes("_change_type"), Literal("INSERT")),
890+
EqualTo(attributes("_change_ordinal"), Literal(0))),
891+
Seq.empty))
892+
893+
cases.foreach { case (condition, expected) =>
894+
def filteredScan: FilterExec = {
895+
val scan = rawChangelogScan(tableName, startSnapshotId, endSnapshotId)
896+
FilterExec(
897+
condition(scan.output.map(attribute => attribute.name -> attribute).toMap),
898+
scan)
899+
}
900+
901+
def checkRows(plan: SparkPlan): Unit = {
902+
val schema = plan.schema
903+
val rows = plan
904+
.execute()
905+
.mapPartitions { rows =>
906+
val toScala = CatalystTypeConverters.createToScalaConverter(schema)
907+
rows.map(row => toScala(row).asInstanceOf[Row])
908+
}
909+
.collect()
910+
.toSeq
911+
assert(rows.sortBy(_.getInt(0)) == expected.sortBy(_.getInt(0)), plan.treeString)
912+
}
913+
914+
withSQLConf("spark.auron.enable" -> "false") {
915+
checkRows(filteredScan)
916+
}
917+
withSQLConf("spark.auron.enable" -> "true", "spark.auron.enable.iceberg.scan" -> "true") {
918+
val transformed =
919+
AuronColumnarOverrides(spark).preColumnarTransitions.apply(filteredScan)
920+
val nativeScan = transformed.collectFirst { case native: NativeIcebergTableScanExec =>
921+
native
922+
}.get
923+
assert(nativeScan.staticPlan.scanTasks.size == expected.size)
924+
checkRows(transformed)
925+
assert(nativeScan.metrics("numFiles").value == expected.size)
926+
}
927+
}
928+
}
929+
}
930+
827931
test("iceberg native changelog scan remains correct in dynamic pruning join") {
828932
withTable("local.db.t_changelog_dpp", "local.db.t_changelog_dpp_dim") {
829933
withTempView("t_changelog_dpp_changes") {

0 commit comments

Comments
 (0)