""" 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