Skip to content

Commit e537d02

Browse files
authored
Merge pull request #22 from macbre/sql-normalization
Move normalize_sql to sql-metadata
2 parents 0298518 + da35476 commit e537d02

3 files changed

Lines changed: 178 additions & 1 deletion

File tree

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[![PyPI](https://img.shields.io/pypi/v/sql_metadata.svg)](https://pypi.python.org/pypi/sql_metadata)
44
[![Build Status](https://travis-ci.org/macbre/sql-metadata.svg?branch=master)](https://travis-ci.org/macbre/sql-metadata)
55

6-
Uses tokenized query returned by [`python-sqlparse`](https://github.com/andialbrecht/sqlparse) and generates query metadata. Extracts column names and tables used by the query.
6+
Uses tokenized query returned by [`python-sqlparse`](https://github.com/andialbrecht/sqlparse) and generates query metadata.
7+
Extracts column names and tables used by the query. Provides a helper for normalization of SQL queries.
78

89
### Usage
910

@@ -34,3 +35,13 @@ pip install sql_metadata
3435
```
3536

3637
> See `test/test_query.py` file for more examples of a bit more complex queries.
38+
39+
#### Queries normalization
40+
41+
```python
42+
>>> from sql_metadata import generalize_sql
43+
>>> generalize_sql('SELECT /* Test */ foo FROM bar WHERE id in (1, 2, 56)')
44+
'SELECT foo FROM bar WHERE id in (XYZ)'
45+
```
46+
47+
> See `test/test_normalization.py` file for more examples of a bit more complex queries.

sql_metadata.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,77 @@ def get_query_limit_and_offset(query):
179179
return None
180180

181181
return limit, offset or 0
182+
183+
184+
# SQL queries normalization (#16)
185+
def normalize_likes(sql):
186+
"""
187+
Normalize and wrap LIKE statements
188+
189+
:type sql str
190+
:rtype: str
191+
"""
192+
sql = sql.replace('%', '')
193+
194+
# LIKE '%bot'
195+
sql = re.sub(r"LIKE '[^\']+'", 'LIKE X', sql)
196+
197+
# or all_groups LIKE X or all_groups LIKE X
198+
matches = re.finditer(r'(or|and) [^\s]+ LIKE X', sql, flags=re.IGNORECASE)
199+
matches = set([match.group(0) for match in matches]) if matches else None
200+
201+
if matches:
202+
for match in matches:
203+
sql = re.sub(r'(\s?' + re.escape(match) + ')+', ' ' + match + ' ...', sql)
204+
205+
return sql
206+
207+
208+
def remove_comments_from_sql(sql):
209+
"""
210+
Removes comments from SQL query
211+
212+
:type sql str|None
213+
:rtype: str
214+
"""
215+
return re.sub(r'\s?/\*.+\*/', '', sql)
216+
217+
218+
def generalize_sql(sql):
219+
"""
220+
Removes most variables from an SQL query and replaces them with X or N for numbers.
221+
222+
Based on Mediawiki's DatabaseBase::generalizeSQL
223+
224+
:type sql str|None
225+
:rtype: str
226+
"""
227+
if sql is None:
228+
return None
229+
230+
# multiple spaces
231+
sql = re.sub(r'\s{2,}', ' ', sql)
232+
233+
# MW comments
234+
# e.g. /* CategoryDataService::getMostVisited N.N.N.N */
235+
sql = remove_comments_from_sql(sql)
236+
237+
# handle LIKE statements
238+
sql = normalize_likes(sql)
239+
240+
sql = re.sub(r"\\\\", '', sql)
241+
sql = re.sub(r"\\'", '', sql)
242+
sql = re.sub(r'\\"', '', sql)
243+
sql = re.sub(r"'[^\']*'", 'X', sql)
244+
sql = re.sub(r'"[^\"]*"', 'X', sql)
245+
246+
# All newlines, tabs, etc replaced by single space
247+
sql = re.sub(r'\s+', ' ', sql)
248+
249+
# All numbers => N
250+
sql = re.sub(r'-?[0-9]+', 'N', sql)
251+
252+
# WHERE foo IN ('880987','882618','708228','522330')
253+
sql = re.sub(r' (IN|VALUES)\s*\([^,]+,[^)]+\)', ' \\1 (XYZ)', sql, flags=re.IGNORECASE)
254+
255+
return sql.strip()

test/test_normalization.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from sql_metadata import generalize_sql, remove_comments_from_sql
5+
6+
7+
def test_generalize_sql():
8+
assert generalize_sql(None) is None
9+
10+
assert remove_comments_from_sql('SELECT /* Test */ foo FROM BAR') == 'SELECT foo FROM BAR'
11+
12+
assert generalize_sql(
13+
"UPDATE `category` SET cat_pages = cat_pages + 1,cat_files = cat_files + 1 WHERE cat_title = 'foo'") == \
14+
"UPDATE `category` SET cat_pages = cat_pages + N,cat_files = cat_files + N WHERE cat_title = X"
15+
16+
assert generalize_sql(
17+
"SELECT entity_key FROM `wall_notification_queue` WHERE (wiki_id = ) AND (event_date > '20150105141012')") == \
18+
"SELECT entity_key FROM `wall_notification_queue` WHERE (wiki_id = ) AND (event_date > X)"
19+
20+
assert generalize_sql("UPDATE `user` SET user_touched = '20150112143631' WHERE user_id = '25239755'") == \
21+
"UPDATE `user` SET user_touched = X WHERE user_id = X"
22+
23+
assert generalize_sql(
24+
"SELECT /* CategoryDataService::getMostVisited 207.46.13.56 */ page_id,cl_to FROM `page` INNER JOIN `categorylinks` ON ((cl_from = page_id)) WHERE cl_to = 'Characters' AND (page_namespace NOT IN(500,6,14)) ORDER BY page_title") == \
25+
"SELECT page_id,cl_to FROM `page` INNER JOIN `categorylinks` ON ((cl_from = page_id)) WHERE cl_to = X AND (page_namespace NOT IN (XYZ)) ORDER BY page_title"
26+
27+
assert generalize_sql(
28+
"SELECT /* ArticleCommentList::getCommentList Dancin'NoViolen... */ page_id,page_title FROM `page` WHERE (page_title LIKE 'Dreams\\_Come\\_True/@comment-%' ) AND page_namespace = '1' ORDER BY page_id DESC") == \
29+
"SELECT page_id,page_title FROM `page` WHERE (page_title LIKE X ) AND page_namespace = X ORDER BY page_id DESC"
30+
31+
assert generalize_sql(
32+
"delete /* DatabaseBase::sourceFile( /usr/wikia/slot1/3690/src/maintenance/cleanupStarter.sql ) CreateWiki scri... */ from text where old_id not in (select rev_text_id from revision)") == \
33+
"delete from text where old_id not in (select rev_text_id from revision)"
34+
35+
assert generalize_sql(
36+
"SELECT /* WallNotifications::getBackupData Craftindiedo */ id,is_read,is_reply,unique_id,entity_key,author_id,notifyeveryone FROM `wall_notification` WHERE user_id = '24944488' AND wiki_id = '1030786' AND unique_id IN ('880987','882618','708228','522330','662055','837815','792393','341504','600103','612640','667267','482428','600389','213400','620177','164442','659210','621286','609757','575865','567668','398132','549770','495396','344814','421448','400650','411028','341771','379461','332587','314176','284499','250207','231714') AND is_hidden = '0' ORDER BY id") == \
37+
"SELECT id,is_read,is_reply,unique_id,entity_key,author_id,notifyeveryone FROM `wall_notification` WHERE user_id = X AND wiki_id = X AND unique_id IN (XYZ) AND is_hidden = X ORDER BY id"
38+
39+
# comments with * inside
40+
assert generalize_sql(
41+
"SELECT /* ArticleCommentList::getCommentList *Crashie* */ page_id,page_title FROM `page` WHERE (page_title LIKE 'Dainava/@comment-%' ) AND page_namespace = '1201' ORDER BY page_id DESC") == \
42+
"SELECT page_id,page_title FROM `page` WHERE (page_title LIKE X ) AND page_namespace = X ORDER BY page_id DESC"
43+
44+
# comments with * inside
45+
assert generalize_sql(
46+
"SELECT /* ListusersData::loadData Lart96 - 413bc6e5-b151-44fd-80bd-3baff733fb91 */ count(0) as cnt FROM `events_local_users` WHERE wiki_id = '7467' AND (user_name != '') AND user_is_closed = '0' AND ( single_group = 'poweruser' or all_groups = '' or all_groups LIKE '%bot' or all_groups LIKE '%bot;%' or all_groups LIKE '%bureaucrat' or all_groups LIKE '%bureaucrat;%' or all_groups LIKE '%sysop' or all_groups LIKE '%sysop;%' or all_groups LIKE '%authenticated' or all_groups LIKE '%authenticated;%' or all_groups LIKE '%bot-global' or all_groups LIKE '%bot-global;%' or all_groups LIKE '%content-reviewer' or all_groups LIKE '%content-reviewer;%' or all_groups LIKE '%council' or all_groups LIKE '%council;%' or all_groups LIKE '%fandom-editor' or all_groups LIKE '%fandom-editor;%' or all_groups LIKE '%helper' or all_groups LIKE '%helper;%' or all_groups LIKE '%restricted-login' or all_groups LIKE '%restricted-login;%' or all_groups LIKE '%restricted-login-exempt' or all_groups LIKE '%restricted-login-exempt;%' or all_groups LIKE '%reviewer' or all_groups LIKE '%reviewer;%' or all_groups LIKE '%staff' or all_groups LIKE '%staff;%' or all_groups LIKE '%translator' or all_groups LIKE '%translator;%' or all_groups LIKE '%util' or all_groups LIKE '%util;%' or all_groups LIKE '%vanguard' or all_groups LIKE '%vanguard;%' or all_groups LIKE '%voldev' or all_groups LIKE '%voldev;%' or all_groups LIKE '%vstf' or all_groups LIKE '%vstf;%' ) AND ( edits >= 5) LIMIT 1 ") == \
47+
"SELECT count(N) as cnt FROM `events_local_users` WHERE wiki_id = X AND (user_name != X) AND user_is_closed = X AND ( single_group = X or all_groups = X or all_groups LIKE X ... ) AND ( edits >= N) LIMIT N"
48+
49+
# multiline query
50+
sql = """
51+
SELECT page_title
52+
FROM page
53+
WHERE page_namespace = '10'
54+
AND page_title COLLATE LATIN1_GENERAL_CI LIKE '%{{Cata%'
55+
"""
56+
57+
assert generalize_sql(sql) == \
58+
"SELECT page_title FROM page WHERE page_namespace = X AND page_title COLLATE LATINN_GENERAL_CI LIKE X"
59+
60+
# queries with IN + brackets (#21)
61+
assert generalize_sql(
62+
'SELECT foo FROM bar WHERE id IN (123,456, 789)') == \
63+
'SELECT foo FROM bar WHERE id IN (XYZ)'
64+
65+
assert generalize_sql(
66+
'SELECT foo FROM bar WHERE id in ( 123, 456, 789 )') == \
67+
'SELECT foo FROM bar WHERE id in (XYZ)'
68+
69+
assert generalize_sql(
70+
"SELECT foo FROM bar WHERE slug in ( 'american-horror-story', 'animated-series', 'batman', 'comics', 'dc', 'fallout', 'game-of-thrones', 'hbo', 'horror', 'marvel', 'mcu', 'movie-reviews', 'movie-trailers', 'movies', 'netflix', 'playstation', 'star-wars', 'stranger-things', 'streaming', 'the-simpsons', 'zelda' )") == \
71+
'SELECT foo FROM bar WHERE slug in (XYZ)'
72+
73+
assert generalize_sql(
74+
'select curation_cms.topics.slug from curation_cms.topics where curation_cms.topics.id in ( 87, 86, 79, 77, 76, 73, 72, 70, 71, 69, 68, 66, 65, 64, 62, 63, 2, 57, 17, 1, 22, 49, 30, 55, 15, 3, 48, 43, 24, 47, 45, 10, 50, 39, 36, 8, 34, 25, 13, 6, 4 )') == \
75+
'select curation_cms.topics.slug from curation_cms.topics where curation_cms.topics.id in (XYZ)'
76+
77+
78+
def test_generalize_timestamp():
79+
assert generalize_sql(
80+
# ODBC syntax - https://dev.mysql.com/doc/refman/5.7/en/date-and-time-literals.html
81+
"SELECT foo FROM bar WHERE publish_date < {ts '2018-04-05 10:14:33.824'}") == \
82+
'SELECT foo FROM bar WHERE publish_date < {ts X}'
83+
84+
85+
def test_generalize_insert():
86+
assert generalize_sql(
87+
'INSERT INTO bar (foo, test) Values ( 123, 456, 789 )') == \
88+
'INSERT INTO bar (foo, test) Values (XYZ)'
89+
90+
assert generalize_sql(
91+
"/* 7e6384e5 */ insert into notification_stats.request_info ( type, request_id, title, message, details ) values ( 'action-notification', '51f8a962-bae0-4d25-9341-130658161541', 'RickSanchez15 replied to What''s your overall favourite Season of South Park?.', 'Cool', 'null' )") == \
92+
'insert into notification_stats.request_info ( type, request_id, title, message, details ) values (XYZ)'

0 commit comments

Comments
 (0)