sql-blocks 0.2.9__tar.gz → 0.31.21__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.9
3
+ Version: 0.31.21
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.9"
3
+ version = "0.31.21"
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.9',
6
+ version = '0.31.21',
7
7
  author = 'Júlio Cascalles',
8
8
  author_email = 'julio.cascalles@outlook.com',
9
9
  packages = ['sql_blocks'],
@@ -63,7 +63,7 @@ class SQLObject:
63
63
 
64
64
  @staticmethod
65
65
  def get_separator(key: str) -> str:
66
- appendix = {WHERE: 'and|', FROM: 'join|JOIN'}
66
+ appendix = {WHERE: r'\s+and\s+|', FROM: r'\s+join\s+|\s+JOIN\s+'}
67
67
  return KEYWORD[key][0].format(appendix.get(key, ''))
68
68
 
69
69
  def diff(self, key: str, search_list: list, exact: bool=False) -> set:
@@ -79,7 +79,7 @@ class SQLObject:
79
79
  fld = fld.lower()
80
80
  return fld.strip()
81
81
  def is_named_field(fld: str) -> bool:
82
- return key == SELECT and re.search(' as | AS ', fld)
82
+ return key == SELECT and re.search(r'\s+as\s+|\s+AS\s+', fld)
83
83
  def field_set(source: list) -> set:
