sql-blocks 0.2.6__py3-none-any.whl → 0.31.13__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.
- sql_blocks/sql_blocks.py +404 -74
- {sql_blocks-0.2.6.dist-info → sql_blocks-0.31.13.dist-info}/METADATA +100 -1
- sql_blocks-0.31.13.dist-info/RECORD +7 -0
- sql_blocks-0.2.6.dist-info/RECORD +0 -7
- {sql_blocks-0.2.6.dist-info → sql_blocks-0.31.13.dist-info}/LICENSE +0 -0
- {sql_blocks-0.2.6.dist-info → sql_blocks-0.31.13.dist-info}/WHEEL +0 -0
- {sql_blocks-0.2.6.dist-info → sql_blocks-0.31.13.dist-info}/top_level.txt +0 -0
sql_blocks/sql_blocks.py
CHANGED
@@ -7,20 +7,18 @@ PATTERN_SUFFIX = '( [A-Za-z_]+)'
|
|
7
7
|
DISTINCT_PREFX = '(DISTINCT|distinct)'
|
8
8
|
|
9
9
|
KEYWORD = {
|
10
|
-
'SELECT':
|
11
|
-
'FROM':
|
12
|
-
'WHERE':
|
13
|
-
'GROUP BY': (',{}',
|
14
|
-
'ORDER BY': (',{}',
|
15
|
-
'LIMIT':
|
16
|
-
}
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# |
|
23
|
-
# +-------- separator
|
10
|
+
'SELECT': (',{}', DISTINCT_PREFX),
|
11
|
+
'FROM': ('{}', PATTERN_SUFFIX),
|
12
|
+
'WHERE': ('{}AND ', ''),
|
13
|
+
'GROUP BY': (',{}', PATTERN_SUFFIX),
|
14
|
+
'ORDER BY': (',{}', PATTERN_SUFFIX),
|
15
|
+
'LIMIT': (' ', ''),
|
16
|
+
}
|
17
|
+
# ^ ^
|
18
|
+
# | |
|
19
|
+
# | +----- pattern to compare fields
|
20
|
+
# |
|
21
|
+
# +-------- separator
|
24
22
|
|
25
23
|
SELECT, FROM, WHERE, GROUP_BY, ORDER_BY, LIMIT = KEYWORD.keys()
|
26
24
|
USUAL_KEYS = [SELECT, WHERE, GROUP_BY, ORDER_BY, LIMIT]
|
@@ -70,6 +68,8 @@ class SQLObject:
|
|
70
68
|
|
71
69
|
def diff(self, key: str, search_list: list, exact: bool=False) -> set:
|
72
70
|
def disassemble(source: list) -> list:
|
71
|
+
if not exact:
|
72
|
+
return source
|
73
73
|
result = []
|
74
74
|
for fld in source:
|
75
75
|
result += re.split(r'([=()]|<>|\s+ON\s+|\s+on\s+)', fld)
|
@@ -89,10 +89,10 @@ class SQLObject:
|
|
89
89
|
for string in disassemble(source)
|
90
90
|
for fld in re.split(separator, string)
|
91
91
|
)
|
92
|
-
pattern = KEYWORD[key][
|
92
|
+
pattern = KEYWORD[key][1]
|
93
93
|
if exact:
|
94
94
|
if key == WHERE:
|
95
|
-
pattern = ' '
|
95
|
+
pattern = r'["\']| '
|
96
96
|
pattern += f'|{PATTERN_PREFIX}'
|
97
97
|
separator = self.get_separator(key)
|
98
98
|
s1 = field_set(search_list)
|
@@ -219,6 +219,7 @@ class ForeignKey:
|
|
219
219
|
|
220
220
|
@staticmethod
|
221
221
|
def get_key(obj1: SQLObject, obj2: SQLObject) -> tuple:
|
222
|
+
# [To-Do] including alias will allow to relate the same table twice
|
222
223
|
return obj1.table_name, obj2.table_name
|
223
224
|
|
224
225
|
def add(self, name: str, main: SQLObject):
|
@@ -418,6 +419,235 @@ class Rule:
|
|
418
419
|
def apply(cls, target: 'Select'):
|
419
420
|
...
|
420
421
|
|
422
|
+
class QueryLanguage:
|
423
|
+
pattern = '{select}{_from}{where}{group_by}{order_by}'
|
424
|
+
has_default = {key: bool(key == SELECT) for key in KEYWORD}
|
425
|
+
|
426
|
+
@staticmethod
|
427
|
+
def remove_alias(fld: str) -> str:
|
428
|
+
return ''.join(re.split(r'\w+[.]', fld))
|
429
|
+
|
430
|
+
def join_with_tabs(self, values: list, sep: str='') -> str:
|
431
|
+
sep = sep + self.TABULATION
|
432
|
+
return sep.join(v for v in values if v)
|
433
|
+
|
434
|
+
def add_field(self, values: list) -> str:
|
435
|
+
if not values:
|
436
|
+
return '*'
|
437
|
+
return self.join_with_tabs(values, ',')
|
438
|
+
|
439
|
+
def get_tables(self, values: list) -> str:
|
440
|
+
return self.join_with_tabs(values)
|
441
|
+
|
442
|
+
def extract_conditions(self, values: list) -> str:
|
443
|
+
return self.join_with_tabs(values, ' AND ')
|
444
|
+
|
445
|
+
def sort_by(self, values: list) -> str:
|
446
|
+
return self.join_with_tabs(values)
|
447
|
+
|
448
|
+
def set_group(self, values: list) -> str:
|
449
|
+
return self.join_with_tabs(values, ',')
|
450
|
+
|
451
|
+
def __init__(self, target: 'Select'):
|
452
|
+
self.KEYWORDS = [SELECT, FROM, WHERE, GROUP_BY, ORDER_BY]
|
453
|
+
self.TABULATION = '\n\t' if target.break_lines else ' '
|
454
|
+
self.LINE_BREAK = '\n' if target.break_lines else ' '
|
455
|
+
self.TOKEN_METHODS = {
|
456
|
+
SELECT: self.add_field, FROM: self.get_tables,
|
457
|
+
WHERE: self.extract_conditions,
|
458
|
+
ORDER_BY: self.sort_by, GROUP_BY: self.set_group,
|
459
|
+
}
|
460
|
+
self.result = {}
|
461
|
+
self.target = target
|
462
|
+
|
463
|
+
def pair(self, key: str) -> str:
|
464
|
+
if key == FROM:
|
465
|
+
return '_from'
|
466
|
+
return key.lower().replace(' ', '_')
|
467
|
+
|
468
|
+
def prefix(self, key: str) -> str:
|
469
|
+
return self.LINE_BREAK + key + self.TABULATION
|
470
|
+
|
471
|
+
def convert(self) -> str:
|
472
|
+
for key in self.KEYWORDS:
|
473
|
+
method = self.TOKEN_METHODS.get(key)
|
474
|
+
ref = self.pair(key)
|
475
|
+
values = self.target.values.get(key, [])
|
476
|
+
if not method or (not values and not self.has_default[key]):
|
477
|
+
self.result[ref] = ''
|
478
|
+
continue
|
479
|
+
text = method(values)
|
480
|
+
self.result[ref] = self.prefix(key) + text
|
481
|
+
return self.pattern.format(**self.result).strip()
|
482
|
+
|
483
|
+
class MongoDBLanguage(QueryLanguage):
|
484
|
+
pattern = '{_from}.{function}({where}{select}{group_by}){order_by}'
|
485
|
+
has_default = {key: False for key in KEYWORD}
|
486
|
+
LOGICAL_OP_TO_MONGO_FUNC = {
|
487
|
+
'>': '$gt', '>=': '$gte',
|
488
|
+
'<': '$lt', '<=': '$lte',
|
489
|
+
'=': '$eq', '<>': '$ne',
|
490
|
+
}
|
491
|
+
OPERATORS = '|'.join(op for op in LOGICAL_OP_TO_MONGO_FUNC)
|
492
|
+
REGEX = {
|
493
|
+
'options': re.compile(r'\s+or\s+|\s+OR\s+'),
|
494
|
+
'condition': re.compile(fr'({OPERATORS})')
|
495
|
+
}
|
496
|
+
|
497
|
+
def join_with_tabs(self, values: list, sep: str=',') -> str:
|
498
|
+
def format_field(fld):
|
499
|
+
return '{indent}{fld}'.format(
|
500
|
+
fld=self.remove_alias(fld),
|
501
|
+
indent=self.TABULATION
|
502
|
+
)
|
503
|
+
return '{begin}{content}{line_break}{end}'.format(
|
504
|
+
begin='{',
|
505
|
+
content= sep.join(
|
506
|
+
format_field(fld) for fld in values if fld
|
507
|
+
),
|
508
|
+
end='}', line_break=self.LINE_BREAK,
|
509
|
+
)
|
510
|
+
|
511
|
+
def add_field(self, values: list) -> str:
|
512
|
+
if self.result['function'] == 'aggregate':
|
513
|
+
return ''
|
514
|
+
return ',{content}'.format(
|
515
|
+
content=self.join_with_tabs([f'{fld}: 1' for fld in values]),
|
516
|
+
)
|
517
|
+
|
518
|
+
def get_tables(self, values: list) -> str:
|
519
|
+
return values[0].split()[0].lower()
|
520
|
+
|
521
|
+
@classmethod
|
522
|
+
def mongo_where_list(cls, values: list) -> list:
|
523
|
+
OR_REGEX = cls.REGEX['options']
|
524
|
+
where_list = []
|
525
|
+
for condition in values:
|
526
|
+
if OR_REGEX.findall(condition):
|
527
|
+
condition = re.sub('[()]', '', condition)
|
528
|
+
expr = '{begin}$or: [{content}]{end}'.format(
|
529
|
+
content=','.join(
|
530
|
+
cls.mongo_where_list( OR_REGEX.split(condition) )
|
531
|
+
), begin='{', end='}',
|
532
|
+
)
|
533
|
+
where_list.append(expr)
|
534
|
+
continue
|
535
|
+
tokens = cls.REGEX['condition'].split(
|
536
|
+
cls.remove_alias(condition)
|
537
|
+
)
|
538
|
+
tokens = [t.strip() for t in tokens if t]
|
539
|
+
field, *op, const = tokens
|
540
|
+
op = ''.join(op)
|
541
|
+
expr = '{begin}{op}:{const}{end}'.format(
|
542
|
+
begin='{', const=const, end='}',
|
543
|
+
op=cls.LOGICAL_OP_TO_MONGO_FUNC[op],
|
544
|
+
)
|
545
|
+
where_list.append(f'{field}:{expr}')
|
546
|
+
return where_list
|
547
|
+
|
548
|
+
def extract_conditions(self, values: list) -> str:
|
549
|
+
return self.join_with_tabs(
|
550
|
+
self.mongo_where_list(values)
|
551
|
+
)
|
552
|
+
|
553
|
+
def sort_by(self, values: list) -> str:
|
554
|
+
return ".sort({begin}{indent}{field}:{flag}{line_break}{end})".format(
|
555
|
+
begin='{', field=self.remove_alias(values[0].split()[0]),
|
556
|
+
flag=-1 if OrderBy.sort == SortType.DESC else 1,
|
557
|
+
end='}', indent=self.TABULATION, line_break=self.LINE_BREAK,
|
558
|
+
)
|
559
|
+
|
560
|
+
def set_group(self, values: list) -> str:
|
561
|
+
self.result['function'] = 'aggregate'
|
562
|
+
return '{"$group" : {_id:"$%%", count:{$sum:1}}}'.replace(
|
563
|
+
'%%', self.remove_alias( values[0] )
|
564
|
+
)
|
565
|
+
|
566
|
+
def __init__(self, target: 'Select'):
|
567
|
+
super().__init__(target)
|
568
|
+
self.result['function'] = 'find'
|
569
|
+
self.KEYWORDS = [GROUP_BY, SELECT, FROM, WHERE, ORDER_BY]
|
570
|
+
|
571
|
+
def prefix(self, key: str):
|
572
|
+
return ''
|
573
|
+
|
574
|
+
|
575
|
+
class Neo4JLanguage(QueryLanguage):
|
576
|
+
pattern = 'MATCH {_from}{where}RETURN {select}{order_by}'
|
577
|
+
has_default = {WHERE: False, FROM: False, ORDER_BY: True, SELECT: True}
|
578
|
+
|
579
|
+
def add_field(self, values: list) -> str:
|
580
|
+
if values:
|
581
|
+
return self.join_with_tabs(values, ',')
|
582
|
+
return self.TABULATION + ','.join(self.aliases.keys())
|
583
|
+
|
584
|
+
def get_tables(self, values: list) -> str:
|
585
|
+
NODE_FORMAT = dict(
|
586
|
+
left='({}:{}{})<-',
|
587
|
+
core='[{}:{}{}]',
|
588
|
+
right='->({}:{}{})'
|
589
|
+
)
|
590
|
+
nodes = {k: '' for k in NODE_FORMAT}
|
591
|
+
for txt in values:
|
592
|
+
found = re.search(
|
593
|
+
r'^(left|right)\s+', txt, re.IGNORECASE
|
594
|
+
)
|
595
|
+
pos, end, i = 'core', 0, 0
|
596
|
+
if found:
|
597
|
+
start, end = found.span()
|
598
|
+
pos = txt[start:end-1].lower()
|
599
|
+
i = 1
|
600
|
+
tokens = re.split(r'JOIN\s+|ON\s+', txt[end:])
|
601
|
+
txt = tokens[i].strip()
|
602
|
+
table_name, *alias = txt.split()
|
603
|
+
if alias:
|
604
|
+
alias = alias[0]
|
605
|
+
else:
|
606
|
+
alias = SQLObject.ALIAS_FUNC(table_name)
|
607
|
+
condition = self.aliases.get(alias, '')
|
608
|
+
if not condition:
|
609
|
+
self.aliases[alias] = ''
|
610
|
+
nodes[pos] = NODE_FORMAT[pos].format(alias, table_name, condition)
|
611
|
+
return self.TABULATION + '{left}{core}{right}'.format(**nodes)
|
612
|
+
|
613
|
+
|
614
|
+
def extract_conditions(self, values: list) -> str:
|
615
|
+
equalities = {}
|
616
|
+
where_list = []
|
617
|
+
for condition in values:
|
618
|
+
other_comparisions = any(
|
619
|
+
char in condition for char in '<>'
|
620
|
+
)
|
621
|
+
where_list.append(condition)
|
622
|
+
if '=' not in condition or other_comparisions:
|
623
|
+
continue
|
624
|
+
alias, field, const = re.split(r'[.=]', condition)
|
625
|
+
begin, end = '{', '}'
|
626
|
+
equalities[alias] = f'{begin}{field}:{const}{end}'
|
627
|
+
if len(equalities) == len(where_list):
|
628
|
+
self.aliases.update(equalities)
|
629
|
+
self.has_default[WHERE] = True
|
630
|
+
return self.LINE_BREAK
|
631
|
+
return self.join_with_tabs(where_list, ' AND ') + self.LINE_BREAK
|
632
|
+
|
633
|
+
def set_group(self, values: list) -> str:
|
634
|
+
return ''
|
635
|
+
|
636
|
+
def __init__(self, target: 'Select'):
|
637
|
+
super().__init__(target)
|
638
|
+
self.aliases = {}
|
639
|
+
self.KEYWORDS = [WHERE, FROM, ORDER_BY, SELECT]
|
640
|
+
|
641
|
+
def prefix(self, key: str):
|
642
|
+
default_prefix = any([
|
643
|
+
(key == WHERE and not self.has_default[WHERE]),
|
644
|
+
key == ORDER_BY
|
645
|
+
])
|
646
|
+
if default_prefix:
|
647
|
+
return super().prefix(key)
|
648
|
+
return ''
|
649
|
+
|
650
|
+
|
421
651
|
class Parser:
|
422
652
|
REGEX = {}
|
423
653
|
|
@@ -446,9 +676,17 @@ class Parser:
|
|
446
676
|
return ''.join(result)
|
447
677
|
|
448
678
|
def get_tokens(self, txt: str) -> list:
|
449
|
-
return
|
450
|
-
self.remove_spaces(
|
451
|
-
|
679
|
+
return [
|
680
|
+
self.remove_spaces(t)
|
681
|
+
for t in self.REGEX['separator'].split(txt)
|
682
|
+
]
|
683
|
+
|
684
|
+
|
685
|
+
class JoinType(Enum):
|
686
|
+
INNER = ''
|
687
|
+
LEFT = 'LEFT '
|
688
|
+
RIGHT = 'RIGHT '
|
689
|
+
FULL = 'FULL '
|
452
690
|
|
453
691
|
|
454
692
|
class SQLParser(Parser):
|
@@ -525,91 +763,150 @@ class SQLParser(Parser):
|
|
525
763
|
self.queries = list( result.values() )
|
526
764
|
|
527
765
|
|
528
|
-
class
|
766
|
+
class CypherParser(Parser):
|
529
767
|
REGEX = {}
|
768
|
+
CHAR_SET = r'[(,?)^{}[\]]'
|
769
|
+
KEYWORDS = '|'.join(
|
770
|
+
fr'\s+{word}\s+'
|
771
|
+
for word in "where return WHERE RETURN and AND".split()
|
772
|
+
)
|
530
773
|
|
531
774
|
def prepare(self):
|
532
|
-
self.REGEX['separator'] = re.compile(
|
775
|
+
self.REGEX['separator'] = re.compile(fr'({self.CHAR_SET}|->|<-|{self.KEYWORDS})')
|
533
776
|
self.REGEX['condition'] = re.compile(r'(^\w+)|([<>=])')
|
777
|
+
self.REGEX['alias_pos'] = re.compile(r'(\w+)[.](\w+)')
|
534
778
|
self.join_type = JoinType.INNER
|
535
779
|
self.TOKEN_METHODS = {
|
536
780
|
'(': self.add_field, '?': self.add_where,
|
537
781
|
',': self.add_field, '^': self.add_order,
|
538
|
-
')': self.new_query, '
|
539
|
-
'
|
782
|
+
')': self.new_query, '<-': self.left_ftable,
|
783
|
+
'->': self.right_ftable,
|
540
784
|
}
|
541
785
|
self.method = self.new_query
|
786
|
+
self.aliases = {}
|
542
787
|
|
543
|
-
def new_query(self, token: str):
|
544
|
-
|
545
|
-
|
788
|
+
def new_query(self, token: str, join_type = JoinType.INNER, alias: str=''):
|
789
|
+
token, *group_fields = token.split('@')
|
790
|
+
if not token.isidentifier():
|
791
|
+
return
|
792
|
+
table_name = f'{token} {alias}' if alias else token
|
793
|
+
query = self.class_type(table_name)
|
794
|
+
if not alias:
|
795
|
+
alias = query.alias
|
796
|
+
self.queries.append(query)
|
797
|
+
self.aliases[alias] = query
|
798
|
+
FieldList(group_fields, [Field, GroupBy]).add('', query)
|
799
|
+
query.join_type = join_type
|
546
800
|
|
547
801
|
def add_where(self, token: str):
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
802
|
+
elements = [t for t in self.REGEX['alias_pos'].split(token) if t]
|
803
|
+
if len(elements) == 3:
|
804
|
+
alias, field, *condition = elements
|
805
|
+
query = self.aliases[alias]
|
806
|
+
else:
|
807
|
+
field, *condition = [
|
808
|
+
t for t in self.REGEX['condition'].split(token) if t
|
809
|
+
]
|
810
|
+
query = self.queries[-1]
|
811
|
+
Where(' '.join(condition)).add(field, query)
|
552
812
|
|
553
813
|
def add_order(self, token: str):
|
554
|
-
|
814
|
+
self.add_field(token, [OrderBy])
|
555
815
|
|
556
|
-
def add_field(self, token: str):
|
557
|
-
|
816
|
+
def add_field(self, token: str, extra_classes: list['type']=[]):
|
817
|
+
if token in self.TOKEN_METHODS:
|
818
|
+
return
|
819
|
+
class_list = [Field]
|
820
|
+
if '$' in token:
|
821
|
+
func_name, token = token.split('$')
|
822
|
+
if func_name == 'count':
|
823
|
+
if not token:
|
824
|
+
token = 'count_1'
|
825
|
+
NamedField(token, Count).add('*', self.queries[-1])
|
826
|
+
class_list = []
|
827
|
+
else:
|
828
|
+
FUNCTION_CLASS = {f.__name__.lower(): f for f in Function.__subclasses__()}
|
829
|
+
class_list = [ FUNCTION_CLASS[func_name] ]
|
830
|
+
class_list += extra_classes
|
831
|
+
FieldList(token, class_list).add('', self.queries[-1])
|
558
832
|
|
559
833
|
def left_ftable(self, token: str):
|
834
|
+
if self.queries:
|
835
|
+
self.queries[-1].join_type = JoinType.LEFT
|
560
836
|
self.new_query(token)
|
561
|
-
self.join_type = JoinType.LEFT
|
562
837
|
|
563
838
|
def right_ftable(self, token: str):
|
564
|
-
self.new_query(token)
|
565
|
-
self.join_type = JoinType.RIGHT
|
839
|
+
self.new_query(token, JoinType.RIGHT)
|
566
840
|
|
567
841
|
def add_foreign_key(self, token: str, pk_field: str=''):
|
568
842
|
curr, last = [self.queries[i] for i in (-1, -2)]
|
569
843
|
if not pk_field:
|
570
844
|
if not last.values.get(SELECT):
|
571
|
-
|
845
|
+
raise IndexError(f'Primary Key not found for {last.table_name}.')
|
572
846
|
pk_field = last.values[SELECT][-1].split('.')[-1]
|
573
847
|
last.delete(pk_field, [SELECT])
|
574
|
-
if self.join_type == JoinType.RIGHT:
|
575
|
-
curr, last = last, curr
|
576
848
|
if '{}' in token:
|
577
|
-
|
849
|
+
foreign_fld = token.format(
|
850
|
+
last.table_name.lower()
|
851
|
+
if last.join_type == JoinType.LEFT else
|
578
852
|
curr.table_name.lower()
|
579
853
|
)
|
580
|
-
|
581
|
-
|
582
|
-
|
854
|
+
else:
|
855
|
+
if not curr.values.get(SELECT):
|
856
|
+
raise IndexError(f'Foreign Key not found for {curr.table_name}.')
|
857
|
+
foreign_fld = curr.values[SELECT][0].split('.')[-1]
|
858
|
+
curr.delete(foreign_fld, [SELECT])
|
859
|
+
if curr.join_type == JoinType.RIGHT:
|
860
|
+
pk_field, foreign_fld = foreign_fld, pk_field
|
861
|
+
if curr.join_type == JoinType.RIGHT:
|
862
|
+
curr, last = last, curr
|
863
|
+
# pk_field, foreign_fld = foreign_fld, pk_field
|
864
|
+
k = ForeignKey.get_key(curr, last)
|
865
|
+
ForeignKey.references[k] = (foreign_fld, pk_field)
|
866
|
+
|
867
|
+
def fk_charset(self) -> str:
|
868
|
+
return '(['
|
583
869
|
|
584
870
|
def eval(self, txt: str):
|
871
|
+
# ====================================
|
872
|
+
def has_side_table() -> bool:
|
873
|
+
count = 0 if len(self.queries) < 2 else sum(
|
874
|
+
q.join_type != JoinType.INNER
|
875
|
+
for q in self.queries[-2:]
|
876
|
+
)
|
877
|
+
return count > 0
|
878
|
+
# -----------------------------------
|
585
879
|
for token in self.get_tokens(txt):
|
586
|
-
if not token:
|
880
|
+
if not token or (token in '([' and self.method):
|
587
881
|
continue
|
588
882
|
if self.method:
|
589
883
|
self.method(token)
|
590
|
-
if token in '
|
591
|
-
self.
|
592
|
-
|
593
|
-
|
884
|
+
if token in ')]' and has_side_table():
|
885
|
+
self.add_foreign_key('')
|
886
|
+
self.method = self.TOKEN_METHODS.get(token.upper())
|
887
|
+
# ====================================
|
594
888
|
|
595
|
-
class Neo4JParser(
|
889
|
+
class Neo4JParser(CypherParser):
|
596
890
|
def prepare(self):
|
597
891
|
super().prepare()
|
598
892
|
self.TOKEN_METHODS = {
|
599
|
-
'(': self.new_query, '{': self.add_where,
|
600
|
-
'
|
601
|
-
'
|
893
|
+
'(': self.new_query, '{': self.add_where, '[': self.new_query,
|
894
|
+
'<-': self.left_ftable, '->': self.right_ftable,
|
895
|
+
'WHERE': self.add_where, 'AND': self.add_where,
|
602
896
|
}
|
603
897
|
self.method = None
|
898
|
+
self.aliases = {}
|
604
899
|
|
605
|
-
def new_query(self, token: str):
|
606
|
-
|
900
|
+
def new_query(self, token: str, join_type = JoinType.INNER):
|
901
|
+
alias = ''
|
902
|
+
if ':' in token:
|
903
|
+
alias, token = token.split(':')
|
904
|
+
super().new_query(token, join_type, alias)
|
607
905
|
|
608
906
|
def add_where(self, token: str):
|
609
907
|
super().add_where(token.replace(':', '='))
|
610
908
|
|
611
909
|
def add_foreign_key(self, token: str, pk_field: str='') -> tuple:
|
612
|
-
self.new_query(token)
|
613
910
|
return super().add_foreign_key('{}_id', 'id')
|
614
911
|
|
615
912
|
# ----------------------------
|
@@ -729,15 +1026,10 @@ class MongoParser(Parser):
|
|
729
1026
|
# ----------------------------
|
730
1027
|
|
731
1028
|
|
732
|
-
class JoinType(Enum):
|
733
|
-
INNER = ''
|
734
|
-
LEFT = 'LEFT '
|
735
|
-
RIGHT = 'RIGHT '
|
736
|
-
FULL = 'FULL '
|
737
|
-
|
738
1029
|
class Select(SQLObject):
|
739
1030
|
join_type: JoinType = JoinType.INNER
|
740
1031
|
REGEX = {}
|
1032
|
+
EQUIVALENT_NAMES = {}
|
741
1033
|
|
742
1034
|
def __init__(self, table_name: str='', **values):
|
743
1035
|
super().__init__(table_name)
|
@@ -753,7 +1045,7 @@ class Select(SQLObject):
|
|
753
1045
|
new_tables = set([
|
754
1046
|
'{jt}JOIN {tb} {a2} ON ({a1}.{f1} = {a2}.{f2})'.format(
|
755
1047
|
jt=self.join_type.value,
|
756
|
-
tb=self.table_name,
|
1048
|
+
tb=self.EQUIVALENT_NAMES.get(self.table_name, self.table_name),
|
757
1049
|
a1=main.alias, f1=name,
|
758
1050
|
a2=self.alias, f2=self.key_field
|
759
1051
|
)
|
@@ -784,16 +1076,7 @@ class Select(SQLObject):
|
|
784
1076
|
return query
|
785
1077
|
|
786
1078
|
def __str__(self) -> str:
|
787
|
-
|
788
|
-
LINE_BREAK = '\n' if self.break_lines else ' '
|
789
|
-
DEFAULT = lambda key: KEYWORD[key][1]
|
790
|
-
FMT_SEP = lambda key: KEYWORD[key][0].format(TABULATION)
|
791
|
-
select, _from, where, groupBy, orderBy, limit = [
|
792
|
-
DEFAULT(key) if not self.values.get(key) else "{}{}{}{}".format(
|
793
|
-
LINE_BREAK, key, TABULATION, FMT_SEP(key).join(self.values[key])
|
794
|
-
) for key in KEYWORD
|
795
|
-
]
|
796
|
-
return f'{select}{_from}{where}{groupBy}{orderBy}{limit}'.strip()
|
1079
|
+
return self.translate_to(QueryLanguage)
|
797
1080
|
|
798
1081
|
def __call__(self, **values):
|
799
1082
|
to_list = lambda x: x if isinstance(x, list) else [x]
|
@@ -836,6 +1119,8 @@ class Select(SQLObject):
|
|
836
1119
|
class_types += [GroupBy]
|
837
1120
|
FieldList(fields, class_types).add('', self)
|
838
1121
|
|
1122
|
+
def translate_to(self, language: QueryLanguage) -> str:
|
1123
|
+
return language(self).convert()
|
839
1124
|
|
840
1125
|
|
841
1126
|
class SelectIN(Select):
|
@@ -863,7 +1148,7 @@ class RuleSelectIN(Rule):
|
|
863
1148
|
@classmethod
|
864
1149
|
def apply(cls, target: Select):
|
865
1150
|
for i, condition in enumerate(target.values[WHERE]):
|
866
|
-
tokens = re.split('
|
1151
|
+
tokens = re.split(r'\s+or\s+|\s+OR\s+', re.sub('\n|\t|[()]', ' ', condition))
|
867
1152
|
if len(tokens) < 2:
|
868
1153
|
continue
|
869
1154
|
fields = [t.split('=')[0].split('.')[-1].lower().strip() for t in tokens]
|
@@ -909,7 +1194,7 @@ class RuleDateFuncReplace(Rule):
|
|
909
1194
|
"""
|
910
1195
|
SQL algorithm by Ralff Matias
|
911
1196
|
"""
|
912
|
-
REGEX = re.compile(r'(
|
1197
|
+
REGEX = re.compile(r'(YEAR[(]|year[(]|=|[)])')
|
913
1198
|
|
914
1199
|
@classmethod
|
915
1200
|
def apply(cls, target: Select):
|
@@ -923,3 +1208,48 @@ class RuleDateFuncReplace(Rule):
|
|
923
1208
|
temp = Select(f'{target.table_name} {target.alias}')
|
924
1209
|
Between(f'{year}-01-01', f'{year}-12-31').add(field, temp)
|
925
1210
|
target.values[WHERE][i] = ' AND '.join(temp.values[WHERE])
|
1211
|
+
|
1212
|
+
|
1213
|
+
def parser_class(text: str) -> Parser:
|
1214
|
+
PARSER_REGEX = [
|
1215
|
+
(r'select.*from', SQLParser),
|
1216
|
+
(r'[.](find|aggregate)[(]', MongoParser),
|
1217
|
+
(r'[(\[]\w*[:]\w+', Neo4JParser),
|
1218
|
+
(r'^\w+[@]*\w*[(]', CypherParser)
|
1219
|
+
]
|
1220
|
+
text = Parser.remove_spaces(text)
|
1221
|
+
for regex, class_type in PARSER_REGEX:
|
1222
|
+
if re.findall(regex, text, re.IGNORECASE):
|
1223
|
+
return class_type
|
1224
|
+
return None
|
1225
|
+
|
1226
|
+
|
1227
|
+
def detect(text: str) -> Select:
|
1228
|
+
from collections import Counter
|
1229
|
+
parser = parser_class(text)
|
1230
|
+
if not parser:
|
1231
|
+
raise SyntaxError('Unknown parser class')
|
1232
|
+
if parser == CypherParser:
|
1233
|
+
for table, count in Counter( re.findall(r'(\w+)[(]', text) ).most_common():
|
1234
|
+
if count < 2:
|
1235
|
+
continue
|
1236
|
+
pos = [ f.span() for f in re.finditer(fr'({table})[(]', text) ]
|
1237
|
+
for begin, end in pos[::-1]:
|
1238
|
+
new_name = f'{table}_{count}' # See set_table (line 45)
|
1239
|
+
Select.EQUIVALENT_NAMES[new_name] = table
|
1240
|
+
text = text[:begin] + new_name + '(' + text[end:]
|
1241
|
+
count -= 1
|
1242
|
+
query_list = Select.parse(text, parser)
|
1243
|
+
result = query_list[0]
|
1244
|
+
for query in query_list[1:]:
|
1245
|
+
result += query
|
1246
|
+
return result
|
1247
|
+
|
1248
|
+
|
1249
|
+
if __name__ == "__main__":
|
1250
|
+
print('@'*100)
|
1251
|
+
print( detect(
|
1252
|
+
# 'User(^name?role="Manager",id)<-Contact(requester, guest)->User(id,name)'
|
1253
|
+
'User(^name,id) <-Contact(requester,guest)-> User(id,name)'
|
1254
|
+
) )
|
1255
|
+
print('@'*100)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sql_blocks
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.31.13
|
4
4
|
Summary: Allows you to create objects for parts of SQL query commands. Also to combine these objects by joining them, adding or removing parts...
|
5
5
|
Home-page: https://github.com/julio-cascalles/sql_blocks
|
6
6
|
Author: Júlio Cascalles
|
@@ -417,3 +417,102 @@ ORDER BY
|
|
417
417
|
* `<-` connects to the table on the left
|
418
418
|
* `->` connects to the table on the right
|
419
419
|
* `^` Put the field in the ORDER BY clause
|
420
|
+
* `@` Immediately after the table name, it indicates the grouping field.
|
421
|
+
* `$` For SQL functions like **avg**$_field_, **sum**$_field_, **count**$_field_...
|
422
|
+
|
423
|
+
|
424
|
+
---
|
425
|
+
## `detect` function
|
426
|
+
|
427
|
+
It is useful to write a query in a few lines, without specifying the script type (cypher, mongoDB, SQL, Neo4J...)
|
428
|
+
### Examples:
|
429
|
+
|
430
|
+
> **1 - Relationship**
|
431
|
+
```
|
432
|
+
query = detect(
|
433
|
+
'MATCH(c:Customer)<-[:Order]->(p:Product)RETURN c, p'
|
434
|
+
)
|
435
|
+
print(query)
|
436
|
+
```
|
437
|
+
##### output:
|
438
|
+
SELECT * FROM
|
439
|
+
Order ord
|
440
|
+
LEFT JOIN Customer cus ON (ord.customer_id = cus.id)
|
441
|
+
RIGHT JOIN Product pro ON (ord.product_id = pro.id)
|
442
|
+
> **2 - Grouping**
|
443
|
+
```
|
444
|
+
query = detect(
|
445
|
+
'People@gender(avg$age?region="SOUTH"^count$qtde)'
|
446
|
+
)
|
447
|
+
print(query)
|
448
|
+
```
|
449
|
+
##### output:
|
450
|
+
SELECT
|
451
|
+
peo.gender,
|
452
|
+
Avg(peo.age),
|
453
|
+
Count(*) as qtde
|
454
|
+
FROM
|
455
|
+
People peo
|
456
|
+
WHERE
|
457
|
+
peo.region = "SOUTH"
|
458
|
+
GROUP BY
|
459
|
+
peo.gender
|
460
|
+
ORDER BY
|
461
|
+
peo.qtde
|
462
|
+
|
463
|
+
> **3 - Many conditions...**
|
464
|
+
```
|
465
|
+
print( detect('''
|
466
|
+
db.people.find({
|
467
|
+
{
|
468
|
+
$or: [
|
469
|
+
status:{$eq:"B"},
|
470
|
+
age:{$lt:50}
|
471
|
+
]
|
472
|
+
},
|
473
|
+
age:{$gte:18}, status:{$eq:"A"}
|
474
|
+
},{
|
475
|
+
name: 1, user_id: 1
|
476
|
+
}).sort({
|
477
|
+
user_id: -1
|
478
|
+
})
|
479
|
+
''') )
|
480
|
+
```
|
481
|
+
#### output:
|
482
|
+
SELECT
|
483
|
+
peo.name,
|
484
|
+
peo.user_id
|
485
|
+
FROM
|
486
|
+
people peo
|
487
|
+
WHERE
|
488
|
+
( peo. = 'B' OR peo.age < 50 ) AND
|
489
|
+
peo.age >= 18 AND
|
490
|
+
peo.status = 'A'
|
491
|
+
ORDER BY
|
492
|
+
peo.user_id DESC
|
493
|
+
|
494
|
+
> **4 - Relations with same table twice (or more)**
|
495
|
+
|
496
|
+
Automatically assigns aliases to each side of the relationship (In this example, one user invites another to add to their contact list)
|
497
|
+
```
|
498
|
+
print( detect(
|
499
|
+
'User(^name,id) <-Contact(requester,guest)-> User(id,name)'
|
500
|
+
# ^^^ u1 ^^^ u2
|
501
|
+
) )
|
502
|
+
```
|
503
|
+
SELECT
|
504
|
+
u1.name,
|
505
|
+
u2.name
|
506
|
+
FROM
|
507
|
+
Contact con
|
508
|
+
RIGHT JOIN User u2 ON (con.guest = u2.id)
|
509
|
+
LEFT JOIN User u1 ON (con.requester = u1.id)
|
510
|
+
ORDER BY
|
511
|
+
u1.name
|
512
|
+
|
513
|
+
---
|
514
|
+
### `translate_to` method
|
515
|
+
It consists of the inverse process of parsing: From a Select object, it returns the text to a script in any of the languages below:
|
516
|
+
* QueryLanguage - default
|
517
|
+
* MongoDBLanguage
|
518
|
+
* Neo4JLanguage
|
@@ -0,0 +1,7 @@
|
|
1
|
+
sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
|
2
|
+
sql_blocks/sql_blocks.py,sha256=AcA-AOZ4XSXD5srMNsnv43ljPF06_DInTu9ljrzQQMQ,42496
|
3
|
+
sql_blocks-0.31.13.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
4
|
+
sql_blocks-0.31.13.dist-info/METADATA,sha256=6Ckz7aC4I8tQ1JU-hX_AIMsyw1rw2MzwXRQvOAZ7KII,12195
|
5
|
+
sql_blocks-0.31.13.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
6
|
+
sql_blocks-0.31.13.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
|
7
|
+
sql_blocks-0.31.13.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
|
2
|
-
sql_blocks/sql_blocks.py,sha256=IT3XUhBdOA1MawoSaw-oMm1v4yv16fDwqcu21sGnIQs,30086
|
3
|
-
sql_blocks-0.2.6.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
4
|
-
sql_blocks-0.2.6.dist-info/METADATA,sha256=4A--jCZFBGeVn_fXcF41szBc_4ReMJrVKr9WWqJZnZA,9675
|
5
|
-
sql_blocks-0.2.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
6
|
-
sql_blocks-0.2.6.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
|
7
|
-
sql_blocks-0.2.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|