sql-blocks 0.0.6__py3-none-any.whl → 0.0.8__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/__init__.py CHANGED
@@ -1 +1 @@
1
- from sql_blocks.sql_blocks import *
1
+ from sql_blocks import *
sql_blocks/sql_blocks.py CHANGED
@@ -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 ', '', ''),
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:
@@ -58,11 +70,17 @@ class SQLObject:
58
70
  return KEYWORD[key][0].format(appendix.get(key, ''))
59
71
 
60
72
  def diff(self, key: str, search_list: list, symmetrical: bool=False) -> set:
61
- pattern = KEYWORD[key][2]
73
+ def cleanup(fld: str) -> str:
74
+ if symmetrical:
75
+ fld = fld.lower()
76
+ return fld.strip()
77
+ pattern = KEYWORD[key][2]
78
+ if key == WHERE and symmetrical:
79
+ pattern = f'{PATTERN_PREFIX}| '
62
80
  separator = self.get_separator(key)
63
81
  def field_set(source: list) -> set:
64
82
  return set(
65
- re.sub(pattern, '', fld.strip()).lower()
83
+ re.sub(pattern, '', cleanup(fld))
66
84
  for string in source
67
85
  for fld in re.split(separator, string)
68
86
  )
@@ -90,9 +108,9 @@ class Field:
90
108
  @classmethod
91
109
  def format(cls, name: str, main: SQLObject) -> str:
92
110
  name = name.strip()
93
- if name == '_':
111
+ if name in ('_', '*'):
94
112
  name = '*'
95
- elif '.' not in name:
113
+ elif not re.findall('[.()0-9]', name):
96
114
  name = f'{main.alias}.{name}'
97
115
  if Function in cls.__bases__:
98
116
  name = f'{cls.__name__}({name})'
@@ -185,7 +203,7 @@ class Where:
185
203
  prefix = ''
186
204
 
187
205
  def __init__(self, expr: str):
188
- self.expr = f'{self.prefix}{expr}'
206
+ self.expr = expr
189
207
 
190
208
  @classmethod
191
209
  def __constructor(cls, operator: str, value):
@@ -226,8 +244,8 @@ class Where:
226
244
  return cls(f'IN ({values})')
227
245
 
228
246
  def add(self, name: str, main: SQLObject):
