import json from unittest import mock import pandas as pd import pytest from mlflow.data.dataset_source_registry import get_dataset_source_from_json from mlflow.data.delta_dataset_source import DeltaDatasetSource from mlflow.exceptions import MlflowException from mlflow.protos.databricks_managed_catalog_messages_pb2 import GetTable, GetTableResponse from mlflow.utils.proto_json_utils import message_to_json @pytest.fixture(scope="module") def spark_session(): from pyspark.sql import SparkSession with ( SparkSession.builder .master("local[*]") .config("spark.jars.packages", "io.delta:delta-spark_2.13:4.0.0") .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") .config( "spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog" ) .getOrCreate() ) as session: yield session def test_delta_dataset_source_from_path(spark_session, tmp_path): df = pd.DataFrame([[1, 2, 3], [1, 2, 3]], columns=["a", "b", "c"]) df_spark = spark_session.createDataFrame(df) path = str(tmp_path / "temp.delta") df_spark.write.format("delta").mode("overwrite").save(path) delta_datasource = DeltaDatasetSource(path=path) loaded_df_spark = delta_datasource.load() assert loaded_df_spark.count() == df_spark.count() assert delta_datasource.to_dict()["path"] == path reloaded_source = get_dataset_source_from_json( delta_datasource.to_json(), source_type=delta_datasource._get_source_type() ) assert isinstance(reloaded_source, DeltaDatasetSource) assert type(delta_datasource) == type(reloaded_source) assert reloaded_source.to_json() == delta_datasource.to_json() def test_delta_dataset_source_from_table(spark_session, tmp_path): df = pd.DataFrame([[1, 2, 3], [1, 2, 3]], columns=["a", "b", "c"]) df_spark = spark_session.createDataFrame(df) df_spark.write.format("delta").mode("overwrite").saveAsTable( "default.temp_delta", path=tmp_path ) delta_datasource = DeltaDatasetSource(delta_table_name="temp_delta") loaded_df_spark = delta_datasource.load() assert loaded_df_spark.count() == df_spark.count() assert delta_datasource.to_dict()["delta_table_name"] == "temp_delta" reloaded_source = get_dataset_source_from_json( delta_datasource.to_json(), source_type=delta_datasource._get_source_type() ) assert isinstance(reloaded_source, DeltaDatasetSource) assert type(delta_datasource) == type(reloaded_source) assert reloaded_source.to_json() == delta_datasource.to_json() def test_delta_dataset_source_from_table_versioned(spark_session, tmp_path): df = pd.DataFrame([[1, 2, 3], [1, 2, 3]], columns=["a", "b", "c"]) df_spark = spark_session.createDataFrame(df) df_spark.write.format("delta").mode("overwrite").saveAsTable( "default.temp_delta_versioned", path=tmp_path ) df2 = pd.DataFrame([[1, 2, 3]], columns=["a", "b", "c"]) df2_spark = spark_session.createDataFrame(df2) df2_spark.write.format("delta").mode("overwrite").saveAsTable( "default.temp_delta_versioned", path=tmp_path ) delta_datasource = DeltaDatasetSource( delta_table_name="temp_delta_versioned", delta_table_version=1 ) loaded_df_spark = delta_datasource.load() assert loaded_df_spark.count() == df2_spark.count() config = delta_datasource.to_dict() assert config["delta_table_name"] == "temp_delta_versioned" assert config["delta_table_version"] == 1 reloaded_source = get_dataset_source_from_json( delta_datasource.to_json(), source_type=delta_datasource._get_source_type() ) assert isinstance(reloaded_source, DeltaDatasetSource) assert type(delta_datasource) == type(reloaded_source) assert reloaded_source.to_json() == delta_datasource.to_json() def test_delta_dataset_source_too_many_inputs(spark_session, tmp_path): df = pd.DataFrame([[1, 2, 3], [1, 2, 3]], columns=["a", "b", "c"]) df_spark = spark_session.createDataFrame(df) df_spark.write.format("delta").mode("overwrite").saveAsTable( "default.temp_delta_too_many_inputs", path=tmp_path ) with pytest.raises(MlflowException, match='Must specify exactly one of "path" or "table_name"'): DeltaDatasetSource(path=tmp_path, delta_table_name="temp_delta_too_many_inputs") def test_uc_table_id_retrieval_works(spark_session, tmp_path): def mock_resolve_table_name(table_name, spark): if table_name == "temp_delta_versioned_with_id": return "default.temp_delta_versioned_with_id" return table_name def mock_lookup_table_id(table_name): if table_name == "default.temp_delta_versioned_with_id": return "uc_table_id_1" return None with ( mock.patch( "mlflow.data.delta_dataset_source.get_full_name_from_sc", side_effect=mock_resolve_table_name, ), mock.patch( "mlflow.data.delta_dataset_source.DeltaDatasetSource._lookup_table_id", side_effect=mock_lookup_table_id, ), mock.patch( "mlflow.data.delta_dataset_source._get_active_spark_session", return_value=None, ), mock.patch( "mlflow.data.delta_dataset_source.DeltaDatasetSource._is_databricks_uc_table", return_value=True, ), ): df = pd.DataFrame([[1, 2, 3], [1, 2, 3]], columns=["a", "b", "c"]) df_spark = spark_session.createDataFrame(df) df_spark.write.format("delta").mode("overwrite").saveAsTable( "default.temp_delta_versioned_with_id", path=tmp_path ) df2 = pd.DataFrame([[1, 2, 3]], columns=["a", "b", "c"]) df2_spark = spark_session.createDataFrame(df2) df2_spark.write.format("delta").mode("overwrite").saveAsTable( "default.temp_delta_versioned_with_id", path=tmp_path ) delta_datasource = DeltaDatasetSource( delta_table_name="temp_delta_versioned_with_id", delta_table_version=1 ) loaded_df_spark = delta_datasource.load() assert loaded_df_spark.count() == df2_spark.count() assert delta_datasource.to_json() == json.dumps({ "delta_table_name": "default.temp_delta_versioned_with_id", "delta_table_version": 1, "is_databricks_uc_table": True, "delta_table_id": "uc_table_id_1", }) def _args(endpoint, json_body): return { "host_creds": None, "endpoint": f"/api/2.0/unity-catalog/tables/{endpoint}", "method": "GET", "json_body": json_body, "response_proto": GetTableResponse, } @pytest.mark.parametrize( ("call_endpoint_response", "expected_lookup_response", "test_table_name"), [ (None, None, "delta_table_1"), (Exception("Exception from call_endpoint"), None, "delta_table_2"), (GetTableResponse(table_id="uc_table_id_1"), "uc_table_id_1", "delta_table_3"), ], ) def test_lookup_table_id( call_endpoint_response, expected_lookup_response, test_table_name, tmp_path ): def mock_resolve_table_name(table_name, spark): if table_name == test_table_name: return f"default.{test_table_name}" return table_name def mock_call_endpoint(host_creds, endpoint, method, json_body, response_proto): if isinstance(call_endpoint_response, Exception): raise call_endpoint_response return call_endpoint_response with ( mock.patch( "mlflow.data.delta_dataset_source.get_full_name_from_sc", side_effect=mock_resolve_table_name, ), mock.patch( "mlflow.data.delta_dataset_source._get_active_spark_session", return_value=None, ), mock.patch( "mlflow.data.delta_dataset_source.get_databricks_host_creds", return_value=None, ), mock.patch( "mlflow.data.delta_dataset_source.DeltaDatasetSource._is_databricks_uc_table", return_value=True, ), mock.patch( "mlflow.data.delta_dataset_source.call_endpoint", side_effect=mock_call_endpoint, ) as mock_endpoint, ): delta_datasource = DeltaDatasetSource( delta_table_name=test_table_name, delta_table_version=1 ) assert delta_datasource._lookup_table_id(test_table_name) == expected_lookup_response req_body = message_to_json(GetTable(full_name_arg=test_table_name)) call_args = _args(test_table_name, req_body) mock_endpoint.assert_any_call(**call_args) @pytest.mark.parametrize( ("table_name", "expected_result"), [ ("default.test", True), ("hive_metastore.test", False), ("spark_catalog.test", False), ("samples.test", False), ], ) def test_is_databricks_uc_table(table_name, expected_result): with ( mock.patch( "mlflow.data.delta_dataset_source.get_full_name_from_sc", return_value=table_name, ), mock.patch( "mlflow.data.delta_dataset_source._get_active_spark_session", return_value=None, ), ): delta_datasource = DeltaDatasetSource(delta_table_name=table_name, delta_table_version=1) assert delta_datasource._is_databricks_uc_table() == expected_result