sql-blocks 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sql_blocks/sql_blocks.py +167 -77
- {sql_blocks-0.2.1.dist-info → sql_blocks-0.2.3.dist-info}/METADATA +7 -5
- sql_blocks-0.2.3.dist-info/RECORD +7 -0
- sql_blocks-0.2.1.dist-info/RECORD +0 -7
- {sql_blocks-0.2.1.dist-info → sql_blocks-0.2.3.dist-info}/LICENSE +0 -0
- {sql_blocks-0.2.1.dist-info → sql_blocks-0.2.3.dist-info}/WHEEL +0 -0
- {sql_blocks-0.2.1.dist-info → sql_blocks-0.2.3.dist-info}/top_level.txt +0 -0
sql_blocks/sql_blocks.py
CHANGED
@@ -4,15 +4,14 @@ import re
|
|
4
4
|
|
5
5
|
PATTERN_PREFIX = '([^0-9 ]+[.])'
|
6
6
|
PATTERN_SUFFIX = '( [A-Za-z_]+)'
|
7
|
-
|
8
|
-
DISTINCT_PREFX = f'(DISTINCT|distinct)|{PATTERN_PREFIX}'
|
7
|
+
DISTINCT_PREFX = '(DISTINCT|distinct)'
|
9
8
|
|
10
9
|
KEYWORD = {
|
11
10
|
'SELECT': (',{}', 'SELECT *', DISTINCT_PREFX),
|
12
11
|
'FROM': ('{}', '', PATTERN_SUFFIX),
|
13
12
|
'WHERE': ('{}AND ', '', ''),
|
14
|
-
'GROUP BY': (',{}', '',
|
15
|
-
'ORDER BY': (',{}', '',
|
13
|
+
'GROUP BY': (',{}', '', PATTERN_SUFFIX),
|
14
|
+
'ORDER BY': (',{}', '', PATTERN_SUFFIX),
|
16
15
|
'LIMIT': (' ', '', ''),
|
17
16
|
}
|
18
17
|
# ^ ^ ^
|
@@ -69,16 +68,18 @@ class SQLObject:
|
|
69
68
|
appendix = {WHERE: 'and|', FROM: 'join|JOIN'}
|
70
69
|
return KEYWORD[key][0].format(appendix.get(key, ''))
|
71
70
|
|
72
|
-
def diff(self, key: str, search_list: list,
|
71
|
+
def diff(self, key: str, search_list: list, exact: bool=False) -> set:
|
73
72
|
def cleanup(fld: str) -> str:
|
74
|
-
if
|
73
|
+
if exact:
|
75
74
|
fld = fld.lower()
|
76
75
|
return fld.strip()
|
77
76
|
def is_named_field(fld: str) -> bool:
|
78
77
|
return key == SELECT and re.search(' as | AS ', fld)
|
79
78
|
pattern = KEYWORD[key][2]
|
80
|
-
if
|
81
|
-
|
79
|
+
if exact:
|
80
|
+
if key == WHERE:
|
81
|
+
pattern = ' '
|
82
|
+
pattern += f'|{PATTERN_PREFIX}'
|
82
83
|
separator = self.get_separator(key)
|
83
84
|
def field_set(source: list) -> set:
|
84
85
|
return set(
|
@@ -91,7 +92,7 @@ class SQLObject:
|
|
91
92
|
)
|
92
93
|
s1 = field_set(search_list)
|
93
94
|
s2 = field_set(self.values.get(key, []))
|
94
|
-
if
|
95
|
+
if exact:
|
95
96
|
return s1.symmetric_difference(s2)
|
96
97
|
return s1 - s2
|
97
98
|
|
@@ -412,6 +413,160 @@ class Rule:
|
|
412
413
|
def apply(cls, target: 'Select'):
|
413
414
|
...
|
414
415
|
|
416
|
+
class Parser:
|
417
|
+
REGEX = {}
|
418
|
+
|
419
|
+
def prepare(self):
|
420
|
+
...
|
421
|
+
|
422
|
+
def __init__(self, txt: str, class_type):
|
423
|
+
self.queries = []
|
424
|
+
if not self.REGEX:
|
425
|
+
self.prepare()
|
426
|
+
self.class_type = class_type
|
427
|
+
self.eval(txt)
|
428
|
+
|
429
|
+
def eval(self, txt: str):
|
430
|
+
...
|
431
|
+
|
432
|
+
|
433
|
+
class SQLParser(Parser):
|
434
|
+
REGEX = {}
|
435
|
+
|
436
|
+
def prepare(self):
|
437
|
+
keywords = '|'.join(k + r'\b' for k in KEYWORD)
|
438
|
+
flags = re.IGNORECASE + re.MULTILINE
|
439
|
+
self.REGEX['keywords'] = re.compile(f'({keywords}|[*])', flags)
|
440
|
+
self.REGEX['subquery'] = re.compile(r'(\w\.)*\w+ +in +\(SELECT.*?\)', flags)
|
441
|
+
|
442
|
+
def eval(self, txt: str):
|
443
|
+
def find_last_word(pos: int) -> int:
|
444
|
+
SPACE, WORD = 1, 2
|
445
|
+
found = set()
|
446
|
+
for i in range(pos, 0, -1):
|
447
|
+
if txt[i] in [' ', '\t', '\n']:
|
448
|
+
if sum(found) == 3:
|
449
|
+
return i
|
450
|
+
found.add(SPACE)
|
451
|
+
if txt[i].isalpha():
|
452
|
+
found.add(WORD)
|
453
|
+
elif txt[i] == '.':
|
454
|
+
found.remove(WORD)
|
455
|
+
def find_parenthesis(pos: int) -> int:
|
456
|
+
for i in range(pos, len(txt)-1):
|
457
|
+
if txt[i] == ')':
|
458
|
+
return i+1
|
459
|
+
result = {}
|
460
|
+
found = self.REGEX['subquery'].search(txt)
|
461
|
+
while found:
|
462
|
+
start, end = found.span()
|
463
|
+
inner = txt[start: end]
|
464
|
+
if inner.count('(') > inner.count(')'):
|
465
|
+
end = find_parenthesis(end)
|
466
|
+
inner = txt[start: end-1]
|
467
|
+
fld, *inner = re.split(r' IN | in', inner, maxsplit=1)
|
468
|
+
if fld.upper() == 'NOT':
|
469
|
+
pos = find_last_word(start)
|
470
|
+
fld = txt[pos: start].strip() # [To-Do] Use the value of `fld`
|
471
|
+
start = pos
|
472
|
+
target_class = NotSelectIN
|
473
|
+
else:
|
474
|
+
target_class = SelectIN
|
475
|
+
obj = SQLParser(
|
476
|
+
' '.join(re.sub(r'^\(', '', s.strip()) for s in inner),
|
477
|
+
class_type=target_class
|
478
|
+
).queries[0]
|
479
|
+
result[obj.alias] = obj
|
480
|
+
txt = txt[:start-1] + txt[end+1:]
|
481
|
+
found = self.REGEX['subquery'].search(txt)
|
482
|
+
tokens = [t.strip() for t in self.REGEX['keywords'].split(txt) if t.strip()]
|
483
|
+
values = {k.upper(): v for k, v in zip(tokens[::2], tokens[1::2])}
|
484
|
+
tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
|
485
|
+
for item in tables:
|
486
|
+
if '=' in item:
|
487
|
+
a1, f1, a2, f2 = [r.strip() for r in re.split('[().=]', item) if r]
|
488
|
+
obj1: SQLObject = result[a1]
|
489
|
+
obj2: SQLObject = result[a2]
|
490
|
+
PrimaryKey.add(f2, obj2)
|
491
|
+
ForeignKey(obj2.table_name).add(f1, obj1)
|
492
|
+
else:
|
493
|
+
obj = self.class_type(item)
|
494
|
+
for key in USUAL_KEYS:
|
495
|
+
if not key in values:
|
496
|
+
continue
|
497
|
+
separator = self.class_type.get_separator(key)
|
498
|
+
obj.values[key] = [
|
499
|
+
Field.format(fld, obj)
|
500
|
+
for fld in re.split(separator, values[key])
|
501
|
+
if (fld != '*' and len(tables) == 1) or obj.match(fld)
|
502
|
+
]
|
503
|
+
result[obj.alias] = obj
|
504
|
+
self.queries = list( result.values() )
|
505
|
+
|
506
|
+
|
507
|
+
class Cypher(Parser):
|
508
|
+
REGEX = {}
|
509
|
+
TOKEN_METHODS = {}
|
510
|
+
|
511
|
+
def prepare(self):
|
512
|
+
self.REGEX['separator'] = re.compile(r'([(,?)^]|->|<-)')
|
513
|
+
self.REGEX['condition'] = re.compile(r'(^\w+)|([<>=])')
|
514
|
+
self.TOKEN_METHODS = {
|
515
|
+
'(': self.add_field, '?': self.add_where,
|
516
|
+
',': self.add_field, '^': self.add_order,
|
517
|
+
')': self.new_query, '->': self.left_ftable,
|
518
|
+
'<-': self.right_ftable,
|
519
|
+
}
|
520
|
+
|
521
|
+
def new_query(self, token: str):
|
522
|
+
if token.isidentifier():
|
523
|
+
self.queries.append( self.class_type(token) )
|
524
|
+
|
525
|
+
def add_where(self, token: str):
|
526
|
+
field, *condition = [
|
527
|
+
t for t in self.REGEX['condition'].split(token) if t
|
528
|
+
]
|
529
|
+
Where(' '.join(condition)).add(field, self.queries[-1])
|
530
|
+
|
531
|
+
def add_order(self, token: str):
|
532
|
+
FieldList(token, [Field, OrderBy]).add('', self.queries[-1])
|
533
|
+
|
534
|
+
def add_field(self, token: str):
|
535
|
+
FieldList(token, [Field]).add('', self.queries[-1])
|
536
|
+
|
537
|
+
def left_ftable(self, token: str):
|
538
|
+
self.new_query(token)
|
539
|
+
self.join_type = JoinType.LEFT
|
540
|
+
|
541
|
+
def right_ftable(self, token: str):
|
542
|
+
self.new_query(token)
|
543
|
+
self.join_type = JoinType.RIGHT
|
544
|
+
|
545
|
+
def add_foreign_key(self, token: str):
|
546
|
+
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:
|
550
|
+
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
|
556
|
+
|
557
|
+
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:
|
562
|
+
continue
|
563
|
+
if self.method:
|
564
|
+
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)
|
569
|
+
|
415
570
|
|
416
571
|
class JoinType(Enum):
|
417
572
|
INNER = ''
|
@@ -503,73 +658,8 @@ class Select(SQLObject):
|
|
503
658
|
return re.findall(f'\b*{self.alias}[.]', expr) != []
|
504
659
|
|
505
660
|
@classmethod
|
506
|
-
def parse(cls, txt: str) -> list[SQLObject]:
|
507
|
-
|
508
|
-
SPACE, WORD = 1, 2
|
509
|
-
found = set()
|
510
|
-
for i in range(pos, 0, -1):
|
511
|
-
if txt[i] in [' ', '\t', '\n']:
|
512
|
-
if sum(found) == 3:
|
513
|
-
return i
|
514
|
-
found.add(SPACE)
|
515
|
-
if txt[i].isalpha():
|
516
|
-
found.add(WORD)
|
517
|
-
elif txt[i] == '.':
|
518
|
-
found.remove(WORD)
|
519
|
-
def find_parenthesis(pos: int) -> int:
|
520
|
-
for i in range(pos, len(txt)-1):
|
521
|
-
if txt[i] == ')':
|
522
|
-
return i+1
|
523
|
-
if not cls.REGEX:
|
524
|
-
keywords = '|'.join(k + r'\b' for k in KEYWORD)
|
525
|
-
flags = re.IGNORECASE + re.MULTILINE
|
526
|
-
cls.REGEX['keywords'] = re.compile(f'({keywords}|[*])', flags)
|
527
|
-
cls.REGEX['subquery'] = re.compile(r'(\w\.)*\w+ +in +\(SELECT.*?\)', flags)
|
528
|
-
result = {}
|
529
|
-
found = cls.REGEX['subquery'].search(txt)
|
530
|
-
while found:
|
531
|
-
start, end = found.span()
|
532
|
-
inner = txt[start: end]
|
533
|
-
if inner.count('(') > inner.count(')'):
|
534
|
-
end = find_parenthesis(end)
|
535
|
-
inner = txt[start: end-1]
|
536
|
-
fld, *inner = re.split(r' IN | in', inner, maxsplit=1)
|
537
|
-
if fld.upper() == 'NOT':
|
538
|
-
pos = find_last_word(start)
|
539
|
-
fld = txt[pos: start].strip() # [To-Do] Use the value of `fld`
|
540
|
-
start = pos
|
541
|
-
class_type = NotSelectIN
|
542
|
-
else:
|
543
|
-
class_type = SelectIN
|
544
|
-
obj = class_type.parse(
|
545
|
-
' '.join(re.sub(r'^\(', '', s.strip()) for s in inner)
|
546
|
-
)[0]
|
547
|
-
result[obj.alias] = obj
|
548
|
-
txt = txt[:start-1] + txt[end+1:]
|
549
|
-
found = cls.REGEX['subquery'].search(txt)
|
550
|
-
tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if t.strip()]
|
551
|
-
values = {k.upper(): v for k, v in zip(tokens[::2], tokens[1::2])}
|
552
|
-
tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
|
553
|
-
for item in tables:
|
554
|
-
if '=' in item:
|
555
|
-
a1, f1, a2, f2 = [r.strip() for r in re.split('[().=]', item) if r]
|
556
|
-
obj1: SQLObject = result[a1]
|
557
|
-
obj2: SQLObject = result[a2]
|
558
|
-
PrimaryKey.add(f2, obj2)
|
559
|
-
ForeignKey(obj2.table_name).add(f1, obj1)
|
560
|
-
else:
|
561
|
-
obj = cls(item)
|
562
|
-
for key in USUAL_KEYS:
|
563
|
-
if not key in values:
|
564
|
-
continue
|
565
|
-
separator = cls.get_separator(key)
|
566
|
-
obj.values[key] = [
|
567
|
-
Field.format(fld, obj)
|
568
|
-
for fld in re.split(separator, values[key])
|
569
|
-
if (fld != '*' and len(tables) == 1) or obj.match(fld)
|
570
|
-
]
|
571
|
-
result[obj.alias] = obj
|
572
|
-
return list( result.values() )
|
661
|
+
def parse(cls, txt: str, parser: Parser = SQLParser) -> list[SQLObject]:
|
662
|
+
return parser(txt, cls).queries
|
573
663
|
|
574
664
|
def optimize(self, rules: list[Rule]=None):
|
575
665
|
if not rules:
|
@@ -646,7 +736,7 @@ class RuleLogicalOp(Rule):
|
|
646
736
|
))
|
647
737
|
for i, condition in enumerate(target.values.get(WHERE, [])):
|
648
738
|
expr = re.sub('\n|\t', ' ', condition)
|
649
|
-
if not re.search(r'\b(NOT|not)
|
739
|
+
if not re.search(r'\b(NOT|not).*[<>=]', expr):
|
650
740
|
continue
|
651
741
|
tokens = [t.strip() for t in re.split(r'NOT\b|not\b|(<|>|=)', expr) if t]
|
652
742
|
op = ''.join(tokens[1: len(tokens)-1])
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sql_blocks
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
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
|
@@ -62,11 +62,13 @@ You can specify your own alias: `a = Select('Actor a')`
|
|
62
62
|
* field=gt(value) - ...the field is GREATER than the value;
|
63
63
|
* field=lt(value) - ...the field is LESS than the value;
|
64
64
|
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
> You may use Where.**eq**, Where.**gt**, Where.**lt** ... or simply **eq**, **gt**, **lt** ... 😉
|
66
|
+
|
67
|
+
3.1 -- If you want to filter the field on a range of values:
|
68
|
+
|
69
|
+
`a = Select( 'Actor a', age=Between(45, 69) )`
|
68
70
|
|
69
|
-
|
71
|
+
3.2 -- Sub-queries:
|
70
72
|
```
|
71
73
|
query = Select('Movie m', title=Field,
|
72
74
|
id=SelectIN(
|
@@ -0,0 +1,7 @@
|
|
1
|
+
sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
|
2
|
+
sql_blocks/sql_blocks.py,sha256=xWP7coiy5Tj8YU_WgtkN8J8tc9-0_BaE-bYljPdEp6o,24361
|
3
|
+
sql_blocks-0.2.3.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
4
|
+
sql_blocks-0.2.3.dist-info/METADATA,sha256=0bRpoB8KehBZ40kf-pXyOvrVcozTUZo-BsC3Aq5xn2I,8892
|
5
|
+
sql_blocks-0.2.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
6
|
+
sql_blocks-0.2.3.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
|
7
|
+
sql_blocks-0.2.3.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
|
2
|
-
sql_blocks/sql_blocks.py,sha256=5WBEFqik86gcV4WwPHx4R2e2vMeA1D_et1C9oEMnoQw,21600
|
3
|
-
sql_blocks-0.2.1.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
4
|
-
sql_blocks-0.2.1.dist-info/METADATA,sha256=g5YLLFr736iWjO8dD0pO7nph5xEbyG6fynoAKo9-XtI,8804
|
5
|
-
sql_blocks-0.2.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
6
|
-
sql_blocks-0.2.1.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
|
7
|
-
sql_blocks-0.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|