sql-blocks 0.0.3__py3-none-any.whl → 0.0.5__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
@@ -1,13 +1,28 @@
1
1
  from enum import Enum
2
+ import re
3
+
4
+
5
+ PATTERN_PREFIX = '([^0-9 ]+[.])'
6
+ PATTERN_SUFFIX = '( [A-Za-z_]+)'
7
+ SUFFIX_AND_PRE = f'{PATTERN_SUFFIX}|{PATTERN_PREFIX}'
8
+ DISTINCT_SF_PR = f'(DISTINCT|distinct)|{SUFFIX_AND_PRE}'
2
9
 
3
10
  KEYWORD = {
4
- 'SELECT': (',{}', 'SELECT *'),
5
- 'FROM': ('{}', ''),
6
- 'WHERE': ('{}AND ', ''),
7
- 'GROUP BY': (',{}', ''),
8
- 'ORDER BY': (',{}', ''),
9
- 'LIMIT': (' ', ''),
11
+ 'SELECT': (',{}', 'SELECT *', DISTINCT_SF_PR),
12
+ 'FROM': ('{}', '', PATTERN_SUFFIX),
13
+ 'WHERE': ('{}AND ', '', PATTERN_PREFIX),
14
+ 'GROUP BY': (',{}', '', SUFFIX_AND_PRE),
15
+ 'ORDER BY': (',{}', '', SUFFIX_AND_PRE),
16
+ 'LIMIT': (' ', '', ''),
10
17
  }
18
+ # ^ ^ ^
19
+ # | | |
20
+ # | | +----- pattern to compare fields
21
+ # | |
22
+ # | +----- default when empty (SELECT * ...)
23
+ # |
24
+ # +-------- separator
25
+
11
26
  SELECT, FROM, WHERE, GROUP_BY, ORDER_BY, LIMIT = KEYWORD.keys()
12
27
  USUAL_KEYS = [SELECT, WHERE, GROUP_BY, ORDER_BY]
13
28
 
@@ -37,6 +52,26 @@ class SQLObject:
37
52
  def table_name(self) -> str:
38
53
  return self.values[FROM][0].split()[0]
39
54
 
55
+ @staticmethod
56
+ def get_separator(key: str) -> str:
57
+ appendix = {WHERE: 'and|', FROM: 'join|JOIN'}
58
+ return KEYWORD[key][0].format(appendix.get(key, ''))
59
+
60
+ def diff(self, key: str, search_list: list, symmetrical: bool=False) -> set:
61
+ pattern = KEYWORD[key][2]
62
+ separator = self.get_separator(key)
63
+ def field_set(source: list) -> set:
64
+ return set(
65
+ re.sub(pattern, '', fld.strip()).lower()
66
+ for string in source
67
+ for fld in re.split(separator, string)
68
+ )
69
+ s1 = field_set(search_list)
70
+ s2 = field_set(self.values.get(key, []))
71
+ if symmetrical:
72
+ return s1.symmetric_difference(s2)
73
+ return s1 - s2
74
+
40
75
  def delete(self, search: str, keys: list=USUAL_KEYS):
41
76
  for key in keys:
42
77
  result = []
@@ -54,9 +89,10 @@ class Field:
54
89
 
55
90
  @classmethod
56
91
  def format(cls, name: str, main: SQLObject) -> str:
92
+ name = name.strip()
57
93
  if name == '_':
58
94
  name = '*'
59
- else:
95
+ elif '.' not in name:
60
96
  name = f'{main.alias}.{name}'
61
97
  if Function in cls.__bases__:
62
98
  name = f'{cls.__name__}({name})'
@@ -319,20 +355,19 @@ class JoinType(Enum):
319
355
 
320
356
  class Select(SQLObject):
321
357
  join_type: JoinType = JoinType.INNER
322
- REGEX = None
358
+ REGEX = {}
323
359
 
324
360
  def __init__(self, table_name: str='', **values):
325
361
  super().__init__(table_name)
326
362
  self.__call__(**values)
327
363
  self.break_lines = True
328
364
 
