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 CHANGED
@@ -4,15 +4,14 @@ import re
4
4
 
5
5
  PATTERN_PREFIX = '([^0-9 ]+[.])'
6
6
  PATTERN_SUFFIX = '( [A-Za-z_]+)'
7
- SUFFIX_AND_PRE = f'{PATTERN_SUFFIX}|{PATTERN_PREFIX}'
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': (',{}', '', SUFFIX_AND_PRE),
15
- 'ORDER BY': (',{}', '', SUFFIX_AND_PRE),
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, symmetrical: bool=False) -> set:
71
+ def diff(self, key: str, search_list: list, exact: bool=False) -> set:
73
72
  def cleanup(fld: str) -> str:
74
- if symmetrical:
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 key == WHERE and symmetrical:
81
- pattern = f'{PATTERN_PREFIX}| '
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 symmetrical:
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
- def find_last_word(pos: int) -> int:
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)\b', expr):
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.1
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
- 3.1 -- If you want to filter the field on a range of values:
66
-
67
- `a = Select( 'Actor a', age=Between(45, 69) )`
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
- 3.2 -- Sub-queries:
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,,