84
84
  return set(
85
85
  (
@@ -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):
@@ -442,7 +443,7 @@ class QueryLanguage:
442
443
  return self.join_with_tabs(values, ' AND ')
443
444
 
444
445
  def sort_by(self, values: list) -> str:
445
- return self.join_with_tabs(values)
446
+ return self.join_with_tabs(values)
446
447
 
447
448
  def set_group(self, values: list) -> str:
448
449
  return self.join_with_tabs(values, ',')
@@ -475,8 +476,12 @@ class QueryLanguage:
475
476
  if not method or (not values and not self.has_default[key]):
476
477
  self.result[ref] = ''
477
478
  continue
478
- text = self.prefix(key) + method(values)
479
- self.result[ref] = text
479
+ if key == FROM:
480
+ values[0] = '{} {}'.format(
481
+ self.target.aka(), self.target.alias
482
+ ).strip()
483
+ text = method(values)
484
+ self.result[ref] = self.prefix(key) + text
480
485
  return self.pattern.format(**self.result).strip()
481
486
 
482
487
  class MongoDBLanguage(QueryLanguage):
@@ -572,11 +577,13 @@ class MongoDBLanguage(QueryLanguage):
572
577
 
573
578
 
574
579
  class Neo4JLanguage(QueryLanguage):
575
- pattern = 'MATCH {_from} RETURN {aliases}'
576
- has_default = {key: False for key in KEYWORD}
580
+ pattern = 'MATCH {_from}{where}RETURN {select}{order_by}'
581
+ has_default = {WHERE: False, FROM: False, ORDER_BY: True, SELECT: True}
577
582
 
578
583
  def add_field(self, values: list) -> str:
579
- return ''
584
+ if values:
585
+ return self.join_with_tabs(values, ',')
586
+ return self.TABULATION + ','.join(self.aliases.keys())
580
587
 
581
588
  def get_tables(self, values: list) -> str:
582
589
  NODE_FORMAT = dict(
@@ -589,12 +596,13 @@ class Neo4JLanguage(QueryLanguage):
589
596
  found = re.search(
590
597
  r'^(left|right)\s+', txt, re.IGNORECASE
591
598
  )
592
- pos = 'core'
599
+ pos, end, i = 'core', 0, 0
593
600
  if found:
594
601
  start, end = found.span()
595
602
  pos = txt[start:end-1].lower()
596
- tokens = re.split(r'JOIN\s+|ON\s+', txt[end:])
597
- txt = tokens[1].strip()
603
+ i = 1
604
+ tokens = re.split(r'JOIN\s+|ON\s+', txt[end:])
605
+ txt = tokens[i].strip()
598
606
  table_name, *alias = txt.split()
599
607
  if alias:
600
608
  alias = alias[0]
@@ -604,23 +612,27 @@ class Neo4JLanguage(QueryLanguage):
604
612
  if not condition:
605
613
  self.aliases[alias] = ''
606
614
  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)
615
+ return self.TABULATION + '{left}{core}{right}'.format(**nodes)
616
+
609
617
 
610
618
  def extract_conditions(self, values: list) -> str:
619
+ equalities = {}
620
+ where_list = []
611
621
  for condition in values:
612
622
  other_comparisions = any(
613
- char in condition for char in '<>%'
623
+ char in condition for char in '<>'
614
624
  )
625
+ where_list.append(condition)
615
626
  if '=' not in condition or other_comparisions:
616
- raise NotImplementedError('Only comparisons with equality are available for now.')
627
+ continue
617
628
  alias, field, const = re.split(r'[.=]', condition)
618
629
  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 ''
630
+ equalities[alias] = f'{begin}{field}:{const}{end}'
631
+ if len(equalities) == len(where_list):
632
+ self.aliases.update(equalities)
633
+ self.has_default[WHERE] = True
634
+ return self.LINE_BREAK
635
+ return self.join_with_tabs(where_list, ' AND ') + self.LINE_BREAK
624
636
 
625
637
  def set_group(self, values: list) -> str:
626
638
  return ''
@@ -628,9 +640,15 @@ class Neo4JLanguage(QueryLanguage):
628
640
  def __init__(self, target: 'Select'):
629
641
  super().__init__(target)
630
642
  self.aliases = {}
631
- self.KEYWORDS = [WHERE, FROM]
643
+ self.KEYWORDS = [WHERE, FROM, ORDER_BY, SELECT]
632
644
 
633
645
  def prefix(self, key: str):
646
+ default_prefix = any([
647
+ (key == WHERE and not self.has_default[WHERE]),
648
+ key == ORDER_BY
649
+ ])
650
+ if default_prefix:
651
+ return super().prefix(key)
634
652
  return ''
635
653
 
636
654
 
@@ -662,9 +680,10 @@ class Parser:
662
680
  return ''.join(result)
663
681
 
664
682
  def get_tokens(self, txt: str) -> list:
665
- return self.REGEX['separator'].split(
666
- self.remove_spaces(txt)
667
- )
683
+ return [
684
+ self.remove_spaces(t)
685
+ for t in self.REGEX['separator'].split(txt)
686
+ ]
668
687
 
669
688
 
670
689
  class JoinType(Enum):
@@ -748,12 +767,18 @@ class SQLParser(Parser):
748
767
  self.queries = list( result.values() )
749
768
 
750
769
 
751
- class Cypher(Parser):
770
+ class CypherParser(Parser):
752
771
  REGEX = {}
772
+ CHAR_SET = r'[(,?)^{}[\]]'
773
+ KEYWORDS = '|'.join(
774
+ fr'\b{word}\b'
775
+ for word in "where return WHERE RETURN and AND".split()
776
+ )
753
777
 
754
778
  def prepare(self):
755
- self.REGEX['separator'] = re.compile(r'([(,?)^{}[\]]|->|<-)')
779
+ self.REGEX['separator'] = re.compile(fr'({self.CHAR_SET}|->|<-|{self.KEYWORDS})')
756
780
  self.REGEX['condition'] = re.compile(r'(^\w+)|([<>=])')
781
+ self.REGEX['alias_pos'] = re.compile(r'(\w+)[.](\w+)')
757
782
  self.join_type = JoinType.INNER
758
783
  self.TOKEN_METHODS = {
759
784
  '(': self.add_field, '?': self.add_where,
@@ -762,24 +787,52 @@ class Cypher(Parser):
762
787
  '->': self.right_ftable,
763
788
  }
764
789
  self.method = self.new_query
790
+ self.aliases = {}
765
791
 
766
- def new_query(self, token: str, join_type = JoinType.INNER):
767
- if token.isidentifier():
768
- query = self.class_type(token)
769
- self.queries.append(query)
770
- query.join_type = join_type
792
+ def new_query(self, token: str, join_type = JoinType.INNER, alias: str=''):
793
+ token, *group_fields = token.split('@')
794
+ if not token.isidentifier():
795
+ return
796
+ table_name = f'{token} {alias}' if alias else token
797
+ query = self.class_type(table_name)
798
+ if not alias:
799
+ alias = query.alias
800
+ self.queries.append(query)
801
+ self.aliases[alias] = query
802
+ FieldList(group_fields, [Field, GroupBy]).add('', query)
803
+ query.join_type = join_type
771
804
 
772
805
  def add_where(self, token: str):
773
- field, *condition = [
774
- t for t in self.REGEX['condition'].split(token) if t
775
- ]
776
- Where(' '.join(condition)).add(field, self.queries[-1])
806
+ elements = [t for t in self.REGEX['alias_pos'].split(token) if t]
807
+ if len(elements) == 3:
808
+ alias, field, *condition = elements
809
+ query = self.aliases[alias]
810
+ else:
811
+ field, *condition = [
812
+ t for t in self.REGEX['condition'].split(token) if t
813
+ ]
814
+ query = self.queries[-1]
815
+ Where(' '.join(condition)).add(field, query)
777
816
 
778
817
  def add_order(self, token: str):
779
- FieldList(token, [Field, OrderBy]).add('', self.queries[-1])
818
+ self.add_field(token, [OrderBy])
780
819
 
781
- def add_field(self, token: str):
782
- FieldList(token, [Field]).add('', self.queries[-1])
820
+ def add_field(self, token: str, extra_classes: list['type']=[]):
821
+ if token in self.TOKEN_METHODS:
822
+ return
823
+ class_list = [Field]
824
+ if '$' in token:
825
+ func_name, token = token.split('$')
826
+ if func_name == 'count':
827
+ if not token:
828
+ token = 'count_1'
829
+ NamedField(token, Count).add('*', self.queries[-1])
830
+ class_list = []
831
+ else:
832
+ FUNCTION_CLASS = {f.__name__.lower(): f for f in Function.__subclasses__()}
833
+ class_list = [ FUNCTION_CLASS[func_name] ]
834
+ class_list += extra_classes
835
+ FieldList(token, class_list).add('', self.queries[-1])
783
836
 
784
837
  def left_ftable(self, token: str):
785
838
  if self.queries:
@@ -807,8 +860,11 @@ class Cypher(Parser):
807
860
  raise IndexError(f'Foreign Key not found for {curr.table_name}.')
808
861
  foreign_fld = curr.values[SELECT][0].split('.')[-1]
809
862
  curr.delete(foreign_fld, [SELECT])
863
+ if curr.join_type == JoinType.RIGHT:
864
+ pk_field, foreign_fld = foreign_fld, pk_field
810
865
  if curr.join_type == JoinType.RIGHT:
811
866
  curr, last = last, curr
867
+ # pk_field, foreign_fld = foreign_fld, pk_field
812
868
  k = ForeignKey.get_key(curr, last)
813
869
  ForeignKey.references[k] = (foreign_fld, pk_field)
814
870
 
@@ -831,21 +887,25 @@ class Cypher(Parser):
831
887
  self.method(token)
832
888
  if token in ')]' and has_side_table():
833
889
  self.add_foreign_key('')
834
- self.method = self.TOKEN_METHODS.get(token)
890
+ self.method = self.TOKEN_METHODS.get(token.upper())
835
891
  # ====================================
836
892
 
837
- class Neo4JParser(Cypher):
893
+ class Neo4JParser(CypherParser):
838
894
  def prepare(self):
839
895
  super().prepare()
840
896
  self.TOKEN_METHODS = {
841
- '(': self.new_query, '{': self.add_where,
842
- '<-': self.left_ftable, '->': self.right_ftable,
843
- '[': self.new_query
897
+ '(': self.new_query, '{': self.add_where, '[': self.new_query,
898
+ '<-': self.left_ftable, '->': self.right_ftable,
899
+ 'WHERE': self.add_where, 'AND': self.add_where,
844
900
  }
845
901
  self.method = None
902
+ self.aliases = {}
846
903
 
847
904
  def new_query(self, token: str, join_type = JoinType.INNER):
848
- super().new_query(token.split(':')[-1], join_type)
905
+ alias = ''
906
+ if ':' in token:
907
+ alias, token = token.split(':')
908
+ super().new_query(token, join_type, alias)
849
909
 
850
910
  def add_where(self, token: str):
851
911
  super().add_where(token.replace(':', '='))
@@ -973,6 +1033,7 @@ class MongoParser(Parser):
973
1033
  class Select(SQLObject):
974
1034
  join_type: JoinType = JoinType.INNER
975
1035
  REGEX = {}
1036
+ EQUIVALENT_NAMES = {}
976
1037
 
977
1038
  def __init__(self, table_name: str='', **values):
978
1039
  super().__init__(table_name)
@@ -983,12 +1044,16 @@ class Select(SQLObject):
983
1044
  for value in self.diff(key, new_values):
984
1045
  self.values.setdefault(key, []).append(value)
985
1046
 
1047
+ def aka(self) -> str:
1048
+ result = self.table_name
1049
+ return self.EQUIVALENT_NAMES.get(result, result)
1050
+
986
1051
  def add(self, name: str, main: SQLObject):
987
1052
  old_tables = main.values.get(FROM, [])
988
1053
  new_tables = set([
989
1054
  '{jt}JOIN {tb} {a2} ON ({a1}.{f1} = {a2}.{f2})'.format(
990
1055
  jt=self.join_type.value,
991
- tb=self.table_name,
1056
+ tb=self.aka(),
992
1057
  a1=main.alias, f1=name,
993
1058
  a2=self.alias, f2=self.key_field
994
1059
  )
@@ -1151,3 +1216,51 @@ class RuleDateFuncReplace(Rule):
1151
1216
  temp = Select(f'{target.table_name} {target.alias}')
1152
1217
  Between(f'{year}-01-01', f'{year}-12-31').add(field, temp)
1153
1218
  target.values[WHERE][i] = ' AND '.join(temp.values[WHERE])
1219
+
1220
+
1221
+ def parser_class(text: str) -> Parser:
1222
+ PARSER_REGEX = [
1223
+ (r'select.*from', SQLParser),
1224
+ (r'[.](find|aggregate)[(]', MongoParser),
1225
+ (r'[(\[]\w*[:]\w+', Neo4JParser),
1226
+ (r'^\w+[@]*\w*[(]', CypherParser)
1227
+ ]
1228
+ text = Parser.remove_spaces(text)
1229
+ for regex, class_type in PARSER_REGEX:
1230
+ if re.findall(regex, text, re.IGNORECASE):
1231
+ return class_type
1232
+ return None
1233
+
1234
+
1235
+ def detect(text: str) -> Select:
1236
+ from collections import Counter
1237
+ parser = parser_class(text)
1238
+ if not parser:
1239
+ raise SyntaxError('Unknown parser class')
1240
+ if parser == CypherParser:
1241
+ for table, count in Counter( re.findall(r'(\w+)[(]', text) ).most_common():
1242
+ if count < 2:
1243
+ continue
1244
+ pos = [ f.span() for f in re.finditer(fr'({table})[(]', text) ]
1245
+ for begin, end in pos[::-1]:
1246
+ new_name = f'{table}_{count}' # See set_table (line 45)
1247
+ Select.EQUIVALENT_NAMES[new_name] = table
1248
+ text = text[:begin] + new_name + '(' + text[end:]
1249
+ count -= 1
1250
+ query_list = Select.parse(text, parser)
1251
+ result = query_list[0]
1252
+ for query in query_list[1:]:
1253
+ result += query
1254
+ return result
1255
+
1256
+
1257
+ if __name__ == "__main__":
1258
+ print('@'*100)
1259
+ print( detect(
1260
+ '''
1261
+ Company(?jobs > 10, id)
1262
+ <- Person(work_at ^name, friend_of) ->
1263
+ Person(id ?skill="pANDas")
1264
+ ''' # ^^^---- Test for confusion with the AND operator
1265
+ ) )
1266
+ print('@'*100)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 0.2.9
3
+ Version: 0.31.21
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