365
+ def update_values(self, key: str, new_values: list):
366
+ for value in self.diff(key, new_values):
367
+ self.values.setdefault(key, []).append(value)
368
+
329
369
  def add(self, name: str, main: SQLObject):
330
- def update_values(key: str, new_values: list):
331
- for value in new_values:
332
- old_values = main.values.get(key, [])
333
- if value not in old_values:
334
- main.values[key] = old_values + [value]
335
- update_values(
370
+ main.update_values(
336
371
  FROM, [
337
372
  '{jt}JOIN {tb} {a2} ON ({a1}.{f1} = {a2}.{f2})'.format(
338
373
  jt=self.join_type.value,
@@ -343,14 +378,18 @@ class Select(SQLObject):
343
378
  ] + self.values[FROM][1:]
344
379
  )
345
380
  for key in USUAL_KEYS:
346
- update_values(key, self.values.get(key, []))
381
+ main.update_values(key, self.values.get(key, []))
347
382
 
348
383
  def __add__(self, other: SQLObject):
384
+ if self.table_name.lower() == other.table_name.lower():
385
+ for key in USUAL_KEYS:
386
+ self.update_values(key, other.values.get(key, []))
387
+ return self
349
388
  foreign_field, primary_key = ForeignKey.find(self, other)
350
389
  if not foreign_field:
351
390
  foreign_field, primary_key = ForeignKey.find(other, self)
352
391
  if foreign_field:
353
- if primary_key and not self.key_field:
392
+ if primary_key:
354
393
  PrimaryKey.add(primary_key, self)
355
394
  self.add(foreign_field, other)
356
395
  return other
@@ -380,10 +419,8 @@ class Select(SQLObject):
380
419
  return self
381
420
 
382
421
  def __eq__(self, other: SQLObject) -> bool:
383
- def sorted_values(obj: SQLObject, key: str) -> list:
384
- return sorted(obj.values.get(key, []))
385
422
  for key in KEYWORD:
386
- if sorted_values(self, key) != sorted_values(other, key):
423
+ if self.diff(key, other.values.get(key, []), True):
387
424
  return False
388
425
  return True
389
426
 
@@ -396,15 +433,52 @@ class Select(SQLObject):
396
433
 
397
434
  @classmethod
398
435
  def parse(cls, txt: str) -> list[SQLObject]:
399
- import re
400
- txt = re.sub(' +', ' ', re.sub('\n|\t', ' ', txt))
436
+ def find_last_word(pos: int) -> int:
437
+ SPACE, WORD = 1, 2
438
+ found = set()
439
+ for i in range(pos, 0, -1):
440
+ if txt[i] in [' ', '\t', '\n']:
441
+ if sum(found) == 3:
442
+ return i
443
+ found.add(SPACE)
444
+ if txt[i].isalpha():
445
+ found.add(WORD)
446
+ elif txt[i] == '.':
447
+ found.remove(WORD)
448
+ def find_parenthesis(pos: int) -> int:
449
+ for i in range(pos, len(txt)-1):
450
+ if txt[i] == ')':
451
+ return i+1
401
452
  if not cls.REGEX:
402
- keywords = '|'.join(KEYWORD)
403
- cls.REGEX = re.compile(f'({keywords})', re.IGNORECASE)
404
- tokens = [t.strip() for t in cls.REGEX.split(txt) if t.strip()]
405
- values = {k: v for k, v in zip(tokens[::2], tokens[1::2])}
406
- tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
453
+ keywords = '|'.join(k + r'\b' for k in KEYWORD)
454
+ flags = re.IGNORECASE + re.MULTILINE
455
+ cls.REGEX['keywords'] = re.compile(f'({keywords})', flags)
456
+ cls.REGEX['subquery'] = re.compile(r'(\w\.)*\w+ +in +\(SELECT.*?\)', flags)
407
457
  result = {}
458
+ found = cls.REGEX['subquery'].search(txt)
459
+ while found:
460
+ start, end = found.span()
461
+ inner = txt[start: end]
462
+ if inner.count('(') > inner.count(')'):
463
+ end = find_parenthesis(end)
464
+ inner = txt[start: end-1]
465
+ fld, *inner = re.split(r' IN | in', inner, maxsplit=1)
466
+ if fld.upper() == 'NOT':
467
+ pos = find_last_word(start)
468
+ fld = txt[pos: start].strip() # [To-Do] Use the value of `fld`
469
+ start = pos
470
+ class_type = NotSelectIN
471
+ else:
472
+ class_type = SelectIN
473
+ obj = class_type.parse(
474
+ ' '.join(re.sub(r'^\(', '', s.strip()) for s in inner)
475
+ )[0]
476
+ result[obj.alias] = obj
477
+ txt = txt[:start-1] + txt[end+1:]
478
+ found = cls.REGEX['subquery'].search(txt)
479
+ tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if re.findall(r'\w+', t)]
480
+ values = {k.upper(): v for k, v in zip(tokens[::2], tokens[1::2])}
481
+ tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
408
482
  for item in tables:
409
483
  if '=' in item:
410
484
  a1, f1, a2, f2 = [r.strip() for r in re.split('[().=]', item) if r]
@@ -417,23 +491,52 @@ class Select(SQLObject):
417
491
  for key in USUAL_KEYS:
418
492
  if not key in values:
419
493
  continue
420
- separator = KEYWORD[key][0].format(
421
- ' ' if key == WHERE else ''
422
- )
423
- fields = [
424
- fld.strip() for fld in re.split(
425
- separator, values[key]
426
- ) if len(tables) == 1
427
- or re.findall(f'^[( ]*{obj.alias}.', fld)
428
- ]
494
+ separator = cls.get_separator(key)
429
495
  obj.values[key] = [
430
- f'{obj.alias}.{f}' if not '.' in f else f for f in fields
496
+ Field.format(fld, obj)
497
+ for fld in re.split(separator, values[key])
498
+ if len(tables) == 1 or re.findall(f'\b*{obj.alias}[.]', fld)
431
499
  ]
432
500
  result[obj.alias] = obj
433
501
  return list( result.values() )
434
502
 
503
+ class SelectIN(Select):
504
+ condition_class = Where
435
505
 
436
- class SubSelect(Select):
437
506
  def add(self, name: str, main: SQLObject):
438
507
  self.break_lines = False
439
- Where.list(self).add(name, main)
508
+ self.condition_class.list(self).add(name, main)
509
+
510
+ SubSelect = SelectIN
511
+
512
+ class NotSelectIN(SelectIN):
513
+ condition_class = Not
514
+
515
+
516
+ if __name__ == "__main__":
517
+ query_list = Select.parse("""
518
+ SELECT
519
+ cas.role,
520
+ m.title,
521
+ m.release_date,
522
+ a.name as actors_name
523
+ FROM
524
+ Actor a
525
+ LEFT JOIN Cast cas ON (a.cast = cas.id)
526
+ LEFT JOIN Movie m ON (cas.movie = m.id)
527
+ WHERE
528
+ m.genre NOT in (SELECT g.id from Genres g where g.name in ('sci-fi', 'horror', 'distopia'))
529
+ AND (m.hashtag = '#cult' OR m.awards LIKE '%Oscar%')
530
+ AND m.id IN (select DISTINCT r.movie FROM Review r GROUP BY r.movie HAVING Avg(r.rate) > 4.5)
531
+ AND a.age <= 69 AND a.age >= 45
532
+ ORDER BY
533
+ m.release_date
534
+ """)
535
+ for query in query_list:
536
+ descr = ' {} ({}) '.format(
537
+ query.table_name,
538
+ query.__class__.__name__
539
+ )
540
+ print(descr.center(50, '-'))
541
+ print(query)
542
+ print('='*50)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 0.0.3
3
+ Version: 0.0.5
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
@@ -55,7 +55,7 @@ You can specify your own alias: `a = Select('Actor a')`
55
55
  3.2 -- Sub-queries:
56
56
  ```
57
57
  query = Select('Movie m', title=Field,
58
- id=SubSelect(
58
+ id=SelectIN(
59
59
  'Review r',
60
60
  rate=Where.gt(4.5),
61
61
  movie_id=Distinct
@@ -86,12 +86,13 @@ query = Select('Movie m', title=Field,
86
86
 
87
87
  3.4 -- Negative conditions use the _Not_ class instead of _Where_
88
88
  ```
89
- franchise
90
89
  based_on_book=Not.is_null()
91
90
  ```
92
91
 
93
92
  3.5 -- List of values
93
+ ```
94
94
  hash_tag=Where.list(['space', 'monster', 'gore'])
95
+ ```
95
96
 
96
97
  ---
97
98
  ### 4 - A field can be two things at the same time:
@@ -100,7 +101,7 @@ hash_tag=Where.list(['space', 'monster', 'gore'])
100
101
  - This means that the field will appear in the results and also that the query will be ordered by that field.
101
102
  * Applying **GROUP BY** to item 3.2, it would look like this:
102
103
  ```
103
- SubSelect(
104
+ SelectIN(
104
105
  'Review r', movie=[GroupBy, Distinct],
105
106
  rate=Having.avg(Where.gt(4.5))
106
107
  )
@@ -248,13 +249,13 @@ m = Select...
248
249
  9.1
249
250
  ```
250
251
  a1 = Select.parse('''
251
- SELECT gender, max(age) FROM Actor act
252
+ SELECT gender, Max(act.age) FROM Actor act
252
253
  WHERE act.age <= 69 AND act.age >= 45
253
254
  GROUP BY gender
254
255
  ''')[0]
255
256
 
256
257
  a2 = Select('Actor',
257
- age=Between(45, 69), gender=GroupBy,
258
+ age=[ Between(45, 69), Max ],
258
259
  gender=[GroupBy, Field]
259
260
  )
260
261
  ```
@@ -282,7 +283,7 @@ m = Select...
282
283
 
283
284
  9.3
284
285
  ```
285
- best_movies = SubSelect(
286
+ best_movies = SelectIN(
286
287
  Review=Table('role'),
287
288
  rate=[GroupBy, Having.avg(Where.gt(4.5))]
288
289
  )
@@ -302,13 +303,13 @@ m2 = Select(
302
303
  ---
303
304
 
304
305
  ### 10 - CASE...WHEN...THEN
305
- Select(
306
- 'Product',
307
- label=Case('price').when(
308
- lt(50), 'cheap'
309
- ).when(
310
- gt(100), 'expensive'
311
- ).else_value(
312
- 'normal'
306
+ Select(
307
+ 'Product',
308
+ label=Case('price').when(
309
+ lt(50), 'cheap'
310
+ ).when(
311
+ gt(100), 'expensive'
312
+ ).else_value(
313
+ 'normal'
314
+ )
313
315
  )
314
- )
@@ -0,0 +1,7 @@
1
+ sql_blocks/__init__.py,sha256=TodC5q-UEdYEz9v1RRoogVqqRcsKnZRY1WDGinrI2zo,26
2
+ sql_blocks/sql_blocks.py,sha256=lTdPxelcWQC9oRFGuDcdEm2g5r6q1typyQAc7sGjmVY,17230
3
+ sql_blocks-0.0.5.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
+ sql_blocks-0.0.5.dist-info/METADATA,sha256=xi_F_iiC8Wr8Hgk6WTp0tQ_KBiv3rDOpSA5hhVulzdk,7025
5
+ sql_blocks-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
+ sql_blocks-0.0.5.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
+ sql_blocks-0.0.5.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- sql_blocks/__init__.py,sha256=TodC5q-UEdYEz9v1RRoogVqqRcsKnZRY1WDGinrI2zo,26
2
- sql_blocks/sql_blocks.py,sha256=r_PIxS-VqaY2zuBGwBgz6Z1Z2zBrx_CrfZ35yTMb7Sw,13352
3
- sql_blocks-0.0.3.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
- sql_blocks-0.0.3.dist-info/METADATA,sha256=MHZb5SM_2RCZim4wESLsLaiCYReDOkn7tYJwGNsGWBE,6992
5
- sql_blocks-0.0.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
- sql_blocks-0.0.3.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
- sql_blocks-0.0.3.dist-info/RECORD,,