sql-blocks 0.0.2__tar.gz → 0.0.4__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.0.2
3
+ Version: 0.0.4
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
@@ -53,10 +53,9 @@ You can specify your own alias: `a = Select('Actor a')`
53
53
  `a = Select( 'Actor a', age=Between(45, 69) )`
54
54
 
55
55
  3.2 -- Sub-queries:
56
-
57
56
  ```
58
57
  query = Select('Movie m', title=Field,
59
- id=SubSelect(
58
+ id=SelectIN(
60
59
  'Review r',
61
60
  rate=Where.gt(4.5),
62
61
  movie_id=Distinct
@@ -85,6 +84,16 @@ query = Select('Movie m', title=Field,
85
84
  ```
86
85
  > Could be AND=Options(...)
87
86
 
87
+ 3.4 -- Negative conditions use the _Not_ class instead of _Where_
88
+ ```
89
+ based_on_book=Not.is_null()
90
+ ```
91
+
92
+ 3.5 -- List of values
93
+ ```
94
+ hash_tag=Where.list(['space', 'monster', 'gore'])
95
+ ```
96
+
88
97
  ---
89
98
  ### 4 - A field can be two things at the same time:
90
99
 
@@ -92,7 +101,7 @@ query = Select('Movie m', title=Field,
92
101
  - This means that the field will appear in the results and also that the query will be ordered by that field.
93
102
  * Applying **GROUP BY** to item 3.2, it would look like this:
94
103
  ```
95
- SubSelect(
104
+ SelectIN(
96
105
  'Review r', movie=[GroupBy, Distinct],
97
106
  rate=Having.avg(Where.gt(4.5))
98
107
  )
@@ -274,7 +283,7 @@ m = Select...
274
283
 
275
284
  9.3
276
285
  ```
277
- best_movies = SubSelect(
286
+ best_movies = SelectIN(
278
287
  Review=Table('role'),
279
288
  rate=[GroupBy, Having.avg(Where.gt(4.5))]
280
289
  )
@@ -292,5 +301,15 @@ m2 = Select(
292
301
  **m1 == m2 # --- True!**
293
302
 
294
303
  ---
295
- ---
296
304
 
305
+ ### 10 - CASE...WHEN...THEN
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
+ )
315
+ )
@@ -38,10 +38,9 @@ You can specify your own alias: `a = Select('Actor a')`
38
38
  `a = Select( 'Actor a', age=Between(45, 69) )`
39
39
 
40
40
  3.2 -- Sub-queries:
41
-
42
41
  ```
43
42
  query = Select('Movie m', title=Field,
44
- id=SubSelect(
43
+ id=SelectIN(
45
44
  'Review r',
46
45
  rate=Where.gt(4.5),
47
46
  movie_id=Distinct
@@ -70,6 +69,16 @@ query = Select('Movie m', title=Field,
70
69
  ```
71
70
  > Could be AND=Options(...)
72
71
 
72
+ 3.4 -- Negative conditions use the _Not_ class instead of _Where_
73
+ ```
74
+ based_on_book=Not.is_null()
75
+ ```
76
+
77
+ 3.5 -- List of values
78
+ ```
79
+ hash_tag=Where.list(['space', 'monster', 'gore'])
80
+ ```
81
+
73
82
  ---
74
83
  ### 4 - A field can be two things at the same time:
75
84
 
@@ -77,7 +86,7 @@ query = Select('Movie m', title=Field,
77
86
  - This means that the field will appear in the results and also that the query will be ordered by that field.
78
87
  * Applying **GROUP BY** to item 3.2, it would look like this:
79
88
  ```
80
- SubSelect(
89
+ SelectIN(
81
90
  'Review r', movie=[GroupBy, Distinct],
82
91
  rate=Having.avg(Where.gt(4.5))
83
92
  )
@@ -259,7 +268,7 @@ m = Select...
259
268
 
260
269
  9.3
261
270
  ```
262
- best_movies = SubSelect(
271
+ best_movies = SelectIN(
263
272
  Review=Table('role'),
264
273
  rate=[GroupBy, Having.avg(Where.gt(4.5))]
265
274
  )
@@ -277,5 +286,15 @@ m2 = Select(
277
286
  **m1 == m2 # --- True!**
278
287
 
279
288
  ---
280
- ---
281
289
 
290
+ ### 10 - CASE...WHEN...THEN
291
+ Select(
292
+ 'Product',
293
+ label=Case('price').when(
294
+ lt(50), 'cheap'
295
+ ).when(
296
+ gt(100), 'expensive'
297
+ ).else_value(
298
+ 'normal'
299
+ )
300
+ )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql_blocks"
3
- version = "0.0.2"
3
+ version = "0.0.4"
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.0.2',
6
+ version = '0.0.4',
7
7
  author = 'Júlio Cascalles',
8
8
  author_email = 'julio.cascalles@outlook.com',
9
9
  packages = ['sql_blocks'],
@@ -1,4 +1,6 @@
1
1
  from enum import Enum
2
+ import re
3
+
2
4
 
3
5
  KEYWORD = {
4
6
  'SELECT': (',{}', 'SELECT *'),
@@ -54,9 +56,10 @@ class Field:
54
56
 
55
57
  @classmethod
56
58
  def format(cls, name: str, main: SQLObject) -> str:
59
+ name = name.strip()
57
60
  if name == '_':
58
61
  name = '*'
59
- else:
62
+ elif '.' not in name:
60
63
  name = f'{main.alias}.{name}'
61
64
  if Function in cls.__bases__:
62
65
  name = f'{cls.__name__}({name})'
@@ -124,14 +127,19 @@ class ForeignKey:
124
127
  def __init__(self, table_name: str):
125
128
  self.table_name = table_name
126
129
 
130
+ @staticmethod
131
+ def get_key(obj1: SQLObject, obj2: SQLObject) -> tuple:
132
+ return obj1.table_name, obj2.table_name
133
+
127
134
  def add(self, name: str, main: SQLObject):
128
- key = f'{main.table_name}_{self.table_name}'
129
- ForeignKey.references[key] = name
135
+ key = self.get_key(main, self)
136
+ ForeignKey.references[key] = (name, '')
130
137
 
131
138
  @classmethod
132
- def find(cls, obj1: SQLObject, obj2: SQLObject) -> list:
133
- key = f'{obj1.table_name}_{obj2.table_name}'
134
- return cls.references.get(key, '').split('_')
139
+ def find(cls, obj1: SQLObject, obj2: SQLObject) -> tuple:
140
+ key = cls.get_key(obj1, obj2)
141
+ a, b = cls.references.get(key, ('', ''))
142
+ return a, (b or obj2.key_field)
135
143
 
136
144
 
137
145
  def quoted(value) -> str:
@@ -314,7 +322,7 @@ class JoinType(Enum):
314
322
 
315
323
  class Select(SQLObject):
316
324
  join_type: JoinType = JoinType.INNER
317
- REGEX = None
325
+ REGEX = {}
318
326
 
319
327
  def __init__(self, table_name: str='', **values):
320
328
  super().__init__(table_name)
@@ -341,17 +349,17 @@ class Select(SQLObject):
341
349
  update_values(key, self.values.get(key, []))
342
350
 
343
351
  def __add__(self, other: SQLObject):
344
- foreign_field, *primary_key = ForeignKey.find(self, other)
352
+ foreign_field, primary_key = ForeignKey.find(self, other)
345
353
  if not foreign_field:
346
- foreign_field, *primary_key = ForeignKey.find(other, self)
354
+ foreign_field, primary_key = ForeignKey.find(other, self)
347
355
  if foreign_field:
348
356
  if primary_key:
349
- PrimaryKey.add(primary_key[0], self)
357
+ PrimaryKey.add(primary_key, self)
350
358
  self.add(foreign_field, other)
351
359
  return other
352
360
  raise ValueError(f'No relationship found between {self.table_name} and {other.table_name}.')
353
361
  elif primary_key:
354
- PrimaryKey.add(primary_key[0], other)
362
+ PrimaryKey.add(primary_key, other)
355
363
  other.add(foreign_field, self)
356
364
  return self
357
365
 
@@ -391,15 +399,52 @@ class Select(SQLObject):
391
399
 
392
400
  @classmethod
393
401
  def parse(cls, txt: str) -> list[SQLObject]:
394
- import re
395
- txt = re.sub(' +', ' ', re.sub('\n|\t', ' ', txt))
402
+ def find_last_word(pos: int) -> int:
403
+ SPACE, WORD = 1, 2
404
+ found = set()
405
+ for i in range(pos, 0, -1):
406
+ if txt[i] in [' ', '\t', '\n']:
407
+ if sum(found) == 3:
408
+ return i
409
+ found.add(SPACE)
410
+ if txt[i].isalpha():
411
+ found.add(WORD)
412
+ elif txt[i] == '.':
413
+ found.remove(WORD)
414
+ def find_parenthesis(pos: int) -> int:
415
+ for i in range(pos, len(txt)-1):
416
+ if txt[i] == ')':
417
+ return i+1
396
418
  if not cls.REGEX:
397
- keywords = '|'.join(KEYWORD)
398
- cls.REGEX = re.compile(f'({keywords})', re.IGNORECASE)
399
- tokens = [t.strip() for t in cls.REGEX.split(txt) if t.strip()]
400
- values = {k: v for k, v in zip(tokens[::2], tokens[1::2])}
401
- tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
419
+ keywords = '|'.join(k + r'\b' for k in KEYWORD)
420
+ flags = re.IGNORECASE + re.MULTILINE
421
+ cls.REGEX['keywords'] = re.compile(f'({keywords})', flags)
422
+ cls.REGEX['subquery'] = re.compile(r'(\w\.)*\w+ +in +\(SELECT.*?\)', flags)
402
423
  result = {}
424
+ found = cls.REGEX['subquery'].search(txt)
425
+ while found:
426
+ start, end = found.span()
427
+ inner = txt[start: end]
428
+ if inner.count('(') > inner.count(')'):
429
+ end = find_parenthesis(end)
430
+ inner = txt[start: end]
431
+ fld, *inner = re.split(r' IN | in', inner, maxsplit=1)
432
+ if fld.upper() == 'NOT':
433
+ pos = find_last_word(start)
434
+ fld = txt[pos: start].strip() # [To-Do] Use the value of `fld`
435
+ start = pos
436
+ class_type = NotSelectIN
437
+ else:
438
+ class_type = SelectIN
439
+ obj = class_type.parse(
440
+ ' '.join(re.sub(r'^\(', '', s.strip()) for s in inner)
441
+ )[0]
442
+ result[obj.alias] = obj
443
+ txt = txt[:start-1] + txt[end+1:]
444
+ found = cls.REGEX['subquery'].search(txt)
445
+ tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if re.findall(r'\w+', t)]
446
+ values = {k.upper(): v for k, v in zip(tokens[::2], tokens[1::2])}
447
+ tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
403
448
  for item in tables:
404
449
  if '=' in item:
405
450
  a1, f1, a2, f2 = [r.strip() for r in re.split('[().=]', item) if r]
@@ -412,23 +457,55 @@ class Select(SQLObject):
412
457
  for key in USUAL_KEYS:
413
458
  if not key in values:
414
459
  continue
415
- separator = KEYWORD[key][0].format(
416
- ' ' if key == WHERE else ''
417
- )
460
+ separator = KEYWORD[key][0].format('')
418
461
  fields = [
419
- fld.strip() for fld in re.split(
462
+ Field.format(fld, obj)
463
+ for fld in re.split(
420
464
  separator, values[key]
421
465
  ) if len(tables) == 1
422
- or re.findall(f'^[( ]*{obj.alias}\.', fld)
423
- ]
424
- obj.values[key] = [
425
- f'{obj.alias}.{f}' if not '.' in f else f for f in fields
466
+ or re.findall(f'\b*{obj.alias}[.]', fld)
426
467
  ]
468
+ obj.values[key] = [ f for f in fields if f.strip() ]
427
469
  result[obj.alias] = obj
428
470
  return list( result.values() )
429
471
 
472
+ class SelectIN(Select):
473
+ condition_class = Where
430
474
 
431
- class SubSelect(Select):
432
475
  def add(self, name: str, main: SQLObject):
433
476
  self.break_lines = False
434
- Where.list(self).add(name, main)
477
+ self.condition_class.list(self).add(name, main)
478
+
479
+ SubSelect = SelectIN
480
+
481
+ class NotSelectIN(SelectIN):
482
+ condition_class = Not
483
+
484
+
485
+ if __name__ == "__main__":
486
+ query_list = Select.parse("""
487
+ SELECT
488
+ cas.role,
489
+ m.title,
490
+ m.release_date,
491
+ a.name as actors_name
492
+ FROM
493
+ Actor a
494
+ LEFT JOIN Cast cas ON (a.cast = cas.id)
495
+ LEFT JOIN Movie m ON (cas.movie = m.id)
496
+ WHERE
497
+ m.genre NOT in (SELECT g.id from Genres g where g.name in ('sci-fi', 'horror', 'distopia'))
498
+ AND (m.hashtag = '#cult' OR m.awards LIKE '%Oscar%')
499
+ AND m.id IN (select DISTINCT r.movie FROM Review r GROUP BY r.movie HAVING Avg(r.rate) > 4.5)
500
+ AND a.age <= 69 AND a.age >= 45
501
+ ORDER BY
502
+ m.release_date
503
+ """)
504
+ for query in query_list:
505
+ descr = ' {} ({}) '.format(
506
+ query.table_name,
507
+ query.__class__.__name__
508
+ )
509
+ print(descr.center(50, '-'))
510
+ print(query)
511
+ print('='*50)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 0.0.2
3
+ Version: 0.0.4
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
@@ -53,10 +53,9 @@ You can specify your own alias: `a = Select('Actor a')`
53
53
  `a = Select( 'Actor a', age=Between(45, 69) )`
54
54
 
55
55
  3.2 -- Sub-queries:
56
-
57
56
  ```
58
57
  query = Select('Movie m', title=Field,
59
- id=SubSelect(
58
+ id=SelectIN(
60
59
  'Review r',
61
60
  rate=Where.gt(4.5),
62
61
  movie_id=Distinct
@@ -85,6 +84,16 @@ query = Select('Movie m', title=Field,
85
84
  ```
86
85
  > Could be AND=Options(...)
87
86
 
87
+ 3.4 -- Negative conditions use the _Not_ class instead of _Where_
88
+ ```
89
+ based_on_book=Not.is_null()
90
+ ```
91
+
92
+ 3.5 -- List of values
93
+ ```
94
+ hash_tag=Where.list(['space', 'monster', 'gore'])
95
+ ```
96
+
88
97
  ---
89
98
  ### 4 - A field can be two things at the same time:
90
99
 
@@ -92,7 +101,7 @@ query = Select('Movie m', title=Field,
92
101
  - This means that the field will appear in the results and also that the query will be ordered by that field.
93
102
  * Applying **GROUP BY** to item 3.2, it would look like this:
94
103
  ```
95
- SubSelect(
104
+ SelectIN(
96
105
  'Review r', movie=[GroupBy, Distinct],
97
106
  rate=Having.avg(Where.gt(4.5))
98
107
  )
@@ -274,7 +283,7 @@ m = Select...
274
283
 
275
284
  9.3
276
285
  ```
277
- best_movies = SubSelect(
286
+ best_movies = SelectIN(
278
287
  Review=Table('role'),
279
288
  rate=[GroupBy, Having.avg(Where.gt(4.5))]
280
289
  )
@@ -292,5 +301,15 @@ m2 = Select(
292
301
  **m1 == m2 # --- True!**
293
302
 
294
303
  ---
295
- ---
296
304
 
305
+ ### 10 - CASE...WHEN...THEN
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
+ )
315
+ )
@@ -68,8 +68,8 @@ def single_text_to_objects():
68
68
 
69
69
  def many_texts_to_objects():
70
70
  ForeignKey.references = {
71
- 'Actor_Cast': 'cast_id',
72
- 'Cast_Movie': 'movie_id',
71
+ ('Actor', 'Cast'): ('cast', 'id'),
72
+ ('Cast', 'Movie'): ('movie', 'id'),
73
73
  }
74
74
  actor = Select.parse('''
75
75
  SELECT name as actors_name FROM Actor a
File without changes
File without changes