diff --git a/build.sbt b/build.sbt index 357d34fb40d..85dd64a19c8 100644 --- a/build.sbt +++ b/build.sbt @@ -462,6 +462,7 @@ lazy val sparkV1Filtered = (project in file("spark-v1-filtered")) lazy val sparkV2 = (project in file("kernel-spark")) .dependsOn(sparkV1Filtered) .dependsOn(kernelDefaults) + .dependsOn(kernelUnityCatalog % "compile->compile;test->test") .dependsOn(goldenTables % "test") .settings( name := "delta-spark-v2", diff --git a/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/CatalogManagedSnapshotManager.java b/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/CatalogManagedSnapshotManager.java new file mode 100644 index 00000000000..b115a2fecb6 --- /dev/null +++ b/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/CatalogManagedSnapshotManager.java @@ -0,0 +1,196 @@ +/* + * Copyright (2025) The Delta Lake Project Authors. + * + * Licensed 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 io.delta.kernel.spark.snapshot; + +import static io.delta.kernel.internal.util.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.delta.kernel.CommitRange; +import io.delta.kernel.Snapshot; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.internal.DeltaHistoryManager; +import io.delta.kernel.internal.SnapshotImpl; +import io.delta.kernel.internal.files.ParsedCatalogCommitData; +import io.delta.kernel.spark.exception.VersionNotFoundException; +import java.util.List; +import java.util.Optional; +import org.apache.hadoop.conf.Configuration; +import org.apache.spark.annotation.Experimental; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of DeltaSnapshotManager for catalog-managed tables (e.g., UC). + * + *
This snapshot manager is agnostic to the underlying catalog implementation. It delegates to a + * {@link ManagedCatalogAdapter}, keeping catalog-specific wiring out of the manager itself. + */ +@Experimental +public class CatalogManagedSnapshotManager implements DeltaSnapshotManager, AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(CatalogManagedSnapshotManager.class); + + private final ManagedCatalogAdapter catalogAdapter; + private final String tableId; + private final String tablePath; + private final Engine kernelEngine; + + public CatalogManagedSnapshotManager( + ManagedCatalogAdapter catalogAdapter, + String tableId, + String tablePath, + Configuration hadoopConf) { + this.catalogAdapter = requireNonNull(catalogAdapter, "catalogAdapter is null"); + this.tableId = requireNonNull(tableId, "tableId is null"); + this.tablePath = requireNonNull(tablePath, "tablePath is null"); + requireNonNull(hadoopConf, "hadoopConf is null"); + + this.kernelEngine = DefaultEngine.create(hadoopConf); + logger.info( + "Created CatalogManagedSnapshotManager for table {} at path {}", tableId, tablePath); + } + + /** Loads the latest snapshot of the catalog-managed Delta table. */ + @Override + public Snapshot loadLatestSnapshot() { + return catalogAdapter.loadSnapshot( + kernelEngine, /* versionOpt = */ Optional.empty(), /* timestampOpt = */ Optional.empty()); + } + + /** + * Loads a specific version of the Unity Catalog managed Delta table. + * + * @param version the version to load (must be >= 0) + * @return the snapshot at the specified version + */ + @Override + public Snapshot loadSnapshotAt(long version) { + checkArgument(version >= 0, "version must be non-negative"); + return catalogAdapter.loadSnapshot( + kernelEngine, Optional.of(version), /* timestampOpt = */ Optional.empty()); + } + + /** + * Finds the active commit at a specific timestamp. + * + *
For catalog-managed tables, this method retrieves ratified commits from the catalog and uses
+ * {@link DeltaHistoryManager#getActiveCommitAtTimestamp} to find the commit that was active at
+ * the specified timestamp.
+ *
+ * @param timestampMillis the timestamp in milliseconds since epoch (UTC)
+ * @param canReturnLastCommit if true, returns the last commit if the timestamp is after all
+ * commits; if false, throws an exception
+ * @param mustBeRecreatable if true, only considers commits that can be fully recreated from
+ * available log files; if false, considers all commits
+ * @param canReturnEarliestCommit if true, returns the earliest commit if the timestamp is before
+ * all commits; if false, throws an exception
+ * @return the commit that was active at the specified timestamp
+ */
+ @Override
+ public DeltaHistoryManager.Commit getActiveCommitAtTime(
+ long timestampMillis,
+ boolean canReturnLastCommit,
+ boolean mustBeRecreatable,
+ boolean canReturnEarliestCommit) {
+ // Load the latest snapshot for timestamp resolution
+ SnapshotImpl latestSnapshot = (SnapshotImpl) loadLatestSnapshot();
+
+ // Extract catalog commits from the snapshot's log segment (avoids redundant UC call)
+ List For catalog-managed tables, versions are assumed to be contiguous (enforced by the catalog
+ * coordinator). This method performs a lightweight check by verifying the version is within the
+ * valid range [0, latestRatifiedVersion].
+ *
+ * This approach is consistent with the existing Spark Delta behavior in {@code
+ * DeltaHistoryManager.checkVersionExists} which also assumes contiguous commits.
+ *
+ * @param version the version to check
+ * @param mustBeRecreatable if true, requires that the version can be fully recreated from
+ * available log files. For catalog-managed tables, all versions are recreatable since the
+ * catalog maintains the complete commit history.
+ * @param allowOutOfRange if true, allows versions greater than the latest version without
+ * throwing an exception; if false, throws exception for out-of-range versions
+ * @throws VersionNotFoundException if the version is not available
+ */
+ @Override
+ public void checkVersionExists(long version, boolean mustBeRecreatable, boolean allowOutOfRange)
+ throws VersionNotFoundException {
+ checkArgument(version >= 0, "version must be non-negative");
+
+ // For catalog-managed tables, the earliest recreatable version is 0 since the catalog
+ // maintains the complete commit history
+ long earliestVersion = 0;
+ long latestVersion = catalogAdapter.getLatestRatifiedVersion();
+
+ if (version < earliestVersion || ((version > latestVersion) && !allowOutOfRange)) {
+ throw new VersionNotFoundException(version, earliestVersion, latestVersion);
+ }
+ }
+
+ /**
+ * Gets a range of table changes between versions.
+ *
+ * Note: This operation delegates to the managed commit client.
+ *
+ * @throws UnsupportedOperationException if not yet implemented for catalog-managed tables
+ */
+ @Override
+ public CommitRange getTableChanges(Engine engine, long startVersion, Optional This method should be called when the snapshot manager is no longer needed. Prefer using
+ * try-with-resources to ensure proper cleanup.
+ */
+ @Override
+ public void close() {
+ try {
+ catalogAdapter.close();
+ logger.info("Closed CatalogManagedSnapshotManager for table {}", tableId);
+ } catch (Exception e) {
+ logger.warn("Error closing catalog-managed client for table {}", tableId, e);
+ }
+ }
+}
diff --git a/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/DeltaSnapshotManagerFactory.java b/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/DeltaSnapshotManagerFactory.java
new file mode 100644
index 00000000000..4b6033681aa
--- /dev/null
+++ b/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/DeltaSnapshotManagerFactory.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (2025) The Delta Lake Project Authors.
+ *
+ * Licensed 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 io.delta.kernel.spark.snapshot;
+
+import static java.util.Objects.requireNonNull;
+
+import io.delta.kernel.spark.snapshot.unitycatalog.UnityCatalogAdapter;
+import java.util.Optional;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.spark.annotation.Experimental;
+import org.apache.spark.sql.SparkSession;
+import org.apache.spark.sql.catalyst.catalog.CatalogTable;
+
+/**
+ * Factory for creating {@link DeltaSnapshotManager} instances.
+ *
+ * This factory provides two creation methods:
+ *
+ * Example usage:
+ *
+ * Use this when no catalog metadata is available or when you want to work directly with a
+ * filesystem path.
+ *
+ * @param tablePath filesystem path to the Delta table root
+ * @param hadoopConf Hadoop configuration for the Delta Kernel engine
+ * @return PathBasedSnapshotManager instance
+ * @throws NullPointerException if tablePath or hadoopConf is null
+ */
+ public static DeltaSnapshotManager fromPath(String tablePath, Configuration hadoopConf) {
+ requireNonNull(tablePath, "tablePath is null");
+ requireNonNull(hadoopConf, "hadoopConf is null");
+ return new PathBasedSnapshotManager(tablePath, hadoopConf);
+ }
+
+ /**
+ * Creates a snapshot manager from catalog table metadata.
+ *
+ * Automatically selects {@link CatalogManagedSnapshotManager} for Unity Catalog managed
+ * tables, or falls back to {@link PathBasedSnapshotManager} for regular tables.
+ *
+ * @param catalogTable Spark catalog table metadata
+ * @param spark SparkSession for resolving Unity Catalog configurations
+ * @param hadoopConf Hadoop configuration for the Delta Kernel engine
+ * @return appropriate snapshot manager implementation
+ * @throws NullPointerException if catalogTable, spark, or hadoopConf is null
+ * @throws IllegalArgumentException if catalogTable is UC-managed but configuration is invalid
+ */
+ public static DeltaSnapshotManager fromCatalogTable(
+ CatalogTable catalogTable, SparkSession spark, Configuration hadoopConf) {
+ requireNonNull(catalogTable, "catalogTable is null");
+ requireNonNull(spark, "spark is null");
+ requireNonNull(hadoopConf, "hadoopConf is null");
+
+ Optional The returned list contains {@link ParsedLogData} representing each ratified commit, sorted
+ * by version in ascending order. These are typically {@code ParsedCatalogCommitData} instances
+ * for catalog-managed tables.
+ *
+ * @param endVersionOpt optional end version (inclusive); if empty, returns commits up to latest
+ * @return list of parsed log data representing ratified commits, sorted by version ascending
+ */
+ List For catalog-managed tables, this is the highest version that has been ratified by the
+ * catalog coordinator.
+ *
+ * @return the latest version ratified by the catalog, or 0 if only the initial commit exists
+ */
+ long getLatestRatifiedVersion();
+
+ @Override
+ void close();
+}
diff --git a/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/unitycatalog/SparkUnityCatalogUtils.java b/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/unitycatalog/SparkUnityCatalogUtils.java
new file mode 100644
index 00000000000..3482e625b58
--- /dev/null
+++ b/kernel-spark/src/main/java/io/delta/kernel/spark/snapshot/unitycatalog/SparkUnityCatalogUtils.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (2025) The Delta Lake Project Authors.
+ *
+ * Licensed 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 io.delta.kernel.spark.snapshot.unitycatalog;
+
+import static java.util.Objects.requireNonNull;
+
+import io.delta.kernel.spark.utils.CatalogTableUtils;
+import io.delta.storage.commit.uccommitcoordinator.UCCommitCoordinatorClient;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.spark.sql.SparkSession;
+import org.apache.spark.sql.catalyst.catalog.CatalogTable;
+import org.apache.spark.sql.delta.coordinatedcommits.UCCommitCoordinatorBuilder$;
+
+/**
+ * Utility class for extracting Unity Catalog connection information from Spark catalog metadata.
+ *
+ * This class isolates Spark dependencies, allowing {@link UnityCatalogAdapter} to be created
+ * without Spark if connection info is provided directly via {@link UnityCatalogConnectionInfo}.
+ */
+public final class SparkUnityCatalogUtils {
+
+ // Utility class - no instances
+ private SparkUnityCatalogUtils() {}
+
+ /**
+ * Extracts Unity Catalog connection information from Spark catalog table metadata.
+ *
+ * @param catalogTable Spark catalog table metadata
+ * @param spark SparkSession for resolving Unity Catalog configurations
+ * @return connection info if table is UC-managed, empty otherwise
+ * @throws IllegalArgumentException if table is UC-managed but configuration is invalid
+ */
+ public static Optional Extracts UC connection info from Spark metadata and delegates to {@link
+ * #fromConnectionInfo}.
+ *
+ * @param catalogTable Spark catalog table metadata
+ * @param spark SparkSession for resolving Unity Catalog configurations
+ * @return adapter if table is UC-managed, empty otherwise
+ * @throws IllegalArgumentException if table is UC-managed but configuration is invalid
+ */
+ public static Optional This method allows creating a UC adapter without Spark dependencies if you have connection
+ * information directly.
+ *
+ * @param info Unity Catalog connection information
+ * @return adapter instance
+ */
+ public static ManagedCatalogAdapter fromConnectionInfo(UnityCatalogConnectionInfo info) {
+ requireNonNull(info, "info is null");
+ UCClient client = new UCTokenBasedRestClient(info.getEndpoint(), info.getToken());
+ return new UnityCatalogAdapter(info.getTableId(), info.getTablePath(), client);
+ }
+
+ public String getTableId() {
+ return tableId;
+ }
+
+ public String getTablePath() {
+ return tablePath;
+ }
+
+ @Override
+ public Snapshot loadSnapshot(
+ Engine engine, Optional This POJO encapsulates all the information needed to connect to a Unity Catalog table without
+ * requiring Spark dependencies.
+ */
+public final class UnityCatalogConnectionInfo {
+ private final String tableId;
+ private final String tablePath;
+ private final String endpoint;
+ private final String token;
+
+ public UnityCatalogConnectionInfo(
+ String tableId, String tablePath, String endpoint, String token) {
+ this.tableId = requireNonNull(tableId, "tableId is null");
+ this.tablePath = requireNonNull(tablePath, "tablePath is null");
+ this.endpoint = requireNonNull(endpoint, "endpoint is null");
+ this.token = requireNonNull(token, "token is null");
+ }
+
+ public String getTableId() {
+ return tableId;
+ }
+
+ public String getTablePath() {
+ return tablePath;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public String getToken() {
+ return token;
+ }
+}
diff --git a/kernel-spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSQLConfV2.scala b/kernel-spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSQLConfV2.scala
index f42f1a44048..e2f8aac7459 100644
--- a/kernel-spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSQLConfV2.scala
+++ b/kernel-spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSQLConfV2.scala
@@ -39,9 +39,8 @@ object DeltaSQLConfV2 extends DeltaSQLConfUtils {
buildConf("v2.enableMode")
.doc(
"Controls the Delta V2 connector enable mode. " +
- "Valid values: NONE (disabled, default), STRICT (should ONLY be enabled for testing).")
+ "Valid values: NONE (disabled, default), STRICT (should ONLY be enabled for testing).")
.stringConf
.checkValues(Set("NONE", "STRICT"))
.createWithDefault("NONE")
}
-
diff --git a/kernel-spark/src/test/java/io/delta/kernel/spark/snapshot/DeltaSnapshotManagerFactoryTest.java b/kernel-spark/src/test/java/io/delta/kernel/spark/snapshot/DeltaSnapshotManagerFactoryTest.java
new file mode 100644
index 00000000000..edb9a388e55
--- /dev/null
+++ b/kernel-spark/src/test/java/io/delta/kernel/spark/snapshot/DeltaSnapshotManagerFactoryTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (2025) The Delta Lake Project Authors.
+ *
+ * Licensed 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 io.delta.kernel.spark.snapshot;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link DeltaSnapshotManagerFactory}. */
+class DeltaSnapshotManagerFactoryTest {
+
+ @Test
+ void testFromPath_NullTablePath_ThrowsException() {
+ assertThrows(
+ NullPointerException.class,
+ () -> DeltaSnapshotManagerFactory.fromPath(null, new Configuration()),
+ "Null tablePath should throw NullPointerException");
+ }
+
+ @Test
+ void testFromPath_NullHadoopConf_ThrowsException() {
+ assertThrows(
+ NullPointerException.class,
+ () -> DeltaSnapshotManagerFactory.fromPath("/tmp/test", null),
+ "Null hadoopConf should throw NullPointerException");
+ }
+
+ @Test
+ void testFromCatalogTable_NullCatalogTable_ThrowsException() {
+ assertThrows(
+ NullPointerException.class,
+ () -> DeltaSnapshotManagerFactory.fromCatalogTable(null, null, new Configuration()),
+ "Null catalogTable should throw NullPointerException");
+ }
+
+ @Test
+ void testFromCatalogTable_NullHadoopConf_ThrowsException() {
+ // Can't test without a real CatalogTable instance, so this test validates the pattern
+ // See integration tests for full validation
+ }
+
+ // Note: Factory behavior tests (which manager type is created) require integration test setup.
+ // The following tests cannot be implemented as unit tests because the factory requires
+ // a non-null SparkSession parameter:
+ //
+ // - testCreate_NonCatalogManagedTable_ReturnsPathBasedManager: Verify non-UC tables use PathBased
+ // - testCreate_EmptyCatalogTable_ReturnsPathBasedManager: Verify empty catalogTable uses
+ // PathBased
+ // - testCreate_UCManagedTable_ReturnsCatalogManagedManager: Verify UC tables use CatalogManaged
+ //
+ // While PathBasedSnapshotManager doesn't technically need SparkSession, the factory API requires
+ // it to maintain a clean, consistent interface (always available in production via SparkTable).
+ // Cannot mock SparkSession effectively for these tests.
+ //
+ // Note: Testing CatalogManagedSnapshotManager creation requires integration tests with
+ // real SparkSession and Unity Catalog configuration. This is because:
+ // 1. CatalogManagedSnapshotManager constructor validates UC table and extracts metadata
+ // 2. It requires configured UC catalog (spark.sql.catalog.*.uri/token)
+ // 3. Unit tests cannot easily mock SparkSession's catalog manager
+ // See CatalogManagedSnapshotManagerTest for integration tests.
+}
diff --git a/kernel-spark/src/test/scala/io/delta/kernel/spark/snapshot/CatalogManagedSnapshotManagerSuite.scala b/kernel-spark/src/test/scala/io/delta/kernel/spark/snapshot/CatalogManagedSnapshotManagerSuite.scala
new file mode 100644
index 00000000000..9b2d1e3abb4
--- /dev/null
+++ b/kernel-spark/src/test/scala/io/delta/kernel/spark/snapshot/CatalogManagedSnapshotManagerSuite.scala
@@ -0,0 +1,245 @@
+/*
+ * Copyright (2025) The Delta Lake Project Authors.
+ *
+ * Licensed 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 io.delta.kernel.spark.snapshot
+
+import java.util.Optional
+
+import io.delta.kernel.spark.exception.VersionNotFoundException
+import io.delta.kernel.spark.snapshot.unitycatalog.UnityCatalogAdapter
+import io.delta.kernel.unitycatalog.{InMemoryUCClient, UCCatalogManagedTestUtils}
+
+import org.apache.hadoop.conf.Configuration
+import org.scalatest.funsuite.AnyFunSuite
+
+/** Tests for [[CatalogManagedSnapshotManager]]. */
+class CatalogManagedSnapshotManagerSuite extends AnyFunSuite with UCCatalogManagedTestUtils {
+
+ private val testUcTableId = "testUcTableId"
+
+ test("constructor throws on null hadoopConf") {
+ val adapter = new UnityCatalogAdapter(
+ testUcTableId,
+ "/tmp/path",
+ new InMemoryUCClient("ucMetastoreId"))
+
+ assertThrows[NullPointerException] {
+ new CatalogManagedSnapshotManager(adapter, testUcTableId, "/tmp/path", null)
+ }
+ }
+
+ test("constructor throws on null catalogAdapter") {
+ assertThrows[NullPointerException] {
+ new CatalogManagedSnapshotManager(null, testUcTableId, "/tmp/path", new Configuration())
+ }
+ }
+
+ test("constructor throws on null tableId") {
+ val adapter = new UnityCatalogAdapter(
+ testUcTableId,
+ "/tmp/path",
+ new InMemoryUCClient("ucMetastoreId"))
+
+ assertThrows[NullPointerException] {
+ new CatalogManagedSnapshotManager(adapter, null, "/tmp/path", new Configuration())
+ }
+ }
+
+ test("constructor throws on null tablePath") {
+ val adapter = new UnityCatalogAdapter(
+ testUcTableId,
+ "/tmp/path",
+ new InMemoryUCClient("ucMetastoreId"))
+
+ assertThrows[NullPointerException] {
+ new CatalogManagedSnapshotManager(adapter, testUcTableId, null, new Configuration())
+ }
+ }
+
+ test("loadLatestSnapshot returns snapshot at max ratified version") {
+ withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ val snapshot = manager.loadLatestSnapshot()
+
+ assert(snapshot != null, "Snapshot should not be null")
+ assert(snapshot.getVersion == maxRatifiedVersion, "Should load max ratified version")
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("loadSnapshotAt loads specified version") {
+ withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ val snapshot = manager.loadSnapshotAt(1L)
+
+ assert(snapshot != null, "Snapshot should not be null")
+ assert(snapshot.getVersion == 1L, "Should load version 1")
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("loadSnapshotAt throws on negative version") {
+ withUCClientAndTestTable { (ucClient, tablePath, _) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ assertThrows[IllegalArgumentException] {
+ manager.loadSnapshotAt(-1L)
+ }
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("checkVersionExists validates version range") {
+ withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ // Versions 0, 1, 2 should exist
+ manager.checkVersionExists(
+ 0L,
+ /* mustBeRecreatable = */ true,
+ /* allowOutOfRange = */ false)
+ manager.checkVersionExists(
+ 1L,
+ /* mustBeRecreatable = */ true,
+ /* allowOutOfRange = */ false)
+ manager.checkVersionExists(
+ maxRatifiedVersion,
+ /* mustBeRecreatable = */ true,
+ /* allowOutOfRange = */ false)
+
+ // Version beyond latest should throw
+ assertThrows[VersionNotFoundException] {
+ manager.checkVersionExists(
+ maxRatifiedVersion + 1,
+ /* mustBeRecreatable = */ true,
+ /* allowOutOfRange = */ false)
+ }
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("checkVersionExists allows out of range when specified") {
+ withUCClientAndTestTable { (ucClient, tablePath, maxRatifiedVersion) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ // Should not throw when allowOutOfRange = true
+ manager.checkVersionExists(
+ maxRatifiedVersion + 10,
+ /* mustBeRecreatable = */ true,
+ /* allowOutOfRange = */ true)
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("checkVersionExists throws on negative version") {
+ withUCClientAndTestTable { (ucClient, tablePath, _) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ assertThrows[IllegalArgumentException] {
+ manager.checkVersionExists(
+ -1L,
+ /* mustBeRecreatable = */ true,
+ /* allowOutOfRange = */ false)
+ }
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("getTableChanges returns commit range") {
+ withUCClientAndTestTable { (ucClient, tablePath, _) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ try {
+ val commitRange = manager.getTableChanges(
+ defaultEngine,
+ /* startVersion = */ 1L,
+ Optional.of(2L) /* endVersion */ )
+
+ assert(commitRange != null, "CommitRange should not be null")
+ } finally {
+ manager.close()
+ }
+ }
+ }
+
+ test("close releases resources") {
+ withUCClientAndTestTable { (ucClient, tablePath, _) =>
+ val adapter = new UnityCatalogAdapter(testUcTableId, tablePath, ucClient)
+ val manager = new CatalogManagedSnapshotManager(
+ adapter,
+ testUcTableId,
+ tablePath,
+ new Configuration())
+
+ // Should not throw
+ manager.close()
+ }
+ }
+}
+ *
+ *
+ * {@code
+ * // For path-based tables
+ * DeltaSnapshotManager manager = DeltaSnapshotManagerFactory.fromPath(
+ * tablePath,
+ * hadoopConf
+ * );
+ *
+ * // For catalog tables
+ * DeltaSnapshotManager manager = DeltaSnapshotManagerFactory.fromCatalogTable(
+ * catalogTable,
+ * spark,
+ * hadoopConf
+ * );
+ * }
+ */
+@Experimental
+public final class DeltaSnapshotManagerFactory {
+
+ // Utility class - no instances
+ private DeltaSnapshotManagerFactory() {}
+
+ /**
+ * Creates a path-based snapshot manager for filesystem Delta tables.
+ *
+ *