acryl-datahub 1.1.0.5rc5__py3-none-any.whl → 1.1.0.5rc7__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-1.1.0.5rc5.dist-info → acryl_datahub-1.1.0.5rc7.dist-info}/METADATA +2603 -2603
- {acryl_datahub-1.1.0.5rc5.dist-info → acryl_datahub-1.1.0.5rc7.dist-info}/RECORD +43 -39
- datahub/_version.py +1 -1
- datahub/ingestion/api/report.py +183 -35
- datahub/ingestion/autogenerated/capability_summary.json +3366 -0
- datahub/ingestion/autogenerated/lineage.json +401 -0
- datahub/ingestion/autogenerated/lineage_helper.py +30 -128
- datahub/ingestion/run/pipeline.py +4 -1
- datahub/ingestion/source/bigquery_v2/bigquery.py +23 -22
- datahub/ingestion/source/bigquery_v2/queries.py +2 -2
- datahub/ingestion/source/cassandra/cassandra_profiling.py +6 -5
- datahub/ingestion/source/common/subtypes.py +1 -1
- datahub/ingestion/source/data_lake_common/object_store.py +40 -0
- datahub/ingestion/source/data_lake_common/path_spec.py +10 -21
- datahub/ingestion/source/dremio/dremio_source.py +6 -3
- datahub/ingestion/source/gcs/gcs_source.py +4 -1
- datahub/ingestion/source/ge_data_profiler.py +28 -20
- datahub/ingestion/source/kafka_connect/source_connectors.py +59 -4
- datahub/ingestion/source/mock_data/datahub_mock_data.py +45 -0
- datahub/ingestion/source/redshift/usage.py +4 -3
- datahub/ingestion/source/s3/report.py +4 -2
- datahub/ingestion/source/s3/source.py +367 -115
- datahub/ingestion/source/snowflake/snowflake_queries.py +47 -3
- datahub/ingestion/source/snowflake/snowflake_usage_v2.py +8 -2
- datahub/ingestion/source/snowflake/stored_proc_lineage.py +143 -0
- datahub/ingestion/source/sql/athena.py +95 -10
- datahub/ingestion/source/sql/athena_properties_extractor.py +777 -0
- datahub/ingestion/source/unity/proxy.py +4 -3
- datahub/ingestion/source/unity/source.py +10 -8
- datahub/integrations/assertion/snowflake/compiler.py +4 -3
- datahub/metadata/_internal_schema_classes.py +85 -4
- datahub/metadata/com/linkedin/pegasus2avro/settings/global/__init__.py +2 -0
- datahub/metadata/schema.avsc +54 -1
- datahub/metadata/schemas/CorpUserSettings.avsc +17 -1
- datahub/metadata/schemas/GlobalSettingsInfo.avsc +37 -0
- datahub/sdk/lineage_client.py +2 -0
- datahub/sql_parsing/sql_parsing_aggregator.py +3 -3
- datahub/sql_parsing/sqlglot_lineage.py +2 -0
- datahub/utilities/sqlalchemy_query_combiner.py +5 -2
- {acryl_datahub-1.1.0.5rc5.dist-info → acryl_datahub-1.1.0.5rc7.dist-info}/WHEEL +0 -0
- {acryl_datahub-1.1.0.5rc5.dist-info → acryl_datahub-1.1.0.5rc7.dist-info}/entry_points.txt +0 -0
- {acryl_datahub-1.1.0.5rc5.dist-info → acryl_datahub-1.1.0.5rc7.dist-info}/licenses/LICENSE +0 -0
- {acryl_datahub-1.1.0.5rc5.dist-info → acryl_datahub-1.1.0.5rc7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Athena Properties Extractor - A robust tool for parsing CREATE TABLE statements.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to extract properties, partitioning information,
|
|
5
|
+
and row format details from Athena CREATE TABLE SQL statements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
12
|
+
|
|
13
|
+
from sqlglot import ParseError, parse_one
|
|
14
|
+
from sqlglot.dialects.athena import Athena
|
|
15
|
+
from sqlglot.expressions import (
|
|
16
|
+
Anonymous,
|
|
17
|
+
ColumnDef,
|
|
18
|
+
Create,
|
|
19
|
+
Day,
|
|
20
|
+
Expression,
|
|
21
|
+
FileFormatProperty,
|
|
22
|
+
Identifier,
|
|
23
|
+
LocationProperty,
|
|
24
|
+
Month,
|
|
25
|
+
PartitionByTruncate,
|
|
26
|
+
PartitionedByBucket,
|
|
27
|
+
PartitionedByProperty,
|
|
28
|
+
Property,
|
|
29
|
+
RowFormatDelimitedProperty,
|
|
30
|
+
Schema,
|
|
31
|
+
SchemaCommentProperty,
|
|
32
|
+
SerdeProperties,
|
|
33
|
+
Year,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AthenaPropertiesExtractionError(Exception):
|
|
38
|
+
"""Custom exception for Athena properties extraction errors."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ColumnInfo:
|
|
45
|
+
"""Information about a table column."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
type: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class TransformInfo:
|
|
53
|
+
"""Information about a partition transform."""
|
|
54
|
+
|
|
55
|
+
type: str
|
|
56
|
+
column: ColumnInfo
|
|
57
|
+
bucket_count: Optional[int] = None
|
|
58
|
+
length: Optional[int] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class PartitionInfo:
|
|
63
|
+
"""Information about table partitioning."""
|
|
64
|
+
|
|
65
|
+
simple_columns: List[ColumnInfo]
|
|
66
|
+
transforms: List[TransformInfo]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class TableProperties:
|
|
71
|
+
"""General table properties."""
|
|
72
|
+
|
|
73
|
+
location: Optional[str] = None
|
|
74
|
+
format: Optional[str] = None
|
|
75
|
+
comment: Optional[str] = None
|
|
76
|
+
serde_properties: Optional[Dict[str, str]] = None
|
|
77
|
+
row_format: Optional[Dict[str, str]] = None
|
|
78
|
+
additional_properties: Optional[Dict[str, str]] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class RowFormatInfo:
|
|
83
|
+
"""Row format information."""
|
|
84
|
+
|
|
85
|
+
properties: Dict[str, str]
|
|
86
|
+
json_formatted: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class AthenaTableInfo:
|
|
91
|
+
"""Complete information about an Athena table."""
|
|
92
|
+
|
|
93
|
+
partition_info: PartitionInfo
|
|
94
|
+
table_properties: TableProperties
|
|
95
|
+
row_format: RowFormatInfo
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AthenaPropertiesExtractor:
|
|
99
|
+
"""A class to extract properties from Athena CREATE TABLE statements."""
|
|
100
|
+
|
|
101
|
+
CREATE_TABLE_REGEXP = re.compile(
|
|
102
|
+
"(CREATE TABLE[\s\n]*)(.*?)(\s*\()", re.MULTILINE | re.IGNORECASE
|
|
103
|
+
)
|
|
104
|
+
PARTITIONED_BY_REGEXP = re.compile(
|
|
105
|
+
"(PARTITIONED BY[\s\n]*\()((?:[^()]|\([^)]*\))*?)(\))",
|
|
106
|
+
re.MULTILINE | re.IGNORECASE,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
"""Initialize the extractor."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def get_table_properties(sql: str) -> AthenaTableInfo:
|
|
115
|
+
"""Get all table properties from a SQL statement.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
sql: The SQL statement to parse
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
An AthenaTableInfo object containing all table properties
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
AthenaPropertiesExtractionError: If extraction fails
|
|
125
|
+
"""
|
|
126
|
+
extractor = AthenaPropertiesExtractor()
|
|
127
|
+
return extractor._extract_all_properties(sql)
|
|
128
|
+
|
|
129
|
+
def _extract_all_properties(self, sql: str) -> AthenaTableInfo:
|
|
130
|
+
"""Extract all properties from a SQL statement.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
sql: The SQL statement to parse
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
An AthenaTableInfo object containing all properties
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
AthenaPropertiesExtractionError: If extraction fails
|
|
140
|
+
"""
|
|
141
|
+
if not sql or not sql.strip():
|
|
142
|
+
raise AthenaPropertiesExtractionError("SQL statement cannot be empty")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# We need to do certain transformations on the sql create statement:
|
|
146
|
+
# - table names are not quoted
|
|
147
|
+
# - column expression is not quoted
|
|
148
|
+
# - sql parser fails if partition colums quoted
|
|
149
|
+
fixed_sql = self._fix_sql_partitioning(sql)
|
|
150
|
+
parsed = parse_one(fixed_sql, dialect=Athena)
|
|
151
|
+
except ParseError as e:
|
|
152
|
+
raise AthenaPropertiesExtractionError(f"Failed to parse SQL: {e}") from e
|
|
153
|
+
except Exception as e:
|
|
154
|
+
raise AthenaPropertiesExtractionError(
|
|
155
|
+
f"Unexpected error during SQL parsing: {e}"
|
|
156
|
+
) from e
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
partition_info = self._extract_partition_info(parsed)
|
|
160
|
+
table_properties = self._extract_table_properties(parsed)
|
|
161
|
+
row_format = self._extract_row_format(parsed)
|
|
162
|
+
|
|
163
|
+
return AthenaTableInfo(
|
|
164
|
+
partition_info=partition_info,
|
|
165
|
+
table_properties=table_properties,
|
|
166
|
+
row_format=row_format,
|
|
167
|
+
)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise AthenaPropertiesExtractionError(
|
|
170
|
+
f"Failed to extract table properties: {e}"
|
|
171
|
+
) from e
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def format_column_definition(line):
|
|
175
|
+
# Use regex to parse the line more accurately
|
|
176
|
+
# Pattern: column_name data_type [COMMENT comment_text] [,]
|
|
177
|
+
# Use greedy match for comment to capture everything until trailing comma
|
|
178
|
+
pattern = r"^\s*(.+?)\s+([\s,\w<>\[\]]+)((\s+COMMENT\s+(.+?)(,?))|(,?)\s*)?$"
|
|
179
|
+
match = re.match(pattern, line, re.IGNORECASE)
|
|
180
|
+
|
|
181
|
+
if not match:
|
|
182
|
+
return line
|
|
183
|
+
column_name = match.group(1)
|
|
184
|
+
data_type = match.group(2)
|
|
185
|
+
comment_part = match.group(5) # COMMENT part
|
|
186
|
+
# there are different number of match groups depending on whether comment exists
|
|
187
|
+
if comment_part:
|
|
188
|
+
trailing_comma = match.group(6) if match.group(6) else ""
|
|
189
|
+
else:
|
|
190
|
+
trailing_comma = match.group(7) if match.group(7) else ""
|
|
191
|
+
|
|
192
|
+
# Add backticks to column name if not already present
|
|
193
|
+
if not (column_name.startswith("`") and column_name.endswith("`")):
|
|
194
|
+
column_name = f"`{column_name}`"
|
|
195
|
+
|
|
196
|
+
# Build the result
|
|
197
|
+
result_parts = [column_name, data_type]
|
|
198
|
+
|
|
199
|
+
if comment_part:
|
|
200
|
+
comment_part = comment_part.strip()
|
|
201
|
+
|
|
202
|
+
# Handle comment quoting and escaping
|
|
203
|
+
if comment_part.startswith("'") and comment_part.endswith("'"):
|
|
204
|
+
# Already properly single quoted - keep as is
|
|
205
|
+
formatted_comment = comment_part
|
|
206
|
+
elif comment_part.startswith('"') and comment_part.endswith('"'):
|
|
207
|
+
# Double quoted - convert to single quotes and escape internal single quotes
|
|
208
|
+
inner_content = comment_part[1:-1]
|
|
209
|
+
escaped_content = inner_content.replace("'", "''")
|
|
210
|
+
formatted_comment = f"'{escaped_content}'"
|
|
211
|
+
else:
|
|
212
|
+
# Not quoted - add quotes and escape any single quotes
|
|
213
|
+
escaped_content = comment_part.replace("'", "''")
|
|
214
|
+
formatted_comment = f"'{escaped_content}'"
|
|
215
|
+
|
|
216
|
+
result_parts.extend(["COMMENT", formatted_comment])
|
|
217
|
+
|
|
218
|
+
result = " " + " ".join(result_parts) + trailing_comma
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def format_athena_column_definitions(sql_statement: str) -> str:
|
|
224
|
+
"""
|
|
225
|
+
Format Athena CREATE TABLE statement by:
|
|
226
|
+
1. Adding backticks around column names in column definitions (only in the main table definition)
|
|
227
|
+
2. Quoting comments (if any exist)
|
|
228
|
+
"""
|
|
229
|
+
lines = sql_statement.split("\n")
|
|
230
|
+
formatted_lines = []
|
|
231
|
+
|
|
232
|
+
in_column_definition = False
|
|
233
|
+
|
|
234
|
+
for line in lines:
|
|
235
|
+
stripped_line = line.strip()
|
|
236
|
+
|
|
237
|
+
# Check if we're entering column definitions
|
|
238
|
+
if "CREATE TABLE" in line.upper() and "(" in line:
|
|
239
|
+
in_column_definition = True
|
|
240
|
+
formatted_lines.append(line)
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Check if we're exiting column definitions (closing parenthesis before PARTITIONED BY or end)
|
|
244
|
+
if in_column_definition and ")" in line:
|
|
245
|
+
in_column_definition = False
|
|
246
|
+
formatted_lines.append(line)
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Process only column definitions (not PARTITIONED BY or other sections)
|
|
250
|
+
if in_column_definition and stripped_line:
|
|
251
|
+
# Match column definition pattern and format it
|
|
252
|
+
formatted_line = AthenaPropertiesExtractor.format_column_definition(
|
|
253
|
+
line
|
|
254
|
+
)
|
|
255
|
+
formatted_lines.append(formatted_line)
|
|
256
|
+
else:
|
|
257
|
+
# For all other lines, keep as-is
|
|
258
|
+
formatted_lines.append(line)
|
|
259
|
+
|
|
260
|
+
return "\n".join(formatted_lines)
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def _fix_sql_partitioning(sql: str) -> str:
|
|
264
|
+
"""Fix SQL partitioning by removing backticks from partition expressions and quoting table names.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
sql: The SQL statement to fix
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
The fixed SQL statement
|
|
271
|
+
"""
|
|
272
|
+
if not sql:
|
|
273
|
+
return sql
|
|
274
|
+
|
|
275
|
+
# Quote table name
|
|
276
|
+
table_name_match = AthenaPropertiesExtractor.CREATE_TABLE_REGEXP.search(sql)
|
|
277
|
+
|
|
278
|
+
if table_name_match:
|
|
279
|
+
table_name = table_name_match.group(2).strip()
|
|
280
|
+
if table_name and not (table_name.startswith("`") or "`" in table_name):
|
|
281
|
+
# Split on dots and quote each part
|
|
282
|
+
quoted_parts = [
|
|
283
|
+
f"`{part.strip()}`"
|
|
284
|
+
for part in table_name.split(".")
|
|
285
|
+
if part.strip()
|
|
286
|
+
]
|
|
287
|
+
if quoted_parts:
|
|
288
|
+
quoted_table = ".".join(quoted_parts)
|
|
289
|
+
create_part = table_name_match.group(0).replace(
|
|
290
|
+
table_name, quoted_table
|
|
291
|
+
)
|
|
292
|
+
sql = sql.replace(table_name_match.group(0), create_part)
|
|
293
|
+
|
|
294
|
+
# Fix partition expressions
|
|
295
|
+
partition_match = AthenaPropertiesExtractor.PARTITIONED_BY_REGEXP.search(sql)
|
|
296
|
+
|
|
297
|
+
if partition_match:
|
|
298
|
+
partition_section = partition_match.group(2)
|
|
299
|
+
if partition_section:
|
|
300
|
+
partition_section_modified = partition_section.replace("`", "")
|
|
301
|
+
sql = sql.replace(partition_section, partition_section_modified)
|
|
302
|
+
|
|
303
|
+
return AthenaPropertiesExtractor.format_athena_column_definitions(sql)
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def _extract_column_types(create_expr: Create) -> Dict[str, str]:
|
|
307
|
+
"""Extract column types from a CREATE TABLE expression.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
create_expr: The CREATE TABLE expression to extract types from
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
A dictionary mapping column names to their types
|
|
314
|
+
"""
|
|
315
|
+
column_types: Dict[str, str] = {}
|
|
316
|
+
|
|
317
|
+
if not create_expr.this or not hasattr(create_expr.this, "expressions"):
|
|
318
|
+
return column_types
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
for expr in create_expr.this.expressions:
|
|
322
|
+
if isinstance(expr, ColumnDef) and expr.this:
|
|
323
|
+
column_types[expr.name] = str(expr.kind)
|
|
324
|
+
except Exception:
|
|
325
|
+
# If we can't extract column types, return empty dict
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
return column_types
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def _create_column_info(column_name: str, column_type: str) -> ColumnInfo:
|
|
332
|
+
"""Create a column info object.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
column_name: Name of the column
|
|
336
|
+
column_type: Type of the column
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
A ColumnInfo object
|
|
340
|
+
"""
|
|
341
|
+
return ColumnInfo(
|
|
342
|
+
name=str(column_name) if column_name else "unknown",
|
|
343
|
+
type=column_type if column_type else "unknown",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _handle_function_expression(
|
|
348
|
+
expr: Identifier, column_types: Dict[str, str]
|
|
349
|
+
) -> Tuple[ColumnInfo, TransformInfo]:
|
|
350
|
+
"""Handle function expressions like day(event_timestamp).
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
expr: The function expression to handle
|
|
354
|
+
column_types: Dictionary of column types
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
A tuple of (column_info, transform_info)
|
|
358
|
+
"""
|
|
359
|
+
func_str = str(expr)
|
|
360
|
+
|
|
361
|
+
if "(" not in func_str or ")" not in func_str:
|
|
362
|
+
# Fallback for malformed function expressions
|
|
363
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
364
|
+
func_str, "unknown"
|
|
365
|
+
)
|
|
366
|
+
transform_info = TransformInfo(type="unknown", column=column_info)
|
|
367
|
+
return column_info, transform_info
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
func_name = func_str.split("(")[0].lower()
|
|
371
|
+
column_part = func_str.split("(")[1].split(")")[0].strip("`")
|
|
372
|
+
|
|
373
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
374
|
+
column_part, column_types.get(column_part, "unknown")
|
|
375
|
+
)
|
|
376
|
+
transform_info = TransformInfo(type=func_name, column=column_info)
|
|
377
|
+
|
|
378
|
+
return column_info, transform_info
|
|
379
|
+
except (IndexError, AttributeError):
|
|
380
|
+
# Fallback for parsing errors
|
|
381
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
382
|
+
func_str, "unknown"
|
|
383
|
+
)
|
|
384
|
+
transform_info = TransformInfo(type="unknown", column=column_info)
|
|
385
|
+
return column_info, transform_info
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def _handle_time_function(
|
|
389
|
+
expr: Union[Year, Month, Day], column_types: Dict[str, str]
|
|
390
|
+
) -> Tuple[ColumnInfo, TransformInfo]:
|
|
391
|
+
"""Handle time-based functions like year, month, day.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
expr: The time function expression to handle
|
|
395
|
+
column_types: Dictionary of column types
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
A tuple of (column_info, transform_info)
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
# Navigate the expression tree safely
|
|
402
|
+
column_name = "unknown"
|
|
403
|
+
if hasattr(expr, "this") and expr.this:
|
|
404
|
+
if hasattr(expr.this, "this") and expr.this.this:
|
|
405
|
+
if hasattr(expr.this.this, "this") and expr.this.this.this:
|
|
406
|
+
column_name = str(expr.this.this.this)
|
|
407
|
+
else:
|
|
408
|
+
column_name = str(expr.this.this)
|
|
409
|
+
else:
|
|
410
|
+
column_name = str(expr.this)
|
|
411
|
+
|
|
412
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
413
|
+
column_name, column_types.get(column_name, "unknown")
|
|
414
|
+
)
|
|
415
|
+
transform_info = TransformInfo(
|
|
416
|
+
type=expr.__class__.__name__.lower(), column=column_info
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return column_info, transform_info
|
|
420
|
+
except (AttributeError, TypeError):
|
|
421
|
+
# Fallback for navigation errors
|
|
422
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
423
|
+
"unknown", "unknown"
|
|
424
|
+
)
|
|
425
|
+
transform_info = TransformInfo(type="unknown", column=column_info)
|
|
426
|
+
return column_info, transform_info
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _handle_transform_function(
|
|
430
|
+
expr: Anonymous, column_types: Dict[str, str]
|
|
431
|
+
) -> Tuple[ColumnInfo, TransformInfo]:
|
|
432
|
+
"""Handle transform functions like bucket, hour, truncate.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
expr: The transform function expression to handle
|
|
436
|
+
column_types: Dictionary of column types
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
A tuple of (column_info, transform_info)
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
# Safely extract column name from the last expression
|
|
443
|
+
column_name = "unknown"
|
|
444
|
+
if (
|
|
445
|
+
hasattr(expr, "expressions")
|
|
446
|
+
and expr.expressions
|
|
447
|
+
and len(expr.expressions) > 0
|
|
448
|
+
):
|
|
449
|
+
last_expr = expr.expressions[-1]
|
|
450
|
+
if hasattr(last_expr, "this") and last_expr.this:
|
|
451
|
+
if hasattr(last_expr.this, "this") and last_expr.this.this:
|
|
452
|
+
column_name = str(last_expr.this.this)
|
|
453
|
+
else:
|
|
454
|
+
column_name = str(last_expr.this)
|
|
455
|
+
|
|
456
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
457
|
+
column_name, column_types.get(column_name, "unknown")
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
transform_type = str(expr.this).lower() if expr.this else "unknown"
|
|
461
|
+
transform_info = TransformInfo(type=transform_type, column=column_info)
|
|
462
|
+
|
|
463
|
+
# Add transform-specific parameters safely
|
|
464
|
+
if (
|
|
465
|
+
transform_type == "bucket"
|
|
466
|
+
and hasattr(expr, "expressions")
|
|
467
|
+
and expr.expressions
|
|
468
|
+
and len(expr.expressions) > 0
|
|
469
|
+
):
|
|
470
|
+
first_expr = expr.expressions[0]
|
|
471
|
+
if hasattr(first_expr, "this"):
|
|
472
|
+
transform_info.bucket_count = first_expr.this
|
|
473
|
+
elif (
|
|
474
|
+
transform_type == "truncate"
|
|
475
|
+
and hasattr(expr, "expressions")
|
|
476
|
+
and expr.expressions
|
|
477
|
+
and len(expr.expressions) > 0
|
|
478
|
+
):
|
|
479
|
+
first_expr = expr.expressions[0]
|
|
480
|
+
if hasattr(first_expr, "this"):
|
|
481
|
+
transform_info.length = first_expr.this
|
|
482
|
+
|
|
483
|
+
return column_info, transform_info
|
|
484
|
+
except (AttributeError, TypeError, IndexError):
|
|
485
|
+
# Fallback for any parsing errors
|
|
486
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
487
|
+
"unknown", "unknown"
|
|
488
|
+
)
|
|
489
|
+
transform_info = TransformInfo(type="unknown", column=column_info)
|
|
490
|
+
return column_info, transform_info
|
|
491
|
+
|
|
492
|
+
def _extract_partition_info(self, parsed: Expression) -> PartitionInfo:
|
|
493
|
+
"""Extract partitioning information from the parsed SQL statement.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
parsed: The parsed SQL expression
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
A PartitionInfo object containing simple columns and transforms
|
|
500
|
+
"""
|
|
501
|
+
# Get the PARTITIONED BY expression
|
|
502
|
+
partition_by_expr: Optional[Schema] = None
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
for prop in parsed.find_all(Property):
|
|
506
|
+
if isinstance(prop, PartitionedByProperty):
|
|
507
|
+
partition_by_expr = prop.this
|
|
508
|
+
break
|
|
509
|
+
except Exception:
|
|
510
|
+
# If we can't find properties, return empty result
|
|
511
|
+
return PartitionInfo(simple_columns=[], transforms=[])
|
|
512
|
+
|
|
513
|
+
if not partition_by_expr:
|
|
514
|
+
return PartitionInfo(simple_columns=[], transforms=[])
|
|
515
|
+
|
|
516
|
+
# Extract partitioning columns and transforms
|
|
517
|
+
simple_columns: List[ColumnInfo] = []
|
|
518
|
+
transforms: List[TransformInfo] = []
|
|
519
|
+
|
|
520
|
+
# Get column types from the table definition
|
|
521
|
+
column_types: Dict[str, str] = {}
|
|
522
|
+
if isinstance(parsed, Create):
|
|
523
|
+
column_types = self._extract_column_types(parsed)
|
|
524
|
+
|
|
525
|
+
# Process each expression in the PARTITIONED BY clause
|
|
526
|
+
if hasattr(partition_by_expr, "expressions") and partition_by_expr.expressions:
|
|
527
|
+
for expr in partition_by_expr.expressions:
|
|
528
|
+
try:
|
|
529
|
+
if isinstance(expr, Identifier) and "(" in str(expr):
|
|
530
|
+
column_info, transform_info = self._handle_function_expression(
|
|
531
|
+
expr, column_types
|
|
532
|
+
)
|
|
533
|
+
simple_columns.append(column_info)
|
|
534
|
+
transforms.append(transform_info)
|
|
535
|
+
elif isinstance(expr, PartitionByTruncate):
|
|
536
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
537
|
+
str(expr.this), column_types.get(str(expr.this), "unknown")
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
expression = expr.args.get("expression")
|
|
541
|
+
transform_info = TransformInfo(
|
|
542
|
+
type="truncate",
|
|
543
|
+
column=column_info,
|
|
544
|
+
length=int(expression.name)
|
|
545
|
+
if expression and expression.name
|
|
546
|
+
else None,
|
|
547
|
+
)
|
|
548
|
+
transforms.append(transform_info)
|
|
549
|
+
simple_columns.append(column_info)
|
|
550
|
+
elif isinstance(expr, PartitionedByBucket):
|
|
551
|
+
column_info = AthenaPropertiesExtractor._create_column_info(
|
|
552
|
+
str(expr.this), column_types.get(str(expr.this), "unknown")
|
|
553
|
+
)
|
|
554
|
+
expression = expr.args.get("expression")
|
|
555
|
+
transform_info = TransformInfo(
|
|
556
|
+
type="bucket",
|
|
557
|
+
column=column_info,
|
|
558
|
+
bucket_count=int(expression.name)
|
|
559
|
+
if expression and expression.name
|
|
560
|
+
else None,
|
|
561
|
+
)
|
|
562
|
+
simple_columns.append(column_info)
|
|
563
|
+
transforms.append(transform_info)
|
|
564
|
+
elif isinstance(expr, (Year, Month, Day)):
|
|
565
|
+
column_info, transform_info = self._handle_time_function(
|
|
566
|
+
expr, column_types
|
|
567
|
+
)
|
|
568
|
+
transforms.append(transform_info)
|
|
569
|
+
simple_columns.append(column_info)
|
|
570
|
+
elif (
|
|
571
|
+
isinstance(expr, Anonymous)
|
|
572
|
+
and expr.this
|
|
573
|
+
and str(expr.this).lower() in ["bucket", "hour", "truncate"]
|
|
574
|
+
):
|
|
575
|
+
column_info, transform_info = self._handle_transform_function(
|
|
576
|
+
expr, column_types
|
|
577
|
+
)
|
|
578
|
+
transforms.append(transform_info)
|
|
579
|
+
simple_columns.append(column_info)
|
|
580
|
+
elif hasattr(expr, "this") and expr.this:
|
|
581
|
+
column_name = str(expr.this)
|
|
582
|
+
column_info = self._create_column_info(
|
|
583
|
+
column_name, column_types.get(column_name, "unknown")
|
|
584
|
+
)
|
|
585
|
+
simple_columns.append(column_info)
|
|
586
|
+
except Exception:
|
|
587
|
+
# Skip problematic expressions rather than failing completely
|
|
588
|
+
continue
|
|
589
|
+
|
|
590
|
+
# Remove duplicates from simple_columns while preserving order
|
|
591
|
+
seen_names: Set[str] = set()
|
|
592
|
+
unique_simple_columns: List[ColumnInfo] = []
|
|
593
|
+
|
|
594
|
+
for col in simple_columns:
|
|
595
|
+
if col.name and col.name not in seen_names:
|
|
596
|
+
seen_names.add(col.name)
|
|
597
|
+
unique_simple_columns.append(col)
|
|
598
|
+
|
|
599
|
+
return PartitionInfo(
|
|
600
|
+
simple_columns=unique_simple_columns, transforms=transforms
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def _extract_table_properties(self, parsed: Expression) -> TableProperties:
|
|
604
|
+
"""Extract table properties from the parsed SQL statement.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
parsed: The parsed SQL expression
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
A TableProperties object
|
|
611
|
+
"""
|
|
612
|
+
location: Optional[str] = None
|
|
613
|
+
format_prop: Optional[str] = None
|
|
614
|
+
comment: Optional[str] = None
|
|
615
|
+
serde_properties: Optional[Dict[str, str]] = None
|
|
616
|
+
row_format: Optional[Dict[str, str]] = None
|
|
617
|
+
additional_properties: Dict[str, str] = {}
|
|
618
|
+
|
|
619
|
+
try:
|
|
620
|
+
props = list(parsed.find_all(Property))
|
|
621
|
+
except Exception:
|
|
622
|
+
return TableProperties()
|
|
623
|
+
|
|
624
|
+
for prop in props:
|
|
625
|
+
try:
|
|
626
|
+
if isinstance(prop, LocationProperty):
|
|
627
|
+
location = self._safe_get_property_value(prop)
|
|
628
|
+
|
|
629
|
+
elif isinstance(prop, FileFormatProperty):
|
|
630
|
+
format_prop = self._safe_get_property_value(prop)
|
|
631
|
+
|
|
632
|
+
elif isinstance(prop, SchemaCommentProperty):
|
|
633
|
+
comment = self._safe_get_property_value(prop)
|
|
634
|
+
|
|
635
|
+
elif isinstance(prop, PartitionedByProperty):
|
|
636
|
+
continue # Skip partition properties here
|
|
637
|
+
|
|
638
|
+
elif isinstance(prop, SerdeProperties):
|
|
639
|
+
serde_props = self._extract_serde_properties(prop)
|
|
640
|
+
if serde_props:
|
|
641
|
+
serde_properties = serde_props
|
|
642
|
+
|
|
643
|
+
elif isinstance(prop, RowFormatDelimitedProperty):
|
|
644
|
+
row_format_props = self._extract_row_format_properties(prop)
|
|
645
|
+
if row_format_props:
|
|
646
|
+
row_format = row_format_props
|
|
647
|
+
|
|
648
|
+
else:
|
|
649
|
+
# Handle generic properties
|
|
650
|
+
key, value = self._extract_generic_property(prop)
|
|
651
|
+
if (
|
|
652
|
+
key
|
|
653
|
+
and value
|
|
654
|
+
and (not serde_properties or key not in serde_properties)
|
|
655
|
+
):
|
|
656
|
+
additional_properties[key] = value
|
|
657
|
+
|
|
658
|
+
except Exception:
|
|
659
|
+
# Skip problematic properties rather than failing completely
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
if (
|
|
663
|
+
not location
|
|
664
|
+
and additional_properties
|
|
665
|
+
and additional_properties.get("external_location")
|
|
666
|
+
):
|
|
667
|
+
location = additional_properties.pop("external_location")
|
|
668
|
+
|
|
669
|
+
return TableProperties(
|
|
670
|
+
location=location,
|
|
671
|
+
format=format_prop,
|
|
672
|
+
comment=comment,
|
|
673
|
+
serde_properties=serde_properties,
|
|
674
|
+
row_format=row_format,
|
|
675
|
+
additional_properties=additional_properties
|
|
676
|
+
if additional_properties
|
|
677
|
+
else None,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def _safe_get_property_value(self, prop: Property) -> Optional[str]:
|
|
681
|
+
"""Safely extract value from a property."""
|
|
682
|
+
try:
|
|
683
|
+
if (
|
|
684
|
+
hasattr(prop, "args")
|
|
685
|
+
and "this" in prop.args
|
|
686
|
+
and prop.args["this"]
|
|
687
|
+
and hasattr(prop.args["this"], "name")
|
|
688
|
+
):
|
|
689
|
+
return prop.args["this"].name
|
|
690
|
+
except (AttributeError, KeyError, TypeError):
|
|
691
|
+
pass
|
|
692
|
+
return None
|
|
693
|
+
|
|
694
|
+
def _extract_serde_properties(self, prop: SerdeProperties) -> Dict[str, str]:
|
|
695
|
+
"""Extract SERDE properties safely."""
|
|
696
|
+
serde_props: Dict[str, str] = {}
|
|
697
|
+
try:
|
|
698
|
+
if hasattr(prop, "expressions") and prop.expressions:
|
|
699
|
+
for exp in prop.expressions:
|
|
700
|
+
if (
|
|
701
|
+
hasattr(exp, "name")
|
|
702
|
+
and hasattr(exp, "args")
|
|
703
|
+
and "value" in exp.args
|
|
704
|
+
and exp.args["value"]
|
|
705
|
+
and hasattr(exp.args["value"], "name")
|
|
706
|
+
):
|
|
707
|
+
serde_props[exp.name] = exp.args["value"].name
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
710
|
+
return serde_props
|
|
711
|
+
|
|
712
|
+
def _extract_row_format_properties(
|
|
713
|
+
self, prop: RowFormatDelimitedProperty
|
|
714
|
+
) -> Dict[str, str]:
|
|
715
|
+
"""Extract row format properties safely."""
|
|
716
|
+
row_format: Dict[str, str] = {}
|
|
717
|
+
try:
|
|
718
|
+
if hasattr(prop, "args") and prop.args:
|
|
719
|
+
for key, value in prop.args.items():
|
|
720
|
+
if hasattr(value, "this"):
|
|
721
|
+
row_format[key] = str(value.this)
|
|
722
|
+
else:
|
|
723
|
+
row_format[key] = str(value)
|
|
724
|
+
except Exception:
|
|
725
|
+
pass
|
|
726
|
+
return row_format
|
|
727
|
+
|
|
728
|
+
def _extract_generic_property(
|
|
729
|
+
self, prop: Property
|
|
730
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
731
|
+
"""Extract key-value pair from generic property."""
|
|
732
|
+
try:
|
|
733
|
+
if (
|
|
734
|
+
hasattr(prop, "args")
|
|
735
|
+
and "this" in prop.args
|
|
736
|
+
and prop.args["this"]
|
|
737
|
+
and hasattr(prop.args["this"], "name")
|
|
738
|
+
and "value" in prop.args
|
|
739
|
+
and prop.args["value"]
|
|
740
|
+
and hasattr(prop.args["value"], "name")
|
|
741
|
+
):
|
|
742
|
+
key = prop.args["this"].name.lower()
|
|
743
|
+
value = prop.args["value"].name
|
|
744
|
+
return key, value
|
|
745
|
+
except (AttributeError, KeyError, TypeError):
|
|
746
|
+
pass
|
|
747
|
+
return None, None
|
|
748
|
+
|
|
749
|
+
def _extract_row_format(self, parsed: Expression) -> RowFormatInfo:
|
|
750
|
+
"""Extract and format RowFormatDelimitedProperty.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
parsed: The parsed SQL expression
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
A RowFormatInfo object
|
|
757
|
+
"""
|
|
758
|
+
row_format_props: Dict[str, str] = {}
|
|
759
|
+
|
|
760
|
+
try:
|
|
761
|
+
props = parsed.find_all(Property)
|
|
762
|
+
for prop in props:
|
|
763
|
+
if isinstance(prop, RowFormatDelimitedProperty):
|
|
764
|
+
row_format_props = self._extract_row_format_properties(prop)
|
|
765
|
+
break
|
|
766
|
+
except Exception:
|
|
767
|
+
pass
|
|
768
|
+
|
|
769
|
+
if row_format_props:
|
|
770
|
+
try:
|
|
771
|
+
json_formatted = json.dumps(row_format_props, indent=2)
|
|
772
|
+
except (TypeError, ValueError):
|
|
773
|
+
json_formatted = "Error formatting row format properties"
|
|
774
|
+
else:
|
|
775
|
+
json_formatted = "No RowFormatDelimitedProperty found"
|
|
776
|
+
|
|
777
|
+
return RowFormatInfo(properties=row_format_props, json_formatted=json_formatted)
|