Skip to content

Commit 4c2abaa

Browse files
[AURON #2434] Support full-data-file Iceberg changelog deletes (#2435)
**Which issue does this PR close?** Closes #2434 **Rationale for this change** Auron native Iceberg changelog scan currently supports insert-only changelog tasks. Full-data-file delete changelog tasks can be executed natively without row-level delete-file handling. Auron can scan the deleted data file directly and materialize changelog metadata from the Iceberg changelog task. Supporting this case improves native Iceberg changelog scan coverage while keeping more complex delete and update semantics on Spark's reader. **What changes are included in this PR?** Allows native Iceberg changelog scan to accept `DeletedDataFileScanTask` when: - the changelog operation is `DELETE` - `existingDeletes()` is empty Keeps existing native support for `AddedRowsScanTask` when: - the changelog operation is `INSERT` - `deletes()` is empty Keeps fallback behavior for unsupported changelog tasks, including row-level deletes, position/equality deletes, update changelog tasks, mixed file formats, and delete tasks with existing delete files. Adds coverage for: - full-data-file delete changelog native scan - a changelog range that contains both insert tasks and full-data-file delete tasks **Are there any user-facing changes?** No user-facing API changes. More Iceberg changelog scans can now be executed natively by Auron. **How was this patch tested?** UT. --------- Co-authored-by: Shilun Fan <slfan1989@apache.org> Signed-off-by: weimingdiit <weimingdiit@gmail.com>
1 parent 83f05f5 commit 4c2abaa

2 files changed

Lines changed: 100 additions & 36 deletions

File tree

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

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import scala.collection.JavaConverters._
2222
import scala.util.control.NonFatal
2323

2424
import org.apache.commons.lang3.reflect.MethodUtils
25-
import org.apache.iceberg.{AddedRowsScanTask, ChangelogOperation, ChangelogScanTask, FileFormat, FileScanTask, MetadataColumns, ScanTask}
25+
import org.apache.iceberg.{AddedRowsScanTask, ChangelogOperation, ChangelogScanTask, DataFile, DeletedDataFileScanTask, FileFormat, FileScanTask, MetadataColumns, ScanTask}
2626
import org.apache.iceberg.expressions.{And => IcebergAnd, BoundPredicate, Expression => IcebergExpression, Not => IcebergNot, Or => IcebergOr, UnboundPredicate}
2727
import org.apache.iceberg.spark.source.AuronIcebergSourceUtil
2828
import org.apache.spark.internal.Logging
@@ -267,22 +267,14 @@ object IcebergScanSupport extends Logging {
267267
return None
268268
}
269269

270-
val addedRowsTasks = changelogTasks.collect { case task: AddedRowsScanTask => task }
271-
// First native changelog support is insert-only. Delete and update images need Iceberg
272-
// delete-file handling, so keep them on Spark's reader for now.
273-
if (addedRowsTasks.size != changelogTasks.size) {
270+
// Native changelog scan can read data-file rows directly for insert tasks and
271+
// full-data-file delete tasks. Row-level deletes still need Iceberg delete-file handling.
272+
val nativeChangelogTasks = changelogTasks.flatMap(toNativeChangelogDataFileTask)
273+
if (nativeChangelogTasks.size != changelogTasks.size) {
274274
return None
275275
}
276276

277-
if (!addedRowsTasks.forall(_.operation() == ChangelogOperation.INSERT)) {
278-
return None
279-
}
280-
281-
if (!addedRowsTasks.forall(task => deletesEmpty(task.deletes()))) {
282-
return None
283-
}
284-
285-
val formats = addedRowsTasks.map(_.file().format()).distinct
277+
val formats = nativeChangelogTasks.map(_.file.format()).distinct
286278
if (formats.size > 1) {
287279
return None
288280
}
@@ -298,7 +290,7 @@ object IcebergScanSupport extends Logging {
298290
}
299291

300292
val pruningPredicates = collectPruningPredicates(scan.asInstanceOf[AnyRef], readSchema)
301-
val nativeTasks = addedRowsTasks.map(task => toNativeScanTask(task, partitionSchema))
293+
val nativeTasks = nativeChangelogTasks.map(task => toNativeScanTask(task, partitionSchema))
302294
Some(
303295
IcebergScanPlan(
304296
nativeTasks,
@@ -528,6 +520,12 @@ object IcebergScanSupport extends Logging {
528520

529521
private case class IcebergPartitionView(tasks: Seq[ScanTask])
530522

523+
private case class NativeChangelogDataFileTask(
524+
file: DataFile,
525+
start: Long,
526+
length: Long,
527+
changelogTask: ChangelogScanTask)
528+
531529
private def icebergPartition(partition: InputPartition): Option[IcebergPartitionView] = {
532530
val className = partition.getClass.getName
533531
// Only accept Iceberg SparkInputPartition to access task groups.
@@ -559,6 +557,21 @@ object IcebergScanSupport extends Logging {
559557
}
560558
}
561559

560+
private def toNativeChangelogDataFileTask(
561+
task: ChangelogScanTask): Option[NativeChangelogDataFileTask] = {
562+
task match {
563+
case added: AddedRowsScanTask
564+
if added.operation() == ChangelogOperation.INSERT &&
565+
deletesEmpty(added.deletes()) =>
566+
Some(NativeChangelogDataFileTask(added.file(), added.start(), added.length(), added))
567+
case deleted: DeletedDataFileScanTask if deletesEmpty(deleted.existingDeletes()) =>
568+
Some(
569+
NativeChangelogDataFileTask(deleted.file(), deleted.start(), deleted.length(), deleted))
570+
case _ =>
571+
None
572+
}
573+
}
574+
562575
private def toNativeScanTask(
563576
task: FileScanTask,
564577
partitionSchema: StructType): IcebergNativeScanTask = {
@@ -572,15 +585,18 @@ object IcebergScanSupport extends Logging {
572585
}
573586

574587
private def toNativeScanTask(
575-
task: AddedRowsScanTask,
588+
task: NativeChangelogDataFileTask,
576589
partitionSchema: StructType): IcebergNativeScanTask = {
577-
val file = task.file()
578590
IcebergNativeScanTask(
579-
file.location(),
580-
task.start(),
581-
task.length(),
582-
file.fileSizeInBytes(),
583-
metadataPartitionValues(file.location(), file.specId(), Some(task), partitionSchema))
591+
task.file.location(),
592+
task.start,
593+
task.length,
594+
task.file.fileSizeInBytes(),
595+
metadataPartitionValues(
596+
task.file.location(),
597+
task.file.specId(),
598+
Some(task.changelogTask),
599+
partitionSchema))
584600
}
585601

586602
private def metadataPartitionValues(

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

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,48 @@ class AuronIcebergIntegrationSuite
598598
}
599599
}
600600

601+
test("iceberg native scan supports full-data-file delete changelog scan") {
602+
withTable("local.db.t_changelog_full_file_delete") {
603+
withTempView("t_changelog_full_file_delete_changes") {
604+
sql("""
605+
|create table local.db.t_changelog_full_file_delete (id int, v string, p int)
606+
|using iceberg
607+
|partitioned by (p)
608+
|tblproperties ('format-version' = '2')
609+
|""".stripMargin)
610+
sql("""
611+
|insert into local.db.t_changelog_full_file_delete
612+
|values (1, 'a', 1), (2, 'b', 2)
613+
|""".stripMargin)
614+
val startSnapshotId = currentSnapshotId("local.db.t_changelog_full_file_delete")
615+
sql("delete from local.db.t_changelog_full_file_delete where p = 1")
616+
val endSnapshotId = currentSnapshotId("local.db.t_changelog_full_file_delete")
617+
createChangelogView(
618+
"local.db.t_changelog_full_file_delete",
619+
"t_changelog_full_file_delete_changes",
620+
startSnapshotId,
621+
endSnapshotId)
622+
623+
val query =
624+
"""
625+
|select id, v, p, _change_type
626+
|from t_changelog_full_file_delete_changes
627+
|order by id
628+
|""".stripMargin
629+
val expected = Seq(Row(1, "a", 1, "DELETE"))
630+
withSQLConf("spark.auron.enable" -> "false") {
631+
checkAnswer(sql(query), expected)
632+
}
633+
withSQLConf("spark.auron.enable" -> "true", "spark.auron.enable.iceberg.scan" -> "true") {
634+
val df = sql(query)
635+
checkAnswer(df, expected)
636+
val nativeScan = executedNativeIcebergTableScanExec(df)
637+
assert(nativeScan.staticPlan.scanTasks.size == 1)
638+
}
639+
}
640+
}
641+
}
642+
601643
test("iceberg native changelog scan remains correct in dynamic pruning join") {
602644
withTable("local.db.t_changelog_dpp", "local.db.t_changelog_dpp_dim") {
603645
withTempView("t_changelog_dpp_changes") {
@@ -701,39 +743,45 @@ class AuronIcebergIntegrationSuite
701743
}
702744
}
703745

704-
test("iceberg changelog scan falls back when delete changes exist") {
705-
withTable("local.db.t_changelog_delete") {
706-
withTempView("t_changelog_delete_changes") {
746+
test("iceberg native scan supports mixed insert and full-data-file delete changelog scan") {
747+
withTable("local.db.t_changelog_mixed_delete") {
748+
withTempView("t_changelog_mixed_delete_changes") {
707749
sql("""
708-
|create table local.db.t_changelog_delete (id int, v string)
750+
|create table local.db.t_changelog_mixed_delete (id int, v string, p int)
709751
|using iceberg
752+
|partitioned by (p)
710753
|tblproperties ('format-version' = '2')
711754
|""".stripMargin)
712-
sql("insert into local.db.t_changelog_delete values (1, 'a'), (2, 'b')")
713-
val startSnapshotId = currentSnapshotId("local.db.t_changelog_delete")
714-
sql("delete from local.db.t_changelog_delete where id = 1")
715-
val endSnapshotId = currentSnapshotId("local.db.t_changelog_delete")
755+
sql("""
756+
|insert into local.db.t_changelog_mixed_delete
757+
|values (1, 'a', 1), (2, 'b', 2)
758+
|""".stripMargin)
759+
val startSnapshotId = currentSnapshotId("local.db.t_changelog_mixed_delete")
760+
sql("delete from local.db.t_changelog_mixed_delete where p = 1")
761+
sql("insert into local.db.t_changelog_mixed_delete values (3, 'c', 3)")
762+
val endSnapshotId = currentSnapshotId("local.db.t_changelog_mixed_delete")
716763
createChangelogView(
717-
"local.db.t_changelog_delete",
718-
"t_changelog_delete_changes",
764+
"local.db.t_changelog_mixed_delete",
765+
"t_changelog_mixed_delete_changes",
719766
startSnapshotId,
720767
endSnapshotId)
721768

722769
val query =
723770
"""
724-
|select id, v, _change_type, _change_ordinal, _commit_snapshot_id
725-
|from t_changelog_delete_changes
771+
|select id, v, p, _change_type, _change_ordinal, _commit_snapshot_id
772+
|from t_changelog_mixed_delete_changes
726773
|order by id, _change_type
727774
|""".stripMargin
728775
var expected: Seq[Row] = Nil
729776
withSQLConf("spark.auron.enable" -> "false") {
730777
expected = sql(query).collect().toSeq
731778
}
779+
assert(expected.exists(row => row.getString(3) == "DELETE"))
780+
assert(expected.exists(row => row.getString(3) == "INSERT"))
732781
withSQLConf("spark.auron.enable" -> "true", "spark.auron.enable.iceberg.scan" -> "true") {
733782
val df = sql(query)
734783
checkAnswer(df, expected)
735-
val plan = df.queryExecution.executedPlan.toString()
736-
assert(!plan.contains("NativeIcebergTableScan"))
784+
executedNativeIcebergTableScanExec(df)
737785
}
738786
}
739787
}

0 commit comments

Comments
 (0)