sqlspec 0.12.1__py3-none-any.whl → 0.13.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (113) hide show
  1. sqlspec/_sql.py +21 -180
  2. sqlspec/adapters/adbc/config.py +10 -12
  3. sqlspec/adapters/adbc/driver.py +120 -118
  4. sqlspec/adapters/aiosqlite/config.py +3 -3
  5. sqlspec/adapters/aiosqlite/driver.py +116 -141
  6. sqlspec/adapters/asyncmy/config.py +3 -4
  7. sqlspec/adapters/asyncmy/driver.py +123 -135
  8. sqlspec/adapters/asyncpg/config.py +3 -7
  9. sqlspec/adapters/asyncpg/driver.py +98 -140
  10. sqlspec/adapters/bigquery/config.py +4 -5
  11. sqlspec/adapters/bigquery/driver.py +231 -181
  12. sqlspec/adapters/duckdb/config.py +3 -6
  13. sqlspec/adapters/duckdb/driver.py +132 -124
  14. sqlspec/adapters/oracledb/config.py +6 -5
  15. sqlspec/adapters/oracledb/driver.py +242 -259
  16. sqlspec/adapters/psqlpy/config.py +3 -7
  17. sqlspec/adapters/psqlpy/driver.py +118 -93
  18. sqlspec/adapters/psycopg/config.py +34 -30
  19. sqlspec/adapters/psycopg/driver.py +342 -214
  20. sqlspec/adapters/sqlite/config.py +3 -3
  21. sqlspec/adapters/sqlite/driver.py +150 -104
  22. sqlspec/config.py +0 -4
  23. sqlspec/driver/_async.py +89 -98
  24. sqlspec/driver/_common.py +52 -17
  25. sqlspec/driver/_sync.py +81 -105
  26. sqlspec/driver/connection.py +207 -0
  27. sqlspec/driver/mixins/_csv_writer.py +91 -0
  28. sqlspec/driver/mixins/_pipeline.py +38 -49
  29. sqlspec/driver/mixins/_result_utils.py +27 -9
  30. sqlspec/driver/mixins/_storage.py +149 -216
  31. sqlspec/driver/mixins/_type_coercion.py +3 -4
  32. sqlspec/driver/parameters.py +138 -0
  33. sqlspec/exceptions.py +10 -2
  34. sqlspec/extensions/aiosql/adapter.py +0 -10
  35. sqlspec/extensions/litestar/handlers.py +0 -1
  36. sqlspec/extensions/litestar/plugin.py +0 -3
  37. sqlspec/extensions/litestar/providers.py +0 -14
  38. sqlspec/loader.py +31 -118
  39. sqlspec/protocols.py +542 -0
  40. sqlspec/service/__init__.py +3 -2
  41. sqlspec/service/_util.py +147 -0
  42. sqlspec/service/base.py +1116 -9
  43. sqlspec/statement/builder/__init__.py +42 -32
  44. sqlspec/statement/builder/_ddl_utils.py +0 -10
  45. sqlspec/statement/builder/_parsing_utils.py +10 -4
  46. sqlspec/statement/builder/base.py +70 -23
  47. sqlspec/statement/builder/column.py +283 -0
  48. sqlspec/statement/builder/ddl.py +102 -65
  49. sqlspec/statement/builder/delete.py +23 -7
  50. sqlspec/statement/builder/insert.py +29 -15
  51. sqlspec/statement/builder/merge.py +4 -4
  52. sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
  53. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
  54. sqlspec/statement/builder/mixins/_delete_from.py +1 -1
  55. sqlspec/statement/builder/mixins/_from.py +10 -8
  56. sqlspec/statement/builder/mixins/_group_by.py +0 -1
  57. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
  58. sqlspec/statement/builder/mixins/_insert_values.py +0 -2
  59. sqlspec/statement/builder/mixins/_join.py +20 -13
  60. sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
  61. sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
  62. sqlspec/statement/builder/mixins/_order_by.py +2 -2
  63. sqlspec/statement/builder/mixins/_pivot.py +4 -7
  64. sqlspec/statement/builder/mixins/_select_columns.py +6 -5
  65. sqlspec/statement/builder/mixins/_unpivot.py +6 -9
  66. sqlspec/statement/builder/mixins/_update_from.py +2 -1
  67. sqlspec/statement/builder/mixins/_update_set.py +11 -8
  68. sqlspec/statement/builder/mixins/_where.py +61 -34
  69. sqlspec/statement/builder/select.py +32 -17
  70. sqlspec/statement/builder/update.py +25 -11
  71. sqlspec/statement/filters.py +39 -14
  72. sqlspec/statement/parameter_manager.py +220 -0
  73. sqlspec/statement/parameters.py +210 -79
  74. sqlspec/statement/pipelines/__init__.py +166 -23
  75. sqlspec/statement/pipelines/analyzers/_analyzer.py +22 -25
  76. sqlspec/statement/pipelines/context.py +35 -39
  77. sqlspec/statement/pipelines/transformers/__init__.py +2 -3
  78. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
  79. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +667 -43
  80. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
  81. sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
  82. sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
  83. sqlspec/statement/pipelines/validators/_performance.py +38 -23
  84. sqlspec/statement/pipelines/validators/_security.py +39 -62
  85. sqlspec/statement/result.py +37 -129
  86. sqlspec/statement/splitter.py +0 -12
  87. sqlspec/statement/sql.py +885 -379
  88. sqlspec/statement/sql_compiler.py +140 -0
  89. sqlspec/storage/__init__.py +10 -2
  90. sqlspec/storage/backends/fsspec.py +82 -35
  91. sqlspec/storage/backends/obstore.py +66 -49
  92. sqlspec/storage/capabilities.py +101 -0
  93. sqlspec/storage/registry.py +56 -83
  94. sqlspec/typing.py +6 -434
  95. sqlspec/utils/cached_property.py +25 -0
  96. sqlspec/utils/correlation.py +0 -2
  97. sqlspec/utils/logging.py +0 -6
  98. sqlspec/utils/sync_tools.py +0 -4
  99. sqlspec/utils/text.py +0 -5
  100. sqlspec/utils/type_guards.py +892 -0
  101. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
  102. sqlspec-0.13.0.dist-info/RECORD +150 -0
  103. sqlspec/statement/builder/protocols.py +0 -20
  104. sqlspec/statement/pipelines/base.py +0 -315
  105. sqlspec/statement/pipelines/result_types.py +0 -41
  106. sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
  107. sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
  108. sqlspec/statement/pipelines/validators/base.py +0 -67
  109. sqlspec/storage/protocol.py +0 -170
  110. sqlspec-0.12.1.dist-info/RECORD +0 -145
  111. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
  112. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
  113. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
