1- # type: ignore
1+ from __future__ import annotations
22
33import re
4+ from typing import Generator
45
56import sqlglot
6- import sqlparse # type: ignore[import-untyped]
7- from sqlparse .sql import Function , Identifier , IdentifierList # type: ignore[import-untyped]
8- from sqlparse .tokens import DML , Keyword , Punctuation # type: ignore[import-untyped]
7+ import sqlparse
8+ from sqlparse .sql import Function , Identifier , IdentifierList , Token , TokenList
9+ from sqlparse .tokens import DML , Keyword , Punctuation
910
10- cleanup_regex = {
11+ cleanup_regex : dict [ str , re . Pattern ] = {
1112 # This matches only alphanumerics and underscores.
1213 "alphanum_underscore" : re .compile (r"(\w+)$" ),
1314 # This matches everything except spaces, parens, colon, and comma
1920}
2021
2122
22- def last_word (text , include = "alphanum_underscore" ):
23+ def last_word (text : str , include : str = "alphanum_underscore" ) -> str :
2324 r"""
2425 Find the last word in a sentence.
2526
@@ -67,7 +68,7 @@ def last_word(text, include="alphanum_underscore"):
6768
6869# This code is borrowed from sqlparse example script.
6970# <url>
70- def is_subselect (parsed ) :
71+ def is_subselect (parsed : TokenList ) -> bool :
7172 if not parsed .is_group :
7273 return False
7374 for item in parsed .tokens :
@@ -76,15 +77,15 @@ def is_subselect(parsed):
7677 return False
7778
7879
79- def extract_from_part (parsed , stop_at_punctuation = True ):
80+ def extract_from_part (parsed : TokenList , stop_at_punctuation : bool = True ) -> Generator [ str , None , None ] :
8081 tbl_prefix_seen = False
8182 for item in parsed .tokens :
8283 if tbl_prefix_seen :
8384 if is_subselect (item ):
8485 for x in extract_from_part (item , stop_at_punctuation ):
8586 yield x
8687 elif stop_at_punctuation and item .ttype is Punctuation :
87- return
88+ return None
8889 # Multiple JOINs in the same query won't work properly since
8990 # "ON" is a keyword and will trigger the next elif condition.
9091 # So instead of stooping the loop when finding an "ON" skip it
@@ -101,7 +102,7 @@ def extract_from_part(parsed, stop_at_punctuation=True):
101102 # condition. So we need to ignore the keyword JOIN and its variants
102103 # INNER JOIN, FULL OUTER JOIN, etc.
103104 elif item .ttype is Keyword and (not item .value .upper () == "FROM" ) and (not item .value .upper ().endswith ("JOIN" )):
104- return
105+ return None
105106 else :
106107 yield item
107108 elif (item .ttype is Keyword or item .ttype is Keyword .DML ) and item .value .upper () in (
@@ -122,7 +123,7 @@ def extract_from_part(parsed, stop_at_punctuation=True):
122123 break
123124
124125
125- def extract_table_identifiers (token_stream ) :
126+ def extract_table_identifiers (token_stream : TokenList ) -> Generator [ tuple [ str | None , str , str ]] :
126127 """yields tuples of (schema_name, table_name, table_alias)"""
127128
128129 for item in token_stream :
@@ -151,7 +152,7 @@ def extract_table_identifiers(token_stream):
151152
152153
153154# extract_tables is inspired from examples in the sqlparse lib.
154- def extract_tables (sql ) :
155+ def extract_tables (sql : str ) -> list [ tuple [ str | None , str , str ]] :
155156 """Extract the table names from an SQL statement.
156157
157158 Returns a list of (schema, table, alias) tuples
@@ -170,7 +171,7 @@ def extract_tables(sql):
170171 return list (extract_table_identifiers (stream ))
171172
172173
173- def extract_tables_from_complete_statements (sql ) :
174+ def extract_tables_from_complete_statements (sql : str ) -> list [ tuple [ str | None , str , str | None ]] :
174175 """Extract the table names from a complete and valid series of SQL
175176 statements.
176177
@@ -195,7 +196,7 @@ def extract_tables_from_complete_statements(sql):
195196 tables = []
196197 for statement in finely_parsed :
197198 for identifier in statement .find_all (sqlglot .exp .Table ):
198- if identifier .parent_select .sql ().startswith ('WITH' ):
199+ if identifier .parent_select and identifier . parent_select .sql ().startswith ('WITH' ):
199200 continue
200201 tables .append ((
201202 None if identifier .db == '' else identifier .db ,
@@ -206,7 +207,7 @@ def extract_tables_from_complete_statements(sql):
206207 return tables
207208
208209
209- def find_prev_keyword (sql ) :
210+ def find_prev_keyword (sql : str ) -> tuple [ Token | None , str ] :
210211 """Find the last sql keyword in an SQL statement
211212
212213 Returns the value of the last keyword, and the text of the query with
@@ -240,45 +241,40 @@ def find_prev_keyword(sql):
240241 return None , ""
241242
242243
243- def query_starts_with (query , prefixes ) :
244+ def query_starts_with (query : str , prefixes : list [ str ]) -> bool :
244245 """Check if the query starts with any item from *prefixes*."""
245246 prefixes = [prefix .lower () for prefix in prefixes ]
246247 formatted_sql = sqlparse .format (query .lower (), strip_comments = True )
247248 return bool (formatted_sql ) and formatted_sql .split ()[0 ] in prefixes
248249
249250
250- def queries_start_with (queries , prefixes ) :
251+ def queries_start_with (queries : str , prefixes : list [ str ]) -> bool :
251252 """Check if any queries start with any item from *prefixes*."""
252253 for query in sqlparse .split (queries ):
253254 if query and query_starts_with (query , prefixes ) is True :
254255 return True
255256 return False
256257
257258
258- def query_has_where_clause (query ) :
259+ def query_has_where_clause (query : str ) -> bool :
259260 """Check if the query contains a where-clause."""
260261 return any (isinstance (token , sqlparse .sql .Where ) for token_list in sqlparse .parse (query ) for token in token_list )
261262
262263
263- def is_destructive (queries ) :
264+ def is_destructive (queries : str ) -> bool :
264265 """Returns if any of the queries in *queries* is destructive."""
265266 keywords = ("drop" , "shutdown" , "delete" , "truncate" , "alter" )
266267 for query in sqlparse .split (queries ):
267268 if query :
268- if query_starts_with (query , keywords ) is True :
269+ if query_starts_with (query , list ( keywords ) ) is True :
269270 return True
270271 elif query_starts_with (query , ["update" ]) is True and not query_has_where_clause (query ):
271272 return True
272273
273274 return False
274275
275276
276- if __name__ == "__main__" :
277- sql = "select * from (select t. from tabl t"
278- print (extract_tables (sql ))
279-
280-
281- def is_dropping_database (queries , dbname ):
277+ def is_dropping_database (queries : list [str ], dbname : str | None ) -> bool :
282278 """Determine if the query is dropping a specific database."""
283279 result = False
284280 if dbname is None :
@@ -301,3 +297,8 @@ def normalize_db_name(db):
301297 if database_token is not None and normalize_db_name (database_token .get_name ()) == dbname :
302298 result = keywords [0 ].normalized == "DROP"
303299 return result
300+
301+
302+ if __name__ == "__main__" :
303+ sql = "select * from (select t. from tabl t"
304+ print (extract_tables (sql ))
0 commit comments