sql-blocks 0.2.6__tar.gz → 0.31.13__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.6
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
@@ -402,3 +402,102 @@ ORDER BY
402
402
  * `<-` connects to the table on the left
403
403
  * `->` connects to the table on the right
404
404
  * `^` Put the field in the ORDER BY clause
405
+ * `@` Immediately after the table name, it indicates the grouping field.
406
+ * `$` For SQL functions like **avg**$_field_, **sum**$_field_, **count**$_field_...
407
+
408
+
409
+ ---
410
+ ## `detect` function
411
+
412
+ It is useful to write a query in a few lines, without specifying the script type (cypher, mongoDB, SQL, Neo4J...)
413
+ ### Examples:
414
+
415
+ > **1 - Relationship**
416
+ ```
417
+ query = detect(
418
+ 'MATCH(c:Customer)<-[:Order]->(p:Product)RETURN c, p'
419
+ )
420
+ print(query)
421
+ ```
422
+ ##### output:
423
+ SELECT * FROM
424
+ Order ord
425
+ LEFT JOIN Customer cus ON (ord.customer_id = cus.id)
426
+ RIGHT JOIN Product pro ON (ord.product_id = pro.id)
427
+ > **2 - Grouping**
428
+ ```
429
+ query = detect(
430
+ 'People@gender(avg$age?region="SOUTH"^count$qtde)'
431
+ )
432
+ print(query)
433
+ ```
434
+ ##### output:
435
+ SELECT
436
+ peo.gender,
437
+ Avg(peo.age),
438
+ Count(*) as qtde
439
+ FROM
440
+ People peo
441
+ WHERE
442
+ peo.region = "SOUTH"
443
+ GROUP BY
444
+ peo.gender
445
+ ORDER BY
446
+ peo.qtde
447
+
448
+ > **3 - Many conditions...**
449
+ ```
450
+ print( detect('''
451
+ db.people.find({
452
+ {
453
+ $or: [
454
+ status:{$eq:"B"},
455
+ age:{$lt:50}
456
+ ]
457
+ },
458
+ age:{$gte:18}, status:{$eq:"A"}
459
+ },{
460
+ name: 1, user_id: 1
461
+ }).sort({
462
+ user_id: -1
463
+ })
464
+ ''') )
465
+ ```
466
+ #### output:
467
+ SELECT
468
+ peo.name,
469
+ peo.user_id
470
+ FROM
471
+ people peo
472
+ WHERE
473
+ ( peo. = 'B' OR peo.age < 50 ) AND
474
+ peo.age >= 18 AND
475
+ peo.status = 'A'
476
+ ORDER BY
477
+ peo.user_id DESC
478
+
479
+ > **4 - Relations with same table twice (or more)**
480
+
481
+ Automatically assigns aliases to each side of the relationship (In this example, one user invites another to add to their contact list)
482
+ ```
483
+ print( detect(
484
+ 'User(^name,id) <-Contact(requester,guest)-> User(id,name)'
485
+ # ^^^ u1 ^^^ u2
486
+ ) )
487
+ ```
488
+ SELECT
489
+ u1.name,
490
+ u2.name
491
+ FROM
492
+ Contact con
493
+ RIGHT JOIN User u2 ON (con.guest = u2.id)
494
+ LEFT JOIN User u1 ON (con.requester = u1.id)
495
+ ORDER BY
496
+ u1.name
497
+
498
+ ---
499
+ ### `translate_to` method
500
+ 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:
501
+ * QueryLanguage - default
502
+ * MongoDBLanguage
503
+ * Neo4JLanguage
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql_blocks"
3
- version = "0.2.6"
3
+ version = "0.31.13"
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.6',
6
+ version = '0.31.13',
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 ', '', r'["\']'),
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]
@@ -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][2]
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 self.REGEX['separator'].split(
450
- self.remove_spaces(txt)
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 Cypher(Parser):
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(r'([(,?)^{}[\]]|->|<-)')
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, '->': self.left_ftable,
539
- '<-': self.right_ftable,
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
- if token.isidentifier():
545
- self.queries.append( self.class_type(token) )
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
- field, *condition = [
549
- t for t in self.REGEX['condition'].split(token) if t
550
- ]
551
- Where(' '.join(condition)).add(field, self.queries[-1])
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
- FieldList(token, [Field, OrderBy]).add('', self.queries[-1])
814
+ self.add_field(token, [OrderBy])
555
815
 
556
- def add_field(self, token: str):
557
- FieldList(token, [Field]).add('', self.queries[-1])
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
- return
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
- token = token.format(
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
- k = ForeignKey.get_key(last, curr)
581
- ForeignKey.references[k] = (token, pk_field)
582
- self.join_type = JoinType.INNER
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 '([' and self.join_type != JoinType.INNER:
591
- self.method = self.add_foreign_key
592
- else:
593
- self.method = self.TOKEN_METHODS.get(token)
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(Cypher):
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
- '->': self.left_ftable, '<-': self.right_ftable,
601
- '[': self.new_query
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
- super().new_query(token.split(':')[-1])
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
- TABULATION = '\n\t' if self.break_lines else ' '
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(' or | OR ', re.sub('\n|\t|[()]', ' ', condition))
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'(\bYEAR[(]|\byear[(]|=|[)])')
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.2.6
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
File without changes
File without changes