@@ -11,8 +11,9 @@ from sqlglot import exp
11
11
  from sqlglot.expressions import EQ, Binary, Func, Literal, Or, Subquery, Union
12
12
 
13
13
  from sqlspec.exceptions import RiskLevel
14
- from sqlspec.statement.pipelines.base import ProcessorProtocol
15
- from sqlspec.statement.pipelines.result_types import ValidationError
14
+ from sqlspec.protocols import ProcessorProtocol
15
+ from sqlspec.statement.pipelines.context import ValidationError
16
+ from sqlspec.utils.type_guards import has_expressions, has_sql_method
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from sqlspec.statement.pipelines.context import SQLProcessingContext
@@ -178,10 +179,26 @@ class SecurityValidator(ProcessorProtocol):
178
179
  with contextlib.suppress(re.error):
179
180
  self._compiled_patterns[f"custom_suspicious_{i}"] = re.compile(pattern, re.IGNORECASE)
180
181
 
181
- def process(self, expression: Optional[exp.Expression], context: "SQLProcessingContext") -> None:
182
+ def add_error(
183
+ self,
184
+ context: "SQLProcessingContext",
185
+ message: str,
186
+ code: str,
187
+ risk_level: RiskLevel,
188
+ expression: "Optional[exp.Expression]" = None,
189
+ ) -> None:
190
+ """Add a validation error to the context."""
191
+ error = ValidationError(
192
+ message=message, code=code, risk_level=risk_level, processor=self.__class__.__name__, expression=expression
193
+ )
194
+ context.validation_errors.append(error)
195
+
196
+ def process(
197
+ self, expression: Optional[exp.Expression], context: "SQLProcessingContext"
198
+ ) -> Optional[exp.Expression]:
182
199
  """Process the SQL expression and detect security issues in a single pass."""
183
200
  if not context.current_expression:
184
- return
201
+ return None
185
202
 
186
203
  security_issues: list[SecurityIssue] = []
187
204
  visited_nodes: set[int] = set()
@@ -198,17 +215,14 @@ class SecurityValidator(ProcessorProtocol):
198
215
  if isinstance(node, (Subquery, exp.Select)):
199
216
  nesting_depth += 1
