singlestoredb 0.4.0__py3-none-any.whl → 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of singlestoredb might be problematic. Click here for more details.

Files changed (120) hide show
  1. singlestoredb/__init__.py +33 -1
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +5 -1
  4. singlestoredb/config.py +116 -14
  5. singlestoredb/connection.py +483 -516
  6. singlestoredb/converters.py +238 -135
  7. singlestoredb/exceptions.py +30 -2
  8. singlestoredb/functions/__init__.py +1 -0
  9. singlestoredb/functions/decorator.py +142 -0
  10. singlestoredb/functions/dtypes.py +1639 -0
  11. singlestoredb/functions/ext/__init__.py +2 -0
  12. singlestoredb/functions/ext/arrow.py +375 -0
  13. singlestoredb/functions/ext/asgi.py +661 -0
  14. singlestoredb/functions/ext/json.py +427 -0
  15. singlestoredb/functions/ext/mmap.py +306 -0
  16. singlestoredb/functions/ext/rowdat_1.py +744 -0
  17. singlestoredb/functions/signature.py +673 -0
  18. singlestoredb/fusion/__init__.py +11 -0
  19. singlestoredb/fusion/graphql.py +213 -0
  20. singlestoredb/fusion/handler.py +621 -0
  21. singlestoredb/fusion/handlers/stage.py +257 -0
  22. singlestoredb/fusion/handlers/utils.py +162 -0
  23. singlestoredb/fusion/handlers/workspace.py +412 -0
  24. singlestoredb/fusion/registry.py +164 -0
  25. singlestoredb/fusion/result.py +399 -0
  26. singlestoredb/http/__init__.py +27 -0
  27. singlestoredb/{http.py → http/connection.py} +555 -154
  28. singlestoredb/management/__init__.py +3 -0
  29. singlestoredb/management/billing_usage.py +148 -0
  30. singlestoredb/management/cluster.py +14 -6
  31. singlestoredb/management/manager.py +100 -38
  32. singlestoredb/management/organization.py +188 -0
  33. singlestoredb/management/region.py +5 -5
  34. singlestoredb/management/utils.py +281 -2
  35. singlestoredb/management/workspace.py +1344 -49
  36. singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
  37. singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
  38. singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
  39. singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
  40. singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
  41. singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
  42. singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
  43. singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
  44. singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
  45. singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
  46. singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
  47. singlestoredb/mysql/converters.py +271 -0
  48. singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
  49. singlestoredb/mysql/err.py +92 -0
  50. singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
  51. singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
  52. singlestoredb/mysql/tests/__init__.py +19 -0
  53. singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
  54. singlestoredb/mysql/tests/conftest.py +37 -0
  55. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
  56. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
  57. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
  58. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
  59. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
  60. singlestoredb/mysql/tests/test_cursor.py +141 -0
  61. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
  62. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
  63. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
  64. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
  65. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
  66. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
  67. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  68. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
  69. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
  70. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
  71. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
  72. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
  73. singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
  74. singlestoredb/pytest.py +283 -0
  75. singlestoredb/tests/empty.sql +0 -0
  76. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  77. singlestoredb/tests/test.sql +210 -0
  78. singlestoredb/tests/test2.sql +1 -0
  79. singlestoredb/tests/test_basics.py +482 -115
  80. singlestoredb/tests/test_config.py +13 -13
  81. singlestoredb/tests/test_connection.py +241 -305
  82. singlestoredb/tests/test_dbapi.py +27 -0
  83. singlestoredb/tests/test_ext_func.py +1193 -0
  84. singlestoredb/tests/test_ext_func_data.py +1101 -0
  85. singlestoredb/tests/test_fusion.py +465 -0
  86. singlestoredb/tests/test_http.py +32 -26
  87. singlestoredb/tests/test_management.py +588 -8
  88. singlestoredb/tests/test_plugin.py +33 -0
  89. singlestoredb/tests/test_results.py +11 -12
  90. singlestoredb/tests/test_udf.py +687 -0
  91. singlestoredb/tests/utils.py +3 -2
  92. singlestoredb/utils/config.py +58 -0
  93. singlestoredb/utils/debug.py +13 -0
  94. singlestoredb/utils/mogrify.py +151 -0
  95. singlestoredb/utils/results.py +4 -1
  96. singlestoredb-1.0.4.dist-info/METADATA +139 -0
  97. singlestoredb-1.0.4.dist-info/RECORD +112 -0
  98. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
  99. singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
  100. singlestoredb/clients/pymysqlsv/converters.py +0 -365
  101. singlestoredb/clients/pymysqlsv/err.py +0 -144
  102. singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
  103. singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
  104. singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
  105. singlestoredb/drivers/__init__.py +0 -45
  106. singlestoredb/drivers/base.py +0 -198
  107. singlestoredb/drivers/cymysql.py +0 -38
  108. singlestoredb/drivers/http.py +0 -47
  109. singlestoredb/drivers/mariadb.py +0 -40
  110. singlestoredb/drivers/mysqlconnector.py +0 -49
  111. singlestoredb/drivers/mysqldb.py +0 -60
  112. singlestoredb/drivers/pymysql.py +0 -37
  113. singlestoredb/drivers/pymysqlsv.py +0 -35
  114. singlestoredb/drivers/pyodbc.py +0 -65
  115. singlestoredb-0.4.0.dist-info/METADATA +0 -111
  116. singlestoredb-0.4.0.dist-info/RECORD +0 -86
  117. /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
  118. /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
  119. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
  120. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,621 @@
