Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e356283
[Spark] Row-level concurrency for concurrent DML via deletion-vector …
sezruby Jul 7, 2026
b64e025
[Spark] Narrow row-level concurrency to sound same-file DV union
sezruby Jul 30, 2026
55f07ab
[Spark] Harden row-level DV-merge concurrency tests + scrub internal …
sezruby Aug 3, 2026
538ef84
Extract row-level concurrency resolution into a self-typed trait
sezruby Aug 4, 2026
1898a55
[Spark] Skip added-file re-scan when no row-level conflict was reconc…
sezruby Aug 4, 2026
6a03f58
[Spark] Add MERGE matched-clause tests to RowLevelConcurrencySuite
sezruby Aug 5, 2026
ccc0f82
Fail safe on DV resolution errors; single-pass action indexing
sezruby Aug 8, 2026
64caa4c
[Spark] Parallelize row-level DV reconciliation across shared files
sezruby Aug 8, 2026
8af17fd
[Spark] Test: multi-file row-level DV reconcile exercises the paralle…
sezruby Aug 8, 2026
e9370cf
[Spark] Document N-way DV-merge correctness; add base-DV chain and Se…
sezruby Aug 13, 2026
0b8f492
[Spark] Simplify RLC integration: prune the winning summary instead o…
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
23a8c77
Only reconcile a shared file when the current txn's reads are unaffected
sezruby Oct 15, 2026
fc37596
Merge remote-tracking branch 'origin/master' into row-level-concurren…
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