200
217
 
201
- # Check injection patterns (enhanced AST-based)
202
218
  if self.config.check_injection:
203
219
  injection_issues = self._check_injection_patterns(node, context)
204
220
  security_issues.extend(injection_issues)
205
221
 
206
- # Check tautology conditions (enhanced)
207
222
  if self.config.check_tautology:
208
223
  tautology_issues = self._check_tautology_patterns(node, context)
209
224
  security_issues.extend(tautology_issues)
210
225
 
211
- # Check suspicious keywords/functions
212
226
  if self.config.check_keywords:
213
227
  keyword_issues = self._check_suspicious_keywords(node, context)
214
228
  security_issues.extend(keyword_issues)
@@ -223,7 +237,6 @@ class SecurityValidator(ProcessorProtocol):
223
237
  structural_issues = self._check_structural_attacks(node, context)
224
238
  security_issues.extend(structural_issues)
225
239
 
226
- # Check combined attack patterns
227
240
  if self.config.check_combined_patterns and security_issues:
228
241
  combined_issues = self._check_combined_patterns(context.current_expression, security_issues)
229
242
  security_issues.extend(combined_issues)
@@ -242,11 +255,9 @@ class SecurityValidator(ProcessorProtocol):
242
255
  )
243
256
  )
244
257
 
245
- # Determine overall risk level
246
258
  if security_issues:
247
259
  max(issue.risk_level for issue in security_issues)
248
260
 
249
- # Create validation errors
250
261
  for issue in security_issues:
251
262
  error = ValidationError(
252
263
  message=issue.description,
@@ -278,9 +289,7 @@ class SecurityValidator(ProcessorProtocol):
278
289
  issue for issue in security_issues if issue.confidence >= self.config.min_confidence_threshold
279
290
  ]
280
291
 
281
- # Update validation result with filtered issues
282
292
  if filtered_issues != security_issues:
283
- # Clear previous errors and add filtered ones
284
293
  context.validation_errors = []
285
294
  for issue in filtered_issues:
286
295
  error = ValidationError(
@@ -292,7 +301,6 @@ class SecurityValidator(ProcessorProtocol):
292
301
  )
293
302
  context.validation_errors.append(error)
294
303
 
295
- # Update metadata with filtered issues
296
304
  context.metadata["security_validator"] = {
297
305
  "security_issues": filtered_issues,
298
306
  "total_issues_found": len(security_issues),
@@ -312,13 +320,14 @@ class SecurityValidator(ProcessorProtocol):
312
320
  },
313
321
  }
314
322
 
323
+ return expression
324
+
315
325
  def _check_injection_patterns(
316
326
  self, node: "exp.Expression", context: "SQLProcessingContext"
317
327
  ) -> "list[SecurityIssue]":
318
328
  """Check for SQL injection patterns in the node."""
319
329
  issues: list[SecurityIssue] = []
320
330
 
321
- # Check UNION-based injection
322
331
  if isinstance(node, exp.Union):
323
332
  union_issues = self._check_union_injection(node, context)
324
333
  issues.extend(union_issues)
@@ -336,7 +345,6 @@ class SecurityValidator(ProcessorProtocol):
336
345
  )
337
346
  )
338
347
 
339
- # Check for encoded characters
340
348
  if PATTERNS["encoded_chars"].search(sql_text) or PATTERNS["hex_encoding"].search(sql_text):
341
349
  issues.append(
342
350
  SecurityIssue(
@@ -349,7 +357,6 @@ class SecurityValidator(ProcessorProtocol):
349
357
  )
350
358
  )
351
359
 
352
- # Check for system schema access
353
360
  if isinstance(node, exp.Table):
354
361
  system_access = self._check_system_schema_access(node)
355
362
  if system_access:
@@ -391,8 +398,7 @@ class SecurityValidator(ProcessorProtocol):
391
398
  )
392
399
  )
393
400
 
394
- # Check for NULL padding in UNION SELECT
395
- if hasattr(union_node, "right") and isinstance(union_node.right, exp.Select):
401
+ if isinstance(union_node, exp.Union) and isinstance(union_node.right, exp.Select):
396
402
  select_expr = union_node.right
397
403
  if select_expr.expressions:
398
404
  null_count = sum(1 for expr in select_expr.expressions if isinstance(expr, exp.Null))
@@ -417,7 +423,6 @@ class SecurityValidator(ProcessorProtocol):
417
423
  schema_name = table_node.db.lower() if table_node.db else ""
