Path: blob/main/singlestoredb/fusion/handler.py
469 views
#!/usr/bin/env python31import abc2import functools3import os4import re5import sys6import textwrap7from typing import Any8from typing import Callable9from typing import Dict10from typing import Iterable11from typing import List12from typing import Optional13from typing import Set14from typing import Tuple1516from parsimonious import Grammar17from parsimonious import ParseError18from parsimonious.nodes import Node19from parsimonious.nodes import NodeVisitor2021from . import result22from ..connection import Connection2324CORE_GRAMMAR = r'''25ws = ~r"(\s+|(\s*/\*.*\*/\s*)+)"26qs = ~r"\"([^\"]*)\"|'([^\']*)'|([A-Za-z0-9_\-\.]+)|`([^\`]+)`" ws*27number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i ws*28integer = ~r"-?\d+" ws*29comma = ws* "," ws*30eq = ws* "=" ws*31open_paren = ws* "(" ws*32close_paren = ws* ")" ws*33open_repeats = ws* ~r"[\(\[\{]" ws*34close_repeats = ws* ~r"[\)\]\}]" ws*35statement = ~r"[\s\S]*" ws*36table = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*37column = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*38link_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*39catalog_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*4041json = ws* json_object ws*42json_object = ~r"{\s*" json_members? ~r"\s*}"43json_members = json_mapping (~r"\s*,\s*" json_mapping)*44json_mapping = json_string ~r"\s*:\s*" json_value45json_array = ~r"\[\s*" json_items? ~r"\s*\]"46json_items = json_value (~r"\s*,\s*" json_value)*47json_value = json_object / json_array / json_string / json_true_val / json_false_val / json_null_val / json_number48json_true_val = "true"49json_false_val = "false"50json_null_val = "null"51json_string = ~r"\"[ !#-\[\]-\U0010ffff]*(?:\\(?:[\"\\/bfnrt]|u[0-9A-Fa-f]{4})[ !#-\[\]-\U0010ffff]*)*\""52json_number = ~r"-?(0|[1-9][0-9]*)(\.\d*)?([eE][-+]?\d+)?"53''' # noqa: E5015455BUILTINS = {56'<order-by>': r'''57order_by = ORDER BY order_by_key_,...58order_by_key_ = '<key>' [ ASC | DESC ]59''',60'<like>': r'''61like = LIKE '<pattern>'62''',63'<extended>': r'''64extended = EXTENDED65''',66'<limit>': r'''67limit = LIMIT <integer>68''',69'<integer>': '',70'<number>': '',71'<json>': '',72'<table>': '',73'<column>': '',74'<catalog-name>': '',75'<link-name>': '',76'<file-type>': r'''77file_type = { FILE | FOLDER }78''',79'<statement>': '',80}8182BUILTIN_DEFAULTS = { # type: ignore83'order_by': {'by': []},84'like': None,85'extended': False,86'limit': None,87'json': {},88}8990_json_unesc_re = re.compile(r'\\(["/\\bfnrt]|u[0-9A-Fa-f])')91_json_unesc_map = {92'"': '"',93'/': '/',94'\\': '\\',95'b': '\b',96'f': '\f',97'n': '\n',98'r': '\r',99't': '\t',100}101102103def _json_unescape(m: Any) -> str:104c = m.group(1)105if c[0] == 'u':106return chr(int(c[1:], 16))107c2 = _json_unesc_map.get(c)108if not c2:109raise ValueError(f'invalid escape sequence: {m.group(0)}')110return c2111112113def json_unescape(s: str) -> str:114return _json_unesc_re.sub(_json_unescape, s[1:-1])115116117def get_keywords(grammar: str) -> Tuple[str, ...]:118"""Return all all-caps words from the beginning of the line."""119m = re.match(r'^\s*((?:[@A-Z0-9_]+)(\s+|$|;))+', grammar)120if not m:121return tuple()122return tuple(re.split(r'\s+', m.group(0).replace(';', '').strip()))123124125def is_bool(grammar: str) -> bool:126"""Determine if the rule is a boolean."""127return bool(re.match(r'^[@A-Z0-9_\s*]+$', grammar))128129130def process_optional(m: Any) -> str:131"""Create options or groups of options."""132sql = m.group(1).strip()133if '|' in sql:134return f'( {sql} )*'135return f'( {sql} )?'136137138def process_alternates(m: Any) -> str:139"""Make alternates mandatory groups."""140sql = m.group(1).strip()141if '|' in sql:142return f'( {sql} )'143raise ValueError(f'alternates must contain "|": {sql}')144145146def process_repeats(m: Any) -> str:147"""Add repeated patterns."""148sql = m.group(1).strip()149return f'open_repeats? {sql} ws* ( comma {sql} ws* )* close_repeats?'150151152def lower_and_regex(m: Any) -> str:153"""Lowercase and convert literal to regex."""154start = m.group(1) or ''155sql = m.group(2)156return f'~"{start}{sql.lower()}"i'157158159def split_unions(grammar: str) -> str:160"""161Convert grammar in the form '[ x ] [ y ]' to '[ x | y ]'.162163Parameters164----------165grammar : str166SQL grammar167168Returns169-------170str171172"""173in_alternate = False174out = []175for c in grammar:176if c == '{':177in_alternate = True178out.append(c)179elif c == '}':180in_alternate = False181out.append(c)182elif not in_alternate and c == '|':183out.append(']')184out.append(' ')185out.append('[')186else:187out.append(c)188return ''.join(out)189190191def expand_rules(rules: Dict[str, str], m: Any) -> str:192"""193Return expanded grammar syntax for given rule.194195Parameters196----------197ops : Dict[str, str]198Dictionary of rules in grammar199200Returns201-------202str203204"""205txt = m.group(1)206if txt in rules:207return f' {rules[txt]} '208return f' <{txt}> '209210211def build_cmd(grammar: str) -> str:212"""Pre-process grammar to construct top-level command."""213if ';' not in grammar:214raise ValueError('a semi-colon exist at the end of the primary rule')215216# Pre-space217m = re.match(r'^\s*', grammar)218space = m.group(0) if m else ''219220# Split on ';' on a line by itself221begin, end = grammar.split(';', 1)222223# Get statement keywords224keywords = get_keywords(begin)225cmd = '_'.join(x.lower() for x in keywords) + '_cmd'226227# Collapse multi-line to one228begin = re.sub(r'\s+', r' ', begin)229230return f'{space}{cmd} ={begin}\n{end}'231232233def build_syntax(grammar: str) -> str:234"""Construct full syntax."""235if ';' not in grammar:236raise ValueError('a semi-colon exist at the end of the primary rule')237238# Split on ';' on a line by itself239cmd, end = grammar.split(';', 1)240241name = ''242rules: Dict[str, Any] = {}243for line in end.split('\n'):244line = line.strip()245if line.startswith('&'):246rules[name] += '\n' + line247continue248if not line:249continue250name, value = line.split('=', 1)251name = name.strip()252value = value.strip()253rules[name] = value254255while re.search(r' [a-z0-9_]+\b', cmd):256cmd = re.sub(r' ([a-z0-9_]+)\b', functools.partial(expand_rules, rules), cmd)257258def add_indent(m: Any) -> str:259return ' ' + (len(m.group(1)) * ' ')260261# Indent line-continuations262cmd = re.sub(r'^(\&+)\s*', add_indent, cmd, flags=re.M)263264cmd = textwrap.dedent(cmd).rstrip() + ';'265cmd = re.sub(r'(\S) +', r'\1 ', cmd)266cmd = re.sub(r'<comma>', ',', cmd)267cmd = re.sub(r'\s+,\s*\.\.\.', ',...', cmd)268269return cmd270271272def _format_examples(ex: str) -> str:273"""Convert examples into sections."""274return re.sub(r'(^Example\s+\d+.*$)', r'### \1', ex, flags=re.M)275276277def _format_arguments(arg: str) -> str:278"""Format arguments as subsections."""279out = []280for line in arg.split('\n'):281if line.startswith('<'):282out.append(f'### {line.replace("<", "<").replace(">", ">")}')283out.append('')284else:285out.append(line.strip())286return '\n'.join(out)287288289def _to_markdown(txt: str) -> str:290"""Convert formatting to markdown."""291txt = re.sub(r'`([^`]+)\s+\<([^\>]+)>`_', r'[\1](\2)', txt)292txt = txt.replace('``', '`')293294# Format code blocks295lines = re.split(r'\n', txt)296out = []297while lines:298line = lines.pop(0)299if line.endswith('::'):300out.append(line[:-2] + '.')301code = []302while lines and (not lines[0].strip() or lines[0].startswith(' ')):303code.append(lines.pop(0).rstrip())304code_str = re.sub(r'^\s*\n', r'', '\n'.join(code).rstrip())305out.extend([f'```sql\n{code_str}\n```\n'])306else:307out.append(line)308309return '\n'.join(out)310311312def build_help(syntax: str, grammar: str) -> str:313"""Build full help text."""314cmd = re.match(r'([A-Z0-9_ ]+)', syntax.strip())315if not cmd:316raise ValueError(f'no command found: {syntax}')317318out = [f'# {cmd.group(1)}\n\n']319320sections: Dict[str, str] = {}321grammar = textwrap.dedent(grammar.rstrip())322desc_re = re.compile(r'^([A-Z][\S ]+)\s*^\-\-\-\-+\s*$', flags=re.M)323if desc_re.search(grammar):324_, *txt = desc_re.split(grammar)325txt = [x.strip() for x in txt]326sections = {}327while txt:328key = txt.pop(0)329value = txt.pop(0)330sections[key.lower()] = _to_markdown(value).strip()331332if 'description' in sections:333out.extend([sections['description'], '\n\n'])334335out.append(f'## Syntax\n\n```sql{syntax}\n```\n\n')336337if 'arguments' in sections:338out.extend([339'## Arguments\n\n',340_format_arguments(sections['arguments']),341'\n\n',342])343if 'argument' in sections:344out.extend([345'## Argument\n\n',346_format_arguments(sections['argument']),347'\n\n',348])349350if 'remarks' in sections:351out.extend(['## Remarks\n\n', sections['remarks'], '\n\n'])352353if 'examples' in sections:354out.extend(['## Examples\n\n', _format_examples(sections['examples']), '\n\n'])355elif 'example' in sections:356out.extend(['## Example\n\n', _format_examples(sections['example']), '\n\n'])357358if 'see also' in sections:359out.extend(['## See Also\n\n', sections['see also'], '\n\n'])360361return ''.join(out).rstrip() + '\n'362363364def strip_comments(grammar: str) -> str:365"""Strip comments from grammar."""366desc_re = re.compile(r'(^\s*Description\s*^\s*-----------\s*$)', flags=re.M)367grammar = desc_re.split(grammar, maxsplit=1)[0]368return re.sub(r'^\s*#.*$', r'', grammar, flags=re.M)369370371def get_rule_info(grammar: str) -> Dict[str, Any]:372"""Compute metadata about rule used in coallescing parsed output."""373return dict(374n_keywords=len(get_keywords(grammar)),375repeats=',...' in grammar,376default=False if is_bool(grammar) else [] if ',...' in grammar else None,377)378379380def inject_builtins(grammar: str) -> str:381"""Inject complex builtin rules."""382for k, v in BUILTINS.items():383if re.search(k, grammar):384grammar = re.sub(385k,386k.replace('<', '').replace('>', '').replace('-', '_'),387grammar,388)389grammar += v390return grammar391392393def process_grammar(394grammar: str,395) -> Tuple[Grammar, Tuple[str, ...], Dict[str, Any], str, str]:396"""397Convert SQL grammar to a Parsimonious grammar.398399Parameters400----------401grammar : str402The SQL grammar403404Returns405-------406(Grammar, Tuple[str, ...], Dict[str, Any], str) - Grammar is the parsimonious407grammar object. The tuple is a series of the keywords that start the command.408The dictionary is a set of metadata about each rule. The final string is409a human-readable version of the grammar for documentation and errors.410411"""412out = []413rules = {}414rule_info = {}415416full_grammar = grammar417grammar = strip_comments(grammar)418grammar = inject_builtins(grammar)419command_key = get_keywords(grammar)420syntax_txt = build_syntax(grammar)421help_txt = build_help(syntax_txt, full_grammar)422grammar = build_cmd(grammar)423424# Remove line-continuations425grammar = re.sub(r'\n\s*&+', r'', grammar)426427# Make sure grouping characters all have whitespace around them428grammar = re.sub(r' *(\[|\{|\||\}|\]) *', r' \1 ', grammar)429430grammar = re.sub(r'\(', r' open_paren ', grammar)431grammar = re.sub(r'\)', r' close_paren ', grammar)432433for line in grammar.split('\n'):434if not line.strip():435continue436437op, sql = line.split('=', 1)438op = op.strip()439sql = sql.strip()440sql = split_unions(sql)441442rules[op] = sql443rule_info[op] = get_rule_info(sql)444445# Convert consecutive optionals to a union446sql = re.sub(r'\]\s+\[', r' | ', sql)447448# Lower-case keywords and make them case-insensitive449sql = re.sub(r'(\b|@+)([A-Z0-9_]+)\b', lower_and_regex, sql)450451# Convert literal strings to 'qs'452sql = re.sub(r"'[^']+'", r'qs', sql)453454# Convert special characters to literal tokens455sql = re.sub(r'([=]) ', r' eq ', sql)456457# Convert [...] groups to (...)*458sql = re.sub(r'\[([^\]]+)\]', process_optional, sql)459460# Convert {...} groups to (...)461sql = re.sub(r'\{([^\}]+)\}', process_alternates, sql)462463# Convert <...> to ... (<...> is the form for core types)464sql = re.sub(r'<([a-z0-9_]+)>', r'\1', sql)465466# Insert ws between every token to allow for whitespace and comments467sql = ' ws '.join(re.split(r'\s+', sql)) + ' ws'468469# Remove ws in optional groupings470sql = sql.replace('( ws', '(')471sql = sql.replace('| ws', '|')472473# Convert | to /474sql = sql.replace('|', '/')475476# Remove ws after operation names, all operations contain ws at the end477sql = re.sub(r'(\s+[a-z0-9_]+)\s+ws\b', r'\1', sql)478479# Convert foo,... to foo ("," foo)*480sql = re.sub(r'(\S+),...', process_repeats, sql)481482# Remove ws before / and )483sql = re.sub(r'(\s*\S+\s+)ws\s+/', r'\1/', sql)484sql = re.sub(r'(\s*\S+\s+)ws\s+\)', r'\1)', sql)485486# Make sure every operation ends with ws487sql = re.sub(r'\s+ws\s+ws$', r' ws', sql + ' ws')488sql = re.sub(r'(\s+ws)*\s+ws\*$', r' ws*', sql)489sql = re.sub(r'\s+ws$', r' ws*', sql)490sql = re.sub(r'\s+ws\s+\(', r' ws* (', sql)491sql = re.sub(r'\)\s+ws\s+', r') ws* ', sql)492sql = re.sub(r'\s+ws\s+', r' ws* ', sql)493sql = re.sub(r'\?\s+ws\+', r'? ws*', sql)494495# Remove extra ws around eq496sql = re.sub(r'ws\+\s*eq\b', r'eq', sql)497498# Remove optional groupings when mandatory groupings are specified499sql = re.sub(r'open_paren\s+ws\*\s+open_repeats\?', r'open_paren', sql)500sql = re.sub(r'close_repeats\?\s+ws\*\s+close_paren', r'close_paren', sql)501sql = re.sub(r'open_paren\s+open_repeats\?', r'open_paren', sql)502sql = re.sub(r'close_repeats\?\s+close_paren', r'close_paren', sql)503504out.append(f'{op} = {sql}')505506for k, v in list(rules.items()):507while re.search(r' ([a-z0-9_]+) ', v):508v = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), v)509rules[k] = v510511for k, v in list(rules.items()):512while re.search(r' <([a-z0-9_]+)> ', v):513v = re.sub(r' <([a-z0-9_]+)> ', r' \1 ', v)514rules[k] = v515516cmds = ' / '.join(x for x in rules if x.endswith('_cmd'))517cmds = f'init = ws* ( {cmds} ) ws* ";"? ws*\n'518519grammar = cmds + CORE_GRAMMAR + '\n'.join(out)520521try:522return (523Grammar(grammar), command_key,524rule_info, syntax_txt, help_txt,525)526except ParseError:527print(grammar, file=sys.stderr)528raise529530531def flatten(items: Iterable[Any]) -> List[Any]:532"""Flatten a list of iterables."""533out = []534for x in items:535if isinstance(x, (str, bytes, dict)):536out.append(x)537elif isinstance(x, Iterable):538for sub_x in flatten(x):539if sub_x is not None:540out.append(sub_x)541elif x is not None:542out.append(x)543return out544545546def merge_dicts(items: List[Dict[str, Any]]) -> Dict[str, Any]:547"""Merge list of dictionaries together."""548out: Dict[str, Any] = {}549for x in items:550if isinstance(x, dict):551same = list(set(x.keys()).intersection(set(out.keys())))552if same:553raise ValueError(f"found duplicate rules for '{same[0]}'")554out.update(x)555return out556557558class SQLHandler(NodeVisitor):559"""Base class for all SQL handler classes."""560561#: Parsimonious grammar object562grammar: Grammar = Grammar(CORE_GRAMMAR)563564#: SQL keywords that start the command565command_key: Tuple[str, ...] = ()566567#: Metadata about the parse rules568rule_info: Dict[str, Any] = {}569570#: Syntax string for use in error messages571syntax: str = ''572573#: Full help for the command574help: str = ''575576#: Rule validation functions577validators: Dict[str, Callable[..., Any]] = {}578579_grammar: str = CORE_GRAMMAR580_is_compiled: bool = False581_enabled: bool = True582583def __init__(self, connection: Connection):584self.connection = connection585self._handled: Set[str] = set()586587@classmethod588def compile(cls, grammar: str = '') -> None:589"""590Compile the grammar held in the docstring.591592This method modifies attributes on the class: ``grammar``,593``command_key``, ``rule_info``, ``syntax``, and ``help``.594595Parameters596----------597grammar : str, optional598Grammar to use instead of docstring599600"""601if cls._is_compiled:602return603604cls.grammar, cls.command_key, cls.rule_info, cls.syntax, cls.help = \605process_grammar(grammar or cls.__doc__ or '')606607cls._grammar = grammar or cls.__doc__ or ''608cls._is_compiled = True609610@classmethod611def register(cls, overwrite: bool = False) -> None:612"""613Register the handler class.614615Paraemeters616-----------617overwrite : bool, optional618Overwrite an existing command with the same name?619620"""621if not cls._enabled and \622os.environ.get('SINGLESTOREDB_FUSION_ENABLE_HIDDEN', '0').lower() not in \623['1', 't', 'true', 'y', 'yes']:624return625626from . import registry627cls.compile()628registry.register_handler(cls, overwrite=overwrite)629630def create_result(self) -> result.FusionSQLResult:631"""632Create a new result object.633634Returns635-------636FusionSQLResult637A new result object for this handler638639"""640return result.FusionSQLResult()641642def execute(self, sql: str) -> result.FusionSQLResult:643"""644Parse the SQL and invoke the handler method.645646Parameters647----------648sql : str649SQL statement to execute650651Returns652-------653DummySQLResult654655"""656type(self).compile()657self._handled = set()658try:659params = self.visit(type(self).grammar.parse(sql))660for k, v in params.items():661params[k] = self.validate_rule(k, v)662663res = self.run(params)664665self._handled = set()666667if res is not None:668res.format_results(self.connection)669return res670671res = result.FusionSQLResult()672res.set_rows([])673res.format_results(self.connection)674return res675676except ParseError as exc:677s = str(exc)678msg = ''679m = re.search(r'(The non-matching portion.*$)', s)680if m:681msg = ' ' + m.group(1)682m = re.search(r"(Rule) '.+?'( didn't match at.*$)", s)683if m:684msg = ' ' + m.group(1) + m.group(2)685raise ValueError(686f'Could not parse statement.{msg} '687'Expecting:\n' + textwrap.indent(type(self).syntax, ' '),688)689690@abc.abstractmethod691def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:692"""693Run the handler command.694695Parameters696----------697params : Dict[str, Any]698Values parsed from the SQL query. Each rule in the grammar699results in a key/value pair in the ``params` dictionary.700701Returns702-------703SQLResult - tuple containing the column definitions and704rows of data in the result705706"""707raise NotImplementedError708709def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:710"""Quoted strings."""711if node is None:712return None713return flatten(visited_children)[0]714715def visit_compound(self, node: Node, visited_children: Iterable[Any]) -> Any:716"""Compound name."""717print(visited_children)718return flatten(visited_children)[0]719720def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:721"""Numeric value."""722return float(flatten(visited_children)[0])723724def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:725"""Integer value."""726return int(flatten(visited_children)[0])727728def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:729"""Whitespace and comments."""730return731732def visit_eq(self, node: Node, visited_children: Iterable[Any]) -> Any:733"""Equals sign."""734return735736def visit_comma(self, node: Node, visited_children: Iterable[Any]) -> Any:737"""Single comma."""738return739740def visit_open_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:741"""Open parenthesis."""742return743744def visit_close_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:745"""Close parenthesis."""746return747748def visit_open_repeats(self, node: Node, visited_children: Iterable[Any]) -> Any:749"""Open repeat grouping."""750return751752def visit_close_repeats(self, node: Node, visited_children: Iterable[Any]) -> Any:753"""Close repeat grouping."""754return755756def visit_init(self, node: Node, visited_children: Iterable[Any]) -> Any:757"""Entry point of the grammar."""758_, out, *_ = visited_children759return out760761def visit_statement(self, node: Node, visited_children: Iterable[Any]) -> Any:762out = ' '.join(flatten(visited_children)).strip()763return {'statement': out}764765def visit_order_by(self, node: Node, visited_children: Iterable[Any]) -> Any:766"""Handle ORDER BY."""767by = []768ascending = []769data = [x for x in flatten(visited_children)[2:] if x]770for item in data:771value = item.popitem()[-1]772if not isinstance(value, list):773value = [value]774value.append('A')775by.append(value[0])776ascending.append(value[1].upper().startswith('A'))777return {'order_by': {'by': by, 'ascending': ascending}}778779def _delimited(self, node: Node, children: Iterable[Any]) -> Any:780children = list(children)781items = [children[0]]782items.extend(item for _, item in children[1])783return items784785def _atomic(self, node: Node, children: Iterable[Any]) -> Any:786return list(children)[0]787788# visitors789visit_json_value = _atomic790visit_json_members = visit_json_items = _delimited791792def visit_json_object(self, node: Node, children: Iterable[Any]) -> Any:793_, members, _ = children794if isinstance(members, list):795members = members[0]796else:797members = []798members = [x for x in members if x != '']799return dict(members)800801def visit_json_array(self, node: Node, children: Iterable[Any]) -> Any:802_, values, _ = children803if isinstance(values, list):804values = values[0]805else:806values = []807return values808809def visit_json_mapping(self, node: Node, children: Iterable[Any]) -> Any:810key, _, value = children811return key, value812813def visit_json_string(self, node: Node, children: Iterable[Any]) -> Any:814return json_unescape(node.text)815816def visit_json_number(self, node: Node, children: Iterable[Any]) -> Any:817if '.' in node.text:818return float(node.text)819return int(node.text)820821def visit_json_true_val(self, node: Node, children: Iterable[Any]) -> Any:822return True823824def visit_json_false_val(self, node: Node, children: Iterable[Any]) -> Any:825return False826827def visit_json_null_val(self, node: Node, children: Iterable[Any]) -> Any:828return None829830def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:831"""832Handle all undefined rules.833834This method processes all user-defined rules. Each rule results in835a dictionary with a single key corresponding to the rule name, with836a value corresponding to the data value following the rule keywords.837838If no value exists, the value True is used. If the rule is not a839rule with possible repeated values, a single value is used. If the840rule can have repeated values, a list of values is returned.841842"""843if node.expr_name.startswith('json'):844return visited_children or node.text845846# Call a grammar rule847if node.expr_name in type(self).rule_info:848n_keywords = type(self).rule_info[node.expr_name]['n_keywords']849repeats = type(self).rule_info[node.expr_name]['repeats']850851# If this is the top-level command, create the final result852if node.expr_name.endswith('_cmd'):853final = merge_dicts(flatten(visited_children)[n_keywords:])854for k, v in type(self).rule_info.items():855if k.endswith('_cmd') or k.endswith('_') or k.startswith('_'):856continue857if k not in final and k not in self._handled:858final[k] = BUILTIN_DEFAULTS.get(k, v['default'])859return final860861# Filter out stray empty strings862out = [x for x in flatten(visited_children)[n_keywords:] if x]863864# Remove underscore prefixes from rule name865key_name = re.sub(r'^_+', r'', node.expr_name)866867if repeats or len(out) > 1:868self._handled.add(node.expr_name)869# If all outputs are dicts, merge them870if len(out) > 1 and not repeats:871is_dicts = [x for x in out if isinstance(x, dict)]872if len(is_dicts) == len(out):873return {key_name: merge_dicts(out)}874return {key_name: out}875876self._handled.add(node.expr_name)877return {key_name: out[0] if out else True}878879if hasattr(node, 'match'):880if not visited_children and not node.match.groups():881return node.text882return visited_children or list(node.match.groups())883884return visited_children or node.text885886def validate_rule(self, rule: str, value: Any) -> Any:887"""888Validate the value of the given rule.889890Paraemeters891-----------892rule : str893Name of the grammar rule the value belongs to894value : Any895Value parsed from the query896897Returns898-------899Any - result of the validator function900901"""902validator = type(self).validators.get(rule)903if validator is not None:904return validator(value)905return value906907908