11
11
from typing import Iterable
12
12
from typing import List
13
13
from typing import Optional
14
+ from typing import Set
14
15
from typing import Tuple
15
16
16
17
from parsimonious import Grammar
23
24
24
25
CORE_GRAMMAR = r'''
25
26
ws = ~r"(\s+|(\s*/\*.*\*/\s*)+)"
26
- qs = ~r"\"([^\"]*)\"|'([^\']*)'|`([^\`]*)`|([ A-Za-z0-9_\-\.]+)"
27
- number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i
28
- integer = ~r"-?\d+"
27
+ qs = ~r"\"([^\"]*)\"|'([^\']*)'|([ A-Za-z0-9_\-\.]+)|`([^\`]+)`" ws*
28
+ number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i ws*
29
+ integer = ~r"-?\d+" ws*
29
30
comma = ws* "," ws*
30
31
eq = ws* "=" ws*
31
32
open_paren = ws* "(" ws*
32
33
close_paren = ws* ")" ws*
33
34
open_repeats = ws* ~r"[\(\[\{]" ws*
34
35
close_repeats = ws* ~r"[\)\]\}]" ws*
35
36
select = ~r"SELECT"i ws+ ~r".+" ws*
37
+ table = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
38
+ column = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
39
+ link_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
40
+ catalog_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
36
41
37
42
json = ws* json_object ws*
38
43
json_object = ~r"{\s*" json_members? ~r"\s*}"
65
70
'<integer>' : '' ,
66
71
'<number>' : '' ,
67
72
'<json>' : '' ,
73
+ '<table>' : '' ,
74
+ '<column>' : '' ,
75
+ '<catalog-name>' : '' ,
76
+ '<link-name>' : '' ,
68
77
}
69
78
70
79
BUILTIN_DEFAULTS = { # type: ignore
@@ -226,9 +235,13 @@ def build_syntax(grammar: str) -> str:
226
235
# Split on ';' on a line by itself
227
236
cmd , end = grammar .split (';' , 1 )
228
237
229
- rules = {}
238
+ name = ''
239
+ rules : Dict [str , Any ] = {}
230
240
for line in end .split ('\n ' ):
231
241
line = line .strip ()
242
+ if line .startswith ('&' ):
243
+ rules [name ] += '\n ' + line
244
+ continue
232
245
if not line :
233
246
continue
234
247
name , value = line .split ('=' , 1 )
@@ -239,10 +252,16 @@ def build_syntax(grammar: str) -> str:
239
252
while re .search (r' [a-z0-9_]+\b' , cmd ):
240
253
cmd = re .sub (r' ([a-z0-9_]+)\b' , functools .partial (expand_rules , rules ), cmd )
241
254
255
+ def add_indent (m : Any ) -> str :
256
+ return ' ' + (len (m .group (1 )) * ' ' )
257
+
258
+ # Indent line-continuations
259
+ cmd = re .sub (r'^(\&+)\s*' , add_indent , cmd , flags = re .M )
260
+
242
261
cmd = textwrap .dedent (cmd ).rstrip () + ';'
243
- cmd = re .sub (r' +' , ' ' , cmd )
244
- cmd = re .sub (r'^ ' , ' ' , cmd , flags = re . M )
245
- cmd = re .sub (r'\s+,\.\.\.' , ',...' , cmd )
262
+ cmd = re .sub (r'(\S) +' , r'\1 ' , cmd )
263
+ cmd = re .sub (r'<comma> ' , ', ' , cmd )
264
+ cmd = re .sub (r'\s+,\s*\ .\.\.' , ',...' , cmd )
246
265
247
266
return cmd
248
267
@@ -399,9 +418,15 @@ def process_grammar(
399
418
help_txt = build_help (syntax_txt , full_grammar )
400
419
grammar = build_cmd (grammar )
401
420
421
+ # Remove line-continuations
422
+ grammar = re .sub (r'\n\s*&+' , r'' , grammar )
423
+
402
424
# Make sure grouping characters all have whitespace around them
403
425
grammar = re .sub (r' *(\[|\{|\||\}|\]) *' , r' \1 ' , grammar )
404
426
427
+ grammar = re .sub (r'\(' , r' open_paren ' , grammar )
428
+ grammar = re .sub (r'\)' , r' close_paren ' , grammar )
429
+
405
430
for line in grammar .split ('\n ' ):
406
431
if not line .strip ():
407
432
continue
@@ -418,7 +443,7 @@ def process_grammar(
418
443
sql = re .sub (r'\]\s+\[' , r' | ' , sql )
419
444
420
445
# Lower-case keywords and make them case-insensitive
421
- sql = re .sub (r'(\b|@+)([A-Z0-9 ]+)\b' , lower_and_regex , sql )
446
+ sql = re .sub (r'(\b|@+)([A-Z0-9_ ]+)\b' , lower_and_regex , sql )
422
447
423
448
# Convert literal strings to 'qs'
424
449
sql = re .sub (r"'[^']+'" , r'qs' , sql )
@@ -461,12 +486,18 @@ def process_grammar(
461
486
sql = re .sub (r'\s+ws$' , r' ws*' , sql )
462
487
sql = re .sub (r'\s+ws\s+\(' , r' ws* (' , sql )
463
488
sql = re .sub (r'\)\s+ws\s+' , r') ws* ' , sql )
464
- sql = re .sub (r'\s+ws\s+' , r' ws+ ' , sql )
489
+ sql = re .sub (r'\s+ws\s+' , r' ws* ' , sql )
465
490
sql = re .sub (r'\?\s+ws\+' , r'? ws*' , sql )
466
491
467
492
# Remove extra ws around eq
468
493
sql = re .sub (r'ws\+\s*eq\b' , r'eq' , sql )
469
494
495
+ # Remove optional groupings when mandatory groupings are specified
496
+ sql = re .sub (r'open_paren\s+ws\*\s+open_repeats\?' , r'open_paren' , sql )
497
+ sql = re .sub (r'close_repeats\?\s+ws\*\s+close_paren' , r'close_paren' , sql )
498
+ sql = re .sub (r'open_paren\s+open_repeats\?' , r'open_paren' , sql )
499
+ sql = re .sub (r'close_repeats\?\s+close_paren' , r'close_paren' , sql )
500
+
470
501
out .append (f'{ op } = { sql } ' )
471
502
472
503
for k , v in list (rules .items ()):
@@ -548,6 +579,7 @@ class SQLHandler(NodeVisitor):
548
579
549
580
def __init__ (self , connection : Connection ):
550
581
self .connection = connection
582
+ self ._handled : Set [str ] = set ()
551
583
552
584
@classmethod
553
585
def compile (cls , grammar : str = '' ) -> None :
@@ -614,12 +646,16 @@ def execute(self, sql: str) -> result.FusionSQLResult:
614
646
)
615
647
616
648
type (self ).compile ()
649
+ self ._handled = set ()
617
650
try :
618
651
params = self .visit (type (self ).grammar .parse (sql ))
619
652
for k , v in params .items ():
620
653
params [k ] = self .validate_rule (k , v )
621
654
622
655
res = self .run (params )
656
+
657
+ self ._handled = set ()
658
+
623
659
if res is not None :
624
660
res .format_results (self .connection )
625
661
return res
@@ -666,16 +702,20 @@ def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:
666
702
"""Quoted strings."""
667
703
if node is None :
668
704
return None
669
- return node .match .group (1 ) or node .match .group (2 ) or \
670
- node .match .group (3 ) or node .match .group (4 )
705
+ return flatten (visited_children )[0 ]
706
+
707
+ def visit_compound (self , node : Node , visited_children : Iterable [Any ]) -> Any :
708
+ """Compound name."""
709
+ print (visited_children )
710
+ return flatten (visited_children )[0 ]
671
711
672
712
def visit_number (self , node : Node , visited_children : Iterable [Any ]) -> Any :
673
713
"""Numeric value."""
674
- return float (node . match . group ( 0 ) )
714
+ return float (flatten ( visited_children )[ 0 ] )
675
715
676
716
def visit_integer (self , node : Node , visited_children : Iterable [Any ]) -> Any :
677
717
"""Integer value."""
678
- return int (node . match . group ( 0 ) )
718
+ return int (flatten ( visited_children )[ 0 ] )
679
719
680
720
def visit_ws (self , node : Node , visited_children : Iterable [Any ]) -> Any :
681
721
"""Whitespace and comments."""
@@ -804,19 +844,29 @@ def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:
804
844
if node .expr_name .endswith ('_cmd' ):
805
845
final = merge_dicts (flatten (visited_children )[n_keywords :])
806
846
for k , v in type (self ).rule_info .items ():
807
- if k .endswith ('_cmd' ) or k .endswith ('_' ):
847
+ if k .endswith ('_cmd' ) or k .endswith ('_' ) or k . startswith ( '_' ) :
808
848
continue
809
- if k not in final :
849
+ if k not in final and k not in self . _handled :
810
850
final [k ] = BUILTIN_DEFAULTS .get (k , v ['default' ])
811
851
return final
812
852
813
853
# Filter out stray empty strings
814
854
out = [x for x in flatten (visited_children )[n_keywords :] if x ]
815
855
816
- if repeats or len ( out ) > 1 :
817
- return { node .expr_name : out }
856
+ # Remove underscore prefixes from rule name
857
+ key_name = re . sub ( r'^_+' , r'' , node .expr_name )
818
858
819
- return {node .expr_name : out [0 ] if out else True }
859
+ if repeats or len (out ) > 1 :
860
+ self ._handled .add (node .expr_name )
861
+ # If all outputs are dicts, merge them
862
+ if len (out ) > 1 and not repeats :
863
+ is_dicts = [x for x in out if isinstance (x , dict )]
864
+ if len (is_dicts ) == len (out ):
865
+ return {key_name : merge_dicts (out )}
866
+ return {key_name : out }
867
+
868
+ self ._handled .add (node .expr_name )
869
+ return {key_name : out [0 ] if out else True }
820
870
821
871
if hasattr (node , 'match' ):
822
872
if not visited_children and not node .match .groups ():
0 commit comments