acryl-datahub 0.15.0rc15__py3-none-any.whl → 0.15.0rc16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of acryl-datahub might be problematic. Click here for more details.
- {acryl_datahub-0.15.0rc15.dist-info → acryl_datahub-0.15.0rc16.dist-info}/METADATA +2466 -2482
- {acryl_datahub-0.15.0rc15.dist-info → acryl_datahub-0.15.0rc16.dist-info}/RECORD +22 -24
- datahub/__init__.py +1 -1
- datahub/cli/cli_utils.py +2 -0
- datahub/ingestion/api/incremental_properties_helper.py +69 -0
- datahub/ingestion/api/source_helpers.py +3 -1
- datahub/ingestion/sink/datahub_rest.py +3 -3
- datahub/ingestion/source/abs/source.py +4 -0
- datahub/ingestion/source/gc/datahub_gc.py +5 -5
- datahub/ingestion/source/gc/soft_deleted_entity_cleanup.py +1 -1
- datahub/ingestion/source/mode.py +0 -23
- datahub/ingestion/source/redash.py +13 -63
- datahub/ingestion/source/redshift/config.py +1 -0
- datahub/ingestion/source/redshift/redshift.py +2 -0
- datahub/ingestion/source/snowflake/snowflake_config.py +4 -0
- datahub/ingestion/source/snowflake/snowflake_v2.py +6 -0
- datahub/ingestion/source/unity/source.py +2 -0
- datahub/ingestion/source/unity/usage.py +20 -11
- datahub/utilities/partition_executor.py +1 -1
- datahub/utilities/sql_lineage_parser_impl.py +0 -160
- datahub/utilities/sql_parser.py +0 -94
- datahub/utilities/sql_parser_base.py +0 -21
- {acryl_datahub-0.15.0rc15.dist-info → acryl_datahub-0.15.0rc16.dist-info}/WHEEL +0 -0
- {acryl_datahub-0.15.0rc15.dist-info → acryl_datahub-0.15.0rc16.dist-info}/entry_points.txt +0 -0
- {acryl_datahub-0.15.0rc15.dist-info → acryl_datahub-0.15.0rc16.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,6 @@ from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, Set,
|
|
|
7
7
|
|
|
8
8
|
import pyspark
|
|
9
9
|
from databricks.sdk.service.sql import QueryStatementType
|
|
10
|
-
from sqllineage.runner import LineageRunner
|
|
11
10
|
|
|
12
11
|
from datahub.emitter.mcp import MetadataChangeProposalWrapper
|
|
13
12
|
from datahub.ingestion.api.source_helpers import auto_empty_dataset_usage_statistics
|
|
@@ -22,7 +21,9 @@ from datahub.ingestion.source.unity.proxy_types import (
|
|
|
22
21
|
from datahub.ingestion.source.unity.report import UnityCatalogReport
|
|
23
22
|
from datahub.ingestion.source.usage.usage_common import UsageAggregator
|
|
24
23
|
from datahub.metadata.schema_classes import OperationClass
|
|
24
|
+
from datahub.sql_parsing.sqlglot_lineage import create_lineage_sql_parsed_result
|
|
25
25
|
from datahub.sql_parsing.sqlglot_utils import get_query_fingerprint
|
|
26
|
+
from datahub.utilities.urns.dataset_urn import DatasetUrn
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -48,6 +49,7 @@ class UnityCatalogUsageExtractor:
|
|
|
48
49
|
proxy: UnityCatalogApiProxy
|
|
49
50
|
table_urn_builder: Callable[[TableReference], str]
|
|
50
51
|
user_urn_builder: Callable[[str], str]
|
|
52
|
+
platform: str = "databricks"
|
|
51
53
|
|
|
52
54
|
def __post_init__(self):
|
|
53
55
|
self.usage_aggregator = UsageAggregator[TableReference](self.config)
|
|
@@ -173,7 +175,7 @@ class UnityCatalogUsageExtractor:
|
|
|
173
175
|
self, query: Query, table_map: TableMap
|
|
174
176
|
) -> Optional[QueryTableInfo]:
|
|
175
177
|
with self.report.usage_perf_report.sql_parsing_timer:
|
|
176
|
-
table_info = self.
|
|
178
|
+
table_info = self._parse_query_via_sqlglot(query.query_text)
|
|
177
179
|
if table_info is None and query.statement_type == QueryStatementType.SELECT:
|
|
178
180
|
with self.report.usage_perf_report.spark_sql_parsing_timer:
|
|
179
181
|
table_info = self._parse_query_via_spark_sql_plan(query.query_text)
|
|
@@ -191,26 +193,33 @@ class UnityCatalogUsageExtractor:
|
|
|
191
193
|
),
|
|
192
194
|
)
|
|
193
195
|
|
|
194
|
-
def
|
|
196
|
+
def _parse_query_via_sqlglot(self, query: str) -> Optional[StringTableInfo]:
|
|
195
197
|
try:
|
|
196
|
-
|
|
198
|
+
sql_parser_in_tables = create_lineage_sql_parsed_result(
|
|
199
|
+
query=query,
|
|
200
|
+
default_db=None,
|
|
201
|
+
platform=self.platform,
|
|
202
|
+
env=self.config.env,
|
|
203
|
+
platform_instance=None,
|
|
204
|
+
)
|
|
205
|
+
|
|
197
206
|
return GenericTableInfo(
|
|
198
207
|
source_tables=[
|
|
199
|
-
self.
|
|
200
|
-
for table in
|
|
208
|
+
self._parse_sqlglot_table(table)
|
|
209
|
+
for table in sql_parser_in_tables.in_tables
|
|
201
210
|
],
|
|
202
211
|
target_tables=[
|
|
203
|
-
self.
|
|
204
|
-
for table in
|
|
212
|
+
self._parse_sqlglot_table(table)
|
|
213
|
+
for table in sql_parser_in_tables.out_tables
|
|
205
214
|
],
|
|
206
215
|
)
|
|
207
216
|
except Exception as e:
|
|
208
|
-
logger.info(f"Could not parse query via
|
|
217
|
+
logger.info(f"Could not parse query via sqlglot, {query}: {e!r}")
|
|
209
218
|
return None
|
|
210
219
|
|
|
211
220
|
@staticmethod
|
|
212
|
-
def
|
|
213
|
-
full_table_name =
|
|
221
|
+
def _parse_sqlglot_table(table_urn: str) -> str:
|
|
222
|
+
full_table_name = DatasetUrn.from_string(table_urn).name
|
|
214
223
|
default_schema = "<default>."
|
|
215
224
|
if full_table_name.startswith(default_schema):
|
|
216
225
|
return full_table_name[len(default_schema) :]
|
|
@@ -268,7 +268,7 @@ class BatchPartitionExecutor(Closeable):
|
|
|
268
268
|
self.process_batch = process_batch
|
|
269
269
|
self.min_process_interval = min_process_interval
|
|
270
270
|
self.read_from_pending_interval = read_from_pending_interval
|
|
271
|
-
assert self.max_workers
|
|
271
|
+
assert self.max_workers >= 1
|
|
272
272
|
|
|
273
273
|
self._state_lock = threading.Lock()
|
|
274
274
|
self._executor = ThreadPoolExecutor(
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
import logging
|
|
3
|
-
import re
|
|
4
|
-
import unittest
|
|
5
|
-
import unittest.mock
|
|
6
|
-
from typing import Dict, List, Optional, Set
|
|
7
|
-
|
|
8
|
-
from sqllineage.core.holders import Column, SQLLineageHolder
|
|
9
|
-
from sqllineage.exceptions import SQLLineageException
|
|
10
|
-
|
|
11
|
-
from datahub.utilities.sql_parser_base import SQLParser, SqlParserException
|
|
12
|
-
|
|
13
|
-
with contextlib.suppress(ImportError):
|
|
14
|
-
import sqlparse
|
|
15
|
-
from networkx import DiGraph
|
|
16
|
-
from sqllineage.core import LineageAnalyzer
|
|
17
|
-
|
|
18
|
-
import datahub.utilities.sqllineage_patch
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SqlLineageSQLParserImpl(SQLParser):
|
|
23
|
-
_DATE_SWAP_TOKEN = "__d_a_t_e"
|
|
24
|
-
_HOUR_SWAP_TOKEN = "__h_o_u_r"
|
|
25
|
-
_TIMESTAMP_SWAP_TOKEN = "__t_i_m_e_s_t_a_m_p"
|
|
26
|
-
_DATA_SWAP_TOKEN = "__d_a_t_a"
|
|
27
|
-
_ADMIN_SWAP_TOKEN = "__a_d_m_i_n"
|
|
28
|
-
_MYVIEW_SQL_TABLE_NAME_TOKEN = "__my_view__.__sql_table_name__"
|
|
29
|
-
_MYVIEW_LOOKER_TOKEN = "my_view.SQL_TABLE_NAME"
|
|
30
|
-
|
|
31
|
-
def __init__(self, sql_query: str, use_raw_names: bool = False) -> None:
|
|
32
|
-
super().__init__(sql_query)
|
|
33
|
-
original_sql_query = sql_query
|
|
34
|
-
self._use_raw_names = use_raw_names
|
|
35
|
-
|
|
36
|
-
# SqlLineageParser makes mistakes on lateral flatten queries, use the prefix
|
|
37
|
-
if "lateral flatten" in sql_query:
|
|
38
|
-
sql_query = sql_query[: sql_query.find("lateral flatten")]
|
|
39
|
-
|
|
40
|
-
# Replace reserved words that break SqlLineageParser
|
|
41
|
-
self.token_to_original: Dict[str, str] = {
|
|
42
|
-
self._DATE_SWAP_TOKEN: "date",
|
|
43
|
-
self._HOUR_SWAP_TOKEN: "hour",
|
|
44
|
-
self._TIMESTAMP_SWAP_TOKEN: "timestamp",
|
|
45
|
-
self._DATA_SWAP_TOKEN: "data",
|
|
46
|
-
self._ADMIN_SWAP_TOKEN: "admin",
|
|
47
|
-
}
|
|
48
|
-
for replacement, original in self.token_to_original.items():
|
|
49
|
-
# Replace original tokens with replacement. Since table and column name can contain a hyphen('-'),
|
|
50
|
-
# also prevent original tokens appearing as part of these names with a hyphen from getting substituted.
|
|
51
|
-
sql_query = re.sub(
|
|
52
|
-
rf"((?<!-)\b{original}\b)(?!-)",
|
|
53
|
-
rf"{replacement}",
|
|
54
|
-
sql_query,
|
|
55
|
-
flags=re.IGNORECASE,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# SqlLineageParser lowercarese tablenames and we need to replace Looker specific token which should be uppercased
|
|
59
|
-
sql_query = re.sub(
|
|
60
|
-
rf"(\${{{self._MYVIEW_LOOKER_TOKEN}}})",
|
|
61
|
-
rf"{self._MYVIEW_SQL_TABLE_NAME_TOKEN}",
|
|
62
|
-
sql_query,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# SqlLineageParser does not handle "encode" directives well. Remove them
|
|
66
|
-
sql_query = re.sub(r"\sencode [a-zA-Z]*", "", sql_query, flags=re.IGNORECASE)
|
|
67
|
-
|
|
68
|
-
# Replace lookml templates with the variable otherwise sqlparse can't parse ${
|
|
69
|
-
sql_query = re.sub(r"(\${)(.+)(})", r"\2", sql_query)
|
|
70
|
-
if sql_query != original_sql_query:
|
|
71
|
-
logger.debug(f"Rewrote original query {original_sql_query} as {sql_query}")
|
|
72
|
-
|
|
73
|
-
self._sql = sql_query
|
|
74
|
-
self._stmt_holders: Optional[List[LineageAnalyzer]] = None
|
|
75
|
-
self._sql_holder: Optional[SQLLineageHolder] = None
|
|
76
|
-
try:
|
|
77
|
-
self._stmt = [
|
|
78
|
-
s
|
|
79
|
-
for s in sqlparse.parse(
|
|
80
|
-
# first apply sqlparser formatting just to get rid of comments, which cause
|
|
81
|
-
# inconsistencies in parsing output
|
|
82
|
-
sqlparse.format(
|
|
83
|
-
self._sql.strip(),
|
|
84
|
-
strip_comments=True,
|
|
85
|
-
use_space_around_operators=True,
|
|
86
|
-
),
|
|
87
|
-
)
|
|
88
|
-
if s.token_first(skip_cm=True)
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
with unittest.mock.patch(
|
|
92
|
-
"sqllineage.core.handlers.source.SourceHandler.end_of_query_cleanup",
|
|
93
|
-
datahub.utilities.sqllineage_patch.end_of_query_cleanup_patch,
|
|
94
|
-
):
|
|
95
|
-
with unittest.mock.patch(
|
|
96
|
-
"sqllineage.core.holders.SubQueryLineageHolder.add_column_lineage",
|
|
97
|
-
datahub.utilities.sqllineage_patch.add_column_lineage_patch,
|
|
98
|
-
):
|
|
99
|
-
self._stmt_holders = [
|
|
100
|
-
LineageAnalyzer().analyze(stmt) for stmt in self._stmt
|
|
101
|
-
]
|
|
102
|
-
self._sql_holder = SQLLineageHolder.of(*self._stmt_holders)
|
|
103
|
-
except SQLLineageException as e:
|
|
104
|
-
raise SqlParserException(
|
|
105
|
-
f"SQL lineage analyzer error '{e}' for query: '{self._sql}"
|
|
106
|
-
) from e
|
|
107
|
-
|
|
108
|
-
def get_tables(self) -> List[str]:
|
|
109
|
-
result: List[str] = []
|
|
110
|
-
if self._sql_holder is None:
|
|
111
|
-
logger.error("sql holder not present so cannot get tables")
|
|
112
|
-
return result
|
|
113
|
-
for table in self._sql_holder.source_tables:
|
|
114
|
-
table_normalized = re.sub(
|
|
115
|
-
r"^<default>.",
|
|
116
|
-
"",
|
|
117
|
-
(
|
|
118
|
-
str(table)
|
|
119
|
-
if not self._use_raw_names
|
|
120
|
-
else f"{table.schema.raw_name}.{table.raw_name}"
|
|
121
|
-
),
|
|
122
|
-
)
|
|
123
|
-
result.append(str(table_normalized))
|
|
124
|
-
|
|
125
|
-
# We need to revert TOKEN replacements
|
|
126
|
-
for token, replacement in self.token_to_original.items():
|
|
127
|
-
result = [replacement if c == token else c for c in result]
|
|
128
|
-
result = [
|
|
129
|
-
self._MYVIEW_LOOKER_TOKEN if c == self._MYVIEW_SQL_TABLE_NAME_TOKEN else c
|
|
130
|
-
for c in result
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
# Sort tables to make the list deterministic
|
|
134
|
-
result.sort()
|
|
135
|
-
|
|
136
|
-
return result
|
|
137
|
-
|
|
138
|
-
def get_columns(self) -> List[str]:
|
|
139
|
-
if self._sql_holder is None:
|
|
140
|
-
raise SqlParserException("sql holder not present so cannot get columns")
|
|
141
|
-
graph: DiGraph = self._sql_holder.graph # For mypy attribute checking
|
|
142
|
-
column_nodes = [n for n in graph.nodes if isinstance(n, Column)]
|
|
143
|
-
column_graph = graph.subgraph(column_nodes)
|
|
144
|
-
|
|
145
|
-
target_columns = {column for column, deg in column_graph.out_degree if deg == 0}
|
|
146
|
-
|
|
147
|
-
result: Set[str] = set()
|
|
148
|
-
for column in target_columns:
|
|
149
|
-
# Let's drop all the count(*) and similard columns which are expression actually if it does not have an alias
|
|
150
|
-
if not any(ele in column.raw_name for ele in ["*", "(", ")"]):
|
|
151
|
-
result.add(str(column.raw_name))
|
|
152
|
-
|
|
153
|
-
# Reverting back all the previously renamed words which confuses the parser
|
|
154
|
-
result = {"date" if c == self._DATE_SWAP_TOKEN else c for c in result}
|
|
155
|
-
result = {
|
|
156
|
-
"timestamp" if c == self._TIMESTAMP_SWAP_TOKEN else c for c in list(result)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
# swap back renamed date column
|
|
160
|
-
return list(result)
|
datahub/utilities/sql_parser.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import multiprocessing
|
|
3
|
-
import traceback
|
|
4
|
-
from multiprocessing import Process, Queue
|
|
5
|
-
from typing import Any, List, Optional, Tuple
|
|
6
|
-
|
|
7
|
-
from datahub.utilities.sql_lineage_parser_impl import SqlLineageSQLParserImpl
|
|
8
|
-
from datahub.utilities.sql_parser_base import SQLParser
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def sql_lineage_parser_impl_func_wrapper(
|
|
14
|
-
queue: Optional[multiprocessing.Queue], sql_query: str, use_raw_names: bool = False
|
|
15
|
-
) -> Optional[Tuple[List[str], List[str], Any]]:
|
|
16
|
-
"""
|
|
17
|
-
The wrapper function that computes the tables and columns using the SqlLineageSQLParserImpl
|
|
18
|
-
and puts the results on the shared IPC queue. This is used to isolate SqlLineageSQLParserImpl
|
|
19
|
-
functionality in a separate process, and hence protect our sources from memory leaks originating in
|
|
20
|
-
the sqllineage module.
|
|
21
|
-
:param queue: The shared IPC queue on to which the results will be put.
|
|
22
|
-
:param sql_query: The SQL query to extract the tables & columns from.
|
|
23
|
-
:param use_raw_names: Parameter used to ignore sqllineage's default lowercasing.
|
|
24
|
-
:return: None.
|
|
25
|
-
"""
|
|
26
|
-
exception_details: Optional[Tuple[BaseException, str]] = None
|
|
27
|
-
tables: List[str] = []
|
|
28
|
-
columns: List[str] = []
|
|
29
|
-
try:
|
|
30
|
-
parser = SqlLineageSQLParserImpl(sql_query, use_raw_names)
|
|
31
|
-
tables = parser.get_tables()
|
|
32
|
-
columns = parser.get_columns()
|
|
33
|
-
except BaseException as e:
|
|
34
|
-
exc_msg = traceback.format_exc()
|
|
35
|
-
exception_details = (e, exc_msg)
|
|
36
|
-
logger.debug(exc_msg)
|
|
37
|
-
|
|
38
|
-
if queue is not None:
|
|
39
|
-
queue.put((tables, columns, exception_details))
|
|
40
|
-
return None
|
|
41
|
-
else:
|
|
42
|
-
return (tables, columns, exception_details)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class SqlLineageSQLParser(SQLParser):
|
|
46
|
-
def __init__(
|
|
47
|
-
self,
|
|
48
|
-
sql_query: str,
|
|
49
|
-
use_external_process: bool = False,
|
|
50
|
-
use_raw_names: bool = False,
|
|
51
|
-
) -> None:
|
|
52
|
-
super().__init__(sql_query, use_external_process)
|
|
53
|
-
if use_external_process:
|
|
54
|
-
self.tables, self.columns = self._get_tables_columns_process_wrapped(
|
|
55
|
-
sql_query, use_raw_names
|
|
56
|
-
)
|
|
57
|
-
else:
|
|
58
|
-
return_tuple = sql_lineage_parser_impl_func_wrapper(
|
|
59
|
-
None, sql_query, use_raw_names
|
|
60
|
-
)
|
|
61
|
-
if return_tuple is not None:
|
|
62
|
-
(
|
|
63
|
-
self.tables,
|
|
64
|
-
self.columns,
|
|
65
|
-
some_exception,
|
|
66
|
-
) = return_tuple
|
|
67
|
-
|
|
68
|
-
@staticmethod
|
|
69
|
-
def _get_tables_columns_process_wrapped(
|
|
70
|
-
sql_query: str, use_raw_names: bool = False
|
|
71
|
-
) -> Tuple[List[str], List[str]]:
|
|
72
|
-
# Invoke sql_lineage_parser_impl_func_wrapper in a separate process to avoid
|
|
73
|
-
# memory leaks from sqllineage module used by SqlLineageSQLParserImpl. This will help
|
|
74
|
-
# shield our sources like lookml & redash, that need to parse a large number of SQL statements,
|
|
75
|
-
# from causing significant memory leaks in the datahub cli during ingestion.
|
|
76
|
-
queue: multiprocessing.Queue = Queue()
|
|
77
|
-
process: multiprocessing.Process = Process(
|
|
78
|
-
target=sql_lineage_parser_impl_func_wrapper,
|
|
79
|
-
args=(queue, sql_query, use_raw_names),
|
|
80
|
-
)
|
|
81
|
-
process.start()
|
|
82
|
-
tables, columns, exception_details = queue.get(block=True)
|
|
83
|
-
if exception_details is not None:
|
|
84
|
-
raise exception_details[0](f"Sub-process exception: {exception_details[1]}")
|
|
85
|
-
return tables, columns
|
|
86
|
-
|
|
87
|
-
def get_tables(self) -> List[str]:
|
|
88
|
-
return self.tables
|
|
89
|
-
|
|
90
|
-
def get_columns(self) -> List[str]:
|
|
91
|
-
return self.columns
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
DefaultSQLParser = SqlLineageSQLParser
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from abc import ABCMeta, abstractmethod
|
|
2
|
-
from typing import List
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class SqlParserException(Exception):
|
|
6
|
-
"""Raised when sql parser fails"""
|
|
7
|
-
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class SQLParser(metaclass=ABCMeta):
|
|
12
|
-
def __init__(self, sql_query: str, use_external_process: bool = True) -> None:
|
|
13
|
-
self._sql_query = sql_query
|
|
14
|
-
|
|
15
|
-
@abstractmethod
|
|
16
|
-
def get_tables(self) -> List[str]:
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
@abstractmethod
|
|
20
|
-
def get_columns(self) -> List[str]:
|
|
21
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|