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.
Files changed (74) hide show
  1. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/PKG-INFO +3 -3
  2. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/pyproject.toml +3 -3
  3. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/requirements.txt +2 -2
  4. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/complications.py +63 -197
  5. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/logical.py +194 -324
  6. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/semantic.py +92 -129
  7. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/syntax.py +62 -103
  8. sql_error_categorizer-0.3.1/tests/2_sem/test_050_constant_column_output.py +35 -0
  9. sql_error_categorizer-0.3.1/tests/2_sem/test_051_duplicate_column_output.py +58 -0
  10. sql_error_categorizer-0.3.1/tests/3_log/test_112_118_missing_extraneous_where_clause.py +74 -0
  11. sql_error_categorizer-0.3.1/tests/3_log/test_113_119_missing_extraneous_group_by_clause.py +74 -0
  12. sql_error_categorizer-0.3.1/tests/3_log/test_114_120_missing_extraneous_having_clause.py +77 -0
  13. sql_error_categorizer-0.3.1/tests/3_log/test_115_121_missing_extraneous_order_by_clause.py +79 -0
  14. sql_error_categorizer-0.3.1/tests/3_log/test_116_121_123_missing_extraneous_incorrect_limit_clause.py +80 -0
  15. sql_error_categorizer-0.3.1/tests/3_log/test_117_122_missing_extraneous_incorrect_offset_clause.py +85 -0
  16. sql_error_categorizer-0.3.1/tests/4_com/test_126_unused_cte.py +44 -0
  17. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/.gitignore +0 -0
  18. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/.readthedocs.yaml +0 -0
  19. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/LICENSE +0 -0
  20. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/Makefile +0 -0
  21. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/README.md +0 -0
  22. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/catalogs/constraints.json +0 -0
  23. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/catalogs/miedema.json +0 -0
  24. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/sql/constraints.sql +0 -0
  25. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/datasets/sql/miedema.sql +0 -0
  26. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/Makefile +0 -0
  27. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/conf.py +0 -0
  28. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/index.rst +0 -0
  29. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/make.bat +0 -0
  30. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/docs/requirements.txt +0 -0
  31. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/__init__.py +0 -0
  32. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/__init__.py +0 -0
  33. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/src/sql_error_categorizer/detectors/base.py +0 -0
  34. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/test_detector.py +0 -0
  35. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_001_ambiguous_column.py +0 -0
  36. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_003_undefined_column.py +0 -0
  37. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_004_undefined_function.py +0 -0
  38. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_005_undefined_parameter.py +0 -0
  39. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_006_undefined_object.py +0 -0
  40. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_007_invalid_schema_names.py +0 -0
  41. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_008_misspellings.py +0 -0
  42. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_012_is_where_not_applicable.py +0 -0
  43. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_013_data_type_mismatch.py +0 -0
  44. {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
  45. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_015_nested_aggregate_functions.py +0 -0
  46. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_016_extraneous_omitted_grouping_column.py +0 -0
  47. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_017_having_without_group_by.py +0 -0
  48. {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
  49. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_021_using_where_twice.py +0 -0
  50. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_022_omitted_from.py +0 -0
  51. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_024_036_additional_omitted_semicolons.py +0 -0
  52. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_024_comparison_with_null.py +0 -0
  53. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_026_duplicate_clause.py +0 -0
  54. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_029_keywords_order.py +0 -0
  55. {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
  56. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/1_syn/test_035_nonstandard_operators.py +0 -0
  57. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/2_sem/test_038_tautological_inconsistent_expressions.py +0 -0
  58. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/2_sem/test_039_distinct_sum_avg.py +0 -0
  59. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_058_join_on_incorrect_table.py +0 -0
  60. {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
  61. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_062_missing_join.py +0 -0
  62. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_067_wildcards_without_like.py +0 -0
  63. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_068_069_wrong_invalid_wildcard.py +0 -0
  64. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_070_extraneous_column_in_select.py +0 -0
  65. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_071_missing_column_from_select.py +0 -0
  66. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_072_missing_distinct_from_select.py +0 -0
  67. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/3_log/test_073_missing_as_from_select.py +0 -0
  68. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_096_unnecessary_distinct_in_select.py +0 -0
  69. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_102_like_without_wildcards.py +0 -0
  70. {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
  71. {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
  72. {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
  73. {sql_error_categorizer-0.3.0 → sql_error_categorizer-0.3.1}/tests/4_com/test_114_order_by_in_subquery.py +0 -0
  74. {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.0
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>=1.1.2
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.15
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.0"
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.15",
25
- "sql_error_taxonomy>=1.1.2",
24
+ "sqlscope>=1.0.16",
25
+ "sql_error_taxonomy>=2.0.0",
26
26
  "z3-solver",
27
27
  "python-dateutil",
28
28
  ]
@@ -4,8 +4,8 @@ sqlparse
4
4
  sqlglot==28.0.0
5
5
  z3-solver
6
6
  python-dateutil
7
- sql_error_taxonomy>=1.1.2
8
- sqlscope>=1.0.15
7
+ sql_error_taxonomy>=2.0.0
8
+ sqlscope>=1.0.16
9
9
 
10
10
  # Dependencies for development
11
11
  ipython
@@ -32,31 +32,31 @@ class ComplicationDetector(BaseDetector):
32
32
  results: list[DetectedError] = super().run()
33
33
 
34
34
  checks = [
35
- self.detect_95_unnecessary_complication,
36
- self.detect_96_unnecessary_distinct_in_select_clause,
37
- self.detect_97_unnecessary_table_reference,
38
- self.detect_98_unused_correlation_name,
39
- self.detect_99_tables_have_same_data,
40
- self.detect_100_correlation_name_identical_to_table_name,
41
- self.detect_101_unnecessary_general_comparison_operator,
42
- self.detect_102_like_without_wildcards,
43
- self.detect_103_unnecessarily_complicated_select_in_exists_subquery,
44
- self.detect_104_in_exists_can_be_replaced_by_comparison,
45
- self.detect_105_unnecessary_aggregate_function,
46
- self.detect_106_unnecessary_distinct_in_aggregate_function,
47
- self.detect_107_unnecessary_argument_of_count,
48
- self.detect_108_unnecessary_group_by_in_exists_subquery,
49
- self.detect_109_group_by_with_singleton_groups,
50
- self.detect_110_group_by_with_only_a_single_group,
51
- self.detect_111_group_by_can_be_replaced_by_distinct,
52
- self.detect_112_union_can_be_replaced_by_or,
53
- self.detect_113_unnecessary_column_in_order_by_clause,
54
- self.detect_114_order_by_in_subquery,
55
- self.detect_115_inefficient_having,
56
- self.detect_116_inefficient_union,
57
- self.detect_117_condition_in_the_subquery_can_be_moved_up,
58
- self.detect_118_outer_join_can_be_replaced_by_inner_join,
59
- self.detect_119_unused_cte,
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 detect_95_unnecessary_complication(self) -> list[DetectedError]:
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 detect_96_unnecessary_distinct_in_select_clause(self) -> list[DetectedError]:
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
- # TODO: refactor
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
- # TODO: implement
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
- # TODO: implement
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
- # TODO: implement
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
- # TODO: implement
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 detect_102_like_without_wildcards(self) -> list[DetectedError]:
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
- # TODO: implement
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
- # TODO: implement
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
- # TODO: implement
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 detect_106_unnecessary_distinct_in_aggregate_function(self) -> list[DetectedError]:
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 detect_107_unnecessary_argument_of_count(self) -> list[DetectedError]:
215
+ def detect_93_unnecessary_argument_of_count(self) -> list[DetectedError]:
224
216
  return []
225
217
 
226
- # TODO: implement
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 detect_109_group_by_with_singleton_groups(self) -> list[DetectedError]:
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
- # TODO: implement
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 detect_111_group_by_can_be_replaced_by_distinct(self) -> list[DetectedError]:
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
- # TODO: refactor
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
- # TODO: implement
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
- # TODO: implement
362
- def detect_115_inefficient_having(self) -> list[DetectedError]:
346
+ def detect_101_inefficient_having(self) -> list[DetectedError]:
363
347
  return []
364
348
 
365
- # TODO: implement
366
- def detect_116_inefficient_union(self) -> list[DetectedError]:
349
+ def detect_102_inefficient_union(self) -> list[DetectedError]:
367
350
  return []
368
351
 
369
- # TODO: implement
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
- # TODO: implement
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
- #region Utility methods
382
- def _get_select_columns(self, ast: dict) -> list:
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
- select_expressions = ast.get('args', {}).get('expressions', [])
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
- # This part handles Join nodes found in the 'joins' list
474
- elif node_class == 'Join':
475
- # The joined table is in the 'this' argument of the Join node
476
- self._collect_tables_recursive(node.get('args', {}).get('this'), tables, with_alias)
477
- # The other side of the join is already handled in the 'from' clause,
478
- # but we check for 'expression' for other potential join structures.
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
- orderby_node = ast.get('args', {}).get('order')
490
- if not orderby_node:
491
- return orderby_terms
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
- try:
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