1
+ #!/usr/bin/env python3
2
+ import abc
3
+ import functools
4
+ import re
5
+ import textwrap
6
+ from typing import Any
7
+ from typing import Callable
8
+ from typing import Dict
9
+ from typing import Iterable
10
+ from typing import List
11
+ from typing import Optional
12
+ from typing import Tuple
13
+
14
+ from parsimonious import Grammar
15
+ from parsimonious import ParseError
16
+ from parsimonious.nodes import Node
17
+ from parsimonious.nodes import NodeVisitor
18
+
19
+ from . import result
20
+ from ..connection import Connection
21
+
22
+ CORE_GRAMMAR = r'''
23
+ ws = ~r"(\s+|(\s*/\*.*\*/\s*)+)"
24
+ qs = ~r"\"([^\"]*)\"|'([^\']*)'|`([^\`]*)`|([A-Za-z0-9_\-\.]+)"
25
+ number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i
26
+ integer = ~r"-?\d+"
27
+ comma = ws* "," ws*
28
+ eq = ws* "=" ws*
29
+ open_paren = ws* "(" ws*
30
+ close_paren = ws* ")" ws*
31
+ select = ~r"SELECT"i ws+ ~r".+" ws*
32
+ '''
33
+
34
+ BUILTINS = {
35
+ '<order-by>': r'''
36
+ order_by = ORDER BY order_by_key_,...
37
+ order_by_key_ = '<key>' [ ASC | DESC ]
38
+ ''',
39
+ '<like>': r'''
40
+ like = LIKE '<pattern>'
41
+ ''',
42
+ '<extended>': r'''
43
+ extended = EXTENDED
44
+ ''',
45
+ '<limit>': r'''
46
+ limit = LIMIT <integer>
47
+ ''',
48
+ }
49
+
50
+ BUILTIN_DEFAULTS = { # type: ignore
51
+ 'order_by': {'by': []},
52
+ 'like': None,
53
+ 'extended': False,
54
+ 'limit': None,
55
+ }
56
+
57
+
58
+ def get_keywords(grammar: str) -> Tuple[str, ...]:
59
+ """Return all all-caps words from the beginning of the line."""
60
+ m = re.match(r'^\s*((?:[A-Z0-9_]+)(\s+|$|;))+', grammar)
61
+ if not m:
62
+ return tuple()
63
+ return tuple(re.split(r'\s+', m.group(0).replace(';', '').strip()))
64
+
65
+
66
+ def is_bool(grammar: str) -> bool:
67
+ """Determine if the rule is a boolean."""
68
+ return bool(re.match(r'^[A-Z0-9_\s*]+$', grammar))
69
+
70
+
71
+ def process_optional(m: Any) -> str:
72
+ """Create options or groups of options."""
73
+ sql = m.group(1).strip()
74
+ if '|' in sql:
75
+ return f'( {sql} )*'
76
+ return f'( {sql} )?'
77
+
78
+
79
+ def process_alternates(m: Any) -> str:
80
+ """Make alternates mandatory groups."""
81
+ sql = m.group(1).strip()
82
+ if '|' in sql:
83
+ return f'( {sql} )'
84
+ raise ValueError(f'alternates must contain "|": {sql}')
85
+
86
+
87
+ def process_repeats(m: Any) -> str:
88
+ """Add repeated patterns."""
89
+ sql = m.group(1).strip()
90
+ return f'open_paren? {sql} ws* ( comma {sql} ws* )* close_paren?'
91
+
92
+
93
+ def lower_and_regex(m: Any) -> str:
94
+ """Lowercase and convert literal to regex."""
95
+ sql = m.group(1)
96
+ return f'~"{sql.lower()}"i'
97
+
98
+
99
+ def split_unions(grammar: str) -> str:
100
+ """
101
+ Convert grammar in the form '[ x ] [ y ]' to '[ x | y ]'.
102
+
103
+ Parameters
104
+ ----------
105
+ grammar : str
106
+ SQL grammar
107
+
108
+ Returns
109
+ -------
110
+ str
111
+
112
+ """
113
+ in_alternate = False
114
+ out = []
115
+ for c in grammar:
116
+ if c == '{':
117
+ in_alternate = True
118
+ out.append(c)
119
+ elif c == '}':
120
+ in_alternate = False
121
+ out.append(c)
122
+ elif not in_alternate and c == '|':
123
+ out.append(']')
124
+ out.append(' ')
125
+ out.append('[')
126
+ else:
127
+ out.append(c)
128
+ return ''.join(out)
129
+
130
+
131
+ def expand_rules(rules: Dict[str, str], m: Any) -> str:
132
+ """
133
+ Return expanded grammar syntax for given rule.
134
+
135
+ Parameters
136
+ ----------
137
+ ops : Dict[str, str]
138
+ Dictionary of rules in grammar
139
+
140
+ Returns
141
+ -------
142
+ str
143
+
144
+ """
145
+ txt = m.group(1)
146
+ if txt in rules:
147
+ return f' {rules[txt]} '
148
+ return f' <{txt}> '
149
+
150
+
151
+ def build_cmd(grammar: str) -> str:
152
+ """Pre-process grammar to construct top-level command."""
153
+ if ';' not in grammar:
154
+ raise ValueError('a semi-colon exist at the end of the primary rule')
155
+
156
+ # Pre-space
157
+ m = re.match(r'^\s*', grammar)
158
+ space = m.group(0) if m else ''
159
+
160
+ # Split on ';' on a line by itself
161
+ begin, end = grammar.split(';', 1)
162
+
163
+ # Get statement keywords
164
+ keywords = get_keywords(begin)
165
+ cmd = '_'.join(x.lower() for x in keywords) + '_cmd'
166
+
167
+ # Collapse multi-line to one
168
+ begin = re.sub(r'\s+', r' ', begin)
169
+
170
+ return f'{space}{cmd} ={begin}\n{end}'
171
+
172
+
173
+ def build_help(grammar: str) -> str:
174
+ """Construct full help syntax."""
175
+ if ';' not in grammar:
176
+ raise ValueError('a semi-colon exist at the end of the primary rule')
177
+
178
+ # Split on ';' on a line by itself
179
+ cmd, end = grammar.split(';', 1)
180
+
181
+ rules = {}
182
+ for line in end.split('\n'):
183
+ line = line.strip()
184
+ if not line:
185
+ continue
186
+ name, value = line.split('=', 1)
187
+ name = name.strip()
188
+ value = value.strip()
189
+ rules[name] = value
190
+
191
+ while re.search(r' [a-z0-9_]+\b', cmd):
192
+ cmd = re.sub(r' ([a-z0-9_]+)\b', functools.partial(expand_rules, rules), cmd)
193
+
194
+ cmd = textwrap.dedent(cmd).rstrip() + ';'
195
+ cmd = re.sub(r' +', ' ', cmd)
196
+ cmd = re.sub(r'^ ', ' ', cmd, flags=re.M)
197
+ cmd = re.sub(r'\s+,\.\.\.', ',...', cmd)
198
+
199
+ return cmd
200
+
201
+
202
+ def strip_comments(grammar: str) -> str:
203
+ """Strip comments from grammar."""
204
+ return re.sub(r'^\s*#.*$', r'', grammar, flags=re.M)
205
+
206
+
207
+ def get_rule_info(grammar: str) -> Dict[str, Any]:
208
+ """Compute metadata about rule used in coallescing parsed output."""
209
+ return dict(
210
+ n_keywords=len(get_keywords(grammar)),
211
+ repeats=',...' in grammar,
212
+ default=False if is_bool(grammar) else [] if ',...' in grammar else None,
213
+ )
214
+
215
+
216
+ def inject_builtins(grammar: str) -> str:
217
+ """Inject complex builtin rules."""
218
+ for k, v in BUILTINS.items():
219
+ if re.search(k, grammar):
220
+ grammar = re.sub(
221
+ k,
222
+ k.replace('<', '').replace('>', '').replace('-', '_'),
223
+ grammar,
224
+ )
225
+ grammar += v
226
+ return grammar
227
+
228
+
229
+ def process_grammar(grammar: str) -> Tuple[Grammar, Tuple[str, ...], Dict[str, Any], str]:
230
+ """
231
+ Convert SQL grammar to a Parsimonious grammar.
232
+
233
+ Parameters
234
+ ----------
235
+ grammar : str
236
+ The SQL grammar
237
+
238
+ Returns
239
+ -------
240
+ (Grammar, Tuple[str, ...], Dict[str, Any], str) - Grammar is the parsimonious
241
+ grammar object. The tuple is a series of the keywords that start the command.
242
+ The dictionary is a set of metadata about each rule. The final string is
243
+ a human-readable version of the grammar for documentation and errors.
244
+
245
+ """
246
+ out = []
247
+ rules = {}
248
+ rule_info = {}
249
+
250
+ grammar = strip_comments(grammar)
251
+ grammar = inject_builtins(grammar)
252
+ command_key = get_keywords(grammar)
253
+ help_txt = build_help(grammar)
254
+ grammar = build_cmd(grammar)
255
+
256
+ # Make sure grouping characters all have whitespace around them
257
+ grammar = re.sub(r' *(\[|\{|\||\}|\]) *', r' \1 ', grammar)
258
+
259
+ for line in grammar.split('\n'):
260
+ if not line.strip():
261
+ continue
262
+
263
+ op, sql = line.split('=', 1)
264
+ op = op.strip()
265
+ sql = sql.strip()
266
+ sql = split_unions(sql)
267
+
268
+ rules[op] = sql
269
+ rule_info[op] = get_rule_info(sql)
270
+
271
+ # Convert consecutive optionals to a union
272
+ sql = re.sub(r'\]\s+\[', r' | ', sql)
273
+
274
+ # Lower-case keywords and make them case-insensitive
275
+ sql = re.sub(r'\b([A-Z0-9]+)\b', lower_and_regex, sql)
276
+
277
+ # Convert literal strings to 'qs'
278
+ sql = re.sub(r"'[^']+'", r'qs', sql)
279
+
280
+ # Convert special characters to literal tokens
281
+ sql = re.sub(r'([=]) ', r' eq ', sql)
282
+
283
+ # Convert [...] groups to (...)*
284
+ sql = re.sub(r'\[([^\]]+)\]', process_optional, sql)
285
+
286
+ # Convert {...} groups to (...)
287
+ sql = re.sub(r'\{([^\}]+)\}', process_alternates, sql)
288
+
289
+ # Convert <...> to ... (<...> is the form for core types)
290
+ sql = re.sub(r'<([a-z0-9_]+)>', r'\1', sql)
291
+
292
+ # Insert ws between every token to allow for whitespace and comments
293
+ sql = ' ws '.join(re.split(r'\s+', sql)) + ' ws'
294
+
295
+ # Remove ws in optional groupings
296
+ sql = sql.replace('( ws', '(')
297
+ sql = sql.replace('| ws', '|')
298
+
299
+ # Convert | to /
300
+ sql = sql.replace('|', '/')
301
+
302
+ # Remove ws after operation names, all operations contain ws at the end
303
+ sql = re.sub(r'(\s+[a-z0-9_]+)\s+ws\b', r'\1', sql)
304
+
305
+ # Convert foo,... to foo ("," foo)*
306
+ sql = re.sub(r'(\S+),...', process_repeats, sql)
307
+
308
+ # Remove ws before / and )
309
+ sql = re.sub(r'(\s*\S+\s+)ws\s+/', r'\1/', sql)
310
+ sql = re.sub(r'(\s*\S+\s+)ws\s+\)', r'\1)', sql)
311
+
312
+ # Make sure every operation ends with ws
313
+ sql = re.sub(r'\s+ws\s+ws$', r' ws', sql + ' ws')
314
+ sql = re.sub(r'(\s+ws)*\s+ws\*$', r' ws*', sql)
315
+ sql = re.sub(r'\s+ws$', r' ws*', sql)
316
+ sql = re.sub(r'\s+ws\s+\(', r' ws* (', sql)
317
+ sql = re.sub(r'\)\s+ws\s+', r') ws* ', sql)
318
+ sql = re.sub(r'\s+ws\s+', r' ws+ ', sql)
319
+ sql = re.sub(r'\?\s+ws\+', r'? ws*', sql)
320
+
321
+ # Remove extra ws around eq
322
+ sql = re.sub(r'ws\+\s*eq\b', r'eq', sql)
323
+
324
+ out.append(f'{op} = {sql}')
325
+
326
+ for k, v in list(rules.items()):
327
+ while re.search(r' ([a-z0-9_]+) ', v):
328
+ v = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), v)
329
+ rules[k] = v
330
+
331
+ for k, v in list(rules.items()):
332
+ while re.search(r' <([a-z0-9_]+)> ', v):
333
+ v = re.sub(r' <([a-z0-9_]+)> ', r' \1 ', v)
334
+ rules[k] = v
335
+
336
+ cmds = ' / '.join(x for x in rules if x.endswith('_cmd'))
337
+ cmds = f'init = ws* ( {cmds} ) ws* ";"? ws*\n'
338
+
339
+ return Grammar(cmds + CORE_GRAMMAR + '\n'.join(out)), command_key, rule_info, help_txt
340
+
341
+
342
+ def flatten(items: Iterable[Any]) -> List[Any]:
343
+ """Flatten a list of iterables."""
344
+ out = []
345
+ for x in items:
346
+ if isinstance(x, (str, bytes, dict)):
347
+ out.append(x)
348
+ elif isinstance(x, Iterable):
349
+ for sub_x in flatten(x):
350
+ if sub_x is not None:
351
+ out.append(sub_x)
352
+ elif x is not None:
353
+ out.append(x)
354
+ return out
355
+
356
+
357
+ def merge_dicts(items: List[Dict[str, Any]]) -> Dict[str, Any]:
358
+ """Merge list of dictionaries together."""
359
+ out: Dict[str, Any] = {}
360
+ for x in items:
361
+ if isinstance(x, dict):
362
+ same = list(set(x.keys()).intersection(set(out.keys())))
363
+ if same:
364
+ raise ValueError(f"found duplicate rules for '{same[0]}'")
365
+ out.update(x)
366
+ return out
367
+
368
+
369
+ class SQLHandler(NodeVisitor):
370
+ """Base class for all SQL handler classes."""
371
+
372
+ #: Parsimonious grammar object
373
+ grammar: Grammar = Grammar(CORE_GRAMMAR)
374
+
375
+ #: SQL keywords that start the command
376
+ command_key: Tuple[str, ...] = ()
377
+
378
+ #: Metadata about the parse rules
379
+ rule_info: Dict[str, Any] = {}
380
+
381
+ #: Help string for use in error messages
382
+ help: str = ''
383
+
384
+ #: Rule validation functions
385
+ validators: Dict[str, Callable[..., Any]] = {}
386
+
387
+ _grammar: str = CORE_GRAMMAR
388
+ _is_compiled: bool = False
389
+
390
+ def __init__(self, connection: Connection):
391
+ self.connection = connection
392
+
393
+ @classmethod
394
+ def compile(cls, grammar: str = '') -> None:
395
+ """
396
+ Compile the grammar held in the docstring.
397
+
398
+ This method modifies attributes on the class: ``grammar``,
399
+ ``command_key``, ``rule_info``, and ``help``.
400
+
401
+ Parameters
402
+ ----------
403
+ grammar : str, optional
404
+ Grammar to use instead of docstring
405
+
406
+ """
407
+ if cls._is_compiled:
408
+ return
409
+
410
+ cls.grammar, cls.command_key, cls.rule_info, cls.help = \
411
+ process_grammar(grammar or cls.__doc__ or '')
412
+
413
+ cls._grammar = grammar or cls.__doc__ or ''
414
+ cls._is_compiled = True
415
+
416
+ @classmethod
417
+ def register(cls, overwrite: bool = False) -> None:
418
+ """
419
+ Register the handler class.
420
+
421
+ Paraemeters
422
+ -----------
423
+ overwrite : bool, optional
424
+ Overwrite an existing command with the same name?
425
+
426
+ """
427
+ from . import registry
428
+ cls.compile()
429
+ registry.register_handler(cls, overwrite=overwrite)
430
+
431
+ def execute(self, sql: str) -> result.FusionSQLResult:
432
+ """
433
+ Parse the SQL and invoke the handler method.
434
+
435
+ Parameters
436
+ ----------
437
+ sql : str
438
+ SQL statement to execute
439
+
440
+ Returns
441
+ -------
442
+ DummySQLResult
443
+
444
+ """
445
+ import warnings
446
+ warnings.warn(
447
+ 'Fusion SQL is currently a preview feature. '
448
+ 'Run `SHOW FUSION COMMANDS` to see all commands.',
449
+ RuntimeWarning,
450
+ )
451
+
452
+ type(self).compile()
453
+ try:
454
+ params = self.visit(type(self).grammar.parse(sql))
455
+ for k, v in params.items():
456
+ params[k] = self.validate_rule(k, v)
457
+
458
+ res = self.run(params)
459
+ if res is not None:
460
+ res.format_results(self.connection)
461
+ return res
462
+
463
+ res = result.FusionSQLResult()
464
+ res.set_rows([])
465
+ res.format_results(self.connection)
466
+ return res
467
+
468
+ except ParseError as exc:
469
+ s = str(exc)
470
+ msg = ''
471
+ m = re.search(r'(The non-matching portion.*$)', s)
472
+ if m:
473
+ msg = ' ' + m.group(1)
474
+ m = re.search(r"(Rule) '.+?'( didn't match at.*$)", s)
475
+ if m:
476
+ msg = ' ' + m.group(1) + m.group(2)
477
+ raise ValueError(
478
+ f'Could not parse statement.{msg} '
479
+ 'Expecting:\n' + textwrap.indent(type(self).help, ' '),
480
+ )
481
+
482
+ @abc.abstractmethod
483
+ def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
484
+ """
485
+ Run the handler command.
486
+
487
+ Parameters
488
+ ----------
489
+ params : Dict[str, Any]
490
+ Values parsed from the SQL query. Each rule in the grammar
491
+ results in a key/value pair in the ``params` dictionary.
492
+
493
+ Returns
494
+ -------
495
+ SQLResult - tuple containing the column definitions and
496
+ rows of data in the result
497
+
498
+ """
499
+ raise NotImplementedError
500
+
501
+ def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:
502
+ """Quoted strings."""
503
+ if node is None:
504
+ return None
505
+ return node.match.group(1) or node.match.group(2) or \
506
+ node.match.group(3) or node.match.group(4)
507
+
508
+ def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:
509
+ """Numeric value."""
510
+ return float(node.match.group(0))
511
+
512
+ def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:
513
+ """Integer value."""
514
+ return int(node.match.group(0))
515
+
516
+ def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:
517
+ """Whitespace and comments."""
518
+ return
519
+
520
+ def visit_eq(self, node: Node, visited_children: Iterable[Any]) -> Any:
521
+ """Equals sign."""
522
+ return
523
+
524
+ def visit_comma(self, node: Node, visited_children: Iterable[Any]) -> Any:
525
+ """Single comma."""
526
+ return
527
+
528
+ def visit_open_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:
529
+ """Open parenthesis."""
530
+ return
531
+
532
+ def visit_close_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:
533
+ """Close parenthesis."""
534
+ return
535
+
536
+ def visit_init(self, node: Node, visited_children: Iterable[Any]) -> Any:
537
+ """Entry point of the grammar."""
538
+ _, out, *_ = visited_children
539
+ return out
540
+
541
+ def visit_select(self, node: Node, visited_children: Iterable[Any]) -> Any:
542
+ out = ' '.join(flatten(visited_children))
543
+ return {'select': out}
544
+
545
+ def visit_order_by(self, node: Node, visited_children: Iterable[Any]) -> Any:
546
+ """Handle ORDER BY."""
547
+ by = []
548
+ ascending = []
549
+ data = [x for x in flatten(visited_children)[2:] if x]
550
+ for item in data:
551
+ value = item.popitem()[-1]
552
+ if not isinstance(value, list):
553
+ value = [value]
554
+ value.append('A')
555
+ by.append(value[0])
556
+ ascending.append(value[1].upper().startswith('A'))
557
+ return {'order_by': {'by': by, 'ascending': ascending}}
558
+
559
+ def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:
560
+ """
561
+ Handle all undefined rules.
562
+
563
+ This method processes all user-defined rules. Each rule results in
564
+ a dictionary with a single key corresponding to the rule name, with
565
+ a value corresponding to the data value following the rule keywords.
566
+
567
+ If no value exists, the value True is used. If the rule is not a
568
+ rule with possible repeated values, a single value is used. If the
569
+ rule can have repeated values, a list of values is returned.
570
+
571
+ """
572
+ # Call a grammar rule
573
+ if node.expr_name in type(self).rule_info:
574
+ n_keywords = type(self).rule_info[node.expr_name]['n_keywords']
575
+ repeats = type(self).rule_info[node.expr_name]['repeats']
576
+
577
+ # If this is the top-level command, create the final result
578
+ if node.expr_name.endswith('_cmd'):
579
+ final = merge_dicts(flatten(visited_children)[n_keywords:])
580
+ for k, v in type(self).rule_info.items():
581
+ if k.endswith('_cmd') or k.endswith('_'):
582
+ continue
583
+ if k not in final:
584
+ final[k] = BUILTIN_DEFAULTS.get(k, v['default'])
585
+ return final
586
+
587
+ # Filter out stray empty strings
588
+ out = [x for x in flatten(visited_children)[n_keywords:] if x]
589
+
590
+ if repeats or len(out) > 1:
591
+ return {node.expr_name: out}
592
+
593
+ return {node.expr_name: out[0] if out else True}
594
+
595
+ if hasattr(node, 'match'):
596
+ if not visited_children and not node.match.groups():
597
+ return node.text
598
+ return visited_children or list(node.match.groups())
599
+
600
+ return visited_children or node.text
601
+
602
+ def validate_rule(self, rule: str, value: Any) -> Any:
603
+ """
604
+ Validate the value of the given rule.
605
+
606
+ Paraemeters
607
+ -----------
608
+ rule : str
609
+ Name of the grammar rule the value belongs to
610
+ value : Any
611
+ Value parsed from the query
612
+
613
+ Returns
614
+ -------
615
+ Any - result of the validator function
616
+
617
+ """
618
+ validator = type(self).validators.get(rule)
619
+ if validator is not None:
620
+ return validator(value)
621
+ return value