singlestoredb 0.9.0__cp36-abi3-win_amd64.whl → 0.9.2__cp36-abi3-win_amd64.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.

_singlestoredb_accel.pyd CHANGED
Binary file
singlestoredb/__init__.py CHANGED
@@ -13,7 +13,7 @@ Examples
13
13
 
14
14
  """
15
15
 
16
- __version__ = '0.9.0'
16
+ __version__ = '0.9.2'
17
17
 
18
18
  from .alchemy import create_engine
19
19
  from .config import options, get_option, set_option, describe_option
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ import importlib
3
+ import os
4
+
5
+ from .registry import execute
6
+ from .registry import get_handler
7
+
8
+ # Load all files in handlers directory
9
+ for f in os.listdir(os.path.join(os.path.dirname(__file__), 'handlers')):
10
+ if f.endswith('.py') and not f.startswith('_'):
11
+ importlib.import_module(f'singlestoredb.fusion.handlers.{f[:-3]}')
@@ -0,0 +1,542 @@
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*)*"
24
+ qs = ~r"\"([^\"]*)\"|'([^\']*)'"
25
+ number = ~r"-?\d+(\.\d+)?|-?\.d+"
26
+ integer = ~r"-?\d+"
27
+ comma = ws "," ws
28
+ open_paren = ws "(" ws
29
+ close_paren = ws ")" ws
30
+ '''
31
+
32
+
33
+ def get_keywords(grammar: str) -> Tuple[str, ...]:
34
+ """Return all all-caps words from the beginning of the line."""
35
+ m = re.match(r'^\s*([A-Z0-9_]+(\s+|$))+', grammar)
36
+ if not m:
37
+ return tuple()
38
+ return tuple(re.split(r'\s+', m.group(0).strip()))
39
+
40
+
41
+ def process_optional(m: Any) -> str:
42
+ """Create options or groups of options."""
43
+ sql = m.group(1).strip()
44
+ if '|' in sql:
45
+ return f'( {sql} )*'
46
+ return f'( {sql} )?'
47
+
48
+
49
+ def process_alternates(m: Any) -> str:
50
+ """Make alternates mandatory groups."""
51
+ sql = m.group(1).strip()
52
+ if '|' in sql:
53
+ return f'( {sql} )'
54
+ raise ValueError(f'alternates must contain "|": {sql}')
55
+
56
+
57
+ def process_repeats(m: Any) -> str:
58
+ """Add repeated patterns."""
59
+ sql = m.group(1).strip()
60
+ return f'open_paren? {sql} ws ( comma {sql} ws )* close_paren?'
61
+
62
+
63
+ def lower_and_regex(m: Any) -> str:
64
+ """Lowercase and convert literal to regex."""
65
+ sql = m.group(1)
66
+ return f'~"{sql.lower()}"i'
67
+
68
+
69
+ def split_unions(grammar: str) -> str:
70
+ """
71
+ Convert grammar in the form '[ x ] [ y ]' to '[ x | y ]'.
72
+
73
+ Parameters
74
+ ----------
75
+ grammar : str
76
+ SQL grammar
77
+
78
+ Returns
79
+ -------
80
+ str
81
+
82
+ """
83
+ in_alternate = False
84
+ out = []
85
+ for c in grammar:
86
+ if c == '{':
87
+ in_alternate = True
88
+ out.append(c)
89
+ elif c == '}':
90
+ in_alternate = False
91
+ out.append(c)
92
+ elif not in_alternate and c == '|':
93
+ out.append(']')
94
+ out.append(' ')
95
+ out.append('[')
96
+ else:
97
+ out.append(c)
98
+ return ''.join(out)
99
+
100
+
101
+ def expand_rules(rules: Dict[str, str], m: Any) -> str:
102
+ """
103
+ Return expanded grammar syntax for given rule.
104
+
105
+ Parameters
106
+ ----------
107
+ ops : Dict[str, str]
108
+ Dictionary of rules in grammar
109
+
110
+ Returns
111
+ -------
112
+ str
113
+
114
+ """
115
+ txt = m.group(1)
116
+ if txt in rules:
117
+ return f' {rules[txt]} '
118
+ return f' <{txt}> '
119
+
120
+
121
+ def build_cmd(grammar: str) -> str:
122
+ """Pre-process grammar to construct top-level command."""
123
+ if ';' not in grammar:
124
+ raise ValueError('a semi-colon exist at the end of the primary rule')
125
+
126
+ # Pre-space
127
+ m = re.match(r'^\s*', grammar)
128
+ space = m.group(0) if m else ''
129
+
130
+ # Split on ';' on a line by itself
131
+ begin, end = grammar.split(';', 1)
132
+
133
+ # Get statement keywords
134
+ keywords = get_keywords(begin)
135
+ cmd = '_'.join(x.lower() for x in keywords) + '_cmd'
136
+
137
+ # Collapse multi-line to one
138
+ begin = re.sub(r'\s+', r' ', begin)
139
+
140
+ return f'{space}{cmd} ={begin}\n{end}'
141
+
142
+
143
+ def build_help(grammar: str) -> str:
144
+ """Construct full help syntax."""
145
+ if ';' not in grammar:
146
+ raise ValueError('a semi-colon exist at the end of the primary rule')
147
+
148
+ # Split on ';' on a line by itself
149
+ cmd, end = grammar.split(';', 1)
150
+
151
+ rules = {}
152
+ for line in end.split('\n'):
153
+ line = line.strip()
154
+ if not line:
155
+ continue
156
+ name, value = line.split('=', 1)
157
+ name = name.strip()
158
+ value = value.strip()
159
+ rules[name] = value
160
+
161
+ while re.search(r' [a-z0-9_]+ ', cmd):
162
+ cmd = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), cmd)
163
+
164
+ return textwrap.dedent(cmd).rstrip() + ';'
165
+
166
+
167
+ def strip_comments(grammar: str) -> str:
168
+ """Strip comments from grammar."""
169
+ return re.sub(r'^\s*#.*$', r'', grammar, flags=re.M)
170
+
171
+
172
+ def get_rule_info(grammar: str) -> Dict[str, Any]:
173
+ """Compute metadata about rule used in coallescing parsed output."""
174
+ return dict(
175
+ n_keywords=len(get_keywords(grammar)),
176
+ repeats=',...' in grammar,
177
+ )
178
+
179
+
180
+ def process_grammar(grammar: str) -> Tuple[Grammar, Tuple[str, ...], Dict[str, Any], str]:
181
+ """
182
+ Convert SQL grammar to a Parsimonious grammar.
183
+
184
+ Parameters
185
+ ----------
186
+ grammar : str
187
+ The SQL grammar
188
+
189
+ Returns
190
+ -------
191
+ (Grammar, Tuple[str, ...], Dict[str, Any], str) - Grammar is the parsimonious
192
+ grammar object. The tuple is a series of the keywords that start the command.
193
+ The dictionary is a set of metadata about each rule. The final string is
194
+ a human-readable version of the grammar for documentation and errors.
195
+
196
+ """
197
+ out = []
198
+ rules = {}
199
+ rule_info = {}
200
+
201
+ grammar = strip_comments(grammar)
202
+ command_key = get_keywords(grammar)
203
+ help_txt = build_help(grammar)
204
+ grammar = build_cmd(grammar)
205
+
206
+ # Make sure grouping characters all have whitespace around them
207
+ grammar = re.sub(r' *(\[|\{|\||\}|\]) *', r' \1 ', grammar)
208
+
209
+ for line in grammar.split('\n'):
210
+ if not line.strip():
211
+ continue
212
+
213
+ op, sql = line.split('=', 1)
214
+ op = op.strip()
215
+ sql = sql.strip()
216
+ sql = split_unions(sql)
217
+
218
+ rules[op] = sql
219
+ rule_info[op] = get_rule_info(sql)
220
+
221
+ # Convert consecutive optionals to a union
222
+ sql = re.sub(r'\]\s+\[', r' | ', sql)
223
+
224
+ # Lower-case keywords and make them case-insensitive
225
+ sql = re.sub(r'\b([A-Z0-9]+)\b', lower_and_regex, sql)
226
+
227
+ # Convert literal strings to 'qs'
228
+ sql = re.sub(r"'[^']+'", r'qs', sql)
229
+
230
+ # Convert [...] groups to (...)*
231
+ sql = re.sub(r'\[([^\]]+)\]', process_optional, sql)
232
+
233
+ # Convert {...} groups to (...)
234
+ sql = re.sub(r'\{([^\}]+)\}', process_alternates, sql)
235
+
236
+ # Convert <...> to ... (<...> is the form for core types)
237
+ sql = re.sub(r'<([a-z0-9_]+)>', r'\1', sql)
238
+
239
+ # Insert ws between every token to allow for whitespace and comments
240
+ sql = ' ws '.join(re.split(r'\s+', sql)) + ' ws'
241
+
242
+ # Remove ws in optional groupings
243
+ sql = sql.replace('( ws', '(')
244
+ sql = sql.replace('| ws', '|')
245
+
246
+ # Convert | to /
247
+ sql = sql.replace('|', '/')
248
+
249
+ # Remove ws after operation names, all operations contain ws at the end
250
+ sql = re.sub(r'(\s+[a-z0-9_]+)\s+ws\b', r'\1', sql)
251
+
252
+ # Convert foo,... to foo ("," foo)*
253
+ sql = re.sub(r'(\S+),...', process_repeats, sql)
254
+
255
+ # Remove ws before / and )
256
+ sql = re.sub(r'(\s*\S+\s+)ws\s+/', r'\1/', sql)
257
+ sql = re.sub(r'(\s*\S+\s+)ws\s+\)', r'\1)', sql)
258
+
259
+ # Make sure every operation ends with ws
260
+ sql = re.sub(r'\s+ws\s+ws$', r' ws', sql + ' ws')
261
+
262
+ out.append(f'{op} = {sql}')
263
+
264
+ for k, v in list(rules.items()):
265
+ while re.search(r' ([a-z0-9_]+) ', v):
266
+ v = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), v)
267
+ rules[k] = v
268
+
269
+ for k, v in list(rules.items()):
270
+ while re.search(r' <([a-z0-9_]+)> ', v):
271
+ v = re.sub(r' <([a-z0-9_]+)> ', r' \1 ', v)
272
+ rules[k] = v
273
+
274
+ cmds = ' / '.join(x for x in rules if x.endswith('_cmd'))
275
+ cmds = f'init = ws ( {cmds} ) ws ";"? ws\n'
276
+
277
+ return Grammar(cmds + CORE_GRAMMAR + '\n'.join(out)), command_key, rule_info, help_txt
278
+
279
+
280
+ def flatten(items: Iterable[Any]) -> List[Any]:
281
+ """Flatten a list of iterables."""
282
+ out = []
283
+ for x in items:
284
+ if isinstance(x, (str, bytes, dict)):
285
+ out.append(x)
286
+ elif isinstance(x, Iterable):
287
+ for sub_x in flatten(x):
288
+ if sub_x is not None:
289
+ out.append(sub_x)
290
+ elif x is not None:
291
+ out.append(x)
292
+ return out
293
+
294
+
295
+ def merge_dicts(items: List[Dict[str, Any]]) -> Dict[str, Any]:
296
+ """Merge list of dictionaries together."""
297
+ out: Dict[str, Any] = {}
298
+ for x in items:
299
+ if isinstance(x, dict):
300
+ same = list(set(x.keys()).intersection(set(out.keys())))
301
+ if same:
302
+ raise ValueError(f"found duplicate rules for '{same[0]}'")
303
+ out.update(x)
304
+ return out
305
+
306
+
307
+ class SQLHandler(NodeVisitor):
308
+ """Base class for all SQL handler classes."""
309
+
310
+ #: Parsimonious grammar object
311
+ grammar: Grammar = Grammar(CORE_GRAMMAR)
312
+
313
+ #: SQL keywords that start the command
314
+ command_key: Tuple[str, ...] = ()
315
+
316
+ #: Metadata about the parse rules
317
+ rule_info: Dict[str, Any] = {}
318
+
319
+ #: Help string for use in error messages
320
+ help: str = ''
321
+
322
+ #: Rule validation functions
323
+ validators: Dict[str, Callable[..., Any]] = {}
324
+
325
+ _is_compiled: bool = False
326
+
327
+ def __init__(self, connection: Connection):
328
+ self.connection = connection
329
+
330
+ @classmethod
331
+ def compile(cls, grammar: str = '') -> None:
332
+ """
333
+ Compile the grammar held in the docstring.
334
+
335
+ This method modifies attributes on the class: ``grammar``,
336
+ ``command_key``, ``rule_info``, and ``help``.
337
+
338
+ Parameters
339
+ ----------
340
+ grammar : str, optional
341
+ Grammar to use instead of docstring
342
+
343
+ """
344
+ if cls._is_compiled:
345
+ return
346
+
347
+ cls.grammar, cls.command_key, cls.rule_info, cls.help = \
348
+ process_grammar(grammar or cls.__doc__ or '')
349
+
350
+ cls._is_compiled = True
351
+
352
+ def create_result(self) -> result.FusionSQLResult:
353
+ """Return a new result object."""
354
+ return result.FusionSQLResult(self.connection)
355
+
356
+ @classmethod
357
+ def register(cls, overwrite: bool = False) -> None:
358
+ """
359
+ Register the handler class.
360
+
361
+ Paraemeters
362
+ -----------
363
+ overwrite : bool, optional
364
+ Overwrite an existing command with the same name?
365
+
366
+ """
367
+ from . import registry
368
+ cls.compile()
369
+ registry.register_handler(cls, overwrite=overwrite)
370
+
371
+ def execute(self, sql: str) -> result.FusionSQLResult:
372
+ """
373
+ Parse the SQL and invoke the handler method.
374
+
375
+ Parameters
376
+ ----------
377
+ sql : str
378
+ SQL statement to execute
379
+
380
+ Returns
381
+ -------
382
+ DummySQLResult
383
+
384
+ """
385
+ type(self).compile()
386
+ try:
387
+ res = self.run(self.visit(type(self).grammar.parse(sql)))
388
+ if res is not None:
389
+ return res
390
+ return result.FusionSQLResult(self.connection)
391
+ except ParseError as exc:
392
+ s = str(exc)
393
+ msg = ''
394
+ m = re.search(r'(The non-matching portion.*$)', s)
395
+ if m:
396
+ msg = ' ' + m.group(1)
397
+ m = re.search(r"(Rule) '.+?'( didn't match at.*$)", s)
398
+ if m:
399
+ msg = ' ' + m.group(1) + m.group(2)
400
+ raise ValueError(
401
+ f'Could not parse statement.{msg} '
402
+ 'Expecting:\n' + textwrap.indent(type(self).help, ' '),
403
+ )
404
+
405
+ @abc.abstractmethod
406
+ def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
407
+ """
408
+ Run the handler command.
409
+
410
+ Parameters
411
+ ----------
412
+ params : Dict[str, Any]
413
+ Values parsed from the SQL query. Each rule in the grammar
414
+ results in a key/value pair in the ``params` dictionary.
415
+
416
+ Returns
417
+ -------
418
+ SQLResult - tuple containing the column definitions and
419
+ rows of data in the result
420
+
421
+ """
422
+ raise NotImplementedError
423
+
424
+ def create_like_func(self, like: str) -> Callable[[str], bool]:
425
+ """
426
+ Construct a function to apply the LIKE clause.
427
+
428
+ Calling the resulting function will return a boolean indicating
429
+ whether the given string matched the ``like`` pattern.
430
+
431
+ Parameters
432
+ ----------
433
+ like : str
434
+ A LIKE pattern (i.e., string with '%' as a wildcard)
435
+
436
+ Returns
437
+ -------
438
+ function
439
+
440
+ """
441
+ if like is None:
442
+ def is_like(x: Any) -> bool:
443
+ return True
444
+ else:
445
+ regex = re.compile(
446
+ '^{}$'.format(
447
+ re.sub(r'\\%', r'.*', re.sub(r'([^\w])', r'\\\1', like)),
448
+ ), flags=re.I,
449
+ )
450
+
451
+ def is_like(x: Any) -> bool:
452
+ return bool(regex.match(x))
453
+
454
+ return is_like
455
+
456
+ def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:
457
+ """Quoted strings."""
458
+ if node is None:
459
+ return None
460
+ return node.match.group(1) or node.match.group(2)
461
+
462
+ def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:
463
+ """Whitespace and comments."""
464
+ return
465
+
466
+ def visit_comma(self, node: Node, visited_children: Iterable[Any]) -> Any:
467
+ """Single comma."""
468
+ return
469
+
470
+ def visit_open_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:
471
+ """Open parenthesis."""
472
+ return
473
+
474
+ def visit_close_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:
475
+ """Close parenthesis."""
476
+ return
477
+
478
+ def visit_init(self, node: Node, visited_children: Iterable[Any]) -> Any:
479
+ """Entry point of the grammar."""
480
+ _, out, *_ = visited_children
481
+ return out
482
+
483
+ def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:
484
+ """
485
+ Handle all undefined rules.
486
+
487
+ This method processes all user-defined rules. Each rule results in
488
+ a dictionary with a single key corresponding to the rule name, with
489
+ a value corresponding to the data value following the rule keywords.
490
+
491
+ If no value exists, the value True is used. If the rule is not a
492
+ rule with possible repeated values, a single value is used. If the
493
+ rule can have repeated values, a list of values is returned.
494
+
495
+ """
496
+ # Call a grammar rule
497
+ if node.expr_name in type(self).rule_info:
498
+ n_keywords = type(self).rule_info[node.expr_name]['n_keywords']
499
+ repeats = type(self).rule_info[node.expr_name]['repeats']
500
+
501
+ # If this is the top-level command, create the final result
502
+ if node.expr_name.endswith('_cmd'):
503
+ return merge_dicts(flatten(visited_children)[n_keywords:])
504
+
505
+ # Filter out stray empty strings
506
+ out = [x for x in flatten(visited_children)[n_keywords:] if x]
507
+
508
+ if repeats:
509
+ return {node.expr_name: self.validate_rule(node.expr_name, out)}
510
+
511
+ return {
512
+ node.expr_name:
513
+ self.validate_rule(node.expr_name, out[0]) if out else True,
514
+ }
515
+
516
+ if hasattr(node, 'match'):
517
+ if not visited_children and not node.match.groups():
518
+ return node.text
519
+ return visited_children or list(node.match.groups())
520
+
521
+ return visited_children or node.text
522
+
523
+ def validate_rule(self, rule: str, value: Any) -> Any:
524
+ """
525
+ Validate the value of the given rule.
526
+
527
+ Paraemeters
528
+ -----------
529
+ rule : str
530
+ Name of the grammar rule the value belongs to
531
+ value : Any
532
+ Value parsed from the query
533
+
534
+ Returns
535
+ -------
536
+ Any - result of the validator function
537
+
538
+ """
539
+ validator = type(self).validators.get(rule)
540
+ if validator is not None:
541
+ return validator(value)
542
+ return value
File without changes