sql-blocks 0.0.6__tar.gz → 0.0.7__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.6
3
+ Version: 0.0.7
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
@@ -306,10 +306,43 @@ m2 = Select(
306
306
  Select(
307
307
  'Product',
308
308
  label=Case('price').when(
309
- Where.lt(50), 'cheap'
309
+ lt(50), 'cheap'
310
310
  ).when(
311
- Where.gt(100), 'expensive'
311
+ gt(100), 'expensive'
312
312
  ).else_value(
313
313
  'normal'
314
314
  )
315
315
  )
316
+
317
+ ---
318
+
319
+ ### 11 - optimize method
320
+ p1 = Select.parse("""
321
+ SELECT * FROM Product p
322
+ WHERE (p.category = 'Gizmo'
323
+ OR p.category = 'Gadget'
324
+ OR p.category = 'Doohickey')
325
+ AND NOT price <= 387.64
326
+ AND YEAR(last_sale) = 2024
327
+ ORDER BY
328
+ category
329
+ """)[0]
330
+ p1.optimize() # <<===============
331
+ p2 = Select.parse("""
332
+ SELECT category FROM Product p
333
+ WHERE category IN ('Gizmo','Gadget','Doohickey')
334
+ and p.price > 387.64
335
+ and p.last_sale >= '2024-01-01'
336
+ and p.last_sale <= '2024-12-31'
337
+ ORDER BY p.category LIMIT 100
338
+ """)[0]
339
+ p1 == p2 # --- True!
340
+
341
+ This will...
342
+ * Replace `OR` conditions to `SELECT IN ...`
343
+ * Put `LIMIT` if no fields or conditions defined;
344
+ * Normalizes inverted conditions;
345
+ * Auto includes fields present in `ORDER/GROUP BY`;
346
+ * Replace `YEAR` function with date range comparison.
347
+
348
+ > The method allows you to select which rules you want to apply in the optimization...Or define your own rules!
@@ -291,10 +291,43 @@ m2 = Select(
291
291
  Select(
292
292
  'Product',
293
293
  label=Case('price').when(
294
- Where.lt(50), 'cheap'
294
+ lt(50), 'cheap'
295
295
  ).when(
296
- Where.gt(100), 'expensive'
296
+ gt(100), 'expensive'
297
297
  ).else_value(
298
298
  'normal'
299
299
  )
300
300
  )
301
+
302
+ ---
303
+
304
+ ### 11 - optimize method
305
+ p1 = Select.parse("""
306
+ SELECT * FROM Product p
307
+ WHERE (p.category = 'Gizmo'
308
+ OR p.category = 'Gadget'
309
+ OR p.category = 'Doohickey')
310
+ AND NOT price <= 387.64
311
+ AND YEAR(last_sale) = 2024
312
+ ORDER BY
313
+ category
314
+ """)[0]
315
+ p1.optimize() # <<===============
316
+ p2 = Select.parse("""
317
+ SELECT category FROM Product p
318
+ WHERE category IN ('Gizmo','Gadget','Doohickey')
319
+ and p.price > 387.64
320
+ and p.last_sale >= '2024-01-01'
321
+ and p.last_sale <= '2024-12-31'
322
+ ORDER BY p.category LIMIT 100
323
+ """)[0]
324
+ p1 == p2 # --- True!
325
+
326
+ This will...
327
+ * Replace `OR` conditions to `SELECT IN ...`
328
+ * Put `LIMIT` if no fields or conditions defined;
329
+ * Normalizes inverted conditions;
330
+ * Auto includes fields present in `ORDER/GROUP BY`;
331
+ * Replace `YEAR` function with date range comparison.
332
+
333
+ > The method allows you to select which rules you want to apply in the optimization...Or define your own rules!
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql_blocks"
3
- version = "0.0.6"
3
+ version = "0.0.7"
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.6',
6
+ version = '0.0.7',
7
7
  author = 'Júlio Cascalles',
8
8
  author_email = 'julio.cascalles@outlook.com',
9
9
  packages = ['sql_blocks'],
@@ -0,0 +1 @@
1
+ from sql_blocks import *
@@ -10,26 +10,32 @@ DISTINCT_SF_PR = f'(DISTINCT|distinct)|{SUFFIX_AND_PRE}'
10
10
  KEYWORD = {
11
11
  'SELECT': (',{}', 'SELECT *', DISTINCT_SF_PR),
12
12
  'FROM': ('{}', '', PATTERN_SUFFIX),
13
- 'WHERE': ('{}AND ', '', PATTERN_PREFIX),
13
+ 'WHERE': ('{}AND ', '', f'{PATTERN_PREFIX}| '),
14
14
  'GROUP BY': (',{}', '', SUFFIX_AND_PRE),
15
15
  'ORDER BY': (',{}', '', SUFFIX_AND_PRE),
16
16
  'LIMIT': (' ', '', ''),
17
17
  }
18
- # ^ ^ ^
19
- # | | |
20
- # | | +----- pattern to compare fields
21
- # | |
22
- # | +----- default when empty (SELECT * ...)
23
- # |
24
- # +-------- separator
18
+ # ^ ^ ^
19
+ # | | |
20
+ # | | +----- pattern to compare fields
21
+ # | |
22
+ # | +----- default when empty (SELECT * ...)
23
+ # |
24
+ # +-------- separator
25
25
 
26
26
  SELECT, FROM, WHERE, GROUP_BY, ORDER_BY, LIMIT = KEYWORD.keys()
27
- USUAL_KEYS = [SELECT, WHERE, GROUP_BY, ORDER_BY]
27
+ USUAL_KEYS = [SELECT, WHERE, GROUP_BY, ORDER_BY, LIMIT]
28
28
 
29
29
 
30
30
  class SQLObject:
31
+ ALIAS_FUNC = lambda t: t.lower()[:3]
32
+ """ ^^^^^^^^^^^^^^^^^^^^^^^^
33
+ You can change the behavior by assigning
34
+ a user function to SQLObject.ALIAS_FUNC
35
+ """
36
+
31
37
  def __init__(self, table_name: str=''):
32
- self.alias = ''
38
+ self.__alias = ''
33
39
  self.values = {}
34
40
  self.key_field = ''
35
41
  self.set_table(table_name)
@@ -37,20 +43,26 @@ class SQLObject:
37
43
  def set_table(self, table_name: str):
38
44
  if not table_name:
39
45
  return
40
- if ' ' in table_name:
41
- table_name, self.alias = table_name.split()
46
+ if ' ' in table_name.strip():
47
+ table_name, self.__alias = table_name.split()
42
48
  elif '_' in table_name:
43
- self.alias = ''.join(
49
+ self.__alias = ''.join(
44
50
  word[0].lower()
45
51
  for word in table_name.split('_')
46
52
  )
47
53
  else:
48
- self.alias = table_name.lower()[:3]
54
+ self.__alias = SQLObject.ALIAS_FUNC(table_name)
49
55
  self.values.setdefault(FROM, []).append(f'{table_name} {self.alias}')
50
56
 
51
57
  @property
52
58
  def table_name(self) -> str:
53
59
  return self.values[FROM][0].split()[0]
60
+
61
+ @property
62
+ def alias(self) -> str:
63
+ if self.__alias:
64
+ return self.__alias
65
+ return self.table_name
54
66
 
55
67
  @staticmethod
56
68
  def get_separator(key: str) -> str:
@@ -90,9 +102,9 @@ class Field:
90
102
  @classmethod
91
103
  def format(cls, name: str, main: SQLObject) -> str:
92
104
  name = name.strip()
93
- if name == '_':
105
+ if name in ('_', '*'):
94
106
  name = '*'
95
- elif '.' not in name:
107
+ elif not re.findall('[.()0-9]', name):
96
108
  name = f'{main.alias}.{name}'
97
109
  if Function in cls.__bases__:
98
110
  name = f'{cls.__name__}({name})'
@@ -185,7 +197,7 @@ class Where:
185
197
  prefix = ''
186
198
 
187
199
  def __init__(self, expr: str):
188
- self.expr = f'{self.prefix}{expr}'
200
+ self.expr = expr
189
201
 
190
202
  @classmethod
191
203
  def __constructor(cls, operator: str, value):
@@ -226,8 +238,8 @@ class Where:
226
238
  return cls(f'IN ({values})')
227
239
 
228
240
  def add(self, name: str, main: SQLObject):
229
- main.values.setdefault(WHERE, []).append('{} {}'.format(
230
- Field.format(name, main), self.expr
241
+ main.values.setdefault(WHERE, []).append('{}{} {}'.format(
242
+ self.prefix, Field.format(name, main), self.expr
231
243
  ))
232
244
 
233
245
 
@@ -347,6 +359,12 @@ class Having:
347
359
  return cls(Count, condition)
348
360
 
349
361
 
362
+ class Rule:
363
+ @classmethod
364
+ def apply(cls, target: 'Select'):
365
+ ...
366
+
367
+
350
368
  class JoinType(Enum):
351
369
  INNER = ''
352
370
  LEFT = 'LEFT '
@@ -424,13 +442,16 @@ class Select(SQLObject):
424
442
  return False
425
443
  return True
426
444
 
427
- def limit(self, row_count: int, offset: int=0):
445
+ def limit(self, row_count: int=100, offset: int=0):
428
446
  result = [str(row_count)]
429
447
  if offset > 0:
430
448
  result.append(f'OFFSET {offset}')
431
449
  self.values.setdefault(LIMIT, result)
432
450
  return self
433
451
 
452
+ def match(self, expr: str) -> bool:
453
+ return re.findall(f'\b*{self.alias}[.]', expr) != []
454
+
434
455
  @classmethod
435
456
  def parse(cls, txt: str) -> list[SQLObject]:
436
457
  def find_last_word(pos: int) -> int:
@@ -452,7 +473,7 @@ class Select(SQLObject):
452
473
  if not cls.REGEX:
453
474
  keywords = '|'.join(k + r'\b' for k in KEYWORD)
454
475
  flags = re.IGNORECASE + re.MULTILINE
455
- cls.REGEX['keywords'] = re.compile(f'({keywords})', flags)
476
+ cls.REGEX['keywords'] = re.compile(f'({keywords}|[*])', flags)
456
477
  cls.REGEX['subquery'] = re.compile(r'(\w\.)*\w+ +in +\(SELECT.*?\)', flags)
457
478
  result = {}
458
479
  found = cls.REGEX['subquery'].search(txt)
@@ -476,7 +497,7 @@ class Select(SQLObject):
476
497
  result[obj.alias] = obj
477
498
  txt = txt[:start-1] + txt[end+1:]
478
499
  found = cls.REGEX['subquery'].search(txt)
479
- tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if re.findall(r'\w+', t)]
500
+ tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if t.strip()]
480
501
  values = {k.upper(): v for k, v in zip(tokens[::2], tokens[1::2])}
481
502
  tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
482
503
  for item in tables:
@@ -495,11 +516,18 @@ class Select(SQLObject):
495
516
  obj.values[key] = [
496
517
  Field.format(fld, obj)
497
518
  for fld in re.split(separator, values[key])
498
- if len(tables) == 1 or re.findall(f'\b*{obj.alias}[.]', fld)
519
+ if (fld != '*' and len(tables) == 1) or obj.match(fld)
499
520
  ]
500
521
  result[obj.alias] = obj
501
522
  return list( result.values() )
502
523
 
524
+ def optimize(self, rules: list[Rule]=None):
525
+ if not rules:
526
+ rules = Rule.__subclasses__()
527
+ for rule in rules:
528
+ rule.apply(self)
529
+
530
+
503
531
  class SelectIN(Select):
504
532
  condition_class = Where
505
533
 
@@ -511,3 +539,76 @@ SubSelect = SelectIN
511
539
 
512
540
  class NotSelectIN(SelectIN):
513
541
  condition_class = Not
542
+
543
+
544
+ class RulePutLimit(Rule):
545
+ @classmethod
546
+ def apply(cls, target: Select):
547
+ need_limit = any(not target.values.get(key) for key in (WHERE, SELECT))
548
+ if need_limit:
549
+ target.limit()
550
+
551
+ class RuleSelectIN(Rule):
552
+ @classmethod
553
+ def apply(cls, target: Select):
554
+ for i, condition in enumerate(target.values[WHERE]):
555
+ tokens = re.split(' or | OR ', re.sub('\n|\t|[()]', ' ', condition))
556
+ if len(tokens) < 2:
557
+ continue
558
+ fields = [t.split('=')[0].split('.')[-1].lower().strip() for t in tokens]
559
+ if len(set(fields)) == 1:
560
+ target.values[WHERE][i] = '{} IN ({})'.format(
561
+ Field.format(fields[0], target),
562
+ ','.join(t.split('=')[-1].strip() for t in tokens)
563
+ )
564
+
565
+ class RuleAutoField(Rule):
566
+ @classmethod
567
+ def apply(cls, target: Select):
568
+ if target.values.get(GROUP_BY):
569
+ target.values[SELECT] = target.values[GROUP_BY]
570
+ target.values[ORDER_BY] = []
571
+ elif target.values.get(ORDER_BY):
572
+ s1 = set(target.values.get(SELECT, []))
573
+ s2 = set(target.values[ORDER_BY])
574
+ target.values.setdefault(SELECT, []).extend( list(s2-s1) )
575
+
576
+ class RuleLogicalOp(Rule):
577
+ REVERSE = {
578
+ ">=": "<",
579
+ "<=": ">",
580
+ "<>": "=",
581
+ "=": "<>"
582
+ }
583
+ @classmethod
584
+ def apply(cls, target: Select):
585
+ REGEX = re.compile('({})'.format(
586
+ '|'.join(cls.REVERSE)
587
+ ))
588
+ for i, condition in enumerate(target.values.get(WHERE, [])):
589
+ expr = re.sub('\n|\t', ' ', condition)
590
+ tokens = [t for t in re.split(r'(NOT\b|not\b)',expr) if t.strip()]
591
+ if len(tokens) < 2 or not REGEX.findall(tokens[-1]):
592
+ continue
593
+ tokens = REGEX.split(tokens[-1])
594
+ tokens[1] = cls.REVERSE[tokens[1]]
595
+ target.values[WHERE][i] = ' '.join(tokens)
596
+
597
+ class RuleDateFuncReplace(Rule):
598
+ """
599
+ SQL algorithm by Ralff Matias
600
+ """
601
+ REGEX = re.compile(r'(\bYEAR[(]|\byear[(]|=|[)])')
602
+
603
+ @classmethod
604
+ def apply(cls, target: Select):
605
+ for i, condition in enumerate(target.values.get(WHERE, [])):
606
+ tokens = [
607
+ t.strip() for t in cls.REGEX.split(condition) if t.strip()
608
+ ]
609
+ if len(tokens) < 3:
610
+ continue
611
+ func, field, *rest, year = tokens
612
+ temp = Select(f'{target.table_name} {target.alias}')
613
+ Between(f'{year}-01-01', f'{year}-12-31').add(field, temp)
614
+ target.values[WHERE][i] = ' AND '.join(temp.values[WHERE])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 0.0.6
3
+ Version: 0.0.7
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
@@ -306,10 +306,43 @@ m2 = Select(
306
306
  Select(
307
307
  'Product',
308
308
  label=Case('price').when(
309
- Where.lt(50), 'cheap'
309
+ lt(50), 'cheap'
310
310
  ).when(
311
- Where.gt(100), 'expensive'
311
+ gt(100), 'expensive'
312
312
  ).else_value(
313
313
  'normal'
314
314
  )
315
315
  )
316
+
317
+ ---
318
+
319
+ ### 11 - optimize method
320
+ p1 = Select.parse("""
321
+ SELECT * FROM Product p
322
+ WHERE (p.category = 'Gizmo'
323
+ OR p.category = 'Gadget'
324
+ OR p.category = 'Doohickey')
325
+ AND NOT price <= 387.64
326
+ AND YEAR(last_sale) = 2024
327
+ ORDER BY
328
+ category
329
+ """)[0]
330
+ p1.optimize() # <<===============
331
+ p2 = Select.parse("""
332
+ SELECT category FROM Product p
333
+ WHERE category IN ('Gizmo','Gadget','Doohickey')
334
+ and p.price > 387.64
335
+ and p.last_sale >= '2024-01-01'
336
+ and p.last_sale <= '2024-12-31'
337
+ ORDER BY p.category LIMIT 100
338
+ """)[0]
339
+ p1 == p2 # --- True!
340
+
341
+ This will...
342
+ * Replace `OR` conditions to `SELECT IN ...`
343
+ * Put `LIMIT` if no fields or conditions defined;
344
+ * Normalizes inverted conditions;
345
+ * Auto includes fields present in `ORDER/GROUP BY`;
346
+ * Replace `YEAR` function with date range comparison.
347
+
348
+ > The method allows you to select which rules you want to apply in the optimization...Or define your own rules!
@@ -7,5 +7,4 @@ sql_blocks/sql_blocks.py
7
7
  sql_blocks.egg-info/PKG-INFO
8
8
  sql_blocks.egg-info/SOURCES.txt
9
9
  sql_blocks.egg-info/dependency_links.txt
10
- sql_blocks.egg-info/top_level.txt
11
- tests/tests.py
10
+ sql_blocks.egg-info/top_level.txt
@@ -1 +0,0 @@
1
- from sql_blocks.sql_blocks import *
@@ -1,110 +0,0 @@
1
- from sql_blocks.sql_blocks import *
2
-
3
-
4
- Select.join_type = JoinType.LEFT
5
- OrderBy.sort = SortType.DESC
6
-
7
- def best_movies() -> SelectIN:
8
- return SelectIN(
9
- 'Review r', movie=[GroupBy, Distinct], rate=Having.avg(Where.gt(4.5))
10
- )
11
-
12
- def detached_objects() -> tuple:
13
- def select_actor() -> Select:
14
- return Select('Actor a', cast=ForeignKey('Cast'),
15
- name=NamedField('actors_name'), age=Between(45, 69)
16
- )
17
- def select_cast() -> Select:
18
- return Select(
19
- Cast=Table('role'), id=PrimaryKey, movie=ForeignKey('Movie'),
20
- )
21
- def select_movie() -> Select:
22
- return Select('Movie m', title=Field,
23
- release_date=[OrderBy, Field], id=PrimaryKey,
24
- OR=Options(
25
- genre=Where.eq('Sci-Fi'), awards=Where.like('Oscar')
26
- ), director=[Where.like('Coppola'), Field, OrderBy]
27
- )
28
- return select_actor(), select_cast(), select_movie()
29
-
30
- def query_reference() -> Select:
31
- return Select('Actor a', age=Between(45, 69),
32
- cast=Select(
33
- Cast=Table('role'), id=PrimaryKey,
34
- movie=Select(
35
- 'Movie m', title=Field,
36
- release_date=[OrderBy, Field],
37
- id=[
38
- SelectIN(
39
- 'Review r', movie=[GroupBy, Distinct],
40
- rate=Having.avg(Where.gt(4.5))
41
- ),
42
- PrimaryKey
43
- ], OR=Options(
44
- genre=Where.eq('Sci-Fi'), awards=Where.like('Oscar')
45
- )
46
- ) # --- Movie
47
- ), # ------- Cast
48
- name=NamedField('actors_name'),
49
- ) # ----------- Actor
50
-
51
- SINGLE_CONDITION_GENRE = "( m.genre = 'Sci-Fi' OR m.awards LIKE '%Oscar%' )"
52
- SUB_QUERIES_CONDITIONS = """
53
- m.genre NOT in (SELECT g.id from Genres g where g.name in ('sci-fi', 'horror', 'distopia'))
54
- AND (m.hashtag = '#cult' OR m.awards LIKE '%Oscar%')
55
- AND m.id IN (select DISTINCT r.movie FROM Review r GROUP BY r.movie HAVING Avg(r.rate) > 4.5)
56
- """
57
-
58
- def single_text_to_objects(conditions: str=SINGLE_CONDITION_GENRE):
59
- return Select.parse(f'''
60
- SELECT
61
- cas.role,
62
- m.title,
63
- m.release_date,
64
- a.name as actors_name
65
- FROM
66
- Actor a
67
- LEFT JOIN Cast cas ON (a.cast = cas.id)
68
- LEFT JOIN Movie m ON (cas.movie = m.id)
69
- WHERE
70
- {conditions}
71
- AND a.age <= 69 AND a.age >= 45
72
- ORDER BY
73
- m.release_date DESC
74
- ''')
75
-
76
- def many_texts_to_objects():
77
- ForeignKey.references = {
78
- ('Actor', 'Cast'): ('cast', 'id'),
79
- ('Cast', 'Movie'): ('movie', 'id'),
80
- }
81
- actor = Select.parse('''
82
- SELECT name as actors_name FROM Actor a
83
- WHERE a.age >= 45 AND a.age <= 69
84
- ''')[0]
85
- cast = Select.parse('SELECT role FROM Cast')[0]
86
- movie = Select.parse("""
87
- SELECT title, release_date FROM Movie m ORDER BY release_date DESC
88
- WHERE ( m.genre = 'Sci-Fi' OR m.awards LIKE '%Oscar%' ) GROUP BY director
89
- """)[0]
90
- return actor, cast, movie
91
-
92
- def two_queries_same_table() -> Select:
93
- txt1 = """SELECT p.name, p.category
94
- ,p.price,p.promotional FROM product p
95
- where p.category in (6,14,29,35,78)
96
- AND p.Status = p.last_st ORDER BY p.EAN"""
97
- txt2 = """select stock_amount, EAN,Name ,expiration_date
98
- from PRODUCT where price < 357.46 and status = Last_ST order by ean"""
99
- return Select.parse(txt1)[0] + Select.parse(txt2)[0]
100
-
101
- def select_product() -> Select:
102
- return Select(
103
- Product=Table('name,promotional,stock_amount,expiration_date'),
104
- category=[Where.list([6,14,29,35,78]),Field], EAN=[Field, OrderBy],
105
- price=[Where.lt(357.46),Field], status=Where('= Last_st')
106
- )
107
-
108
- def extract_subqueries() -> dict:
109
- query_list = single_text_to_objects(SUB_QUERIES_CONDITIONS)
110
- return {query.table_name: query for query in query_list}
File without changes
File without changes