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.
- {sql_blocks-0.2.9/sql_blocks.egg-info → sql_blocks-0.31.21}/PKG-INFO +100 -1
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/README.md +99 -0
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/pyproject.toml +1 -1
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/setup.py +1 -1
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/sql_blocks/sql_blocks.py +158 -45
- {sql_blocks-0.2.9 → sql_blocks-0.31.21/sql_blocks.egg-info}/PKG-INFO +100 -1
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/LICENSE +0 -0
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/setup.cfg +0 -0
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/sql_blocks/__init__.py +0 -0
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/sql_blocks.egg-info/SOURCES.txt +0 -0
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/sql_blocks.egg-info/dependency_links.txt +0 -0
- {sql_blocks-0.2.9 → sql_blocks-0.31.21}/sql_blocks.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sql_blocks
|
3
|
-
Version: 0.
|
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
|
@@ -63,7 +63,7 @@ class SQLObject:
|
|
63
63
|
|
64
64
|
@staticmethod
|
65
65
|
def get_separator(key: str) -> str:
|
66
|
-
appendix = {WHERE: 'and
|
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('
|
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
|
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
|
-
|
479
|
-
|
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}
|
576
|
-
has_default = {
|
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
|
-
|
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
|
-
|
597
|
-
|
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.
|
608
|
-
|
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
|
-
|
627
|
+
continue
|
617
628
|
alias, field, const = re.split(r'[.=]', condition)
|
618
629
|
begin, end = '{', '}'
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
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
|
666
|
-
self.remove_spaces(
|
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
|
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(
|
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
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
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
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
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
|
-
|
818
|
+
self.add_field(token, [OrderBy])
|
780
819
|
|
781
|
-
def add_field(self, token: str):
|
782
|
-
|
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(
|
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
|
-
'
|
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
|
-
|
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.
|
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.
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|