@@ -799,6 +799,28 @@ def test_get_create_table_request(self, mock_get_schema_fqn, mock_get_table_fqn)
799799 create_request .columns [i ].dataTypeDisplay , expected_type_display
800800 )
801801
802+ @patch (
803+ "metadata.ingestion.source.pipeline.openlineage.metadata.OpenlineageSource._get_table_fqn_from_om"
804+ )
805+ @patch (
806+ "metadata.ingestion.source.pipeline.openlineage.metadata.OpenlineageSource._get_schema_fqn_from_om"
807+ )
808+ def test_get_create_table_request_schema_not_found_returns_none (
809+ self , mock_get_schema_fqn , mock_get_table_fqn
810+ ):
811+ """Schema not found in any configured service — returns None without raising."""
812+ mock_get_table_fqn .side_effect = FQNNotFoundException ("Table not found" )
813+ mock_get_schema_fqn .side_effect = FQNNotFoundException ("Schema not found" )
814+ table_data = {
815+ "name" : "unknown_schema.employees" ,
816+ "namespace" : "bigquery" ,
817+ "facets" : {},
818+ }
819+
820+ result = self .open_lineage_source .get_create_table_request (table_data )
821+
822+ assert result is None
823+
802824 @patch ("confluent_kafka.Consumer" )
803825 def test_get_pipelines_list_filters_complete_events (self , mock_consumer_class ):
804826 """Test that get_pipelines_list returns COMPLETE events"""
@@ -1689,16 +1711,18 @@ def test_yield_pipeline_lineage_topic_not_found_skips_gracefully(self):
16891711 mock_pipeline = Mock ()
16901712 mock_pipeline .id .root = pipeline_id
16911713
1692- with patch .object (
1693- self .open_lineage_source , "metadata"
1694- ) as mock_metadata , patch .object (
1695- self .open_lineage_source ,
1696- "_get_table_fqn" ,
1697- return_value = "db-service.public.some_table" ,
1698- ), patch .object (
1699- self .open_lineage_source ,
1700- "get_create_table_request" ,
1701- return_value = None ,
1714+ with (
1715+ patch .object (self .open_lineage_source , "metadata" ) as mock_metadata ,
1716+ patch .object (
1717+ self .open_lineage_source ,
1718+ "_get_table_fqn" ,
1719+ return_value = "db-service.public.some_table" ,
1720+ ),
1721+ patch .object (
1722+ self .open_lineage_source ,
1723+ "get_create_table_request" ,
1724+ return_value = None ,
1725+ ),
17021726 ):
17031727 # Empty messaging services list — no broker match for unknown-broker
17041728 mock_metadata .list_all_entities .return_value = iter ([])
@@ -1735,6 +1759,134 @@ def mock_get_by_name(entity, fqn, **kwargs):
17351759 "No lineage edges should be produced when input topic cannot be resolved" ,
17361760 )
17371761
1762+ def test_parse_glue_table_name_trino_glue_catalog_schema (self ):
1763+ """Trino backed by AWS Glue Data Catalog uses the public schema and underscore-separated table names.
1764+ Verifies the parser handles the common Glue catalog table naming pattern correctly.
1765+ """
1766+ result = OpenlineageSource ._parse_glue_table_name (
1767+ "table/public/order_line_items"
1768+ )
1769+ self .assertEqual (result .name , "order_line_items" )
1770+ self .assertEqual (result .schema , "public" )
1771+
1772+ def test_parse_glue_table_name_happy_path (self ):
1773+ """Glue OL naming: table/{database}/{table} — source: Naming.java GlueNaming."""
1774+ result = OpenlineageSource ._parse_glue_table_name ("table/sales/users" )
1775+ self .assertEqual (result .name , "users" )
1776+ self .assertEqual (result .schema , "sales" )
1777+
1778+ def test_parse_glue_table_name_normalizes_to_lowercase (self ):
1779+ """Glue table and database names are normalized to lowercase for FQN matching."""
1780+ result = OpenlineageSource ._parse_glue_table_name ("table/Sales/Users" )
1781+ self .assertEqual (result .name , "users" )
1782+ self .assertEqual (result .schema , "sales" )
1783+
1784+ def test_parse_glue_table_name_not_glue_format_returns_none (self ):
1785+ """Names without the table/ prefix are not Glue format and return None."""
1786+ self .assertIsNone (OpenlineageSource ._parse_glue_table_name ("sales.users" ))
1787+
1788+ def test_parse_glue_table_name_missing_table_part_returns_none (self ):
1789+ """table/ prefix with only one path segment is malformed and returns None."""
1790+ self .assertIsNone (OpenlineageSource ._parse_glue_table_name ("table/only_db" ))
1791+
1792+ def test_parse_slash_table_name_happy_path (self ):
1793+ """Kusto OL naming: {database}/{table} — source: Naming.java KustoNaming."""
1794+ result = OpenlineageSource ._parse_slash_table_name ("mydb/mytable" )
1795+ self .assertEqual (result .name , "mytable" )
1796+ self .assertEqual (result .schema , "mydb" )
1797+
1798+ def test_parse_slash_table_name_normalizes_to_lowercase (self ):
1799+ """Kusto table and database names are normalized to lowercase for FQN matching."""
1800+ result = OpenlineageSource ._parse_slash_table_name ("MyDB/MyTable" )
1801+ self .assertEqual (result .name , "mytable" )
1802+ self .assertEqual (result .schema , "mydb" )
1803+
1804+ def test_parse_slash_table_name_single_part_returns_none (self ):
1805+ """A single path segment without a slash cannot be split into db/table and returns None."""
1806+ self .assertIsNone (OpenlineageSource ._parse_slash_table_name ("only_table" ))
1807+
1808+ def test_parse_cosmos_table_name_happy_path (self ):
1809+ """Cosmos OL naming: db from namespace /dbs/{db}, name colls/{coll} — source: Naming.java CosmosNaming."""
1810+ result = OpenlineageSource ._parse_cosmos_table_name (
1811+ "azurecosmos://myaccount.documents.azure.com/dbs/mydb" ,
1812+ "colls/mycollection" ,
1813+ )
1814+ self .assertEqual (result .name , "mycollection" )
1815+ self .assertEqual (result .schema , "mydb" )
1816+
1817+ def test_parse_cosmos_table_name_normalizes_to_lowercase (self ):
1818+ """Cosmos database and collection names are normalized to lowercase for FQN matching."""
1819+ result = OpenlineageSource ._parse_cosmos_table_name (
1820+ "azurecosmos://host/dbs/MyDB" , "colls/MyCollection"
1821+ )
1822+ self .assertEqual (result .name , "mycollection" )
1823+ self .assertEqual (result .schema , "mydb" )
1824+
1825+ def test_parse_cosmos_table_name_no_dbs_segment_returns_none (self ):
1826+ """A Cosmos namespace without /dbs/{db} cannot provide the database name and returns None."""
1827+ self .assertIsNone (
1828+ OpenlineageSource ._parse_cosmos_table_name (
1829+ "azurecosmos://host" , "colls/mycoll"
1830+ )
1831+ )
1832+
1833+ def test_parse_cosmos_table_name_non_colls_name_returns_none (self ):
1834+ """A Cosmos name not matching colls/{collection} is non-conformant and returns None."""
1835+ self .assertIsNone (
1836+ OpenlineageSource ._parse_cosmos_table_name (
1837+ "azurecosmos://host/dbs/mydb" , "mycollection"
1838+ )
1839+ )
1840+
1841+ def test_get_table_details_glue_namespace_parses_slash_name (self ):
1842+ """AWS Glue EMR events use arn:aws:glue namespace + table/{db}/{table} name."""
1843+ data = {
1844+ "namespace" : "arn:aws:glue:us-east-1:123456789012" ,
1845+ "name" : "table/sales/users" ,
1846+ }
1847+ result = OpenlineageSource ._get_table_details (data )
1848+ self .assertEqual (result .name , "users" )
1849+ self .assertEqual (result .schema , "sales" )
1850+
1851+ def test_get_table_details_kusto_namespace_parses_slash_name (self ):
1852+ """Azure Kusto events use azurekusto namespace + {db}/{table} name."""
1853+ data = {
1854+ "namespace" : "azurekusto://mycluster.kusto.windows.net" ,
1855+ "name" : "mydb/mytable" ,
1856+ }
1857+ result = OpenlineageSource ._get_table_details (data )
1858+ self .assertEqual (result .name , "mytable" )
1859+ self .assertEqual (result .schema , "mydb" )
1860+
1861+ def test_get_table_details_cosmos_namespace_parses_colls_name (self ):
1862+ """Azure Cosmos DB events carry the database in the namespace path."""
1863+ data = {
1864+ "namespace" : "azurecosmos://host.documents.azure.com/dbs/mydb" ,
1865+ "name" : "colls/orders" ,
1866+ }
1867+ result = OpenlineageSource ._get_table_details (data )
1868+ self .assertEqual (result .name , "orders" )
1869+ self .assertEqual (result .schema , "mydb" )
1870+
1871+ def test_get_entity_details_glue_namespace_resolves_to_table (self ):
1872+ """Glue ARN namespace + table/{db}/{table} name resolves to a table entity."""
1873+ data = {
1874+ "namespace" : "arn:aws:glue:us-east-1:123456789012" ,
1875+ "name" : "table/sales/users" ,
1876+ "facets" : {},
1877+ }
1878+ result = OpenlineageSource ._get_entity_details (data )
1879+ self .assertIsNotNone (result )
1880+ self .assertEqual (result .entity_type , "table" )
1881+ self .assertEqual (result .table_details .name , "users" )
1882+ self .assertEqual (result .table_details .schema , "sales" )
1883+
1884+ def test_get_entity_details_unparseable_name_raises_value_error (self ):
1885+ """Unrecognised name formats raise ValueError so callers can surface the error."""
1886+ data = {"namespace" : "trino://host:8080" , "name" : "invalidname" }
1887+ with self .assertRaises (ValueError ):
1888+ OpenlineageSource ._get_entity_details (data )
1889+
17381890
17391891if __name__ == "__main__" :
17401892 unittest .main ()
0 commit comments