sql-testing-tools 0.1.9__tar.gz → 0.2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: sql_testing_tools
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: sqlparse>=0.5.1
6
6
  Requires-Dist: requests>=2.32.3
@@ -15,8 +15,8 @@ A library that normalizes simple SQL queries and compares them first by equality
15
15
  [![Build and Test](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml/badge.svg)](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml)
16
16
  [![Build and Test](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml/badge.svg)](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml)
17
17
 
18
- ### V 0.1.9
19
- - Support GROUP BY
18
+ ### V 0.1.9 + 0.2.0
19
+ - Support ORDER BY
20
20
 
21
21
  ### V 0.1.8
22
22
  - Fixed linebreak problems: Linebreaks are now converted into whitespaces before parsing where tokens
@@ -5,8 +5,8 @@ A library that normalizes simple SQL queries and compares them first by equality
5
5
  [![Build and Test](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml/badge.svg)](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml)
6
6
  [![Build and Test](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml/badge.svg)](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml)
7
7
 
8
- ### V 0.1.9
9
- - Support GROUP BY
8
+ ### V 0.1.9 + 0.2.0
9
+ - Support ORDER BY
10
10
 
11
11
  ### V 0.1.8
12
12
  - Fixed linebreak problems: Linebreaks are now converted into whitespaces before parsing where tokens
@@ -6,7 +6,7 @@ with open("README.md", "r") as fh:
6
6
 
