Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a2ad862
[Spark] Reader-side conflict-time data skipping to reduce unnecessary…
sezruby Jul 8, 2026
f6b092f
Guard conflict-time data skipping on non-empty read predicates
sezruby Aug 3, 2026
e2f12cb
Address review: single-job multi-read skipping + eval fallback
sezruby Aug 8, 2026
e4ac0da
Address review: isolate conflict-time skipping in a self-typed trait
sezruby Aug 13, 2026
0e1c865
[Spark] Preserve KernelContext when wrapping Kernel snapshots (#7818)
seewishnew Oct 10, 2026
891d385
[Spark] Serialize AMT checkpoints as action arrays (#7824)
yumingxuanguo-db Oct 12, 2026
dc219cd
[Spark] Generate snapshot_id and modified_snapshot_id for AMT data en…
PhilPlato Oct 13, 2026
72b5635
[Spark] Avoid file planning for unfiltered Delta V2 size statistics (…
murali-db Oct 13, 2026
a8c02bc
[SPARK] Block non determinisitic predicate pushdown on cdf nodes (#7847)
Omar-Saleh Oct 13, 2026
c327335
[Spark] Support object identity in DV uniqueFileId and handle unencod…
geraintcjy Oct 13, 2026
26ca1b9
[PROTOCOL] Use Mumbling bitmap encoding for manifest deletion vectors…
anoopj Oct 13, 2026
79af89a
[Spark] Back AMT manifest deletion vectors with the Mumbling bitmap f…
prakharjain09 Oct 13, 2026
ab0668b
[Spark] Convert DeltaV2Table from Java to Scala (#7856)
HaoSun1993 Oct 13, 2026
a81a187
[Spark] Align AMT manifest_info field IDs with the Iceberg spec and d…
amogh-jahagirdar Oct 13, 2026
f1a1e1b
[Spark] Log streaming source events through snapshots (#7859)
PorridgeSwim Oct 14, 2026
589c125
[Spark] Decode CDC actions from Kernel commit batches (#7857)
HaoSun1993 Oct 14, 2026
85ad0c3
[Spark] Block CDF batch reads across incompatible partition changes (…
johanl-db Oct 14, 2026
05d41c9
[Spark] [DSv2 Connector] Convert TableNotFoundException to Scala (#7860)
raveeram-db Oct 14, 2026
4e0d160
[Spark] Foundation for Concurrent Identity Columns: metadata key, err…
Rob2U Oct 14, 2026
b573e00
[Spark] Refactor DeleteSuiteBase: replace SharedSparkSession with Del…
fwc Oct 14, 2026
91fd083
[Spark] Preserve numeric precision when widening file statistics (#7854)
zhipengmao-db Oct 14, 2026
6256b6a
[Spark] Refactor DVTestUtils: replace SharedSparkSession with DeltaSQ…
fwc Oct 14, 2026
42777ea
[Spark] Block users from setting ICT enablement table properties (#7865)
dhruvarya-db Oct 14, 2026
b315fbe
[Spark][Fix] Keep partition column names as single-part references in…
hariuserx Oct 15, 2026
f2eff2a
[Spark] Add AMT variants of the DELETE test suites (#7849)
geraintcjy Oct 15, 2026
b1c7893
[Spark] Use DELTA_LIMIT_PUSHDOWN_ENABLED in the V2 scan builder (#7870)
hariuserx Oct 15, 2026
56f12d4
Address review: pair each read's data skipping with its own partition…
sezruby Oct 15, 2026
c95239c
Merge remote-tracking branch 'origin/master' into conflict-data-skipping
sezruby Oct 15, 2026
536ae8a
Test the conflict-time data skipping evaluation fallback
sezruby Oct 15, 2026
9f7735f
Address review: batch conflict-time skipping, skip toDF when unusable…
sezruby Oct 15, 2026
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
2 changes: 1 addition & 1 deletion protocol_rfcs/iceberg-v4-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ Summary information for `content_type` = DATA_MANIFEST entries. Includes file/ro

### Manifest Deletion Vectors (MDVs)

MDVs track which entries in a leaf manifest have been deleted or replaced. The `dv` field in `manifest_info` contains a Roaring bitmap of all invalidated positions (cumulative, used for reading). The `deleted_positions` and `replaced_positions` fields in `tracking` represent what changed in the current commit only (used for CDF):
MDVs track which entries in a leaf manifest have been deleted or replaced. The `dv` field in `manifest_info` contains all invalidated positions (cumulative, used for reading). The `deleted_positions` and `replaced_positions` fields in `tracking` represent what changed in the current commit only (used for CDF). All three bitmaps are encoded using the [Mumbling bitmap](https://iceberg.apache.org/mumbling-spec/) spec:

- **`manifest_info.dv`**: Cumulative set of all invalidated positions. Grows
monotonically. Used by readers to skip entries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@
*/
package io.delta.spark.internal.v2;

import io.delta.kernel.defaults.engine.DefaultEngine;
import io.delta.kernel.engine.Engine;
import io.delta.spark.internal.v2.kernel.KernelContext;
import io.delta.spark.internal.v2.utils.ScalaUtils;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.delta.storage.LogStore;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;

public abstract class DeltaV2TestBase {

protected static SparkSession spark;
protected static Engine defaultEngine;
protected static KernelContext defaultKernelContext;

@BeforeAll
public static void setUpSparkAndEngine() {
Expand All @@ -39,7 +45,18 @@ public static void setUpSparkAndEngine() {
.config("spark.sql.catalog.dsv2", "io.delta.spark.internal.v2.catalog.TestCatalog")
.config("spark.sql.catalog.dsv2.base_path", System.getProperty("java.io.tmpdir"))
.getOrCreate();
defaultEngine = DefaultEngine.create(spark.sessionState().newHadoopConf());
defaultKernelContext = createKernelContext();
defaultEngine = defaultKernelContext.getDefaultEngine();
}

protected static KernelContext createKernelContext() {
return createKernelContext(spark.sessionState().newHadoopConf());
}

protected static KernelContext createKernelContext(Configuration hadoopConf) {
Map<String, String> options = new HashMap<>();
hadoopConf.forEach(entry -> options.put(entry.getKey(), entry.getValue()));
return new KernelContext(ScalaUtils.toScalaMap(options), LogStore.apply(spark));
}

@AfterAll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private java.sql.Timestamp commitTimestamp(String tableLocation, long version) {
// Reads through the kernel snapshot manager, so it works inside the STRICT block the test body
// runs in. tableLocation is resolved by withHistoryTable before STRICT is set.
DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tableLocation, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tableLocation, defaultKernelContext, Optional.empty());
long millis = snapshotManager.loadSnapshotAt(version).timestamp();
return new java.sql.Timestamp(millis);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void testDirectBatchExecutionWithExplicitExpectedRows(boolean enableDvs)
spark.sql(String.format("DELETE FROM %s WHERE id = 1", tableName));

DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tablePath, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tablePath, defaultKernelContext, Optional.empty());
StructType dataSchema = spark.table(tableName).schema();
long latestVersion = snapshotManager.loadLatestSnapshot().version();
Map<Long, Long> commitTimestampsMicros = loadCommitTimestampsMicros(tableName);
Expand Down Expand Up @@ -149,7 +149,7 @@ public void testRowTrackingMetadataReferences() throws Exception {
spark.sql(String.format("INSERT INTO %s VALUES (1)", tableName));

DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tablePath, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tablePath, defaultKernelContext, Optional.empty());
StructType dataSchema = spark.table(tableName).schema();
long latestVersion = snapshotManager.loadLatestSnapshot().version();

Expand Down Expand Up @@ -200,7 +200,7 @@ public void testUpdateProducesPairedDeleteAndInsert(boolean enableDvs) throws Ex
spark.sql(String.format("UPDATE %s SET name = 'AliceX' WHERE id = 1", tableName));

DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tablePath, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tablePath, defaultKernelContext, Optional.empty());
StructType dataSchema = spark.table(tableName).schema();
long latestVersion = snapshotManager.loadLatestSnapshot().version();
Map<Long, Long> commitTimestampsMicros = loadCommitTimestampsMicros(tableName);
Expand Down Expand Up @@ -273,7 +273,7 @@ public void testRangeSlicingNonZeroStart() throws Exception {
spark.sql(String.format("INSERT INTO %s VALUES (3, 'Charlie')", tableName));

DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tablePath, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tablePath, defaultKernelContext, Optional.empty());
StructType dataSchema = spark.table(tableName).schema();
Map<Long, Long> commitTimestampsMicros = loadCommitTimestampsMicros(tableName);

Expand Down Expand Up @@ -342,7 +342,7 @@ public void testChangelogWithReservedCharsInPath() throws Exception {
spark.sql(String.format("INSERT INTO %s VALUES (1, 'Alice'), (2, 'Bob')", tableName));

DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tablePath, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tablePath, defaultKernelContext, Optional.empty());
long latestVersion = snapshotManager.loadLatestSnapshot().version();
Map<Long, Long> commitTimestampsMicros = loadCommitTimestampsMicros(tableName);

Expand Down Expand Up @@ -407,7 +407,7 @@ public void testChangelogWithReservedCharsInDataFileNames() throws Exception {
spark.sql(String.format("INSERT INTO %s VALUES (1, 'Alice'), (2, 'Bob')", tableName));

DeltaV2SnapshotManager snapshotManager =
SnapshotManagerFactory.create(tablePath, defaultEngine, Optional.empty());
SnapshotManagerFactory.create(tablePath, defaultKernelContext, Optional.empty());
long latestVersion = snapshotManager.loadLatestSnapshot().version();
Map<Long, Long> commitTimestampsMicros = loadCommitTimestampsMicros(tableName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.delta.spark.internal.v2.read.changelog;

import io.delta.kernel.defaults.engine.DefaultEngine;
import io.delta.spark.internal.v2.DeltaV2TestBase;
import org.apache.spark.sql.SparkSession;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -57,6 +56,7 @@ public static void setUpChangelogSparkAndEngine() {
.config("spark.sql.catalog.dsv2.base_path", System.getProperty("java.io.tmpdir"))
.config("spark.databricks.delta.changelogV2.enabled", "true")
.getOrCreate();
defaultEngine = DefaultEngine.create(spark.sessionState().newHadoopConf());
defaultKernelContext = createKernelContext();
defaultEngine = defaultKernelContext.getDefaultEngine();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.apache.spark.sql.delta.sources.{DeltaSourceMetadataTrackingLog, Delta
import org.apache.spark.sql.delta.test.DeltaSQLCommandTest
import org.apache.spark.sql.util.CaseInsensitiveStringMap
import io.delta.spark.internal.v2.catalog.DeltaV2Table
import io.delta.spark.internal.v2.kernel.KernelContext
import io.delta.spark.internal.v2.snapshot.PathBasedSnapshotManager
import io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient

Expand Down Expand Up @@ -270,7 +271,7 @@ class ApplyV2ReadOptionsSuite extends DeltaSQLCommandTest {
private def seedSchemaLogWithExtraColumn(tablePath: String, schemaLogPath: String): Unit = {
val deltaLog = DeltaLog.forTable(spark, tablePath)
val snapshotManager =
new PathBasedSnapshotManager(tablePath, deltaLog.newDeltaHadoopConf())
new PathBasedSnapshotManager(tablePath, new KernelContext(deltaLog.options, deltaLog.store))
val tableId = snapshotManager.loadLatestSnapshot.metadata.getId
val trackingLog = DeltaSourceMetadataTrackingLog.create(
spark, schemaLogPath, tableId, tablePath, parameters = Map.empty[String, String])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,29 @@ private[read] trait DeltaV2LimitPushdownE2ETests {
}
}

test("disabling Delta limit pushdown keeps LIMIT off the scan") {
val table = "v2_scan_e2e_limit_pushdown_disabled"
withTable(table) {
withV1Mode {
sql(s"CREATE TABLE $table (id INT) USING $tableProvider")
// One row per file so that a pushed limit would select fewer files than the table has.
(0 until 5).foreach { id =>
sql(s"INSERT INTO $table VALUES ($id)")
}
}

withSQLConf(DeltaSQLConf.DELTA_LIMIT_PUSHDOWN_ENABLED.key -> "false") {
val df = sql(s"SELECT * FROM $table LIMIT 2")
assert(df.collect().length == 2, "LIMIT 2 should return exactly 2 rows")
assertExpectedNoPushedLimit(df, "LIMIT with Delta limit pushdown disabled")
assertRouteSpecificInputFileCount(
df,
expected = 5,
context = "LIMIT with Delta limit pushdown disabled reads every file")
}
}
}

test("a non-deterministic filter prevents LIMIT pushdown") {
val table = "v2_scan_e2e_limit_nondeterministic_filter"
withTable(table) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,28 @@ private[read] trait DeltaV2ReadE2ETests {
}
}

test("partition names with spaces and backticks under column mapping prune on V2") {
val table = "v2_scan_e2e_spaced_partitions"
withTable(table) {
withV1Mode {
sql(
s"CREATE TABLE $table (id LONG, `order date` STRING, `a``b` STRING) " +
s"USING $tableProvider PARTITIONED BY (`order date`, `a``b`) " +
"TBLPROPERTIES ('delta.columnMapping.mode' = 'name')")
Seq("(1, 'd1', 'x')", "(2, 'd2', 'x')", "(3, 'd1', 'y')").foreach { values =>
sql(s"INSERT INTO $table VALUES $values")
}
}
// Spark 4.0 keeps the partition filter above the scan, so the scan reads all 3 files.
checkPartitionPrunedReadMatchesV1(
s"SELECT id, `order date`, `a``b` FROM $table " +
"WHERE `order date` = 'd1' AND `a``b` = 'y'",
expectedRows = Seq(Row(3L, "d1", "y")),
expectedSelectedFiles = if (spark.version.startsWith("4.0")) 3 else 1,
context = "space and backtick partition-column filter")
}
}

test("deletion vectors with a middle partition column preserve rows and DDL order") {
val table = "v2_scan_e2e_dv_partition_middle"
withTable(table) {
Expand Down Expand Up @@ -422,6 +444,25 @@ private[read] trait DeltaV2ReadE2ETests {
}
}
}

private def checkPartitionPrunedReadMatchesV1(
query: String,
expectedRows: Seq[Row],
expectedSelectedFiles: Int,
context: String): Unit = {
withV1Mode {
checkAnswer(sql(query), expectedRows)
}
checkRead(context)(sql(query)) { df =>
checkAnswer(df, expectedRows)
}
withDeltaV2ScanForSelectedFiles(sql(query), context) { scan =>
val numSelectedFiles = scan.preparedScan().files.size
assert(
numSelectedFiles == expectedSelectedFiles,
s"expected $expectedSelectedFiles selected files for $context, got $numSelectedFiles")
}
}
}

/** Covers read, schema, column-mapping, and deletion-vector semantics shared across Delta V2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import java.nio.file.{Files, StandardCopyOption}

import org.apache.spark.sql.delta.{DeltaColumnMapping, DeltaOperations, OptimisticTransaction, OptimisticTransactionSuite}
import org.apache.spark.sql.delta.actions.{AddFile, Metadata, Protocol, SetTransaction}
import org.apache.spark.sql.delta.storage.LogStore
import org.apache.spark.sql.delta.test.V2ForceTest
import io.delta.spark.internal.v2.kernel.KernelEngineFactory
import io.delta.spark.internal.v2.kernel.KernelContext
import org.apache.hadoop.fs.Path
import io.delta.kernel.{Table => KernelTable}
import io.delta.kernel.internal.{SnapshotImpl => KernelSnapshotImpl}
Expand All @@ -48,15 +49,14 @@ class DeltaV2OptimisticTransactionSuite
* inherited V1 conflict test cases.
*/
override protected def startTestTransaction(dataPath: Path): OptimisticTransaction = {
// scalastyle:off deltahadoopconfiguration
val kernelEngine = KernelEngineFactory.createDefaultEngine(spark.sessionState.newHadoopConf())
// scalastyle:on deltahadoopconfiguration
val kernelContext = new KernelContext(Map.empty, LogStore(spark))
val kernelEngine = kernelContext.getDefaultEngine()
val kernelSnap = KernelTable
.forPath(kernelEngine, dataPath.toString)
.getLatestSnapshot(kernelEngine)
.asInstanceOf[KernelSnapshotImpl]
val deltaV2Snapshot = new DeltaV2Snapshot(kernelSnap)
new DeltaV2OptimisticTransaction(catalogTable = None, deltaV2Snapshot, kernelEngine)
val deltaV2Snapshot = new DeltaV2Snapshot(kernelSnap, kernelContext)
new DeltaV2OptimisticTransaction(catalogTable = None, deltaV2Snapshot, kernelContext)
}

override protected def testDefaultMetadata(): Metadata =
Expand Down Expand Up @@ -93,27 +93,24 @@ class DeltaV2OptimisticTransactionSuite

/** Builds a Kernel-backed transaction over the latest snapshot of the table at `dir`. */
private def startKernelTxn(dir: File): DeltaV2OptimisticTransaction = {
// scalastyle:off deltahadoopconfiguration
// No DeltaLog here (the snapshot is loaded via Kernel), so use the session Hadoop conf.
val kernelEngine = KernelEngineFactory.createDefaultEngine(spark.sessionState.newHadoopConf())
// scalastyle:on deltahadoopconfiguration
val kernelContext = new KernelContext(Map.empty, LogStore(spark))
val kernelEngine = kernelContext.getDefaultEngine()
val kernelSnap = KernelTable
.forPath(kernelEngine, dir.getCanonicalPath)
.getLatestSnapshot(kernelEngine)
.asInstanceOf[KernelSnapshotImpl]
val deltaV2Snapshot = new DeltaV2Snapshot(kernelSnap)
new DeltaV2OptimisticTransaction(catalogTable = None, deltaV2Snapshot, kernelEngine)
val deltaV2Snapshot = new DeltaV2Snapshot(kernelSnap, kernelContext)
new DeltaV2OptimisticTransaction(catalogTable = None, deltaV2Snapshot, kernelContext)
}

private def latestKernelSnapshot(dir: File): DeltaV2Snapshot = {
// scalastyle:off deltahadoopconfiguration
val kernelEngine = KernelEngineFactory.createDefaultEngine(spark.sessionState.newHadoopConf())
// scalastyle:on deltahadoopconfiguration
val kernelContext = new KernelContext(Map.empty, LogStore(spark))
val kernelEngine = kernelContext.getDefaultEngine()
val kernelSnap = KernelTable
.forPath(kernelEngine, dir.getCanonicalPath)
.getLatestSnapshot(kernelEngine)
.asInstanceOf[KernelSnapshotImpl]
new DeltaV2Snapshot(kernelSnap)
new DeltaV2Snapshot(kernelSnap, kernelContext)
}

/** Seeds a simple (unpartitioned) V1 Delta table at `dir`. */
Expand Down
Loading