afv-library/skills/handling-sf-data/scripts/soql_validator.py
sandipkumar-yadav 37aa84df42
feat: @W-22444026@ Introducing Core Skills, Datacloud Skills, Industries and Utility Skills. (#268)
* Migrating Core Salesforce Skills

* Updating pr comments

* updat reference

* Updating a skill

* Migrating Datacloud skills

* Migrating Industries cloud skills

* Validating - skills fixing

---------

Co-authored-by: Sandip Kumar Yadav <sandipkumar.yadav+sfemu@salesforce.com>
2026-05-14 19:32:15 +05:30

293 lines
11 KiB
Python

#!/usr/bin/env python3
"""
SOQL Validator Module
=====================
Validates SOQL query syntax and patterns.
Used by the main validation module for query-specific checks.
"""
import re
from typing import Dict, List, Any, Optional
class SOQLValidator:
"""Validates SOQL queries for best practices."""
# Common SOQL keywords
KEYWORDS = [
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE',
'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'OFFSET',
'ASC', 'DESC', 'NULLS FIRST', 'NULLS LAST',
'WITH', 'TYPEOF', 'WHEN', 'THEN', 'ELSE', 'END',
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'COUNT_DISTINCT',
'CALENDAR_MONTH', 'CALENDAR_YEAR', 'DAY_IN_WEEK', 'WEEK_IN_YEAR',
'TODAY', 'YESTERDAY', 'TOMORROW', 'THIS_WEEK', 'LAST_WEEK',
'THIS_MONTH', 'LAST_MONTH', 'THIS_QUARTER', 'LAST_QUARTER',
'THIS_YEAR', 'LAST_YEAR', 'NEXT_N_DAYS', 'LAST_N_DAYS'
]
# Indexed standard fields for selectivity
INDEXED_FIELDS = [
'Id', 'Name', 'OwnerId', 'CreatedDate', 'LastModifiedDate',
'SystemModstamp', 'RecordTypeId', 'IsDeleted'
]
def __init__(self, content: str):
self.content = content
self.issues: List[Dict[str, Any]] = []
self.recommendations: List[str] = []
def validate(self) -> Dict[str, Any]:
"""Validate the SOQL content and return results."""
result = {
'is_valid': True,
'has_where_clause': False,
'has_limit': False,
'has_order_by': False,
'has_hardcoded_ids': False,
'uses_indexed_fields': False,
'has_subquery': False,
'has_relationship': False,
'issues': [],
'recommendations': []
}
# Clean content (remove comments)
clean_content = self._remove_comments(self.content)
# Check for WHERE clause
result['has_where_clause'] = self._has_where_clause(clean_content)
# Check for LIMIT
result['has_limit'] = self._has_limit(clean_content)
# Check for ORDER BY
result['has_order_by'] = bool(re.search(r'\bORDER\s+BY\b', clean_content, re.IGNORECASE))
# Check for hardcoded IDs
result['has_hardcoded_ids'] = self._has_hardcoded_ids(clean_content)
# Check for indexed fields in WHERE
result['uses_indexed_fields'] = self._uses_indexed_fields(clean_content)
# Check for subqueries
result['has_subquery'] = self._has_subquery(clean_content)
# Check for relationship queries
result['has_relationship'] = self._has_relationship(clean_content)
# Syntax validation
syntax_issues = self._validate_syntax(clean_content)
result['issues'].extend(syntax_issues)
# Add recommendations
if not result['has_where_clause']:
result['recommendations'].append('Add WHERE clause for better query selectivity')
if not result['has_limit']:
result['recommendations'].append('Add LIMIT clause to prevent large result sets')
if result['has_hardcoded_ids']:
result['recommendations'].append('Avoid hardcoded IDs - use bind variables')
result['is_valid'] = len([i for i in result['issues'] if i.get('severity') == 'error']) == 0
self.issues = result['issues']
self.recommendations = result['recommendations']
return result
def _remove_comments(self, content: str) -> str:
"""Remove SQL comments from content."""
# Remove single-line comments
content = re.sub(r'--.*$', '', content, flags=re.MULTILINE)
# Remove multi-line comments
content = re.sub(r'/\*[\s\S]*?\*/', '', content)
return content
def _has_where_clause(self, content: str) -> bool:
"""Check if query has a WHERE clause."""
return bool(re.search(r'\bWHERE\b', content, re.IGNORECASE))
def _has_limit(self, content: str) -> bool:
"""Check if query has a LIMIT clause."""
return bool(re.search(r'\bLIMIT\s+\d+', content, re.IGNORECASE))
def _has_hardcoded_ids(self, content: str) -> bool:
"""Check for hardcoded Salesforce IDs."""
# Salesforce IDs are 15 or 18 character alphanumeric
# Starting with specific prefixes: 001 (Account), 003 (Contact), etc.
id_pattern = r"'[a-zA-Z0-9]{15}'" # 15-char ID
id_pattern_18 = r"'[a-zA-Z0-9]{18}'" # 18-char ID
return bool(re.search(id_pattern, content) or re.search(id_pattern_18, content))
def _uses_indexed_fields(self, content: str) -> bool:
"""Check if WHERE clause uses indexed fields."""
# Extract WHERE clause
where_match = re.search(r'\bWHERE\b(.*?)(?:\bORDER\b|\bGROUP\b|\bLIMIT\b|$)',
content, re.IGNORECASE | re.DOTALL)
if not where_match:
return False
where_clause = where_match.group(1)
# Check for indexed fields
for field in self.INDEXED_FIELDS:
if re.search(rf'\b{field}\b', where_clause, re.IGNORECASE):
return True
return False
def _has_subquery(self, content: str) -> bool:
"""Check if query has subqueries."""
# Look for nested SELECT in parentheses
return bool(re.search(r'\(\s*SELECT\b', content, re.IGNORECASE))
def _has_relationship(self, content: str) -> bool:
"""Check if query uses relationship queries."""
# Look for dot notation (child-to-parent) or __r suffix
return bool(re.search(r'\w+\.\w+|__r\.', content))
def _validate_syntax(self, content: str) -> List[Dict[str, Any]]:
"""Validate SOQL syntax and return issues."""
issues = []
# Check for SELECT without FROM
if re.search(r'\bSELECT\b', content, re.IGNORECASE):
if not re.search(r'\bFROM\b', content, re.IGNORECASE):
issues.append({
'severity': 'error',
'message': 'SELECT statement missing FROM clause'
})
# Check for invalid operators
if re.search(r'==', content):
issues.append({
'severity': 'error',
'message': 'Invalid operator "==" - use "=" in SOQL'
})
if re.search(r'!=\s*null', content, re.IGNORECASE):
pass # Valid
elif re.search(r'<>', content):
issues.append({
'severity': 'warning',
'message': 'Consider using "!=" instead of "<>" for consistency'
})
# Check for SELECT *
if re.search(r'\bSELECT\s+\*', content, re.IGNORECASE):
issues.append({
'severity': 'error',
'message': 'SELECT * is not valid in SOQL - specify field names'
})
# Check for proper string quoting
if re.search(r"=\s*\"[^\"]*\"", content):
issues.append({
'severity': 'warning',
'message': 'Use single quotes for string literals in SOQL'
})
# Check for unbalanced parentheses
open_parens = content.count('(')
close_parens = content.count(')')
if open_parens != close_parens:
issues.append({
'severity': 'error',
'message': f'Unbalanced parentheses: {open_parens} open, {close_parens} close'
})
# Check for TYPEOF without END
if re.search(r'\bTYPEOF\b', content, re.IGNORECASE):
if not re.search(r'\bEND\b', content, re.IGNORECASE):
issues.append({
'severity': 'error',
'message': 'TYPEOF expression missing END keyword'
})
# Check for reserved words as field names (common issues)
reserved = ['SELECT', 'FROM', 'WHERE', 'ORDER', 'GROUP', 'LIMIT']
for word in reserved:
# Look for patterns like "SELECT SELECT" or "field, SELECT"
if re.search(rf'\b{word}\s*,|\,\s*{word}\b', content, re.IGNORECASE):
issues.append({
'severity': 'warning',
'message': f'Possible misuse of reserved word "{word}"'
})
return issues
def get_query_complexity(self, content: str) -> Dict[str, int]:
"""Analyze query complexity metrics."""
clean = self._remove_comments(content)
return {
'select_fields': len(re.findall(r'\bSELECT\b.*?\bFROM\b', clean, re.IGNORECASE | re.DOTALL)),
'where_conditions': len(re.findall(r'\b(AND|OR)\b', clean, re.IGNORECASE)) + 1 if self._has_where_clause(clean) else 0,
'subqueries': len(re.findall(r'\(\s*SELECT\b', clean, re.IGNORECASE)),
'joins': len(re.findall(r'__r\.|\.Name|\.Id', clean)),
'aggregates': len(re.findall(r'\b(COUNT|SUM|AVG|MIN|MAX)\s*\(', clean, re.IGNORECASE))
}
def suggest_optimizations(self, content: str) -> List[str]:
"""Suggest query optimizations."""
suggestions = []
clean = self._remove_comments(content)
# Check for missing indexed field in WHERE
if self._has_where_clause(clean) and not self._uses_indexed_fields(clean):
suggestions.append('Add an indexed field (Id, Name, CreatedDate) to WHERE for better performance')
# Check for ORDER BY without LIMIT
if re.search(r'\bORDER\s+BY\b', clean, re.IGNORECASE):
if not self._has_limit(clean):
suggestions.append('Consider adding LIMIT when using ORDER BY')
# Check for SELECT with many fields
select_match = re.search(r'\bSELECT\b(.*?)\bFROM\b', clean, re.IGNORECASE | re.DOTALL)
if select_match:
fields = select_match.group(1)
field_count = len([f.strip() for f in fields.split(',') if f.strip()])
if field_count > 20:
suggestions.append(f'Query selects {field_count} fields - consider selecting only needed fields')
# Check for deeply nested subqueries
subquery_depth = len(re.findall(r'\(\s*SELECT\b', clean, re.IGNORECASE))
if subquery_depth > 2:
suggestions.append('Consider simplifying query - deeply nested subqueries may impact performance')
return suggestions
# Standalone execution for testing
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
print('Usage: python soql_validator.py <soql_file>')
sys.exit(1)
with open(sys.argv[1], 'r') as f:
content = f.read()
validator = SOQLValidator(content)
result = validator.validate()
print('SOQL Validation Results:')
print('=' * 40)
for key, value in result.items():
if key not in ['issues', 'recommendations']:
print(f'{key}: {value}')
if result['issues']:
print('\nIssues:')
for issue in result['issues']:
print(f" [{issue['severity']}] {issue['message']}")
if result['recommendations']:
print('\nRecommendations:')
for rec in result['recommendations']:
print(f' - {rec}')