sql-blocks 0.0.6__py3-none-any.whl → 0.0.7__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 +1 -1
- sql_blocks/sql_blocks.py +124 -23
- {sql_blocks-0.0.6.dist-info → sql_blocks-0.0.7.dist-info}/METADATA +36 -3
- sql_blocks-0.0.7.dist-info/RECORD +7 -0
- sql_blocks-0.0.6.dist-info/RECORD +0 -7
- {sql_blocks-0.0.6.dist-info → sql_blocks-0.0.7.dist-info}/LICENSE +0 -0
- {sql_blocks-0.0.6.dist-info → sql_blocks-0.0.7.dist-info}/WHEEL +0 -0
- {sql_blocks-0.0.6.dist-info → sql_blocks-0.0.7.dist-info}/top_level.txt +0 -0
sql_blocks/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
from sql_blocks
|
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 ', '', 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
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
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.
|
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.
|
46
|
+
if ' ' in table_name.strip():
|
47
|
+
table_name, self.__alias = table_name.split()
|
42
48
|
elif '_' in table_name:
|
43
|
-
self.
|
49
|
+
self.__alias = ''.join(
|
44
50
|
word[0].lower()
|
45
51
|
for word in table_name.split('_')
|
46
52
|
)
|
47
53
|
else:
|
48
|
-
self.
|
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 '.'
|
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 =
|
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
|
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
|
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.
|
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
|
-
|
309
|
+
lt(50), 'cheap'
|
310
310
|
).when(
|
311
|
-
|
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=w8vI-UxyBXj_YD09zKF3AKzJlmGN1wIRJgzbcCh6rmU,19684
|
3
|
+
sql_blocks-0.0.7.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
4
|
+
sql_blocks-0.0.7.dist-info/METADATA,sha256=2WFDh50ro70p_8f850VxyXQx82MY7BIq1nZEE-EY8Jk,8190
|
5
|
+
sql_blocks-0.0.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
6
|
+
sql_blocks-0.0.7.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
|
7
|
+
sql_blocks-0.0.7.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,,
|
File without changes
|
File without changes
|
File without changes
|