351 lines
16 KiB
Python
351 lines
16 KiB
Python
"""
|
||
Cypher 验证模块
|
||
|
||
功能:验证 Cypher 语句的语法、Schema 和语义正确性
|
||
"""
|
||
|
||
import re
|
||
import json
|
||
import logging
|
||
from typing import Optional, List, Dict, Tuple, Any, Set
|
||
from neo4j.exceptions import CypherSyntaxError
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class CypherValidator:
|
||
"""Cypher 验证器类"""
|
||
|
||
def __init__(
|
||
self,
|
||
driver,
|
||
neo4j_schema: str,
|
||
allowed_node_types: Optional[List[str]] = None,
|
||
allowed_relationship_types: Optional[List[str]] = None
|
||
):
|
||
"""
|
||
初始化验证器
|
||
|
||
Args:
|
||
driver: Neo4j driver 实例
|
||
neo4j_schema: Neo4j schema 字符串
|
||
allowed_node_types: 允许的节点类型列表(从graph_service.py传递)
|
||
allowed_relationship_types: 允许的关系类型列表(从graph_service.py传递)
|
||
"""
|
||
self.driver = driver
|
||
self.neo4j_schema = neo4j_schema
|
||
# 使用传入的节点类型和关系类型,如果没有传入则使用空集合(不进行验证)
|
||
self.allowed_node_types = set(allowed_node_types) if allowed_node_types else set()
|
||
self.allowed_relationship_types = set(allowed_relationship_types) if allowed_relationship_types else set()
|
||
|
||
def validate_syntax(self, cypher: str) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
第一层验证:语法验证(快速检查)
|
||
"""
|
||
if not cypher or not cypher.strip():
|
||
return False, "Cypher语句为空"
|
||
|
||
cypher_upper = cypher.upper()
|
||
|
||
# ========== 必须包含的关键字检查 ==========
|
||
if "MATCH" not in cypher_upper or "RETURN" not in cypher_upper:
|
||
return False, "缺少必要的Cypher关键字(MATCH/RETURN)"
|
||
|
||
# ========== 危险操作检查(防止注入) ==========
|
||
dangerous = ["DELETE", "DROP", "REMOVE", "SET", "CREATE", "MERGE"]
|
||
for kw in dangerous:
|
||
# 使用正则表达式匹配完整的单词,避免匹配到属性名中的子串
|
||
# 例如:避免将 created_at 中的 CREATE 误判为危险操作
|
||
pattern = r'\b' + re.escape(kw) + r'\b'
|
||
matches = list(re.finditer(pattern, cypher_upper))
|
||
if matches:
|
||
return_pos = cypher_upper.find("RETURN")
|
||
# 检查是否有危险关键字在RETURN之前(作为独立单词)
|
||
for match in matches:
|
||
kw_pos = match.start()
|
||
if return_pos == -1 or kw_pos < return_pos:
|
||
return False, f"检测到危险操作: {kw}"
|
||
|
||
# ========== 括号匹配检查(使用栈方法,力扣经典题) ==========
|
||
# 使用栈来检查括号是否匹配
|
||
bracket_stack = [] # 栈:存储 (位置, 括号类型, 上下文信息)
|
||
bracket_pairs = {')': '(', ']': '[', '}': '{'}
|
||
|
||
i = 0
|
||
while i < len(cypher):
|
||
char = cypher[i]
|
||
|
||
# 遇到左括号,入栈
|
||
if char in ['(', '[', '{']:
|
||
# 获取括号前的上下文(用于判断是否合法)
|
||
before_start = max(0, i - 30)
|
||
before_context = cypher[before_start:i]
|
||
bracket_stack.append((i, char, before_context))
|
||
|
||
# 遇到右括号,检查是否匹配
|
||
elif char in [')', ']', '}']:
|
||
if not bracket_stack:
|
||
return False, f"括号不匹配:位置 {i} 处有多余的右括号 '{char}'"
|
||
|
||
last_pos, last_char, last_context = bracket_stack.pop()
|
||
expected_left = bracket_pairs[char]
|
||
|
||
if last_char != expected_left:
|
||
return False, f"括号类型不匹配:位置 {last_pos} 的 '{last_char}' 与位置 {i} 的 '{char}' 不匹配"
|
||
|
||
i += 1
|
||
|
||
# 检查栈是否为空(所有括号是否都匹配)
|
||
if bracket_stack:
|
||
unmatched_pos, unmatched_char, _ = bracket_stack[0]
|
||
return False, f"括号不匹配:位置 {unmatched_pos} 处有未匹配的左括号 '{unmatched_char}'"
|
||
|
||
# ========== 检查多余的括号嵌套(使用栈方法分析上下文) ==========
|
||
# 检查连续的左括号 `( (` 是否是合法的
|
||
i = 0
|
||
while i < len(cypher) - 1:
|
||
# 查找 `( (` 模式(外层括号)
|
||
if cypher[i] == '(':
|
||
# 跳过空白字符,查找内层括号
|
||
inner_pos = i + 1
|
||
while inner_pos < len(cypher) and cypher[inner_pos] in ' \t\n\r':
|
||
inner_pos += 1
|
||
|
||
# 如果找到内层括号 `(`
|
||
if inner_pos < len(cypher) and cypher[inner_pos] == '(':
|
||
# 获取上下文
|
||
before_start = max(0, i - 30)
|
||
before_context = cypher[before_start:i]
|
||
after_end = min(len(cypher), inner_pos + 30)
|
||
after_context = cypher[inner_pos:after_end]
|
||
|
||
# 检查是否是合法的括号嵌套
|
||
is_valid = self._is_valid_bracket_nesting(before_context, after_context)
|
||
|
||
if not is_valid:
|
||
error_context = cypher[max(0, i-30):min(len(cypher), inner_pos+30)]
|
||
return False, f"语法错误:检测到多余的括号。位置:...{error_context}..."
|
||
|
||
i += 1
|
||
|
||
return True, None
|
||
|
||
def _is_valid_bracket_nesting(self, before_context: str, after_context: str) -> bool:
|
||
"""
|
||
判断括号嵌套是否合法
|
||
"""
|
||
# 检查是否是节点定义(变量名后跟冒号)
|
||
# 支持中文字符,允许开头有空白字符
|
||
is_node_def = re.match(r'\s*[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*\s*:', after_context)
|
||
|
||
# 检查是否是属性访问(变量名后跟点号)
|
||
# 支持中文字符,允许开头有空白字符
|
||
is_property_access = re.match(r'\s*[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*\s*\.', after_context)
|
||
|
||
# 检查是否在WHERE/CASE/WHEN上下文中(这些上下文中的括号嵌套通常是合法的)
|
||
before_upper = before_context.upper()
|
||
is_in_where_context = (
|
||
'WHERE' in before_upper or
|
||
'AND' in before_upper or
|
||
'OR' in before_upper or
|
||
'CASE' in before_upper or
|
||
'WHEN' in before_upper or
|
||
'THEN' in before_upper or
|
||
'RETURN' in before_upper # RETURN子句中的CASE表达式也可能有括号嵌套
|
||
)
|
||
|
||
# 检查是否在函数调用中(如 count(, sum(, max( 等)
|
||
is_in_function = re.search(r'[a-zA-Z_][a-zA-Z0-9_]*\s*\(\s*$', before_context)
|
||
|
||
# 如果匹配任何一种合法情况,返回True
|
||
return is_node_def or is_property_access or is_in_where_context or is_in_function
|
||
|
||
def dry_run(self, cypher: str, parse_error_func=None) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
|
||
"""
|
||
第三层验证:执行前验证(Dry Run,使用EXPLAIN检查)
|
||
|
||
Args:
|
||
cypher: 待验证的Cypher语句
|
||
parse_error_func: 错误解析函数(可选)
|
||
|
||
Returns:
|
||
Tuple[bool, Optional[str], Optional[Dict]]: (是否有效, 错误信息, 错误详情)
|
||
错误详情包含:error_type ('syntax_error' | 'schema_error'), error_message, error_position等
|
||
"""
|
||
try:
|
||
explain_cypher = f"EXPLAIN {cypher}"
|
||
result = self.driver.execute_query(explain_cypher)
|
||
|
||
summary = None
|
||
notifications = None
|
||
|
||
# 方式1: 直接访问 result.summary
|
||
if hasattr(result, 'summary'):
|
||
summary = result.summary
|
||
# 方式2: 如果是元组,取第二个元素
|
||
elif isinstance(result, tuple) and len(result) >= 2:
|
||
summary = result[1]
|
||
# 方式3: 尝试通过索引访问
|
||
elif hasattr(result, '__getitem__'):
|
||
try:
|
||
if len(result) > 1:
|
||
summary = result[1]
|
||
except:
|
||
pass
|
||
|
||
# 如果找到了 summary,尝试获取 notifications
|
||
if summary:
|
||
notifications = getattr(summary, 'notifications', None)
|
||
# 如果 notifications 是 None,尝试其他属性名
|
||
if notifications is None:
|
||
for attr_name in ['notifications', '_notifications', 'warnings', '_warnings']:
|
||
if hasattr(summary, attr_name):
|
||
notifications = getattr(summary, attr_name)
|
||
break
|
||
|
||
# 如果仍然没有找到,尝试直接从 result 访问
|
||
if notifications is None:
|
||
for attr_name in ['notifications', '_notifications', 'summary', '_summary']:
|
||
if hasattr(result, attr_name):
|
||
obj = getattr(result, attr_name)
|
||
if hasattr(obj, 'notifications'):
|
||
notifications = obj.notifications
|
||
break
|
||
elif isinstance(obj, list):
|
||
notifications = obj
|
||
break
|
||
|
||
return True, None, None
|
||
except CypherSyntaxError as e:
|
||
error_info = None
|
||
if parse_error_func:
|
||
error_info = parse_error_func(str(e), cypher)
|
||
# 确保 error_info 包含 error_type
|
||
if error_info:
|
||
error_info['error_type'] = 'syntax_error'
|
||
else:
|
||
error_info = {'error_type': 'syntax_error', 'error_message': str(e)}
|
||
return False, f"语法错误: {str(e)}", error_info
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
error_info = None
|
||
if parse_error_func:
|
||
error_info = parse_error_func(error_msg, cypher)
|
||
if "SyntaxError" in error_msg or "Syntax" in error_msg:
|
||
if error_info:
|
||
error_info['error_type'] = 'syntax_error'
|
||
else:
|
||
error_info = {'error_type': 'syntax_error', 'error_message': error_msg}
|
||
return False, f"语法错误: {error_msg}", error_info
|
||
# 其他异常,默认为语法错误
|
||
if error_info:
|
||
error_info['error_type'] = 'syntax_error'
|
||
else:
|
||
error_info = {'error_type': 'syntax_error', 'error_message': error_msg}
|
||
return False, f"执行计划失败: {error_msg}", error_info
|
||
|
||
def validate_schema(self, cypher: str) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
第二层验证:Schema 验证(验证节点类型和关系类型)
|
||
|
||
简单逻辑:在 MATCH 后面找到 `-`,向前查找 `()` 或 `[]`,提取 `:` 后的内容进行验证。
|
||
|
||
Args:
|
||
cypher: 待验证的Cypher语句
|
||
|
||
Returns:
|
||
Tuple[bool, Optional[str]]: (是否有效, 错误信息)
|
||
"""
|
||
if not cypher or not cypher.strip():
|
||
return False, "Cypher语句为空"
|
||
|
||
# 如果节点类型和关系类型列表都为空,跳过验证
|
||
if not self.allowed_node_types and not self.allowed_relationship_types:
|
||
return True, None
|
||
|
||
cypher_upper = cypher.upper()
|
||
invalid_labels = []
|
||
invalid_types = []
|
||
|
||
# 找到所有 MATCH 关键字的位置
|
||
i = 0
|
||
while i < len(cypher):
|
||
match_pos = cypher_upper.find("MATCH", i)
|
||
if match_pos == -1:
|
||
break
|
||
|
||
# 从 MATCH 后开始查找,找到子句结束位置
|
||
start_pos = match_pos + 5
|
||
end_pos = len(cypher)
|
||
for keyword in ['WHERE', 'WITH', 'RETURN', 'OPTIONAL', 'MATCH']:
|
||
kw_pos = cypher_upper.find(keyword, start_pos)
|
||
if kw_pos != -1 and kw_pos < end_pos:
|
||
end_pos = kw_pos
|
||
|
||
# 在 MATCH 子句范围内查找所有 `-`
|
||
pos = start_pos
|
||
while pos < end_pos:
|
||
dash_pos = cypher.find('-', pos, end_pos)
|
||
if dash_pos == -1:
|
||
break
|
||
|
||
# 向前查找最近的 `(` 或 `[`
|
||
bracket_start = -1
|
||
bracket_type = None
|
||
for j in range(dash_pos - 1, max(0, dash_pos - 200), -1):
|
||
if cypher[j] == '(':
|
||
bracket_start = j
|
||
bracket_type = '('
|
||
break
|
||
elif cypher[j] == '[':
|
||
bracket_start = j
|
||
bracket_type = '['
|
||
break
|
||
|
||
if bracket_start != -1:
|
||
# 找到对应的右括号
|
||
right_bracket = ')' if bracket_type == '(' else ']'
|
||
bracket_end = cypher.find(right_bracket, bracket_start + 1, dash_pos + 100)
|
||
|
||
if bracket_end != -1:
|
||
# 提取括号内容
|
||
content = cypher[bracket_start + 1:bracket_end]
|
||
# 查找 `:` 后的内容
|
||
colon_pos = content.find(':')
|
||
if colon_pos != -1:
|
||
after_colon = content[colon_pos + 1:]
|
||
# 去除 `{}` 及其内容
|
||
after_colon = re.sub(r'\{[^}]*\}', '', after_colon).strip()
|
||
|
||
if bracket_type == '(':
|
||
# 节点标签,用 `:` 分隔
|
||
for label in after_colon.split(':'):
|
||
label = label.strip()
|
||
# 只取第一个单词(去除空格后的内容)
|
||
label = re.split(r'[\s\{]', label)[0]
|
||
if label and self.allowed_node_types and label not in self.allowed_node_types:
|
||
invalid_labels.append(label)
|
||
else:
|
||
# 关系类型,用 `|` 分隔
|
||
for rel_type in after_colon.split('|'):
|
||
rel_type = rel_type.strip()
|
||
# 只取关系类型本体,剥离可变长度/属性等修饰
|
||
# 例如:`:包含*0..8` 应识别为关系类型 `包含`
|
||
# 例如:`:REL*` / `:REL*1..3` / `:REL {a:1}` / `:REL*0..8 {a:1}`
|
||
rel_type = re.split(r'[\s\{]', rel_type)[0]
|
||
rel_type = re.sub(r'\*.*$', '', rel_type).strip()
|
||
if rel_type and self.allowed_relationship_types and rel_type not in self.allowed_relationship_types:
|
||
invalid_types.append(rel_type)
|
||
|
||
pos = dash_pos + 1
|
||
|
||
i = end_pos
|
||
|
||
# 返回错误信息
|
||
if invalid_labels:
|
||
return False, f"检测到不允许的节点类型: {', '.join(sorted(set(invalid_labels)))}"
|
||
if invalid_types:
|
||
return False, f"检测到不允许的关系类型: {', '.join(sorted(set(invalid_types)))}"
|
||
|
||
return True, None
|