sql-testing-tools 0.1.3__tar.gz → 0.1.5__tar.gz
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.
- sql_testing_tools-0.1.5/PKG-INFO +25 -0
- sql_testing_tools-0.1.5/README.md +3 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/setup.py +2 -2
- sql_testing_tools-0.1.5/sql_testing_tools/Helper.py +206 -0
- sql_testing_tools-0.1.5/sql_testing_tools/TokenProcessing.py +356 -0
- sql_testing_tools-0.1.5/sql_testing_tools.egg-info/PKG-INFO +25 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools.egg-info/SOURCES.txt +1 -0
- sql_testing_tools-0.1.3/PKG-INFO +0 -10
- sql_testing_tools-0.1.3/README.md +0 -3
- sql_testing_tools-0.1.3/sql_testing_tools/Helper.py +0 -410
- sql_testing_tools-0.1.3/sql_testing_tools.egg-info/PKG-INFO +0 -10
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/setup.cfg +0 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools/BaseAccess.py +0 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools/__init__.py +0 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools.egg-info/dependency_links.txt +0 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools.egg-info/requires.txt +0 -0
- {sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sql_testing_tools
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Description-Content-Type: text/markdown
|
|
5
|
+
Requires-Dist: sqlparse>=0.5.1
|
|
6
|
+
Requires-Dist: requests>=2.32.3
|
|
7
|
+
|
|
8
|
+
A library that normalizes simple SQL queries and compares them first by equality of the normalized string and then using the cosette API.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### [Beta in Development!]
|
|
12
|
+
[](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml)
|
|
13
|
+
[](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### V0.1.4 + V0.1.5
|
|
18
|
+
- V0.1.5 is just a reupload of V0.1.4
|
|
19
|
+
- Chained conditions (with AND,OR and Paranthesises) in WHERE statement
|
|
20
|
+
|
|
21
|
+
### V0.1.3
|
|
22
|
+
- SELECT: columns with our without table prefix
|
|
23
|
+
- FROM: one or more table from DB; no queries as tables!
|
|
24
|
+
- WHERE: single conditions; no Paranthesises!
|
|
25
|
+
- GROUP BY one or more columns
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from setuptools import setup, find_packages
|
|
2
2
|
|
|
3
|
-
with open("README.md", "r") as fh:
|
|
3
|
+
with open("sql_testing_tools/README.md", "r") as fh:
|
|
4
4
|
description = fh.read()
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name='sql_testing_tools',
|
|
9
|
-
version='0.1.
|
|
9
|
+
version='0.1.5',
|
|
10
10
|
packages=find_packages(),
|
|
11
11
|
install_requires=[
|
|
12
12
|
'sqlparse>=0.5.1',
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import BaseAccess as Ba
|
|
2
|
+
import TokenProcessing as Tp
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
import sqlparse
|
|
6
|
+
|
|
7
|
+
from sqlparse.sql import Identifier, IdentifierList, Where, Comparison, Function, Parenthesis
|
|
8
|
+
from sqlparse.tokens import Keyword, DML, Name, Wildcard
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalizeSQLQuery(query, baseDict):
|
|
13
|
+
try:
|
|
14
|
+
query = query.replace("\"", "'")
|
|
15
|
+
parsed = sqlparse.parse(query)[0]
|
|
16
|
+
parsed.tokens = [token for token in parsed.tokens if not token.is_whitespace]
|
|
17
|
+
except Exception as e:
|
|
18
|
+
raise Exception(f"\nSyntax-Fehler in der SQL-Abfrage.")
|
|
19
|
+
|
|
20
|
+
formatted_query = []
|
|
21
|
+
alias_map = {}
|
|
22
|
+
|
|
23
|
+
# First pass to process FROM clause and populate alias_map
|
|
24
|
+
for token in parsed.tokens:
|
|
25
|
+
if token.is_whitespace:
|
|
26
|
+
continue
|
|
27
|
+
elif token.ttype is Keyword and token.value.upper() == 'FROM':
|
|
28
|
+
formatted_query.append('FROM')
|
|
29
|
+
pass
|
|
30
|
+
elif isinstance(token, Identifier) and formatted_query and formatted_query[-1] == 'FROM':
|
|
31
|
+
il = IdentifierList([token])
|
|
32
|
+
formatted_query.append(Tp._from(il, alias_map, baseDict))
|
|
33
|
+
Tp._from(il, alias_map, baseDict)
|
|
34
|
+
elif isinstance(token, IdentifierList) and formatted_query and formatted_query[-1] == 'FROM':
|
|
35
|
+
formatted_query.append(Tp._from(token, alias_map, baseDict))
|
|
36
|
+
Tp._from(token, alias_map, baseDict)
|
|
37
|
+
|
|
38
|
+
formatted_query = []
|
|
39
|
+
|
|
40
|
+
# Second pass to process SELECT and WHERE clauses
|
|
41
|
+
for token in parsed.tokens:
|
|
42
|
+
if token.is_whitespace:
|
|
43
|
+
continue
|
|
44
|
+
elif token.ttype is DML and token.value.upper() == 'SELECT':
|
|
45
|
+
formatted_query.append('SELECT')
|
|
46
|
+
elif token.ttype is Keyword and token.value.upper() == 'FROM':
|
|
47
|
+
formatted_query.append('FROM')
|
|
48
|
+
elif token.ttype is Keyword and token.value.upper() == 'GROUP BY':
|
|
49
|
+
formatted_query.append('GROUP BY')
|
|
50
|
+
elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'FROM':
|
|
51
|
+
formatted_query.append(Tp._from(token, alias_map, baseDict))
|
|
52
|
+
elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'GROUP BY':
|
|
53
|
+
formatted_query.append(Tp._groupby(token, alias_map, baseDict))
|
|
54
|
+
elif isinstance(token, Where):
|
|
55
|
+
formatted_query.append('WHERE')
|
|
56
|
+
formatted_query.append(Tp._where(token, alias_map, baseDict))
|
|
57
|
+
elif formatted_query and formatted_query[-1] == 'SELECT' and (isinstance(token, IdentifierList) or isinstance(token, Function) or isinstance(token, Identifier)):
|
|
58
|
+
if isinstance(token, Function):
|
|
59
|
+
token = IdentifierList([token])
|
|
60
|
+
formatted_query.append(Tp._select(token, alias_map, baseDict))
|
|
61
|
+
else:
|
|
62
|
+
formatted_query.append(str(token))
|
|
63
|
+
|
|
64
|
+
return " ".join(formatted_query)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def findTableForColumn(data_dict, target_value, relevantTables):
|
|
68
|
+
l = []
|
|
69
|
+
for key, value_list in data_dict.items():
|
|
70
|
+
if key.lower() in relevantTables:
|
|
71
|
+
for sublist in value_list:
|
|
72
|
+
if sublist and sublist[0].lower() == target_value.lower():
|
|
73
|
+
l.append(key)
|
|
74
|
+
if len(l) == 0:
|
|
75
|
+
for key, value_list in data_dict.items():
|
|
76
|
+
for sublist in value_list:
|
|
77
|
+
if sublist and sublist[0].lower() == target_value.lower():
|
|
78
|
+
l.append(key)
|
|
79
|
+
return l
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def getTableScheme(table_name: str, tableDict: dict):
|
|
83
|
+
tab = tableDict[table_name]
|
|
84
|
+
|
|
85
|
+
# Format the schema
|
|
86
|
+
schema = "(" + ",".join([f"{col[0]}:{col[1]}" for col in tab]) + ")"
|
|
87
|
+
return schema
|
|
88
|
+
|
|
89
|
+
def getCosetteKeyFromFile():
|
|
90
|
+
try:
|
|
91
|
+
with open("cosette_apikey.txt", "r") as f:
|
|
92
|
+
return f.read()
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
return "NOKEY"
|
|
95
|
+
|
|
96
|
+
def buildAndSendCosetteRequest(baseDict, sql, sol):
|
|
97
|
+
|
|
98
|
+
err = ""
|
|
99
|
+
for i in range(2):
|
|
100
|
+
try:
|
|
101
|
+
apiKey=getCosetteKeyFromFile()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
schema = ""
|
|
105
|
+
for tab in baseDict.keys():
|
|
106
|
+
schema += f"schema sch{tab}{getTableScheme(tab, baseDict)};\n"
|
|
107
|
+
for tab in baseDict.keys():
|
|
108
|
+
schema += f"table {tab}(sch{tab});\n"
|
|
109
|
+
|
|
110
|
+
q1 = "query q1\n`"+sql+"`;\n"
|
|
111
|
+
q2 = "query q2\n`"+sol+"`;\n"
|
|
112
|
+
|
|
113
|
+
cosette = "-- random Kommentar\n" + schema + q1 + q2 + "verify q1 q2;\n"
|
|
114
|
+
print(cosette)
|
|
115
|
+
|
|
116
|
+
r = requests.post("https://demo.cosette.cs.washington.edu/solve",data={"api_key": apiKey, "query": cosette}, verify=False)
|
|
117
|
+
|
|
118
|
+
print(r.text)
|
|
119
|
+
return (r.json()['result'],r.text)
|
|
120
|
+
#return r.json()['result']
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
err = str(e)
|
|
124
|
+
return ("ERR", err)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def checkColumns(sqlPath, solPath):
|
|
128
|
+
bd = Ba.getTableDict()
|
|
129
|
+
sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
|
|
130
|
+
sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
|
|
131
|
+
|
|
132
|
+
if("SELECT" in sql and "FROM" in sql):
|
|
133
|
+
start = str.find(sql, "SELECT")
|
|
134
|
+
end = str.find(sql, "FROM")
|
|
135
|
+
submission = str.strip(sql[start:end])
|
|
136
|
+
print("'"+submission+"'")
|
|
137
|
+
|
|
138
|
+
start = str.find(sol, "SELECT")
|
|
139
|
+
end = str.find(sol, "FROM")
|
|
140
|
+
solution = str.strip(sol[start:end])
|
|
141
|
+
print("'"+solution+"'")
|
|
142
|
+
|
|
143
|
+
if submission == solution:
|
|
144
|
+
return ""
|
|
145
|
+
return "Ausgegebene Spalten sind nicht korrekt (oder nicht automatisch überprüfbar)."
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def checkTables(sqlPath, solPath):
|
|
149
|
+
bd = Ba.getTableDict()
|
|
150
|
+
sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
|
|
151
|
+
sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
|
|
152
|
+
|
|
153
|
+
if("SELECT" in sql and "FROM" in sql):
|
|
154
|
+
endFromKeywords = ["WHERE", "GROUP", "ORDER", "LIMIT", ";"]
|
|
155
|
+
|
|
156
|
+
start = str.find(sql, "FROM")
|
|
157
|
+
end = -1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
for keyword in endFromKeywords:
|
|
161
|
+
if(str.find(sql, keyword) != -1):
|
|
162
|
+
end = str.find(sql, keyword)
|
|
163
|
+
break
|
|
164
|
+
if(end == -1):
|
|
165
|
+
end = len(sql)
|
|
166
|
+
|
|
167
|
+
submission = str.strip(sql[start:end])
|
|
168
|
+
print("'"+submission+"'")
|
|
169
|
+
|
|
170
|
+
start = str.find(sol, "FROM")
|
|
171
|
+
end = -1
|
|
172
|
+
|
|
173
|
+
for keyword in endFromKeywords:
|
|
174
|
+
if(str.find(sol, keyword) != -1):
|
|
175
|
+
end = str.find(sol, keyword)
|
|
176
|
+
break
|
|
177
|
+
if(end == -1):
|
|
178
|
+
end = len(sol)
|
|
179
|
+
|
|
180
|
+
solution = str.strip(sol[start:end])
|
|
181
|
+
print("'"+solution+"'")
|
|
182
|
+
|
|
183
|
+
if submission == solution:
|
|
184
|
+
return ""
|
|
185
|
+
return "Verwendete Tabellen sind nicht korrekt (oder nicht automatisch überprüfbar)."
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def checkEquality(sqlPath, solPath):
|
|
189
|
+
bd = Ba.getTableDict()
|
|
190
|
+
sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
|
|
191
|
+
sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
|
|
192
|
+
if(sql=='' or sol==''):
|
|
193
|
+
return "\n\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet."
|
|
194
|
+
|
|
195
|
+
if(sql==sol):
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
result = buildAndSendCosetteRequest(bd, sql, sol)
|
|
199
|
+
|
|
200
|
+
if(result[0] == "ERR"):
|
|
201
|
+
return "\n\nFehler bei der automatischen Überprüfung der Abgabe. Es kann keine Aussage über die Korrektheit der Abgabe getroffen werden."
|
|
202
|
+
elif(result[0] != "EQ"):
|
|
203
|
+
return "\n\nDie Abgabe stimmt nicht mit der Musterlösung überein."
|
|
204
|
+
return ""
|
|
205
|
+
|
|
206
|
+
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import sqlparse
|
|
2
|
+
from sqlparse.sql import Identifier, IdentifierList, Where, Comparison, Function, Parenthesis
|
|
3
|
+
from sqlparse.tokens import Keyword, DML, Name, Wildcard, Whitespace
|
|
4
|
+
|
|
5
|
+
import Helper as He
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
#####################################
|
|
9
|
+
#####################################
|
|
10
|
+
|
|
11
|
+
def _identifier(identifier, alias_map, baseDict: dict):
|
|
12
|
+
if isinstance(identifier, Identifier):
|
|
13
|
+
if identifier.get_real_name() and identifier.get_parent_name() and identifier.get_parent_name().lower() in alias_map.keys():
|
|
14
|
+
return f"{alias_map[identifier.get_parent_name().lower()].lower()}.{identifier.get_real_name().lower()}"
|
|
15
|
+
elif identifier.get_real_name():
|
|
16
|
+
tables = He.findTableForColumn(baseDict, identifier.get_real_name(), alias_map.keys())
|
|
17
|
+
if len(tables) == 1:
|
|
18
|
+
alias_map[tables[0].lower()] = tables[0]
|
|
19
|
+
return f"{tables[0].lower()}.{identifier.get_real_name().lower()}"
|
|
20
|
+
else:
|
|
21
|
+
return f"{identifier.get_real_name().lower()}"
|
|
22
|
+
return str(identifier)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#####################################
|
|
26
|
+
#####################################
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _select(select, alias_map, baseDict: dict, insideFunction=False):
|
|
30
|
+
select_tokens = []
|
|
31
|
+
for token in select.tokens:
|
|
32
|
+
if isinstance(token, IdentifierList):
|
|
33
|
+
for identifier in token.get_identifiers():
|
|
34
|
+
select_tokens.append(f"{_identifier(identifier, alias_map, baseDict).lower()} as {identifier.get_real_name().lower()}")
|
|
35
|
+
elif isinstance(token, Identifier):
|
|
36
|
+
if insideFunction:
|
|
37
|
+
select_tokens.append(_identifier(token, alias_map, baseDict).lower())
|
|
38
|
+
else:
|
|
39
|
+
select_tokens.append(f"{_identifier(token, alias_map, baseDict).lower()} as {token.get_real_name().lower()}")
|
|
40
|
+
elif isinstance(token, Function):
|
|
41
|
+
#select_tokens.append(f"{token.get_name().lower()}({_identifier(token.get_parameters(), alias_map, baseDict).lower()})")
|
|
42
|
+
for par in token.tokens:
|
|
43
|
+
if isinstance(par, Identifier):
|
|
44
|
+
select_tokens.append(par.get_name().lower()+"(")
|
|
45
|
+
if isinstance(par, Parenthesis):
|
|
46
|
+
select_tokens[-1] += _select(par, alias_map, baseDict, True)+") as funcResult"
|
|
47
|
+
elif token.ttype is Wildcard:
|
|
48
|
+
select_tokens.append("*")
|
|
49
|
+
else:
|
|
50
|
+
continue
|
|
51
|
+
select_tokens.sort()
|
|
52
|
+
return ",".join(select_tokens)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#####################################
|
|
56
|
+
#####################################
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _from(from_, alias_map, baseDict: dict):
|
|
60
|
+
from_tokens = []
|
|
61
|
+
if hasattr(from_, 'tokens'):
|
|
62
|
+
for token in from_.tokens:
|
|
63
|
+
if isinstance(token, IdentifierList):
|
|
64
|
+
for identifier in token.get_identifiers():
|
|
65
|
+
alias_map[identifier.get_real_name().lower()] = identifier.get_alias() or identifier.get_real_name()
|
|
66
|
+
from_tokens.append(f"{identifier.get_real_name().lower()} {alias_map[identifier.get_real_name().lower()].lower()}")
|
|
67
|
+
elif isinstance(token, Identifier):
|
|
68
|
+
alias_map[token.get_real_name().lower()] = token.get_alias() or token.get_real_name()
|
|
69
|
+
from_tokens.append(f"{token.get_real_name().lower()} {alias_map[token.get_real_name().lower()].lower()}")
|
|
70
|
+
elif token.ttype is not None and token.ttype is Name:
|
|
71
|
+
alias_map[token.value.lower()] = token.value.lower()
|
|
72
|
+
from_tokens.append(f"{token.value.lower()} {alias_map[token.value.lower()].lower()}")
|
|
73
|
+
else:
|
|
74
|
+
continue
|
|
75
|
+
from_tokens.sort()
|
|
76
|
+
return ",".join(from_tokens)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#####################################
|
|
80
|
+
#####################################
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _groupby(groupby_, alias_map, baseDict: dict):
|
|
84
|
+
groupby_tokens = []
|
|
85
|
+
if(isinstance(groupby_, Identifier)):
|
|
86
|
+
groupby_tokens.append(_identifier(groupby_, alias_map, baseDict).lower())
|
|
87
|
+
elif isinstance(groupby_, IdentifierList):
|
|
88
|
+
for identifier in groupby_.get_identifiers():
|
|
89
|
+
groupby_tokens.append(_identifier(identifier, alias_map, baseDict).lower())
|
|
90
|
+
groupby_tokens.sort()
|
|
91
|
+
return ",".join(groupby_tokens)
|
|
92
|
+
|
|
93
|
+
def _condition(token, alias_map, baseDict: dict):
|
|
94
|
+
left, operator, right = [t for t in token.tokens if not t.is_whitespace]
|
|
95
|
+
|
|
96
|
+
flipAllowed = True
|
|
97
|
+
leftLiteral = False
|
|
98
|
+
rightLiteral = False
|
|
99
|
+
|
|
100
|
+
if is_value(right):
|
|
101
|
+
flipAllowed = False
|
|
102
|
+
rightLiteral = True
|
|
103
|
+
|
|
104
|
+
if is_value(left):
|
|
105
|
+
left, right = right, left
|
|
106
|
+
if operator.value == ">":
|
|
107
|
+
operator.value = "<"
|
|
108
|
+
elif operator.value == "<":
|
|
109
|
+
operator.value = ">"
|
|
110
|
+
leftLiteral = rightLiteral
|
|
111
|
+
rightLiteral = True
|
|
112
|
+
flipAllowed = False
|
|
113
|
+
|
|
114
|
+
left = _identifier(left, alias_map, baseDict)
|
|
115
|
+
right = _identifier(right, alias_map, baseDict)
|
|
116
|
+
|
|
117
|
+
if flipAllowed and left.lower() >= right.lower():
|
|
118
|
+
left, right = right, left
|
|
119
|
+
if operator.value == ">":
|
|
120
|
+
operator = "<"
|
|
121
|
+
elif operator.value == "<":
|
|
122
|
+
operator = ">"
|
|
123
|
+
|
|
124
|
+
return f"{left if leftLiteral else left.lower()} {operator} {right if rightLiteral else right.lower()}"
|
|
125
|
+
|
|
126
|
+
def is_value(token):
|
|
127
|
+
return token.ttype in (sqlparse.tokens.Token.Literal.Number.Integer,
|
|
128
|
+
sqlparse.tokens.Token.Literal.Number.Float,
|
|
129
|
+
sqlparse.tokens.Token.Literal.String.Single,
|
|
130
|
+
sqlparse.tokens.Token.Literal.String.Symbol)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
#####################################
|
|
135
|
+
#####################################
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _paranthesis(parenthesis, alias_map, baseDict: dict):
|
|
139
|
+
toks = []
|
|
140
|
+
|
|
141
|
+
bracketsRequired = True
|
|
142
|
+
closeBracketsAfterNext = False
|
|
143
|
+
|
|
144
|
+
and_count = sum(1 for token in parenthesis.tokens if token.ttype is Keyword and token.value == "AND") # Zähle "AND"-Tokens
|
|
145
|
+
or_count = sum(1 for token in parenthesis.tokens if token.ttype is Keyword and token.value == "OR") # Zähle "AND"-Tokens
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if(and_count==1 and or_count == 0):
|
|
149
|
+
val = _2element_par(parenthesis, alias_map, baseDict, "AND")
|
|
150
|
+
return val
|
|
151
|
+
elif(and_count==0 and or_count == 1):
|
|
152
|
+
val = _2element_par(parenthesis, alias_map, baseDict, "OR")
|
|
153
|
+
return val
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
for token in parenthesis.tokens:
|
|
161
|
+
if isinstance(token, Comparison):
|
|
162
|
+
toks.append(_condition(token, alias_map, baseDict))
|
|
163
|
+
elif isinstance(token, Parenthesis):
|
|
164
|
+
toks.append(_paranthesis(token, alias_map, baseDict))
|
|
165
|
+
|
|
166
|
+
elif token.ttype is Keyword: #AND,OR
|
|
167
|
+
if token.value == "AND":
|
|
168
|
+
pass
|
|
169
|
+
elif token.value == "OR":
|
|
170
|
+
pass
|
|
171
|
+
elif token.value == "NOT":
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
elif token.value == '(':
|
|
175
|
+
toks.append(token.value)
|
|
176
|
+
else:
|
|
177
|
+
continue
|
|
178
|
+
x = parenthesis.flatten()
|
|
179
|
+
return " ".join(toks)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _2element_par(parenthesis, alias_map, baseDict: dict, keyword):
|
|
183
|
+
toks = []
|
|
184
|
+
|
|
185
|
+
for token in parenthesis.tokens:
|
|
186
|
+
if isinstance(token, Comparison):
|
|
187
|
+
toks.append(_condition(token, alias_map, baseDict))
|
|
188
|
+
elif isinstance(token, Parenthesis):
|
|
189
|
+
toks.append(_paranthesis(token, alias_map, baseDict))
|
|
190
|
+
toks = toks.sort()
|
|
191
|
+
fill = (" "+keyword+" ")
|
|
192
|
+
return fill.join(toks)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def count_keywordValues(tokens, values):
|
|
196
|
+
return len([token for token in tokens if token.ttype is Keyword and token.value.upper() in values])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def find_index_of_keyword(tokens, keyword):
|
|
200
|
+
for index, token in enumerate(tokens):
|
|
201
|
+
if token.ttype is Keyword and token.value.upper() == keyword:
|
|
202
|
+
return index
|
|
203
|
+
return -1 # Wenn kein AND-Token gefunden wird
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _where(where, alias_map, baseDict: dict):
|
|
207
|
+
conditions = []
|
|
208
|
+
current_condition = []
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
isParanthesis = isinstance(where, Parenthesis)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
and_count = where.normalized.upper().count(" AND ")
|
|
216
|
+
or_count = where.normalized.upper().count(" OR ")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# No AND / OR
|
|
220
|
+
if and_count + or_count == 0:
|
|
221
|
+
return _where_simpleCondition(where, alias_map, baseDict)
|
|
222
|
+
|
|
223
|
+
# only ANDs / only ORs
|
|
224
|
+
elif and_count == 0 or or_count == 0:
|
|
225
|
+
return _where_sameStrengthKeywords(where, alias_map, baseDict, " AND " if and_count > 0 else " OR ")
|
|
226
|
+
|
|
227
|
+
# ANDs and ORs
|
|
228
|
+
|
|
229
|
+
where_tokens = [token for token in where.tokens if token.ttype is not Whitespace and token.value != "WHERE"]
|
|
230
|
+
|
|
231
|
+
# count keywords on top level
|
|
232
|
+
top_and_count = count_keywordValues(where_tokens, ["AND"])
|
|
233
|
+
top_or_count = count_keywordValues(where_tokens, ["OR"])
|
|
234
|
+
|
|
235
|
+
# two conditions on top level
|
|
236
|
+
if top_and_count + top_or_count == 1:
|
|
237
|
+
keyword = " AND " if top_and_count == 1 else " OR "
|
|
238
|
+
return _where_twoConditionsOnTopLevel(where, alias_map, baseDict, keyword, isParanthesis)
|
|
239
|
+
else:
|
|
240
|
+
where_tokens = _where_addBracketsAroundAND(where, alias_map, baseDict)
|
|
241
|
+
return _where(Where(where_tokens), alias_map, baseDict)
|
|
242
|
+
|
|
243
|
+
def _where_addBracketsAroundAND(where, alias_map, baseDict: dict):
|
|
244
|
+
index = find_index_of_keyword(where.tokens, "AND")
|
|
245
|
+
par = Parenthesis(where.tokens[index-2:index+3])
|
|
246
|
+
where_tokens = where.tokens[:index-2] + [par] + where.tokens[index+3:]
|
|
247
|
+
return where_tokens
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# for token in where.tokens:
|
|
252
|
+
# if token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
|
|
253
|
+
# continue
|
|
254
|
+
|
|
255
|
+
# if token.ttype is Keyword and token.value.upper() in ('AND', 'OR'):
|
|
256
|
+
# if current_condition:
|
|
257
|
+
# conditions.append(''.join(str(t) for t in current_condition).strip())
|
|
258
|
+
# current_condition = []
|
|
259
|
+
# conditions.append(token.value.upper())
|
|
260
|
+
# else:
|
|
261
|
+
# current_condition.append(token)
|
|
262
|
+
|
|
263
|
+
# if current_condition:
|
|
264
|
+
# conditions.append(''.join(str(t) for t in current_condition).strip())
|
|
265
|
+
|
|
266
|
+
# sorted_conditions = []
|
|
267
|
+
# current_group = []
|
|
268
|
+
# last_connector = ""
|
|
269
|
+
|
|
270
|
+
# if "OR" not in conditions:
|
|
271
|
+
# for condition in conditions:
|
|
272
|
+
# if condition == 'AND':
|
|
273
|
+
# pass
|
|
274
|
+
# else:
|
|
275
|
+
# current_group.append(condition)
|
|
276
|
+
|
|
277
|
+
# if current_group:
|
|
278
|
+
# sorted_conditions.extend(sorted(current_group))
|
|
279
|
+
|
|
280
|
+
# return " AND ".join(sorted_conditions)
|
|
281
|
+
# else:
|
|
282
|
+
# return " ".join(conditions)
|
|
283
|
+
|
|
284
|
+
# where_index = query.upper().find('WHERE')
|
|
285
|
+
|
|
286
|
+
# normalized_query = query[:where_index + 5] + ' ' + ' '.join(sorted_conditions)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _where_simpleCondition(where, alias_map, baseDict: dict):
|
|
291
|
+
cond = []
|
|
292
|
+
for tok in where.tokens:
|
|
293
|
+
if isinstance(tok, Comparison):
|
|
294
|
+
cond.append(_condition(tok, alias_map, baseDict))
|
|
295
|
+
elif isinstance(tok, Parenthesis):
|
|
296
|
+
for _tok in tok.tokens:
|
|
297
|
+
if isinstance(_tok, Comparison):
|
|
298
|
+
cond.append(_condition(_tok, alias_map, baseDict))
|
|
299
|
+
elif isinstance(_tok, Parenthesis):
|
|
300
|
+
tok = _tok
|
|
301
|
+
return "".join(cond)
|
|
302
|
+
|
|
303
|
+
def _where_sameStrengthKeywords(where, alias_map, baseDict: dict, keyword: str):
|
|
304
|
+
cond = []
|
|
305
|
+
for tok in where.tokens:
|
|
306
|
+
if isinstance(tok, Comparison):
|
|
307
|
+
cond.append(_condition(tok, alias_map, baseDict))
|
|
308
|
+
elif isinstance(tok, Parenthesis):
|
|
309
|
+
for _tok in tok.tokens:
|
|
310
|
+
if isinstance(_tok, Comparison):
|
|
311
|
+
cond.append(_condition(_tok, alias_map, baseDict))
|
|
312
|
+
elif isinstance(_tok, Parenthesis):
|
|
313
|
+
tok = _tok
|
|
314
|
+
cond.sort()
|
|
315
|
+
return keyword.join(cond)
|
|
316
|
+
|
|
317
|
+
def _where_twoConditionsOnTopLevel(where, alias_map, baseDict: dict, keyword, isParanthesis):
|
|
318
|
+
current_condition = []
|
|
319
|
+
for tok in where.tokens:
|
|
320
|
+
if isinstance(tok, Comparison):
|
|
321
|
+
current_condition.append(_condition(tok, alias_map, baseDict))
|
|
322
|
+
elif isinstance(tok, Parenthesis):
|
|
323
|
+
current_condition.append(_where(tok, alias_map, baseDict))
|
|
324
|
+
current_condition.sort()
|
|
325
|
+
if keyword == " AND " or isParanthesis:
|
|
326
|
+
return "(" + keyword.join(current_condition) + ")"
|
|
327
|
+
else:
|
|
328
|
+
return keyword.join(current_condition)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# def _where_xx(where, alias_map, baseDict: dict):
|
|
334
|
+
# where_tokens = []
|
|
335
|
+
# for token in where.tokens:
|
|
336
|
+
|
|
337
|
+
# if isinstance(token, Comparison):
|
|
338
|
+
# left, operator, right = [t for t in token.tokens if not t.is_whitespace]
|
|
339
|
+
# left = _identifier(left, alias_map, baseDict)
|
|
340
|
+
# right = _identifier(right, alias_map, baseDict)
|
|
341
|
+
# if left.lower() >= right.lower():
|
|
342
|
+
# left, right = right, left
|
|
343
|
+
# if operator.value == ">":
|
|
344
|
+
# operator = "<"
|
|
345
|
+
# elif operator.value == "<":
|
|
346
|
+
# operator = ">"
|
|
347
|
+
|
|
348
|
+
# where_tokens.append(f"{left.lower()} {operator} {right.lower()}")
|
|
349
|
+
# elif token.ttype is Keyword:
|
|
350
|
+
# where_tokens.append(token.value.upper())
|
|
351
|
+
# elif token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
|
|
352
|
+
# continue
|
|
353
|
+
# else:
|
|
354
|
+
# where_tokens.append(str(token))
|
|
355
|
+
# return " ".join(where_tokens)
|
|
356
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sql_testing_tools
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Description-Content-Type: text/markdown
|
|
5
|
+
Requires-Dist: sqlparse>=0.5.1
|
|
6
|
+
Requires-Dist: requests>=2.32.3
|
|
7
|
+
|
|
8
|
+
A library that normalizes simple SQL queries and compares them first by equality of the normalized string and then using the cosette API.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### [Beta in Development!]
|
|
12
|
+
[](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml)
|
|
13
|
+
[](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### V0.1.4 + V0.1.5
|
|
18
|
+
- V0.1.5 is just a reupload of V0.1.4
|
|
19
|
+
- Chained conditions (with AND,OR and Paranthesises) in WHERE statement
|
|
20
|
+
|
|
21
|
+
### V0.1.3
|
|
22
|
+
- SELECT: columns with our without table prefix
|
|
23
|
+
- FROM: one or more table from DB; no queries as tables!
|
|
24
|
+
- WHERE: single conditions; no Paranthesises!
|
|
25
|
+
- GROUP BY one or more columns
|
sql_testing_tools-0.1.3/PKG-INFO
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: sql_testing_tools
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Description-Content-Type: text/markdown
|
|
5
|
-
Requires-Dist: sqlparse>=0.5.1
|
|
6
|
-
Requires-Dist: requests>=2.32.3
|
|
7
|
-
|
|
8
|
-
[Beta in Development]
|
|
9
|
-
|
|
10
|
-
A library that normalizes simple SQL queries and compares them first by equality of the normalized string and then using the cosette API.
|
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from . import BaseAccess as Ba
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
import sqlparse
|
|
7
|
-
except ImportError:
|
|
8
|
-
print("Trying to Install required module: sqlparse\n")
|
|
9
|
-
os.system('pip install sqlparse')
|
|
10
|
-
import sqlparse
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
import requests
|
|
15
|
-
except ImportError:
|
|
16
|
-
print("Trying to Install required module: requests\n")
|
|
17
|
-
os.system('pip install requests')
|
|
18
|
-
import requests
|
|
19
|
-
|
|
20
|
-
from sqlparse.sql import Identifier, IdentifierList, Where, Comparison, Function, Parenthesis
|
|
21
|
-
from sqlparse.tokens import Keyword, DML, Name, Wildcard
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def normalizeSQLQuery(query, baseDict):
|
|
25
|
-
try:
|
|
26
|
-
query = query.replace("\"", "'")
|
|
27
|
-
parsed = sqlparse.parse(query)[0]
|
|
28
|
-
parsed.tokens = [token for token in parsed.tokens if not token.is_whitespace]
|
|
29
|
-
except Exception as e:
|
|
30
|
-
raise Exception(f"\nSyntax-Fehler in der SQL-Abfrage.")
|
|
31
|
-
|
|
32
|
-
formatted_query = []
|
|
33
|
-
alias_map = {}
|
|
34
|
-
|
|
35
|
-
def process_identifier(identifier, alias_map, baseDict: dict):
|
|
36
|
-
if isinstance(identifier, Identifier):
|
|
37
|
-
if identifier.get_real_name() and identifier.get_parent_name() and identifier.get_parent_name().lower() in alias_map.keys():
|
|
38
|
-
return f"{alias_map[identifier.get_parent_name().lower()].lower()}.{identifier.get_real_name().lower()}"
|
|
39
|
-
elif identifier.get_real_name():
|
|
40
|
-
tables = findTableForColumn(baseDict, identifier.get_real_name(), alias_map.keys())
|
|
41
|
-
if len(tables) == 1:
|
|
42
|
-
alias_map[tables[0].lower()] = tables[0]
|
|
43
|
-
return f"{tables[0].lower()}.{identifier.get_real_name().lower()}"
|
|
44
|
-
else:
|
|
45
|
-
return f"{identifier.get_real_name().lower()}"
|
|
46
|
-
return str(identifier)
|
|
47
|
-
|
|
48
|
-
def process_select(select, alias_map, insideFunction=False):
|
|
49
|
-
select_tokens = []
|
|
50
|
-
for token in select.tokens:
|
|
51
|
-
if isinstance(token, IdentifierList):
|
|
52
|
-
for identifier in token.get_identifiers():
|
|
53
|
-
select_tokens.append(f"{process_identifier(identifier, alias_map, baseDict).lower()} as {identifier.get_real_name().lower()}")
|
|
54
|
-
elif isinstance(token, Identifier):
|
|
55
|
-
if insideFunction:
|
|
56
|
-
select_tokens.append(process_identifier(token, alias_map, baseDict).lower())
|
|
57
|
-
else:
|
|
58
|
-
select_tokens.append(f"{process_identifier(token, alias_map, baseDict).lower()} as {token.get_real_name().lower()}")
|
|
59
|
-
elif isinstance(token, Function):
|
|
60
|
-
#select_tokens.append(f"{token.get_name().lower()}({process_identifier(token.get_parameters(), alias_map, baseDict).lower()})")
|
|
61
|
-
for par in token.tokens:
|
|
62
|
-
if isinstance(par, Identifier):
|
|
63
|
-
select_tokens.append(par.get_name().lower()+"(")
|
|
64
|
-
if isinstance(par, Parenthesis):
|
|
65
|
-
select_tokens[-1] += process_select(par, alias_map, True)+") as funcResult"
|
|
66
|
-
elif token.ttype is Wildcard:
|
|
67
|
-
select_tokens.append("*")
|
|
68
|
-
else:
|
|
69
|
-
continue
|
|
70
|
-
select_tokens.sort()
|
|
71
|
-
return ",".join(select_tokens)
|
|
72
|
-
|
|
73
|
-
def process_from(from_, alias_map):
|
|
74
|
-
from_tokens = []
|
|
75
|
-
if hasattr(from_, 'tokens'):
|
|
76
|
-
for token in from_.tokens:
|
|
77
|
-
if isinstance(token, IdentifierList):
|
|
78
|
-
for identifier in token.get_identifiers():
|
|
79
|
-
alias_map[identifier.get_real_name().lower()] = identifier.get_alias() or identifier.get_real_name()
|
|
80
|
-
from_tokens.append(f"{identifier.get_real_name().lower()} {alias_map[identifier.get_real_name().lower()].lower()}")
|
|
81
|
-
elif isinstance(token, Identifier):
|
|
82
|
-
alias_map[token.get_real_name().lower()] = token.get_alias() or token.get_real_name()
|
|
83
|
-
from_tokens.append(f"{token.get_real_name().lower()} {alias_map[token.get_real_name().lower()].lower()}")
|
|
84
|
-
elif token.ttype is not None and token.ttype is Name:
|
|
85
|
-
alias_map[token.value.lower()] = token.value.lower()
|
|
86
|
-
from_tokens.append(f"{token.value.lower()} {alias_map[token.value.lower()].lower()}")
|
|
87
|
-
else:
|
|
88
|
-
continue
|
|
89
|
-
from_tokens.sort()
|
|
90
|
-
return ",".join(from_tokens)
|
|
91
|
-
|
|
92
|
-
def process_groupby(groupby_, alias_map):
|
|
93
|
-
groupby_tokens = []
|
|
94
|
-
if(isinstance(groupby_, Identifier)):
|
|
95
|
-
groupby_tokens.append(process_identifier(groupby_, alias_map, baseDict).lower())
|
|
96
|
-
elif isinstance(token, IdentifierList):
|
|
97
|
-
for identifier in token.get_identifiers():
|
|
98
|
-
groupby_tokens.append(process_identifier(identifier, alias_map, baseDict).lower())
|
|
99
|
-
groupby_tokens.sort()
|
|
100
|
-
return ",".join(groupby_tokens)
|
|
101
|
-
|
|
102
|
-
def process_condition(token, alias_map, baseDict):
|
|
103
|
-
left, operator, right = [t for t in token.tokens if not t.is_whitespace]
|
|
104
|
-
|
|
105
|
-
flipAllowed = True
|
|
106
|
-
leftLiteral = False
|
|
107
|
-
rightLiteral = False
|
|
108
|
-
|
|
109
|
-
if is_value(right):
|
|
110
|
-
flipAllowed = False
|
|
111
|
-
rightLiteral = True
|
|
112
|
-
|
|
113
|
-
if is_value(left):
|
|
114
|
-
left, right = right, left
|
|
115
|
-
if operator.value == ">":
|
|
116
|
-
operator.value = "<"
|
|
117
|
-
elif operator.value == "<":
|
|
118
|
-
operator.value = ">"
|
|
119
|
-
leftLiteral = rightLiteral
|
|
120
|
-
rightLiteral = True
|
|
121
|
-
flipAllowed = False
|
|
122
|
-
|
|
123
|
-
left = process_identifier(left, alias_map, baseDict)
|
|
124
|
-
right = process_identifier(right, alias_map, baseDict)
|
|
125
|
-
|
|
126
|
-
if flipAllowed and left.lower() >= right.lower():
|
|
127
|
-
left, right = right, left
|
|
128
|
-
if operator.value == ">":
|
|
129
|
-
operator = "<"
|
|
130
|
-
elif operator.value == "<":
|
|
131
|
-
operator = ">"
|
|
132
|
-
|
|
133
|
-
return f"{left if leftLiteral else left.lower()} {operator} {right if rightLiteral else right.lower()}"
|
|
134
|
-
|
|
135
|
-
def is_value(token):
|
|
136
|
-
return token.ttype in (sqlparse.tokens.Token.Literal.Number.Integer,
|
|
137
|
-
sqlparse.tokens.Token.Literal.Number.Float,
|
|
138
|
-
sqlparse.tokens.Token.Literal.String.Single,
|
|
139
|
-
sqlparse.tokens.Token.Literal.String.Symbol)
|
|
140
|
-
|
|
141
|
-
def process_paranthesis(parenthesis, alias_map, baseDict):
|
|
142
|
-
toks = []
|
|
143
|
-
|
|
144
|
-
bracketsRequired = True
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for token in parenthesis.tokens:
|
|
148
|
-
if isinstance(token, Comparison):
|
|
149
|
-
toks.append(process_condition(token, alias_map, baseDict))
|
|
150
|
-
elif isinstance(token, Parenthesis):
|
|
151
|
-
toks.append(process_paranthesis(token, alias_map, baseDict))
|
|
152
|
-
|
|
153
|
-
elif token.value == '(':
|
|
154
|
-
toks.append(token.value)
|
|
155
|
-
else:
|
|
156
|
-
continue
|
|
157
|
-
x = parenthesis.flatten()
|
|
158
|
-
return " ".join(toks)
|
|
159
|
-
|
|
160
|
-
def process_where(where, alias_map, baseDict):
|
|
161
|
-
conditions = []
|
|
162
|
-
current_condition = []
|
|
163
|
-
|
|
164
|
-
for token in where.tokens:
|
|
165
|
-
if token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
|
|
166
|
-
continue
|
|
167
|
-
|
|
168
|
-
if token.ttype is Keyword and token.value.upper() in ('AND', 'OR'):
|
|
169
|
-
if current_condition:
|
|
170
|
-
conditions.append(''.join(str(t) for t in current_condition).strip())
|
|
171
|
-
current_condition = []
|
|
172
|
-
conditions.append(token.value.upper())
|
|
173
|
-
elif isinstance(token, Comparison):
|
|
174
|
-
current_condition.append(process_condition(token, alias_map, baseDict))
|
|
175
|
-
elif isinstance(token, Parenthesis):
|
|
176
|
-
current_condition.append(process_paranthesis(token, alias_map, baseDict))
|
|
177
|
-
else:
|
|
178
|
-
current_condition.append(token)
|
|
179
|
-
|
|
180
|
-
if current_condition:
|
|
181
|
-
conditions.append(''.join(str(t) for t in current_condition).strip())
|
|
182
|
-
|
|
183
|
-
sorted_conditions = []
|
|
184
|
-
current_group = []
|
|
185
|
-
last_connector = ""
|
|
186
|
-
|
|
187
|
-
if "OR" not in conditions:
|
|
188
|
-
for condition in conditions:
|
|
189
|
-
if condition == 'AND':
|
|
190
|
-
pass
|
|
191
|
-
else:
|
|
192
|
-
current_group.append(condition)
|
|
193
|
-
|
|
194
|
-
if current_group:
|
|
195
|
-
sorted_conditions.extend(sorted(current_group))
|
|
196
|
-
|
|
197
|
-
return " AND ".join(sorted_conditions)
|
|
198
|
-
else:
|
|
199
|
-
return " ".join(conditions)
|
|
200
|
-
|
|
201
|
-
where_index = query.upper().find('WHERE')
|
|
202
|
-
normalized_query = query[:where_index + 5] + ' ' + ' '.join(sorted_conditions)
|
|
203
|
-
|
|
204
|
-
def process_where_xx(where, alias_map, baseDict):
|
|
205
|
-
where_tokens = []
|
|
206
|
-
for token in where.tokens:
|
|
207
|
-
|
|
208
|
-
if isinstance(token, Comparison):
|
|
209
|
-
left, operator, right = [t for t in token.tokens if not t.is_whitespace]
|
|
210
|
-
left = process_identifier(left, alias_map, baseDict)
|
|
211
|
-
right = process_identifier(right, alias_map, baseDict)
|
|
212
|
-
if left.lower() >= right.lower():
|
|
213
|
-
left, right = right, left
|
|
214
|
-
if operator.value == ">":
|
|
215
|
-
operator = "<"
|
|
216
|
-
elif operator.value == "<":
|
|
217
|
-
operator = ">"
|
|
218
|
-
|
|
219
|
-
where_tokens.append(f"{left.lower()} {operator} {right.lower()}")
|
|
220
|
-
elif token.ttype is Keyword:
|
|
221
|
-
where_tokens.append(token.value.upper())
|
|
222
|
-
elif token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
|
|
223
|
-
continue
|
|
224
|
-
else:
|
|
225
|
-
where_tokens.append(str(token))
|
|
226
|
-
return " ".join(where_tokens)
|
|
227
|
-
|
|
228
|
-
# First pass to process FROM clause and populate alias_map
|
|
229
|
-
for token in parsed.tokens:
|
|
230
|
-
if token.is_whitespace:
|
|
231
|
-
continue
|
|
232
|
-
elif token.ttype is Keyword and token.value.upper() == 'FROM':
|
|
233
|
-
formatted_query.append('FROM')
|
|
234
|
-
pass
|
|
235
|
-
elif isinstance(token, Identifier) and formatted_query and formatted_query[-1] == 'FROM':
|
|
236
|
-
il = IdentifierList([token])
|
|
237
|
-
formatted_query.append(process_from(il, alias_map))
|
|
238
|
-
process_from(il, alias_map)
|
|
239
|
-
elif isinstance(token, IdentifierList) and formatted_query and formatted_query[-1] == 'FROM':
|
|
240
|
-
formatted_query.append(process_from(token, alias_map))
|
|
241
|
-
process_from(token, alias_map)
|
|
242
|
-
|
|
243
|
-
formatted_query = []
|
|
244
|
-
|
|
245
|
-
# Second pass to process SELECT and WHERE clauses
|
|
246
|
-
for token in parsed.tokens:
|
|
247
|
-
if token.is_whitespace:
|
|
248
|
-
continue
|
|
249
|
-
elif token.ttype is DML and token.value.upper() == 'SELECT':
|
|
250
|
-
formatted_query.append('SELECT')
|
|
251
|
-
elif token.ttype is Keyword and token.value.upper() == 'FROM':
|
|
252
|
-
formatted_query.append('FROM')
|
|
253
|
-
elif token.ttype is Keyword and token.value.upper() == 'GROUP BY':
|
|
254
|
-
formatted_query.append('GROUP BY')
|
|
255
|
-
elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'FROM':
|
|
256
|
-
formatted_query.append(process_from(token, alias_map))
|
|
257
|
-
elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'GROUP BY':
|
|
258
|
-
formatted_query.append(process_groupby(token, alias_map))
|
|
259
|
-
elif isinstance(token, Where):
|
|
260
|
-
formatted_query.append('WHERE')
|
|
261
|
-
formatted_query.append(process_where(token, alias_map, baseDict))
|
|
262
|
-
elif formatted_query and formatted_query[-1] == 'SELECT' and (isinstance(token, IdentifierList) or isinstance(token, Function) or isinstance(token, Identifier)):
|
|
263
|
-
if isinstance(token, Function):
|
|
264
|
-
token = IdentifierList([token])
|
|
265
|
-
formatted_query.append(process_select(token, alias_map))
|
|
266
|
-
else:
|
|
267
|
-
formatted_query.append(str(token))
|
|
268
|
-
|
|
269
|
-
return " ".join(formatted_query)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def findTableForColumn(data_dict, target_value, relevantTables):
|
|
273
|
-
l = []
|
|
274
|
-
for key, value_list in data_dict.items():
|
|
275
|
-
if key.lower() in relevantTables:
|
|
276
|
-
for sublist in value_list:
|
|
277
|
-
if sublist and sublist[0].lower() == target_value.lower():
|
|
278
|
-
l.append(key)
|
|
279
|
-
if len(l) == 0:
|
|
280
|
-
for key, value_list in data_dict.items():
|
|
281
|
-
for sublist in value_list:
|
|
282
|
-
if sublist and sublist[0].lower() == target_value.lower():
|
|
283
|
-
l.append(key)
|
|
284
|
-
return l
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def getTableScheme(table_name: str, tableDict: dict):
|
|
288
|
-
tab = tableDict[table_name]
|
|
289
|
-
|
|
290
|
-
# Format the schema
|
|
291
|
-
schema = "(" + ",".join([f"{col[0]}:{col[1]}" for col in tab]) + ")"
|
|
292
|
-
return schema
|
|
293
|
-
|
|
294
|
-
def getCosetteKeyFromFile():
|
|
295
|
-
try:
|
|
296
|
-
with open("cosette_apikey.txt", "r") as f:
|
|
297
|
-
return f.read()
|
|
298
|
-
except FileNotFoundError:
|
|
299
|
-
return "NOKEY"
|
|
300
|
-
|
|
301
|
-
def buildAndSendCosetteRequest(baseDict, sql, sol):
|
|
302
|
-
|
|
303
|
-
err = ""
|
|
304
|
-
for i in range(2):
|
|
305
|
-
try:
|
|
306
|
-
apiKey=getCosetteKeyFromFile()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
schema = ""
|
|
310
|
-
for tab in baseDict.keys():
|
|
311
|
-
schema += f"schema sch{tab}{getTableScheme(tab, baseDict)};\n"
|
|
312
|
-
for tab in baseDict.keys():
|
|
313
|
-
schema += f"table {tab}(sch{tab});\n"
|
|
314
|
-
|
|
315
|
-
q1 = "query q1\n`"+sql+"`;\n"
|
|
316
|
-
q2 = "query q2\n`"+sol+"`;\n"
|
|
317
|
-
|
|
318
|
-
cosette = "-- random Kommentar\n" + schema + q1 + q2 + "verify q1 q2;\n"
|
|
319
|
-
print(cosette)
|
|
320
|
-
|
|
321
|
-
r = requests.post("https://demo.cosette.cs.washington.edu/solve",data={"api_key": apiKey, "query": cosette}, verify=False)
|
|
322
|
-
|
|
323
|
-
print(r.text)
|
|
324
|
-
return (r.json()['result'],r.text)
|
|
325
|
-
#return r.json()['result']
|
|
326
|
-
|
|
327
|
-
except Exception as e:
|
|
328
|
-
err = str(e)
|
|
329
|
-
return ("ERR", err)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def checkColumns(sqlPath, solPath):
|
|
333
|
-
bd = Ba.getTableDict()
|
|
334
|
-
sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
|
|
335
|
-
sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
|
|
336
|
-
|
|
337
|
-
if("SELECT" in sql and "FROM" in sql):
|
|
338
|
-
start = str.find(sql, "SELECT")
|
|
339
|
-
end = str.find(sql, "FROM")
|
|
340
|
-
submission = str.strip(sql[start:end])
|
|
341
|
-
print("'"+submission+"'")
|
|
342
|
-
|
|
343
|
-
start = str.find(sol, "SELECT")
|
|
344
|
-
end = str.find(sol, "FROM")
|
|
345
|
-
solution = str.strip(sol[start:end])
|
|
346
|
-
print("'"+solution+"'")
|
|
347
|
-
|
|
348
|
-
if submission == solution:
|
|
349
|
-
return ""
|
|
350
|
-
return "Ausgegebene Spalten sind nicht korrekt (oder nicht automatisch überprüfbar)."
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def checkTables(sqlPath, solPath):
|
|
354
|
-
bd = Ba.getTableDict()
|
|
355
|
-
sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
|
|
356
|
-
sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
|
|
357
|
-
|
|
358
|
-
if("SELECT" in sql and "FROM" in sql):
|
|
359
|
-
endFromKeywords = ["WHERE", "GROUP", "ORDER", "LIMIT", ";"]
|
|
360
|
-
|
|
361
|
-
start = str.find(sql, "FROM")
|
|
362
|
-
end = -1
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
for keyword in endFromKeywords:
|
|
366
|
-
if(str.find(sql, keyword) != -1):
|
|
367
|
-
end = str.find(sql, keyword)
|
|
368
|
-
break
|
|
369
|
-
if(end == -1):
|
|
370
|
-
end = len(sql)
|
|
371
|
-
|
|
372
|
-
submission = str.strip(sql[start:end])
|
|
373
|
-
print("'"+submission+"'")
|
|
374
|
-
|
|
375
|
-
start = str.find(sol, "FROM")
|
|
376
|
-
end = -1
|
|
377
|
-
|
|
378
|
-
for keyword in endFromKeywords:
|
|
379
|
-
if(str.find(sol, keyword) != -1):
|
|
380
|
-
end = str.find(sol, keyword)
|
|
381
|
-
break
|
|
382
|
-
if(end == -1):
|
|
383
|
-
end = len(sol)
|
|
384
|
-
|
|
385
|
-
solution = str.strip(sol[start:end])
|
|
386
|
-
print("'"+solution+"'")
|
|
387
|
-
|
|
388
|
-
if submission == solution:
|
|
389
|
-
return ""
|
|
390
|
-
return "Verwendete Tabellen sind nicht korrekt (oder nicht automatisch überprüfbar)."
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def checkEquality(sqlPath, solPath):
|
|
394
|
-
bd = Ba.getTableDict()
|
|
395
|
-
sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
|
|
396
|
-
sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
|
|
397
|
-
if(sql=='' or sol==''):
|
|
398
|
-
return "\n\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet."
|
|
399
|
-
|
|
400
|
-
if(sql==sol):
|
|
401
|
-
return ""
|
|
402
|
-
|
|
403
|
-
result = buildAndSendCosetteRequest(bd, sql, sol)
|
|
404
|
-
|
|
405
|
-
if(result[0] == "ERR"):
|
|
406
|
-
return "\n\nFehler bei der automatischen Überprüfung der Abgabe. Es kann keine Aussage über die Korrektheit der Abgabe getroffen werden."
|
|
407
|
-
elif(result[0] != "EQ"):
|
|
408
|
-
return "\n\nDie Abgabe stimmt nicht mit der Musterlösung überein."
|
|
409
|
-
return ""
|
|
410
|
-
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: sql_testing_tools
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Description-Content-Type: text/markdown
|
|
5
|
-
Requires-Dist: sqlparse>=0.5.1
|
|
6
|
-
Requires-Dist: requests>=2.32.3
|
|
7
|
-
|
|
8
|
-
[Beta in Development]
|
|
9
|
-
|
|
10
|
-
A library that normalizes simple SQL queries and compares them first by equality of the normalized string and then using the cosette API.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{sql_testing_tools-0.1.3 → sql_testing_tools-0.1.5}/sql_testing_tools.egg-info/top_level.txt
RENAMED
|
File without changes
|