418
424
  table_node.catalog.lower() if table_node.catalog else ""
419
425
 
420
- # Check if schema is in allowed list
421
426
  if schema_name in self.config.allowed_system_schemas:
422
427
  return None
423
428
 
@@ -442,7 +447,6 @@ class SecurityValidator(ProcessorProtocol):
442
447
  """Check for tautology conditions that are always true."""
443
448
  issues: list[SecurityIssue] = []
444
449
 
445
- # Check for boolean literals in WHERE conditions
446
450
  if isinstance(node, exp.Boolean) and node.this is True:
447
451
  issues.append(
448
452
  SecurityIssue(
@@ -455,7 +459,6 @@ class SecurityValidator(ProcessorProtocol):
455
459
  )
456
460
  )
457
461
 
458
- # Check for tautological conditions
459
462
  if isinstance(node, (exp.EQ, exp.NEQ, exp.GT, exp.LT, exp.GTE, exp.LTE)) and self._is_tautology(node):
460
463
  issues.append(
461
464
  SecurityIssue(
@@ -468,7 +471,6 @@ class SecurityValidator(ProcessorProtocol):
468
471
  )
469
472
  )
470
473
 
471
- # Check for OR 1=1 patterns
472
474
  if isinstance(node, exp.Or):
473
475
  or_sql = node.sql()
474
476
  if PATTERNS["or_patterns"].search(or_sql) or PATTERNS["always_true"].search(or_sql):
@@ -494,14 +496,12 @@ class SecurityValidator(ProcessorProtocol):
494
496
  left = comparison.this
495
497
  right = comparison.expression
496
498
 
497
- # Check if comparing identical expressions
498
499
  if self._expressions_identical(left, right):
499
500
  if isinstance(comparison, (exp.EQ, exp.GTE, exp.LTE)):
500
501
  return True
501
502
  if isinstance(comparison, (exp.NEQ, exp.GT, exp.LT)):
502
503
  return False
503
504
 
504
- # Check for literal comparisons
505
505
  if isinstance(left, exp.Literal) and isinstance(right, exp.Literal):
506
506
  try:
507
507
  left_val = left.this
@@ -511,7 +511,6 @@ class SecurityValidator(ProcessorProtocol):
511
511
  return bool(left_val == right_val)
512
512
  if isinstance(comparison, exp.NEQ):
513
513
  return bool(left_val != right_val)
514
- # Add more comparison logic as needed
515
514
  except Exception:
516
515
  # Value extraction failed, can't evaluate the condition
517
516
  logger.debug("Failed to extract values for comparison evaluation")
@@ -539,11 +538,9 @@ class SecurityValidator(ProcessorProtocol):
539
538
  """Check for suspicious functions and keywords."""
540
539
  issues: list[SecurityIssue] = []
541
540
 
542
- # Check function calls
543
541
  if isinstance(node, exp.Func):
544
542
  func_name = node.name.lower() if node.name else ""
545
543
 
546
- # Check if function is explicitly blocked
547
544
  if func_name in self.config.blocked_functions:
548
545
  issues.append(
549
546
  SecurityIssue(
@@ -555,7 +552,6 @@ class SecurityValidator(ProcessorProtocol):
555
552
  recommendation=f"Function {func_name} is not allowed",
556
553
  )
557
554
  )
558
- # Check if function is suspicious but not explicitly allowed
559
555
  elif func_name in SUSPICIOUS_FUNCTIONS and func_name not in self.config.allowed_functions:
560
556
  issues.append(
561
557
  SecurityIssue(
@@ -569,7 +565,6 @@ class SecurityValidator(ProcessorProtocol):
569
565
  )
570
566
  )
571
567
 
572
- # Special handling for Command nodes (e.g., EXECUTE statements)
573
568
  if isinstance(node, exp.Command):
574
569
  # Commands are often used for dynamic SQL execution
575
570
  command_text = str(node)
@@ -587,8 +582,7 @@ class SecurityValidator(ProcessorProtocol):
587
582
  )
588
583
  )
589
584
 
590
- # Check for specific patterns in SQL text
591
- if hasattr(node, "sql"):
585
+ if has_sql_method(node):
592
586
  sql_text = node.sql()
593
587
 
594
588
  # File operations