229
- main.values.setdefault(WHERE, []).append('{} {}'.format(
230
- Field.format(name, main), self.expr
247
+ main.values.setdefault(WHERE, []).append('{}{} {}'.format(
248
+ self.prefix, Field.format(name, main), self.expr
231
249
  ))
232
250
 
233
251
 
@@ -347,6 +365,12 @@ class Having:
347
365
  return cls(Count, condition)
348
366
 
349
367
 
368
+ class Rule:
369
+ @classmethod
370
+ def apply(cls, target: 'Select'):
371
+ ...
372
+
373
+
350
374
  class JoinType(Enum):
351
375
  INNER = ''
352
376
  LEFT = 'LEFT '
@@ -367,16 +391,17 @@ class Select(SQLObject):
367
391
  self.values.setdefault(key, []).append(value)
368
392
 
369
393
  def add(self, name: str, main: SQLObject):
370
- main.update_values(
371
- FROM, [
372
- '{jt}JOIN {tb} {a2} ON ({a1}.{f1} = {a2}.{f2})'.format(
373
- jt=self.join_type.value,
374
- tb=self.table_name,
375
- a1=main.alias, f1=name,
376
- a2=self.alias, f2=self.key_field
377
- )
378
- ] + self.values[FROM][1:]
379
- )
394
+ new_tables = [
395
+ '{jt}JOIN {tb} {a2} ON ({a1}.{f1} = {a2}.{f2})'.format(
396
+ jt=self.join_type.value,
397
+ tb=self.table_name,
398
+ a1=main.alias, f1=name,
399
+ a2=self.alias, f2=self.key_field
400
+ )
401
+ ]
402
+ if new_tables not in main.values.get(FROM, []):
403
+ new_tables += self.values[FROM][1:]
404
+ main.values.setdefault(FROM, []).extend(new_tables)
380
405
  for key in USUAL_KEYS:
381
406
  main.update_values(key, self.values.get(key, []))
382
407
 
@@ -424,13 +449,16 @@ class Select(SQLObject):
424
449
  return False
425
450
  return True
426
451
 
427
- def limit(self, row_count: int, offset: int=0):
452
+ def limit(self, row_count: int=100, offset: int=0):
428
453
  result = [str(row_count)]
429
454
  if offset > 0:
430
455
  result.append(f'OFFSET {offset}')
431
456
  self.values.setdefault(LIMIT, result)
432
457
  return self
433
458
 
459
+ def match(self, expr: str) -> bool:
460
+ return re.findall(f'\b*{self.alias}[.]', expr) != []
461
+
434
462
  @classmethod
435
463
  def parse(cls, txt: str) -> list[SQLObject]:
436
464
  def find_last_word(pos: int) -> int:
@@ -452,7 +480,7 @@ class Select(SQLObject):
452
480
  if not cls.REGEX:
453
481
  keywords = '|'.join(k + r'\b' for k in KEYWORD)
454
482
  flags = re.IGNORECASE + re.MULTILINE
455
- cls.REGEX['keywords'] = re.compile(f'({keywords})', flags)
483
+ cls.REGEX['keywords'] = re.compile(f'({keywords}|[*])', flags)
456
484
  cls.REGEX['subquery'] = re.compile(r'(\w\.)*\w+ +in +\(SELECT.*?\)', flags)
457
485
  result = {}
458
486
  found = cls.REGEX['subquery'].search(txt)
@@ -476,7 +504,7 @@ class Select(SQLObject):
476
504
  result[obj.alias] = obj
477
505
  txt = txt[:start-1] + txt[end+1:]
478
506
  found = cls.REGEX['subquery'].search(txt)
479
- tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if re.findall(r'\w+', t)]
507
+ tokens = [t.strip() for t in cls.REGEX['keywords'].split(txt) if t.strip()]
480
508
  values = {k.upper(): v for k, v in zip(tokens[::2], tokens[1::2])}
481
509
  tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
482
510
  for item in tables:
@@ -495,11 +523,18 @@ class Select(SQLObject):
495
523
  obj.values[key] = [
496
524
  Field.format(fld, obj)
497
525
  for fld in re.split(separator, values[key])
498
- if len(tables) == 1 or re.findall(f'\b*{obj.alias}[.]', fld)
526
+ if (fld != '*' and len(tables) == 1) or obj.match(fld)
499
527
  ]
500
528
  result[obj.alias] = obj
501
529
  return list( result.values() )
502
530
 
531
+ def optimize(self, rules: list[Rule]=None):
532
+ if not rules:
533
+ rules = Rule.__subclasses__()
534
+ for rule in rules:
535
+ rule.apply(self)
536
+
537
+
503
538
  class SelectIN(Select):
504
539
  condition_class = Where
505
540
 
@@ -511,3 +546,76 @@ SubSelect = SelectIN
511
546
 
512
547
  class NotSelectIN(SelectIN):
513
548
  condition_class = Not
549
+
550
+
551
+ class RulePutLimit(Rule):
552
+ @classmethod
553
+ def apply(cls, target: Select):
554
+ need_limit = any(not target.values.get(key) for key in (WHERE, SELECT))
555
+ if need_limit:
556
+ target.limit()
557
+
558
+ class RuleSelectIN(Rule):
559
+ @classmethod
560
+ def apply(cls, target: Select):
561
+ for i, condition in enumerate(target.values[WHERE]):
562
+ tokens = re.split(' or | OR ', re.sub('\n|\t|[()]', ' ', condition))
563
+ if len(tokens) < 2:
564
+ continue
565
+ fields = [t.split('=')[0].split('.')[-1].lower().strip() for t in tokens]
566
+ if len(set(fields)) == 1:
567
+ target.values[WHERE][i] = '{} IN ({})'.format(
568
+ Field.format(fields[0], target),
569
+ ','.join(t.split('=')[-1].strip() for t in tokens)
570
+ )
571
+
572
+ class RuleAutoField(Rule):
573
+ @classmethod
574
+ def apply(cls, target: Select):
575
+ if target.values.get(GROUP_BY):
576
+ target.values[SELECT] = target.values[GROUP_BY]
577
+ target.values[ORDER_BY] = []
578
+ elif target.values.get(ORDER_BY):
579
+ s1 = set(target.values.get(SELECT, []))
580
+ s2 = set(target.values[ORDER_BY])
581
+ target.values.setdefault(SELECT, []).extend( list(s2-s1) )
582
+
583
+ class RuleLogicalOp(Rule):
584
+ REVERSE = {
585
+ ">=": "<",
586
+ "<=": ">",
587
+ "<>": "=",
588
+ "=": "<>"
589
+ }
590
+ @classmethod
591
+ def apply(cls, target: Select):
592
+ REGEX = re.compile('({})'.format(
593
+ '|'.join(cls.REVERSE)
594
+ ))
595
+ for i, condition in enumerate(target.values.get(WHERE, [])):
596
+ expr = re.sub('\n|\t', ' ', condition)
597
+ tokens = [t for t in re.split(r'(NOT\b|not\b)',expr) if t.strip()]
598
+ if len(tokens) < 2 or not REGEX.findall(tokens[-1]):
599
+ continue
600
+ tokens = REGEX.split(tokens[-1])
601
+ tokens[1] = cls.REVERSE[tokens[1]]
602
+ target.values[WHERE][i] = ' '.join(tokens)
603
+
604
+ class RuleDateFuncReplace(Rule):
605
+ """
606
+ SQL algorithm by Ralff Matias
607
+ """
608
+ REGEX = re.compile(r'(\bYEAR[(]|\byear[(]|=|[)])')
609
+
610
+ @classmethod
611
+ def apply(cls, target: Select):
612
+ for i, condition in enumerate(target.values.get(WHERE, [])):
613
+ tokens = [
614
+ t.strip() for t in cls.REGEX.split(condition) if t.strip()
615
+ ]
616
+ if len(tokens) < 3:
617
+ continue
618
+ func, field, *rest, year = tokens
619
+ temp = Select(f'{target.table_name} {target.alias}')
620
+ Between(f'{year}-01-01', f'{year}-12-31').add(field, temp)
621
+ 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.8
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!
@@ -0,0 +1,7 @@
1
+ sql_blocks/__init__.py,sha256=TodC5q-UEdYEz9v1RRoogVqqRcsKnZRY1WDGinrI2zo,26
2
+ sql_blocks/sql_blocks.py,sha256=mC8TTgAXrGL0X_Zn5GPv2HytlaRKm9jpcUyaSnepFOs,19963
3
+ sql_blocks-0.0.8.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
+ sql_blocks-0.0.8.dist-info/METADATA,sha256=B7L8E2UI-0GWHsDVPWibQsMrp69wzP4XRfXOOfg5GV4,8190
5
+ sql_blocks-0.0.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
+ sql_blocks-0.0.8.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
+ sql_blocks-0.0.8.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
2
- sql_blocks/sql_blocks.py,sha256=Fs636uSwXTxlzHH9SCOxmGTgt2MjhQ6Pf6v92CRtQ9E,16229
3
- sql_blocks-0.0.6.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
- sql_blocks-0.0.6.dist-info/METADATA,sha256=-2-vPw1nkpqKvlp0l2Bt8VF_sBZ_TM8BGf-Nmk8X2oo,7037
5
- sql_blocks-0.0.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
- sql_blocks-0.0.6.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
- sql_blocks-0.0.6.dist-info/RECORD,,