Path: blob/main/singlestoredb/fusion/handler.py
801 views
#!/usr/bin/env python31import abc2import functools3import os4import re5import sys6import textwrap7import warnings8from collections.abc import Iterable9from typing import Any10from typing import Callable11from typing import Dict12from typing import List13from typing import Optional14from typing import Set15from typing import Tuple1617from parsimonious import Grammar18from parsimonious import ParseError19from parsimonious.nodes import Node20from parsimonious.nodes import NodeVisitor2122from . import result23from ..connection import Connection24from ..warnings import PreviewFeatureWarning2526CORE_GRAMMAR = r'''27ws = ~r"(\s+|(\s*/\*.*\*/\s*)+)"28qs = ~r"\"([^\"]*)\"|'([^\']*)'|([A-Za-z0-9_\-\.]+)|`([^\`]+)`" ws*29number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i ws*30integer = ~r"-?\d+" ws*31comma = ws* "," ws*32eq = ws* "=" ws*33open_paren = ws* "(" ws*34close_paren = ws* ")" ws*35open_repeats = ws* ~r"[\(\[\{]" ws*36close_repeats = ws* ~r"[\)\]\}]" ws*37statement = ~r"[\s\S]*" ws*38table = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*39column = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*40link_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*41catalog_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*4243json = ws* json_object ws*44json_object = ~r"{\s*" json_members? ~r"\s*}"45json_members = json_mapping (~r"\s*,\s*" json_mapping)*46json_mapping = json_string ~r"\s*:\s*" json_value47json_array = ~r"\[\s*" json_items? ~r"\s*\]"48json_items = json_value (~r"\s*,\s*" json_value)*49json_value = json_object / json_array / json_string / json_true_val / json_false_val / json_null_val / json_number50json_true_val = "true"51json_false_val = "false"52json_null_val = "null"53json_string = ~r"\"[ !#-\[\]-\U0010ffff]*(?:\\(?:[\"\\/bfnrt]|u[0-9A-Fa-f]{4})[ !#-\[\]-\U0010ffff]*)*\""54json_number = ~r"-?(0|[1-9][0-9]*)(\.\d*)?([eE][-+]?\d+)?"55''' # noqa: E5015657BUILTINS = {58'<order-by>': r'''59order_by = ORDER BY order_by_key_,...60order_by_key_ = '<key>' [ ASC | DESC ]61''',62'<like>': r'''63like = LIKE '<pattern>'64''',65'<extended>': r'''66extended = EXTENDED67''',68'<limit>': r'''69limit = LIMIT <integer>70''',71'<integer>': '',72'<number>': '',73'<json>': '',74'<table>': '',75'<column>': '',76'<catalog-name>': '',77'<link-name>': '',78'<file-type>': r'''79file_type = { FILE | FOLDER }80''',81'<statement>': '',82}8384BUILTIN_DEFAULTS = { # type: ignore85'order_by': {'by': []},86'like': None,87'extended': False,88'limit': None,89'json': {},90}9192_json_unesc_re = re.compile(r'\\(["/\\bfnrt]|u[0-9A-Fa-f])')93_json_unesc_map = {94'"': '"',95'/': '/',96'\\': '\\',97'b': '\b',98'f': '\f',99'n': '\n',100'r': '\r',101't': '\t',102}103104105def _json_unescape(m: Any) -> str:106c = m.group(1)107if c[0] == 'u':108return chr(int(c[1:], 16))109c2 = _json_unesc_map.get(c)110if not c2:111raise ValueError(f'invalid escape sequence: {m.group(0)}')112return c2113114115def json_unescape(s: str) -> str:116return _json_unesc_re.sub(_json_unescape, s[1:-1])117118119def get_keywords(grammar: str) -> Tuple[str, ...]:120"""Return all all-caps words from the beginning of the line."""121m = re.match(r'^\s*((?:[@A-Z0-9_]+)(\s+|$|;))+', grammar)122if not m:123return tuple()124return tuple(re.split(r'\s+', m.group(0).replace(';', '').strip()))125126127def is_bool(grammar: str) -> bool:128"""Determine if the rule is a boolean."""129return bool(re.match(r'^[@A-Z0-9_\s*]+$', grammar))130131132def process_optional(m: Any) -> str:133"""Create options or groups of options."""134sql = m.group(1).strip()135if '|' in sql:136return f'( {sql} )*'137return f'( {sql} )?'138139140def process_alternates(m: Any) -> str:141"""Make alternates mandatory groups."""142sql = m.group(1).strip()143if '|' in sql:144return f'( {sql} )'145raise ValueError(f'alternates must contain "|": {sql}')146147148def process_repeats(m: Any) -> str:149"""Add repeated patterns."""150sql = m.group(1).strip()151return f'open_repeats? {sql} ws* ( comma {sql} ws* )* close_repeats?'152153154def lower_and_regex(m: Any) -> str:155"""Lowercase and convert literal to regex."""156start = m.group(1) or ''157sql = m.group(2)158return f'~"{start}{sql.lower()}"i'159160161def split_unions(grammar: str) -> str:162"""163Convert grammar in the form '[ x ] [ y ]' to '[ x | y ]'.164165Parameters166----------167grammar : str168SQL grammar169170Returns171-------172str173174"""175in_alternate = False176out = []177for c in grammar:178if c == '{':179in_alternate = True180out.append(c)181elif c == '}':182in_alternate = False183out.append(c)184elif not in_alternate and c == '|':185out.append(']')186out.append(' ')187out.append('[')188else:189out.append(c)190return ''.join(out)191192193def expand_rules(rules: Dict[str, str], m: Any) -> str:194"""195Return expanded grammar syntax for given rule.196197Parameters198----------199ops : Dict[str, str]200Dictionary of rules in grammar201202Returns203-------204str205206"""207txt = m.group(1)208if txt in rules:209return f' {rules[txt]} '210return f' <{txt}> '211212213def build_cmd(grammar: str) -> str:214"""Pre-process grammar to construct top-level command."""215if ';' not in grammar:216raise ValueError('a semi-colon exist at the end of the primary rule')217218# Pre-space219m = re.match(r'^\s*', grammar)220space = m.group(0) if m else ''221222# Split on ';' on a line by itself223begin, end = grammar.split(';', 1)224225# Get statement keywords226keywords = get_keywords(begin)227cmd = '_'.join(x.lower() for x in keywords) + '_cmd'228229# Collapse multi-line to one230begin = re.sub(r'\s+', r' ', begin)231232return f'{space}{cmd} ={begin}\n{end}'233234235def build_syntax(grammar: str) -> str:236"""Construct full syntax."""237if ';' not in grammar:238raise ValueError('a semi-colon exist at the end of the primary rule')239240# Split on ';' on a line by itself241cmd, end = grammar.split(';', 1)242243name = ''244rules: Dict[str, Any] = {}245for line in end.split('\n'):246line = line.strip()247if line.startswith('&'):248rules[name] += '\n' + line249continue250if not line:251continue252name, value = line.split('=', 1)253name = name.strip()254value = value.strip()255rules[name] = value256257while re.search(r' [a-z0-9_]+\b', cmd):258cmd = re.sub(r' ([a-z0-9_]+)\b', functools.partial(expand_rules, rules), cmd)259260def add_indent(m: Any) -> str:261return ' ' + (len(m.group(1)) * ' ')262263# Indent line-continuations264cmd = re.sub(r'^(\&+)\s*', add_indent, cmd, flags=re.M)265266cmd = textwrap.dedent(cmd).rstrip() + ';'267cmd = re.sub(r'(\S) +', r'\1 ', cmd)268cmd = re.sub(r'<comma>', ',', cmd)269cmd = re.sub(r'\s+,\s*\.\.\.', ',...', cmd)270271return cmd272273274def _format_examples(ex: str) -> str:275"""Convert examples into sections."""276return re.sub(r'(^Example\s+\d+.*$)', r'### \1', ex, flags=re.M)277278279def _format_arguments(arg: str) -> str:280"""Format arguments as subsections."""281out = []282for line in arg.split('\n'):283if line.startswith('<'):284out.append(f'### {line.replace("<", "<").replace(">", ">")}')285out.append('')286else:287out.append(line.strip())288return '\n'.join(out)289290291def _to_markdown(txt: str) -> str:292"""Convert formatting to markdown."""293txt = re.sub(r'`([^`]+)\s+\<([^\>]+)>`_', r'[\1](\2)', txt)294txt = txt.replace('``', '`')295296# Format code blocks297lines = re.split(r'\n', txt)298out = []299while lines:300line = lines.pop(0)301if line.endswith('::'):302out.append(line[:-2] + '.')303code = []304while lines and (not lines[0].strip() or lines[0].startswith(' ')):305code.append(lines.pop(0).rstrip())306code_str = re.sub(r'^\s*\n', r'', '\n'.join(code).rstrip())307out.extend([f'```sql\n{code_str}\n```\n'])308else:309out.append(line)310311return '\n'.join(out)312313314def build_help(syntax: str, grammar: str) -> str:315"""Build full help text."""316cmd = re.match(r'([A-Z0-9_ ]+)', syntax.strip())317if not cmd:318raise ValueError(f'no command found: {syntax}')319320out = [f'# {cmd.group(1)}\n\n']321322sections: Dict[str, str] = {}323grammar = textwrap.dedent(grammar.rstrip())324desc_re = re.compile(r'^([A-Z][\S ]+)\s*^\-\-\-\-+\s*$', flags=re.M)325if desc_re.search(grammar):326_, *txt = desc_re.split(grammar)327txt = [x.strip() for x in txt]328sections = {}329while txt:330key = txt.pop(0)331value = txt.pop(0)332sections[key.lower()] = _to_markdown(value).strip()333334if 'description' in sections:335out.extend([sections['description'], '\n\n'])336337out.append(f'## Syntax\n\n```sql{syntax}\n```\n\n')338339if 'arguments' in sections:340out.extend([341'## Arguments\n\n',342_format_arguments(sections['arguments']),343'\n\n',344])345if 'argument' in sections:346out.extend([347'## Argument\n\n',348_format_arguments(sections['argument']),349'\n\n',350])351352if 'remarks' in sections:353out.extend(['## Remarks\n\n', sections['remarks'], '\n\n'])354355if 'examples' in sections:356out.extend(['## Examples\n\n', _format_examples(sections['examples']), '\n\n'])357elif 'example' in sections:358out.extend(['## Example\n\n', _format_examples(sections['example']), '\n\n'])359360if 'see also' in sections:361out.extend(['## See Also\n\n', sections['see also'], '\n\n'])362363return ''.join(out).rstrip() + '\n'364365366def strip_comments(grammar: str) -> str:367"""Strip comments from grammar."""368desc_re = re.compile(r'(^\s*Description\s*^\s*-----------\s*$)', flags=re.M)369grammar = desc_re.split(grammar, maxsplit=1)[0]370return re.sub(r'^\s*#.*$', r'', grammar, flags=re.M)371372373def get_rule_info(grammar: str) -> Dict[str, Any]:374"""Compute metadata about rule used in coallescing parsed output."""375return dict(376n_keywords=len(get_keywords(grammar)),377repeats=',...' in grammar,378default=False if is_bool(grammar) else [] if ',...' in grammar else None,379)380381382def inject_builtins(grammar: str) -> str:383"""Inject complex builtin rules."""384for k, v in BUILTINS.items():385if re.search(k, grammar):386grammar = re.sub(387k,388k.replace('<', '').replace('>', '').replace('-', '_'),389grammar,390)391grammar += v392return grammar393394395def process_grammar(396grammar: str,397) -> Tuple[Grammar, Tuple[str, ...], Dict[str, Any], str, str]:398"""399Convert SQL grammar to a Parsimonious grammar.400401Parameters402----------403grammar : str404The SQL grammar405406Returns407-------408(Grammar, Tuple[str, ...], Dict[str, Any], str) - Grammar is the parsimonious409grammar object. The tuple is a series of the keywords that start the command.410The dictionary is a set of metadata about each rule. The final string is411a human-readable version of the grammar for documentation and errors.412413"""414out = []415rules = {}416rule_info = {}417418full_grammar = grammar419grammar = strip_comments(grammar)420grammar = inject_builtins(grammar)421command_key = get_keywords(grammar)422syntax_txt = build_syntax(grammar)423help_txt = build_help(syntax_txt, full_grammar)424grammar = build_cmd(grammar)425426# Remove line-continuations427grammar = re.sub(r'\n\s*&+', r'', grammar)428429# Make sure grouping characters all have whitespace around them430grammar = re.sub(r' *(\[|\{|\||\}|\]) *', r' \1 ', grammar)431432grammar = re.sub(r'\(', r' open_paren ', grammar)433grammar = re.sub(r'\)', r' close_paren ', grammar)434435for line in grammar.split('\n'):436if not line.strip():437continue438439op, sql = line.split('=', 1)440op = op.strip()441sql = sql.strip()442sql = split_unions(sql)443444rules[op] = sql445rule_info[op] = get_rule_info(sql)446447# Convert consecutive optionals to a union448sql = re.sub(r'\]\s+\[', r' | ', sql)449450# Lower-case keywords and make them case-insensitive451sql = re.sub(r'(\b|@+)([A-Z0-9_]+)\b', lower_and_regex, sql)452453# Convert literal strings to 'qs'454sql = re.sub(r"'[^']+'", r'qs', sql)455456# Convert special characters to literal tokens457sql = re.sub(r'([=]) ', r' eq ', sql)458459# Convert [...] groups to (...)*460sql = re.sub(r'\[([^\]]+)\]', process_optional, sql)461462# Convert {...} groups to (...)463sql = re.sub(r'\{([^\}]+)\}', process_alternates, sql)464465# Convert <...> to ... (<...> is the form for core types)466sql = re.sub(r'<([a-z0-9_]+)>', r'\1', sql)467468# Insert ws between every token to allow for whitespace and comments469sql = ' ws '.join(re.split(r'\s+', sql)) + ' ws'470471# Remove ws in optional groupings472sql = sql.replace('( ws', '(')473sql = sql.replace('| ws', '|')474475# Convert | to /476sql = sql.replace('|', '/')477478# Remove ws after operation names, all operations contain ws at the end479sql = re.sub(r'(\s+[a-z0-9_]+)\s+ws\b', r'\1', sql)480481# Convert foo,... to foo ("," foo)*482sql = re.sub(r'(\S+),...', process_repeats, sql)483484# Remove ws before / and )485sql = re.sub(r'(\s*\S+\s+)ws\s+/', r'\1/', sql)486sql = re.sub(r'(\s*\S+\s+)ws\s+\)', r'\1)', sql)487488# Make sure every operation ends with ws489sql = re.sub(r'\s+ws\s+ws$', r' ws', sql + ' ws')490sql = re.sub(r'(\s+ws)*\s+ws\*$', r' ws*', sql)491sql = re.sub(r'\s+ws$', r' ws*', sql)492sql = re.sub(r'\s+ws\s+\(', r' ws* (', sql)493sql = re.sub(r'\)\s+ws\s+', r') ws* ', sql)494sql = re.sub(r'\s+ws\s+', r' ws* ', sql)495sql = re.sub(r'\?\s+ws\+', r'? ws*', sql)496497# Remove extra ws around eq498sql = re.sub(r'ws\+\s*eq\b', r'eq', sql)499500# Remove optional groupings when mandatory groupings are specified501sql = re.sub(r'open_paren\s+ws\*\s+open_repeats\?', r'open_paren', sql)502sql = re.sub(r'close_repeats\?\s+ws\*\s+close_paren', r'close_paren', sql)503sql = re.sub(r'open_paren\s+open_repeats\?', r'open_paren', sql)504sql = re.sub(r'close_repeats\?\s+close_paren', r'close_paren', sql)505506out.append(f'{op} = {sql}')507508for k, v in list(rules.items()):509while re.search(r' ([a-z0-9_]+) ', v):510v = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), v)511rules[k] = v512513for k, v in list(rules.items()):514while re.search(r' <([a-z0-9_]+)> ', v):515v = re.sub(r' <([a-z0-9_]+)> ', r' \1 ', v)516rules[k] = v517518cmds = ' / '.join(x for x in rules if x.endswith('_cmd'))519cmds = f'init = ws* ( {cmds} ) ws* ";"? ws*\n'520521grammar = cmds + CORE_GRAMMAR + '\n'.join(out)522523try:524return (525Grammar(grammar), command_key,526rule_info, syntax_txt, help_txt,527)528except ParseError:529print(grammar, file=sys.stderr)530raise531532533def flatten(items: Iterable[Any]) -> List[Any]:534"""Flatten a list of iterables."""535out = []536for x in items:537if isinstance(x, (str, bytes, dict)):538out.append(x)539elif isinstance(x, Iterable):540for sub_x in flatten(x):541if sub_x is not None:542out.append(sub_x)543elif x is not None:544out.append(x)545return out546547548def merge_dicts(items: List[Dict[str, Any]]) -> Dict[str, Any]:549"""Merge list of dictionaries together."""550out: Dict[str, Any] = {}551for x in items:552if isinstance(x, dict):553same = list(set(x.keys()).intersection(set(out.keys())))554if same:555raise ValueError(f"found duplicate rules for '{same[0]}'")556out.update(x)557return out558559560class SQLHandler(NodeVisitor):561"""Base class for all SQL handler classes."""562563#: Parsimonious grammar object564grammar: Grammar = Grammar(CORE_GRAMMAR)565566#: SQL keywords that start the command567command_key: Tuple[str, ...] = ()568569#: Metadata about the parse rules570rule_info: Dict[str, Any] = {}571572#: Syntax string for use in error messages573syntax: str = ''574575#: Full help for the command576help: str = ''577578#: Rule validation functions579validators: Dict[str, Callable[..., Any]] = {}580581_grammar: str = CORE_GRAMMAR582_is_compiled: bool = False583_enabled: bool = True584_preview: bool = False585586def __init__(self, connection: Connection):587self.connection = connection588self._handled: Set[str] = set()589590@classmethod591def compile(cls, grammar: str = '') -> None:592"""593Compile the grammar held in the docstring.594595This method modifies attributes on the class: ``grammar``,596``command_key``, ``rule_info``, ``syntax``, and ``help``.597598Parameters599----------600grammar : str, optional601Grammar to use instead of docstring602603"""604if cls._is_compiled:605return606607cls.grammar, cls.command_key, cls.rule_info, cls.syntax, cls.help = \608process_grammar(grammar or cls.__doc__ or '')609610cls._grammar = grammar or cls.__doc__ or ''611cls._is_compiled = True612613@classmethod614def register(cls, overwrite: bool = False) -> None:615"""616Register the handler class.617618Paraemeters619-----------620overwrite : bool, optional621Overwrite an existing command with the same name?622623"""624if not cls._enabled and \625os.environ.get('SINGLESTOREDB_FUSION_ENABLE_HIDDEN', '0').lower() not in \626['1', 't', 'true', 'y', 'yes']:627return628629from . import registry630cls.compile()631registry.register_handler(cls, overwrite=overwrite)632633def create_result(self) -> result.FusionSQLResult:634"""635Create a new result object.636637Returns638-------639FusionSQLResult640A new result object for this handler641642"""643return result.FusionSQLResult()644645def execute(self, sql: str) -> result.FusionSQLResult:646"""647Parse the SQL and invoke the handler method.648649Parameters650----------651sql : str652SQL statement to execute653654Returns655-------656DummySQLResult657658"""659if type(self)._preview:660warnings.warn(661'This is a preview Fusion SQL command. '662'The options and syntax may change in the future.',663PreviewFeatureWarning, stacklevel=2,664)665666type(self).compile()667self._handled = set()668try:669params = self.visit(type(self).grammar.parse(sql))670for k, v in params.items():671params[k] = self.validate_rule(k, v)672673res = self.run(params)674675self._handled = set()676677if res is not None:678res.format_results(self.connection)679return res680681res = result.FusionSQLResult()682res.set_rows([])683res.format_results(self.connection)684return res685686except ParseError as exc:687s = str(exc)688msg = ''689m = re.search(r'(The non-matching portion.*$)', s)690if m:691msg = ' ' + m.group(1)692m = re.search(r"(Rule) '.+?'( didn't match at.*$)", s)693if m:694msg = ' ' + m.group(1) + m.group(2)695raise ValueError(696f'Could not parse statement.{msg} '697'Expecting:\n' + textwrap.indent(type(self).syntax, ' '),698)699700@abc.abstractmethod701def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:702"""703Run the handler command.704705Parameters706----------707params : Dict[str, Any]708Values parsed from the SQL query. Each rule in the grammar709results in a key/value pair in the ``params` dictionary.710711Returns712-------713SQLResult - tuple containing the column definitions and714rows of data in the result715716"""717raise NotImplementedError718719def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:720"""Quoted strings."""721if node is None:722return None723return flatten(visited_children)[0]724725def visit_compound(self, node: Node, visited_children: Iterable[Any]) -> Any:726"""Compound name."""727print(visited_children)728return flatten(visited_children)[0]729730def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:731"""Numeric value."""732return float(flatten(visited_children)[0])733734def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:735"""Integer value."""736return int(flatten(visited_children)[0])737738def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:739"""Whitespace and comments."""740return741742def visit_eq(self, node: Node, visited_children: Iterable[Any]) -> Any:743"""Equals sign."""744return745746def visit_comma(self, node: Node, visited_children: Iterable[Any]) -> Any:747"""Single comma."""748return749750def visit_open_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:751"""Open parenthesis."""752return753754def visit_close_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:755"""Close parenthesis."""756return757758def visit_open_repeats(self, node: Node, visited_children: Iterable[Any]) -> Any:759"""Open repeat grouping."""760return761762def visit_close_repeats(self, node: Node, visited_children: Iterable[Any]) -> Any:763"""Close repeat grouping."""764return765766def visit_init(self, node: Node, visited_children: Iterable[Any]) -> Any:767"""Entry point of the grammar."""768_, out, *_ = visited_children769return out770771def visit_statement(self, node: Node, visited_children: Iterable[Any]) -> Any:772out = ' '.join(flatten(visited_children)).strip()773return {'statement': out}774775def visit_order_by(self, node: Node, visited_children: Iterable[Any]) -> Any:776"""Handle ORDER BY."""777by = []778ascending = []779data = [x for x in flatten(visited_children)[2:] if x]780for item in data:781value = item.popitem()[-1]782if not isinstance(value, list):783value = [value]784value.append('A')785by.append(value[0])786ascending.append(value[1].upper().startswith('A'))787return {'order_by': {'by': by, 'ascending': ascending}}788789def _delimited(self, node: Node, children: Iterable[Any]) -> Any:790children = list(children)791items = [children[0]]792items.extend(item for _, item in children[1])793return items794795def _atomic(self, node: Node, children: Iterable[Any]) -> Any:796return list(children)[0]797798# visitors799visit_json_value = _atomic800visit_json_members = visit_json_items = _delimited801802def visit_json_object(self, node: Node, children: Iterable[Any]) -> Any:803_, members, _ = children804if isinstance(members, list):805members = members[0]806else:807members = []808members = [x for x in members if x != '']809return dict(members)810811def visit_json_array(self, node: Node, children: Iterable[Any]) -> Any:812_, values, _ = children813if isinstance(values, list):814values = values[0]815else:816values = []817return values818819def visit_json_mapping(self, node: Node, children: Iterable[Any]) -> Any:820key, _, value = children821return key, value822823def visit_json_string(self, node: Node, children: Iterable[Any]) -> Any:824return json_unescape(node.text)825826def visit_json_number(self, node: Node, children: Iterable[Any]) -> Any:827if '.' in node.text:828return float(node.text)829return int(node.text)830831def visit_json_true_val(self, node: Node, children: Iterable[Any]) -> Any:832return True833834def visit_json_false_val(self, node: Node, children: Iterable[Any]) -> Any:835return False836837def visit_json_null_val(self, node: Node, children: Iterable[Any]) -> Any:838return None839840def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:841"""842Handle all undefined rules.843844This method processes all user-defined rules. Each rule results in845a dictionary with a single key corresponding to the rule name, with846a value corresponding to the data value following the rule keywords.847848If no value exists, the value True is used. If the rule is not a849rule with possible repeated values, a single value is used. If the850rule can have repeated values, a list of values is returned.851852"""853if node.expr_name.startswith('json'):854return visited_children or node.text855856# Call a grammar rule857if node.expr_name in type(self).rule_info:858n_keywords = type(self).rule_info[node.expr_name]['n_keywords']859repeats = type(self).rule_info[node.expr_name]['repeats']860861# If this is the top-level command, create the final result862if node.expr_name.endswith('_cmd'):863final = merge_dicts(flatten(visited_children)[n_keywords:])864for k, v in type(self).rule_info.items():865if k.endswith('_cmd') or k.endswith('_') or k.startswith('_'):866continue867if k not in final and k not in self._handled:868final[k] = BUILTIN_DEFAULTS.get(k, v['default'])869return final870871# Filter out stray empty strings872out = [x for x in flatten(visited_children)[n_keywords:] if x]873874# Remove underscore prefixes from rule name875key_name = re.sub(r'^_+', r'', node.expr_name)876877if repeats or len(out) > 1:878self._handled.add(node.expr_name)879# If all outputs are dicts, merge them880if len(out) > 1 and not repeats:881is_dicts = [x for x in out if isinstance(x, dict)]882if len(is_dicts) == len(out):883return {key_name: merge_dicts(out)}884return {key_name: out}885886self._handled.add(node.expr_name)887return {key_name: out[0] if out else True}888889if hasattr(node, 'match'):890if not visited_children and not node.match.groups():891return node.text892return visited_children or list(node.match.groups())893894return visited_children or node.text895896def validate_rule(self, rule: str, value: Any) -> Any:897"""898Validate the value of the given rule.899900Paraemeters901-----------902rule : str903Name of the grammar rule the value belongs to904value : Any905Value parsed from the query906907Returns908-------909Any - result of the validator function910911"""912validator = type(self).validators.get(rule)913if validator is not None:914return validator(value)915return value916917918