sql-blocks 0.2.4__tar.gz → 0.2.9__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 0.2.4
3
+ Version: 0.2.9
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql_blocks"
3
- version = "0.2.4"
3
+ version = "0.2.9"
4
4
  authors = [
5
5
  { name="Julio Cascalles", email="julio.cascalles@outlook.com" },
6
6
  ]
@@ -3,7 +3,7 @@ from setuptools import setup
3
3
 
4
4
  setup(
5
5
  name = 'sql_blocks',
6
- version = '0.2.4',
6
+ version = '0.2.9',
7
7
  author = 'Júlio Cascalles',
8
8
  author_email = 'julio.cascalles@outlook.com',
9
9
  packages = ['sql_blocks'],
@@ -7,20 +7,18 @@ PATTERN_SUFFIX = '( [A-Za-z_]+)'
7
7
  DISTINCT_PREFX = '(DISTINCT|distinct)'
8
8
 
9
9
  KEYWORD = {
10
- 'SELECT': (',{}', '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
- # | +----- default when empty (SELECT * ...)
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]
@@ -69,27 +67,34 @@ class SQLObject:
69
67
  return KEYWORD[key][0].format(appendix.get(key, ''))
70
68
 
71
69
  def diff(self, key: str, search_list: list, exact: bool=False) -> set:
70
+ def disassemble(source: list) -> list:
71
+ if not exact:
72
+ return source
73
+ result = []
74
+ for fld in source:
75
+ result += re.split(r'([=()]|<>|\s+ON\s+|\s+on\s+)', fld)
76
+ return result
72
77
  def cleanup(fld: str) -> str:
73
78
  if exact:
74
79
  fld = fld.lower()
75
80
  return fld.strip()
76
81
  def is_named_field(fld: str) -> bool:
77
82
  return key == SELECT and re.search(' as | AS ', fld)
78
- pattern = KEYWORD[key][2]
79
- if exact:
80
- if key == WHERE:
81
- pattern = ' '
82
- pattern += f'|{PATTERN_PREFIX}'
83
- separator = self.get_separator(key)
84
83
  def field_set(source: list) -> set:
85
84
  return set(
86
85
  (
87
86
  fld if is_named_field(fld) else
88
87
  re.sub(pattern, '', cleanup(fld))
89
88
  )
90
- for string in source
89
+ for string in disassemble(source)
91
90
  for fld in re.split(separator, string)
92
91
  )
92
+ pattern = KEYWORD[key][1]
93
+ if exact:
94
+ if key == WHERE:
95
+ pattern = r'["\']| '
96
+ pattern += f'|{PATTERN_PREFIX}'
97
+ separator = self.get_separator(key)
93
98
  s1 = field_set(search_list)
94
99
  s2 = field_set(self.values.get(key, []))
95
100
  if exact:
@@ -413,6 +418,222 @@ class Rule:
413
418
  def apply(cls, target: 'Select'):
414
419
  ...
415
420
 
421
+ class QueryLanguage:
422
+ pattern = '{select}{_from}{where}{group_by}{order_by}'
423
+ has_default = {key: bool(key == SELECT) for key in KEYWORD}
424
+
425
+ @staticmethod
426
+ def remove_alias(fld: str) -> str:
427
+ return ''.join(re.split(r'\w+[.]', fld))
428
+
429
+ def join_with_tabs(self, values: list, sep: str='') -> str:
430
+ sep = sep + self.TABULATION
431
+ return sep.join(v for v in values if v)
432
+
433
+ def add_field(self, values: list) -> str:
434
+ if not values:
435
+ return '*'
436
+ return self.join_with_tabs(values, ',')
437
+
438
+ def get_tables(self, values: list) -> str:
439
+ return self.join_with_tabs(values)
440
+
441
+ def extract_conditions(self, values: list) -> str:
442
+ return self.join_with_tabs(values, ' AND ')
443
+
444
+ def sort_by(self, values: list) -> str:
445
+ return self.join_with_tabs(values)
446
+
447
+ def set_group(self, values: list) -> str:
448
+ return self.join_with_tabs(values, ',')
449
+
450
+ def __init__(self, target: 'Select'):
451
+ self.KEYWORDS = [SELECT, FROM, WHERE, GROUP_BY, ORDER_BY]
452
+ self.TABULATION = '\n\t' if target.break_lines else ' '
453
+ self.LINE_BREAK = '\n' if target.break_lines else ' '
454
+ self.TOKEN_METHODS = {
455
+ SELECT: self.add_field, FROM: self.get_tables,
456
+ WHERE: self.extract_conditions,
457
+ ORDER_BY: self.sort_by, GROUP_BY: self.set_group,
458
+ }
459
+ self.result = {}
460
+ self.target = target
461
+
462
+ def pair(self, key: str) -> str:
463
+ if key == FROM:
464
+ return '_from'
465
+ return key.lower().replace(' ', '_')
466
+
467
+ def prefix(self, key: str) -> str:
468
+ return self.LINE_BREAK + key + self.TABULATION
469
+
470
+ def convert(self) -> str:
471
+ for key in self.KEYWORDS:
472
+ method = self.TOKEN_METHODS.get(key)
473
+ ref = self.pair(key)
474
+ values = self.target.values.get(key, [])
475
+ if not method or (not values and not self.has_default[key]):
476
+ self.result[ref] = ''
477
+ continue
478
+ text = self.prefix(key) + method(values)
479
+ self.result[ref] = text
480
+ return self.pattern.format(**self.result).strip()
481
+
482
+ class MongoDBLanguage(QueryLanguage):
483
+ pattern = '{_from}.{function}({where}{select}{group_by}){order_by}'
484
+ has_default = {key: False for key in KEYWORD}
485
+ LOGICAL_OP_TO_MONGO_FUNC = {
486
+ '>': '$gt', '>=': '$gte',
487
+ '<': '$lt', '<=': '$lte',
488
+ '=': '$eq', '<>': '$ne',
489
+ }
490
+ OPERATORS = '|'.join(op for op in LOGICAL_OP_TO_MONGO_FUNC)
491
+ REGEX = {
492
+ 'options': re.compile(r'\s+or\s+|\s+OR\s+'),
493
+ 'condition': re.compile(fr'({OPERATORS})')
494
+ }
495
+
496
+ def join_with_tabs(self, values: list, sep: str=',') -> str:
497
+ def format_field(fld):
498
+ return '{indent}{fld}'.format(
499
+ fld=self.remove_alias(fld),
500
+ indent=self.TABULATION
501
+ )
502
+ return '{begin}{content}{line_break}{end}'.format(
503
+ begin='{',
504
+ content= sep.join(
505
+ format_field(fld) for fld in values if fld
506
+ ),
507
+ end='}', line_break=self.LINE_BREAK,
508
+ )
509
+
510
+ def add_field(self, values: list) -> str:
511
+ if self.result['function'] == 'aggregate':
512
+ return ''
513
+ return ',{content}'.format(
514
+ content=self.join_with_tabs([f'{fld}: 1' for fld in values]),
515
+ )
516
+
517
+ def get_tables(self, values: list) -> str:
518
+ return values[0].split()[0].lower()
519
+
520
+ @classmethod
521
+ def mongo_where_list(cls, values: list) -> list:
522
+ OR_REGEX = cls.REGEX['options']
523
+ where_list = []
524
+ for condition in values:
525
+ if OR_REGEX.findall(condition):
526
+ condition = re.sub('[()]', '', condition)
527
+ expr = '{begin}$or: [{content}]{end}'.format(
528
+ content=','.join(
529
+ cls.mongo_where_list( OR_REGEX.split(condition) )
530
+ ), begin='{', end='}',
531
+ )
532
+ where_list.append(expr)
533
+ continue
534
+ tokens = cls.REGEX['condition'].split(
535
+ cls.remove_alias(condition)
536
+ )
537
+ tokens = [t.strip() for t in tokens if t]
538
+ field, *op, const = tokens
539
+ op = ''.join(op)
540
+ expr = '{begin}{op}:{const}{end}'.format(
541
+ begin='{', const=const, end='}',
542
+ op=cls.LOGICAL_OP_TO_MONGO_FUNC[op],
543
+ )
544
+ where_list.append(f'{field}:{expr}')
545
+ return where_list
546
+
547
+ def extract_conditions(self, values: list) -> str:
548
+ return self.join_with_tabs(
549
+ self.mongo_where_list(values)
550
+ )
551
+
552
+ def sort_by(self, values: list) -> str:
553
+ return ".sort({begin}{indent}{field}:{flag}{line_break}{end})".format(
554
+ begin='{', field=self.remove_alias(values[0].split()[0]),
555
+ flag=-1 if OrderBy.sort == SortType.DESC else 1,
556
+ end='}', indent=self.TABULATION, line_break=self.LINE_BREAK,
557
+ )
558
+
559
+ def set_group(self, values: list) -> str:
560
+ self.result['function'] = 'aggregate'
561
+ return '{"$group" : {_id:"$%%", count:{$sum:1}}}'.replace(
562
+ '%%', self.remove_alias( values[0] )
563
+ )
564
+
565
+ def __init__(self, target: 'Select'):
566
+ super().__init__(target)
567
+ self.result['function'] = 'find'
568
+ self.KEYWORDS = [GROUP_BY, SELECT, FROM, WHERE, ORDER_BY]
569
+
570
+ def prefix(self, key: str):
571
+ return ''
572
+
573
+
574
+ class Neo4JLanguage(QueryLanguage):
575
+ pattern = 'MATCH {_from} RETURN {aliases}'
576
+ has_default = {key: False for key in KEYWORD}
577
+
578
+ def add_field(self, values: list) -> str:
579
+ return ''
580
+
581
+ def get_tables(self, values: list) -> str:
582
+ NODE_FORMAT = dict(
583
+ left='({}:{}{})<-',
584
+ core='[{}:{}{}]',
585
+ right='->({}:{}{})'
586
+ )
587
+ nodes = {k: '' for k in NODE_FORMAT}
588
+ for txt in values:
589
+ found = re.search(
590
+ r'^(left|right)\s+', txt, re.IGNORECASE
591
+ )
592
+ pos = 'core'
593
+ if found:
594
+ start, end = found.span()
595
+ pos = txt[start:end-1].lower()
596
+ tokens = re.split(r'JOIN\s+|ON\s+', txt[end:])
597
+ txt = tokens[1].strip()
598
+ table_name, *alias = txt.split()
599
+ if alias:
600
+ alias = alias[0]
601
+ else:
602
+ alias = SQLObject.ALIAS_FUNC(table_name)
603
+ condition = self.aliases.get(alias, '')
604
+ if not condition:
605
+ self.aliases[alias] = ''
606
+ nodes[pos] = NODE_FORMAT[pos].format(alias, table_name, condition)
607
+ self.result['aliases'] = ','.join(self.aliases.keys())
608
+ return '{left}{core}{right}'.format(**nodes)
609
+
610
+ def extract_conditions(self, values: list) -> str:
611
+ for condition in values:
612
+ other_comparisions = any(
613
+ char in condition for char in '<>%'
614
+ )
615
+ if '=' not in condition or other_comparisions:
616
+ raise NotImplementedError('Only comparisons with equality are available for now.')
617
+ alias, field, const = re.split(r'[.=]', condition)
618
+ begin, end = '{', '}'
619
+ self.aliases[alias] = f'{begin}{field}:{const}{end}'
620
+ return '' # --- WHERE [*other_comparisions*] ...
621
+
622
+ def sort_by(self, values: list) -> str:
623
+ return ''
624
+
625
+ def set_group(self, values: list) -> str:
626
+ return ''
627
+
628
+ def __init__(self, target: 'Select'):
629
+ super().__init__(target)
630
+ self.aliases = {}
631
+ self.KEYWORDS = [WHERE, FROM]
632
+
633
+ def prefix(self, key: str):
634
+ return ''
635
+
636
+
416
637
  class Parser:
417
638
  REGEX = {}
418
639
 
@@ -421,14 +642,37 @@ class Parser:
421
642
 
422
643
  def __init__(self, txt: str, class_type):
423
644
  self.queries = []
424
- if not self.REGEX:
425
- self.prepare()
645
+ self.prepare()
426
646
  self.class_type = class_type
427
647
  self.eval(txt)
428
648
 
429
649
  def eval(self, txt: str):
430
650
  ...
431
651
 
652
+ @staticmethod
653
+ def remove_spaces(script: str) -> str:
654
+ is_string = False
655
+ result = []
656
+ for token in re.split(r'(")', script):
657
+ if token == '"':
658
+ is_string = not is_string
659
+ if not is_string:
660
+ token = re.sub(r'\s+', '', token)
661
+ result.append(token)
662
+ return ''.join(result)
663
+
664
+ def get_tokens(self, txt: str) -> list:
665
+ return self.REGEX['separator'].split(
666
+ self.remove_spaces(txt)
667
+ )
668
+
669
+
670
+ class JoinType(Enum):
671
+ INNER = ''
672
+ LEFT = 'LEFT '
673
+ RIGHT = 'RIGHT '
674
+ FULL = 'FULL '
675
+
432
676
 
433
677
  class SQLParser(Parser):
434
678
  REGEX = {}
@@ -506,21 +750,24 @@ class SQLParser(Parser):
506
750
 
507
751
  class Cypher(Parser):
508
752
  REGEX = {}
509
- TOKEN_METHODS = {}
510
753
 
511
754
  def prepare(self):
512
- self.REGEX['separator'] = re.compile(r'([(,?)^]|->|<-)')
755
+ self.REGEX['separator'] = re.compile(r'([(,?)^{}[\]]|->|<-)')
513
756
  self.REGEX['condition'] = re.compile(r'(^\w+)|([<>=])')
757
+ self.join_type = JoinType.INNER
514
758
  self.TOKEN_METHODS = {
515
759
  '(': self.add_field, '?': self.add_where,
516
760
  ',': self.add_field, '^': self.add_order,
517
- ')': self.new_query, '->': self.left_ftable,
518
- '<-': self.right_ftable,
761
+ ')': self.new_query, '<-': self.left_ftable,
762
+ '->': self.right_ftable,
519
763
  }
764
+ self.method = self.new_query
520
765
 
521
- def new_query(self, token: str):
766
+ def new_query(self, token: str, join_type = JoinType.INNER):
522
767
  if token.isidentifier():
523
- self.queries.append( self.class_type(token) )
768
+ query = self.class_type(token)
769
+ self.queries.append(query)
770
+ query.join_type = join_type
524
771
 
525
772
  def add_where(self, token: str):
526
773
  field, *condition = [
@@ -535,37 +782,76 @@ class Cypher(Parser):
535
782
  FieldList(token, [Field]).add('', self.queries[-1])
536
783
 
537
784
  def left_ftable(self, token: str):
785
+ if self.queries:
786
+ self.queries[-1].join_type = JoinType.LEFT
538
787
  self.new_query(token)
539
- self.join_type = JoinType.LEFT
540
788
 
541
789
  def right_ftable(self, token: str):
542
- self.new_query(token)
543
- self.join_type = JoinType.RIGHT
790
+ self.new_query(token, JoinType.RIGHT)
544
791
 
545
- def add_foreign_key(self, token: str):
792
+ def add_foreign_key(self, token: str, pk_field: str=''):
546
793
  curr, last = [self.queries[i] for i in (-1, -2)]
547
- pk_field = last.values[SELECT][-1].split('.')[-1]
548
- last.delete(pk_field, [SELECT])
549
- if self.join_type == JoinType.RIGHT:
794
+ if not pk_field:
795
+ if not last.values.get(SELECT):
796
+ raise IndexError(f'Primary Key not found for {last.table_name}.')
797
+ pk_field = last.values[SELECT][-1].split('.')[-1]
798
+ last.delete(pk_field, [SELECT])
799
+ if '{}' in token:
800
+ foreign_fld = token.format(
801
+ last.table_name.lower()
802
+ if last.join_type == JoinType.LEFT else
803
+ curr.table_name.lower()
804
+ )
805
+ else:
806
+ if not curr.values.get(SELECT):
807
+ raise IndexError(f'Foreign Key not found for {curr.table_name}.')
808
+ foreign_fld = curr.values[SELECT][0].split('.')[-1]
809
+ curr.delete(foreign_fld, [SELECT])
810
+ if curr.join_type == JoinType.RIGHT:
550
811
  curr, last = last, curr
551
- pk_field, token = token, pk_field
552
- last.key_field = pk_field
553
- k = ForeignKey.get_key(last, curr)
554
- ForeignKey.references[k] = (pk_field, token)
555
- self.join_type = JoinType.INNER
812
+ k = ForeignKey.get_key(curr, last)
813
+ ForeignKey.references[k] = (foreign_fld, pk_field)
814
+
815
+ def fk_charset(self) -> str:
816
+ return '(['
556
817
 
557
818
  def eval(self, txt: str):
558
- self.join_type = JoinType.INNER
559
- self.method = self.new_query
560
- for token in self.REGEX['separator'].split( re.sub(r'\s+', '', txt) ):
561
- if not token:
819
+ # ====================================
820
+ def has_side_table() -> bool:
821
+ count = 0 if len(self.queries) < 2 else sum(
822
+ q.join_type != JoinType.INNER
823
+ for q in self.queries[-2:]
824
+ )
825
+ return count > 0
826
+ # -----------------------------------
827
+ for token in self.get_tokens(txt):
828
+ if not token or (token in '([' and self.method):
562
829
  continue
563
830
  if self.method:
564
831
  self.method(token)
565
- if token == '(' and self.join_type != JoinType.INNER:
566
- self.method = self.add_foreign_key
567
- else:
568
- self.method = self.TOKEN_METHODS.get(token)
832
+ if token in ')]' and has_side_table():
833
+ self.add_foreign_key('')
834
+ self.method = self.TOKEN_METHODS.get(token)
835
+ # ====================================
836
+
837
+ class Neo4JParser(Cypher):
838
+ def prepare(self):
839
+ super().prepare()
840
+ self.TOKEN_METHODS = {
841
+ '(': self.new_query, '{': self.add_where,
842
+ '<-': self.left_ftable, '->': self.right_ftable,
843
+ '[': self.new_query
844
+ }
845
+ self.method = None
846
+
847
+ def new_query(self, token: str, join_type = JoinType.INNER):
848
+ super().new_query(token.split(':')[-1], join_type)
849
+
850
+ def add_where(self, token: str):
851
+ super().add_where(token.replace(':', '='))
852
+
853
+ def add_foreign_key(self, token: str, pk_field: str='') -> tuple:
854
+ return super().add_foreign_key('{}_id', 'id')
569
855
 
570
856
  # ----------------------------
571
857
  class MongoParser(Parser):
@@ -669,7 +955,7 @@ class MongoParser(Parser):
669
955
  self.TOKEN_METHODS = {
670
956
  '{': self.get_param, ',': self.next_param, ')': self.new_query,
671
957
  }
672
- for token in self.REGEX['separator'].split( re.sub(r'\s+', '', txt) ):
958
+ for token in self.get_tokens(txt):
673
959
  if not token:
674
960
  continue
675
961
  if self.method:
@@ -684,12 +970,6 @@ class MongoParser(Parser):
684
970
  # ----------------------------
685
971
 
686
972
 
687
- class JoinType(Enum):
688
- INNER = ''
689
- LEFT = 'LEFT '
690
- RIGHT = 'RIGHT '
691
- FULL = 'FULL '
692
-
693
973
  class Select(SQLObject):
694
974
  join_type: JoinType = JoinType.INNER
695
975
  REGEX = {}
@@ -739,16 +1019,7 @@ class Select(SQLObject):
739
1019
  return query
740
1020
 
741
1021
  def __str__(self) -> str:
742
- TABULATION = '\n\t' if self.break_lines else ' '
743
- LINE_BREAK = '\n' if self.break_lines else ' '
744
- DEFAULT = lambda key: KEYWORD[key][1]
745
- FMT_SEP = lambda key: KEYWORD[key][0].format(TABULATION)
746
- select, _from, where, groupBy, orderBy, limit = [
747
- DEFAULT(key) if not self.values.get(key) else "{}{}{}{}".format(
748
- LINE_BREAK, key, TABULATION, FMT_SEP(key).join(self.values[key])
749
- ) for key in KEYWORD
750
- ]
751
- return f'{select}{_from}{where}{groupBy}{orderBy}{limit}'.strip()
1022
+ return self.translate_to(QueryLanguage)
752
1023
 
753
1024
  def __call__(self, **values):
754
1025
  to_list = lambda x: x if isinstance(x, list) else [x]
@@ -791,6 +1062,8 @@ class Select(SQLObject):
791
1062
  class_types += [GroupBy]
792
1063
  FieldList(fields, class_types).add('', self)
793
1064
 
1065
+ def translate_to(self, language: QueryLanguage) -> str:
1066
+ return language(self).convert()
794
1067
 
795
1068
 
796
1069
  class SelectIN(Select):
@@ -818,7 +1091,7 @@ class RuleSelectIN(Rule):
818
1091
  @classmethod
819
1092
  def apply(cls, target: Select):
820
1093
  for i, condition in enumerate(target.values[WHERE]):
821
- tokens = re.split(' or | OR ', re.sub('\n|\t|[()]', ' ', condition))
1094
+ tokens = re.split(r'\s+or\s+|\s+OR\s+', re.sub('\n|\t|[()]', ' ', condition))
822
1095
  if len(tokens) < 2:
823
1096
  continue
824
1097
  fields = [t.split('=')[0].split('.')[-1].lower().strip() for t in tokens]
@@ -864,7 +1137,7 @@ class RuleDateFuncReplace(Rule):
864
1137
  """
865
1138
  SQL algorithm by Ralff Matias
866
1139
  """
867
- REGEX = re.compile(r'(\bYEAR[(]|\byear[(]|=|[)])')
1140
+ REGEX = re.compile(r'(YEAR[(]|year[(]|=|[)])')
868
1141
 
869
1142
  @classmethod
870
1143
  def apply(cls, target: Select):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 0.2.4
3
+ Version: 0.2.9
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
File without changes
File without changes
File without changes