From 670dc53b4646d672f4a309154fe8301f849446f9 Mon Sep 17 00:00:00 2001 From: Suman Maharana Date: Tue, 29 Jul 2025 17:28:11 +0530 Subject: [PATCH] Minor: fix tableau handle none entities (#22630) * Minor: fix tableau handle none entities * added tests --- .../source/dashboard/tableau/metadata.py | 2 +- .../unit/topology/dashboard/test_tableau.py | 173 +++++++++++++++++- 2 files changed, 172 insertions(+), 3 deletions(-) diff --git a/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py b/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py index 825b3deb511..dcf38717cf4 100644 --- a/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py +++ b/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py @@ -697,7 +697,7 @@ class TableauSource(DashboardServiceSource): "No table entities found for custom SQL lineage." f"fqn_search_string={fqn_search_string}, table_name={table_name}, query={query}" ) - for table_entity in from_entities: + for table_entity in from_entities or []: yield self._get_add_lineage_request( to_entity=upstream_data_model_entity, from_entity=table_entity, diff --git a/ingestion/tests/unit/topology/dashboard/test_tableau.py b/ingestion/tests/unit/topology/dashboard/test_tableau.py index c75f5353afe..bd1cc41ce30 100644 --- a/ingestion/tests/unit/topology/dashboard/test_tableau.py +++ b/ingestion/tests/unit/topology/dashboard/test_tableau.py @@ -5,11 +5,12 @@ import uuid from datetime import datetime, timedelta from types import SimpleNamespace from unittest import TestCase -from unittest.mock import patch +from unittest.mock import MagicMock, patch from metadata.generated.schema.api.data.createChart import CreateChartRequest from metadata.generated.schema.api.data.createDashboard import CreateDashboardRequest from metadata.generated.schema.entity.data.dashboard import Dashboard +from metadata.generated.schema.entity.data.dashboardDataModel import DashboardDataModel from metadata.generated.schema.entity.services.dashboardService import ( DashboardConnection, DashboardService, @@ -30,9 +31,11 @@ from metadata.ingestion.source.dashboard.tableau.metadata import ( TableauSource, ) from metadata.ingestion.source.dashboard.tableau.models import ( + DataSource, TableauBaseModel, TableauChart, TableauOwner, + UpstreamTable, ) MOCK_DASHBOARD_SERVICE = DashboardService( @@ -67,7 +70,7 @@ mock_tableau_config = { "securityConfig": { "jwtToken": "eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrOTQzNTYiLCJ0eXAiOiJKV1QiLCJhbGc" "iOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzQm90IjpmYWxzZSwiaXNzIjoib3Blbi1tZXRhZGF0YS5vcmciLCJpYXQiOjE" - "2NjM5Mzg0NjIsImVtYWlsIjoiYWRtaW5Ab3Blbm1ldGFkYXRhLm9yZyJ9.tS8um_5DKu7HgzGBzS1VTA5uUjKWOCU0B_j08WXB" + "6NjM5Mzg0NjIsImVtYWlsIjoiYWRtaW5Ab3Blbm1ldGFkYXRhLm9yZyJ9.tS8um_5DKu7HgzGBzS1VTA5uUjKWOCU0B_j08WXB" "iEC0mr0zNREkqVfwFDD-d24HlNEbrqioLsBuFRiwIWKc1m_ZlVQbG7P36RUxhuv2vbSp80FKyNM-Tj93FDzq91jsyNmsQhyNv_fN" "r3TXfzzSPjHt8Go0FMMP66weoKMgW2PbXlhVKwEuXUHyakLLzewm9UMeQaEiRzhiTMU3UkLXcKbYEJJvfNFcLwSl9W8JCO_l0Yj3u" "d-qt_nQYEZwqW6u5nfdQllN133iikV4fM5QZsMCnm8Rq1mvLR0y9bmJiD7fwM1tmJ791TUWqmKaTnP49U493VanKpUAfzIiOiIbhg" @@ -469,3 +472,169 @@ class TableauUnitTest(TestCase): result[0].right.sourceUrl.root, "http://mockTableauServer.com/#/site/hidarsite/workbooks/897790/views", ) + + def test_get_datamodel_table_lineage_with_empty_from_entities(self): + """ + Test that _get_datamodel_table_lineage handles empty from_entities gracefully + """ + # Mock data for the test + mock_datamodel = DataSource( + id="datasource1", + name="Test Datasource", + upstreamDatasources=[ + DataSource( + id="upstream_datasource1", + name="Upstream Datasource", + upstreamTables=[ + UpstreamTable( + id="table1", + luid="table1_luid", + name="test_table", + referencedByQueries=[ + { + "id": "query1", + "name": "test_query", + "query": "SELECT * FROM test_table", + } + ], + ) + ], + ) + ], + ) + + mock_data_model_entity = DashboardDataModel( + id=uuid.uuid4(), + name="Test Data Model", + service=EntityReference(id=uuid.uuid4(), type="dashboardService"), + dataModelType="TableauDataModel", + columns=[], + ) + + mock_upstream_data_model_entity = DashboardDataModel( + id=uuid.uuid4(), + name="Upstream Data Model", + service=EntityReference(id=uuid.uuid4(), type="dashboardService"), + dataModelType="TableauDataModel", + columns=[], + ) + + # Mock the client to return custom SQL queries + self.tableau.client.get_custom_sql_table_queries = MagicMock( + return_value=["SELECT * FROM test_table"] + ) + + # Mock the _get_datamodel method + with patch.object( + self.tableau, "_get_datamodel", return_value=mock_upstream_data_model_entity + ): + # Mock the metadata search to return empty results (simulating no table entities found) + with patch.object( + self.tableau.metadata, "search_in_any_service", return_value=[] + ): + # Mock the _get_add_lineage_request method to avoid actual lineage creation + with patch.object( + self.tableau, "_get_add_lineage_request" + ) as mock_lineage_request: + # Call the method under test + lineage_results = list( + self.tableau._get_datamodel_table_lineage( + datamodel=mock_datamodel, + data_model_entity=mock_data_model_entity, + db_service_prefix=None, + ) + ) + + # Verify that the method completes without throwing an error + # Even though from_entities is empty, the method should handle it gracefully + self.assertIsInstance(lineage_results, list) + + # Verify that the lineage request was called for the datamodel lineage + # (but not for table lineage since from_entities was empty) + mock_lineage_request.assert_called() + + # Verify that the method didn't throw any exceptions + # The test passes if we reach this point without exceptions + + def test_get_datamodel_table_lineage_with_none_from_entities(self): + """ + Test that _get_datamodel_table_lineage handles None from_entities gracefully + """ + # Mock data for the test + mock_datamodel = DataSource( + id="datasource2", + name="Test Datasource 2", + upstreamDatasources=[ + DataSource( + id="upstream_datasource2", + name="Upstream Datasource 2", + upstreamTables=[ + UpstreamTable( + id="table2", + luid="table2_luid", + name="test_table_2", + referencedByQueries=[ + { + "id": "query2", + "name": "test_query_2", + "query": "SELECT * FROM test_table_2", + } + ], + ) + ], + ) + ], + ) + + mock_data_model_entity = DashboardDataModel( + id=uuid.uuid4(), + name="Test Data Model 2", + service=EntityReference(id=uuid.uuid4(), type="dashboardService"), + dataModelType="TableauDataModel", + columns=[], + ) + + mock_upstream_data_model_entity = DashboardDataModel( + id=uuid.uuid4(), + name="Upstream Data Model 2", + service=EntityReference(id=uuid.uuid4(), type="dashboardService"), + dataModelType="TableauDataModel", + columns=[], + ) + + # Mock the client to return custom SQL queries + self.tableau.client.get_custom_sql_table_queries = MagicMock( + return_value=["SELECT * FROM test_table_2"] + ) + + # Mock the _get_datamodel method + with patch.object( + self.tableau, "_get_datamodel", return_value=mock_upstream_data_model_entity + ): + # Mock the metadata search to return None (simulating search failure) + with patch.object( + self.tableau.metadata, "search_in_any_service", return_value=None + ): + # Mock the _get_add_lineage_request method to avoid actual lineage creation + with patch.object( + self.tableau, "_get_add_lineage_request" + ) as mock_lineage_request: + # Call the method under test + lineage_results = list( + self.tableau._get_datamodel_table_lineage( + datamodel=mock_datamodel, + data_model_entity=mock_data_model_entity, + db_service_prefix=None, + ) + ) + + # Verify that the method completes without throwing an error + # Even though from_entities is None, the method should handle it gracefully + self.assertIsInstance(lineage_results, list) + + # Verify that the lineage request was called for the datamodel lineage + # (but not for table lineage since from_entities was None) + mock_lineage_request.assert_called() + + # Verify that the method didn't throw any exceptions + # The test passes if we reach this point without exceptions