sql-error-categorizer 0.3.0__tar.gz → 0.3.1__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_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/PKG-INFO +3 -3
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/pyproject.toml +3 -3
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/requirements.txt +2 -2
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/complications.py +63 -197
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/logical.py +194 -324
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/semantic.py +92 -129
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/syntax.py +62 -103
- sql_error_categorizer-0.3.1/tests/2_sem/test_050_constant_column_output.py +35 -0
- sql_error_categorizer-0.3.1/tests/2_sem/test_051_duplicate_column_output.py +58 -0
- sql_error_categorizer-0.3.1/tests/3_log/test_112_118_missing_extraneous_where_clause.py +74 -0
- sql_error_categorizer-0.3.1/tests/3_log/test_113_119_missing_extraneous_group_by_clause.py +74 -0
- sql_error_categorizer-0.3.1/tests/3_log/test_114_120_missing_extraneous_having_clause.py +77 -0
- sql_error_categorizer-0.3.1/tests/3_log/test_115_121_missing_extraneous_order_by_clause.py +79 -0
- sql_error_categorizer-0.3.1/tests/3_log/test_116_121_123_missing_extraneous_incorrect_limit_clause.py +80 -0
- sql_error_categorizer-0.3.1/tests/3_log/test_117_122_missing_extraneous_incorrect_offset_clause.py +85 -0
- sql_error_categorizer-0.3.1/tests/4_com/test_126_unused_cte.py +44 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/.gitignore +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/.readthedocs.yaml +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/LICENSE +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/Makefile +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/README.md +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/catalogs/constraints.json +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/catalogs/miedema.json +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/sql/constraints.sql +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/sql/miedema.sql +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/Makefile +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/conf.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/index.rst +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/make.bat +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/requirements.txt +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/__init__.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/__init__.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/base.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/test_detector.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_001_ambiguous_column.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_003_undefined_column.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_004_undefined_function.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_005_undefined_parameter.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_006_undefined_object.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_007_invalid_schema_names.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_008_misspellings.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_012_is_where_not_applicable.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_013_data_type_mismatch.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_014_aggregate_function_outside_select_or_having.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_015_nested_aggregate_functions.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_016_extraneous_omitted_grouping_column.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_017_having_without_group_by.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_018_too_many_columns_in_subquery.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_021_using_where_twice.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_022_omitted_from.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_024_036_additional_omitted_semicolons.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_024_comparison_with_null.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_026_duplicate_clause.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_029_keywords_order.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_032_033_curly_square_or_unmatched_brackets.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_035_nonstandard_operators.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/2_sem/test_038_tautological_inconsistent_expressions.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/2_sem/test_039_distinct_sum_avg.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_058_join_on_incorrect_table.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_059_join_when_join_needs_to_be_omitted.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_062_missing_join.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_067_wildcards_without_like.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_068_069_wrong_invalid_wildcard.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_070_extraneous_column_in_select.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_071_missing_column_from_select.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_072_missing_distinct_from_select.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_073_missing_as_from_select.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_096_unnecessary_distinct_in_select.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_102_like_without_wildcards.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_106_unnecessary_distinct_in_aggregate_function.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_109_group_by_with_singleton_groups.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_111_group_by_can_be_replaced_by_distinct.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_114_order_by_in_subquery.py +0 -0
- {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql_error_categorizer
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: This project analyses SQL statements and labels possible errors or complications.
|
|
5
5
|
Project-URL: Repository, https://github.com/DavidePonzini/sql_error_categorizer
|
|
6
6
|
Project-URL: Documentation, https://sql-error-categorizer.readthedocs.io/en/latest/index.html
|
|
@@ -14,10 +14,10 @@ Requires-Python: >=3.11
|
|
|
14
14
|
Requires-Dist: psycopg2
|
|
15
15
|
Requires-Dist: python-dateutil
|
|
16
16
|
Requires-Dist: pyyaml
|
|
17
|
-
Requires-Dist: sql-error-taxonomy>=
|
|
17
|
+
Requires-Dist: sql-error-taxonomy>=2.0.0
|
|
18
18
|
Requires-Dist: sqlglot
|
|
19
19
|
Requires-Dist: sqlparse
|
|
20
|
-
Requires-Dist: sqlscope>=1.0.
|
|
20
|
+
Requires-Dist: sqlscope>=1.0.16
|
|
21
21
|
Requires-Dist: z3-solver
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sql_error_categorizer"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Davide Ponzini", email="davide.ponzini95@gmail.com" },
|
|
10
10
|
]
|
|
@@ -21,8 +21,8 @@ dependencies = [
|
|
|
21
21
|
"pyyaml",
|
|
22
22
|
"sqlparse",
|
|
23
23
|
"sqlglot",
|
|
24
|
-
"sqlscope>=1.0.
|
|
25
|
-
"sql_error_taxonomy>=
|
|
24
|
+
"sqlscope>=1.0.16",
|
|
25
|
+
"sql_error_taxonomy>=2.0.0",
|
|
26
26
|
"z3-solver",
|
|
27
27
|
"python-dateutil",
|
|
28
28
|
]
|
|
@@ -32,31 +32,31 @@ class ComplicationDetector(BaseDetector):
|
|
|
32
32
|
results: list[DetectedError] = super().run()
|
|
33
33
|
|
|
34
34
|
checks = [
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
self.
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
-
self.
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
52
|
-
self.
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
56
|
-
self.
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
35
|
+
self.detect_82_unnecessary_complication, # ok
|
|
36
|
+
self.detect_83_unnecessary_distinct_in_select_clause, # ok
|
|
37
|
+
self.detect_84_unnecessary_table_reference, # TODO: refactor/implement
|
|
38
|
+
self.detect_85_unused_correlation_name, # TODO: implement
|
|
39
|
+
self.detect_86_tables_have_same_data, # TODO: implement
|
|
40
|
+
self.detect_125_correlation_name_identical_to_table_name, # TODO: implement
|
|
41
|
+
self.detect_87_unnecessary_general_comparison_operator, # TODO: implement
|
|
42
|
+
self.detect_88_like_without_wildcards, # ok
|
|
43
|
+
self.detect_89_unnecessarily_complicated_select_in_exists_subquery, # TODO: implement
|
|
44
|
+
self.detect_90_in_exists_can_be_replaced_by_comparison, # TODO: implement
|
|
45
|
+
self.detect_91_unnecessary_aggregate_function, # TODO: implement
|
|
46
|
+
self.detect_92_unnecessary_distinct_in_aggregate_function, # ok
|
|
47
|
+
self.detect_93_unnecessary_argument_of_count, # ok
|
|
48
|
+
self.detect_94_unnecessary_group_by_in_exists_subquery, # TODO: implement
|
|
49
|
+
self.detect_95_group_by_with_singleton_groups, # ok
|
|
50
|
+
self.detect_96_group_by_with_only_a_single_group, # TODO: implement
|
|
51
|
+
self.detect_97_group_by_can_be_replaced_by_distinct, # ok
|
|
52
|
+
self.detect_98_union_can_be_replaced_by_or, # TODO: implement
|
|
53
|
+
self.detect_99_unnecessary_column_in_order_by_clause, # TODO: refactor/implement
|
|
54
|
+
self.detect_100_order_by_in_subquery, # TODO: implement
|
|
55
|
+
self.detect_101_inefficient_having, # TODO: implement
|
|
56
|
+
self.detect_102_inefficient_union, # TODO: implement
|
|
57
|
+
self.detect_103_condition_in_the_subquery_can_be_moved_up, # TODO: implement
|
|
58
|
+
self.detect_104_outer_join_can_be_replaced_by_inner_join, # TODO: implement
|
|
59
|
+
self.detect_126_unused_cte, #
|
|
60
60
|
]
|
|
61
61
|
|
|
62
62
|
for chk in checks:
|
|
@@ -64,11 +64,11 @@ class ComplicationDetector(BaseDetector):
|
|
|
64
64
|
|
|
65
65
|
return results
|
|
66
66
|
|
|
67
|
-
def
|
|
67
|
+
def detect_82_unnecessary_complication(self) -> list[DetectedError]:
|
|
68
68
|
'''NOTE: this is an umbrella term, so it can't be directly detected.'''
|
|
69
69
|
return []
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def detect_83_unnecessary_distinct_in_select_clause(self) -> list[DetectedError]:
|
|
72
72
|
'''
|
|
73
73
|
Flags a SELECT DISTINCT clause that is unnecessary because the selected
|
|
74
74
|
columns are already unique due to existing constraints.
|
|
@@ -87,8 +87,7 @@ class ComplicationDetector(BaseDetector):
|
|
|
87
87
|
|
|
88
88
|
return result
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
def detect_97_unnecessary_table_reference(self) -> list[DetectedError]:
|
|
90
|
+
def detect_84_unnecessary_table_reference(self) -> list[DetectedError]:
|
|
92
91
|
'''
|
|
93
92
|
Flags a query that joins to a table not present in the correct solution.
|
|
94
93
|
'''
|
|
@@ -118,23 +117,19 @@ class ComplicationDetector(BaseDetector):
|
|
|
118
117
|
|
|
119
118
|
return results
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
def detect_98_unused_correlation_name(self) -> list[DetectedError]:
|
|
120
|
+
def detect_85_unused_correlation_name(self) -> list[DetectedError]:
|
|
123
121
|
return []
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
def detect_99_tables_have_same_data(self) -> list[DetectedError]:
|
|
123
|
+
def detect_86_tables_have_same_data(self) -> list[DetectedError]:
|
|
127
124
|
return []
|
|
128
125
|
|
|
129
|
-
|
|
130
|
-
def detect_100_correlation_name_identical_to_table_name(self) -> list[DetectedError]:
|
|
126
|
+
def detect_125_correlation_name_identical_to_table_name(self) -> list[DetectedError]:
|
|
131
127
|
return []
|
|
132
128
|
|
|
133
|
-
|
|
134
|
-
def detect_101_unnecessary_general_comparison_operator(self) -> list[DetectedError]:
|
|
129
|
+
def detect_87_unnecessary_general_comparison_operator(self) -> list[DetectedError]:
|
|
135
130
|
return []
|
|
136
131
|
|
|
137
|
-
def
|
|
132
|
+
def detect_88_like_without_wildcards(self) -> list[DetectedError]:
|
|
138
133
|
'''
|
|
139
134
|
Flags queries where the LIKE operator is used without wildcards ('%' or '_').
|
|
140
135
|
This indicates a potential misunderstanding, where the '=' operator should
|
|
@@ -167,19 +162,16 @@ class ComplicationDetector(BaseDetector):
|
|
|
167
162
|
|
|
168
163
|
return results
|
|
169
164
|
|
|
170
|
-
|
|
171
|
-
def detect_103_unnecessarily_complicated_select_in_exists_subquery(self) -> list[DetectedError]:
|
|
165
|
+
def detect_89_unnecessarily_complicated_select_in_exists_subquery(self) -> list[DetectedError]:
|
|
172
166
|
return []
|
|
173
167
|
|
|
174
|
-
|
|
175
|
-
def detect_104_in_exists_can_be_replaced_by_comparison(self) -> list[DetectedError]:
|
|
168
|
+
def detect_90_in_exists_can_be_replaced_by_comparison(self) -> list[DetectedError]:
|
|
176
169
|
return []
|
|
177
170
|
|
|
178
|
-
|
|
179
|
-
def detect_105_unnecessary_aggregate_function(self) -> list[DetectedError]:
|
|
171
|
+
def detect_91_unnecessary_aggregate_function(self) -> list[DetectedError]:
|
|
180
172
|
return []
|
|
181
173
|
|
|
182
|
-
def
|
|
174
|
+
def detect_92_unnecessary_distinct_in_aggregate_function(self) -> list[DetectedError]:
|
|
183
175
|
'''MIN and MAX never require DISTINCT. For other aggregate functions, DISTINCT is unnecessary if the argument is unique.'''
|
|
184
176
|
|
|
185
177
|
results: list[DetectedError] = []
|
|
@@ -220,14 +212,13 @@ class ComplicationDetector(BaseDetector):
|
|
|
220
212
|
break
|
|
221
213
|
return results
|
|
222
214
|
|
|
223
|
-
def
|
|
215
|
+
def detect_93_unnecessary_argument_of_count(self) -> list[DetectedError]:
|
|
224
216
|
return []
|
|
225
217
|
|
|
226
|
-
|
|
227
|
-
def detect_108_unnecessary_group_by_in_exists_subquery(self) -> list[DetectedError]:
|
|
218
|
+
def detect_94_unnecessary_group_by_in_exists_subquery(self) -> list[DetectedError]:
|
|
228
219
|
return []
|
|
229
220
|
|
|
230
|
-
def
|
|
221
|
+
def detect_95_group_by_with_singleton_groups(self) -> list[DetectedError]:
|
|
231
222
|
'''
|
|
232
223
|
Flags GROUP BY clauses on singleton groups due to the presence
|
|
233
224
|
of UNIQUE constraints on the grouped columns.
|
|
@@ -252,11 +243,10 @@ class ComplicationDetector(BaseDetector):
|
|
|
252
243
|
|
|
253
244
|
return results
|
|
254
245
|
|
|
255
|
-
|
|
256
|
-
def detect_110_group_by_with_only_a_single_group(self) -> list[DetectedError]:
|
|
246
|
+
def detect_96_group_by_with_only_a_single_group(self) -> list[DetectedError]:
|
|
257
247
|
return []
|
|
258
248
|
|
|
259
|
-
def
|
|
249
|
+
def detect_97_group_by_can_be_replaced_by_distinct(self) -> list[DetectedError]:
|
|
260
250
|
'''
|
|
261
251
|
Flags GROUP BY clauses that can be replaced by SELECT DISTINCT.
|
|
262
252
|
This occurs when all selected columns are included in the GROUP BY clause
|
|
@@ -300,14 +290,10 @@ class ComplicationDetector(BaseDetector):
|
|
|
300
290
|
|
|
301
291
|
return results
|
|
302
292
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
# TODO: implement
|
|
306
|
-
def detect_112_union_can_be_replaced_by_or(self) -> list[DetectedError]:
|
|
293
|
+
def detect_98_union_can_be_replaced_by_or(self) -> list[DetectedError]:
|
|
307
294
|
return []
|
|
308
295
|
|
|
309
|
-
|
|
310
|
-
def detect_113_unnecessary_column_in_order_by_clause(self) -> list[DetectedError]:
|
|
296
|
+
def detect_99_unnecessary_column_in_order_by_clause(self) -> list[DetectedError]:
|
|
311
297
|
'''
|
|
312
298
|
Flags when the ORDER BY clause contains unnecessary columns in addition
|
|
313
299
|
to the required ones.
|
|
@@ -335,8 +321,7 @@ class ComplicationDetector(BaseDetector):
|
|
|
335
321
|
|
|
336
322
|
return results
|
|
337
323
|
|
|
338
|
-
|
|
339
|
-
def detect_114_order_by_in_subquery(self) -> list[DetectedError]:
|
|
324
|
+
def detect_100_order_by_in_subquery(self) -> list[DetectedError]:
|
|
340
325
|
'''
|
|
341
326
|
Flags when a subquery contains an ORDER BY clause.
|
|
342
327
|
Subqueries both ORDER BY and LIMIT are considered valid.
|
|
@@ -358,152 +343,33 @@ class ComplicationDetector(BaseDetector):
|
|
|
358
343
|
|
|
359
344
|
return results
|
|
360
345
|
|
|
361
|
-
|
|
362
|
-
def detect_115_inefficient_having(self) -> list[DetectedError]:
|
|
346
|
+
def detect_101_inefficient_having(self) -> list[DetectedError]:
|
|
363
347
|
return []
|
|
364
348
|
|
|
365
|
-
|
|
366
|
-
def detect_116_inefficient_union(self) -> list[DetectedError]:
|
|
349
|
+
def detect_102_inefficient_union(self) -> list[DetectedError]:
|
|
367
350
|
return []
|
|
368
351
|
|
|
369
|
-
|
|
370
|
-
def detect_117_condition_in_the_subquery_can_be_moved_up(self) -> list[DetectedError]:
|
|
352
|
+
def detect_103_condition_in_the_subquery_can_be_moved_up(self) -> list[DetectedError]:
|
|
371
353
|
return []
|
|
372
354
|
|
|
373
|
-
|
|
374
|
-
def detect_118_outer_join_can_be_replaced_by_inner_join(self) -> list[DetectedError]:
|
|
375
|
-
return []
|
|
376
|
-
|
|
377
|
-
# TODO: implement
|
|
378
|
-
def detect_119_unused_cte(self) -> list[DetectedError]:
|
|
355
|
+
def detect_104_outer_join_can_be_replaced_by_inner_join(self) -> list[DetectedError]:
|
|
379
356
|
return []
|
|
380
357
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
'''
|
|
384
|
-
Extracts a list of simple column names from a SELECT query's AST.
|
|
385
|
-
'''
|
|
386
|
-
columns = []
|
|
387
|
-
if not ast:
|
|
388
|
-
return columns
|
|
358
|
+
def detect_126_unused_cte(self) -> list[DetectedError]:
|
|
359
|
+
results: list[DetectedError] = []
|
|
389
360
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
for expr_node in select_expressions:
|
|
393
|
-
col_name = self._find_underlying_column(expr_node)
|
|
394
|
-
if col_name:
|
|
395
|
-
columns.append(col_name)
|
|
396
|
-
|
|
397
|
-
return columns
|
|
398
|
-
def _find_underlying_column(self, node: dict):
|
|
399
|
-
'''
|
|
400
|
-
Recursively traverses an expression node to find the underlying column identifier.
|
|
401
|
-
'''
|
|
402
|
-
if not isinstance(node, dict):
|
|
403
|
-
return None
|
|
404
|
-
|
|
405
|
-
node_class = node.get('class')
|
|
406
|
-
|
|
407
|
-
if node_class == 'Paren':
|
|
408
|
-
return self._find_underlying_column(node.get('args', {}).get('this'))
|
|
409
|
-
|
|
410
|
-
if node_class == 'Column':
|
|
411
|
-
try:
|
|
412
|
-
return node['args']['expression']['args']['this']
|
|
413
|
-
except (KeyError, TypeError):
|
|
414
|
-
try:
|
|
415
|
-
return node['args']['this']['args']['this']
|
|
416
|
-
except (KeyError, TypeError):
|
|
417
|
-
return None
|
|
418
|
-
|
|
419
|
-
if node_class == 'Alias':
|
|
420
|
-
return self._find_underlying_column(node.get('args', {}).get('this'))
|
|
421
|
-
def _get_from_tables(self, ast: dict, with_alias=False) -> list:
|
|
422
|
-
'''
|
|
423
|
-
Extracts a list of all table names from the FROM and JOIN clauses of a query's AST.
|
|
424
|
-
'''
|
|
425
|
-
tables = []
|
|
426
|
-
if not ast:
|
|
427
|
-
return tables
|
|
428
|
-
|
|
429
|
-
args = ast.get('args', {})
|
|
430
|
-
|
|
431
|
-
# 1. Process the main table from the 'from' clause
|
|
432
|
-
from_node = args.get('from')
|
|
433
|
-
if from_node:
|
|
434
|
-
# The actual table data is inside the 'this' argument of the 'From' node
|
|
435
|
-
main_table_node = from_node.get('args', {}).get('this')
|
|
436
|
-
if main_table_node:
|
|
437
|
-
self._collect_tables_recursive(main_table_node, tables, with_alias)
|
|
438
|
-
|
|
439
|
-
# 2. Process all tables from the 'joins' list
|
|
440
|
-
join_nodes = args.get('joins', [])
|
|
441
|
-
for join_node in join_nodes:
|
|
442
|
-
self._collect_tables_recursive(join_node, tables, with_alias)
|
|
443
|
-
|
|
444
|
-
return list(set(tables))
|
|
445
|
-
def _collect_tables_recursive(self, node: dict, tables: list, with_alias=False):
|
|
446
|
-
'''
|
|
447
|
-
Recursively traverses a FROM clause node (including joins) to collect table names.
|
|
448
|
-
'''
|
|
449
|
-
if not isinstance(node, dict):
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
node_class = node.get('class')
|
|
453
|
-
|
|
454
|
-
# This part handles aliased tables (e.g., "customer c") and regular tables
|
|
455
|
-
if node_class == 'Alias':
|
|
456
|
-
underlying_node = node.get('args', {}).get('this')
|
|
457
|
-
# Recurse in case the alias is on a subquery or another join
|
|
458
|
-
self._collect_tables_recursive(underlying_node, tables, with_alias)
|
|
459
|
-
|
|
460
|
-
elif node_class == 'Table':
|
|
461
|
-
try:
|
|
462
|
-
# The AST nests identifiers, so we go deep to get the name
|
|
463
|
-
table_name = node['args']['this']['args']['this']
|
|
464
|
-
alias_node = node.get('args', {}).get('alias')
|
|
465
|
-
if with_alias and alias_node:
|
|
466
|
-
alias_name = alias_node.get('args', {}).get('this', {}).get('args', {}).get('this')
|
|
467
|
-
tables.append(f"{table_name} AS {alias_name}")
|
|
468
|
-
else:
|
|
469
|
-
tables.append(table_name)
|
|
470
|
-
except (KeyError, TypeError):
|
|
471
|
-
pass
|
|
361
|
+
if not self.query.ctes:
|
|
362
|
+
return results
|
|
472
363
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if 'expression' in node.get('args', {}):
|
|
480
|
-
self._collect_tables_recursive(node.get('args', {}).get('expression'), tables, with_alias)
|
|
481
|
-
def _get_orderby_columns(self, ast: dict) -> list:
|
|
482
|
-
'''
|
|
483
|
-
Extracts a list of columns and their sort direction from an ORDER BY clause.
|
|
484
|
-
'''
|
|
485
|
-
orderby_terms = []
|
|
486
|
-
if not ast:
|
|
487
|
-
return orderby_terms
|
|
364
|
+
used_ctes: dict[int, bool] = {i: False for i in range(len(self.query.ctes))}
|
|
365
|
+
|
|
366
|
+
for select in self.query.selects:
|
|
367
|
+
for table in select.referenced_tables:
|
|
368
|
+
if table.cte_idx is not None:
|
|
369
|
+
used_ctes[table.cte_idx] = True
|
|
488
370
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
371
|
+
for cte_idx, used in used_ctes.items():
|
|
372
|
+
if not used:
|
|
373
|
+
results.append(DetectedError(SqlErrors.UNUSED_CTE, (self.query.ctes[cte_idx].sql,)))
|
|
492
374
|
|
|
493
|
-
|
|
494
|
-
for term_node in orderby_node['args']['expressions']:
|
|
495
|
-
if term_node.get('class') != 'Ordered':
|
|
496
|
-
continue
|
|
497
|
-
|
|
498
|
-
column_node = term_node.get('args', {}).get('this')
|
|
499
|
-
|
|
500
|
-
col_name = self._find_underlying_column(column_node)
|
|
501
|
-
|
|
502
|
-
if col_name:
|
|
503
|
-
direction = term_node.get('args', {}).get('direction', 'ASC').upper()
|
|
504
|
-
orderby_terms.append((col_name, direction))
|
|
505
|
-
except (KeyError, AttributeError):
|
|
506
|
-
return []
|
|
507
|
-
|
|
508
|
-
return orderby_terms
|
|
509
|
-
#endregion Utility methods
|
|
375
|
+
return results
|