@@ -725,14 +719,13 @@ class SecurityValidator(ProcessorProtocol):
725
719
  """
726
720
  issues: list[SecurityIssue] = []
727
721
 
728
- # Check for excessive nesting (potential injection)
729
722
  if nesting_depth > self.config.max_nesting_depth:
730
723
  issues.append(
731
724
  SecurityIssue(
732
725
  issue_type=SecurityIssueType.AST_ANOMALY,
733
726
  risk_level=self.config.ast_anomaly_risk_level,
734
727
  description=f"Excessive query nesting detected (depth: {nesting_depth})",
735
- location=node.sql()[:100] if hasattr(node, "sql") else str(node)[:100],
728
+ location=node.sql()[:100] if has_sql_method(node) else str(node)[:100],
736
729
  pattern_matched="excessive_nesting",
737
730
  recommendation="Review query structure for potential injection",
738
731
  ast_node_type=type(node).__name__,
@@ -741,7 +734,6 @@ class SecurityValidator(ProcessorProtocol):
741
734
  )
742
735
  )
743
736
 
744
- # Check for suspiciously long literals (potential injection payload)
745
737
  if isinstance(node, Literal) and isinstance(node.this, str):
746
738
  literal_length = len(str(node.this))
747
739
  if literal_length > self.config.max_literal_length:
@@ -759,12 +751,10 @@ class SecurityValidator(ProcessorProtocol):
759
751
  )
760
752
  )
761
753
 
762
- # Check for unusual function call patterns
763
754
  if isinstance(node, Func):
764
755
  func_issues = self._analyze_function_anomalies(node)
765
756
  issues.extend(func_issues)
766
757
 
767
- # Check for suspicious binary operations (potential injection)
768
758
  if isinstance(node, Binary):
769
759
  binary_issues = self._analyze_binary_anomalies(node)
770
760
  issues.extend(binary_issues)
@@ -777,17 +767,14 @@ class SecurityValidator(ProcessorProtocol):
777
767
  """Check for structural attack patterns using AST analysis."""
778
768
  issues: list[SecurityIssue] = []
779
769
 
780
- # Check for UNION-based injection using AST structure
781
770
  if isinstance(node, Union):
782
771
  union_issues = self._analyze_union_structure(node)
783
772
  issues.extend(union_issues)
784
773
 
785
- # Check for subquery injection patterns
786
774
  if isinstance(node, Subquery):
787
775
  subquery_issues = self._analyze_subquery_structure(node)
788
776
  issues.extend(subquery_issues)
789
777
 
790
- # Check for OR-based injection using AST structure
791
778
  if isinstance(node, Or):
792
779
  or_issues = self._analyze_or_structure(node)
793
780
  issues.extend(or_issues)
@@ -804,8 +791,7 @@ class SecurityValidator(ProcessorProtocol):
804
791
 
805
792
  func_name = func_node.name.lower()
806
793
 
807
- # Check for chained function calls (potential evasion)
808
- if hasattr(func_node, "this") and isinstance(func_node.this, Func):
794
+ if func_node.this and isinstance(func_node.this, Func):
809
795
  nested_func = func_node.this
810
796
  if nested_func.name and nested_func.name.lower() in SUSPICIOUS_FUNCTIONS:
811
797
  issues.append(
@@ -813,7 +799,7 @@ class SecurityValidator(ProcessorProtocol):
813
799
  issue_type=SecurityIssueType.AST_ANOMALY,
814
800
  risk_level=RiskLevel.MEDIUM,
815
801
  description=f"Nested suspicious function call: {nested_func.name.lower()} inside {func_name}",
816
- location=func_node.sql()[:100],
802
+ location=func_node.sql()[:100] if has_sql_method(func_node) else str(func_node)[:100],
817
803
  pattern_matched="nested_suspicious_function",
818
804
  recommendation="Review nested function calls for evasion attempts",
819
805
  ast_node_type="Func",
@@ -822,8 +808,7 @@ class SecurityValidator(ProcessorProtocol):
822
808
  )
823
809
  )
824
810
 
825
- # Check for unusual argument patterns
826
- if hasattr(func_node, "expressions") and func_node.expressions:
811
+ if has_expressions(func_node) and func_node.expressions:
827
812
  arg_count = len(func_node.expressions)
828
813
  if func_name in {"concat", "concat_ws"} and arg_count > MAX_FUNCTION_ARGS:
829
814
  issues.append(
@@ -831,7 +816,7 @@ class SecurityValidator(ProcessorProtocol):
831
816
  issue_type=SecurityIssueType.AST_ANOMALY,
832
817
  risk_level=RiskLevel.MEDIUM,
833
818
  description=f"Excessive arguments to {func_name} function ({arg_count} args)",
834
- location=func_node.sql()[:100],
819
+ location=func_node.sql()[:100] if has_sql_method(func_node) else str(func_node)[:100],
835
820
  pattern_matched="excessive_function_args",
836
821
  recommendation="Review function arguments for potential injection",
837
822
  ast_node_type="Func",
@@ -869,8 +854,7 @@ class SecurityValidator(ProcessorProtocol):
869
854
  """Analyze UNION structure for injection patterns."""
870
855
  issues: list[SecurityIssue] = []
871
856
 
872
- # Check if UNION has mismatched column counts (classic injection)
873
- if hasattr(union_node, "left") and hasattr(union_node, "right"):
857
+ if isinstance(union_node, exp.Union):
874
858
  left_cols = self._count_select_columns(union_node.left)
875
859
  right_cols = self._count_select_columns(union_node.right)
876
860
 
@@ -896,12 +880,10 @@ class SecurityValidator(ProcessorProtocol):
896
880
  """Analyze subquery structure for injection patterns."""
897
881
  issues: list[SecurityIssue] = []
898
882
 
899
- # Check for subqueries that return unusual patterns
900
- if hasattr(subquery_node, "this") and isinstance(subquery_node.this, exp.Select):
883
+ if subquery_node.this and isinstance(subquery_node.this, exp.Select):
901
884
  select_expr = subquery_node.this
902
885
 
903
- # Check if subquery selects only literals (potential injection)
904
- if hasattr(select_expr, "expressions") and select_expr.expressions:
886
+ if has_expressions(select_expr) and select_expr.expressions:
905
887
  literal_count = sum(1 for expr in select_expr.expressions if isinstance(expr, Literal))
906
888
  total_expressions = len(select_expr.expressions)
907
889
 
@@ -926,11 +908,8 @@ class SecurityValidator(ProcessorProtocol):
926
908
  """Analyze OR conditions for tautology patterns."""
927
909
  issues: list[SecurityIssue] = []
928
910
 
929
- # Check for OR with tautological conditions using AST
930
- if (
931
- hasattr(or_node, "left")
932
- and hasattr(or_node, "right")
933
- and (self._is_always_true_condition(or_node.left) or self._is_always_true_condition(or_node.right))
911
+ if isinstance(or_node, exp.Binary) and (
912
+ self._is_always_true_condition(or_node.left) or self._is_always_true_condition(or_node.right)
934
913
  ):
935
914
  issues.append(
936
915
  SecurityIssue(
@@ -955,10 +934,10 @@ class SecurityValidator(ProcessorProtocol):
955
934
  """Calculate the depth of nested binary operations."""
956
935
  max_depth = depth
957
936
 
958
- if hasattr(node, "left") and isinstance(node.left, Binary):
937
+ if isinstance(node, exp.Binary) and isinstance(node.left, Binary):
959
938
  max_depth = max(max_depth, self._calculate_binary_depth(node.left, depth + 1))
960
939
 
961
- if hasattr(node, "right") and isinstance(node.right, Binary):
940
+ if isinstance(node, exp.Binary) and isinstance(node.right, Binary):
962
941
  max_depth = max(max_depth, self._calculate_binary_depth(node.right, depth + 1))
963
942
 
964
943
  return max_depth
@@ -966,22 +945,20 @@ class SecurityValidator(ProcessorProtocol):
966
945
  @staticmethod
967
946
  def _count_select_columns(node: "exp.Expression") -> int:
968
947
  """Count the number of columns in a SELECT statement."""
969
- if isinstance(node, exp.Select) and hasattr(node, "expressions"):
948
+ if isinstance(node, exp.Select) and has_expressions(node):
970
949
  return len(node.expressions) if node.expressions else 0
971
950
  return 0
972
951
 
973
952
  @staticmethod
974
953
  def _is_always_true_condition(node: "exp.Expression") -> bool:
975
954
  """Check if a condition is always true using AST analysis."""
976
- # Check for literal true
977
955
  if isinstance(node, Literal) and str(node.this).upper() in {"TRUE", "1"}:
978
956
  return True
979
957
 
980
958
  # Check for 1=1 or similar tautologies
981
959
  return bool(
982
960
  isinstance(node, EQ)
983
- and hasattr(node, "left")
984
- and hasattr(node, "right")
961
+ and isinstance(node, exp.Binary)
985
962
  and (
986
963
  isinstance(node.left, Literal)
987
964
  and isinstance(node.right, Literal)