7
7
  setup(
8
8
  name='sql_testing_tools',
9
- version='0.1.9',
9
+ version='0.2.0',
10
10
  packages=find_packages(),
11
11
  install_requires=[
12
12
  'sqlparse>=0.5.1',
@@ -0,0 +1,171 @@
1
+ import sqlite3
2
+ import os
3
+ #from importlib import resources
4
+
5
+
6
+ __db=""
7
+
8
+ def setDBName(path: str):
9
+ global __db
10
+ print("Setting DB to: " + path)
11
+ __db = path
12
+ #with resources.path('sql_testing_tools', path) as db_path:
13
+ # __db = str(db_path)
14
+ # print(__db)
15
+
16
+
17
+ #setDBName("databases/bayern.db")
18
+
19
+ def run(sql: str):
20
+ con = sqlite3.connect(__db)
21
+ cur = con.cursor()
22
+ cur.execute(sql)
23
+ con.commit()
24
+ return cur
25
+
26
+ def runFromFile(path: str):
27
+ sql = getSQLFromFile(path)
28
+ if not sql:
29
+ raise Exception("\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet.")
30
+
31
+
32
+ if sql.lower().find("drop") != -1:
33
+ raise Exception("Guter Versuch, aber die Datenbank wird bei jedem Upload zurückgesetzt ;)")
34
+
35
+ try:
36
+ res = run(sql)
37
+ except Exception as e:
38
+ raise Exception(f"\n\nSyntax-Fehler in der SQL-Abfrage:\n{str(e)}")
39
+
40
+ headers = [h[0] for h in res.description] if res.description else []
41
+ rows = res.fetchall() if res else []
42
+
43
+ return headers, rows
44
+
45
+
46
+ def runAndGetStringTable_fromFile(path: str, count: int = 5, maxLineLength: int = 85):
47
+ try:
48
+ headers, rows = runFromFile(path)
49
+ resultCount = len(rows)
50
+ rows = rows[:count]
51
+ s = ""
52
+
53
+ matrix = [] #[[]*(len(rows)+1)]*len(headers)
54
+
55
+ for col in range(len(headers)):
56
+ matrix.append([])
57
+ matrix[-1].append(headers[col])
58
+
59
+ if rows is not None:
60
+ for col in range(len(matrix)):
61
+ for row in range(0, len(rows)):
62
+ if len(rows[row]) > col:
63
+ matrix[col].append(rows[row][col])
64
+
65
+ for col in range(len(matrix)):
66
+ maxLength = max([len(str(x)) for x in matrix[col]])
67
+
68
+ for row in range(0, len(matrix[col])):
69
+ matrix[col][row] = str(matrix[col][row]).ljust(maxLength)
70
+
71
+ normalizedRows = []
72
+ spacing = " "
73
+ spacingLength = len(spacing) * (len(matrix[0]) - 1)
74
+
75
+ lineLength = sum([len(x[0]) for x in matrix]) + spacingLength
76
+
77
+
78
+
79
+ if lineLength > maxLineLength:
80
+ valLength = lineLength - spacingLength
81
+ maxColLength = int(maxLineLength/len(matrix))
82
+
83
+ for col in range(len(matrix)):
84
+ for row in range(0, len(matrix[col])):
85
+ cutContent = len(matrix[col][row].strip()) > maxColLength
86
+ if cutContent:
87
+ matrix[col][row] = matrix[col][row][:maxColLength-2] + ".."
88
+ else:
89
+ matrix[col][row] = matrix[col][row][:maxColLength]
90
+
91
+
92
+ for row in range(min(count+1, len(matrix[0]))):
93
+ normalizedRows.append(spacing.join([str(matrix[col][row]) for col in range(len(matrix))]))
94
+ normalizedRows.insert(1, len(normalizedRows[0])*"-")
95
+
96
+ s = "\n".join(normalizedRows[:count+2])
97
+ s += "\n" + normalizedRows[1]
98
+
99
+ if (resultCount > count):
100
+ s += "\n... " + str(resultCount - count) + " weitere Zeilen"
101
+ else:
102
+ s += "\n... keine weiteren Zeilen"
103
+
104
+ #longestLineInS = 85 # max([len(x.replace("\t", " ")) for x in s.split("\n")])
105
+
106
+ #s = "-" * longestLineInS + "\n" + s + "\n" + "-" * longestLineInS
107
+ s = "\n\nDiese Meldung sagt nichts über die Korrektheit der Abgabe aus!\nDie ersten " + str(
108
+ count) + " Zeilen des Ergebnisses der SQL-Abfrage:\n\n" + s
109
+
110
+ return s.replace("None", "NULL")
111
+ except Exception as ex:
112
+ raise ex
113
+
114
+ def getSQLFromFile(path: str):
115
+ try:
116
+ with open(path, "r") as f:
117
+ s = f.read()
118
+ if not s:
119
+ raise Exception("\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet.")
120
+ return s
121
+ except FileNotFoundError:
122
+ raise Exception(f"\nSQL-Datei nicht gefunden! Überprüfe, dass der Name korrekt ist ({path.split('/')[-1]}) und die Datei nicht gelöscht oder in einen Unterordner verschoben wurde.")
123
+
124
+
125
+
126
+ def getWorkingDir():
127
+ return os.getcwd()
128
+
129
+
130
+ def getWorkingDirFiles():
131
+ return os.listdir()
132
+
133
+ def mapDatabaseTypes(t):
134
+ # Map SQLite types to Python types
135
+ type_mapping = {
136
+ "INTEGER": "int",
137
+ "TEXT": "str",
138
+ "VARCHAR": "str",
139
+ "REAL": "float",
140
+ "BLOB": "bytes",
141
+ "NUMERIC": "float",
142
+ "DOUBLE": "float"
143
+ }
144
+ t = t.upper()
145
+ t = type_mapping.get(t, t)
146
+
147
+ if "INT" in t:
148
+ t = "int"
149
+ elif "VARCHAR" in t:
150
+ t = "str"
151
+ elif "CHAR" in t:
152
+ t = "str"
153
+
154
+ return t
155
+
156
+
157
+
158
+
159
+ def getTableDict():
160
+ res = run("SELECT name FROM sqlite_master WHERE type='table';")
161
+ tables = res.fetchall()
162
+
163
+ table_dict = {}
164
+ for table in tables:
165
+ res = run(f"PRAGMA table_info({table[0]})")
166
+ r = res.fetchall()
167
+ if table[0] == "sqlite_sequence":
168
+ continue
169
+ table_dict[table[0]] = [[c[1], mapDatabaseTypes(c[2])] for c in r]
170
+
171
+ return table_dict
@@ -0,0 +1,214 @@
1
+ try:
2
+ import BaseAccess as Ba
3
+ import TokenProcessing as Tp
4
+ except ImportError:
5
+ from . import BaseAccess as Ba
6
+ from . import TokenProcessing as Tp
7
+
8
+ import requests
9
+ import sqlparse
10
+
11
+ from sqlparse.sql import Identifier, IdentifierList, Where, Comparison, Function, Parenthesis
12
+ from sqlparse.tokens import Keyword, DML, Name, Wildcard
13
+
14
+
15
+
16
+ def normalizeSQLQuery(query, baseDict):
17
+ try:
18
+ query = query.replace("\"", "'")
19
+ parsed = sqlparse.parse(query)[0]
20
+ parsed.tokens = [token for token in parsed.tokens if not token.is_whitespace]
21
+ except Exception as e:
22
+ raise Exception(f"\nSyntax-Fehler in der SQL-Abfrage.")
23
+
24
+ formatted_query = []
25
+ alias_map = {}
26
+
27
+ # First pass to process FROM clause and populate alias_map
28
+ for token in parsed.tokens:
29
+ if token.is_whitespace:
30
+ continue
31
+ elif token.ttype is Keyword and token.value.upper() == 'FROM':
32
+ formatted_query.append('FROM')
33
+ pass
34
+ elif isinstance(token, Identifier) and formatted_query and formatted_query[-1] == 'FROM':
35
+ il = IdentifierList([token])
36
+ formatted_query.append(Tp._from(il, alias_map, baseDict))
37
+ Tp._from(il, alias_map, baseDict)
38
+ elif isinstance(token, IdentifierList) and formatted_query and formatted_query[-1] == 'FROM':
39
+ formatted_query.append(Tp._from(token, alias_map, baseDict))
40
+ Tp._from(token, alias_map, baseDict)
41
+
42
+ formatted_query = []
43
+
44
+ # Second pass to process SELECT, WHERE, GROUP BY, and ORDER BY clauses
45
+ for token in parsed.tokens:
46
+ if token.is_whitespace:
47
+ continue
48
+ elif token.ttype is DML and token.value.upper() == 'SELECT':
49
+ formatted_query.append('SELECT')
50
+ elif token.ttype is Keyword and token.value.upper() == 'FROM':
51
+ formatted_query.append('FROM')
52
+ elif token.ttype is Keyword and token.value.upper() == 'GROUP BY':
53
+ formatted_query.append('GROUP BY')
54
+ elif token.ttype is Keyword and token.value.upper() == 'ORDER BY':
55
+ formatted_query.append('ORDER BY')
56
+ elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'FROM':
57
+ formatted_query.append(Tp._from(token, alias_map, baseDict))
58
+ elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'GROUP BY':
59
+ formatted_query.append(Tp._groupby(token, alias_map, baseDict))
60
+ elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'ORDER BY':
61
+ formatted_query.append(Tp._orderby(token, alias_map, baseDict))
62
+ elif isinstance(token, Where):
63
+ formatted_query.append('WHERE')
64
+ formatted_query.append(Tp._where(token, alias_map, baseDict))
65
+ elif formatted_query and formatted_query[-1] == 'SELECT' and (isinstance(token, IdentifierList) or isinstance(token, Function) or isinstance(token, Identifier)):
66
+ if isinstance(token, Function):
67
+ token = IdentifierList([token])
68
+ formatted_query.append(Tp._select(token, alias_map, baseDict))
69
+ else:
70
+ formatted_query.append(str(token))
71
+
72
+ return " ".join(formatted_query)
73
+
74
+
75
+ def findTableForColumn(data_dict, target_value, relevantTables):
76
+ l = []
77
+ for key, value_list in data_dict.items():
78
+ if key.lower() in relevantTables:
79
+ for sublist in value_list:
80
+ if sublist and sublist[0].lower() == target_value.lower():
81
+ l.append(key)
82
+ if len(l) == 0:
83
+ for key, value_list in data_dict.items():
84
+ for sublist in value_list:
85
+ if sublist and sublist[0].lower() == target_value.lower():
86
+ l.append(key)
87
+ return l
88
+
89
+
90
+ def getTableScheme(table_name: str, tableDict: dict):
91
+ tab = tableDict[table_name]
92
+
93
+ # Format the schema
94
+ schema = "(" + ",".join([f"{col[0]}:{col[1]}" for col in tab]) + ")"
95
+ return schema
96
+
97
+ def getCosetteKeyFromFile():
98
+ try:
99
+ with open("cosette_apikey.txt", "r") as f:
100
+ return f.read()
101
+ except FileNotFoundError:
102
+ return "NOKEY"
103
+
104
+ def buildAndSendCosetteRequest(baseDict, sql, sol):
105
+
106
+ err = ""
107
+ for i in range(2):
108
+ try:
109
+ apiKey=getCosetteKeyFromFile()
110
+
111
+
112
+ schema = ""
113
+ for tab in baseDict.keys():
114
+ schema += f"schema sch{tab}{getTableScheme(tab, baseDict)};\n"
115
+ for tab in baseDict.keys():
116
+ schema += f"table {tab}(sch{tab});\n"
117
+
118
+ q1 = "query q1\n`"+sql+"`;\n"
119
+ q2 = "query q2\n`"+sol+"`;\n"
120
+
121
+ cosette = "-- random Kommentar\n" + schema + q1 + q2 + "verify q1 q2;\n"
122
+ print(cosette)
123
+
124
+ r = requests.post("https://demo.cosette.cs.washington.edu/solve",data={"api_key": apiKey, "query": cosette}, verify=False)
125
+
126
+ print(r.text)
127
+ return (r.json()['result'],r.text)
128
+ #return r.json()['result']
129
+
130
+ except Exception as e:
131
+ err = str(e)
132
+ return ("ERR", err)
133
+
134
+
135
+ def checkColumns(sqlPath, solPath):
136
+ bd = Ba.getTableDict()
137
+ sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
138
+ sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
139
+
140
+ if("SELECT" in sql and "FROM" in sql):
141
+ start = str.find(sql, "SELECT")
142
+ end = str.find(sql, "FROM")
143
+ submission = str.strip(sql[start:end])
144
+ print("'"+submission+"'")
145
+
146
+ start = str.find(sol, "SELECT")
147
+ end = str.find(sol, "FROM")
148
+ solution = str.strip(sol[start:end])
149
+ print("'"+solution+"'")
150
+
151
+ if submission == solution:
152
+ return ""
153
+ return "Ausgegebene Spalten sind nicht korrekt (oder nicht automatisch überprüfbar)."
154
+
155
+
156
+ def checkTables(sqlPath, solPath):
157
+ bd = Ba.getTableDict()
158
+ sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
159
+ sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
160
+
161
+ if("SELECT" in sql and "FROM" in sql):
162
+ endFromKeywords = ["WHERE", "GROUP", "ORDER", "LIMIT", ";"]
163
+
164
+ start = str.find(sql, "FROM")
165
+ end = -1
166
+
167
+
168
+ for keyword in endFromKeywords:
169
+ if(str.find(sql, keyword) != -1):
170
+ end = str.find(sql, keyword)
171
+ break
172
+ if(end == -1):
173
+ end = len(sql)
174
+
175
+ submission = str.strip(sql[start:end])
176
+ print("'"+submission+"'")
177
+
178
+ start = str.find(sol, "FROM")
179
+ end = -1
180
+
181
+ for keyword in endFromKeywords:
182
+ if(str.find(sol, keyword) != -1):
183
+ end = str.find(sol, keyword)
184
+ break
185
+ if(end == -1):
186
+ end = len(sol)
187
+
188
+ solution = str.strip(sol[start:end])
189
+ print("'"+solution+"'")
190
+
191
+ if submission == solution:
192
+ return ""
193
+ return "Verwendete Tabellen sind nicht korrekt (oder nicht automatisch überprüfbar)."
194
+
195
+
196
+ def checkEquality(sqlPath, solPath):
197
+ bd = Ba.getTableDict()
198
+ sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
199
+ sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
200
+ if(sql=='' or sol==''):
201
+ return "\n\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet."
202
+
203
+ if(sql==sol):
204
+ return ""
205
+
206
+ result = buildAndSendCosetteRequest(bd, sql, sol)
207
+
208
+ if(result[0] == "ERR"):
209
+ return "\n\nFehler bei der automatischen Überprüfung der Abgabe. Es kann keine Aussage über die Korrektheit der Abgabe getroffen werden."
210
+ elif(result[0] != "EQ"):
211
+ return "\n\nDie Abgabe stimmt nicht mit der Musterlösung überein."
212
+ return ""
213
+
214
+
@@ -0,0 +1,369 @@
1
+ import sqlparse
2
+ from sqlparse.sql import Identifier, IdentifierList, Where, Comparison, Function, Parenthesis
3
+ from sqlparse.tokens import Keyword, DML, Name, Wildcard, Whitespace, Newline
4
+
5
+ try:
6
+ import Helper as He
7
+ except ImportError:
8
+ from . import Helper as He
9
+
10
+
11
+ #####################################
12
+ #####################################
13
+
14
+ def _identifier(identifier, alias_map, baseDict: dict):
15
+ if isinstance(identifier, Identifier):
16
+ if identifier.get_real_name() and identifier.get_parent_name() and identifier.get_parent_name().lower() in alias_map.keys():
17
+ return f"{alias_map[identifier.get_parent_name().lower()].lower()}.{identifier.get_real_name().lower()}"
18
+ elif identifier.get_real_name():
19
+ tables = He.findTableForColumn(baseDict, identifier.get_real_name(), alias_map.keys())
20
+ if len(tables) == 1:
21
+ alias_map[tables[0].lower()] = tables[0]
22
+ return f"{tables[0].lower()}.{identifier.get_real_name().lower()}"
23
+ else:
24
+ return f"{identifier.get_real_name().lower()}"
25
+ return str(identifier)
26
+
27
+
28
+ #####################################
29
+ #####################################
30
+
31
+
32
+ def _select(select, alias_map, baseDict: dict, insideFunction=False):
33
+ select_tokens = []
34
+ for token in select.tokens:
35
+ if isinstance(token, IdentifierList):
36
+ for identifier in token.get_identifiers():
37
+ select_tokens.append(f"{_identifier(identifier, alias_map, baseDict).lower()} as {identifier.get_real_name().lower()}")
38
+ elif isinstance(token, Identifier):
39
+ if insideFunction:
40
+ select_tokens.append(_identifier(token, alias_map, baseDict).lower())
41
+ else:
42
+ select_tokens.append(f"{_identifier(token, alias_map, baseDict).lower()} as {token.get_real_name().lower()}")
43
+ elif isinstance(token, Function):
44
+ #select_tokens.append(f"{token.get_name().lower()}({_identifier(token.get_parameters(), alias_map, baseDict).lower()})")
45
+ for par in token.tokens:
46
+ if isinstance(par, Identifier):
47
+ select_tokens.append(par.get_name().lower()+"(")
48
+ if isinstance(par, Parenthesis):
49
+ select_tokens[-1] += _select(par, alias_map, baseDict, True)+") as funcResult"
50
+ elif token.ttype is Wildcard:
51
+ select_tokens.append("*")
52
+ else:
53
+ continue
54
+ select_tokens.sort()
55
+ return ",".join(select_tokens)
56
+
57
+
58
+ #####################################
59
+ #####################################
60
+
61
+
62
+ def _from(from_, alias_map, baseDict: dict):
63
+ from_tokens = []
64
+ if hasattr(from_, 'tokens'):
65
+ for token in from_.tokens:
66
+ if isinstance(token, IdentifierList):
67
+ for identifier in token.get_identifiers():
68
+ alias_map[identifier.get_real_name().lower()] = identifier.get_alias() or identifier.get_real_name()
69
+ from_tokens.append(f"{identifier.get_real_name().lower()} {alias_map[identifier.get_real_name().lower()].lower()}")
70
+ elif isinstance(token, Identifier):
71
+ alias_map[token.get_real_name().lower()] = token.get_alias() or token.get_real_name()
72
+ from_tokens.append(f"{token.get_real_name().lower()} {alias_map[token.get_real_name().lower()].lower()}")
73
+ elif token.ttype is not None and token.ttype is Name:
74
+ alias_map[token.value.lower()] = token.value.lower()
75
+ from_tokens.append(f"{token.value.lower()} {alias_map[token.value.lower()].lower()}")
76
+ else:
77
+ continue
78
+ from_tokens.sort()
79
+ return ",".join(from_tokens)
80
+
81
+
82
+ #####################################
83
+ #####################################
84
+
85
+
86
+ def _groupby(groupby_, alias_map, baseDict: dict):
87
+ groupby_tokens = []
88
+ if(isinstance(groupby_, Identifier)):
89
+ groupby_tokens.append(_identifier(groupby_, alias_map, baseDict).lower())
90
+ elif isinstance(groupby_, IdentifierList):
91
+ for identifier in groupby_.get_identifiers():
92
+ groupby_tokens.append(_identifier(identifier, alias_map, baseDict).lower())
93
+ groupby_tokens.sort()
94
+ return ",".join(groupby_tokens)
95
+
96
+ def _orderby(orderby_, alias_map, baseDict: dict):
97
+ orderby_tokens = []
98
+ if isinstance(orderby_, Identifier):
99
+ orderby_tokens.append(_identifier(orderby_, alias_map, baseDict).lower())
100
+ elif isinstance(orderby_, IdentifierList):
101
+ for identifier in orderby_.get_identifiers():
102
+ orderby_tokens.append(_identifier(identifier, alias_map, baseDict).lower())
103
+ # orderby_tokens.sort()
104
+ return ",".join(orderby_tokens)
105
+
106
+ def _condition(token, alias_map, baseDict: dict):
107
+ left, operator, right = [t for t in token.tokens if not t.is_whitespace]
108
+
109
+ flipAllowed = True
110
+ leftLiteral = False
111
+ rightLiteral = False
112
+
113
+ if is_value(right):
114
+ flipAllowed = False
115
+ rightLiteral = True
116
+
117
+ if is_value(left):
118
+ left, right = right, left
119
+ if operator.value == ">":
120
+ operator.value = "<"
121
+ elif operator.value == "<":
122
+ operator.value = ">"
123
+ leftLiteral = rightLiteral
124
+ rightLiteral = True
125
+ flipAllowed = False
126
+
127
+ left = _identifier(left, alias_map, baseDict)
128
+ right = _identifier(right, alias_map, baseDict)
129
+
130
+ if flipAllowed and left.lower() >= right.lower():
131
+ left, right = right, left
132
+ if operator.value == ">":
133
+ operator = "<"
134
+ elif operator.value == "<":
135
+ operator = ">"
136
+
137
+ return f"{left if leftLiteral else left.lower()} {operator} {right if rightLiteral else right.lower()}"
138
+
139
+ def is_value(token):
140
+ return token.ttype in (sqlparse.tokens.Token.Literal.Number.Integer,
141
+ sqlparse.tokens.Token.Literal.Number.Float,
142
+ sqlparse.tokens.Token.Literal.String.Single,
143
+ sqlparse.tokens.Token.Literal.String.Symbol)
144
+
145
+
146
+
147
+ #####################################
148
+ #####################################
149
+
150
+
151
+ def _paranthesis(parenthesis, alias_map, baseDict: dict):
152
+ toks = []
153
+
154
+ bracketsRequired = True
155
+ closeBracketsAfterNext = False
156
+
157
+ and_count = sum(1 for token in parenthesis.tokens if token.ttype is Keyword and token.value == "AND") # Zähle "AND"-Tokens
158
+ or_count = sum(1 for token in parenthesis.tokens if token.ttype is Keyword and token.value == "OR") # Zähle "AND"-Tokens
159
+
160
+
161
+ if(and_count==1 and or_count == 0):
162
+ val = _2element_par(parenthesis, alias_map, baseDict, "AND")
163
+ return val
164
+ elif(and_count==0 and or_count == 1):
165
+ val = _2element_par(parenthesis, alias_map, baseDict, "OR")
166
+ return val
167
+
168
+
169
+
170
+
171
+
172
+
173
+ for token in parenthesis.tokens:
174
+ if isinstance(token, Comparison):
175
+ toks.append(_condition(token, alias_map, baseDict))
176
+ elif isinstance(token, Parenthesis):
177
+ toks.append(_paranthesis(token, alias_map, baseDict))
178
+
179
+ elif token.ttype is Keyword: #AND,OR
180
+ if token.value == "AND":
181
+ pass
182
+ elif token.value == "OR":
183
+ pass
184
+ elif token.value == "NOT":
185
+ pass
186
+
187
+ elif token.value == '(':
188
+ toks.append(token.value)
189
+ else:
190
+ continue
191
+ x = parenthesis.flatten()
192
+ return " ".join(toks)
193
+
194
+
195
+ def _2element_par(parenthesis, alias_map, baseDict: dict, keyword):
196
+ toks = []
197
+
198
+ for token in parenthesis.tokens:
199
+ if isinstance(token, Comparison):
200
+ toks.append(_condition(token, alias_map, baseDict))
201
+ elif isinstance(token, Parenthesis):
202
+ toks.append(_paranthesis(token, alias_map, baseDict))
203
+ toks = toks.sort()
204
+ fill = (" "+keyword+" ")
205
+ return fill.join(toks)
206
+
207
+
208
+ def count_keywordValues(tokens, values):
209
+ return len([token for token in tokens if token.ttype is Keyword and token.value.upper() in values])
210
+
211
+
212
+ def find_index_of_keyword(tokens, keyword):
213
+ for index, token in enumerate(tokens):
214
+ if token.ttype is Keyword and token.value.upper() == keyword:
215
+ return index
216
+ return -1 # Wenn kein AND-Token gefunden wird
217
+
218
+
219
+ def _where(where, alias_map, baseDict: dict):
220
+ conditions = []
221
+ current_condition = []
222
+
223
+
224
+
225
+ isParanthesis = isinstance(where, Parenthesis)
226
+
227
+ where_tokens = [token for token in where.tokens if (token.ttype is not Whitespace and token.ttype is not Newline)and token.value != "WHERE"]
228
+ where_string = " ".join([str(token) for token in where_tokens])
229
+
230
+ and_count = where_string.upper().count(" AND ")
231
+ or_count = where_string.upper().count(" OR ")
232
+
233
+ # No AND / OR
234
+ if and_count + or_count == 0:
235
+ return _where_simpleCondition(where, alias_map, baseDict)
236
+
237
+ # only ANDs / only ORs
238
+ elif and_count == 0 or or_count == 0:
239
+ return _where_sameStrengthKeywords(where_tokens, alias_map, baseDict, " AND " if and_count > 0 else " OR ")
240
+
241
+ # ANDs and ORs
242
+
243
+
244
+ # count keywords on top level
245
+ top_and_count = count_keywordValues(where_tokens, ["AND"])
246
+ top_or_count = count_keywordValues(where_tokens, ["OR"])
247
+
248
+ # two conditions on top level
249
+ if top_and_count + top_or_count == 1:
250
+ keyword = " AND " if top_and_count == 1 else " OR "
251
+ return _where_twoConditionsOnTopLevel(where, alias_map, baseDict, keyword, isParanthesis)
252
+ else:
253
+ where_tokens = _where_addBracketsAroundAND(where, alias_map, baseDict)
254
+ return _where(Where(where_tokens), alias_map, baseDict)
255
+
256
+ def _where_addBracketsAroundAND(where, alias_map, baseDict: dict):
257
+ index = find_index_of_keyword(where.tokens, "AND")
258
+ par = Parenthesis(where.tokens[index-2:index+3])
259
+ where_tokens = where.tokens[:index-2] + [par] + where.tokens[index+3:]
260
+ return where_tokens
261
+
262
+
263
+
264
+ # for token in where.tokens:
265
+ # if token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
266
+ # continue
267
+
268
+ # if token.ttype is Keyword and token.value.upper() in ('AND', 'OR'):
269
+ # if current_condition:
270
+ # conditions.append(''.join(str(t) for t in current_condition).strip())
271
+ # current_condition = []
272
+ # conditions.append(token.value.upper())
273
+ # else:
274
+ # current_condition.append(token)
275
+
276
+ # if current_condition:
277
+ # conditions.append(''.join(str(t) for t in current_condition).strip())
278
+
279
+ # sorted_conditions = []
280
+ # current_group = []
281
+ # last_connector = ""
282
+
283
+ # if "OR" not in conditions:
284
+ # for condition in conditions:
285
+ # if condition == 'AND':
286
+ # pass
287
+ # else:
288
+ # current_group.append(condition)
289
+
290
+ # if current_group:
291
+ # sorted_conditions.extend(sorted(current_group))
292
+
293
+ # return " AND ".join(sorted_conditions)
294
+ # else:
295
+ # return " ".join(conditions)
296
+
297
+ # where_index = query.upper().find('WHERE')
298
+
299
+ # normalized_query = query[:where_index + 5] + ' ' + ' '.join(sorted_conditions)
300
+
301
+
302
+
303
+ def _where_simpleCondition(where_tokens, alias_map, baseDict: dict):
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
+ return "".join(cond)
315
+
316
+ def _where_sameStrengthKeywords(where_tokens, alias_map, baseDict: dict, keyword: str):
317
+ cond = []
318
+ for tok in where_tokens:
319
+ if isinstance(tok, Comparison):
320
+ cond.append(_condition(tok, alias_map, baseDict))
321
+ elif isinstance(tok, Parenthesis):
322
+ for _tok in tok.tokens:
323
+ if isinstance(_tok, Comparison):
324
+ cond.append(_condition(_tok, alias_map, baseDict))
325
+ elif isinstance(_tok, Parenthesis):
326
+ tok = _tok
327
+ cond.sort()
328
+ return keyword.join(cond)
329
+
330
+ def _where_twoConditionsOnTopLevel(where, alias_map, baseDict: dict, keyword, isParanthesis):
331
+ current_condition = []
332
+ for tok in where.tokens:
333
+ if isinstance(tok, Comparison):
334
+ current_condition.append(_condition(tok, alias_map, baseDict))
335
+ elif isinstance(tok, Parenthesis):
336
+ current_condition.append(_where(tok, alias_map, baseDict))
337
+ current_condition.sort()
338
+ if keyword == " AND " or isParanthesis:
339
+ return "(" + keyword.join(current_condition) + ")"
340
+ else:
341
+ return keyword.join(current_condition)
342
+
343
+
344
+
345
+
346
+ # def _where_xx(where, alias_map, baseDict: dict):
347
+ # where_tokens = []
348
+ # for token in where.tokens:
349
+
350
+ # if isinstance(token, Comparison):
351
+ # left, operator, right = [t for t in token.tokens if not t.is_whitespace]
352
+ # left = _identifier(left, alias_map, baseDict)
353
+ # right = _identifier(right, alias_map, baseDict)
354
+ # if left.lower() >= right.lower():
355
+ # left, right = right, left
356
+ # if operator.value == ">":
357
+ # operator = "<"
358
+ # elif operator.value == "<":
359
+ # operator = ">"
360
+
361
+ # where_tokens.append(f"{left.lower()} {operator} {right.lower()}")
362
+ # elif token.ttype is Keyword:
363
+ # where_tokens.append(token.value.upper())
364
+ # elif token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
365
+ # continue
366
+ # else:
367
+ # where_tokens.append(str(token))
368
+ # return " ".join(where_tokens)
369
+
@@ -0,0 +1,3 @@
1
+ from .BaseAccess import *
2
+ from .Helper import *
3
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: sql_testing_tools
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: sqlparse>=0.5.1
6
6
  Requires-Dist: requests>=2.32.3
@@ -15,8 +15,8 @@ A library that normalizes simple SQL queries and compares them first by equality
15
15
  [![Build and Test](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml/badge.svg)](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-build.yml)
16
16
  [![Build and Test](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml/badge.svg)](https://github.com/ValentinHerrmann/sql_testing_tools/actions/workflows/python-unittests.yml)
17
17
 
18
- ### V 0.1.9
19
- - Support GROUP BY
18
+ ### V 0.1.9 + 0.2.0
19
+ - Support ORDER BY
20
20
 
21
21
  ### V 0.1.8
22
22
  - Fixed linebreak problems: Linebreaks are now converted into whitespaces before parsing where tokens
@@ -1,5 +1,9 @@
1
1
  README.md
2
2
  setup.py
3
+ sql_testing_tools/BaseAccess.py
4
+ sql_testing_tools/Helper.py
5
+ sql_testing_tools/TokenProcessing.py
6
+ sql_testing_tools/__init__.py
3
7
  sql_testing_tools.egg-info/PKG-INFO
4
8
  sql_testing_tools.egg-info/SOURCES.txt
5
9
  sql_testing_tools.egg-info/dependency_links.txt
@@ -0,0 +1 @@
1
+ sql_testing_tools