sql-blocks 1.25.13__tar.gz → 1.25.111__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: 1.25.13
3
+ Version: 1.25.111
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
@@ -97,8 +97,13 @@ query = Select('Movie m', title=Field,
97
97
  genre=eq("Sci-Fi"),
98
98
  awards=contains("Oscar")
99
99
  )
100
+ AND=Options(
101
+ ..., name=contains(
102
+ 'Chris',
103
+ Position.StartsWith
104
+ )
105
+ )
100
106
  ```
101
- > Could be AND=Options(...)
102
107
 
103
108
  3.4 -- Negative conditions use the _Not_ class instead of _Where_
104
109
  ```
@@ -363,6 +368,35 @@ m2 = Select(
363
368
 
364
369
  > The method allows you to select which rules you want to apply in the optimization...Or define your own rules!
365
370
 
371
+ >> NOTE: When a joined table is used only as a filter, it is possible that it can be changed to a sub-query:
372
+
373
+ query = Select(
374
+ 'Installments i', due_date=Field, customer=Select(
375
+ 'Customer c', id=PrimaryKey,
376
+ name=contains('Smith', Position.EndsWith)
377
+ )
378
+ )
379
+ print(query)
380
+ print('-----')
381
+ query.optimize([RuleReplaceJoinBySubselect])
382
+ print(query)
383
+ ```
384
+ SELECT
385
+ i.due_date
386
+ FROM
387
+ Installments i
388
+ JOIN Customer c ON (i.customer = c.id)
389
+ WHERE
390
+ c.name LIKE '%Smith'
391
+ -----
392
+ SELECT
393
+ i.due_date
394
+ FROM
395
+ Installments i
396
+ WHERE
397
+ i.customer IN (SELECT c.id FROM Customer c WHERE c.name LIKE '%Smith')
398
+ ```
399
+
366
400
  ---
367
401
 
368
402
  ### 12 - Adding multiple fields at once
@@ -567,3 +601,21 @@ GROUP BY
567
601
  ORDER BY
568
602
  customer_count
569
603
  ```
604
+ ---
605
+ ### 16 - Function classes
606
+ You may use this functions:
607
+ * SubString
608
+ * Round
609
+ * DateDiff
610
+ * Year
611
+ * Current_Date
612
+ * Avg
613
+ * Min
614
+ * Max
615
+ * Sum
616
+ * Count
617
+ > Some of these functions may vary in syntax depending on the database.
618
+ For example, if your query is going to run on Oracle, do the following:
619
+
620
+ `Function.dialect = Dialect.ORACLE`
621
+
@@ -82,8 +82,13 @@ query = Select('Movie m', title=Field,
82
82
  genre=eq("Sci-Fi"),
83
83
  awards=contains("Oscar")
84
84
  )
85
+ AND=Options(
86
+ ..., name=contains(
87
+ 'Chris',
88
+ Position.StartsWith
89
+ )
90
+ )
85
91
  ```
86
- > Could be AND=Options(...)
87
92
 
88
93
  3.4 -- Negative conditions use the _Not_ class instead of _Where_
89
94
  ```
@@ -348,6 +353,35 @@ m2 = Select(
348
353
 
349
354
  > The method allows you to select which rules you want to apply in the optimization...Or define your own rules!
350
355
 
356
+ >> NOTE: When a joined table is used only as a filter, it is possible that it can be changed to a sub-query:
357
+
358
+ query = Select(
359
+ 'Installments i', due_date=Field, customer=Select(
360
+ 'Customer c', id=PrimaryKey,
361
+ name=contains('Smith', Position.EndsWith)
362
+ )
363
+ )
364
+ print(query)
365
+ print('-----')
366
+ query.optimize([RuleReplaceJoinBySubselect])
367
+ print(query)
368
+ ```
369
+ SELECT
370
+ i.due_date
371
+ FROM
372
+ Installments i
373
+ JOIN Customer c ON (i.customer = c.id)
374
+ WHERE
375
+ c.name LIKE '%Smith'
376
+ -----
377
+ SELECT
378
+ i.due_date
379
+ FROM
380
+ Installments i
381
+ WHERE
382
+ i.customer IN (SELECT c.id FROM Customer c WHERE c.name LIKE '%Smith')
383
+ ```
384
+
351
385
  ---
352
386
 
353
387
  ### 12 - Adding multiple fields at once
@@ -552,3 +586,21 @@ GROUP BY
552
586
  ORDER BY
553
587
  customer_count
554
588
  ```
589
+ ---
590
+ ### 16 - Function classes
591
+ You may use this functions:
592
+ * SubString
593
+ * Round
594
+ * DateDiff
595
+ * Year
596
+ * Current_Date
597
+ * Avg
598
+ * Min
599
+ * Max
600
+ * Sum
601
+ * Count
602
+ > Some of these functions may vary in syntax depending on the database.
603
+ For example, if your query is going to run on Oracle, do the following:
604
+
605
+ `Function.dialect = Dialect.ORACLE`
606
+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql_blocks"
3
- version = "1.25.13"
3
+ version = "1.25.111"
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 = '1.25.13',
6
+ version = '1.25.111',
7
7
  author = 'Júlio Cascalles',
8
8
  author_email = 'julio.cascalles@outlook.com',
9
9
  packages = ['sql_blocks'],
@@ -26,7 +26,7 @@ TO_LIST = lambda x: x if isinstance(x, list) else [x]
26
26
 
27
27
 
28
28
  class SQLObject:
29
- ALIAS_FUNC = lambda t: t.lower()[:3]
29
+ ALIAS_FUNC = None
30
30
  """ ^^^^^^^^^^^^^^^^^^^^^^^^
31
31
  You can change the behavior by assigning
32
32
  a user function to SQLObject.ALIAS_FUNC
@@ -41,7 +41,10 @@ class SQLObject:
41
41
  def set_table(self, table_name: str):
42
42
  if not table_name:
43
43
  return
44
- if ' ' in table_name.strip():
44
+ cls = SQLObject
45
+ if cls.ALIAS_FUNC:
46
+ self.__alias = cls.ALIAS_FUNC(table_name)
47
+ elif ' ' in table_name.strip():
45
48
  table_name, self.__alias = table_name.split()
46
49
  elif '_' in table_name:
47
50
  self.__alias = ''.join(
@@ -49,7 +52,7 @@ class SQLObject:
49
52
  for word in table_name.split('_')
50
53
  )
51
54
  else:
52
- self.__alias = SQLObject.ALIAS_FUNC(table_name)
55
+ self.__alias = table_name.lower()[:3]
53
56
  self.values.setdefault(FROM, []).append(f'{table_name} {self.alias}')
54
57
 
55
58
  @property
@@ -111,15 +114,26 @@ class SQLObject:
111
114
  self.values[key] = result
112
115
 
113
116
 
117
+ SQL_CONST_SYSDATE = 'SYSDATE'
118
+ SQL_CONST_CURR_DATE = 'Current_date'
119
+ SQL_CONSTS = [SQL_CONST_SYSDATE, SQL_CONST_CURR_DATE]
120
+
121
+
114
122
  class Field:
115
123
  prefix = ''
116
124
 
117
125
  @classmethod
118
126
  def format(cls, name: str, main: SQLObject) -> str:
127
+ def is_const() -> bool:
128
+ return any([
129
+ re.findall('[.()0-9]', name),
130
+ name in SQL_CONSTS,
131
+ re.findall(r'\w+\s*[+-]\s*\w+', name)
132
+ ])
119
133
  name = name.strip()
120
134
  if name in ('_', '*'):
121
135
  name = '*'
122
- elif not re.findall('[.()0-9]', name):
136
+ elif not is_const():
123
137
  name = f'{main.alias}.{name}'
124
138
  if Function in cls.__bases__:
125
139
  name = f'{cls.__name__}({name})'
@@ -150,7 +164,16 @@ class NamedField:
150
164
  )
151
165
 
152
166
 
167
+ class Dialect(Enum):
168
+ ANSI = 0
169
+ SQL_SERVER = 1
170
+ ORACLE = 2
171
+ POSTGRESQL = 3
172
+ MYSQL = 4
173
+
153
174
  class Function:
175
+ dialect = Dialect.ANSI
176
+
154
177
  def __init__(self, *params: list):
155
178
  # --- Replace class methods by instance methods: ------
156
179
  self.add = self.__add
@@ -158,26 +181,30 @@ class Function:
158
181
  # -----------------------------------------------------
159
182
  self.params = [str(p) for p in params]
160
183
  self.field_class = Field
161
- self.pattern = '{}({})'
184
+ self.pattern = self.get_pattern()
162
185
  self.extra = {}
163
186
 
187
+ def get_pattern(self) -> str:
188
+ return '{func_name}({params})'
189
+
164
190
  def As(self, field_alias: str, modifiers=None):
165
191
  if modifiers:
166
192
  self.extra[field_alias] = TO_LIST(modifiers)
167
193
  self.field_class = NamedField(field_alias)
168
194
  return self
169
195
 
196
+ def __str__(self) -> str:
197
+ return self.pattern.format(
198
+ func_name=self.__class__.__name__,
199
+ params=', '.join(self.params)
200
+ )
201
+
170
202
  def __format(self, name: str, main: SQLObject) -> str:
171
- if name in '*_' and self.params:
172
- params = self.params
173
- else:
174
- params = [
203
+ if name not in '*_':
204
+ self.params = [
175
205
  Field.format(name, main)
176
206
  ] + self.params
177
- return self.pattern.format(
178
- self.__class__.__name__,
179
- ', '.join(params)
180
- )
207
+ return str(self)
181
208
 
182
209
  @classmethod
183
210
  def format(cls, name: str, main: SQLObject):
@@ -196,7 +223,10 @@ class Function:
196
223
 
197
224
  # ---- String Functions: ---------------------------------
198
225
  class SubString(Function):
199
- ...
226
+ def get_pattern(self) -> str:
227
+ if self.dialect in (Dialect.ORACLE, Dialect.MYSQL):
228
+ return 'Substr({params})'
229
+ return super().get_pattern()
200
230
 
201
231
  # ---- Numeric Functions: --------------------------------
202
232
  class Round(Function):
@@ -204,13 +234,37 @@ class Round(Function):
204
234
 
205
235
  # --- Date Functions: ------------------------------------
206
236
  class DateDiff(Function):
207
- ...
208
- class Extract(Function):
209
- ...
210
- class DatePart(Function):
211
- ...
237
+ def get_pattern(self) -> str:
238
+ def is_field_or_func(name: str) -> bool:
239
+ return re.sub('[()]', '', name).isidentifier()
240
+ if self.dialect != Dialect.SQL_SERVER:
241
+ return ' - '.join(
242
+ p if is_field_or_func(p) else f"'{p}'"
243
+ for p in self.params
244
+ ) # <==== Date subtract
245
+ return super().get_pattern()
246
+
247
+ class Year(Function):
248
+ def get_pattern(self) -> str:
249
+ database_type = {
250
+ Dialect.ORACLE: 'Extract(YEAR FROM {params})',
251
+ Dialect.POSTGRESQL: "Date_Part('year', {params})",
252
+ }
253
+ if self.dialect in database_type:
254
+ return database_type[self.dialect]
255
+ return super().get_pattern()
256
+
212
257
  class Current_Date(Function):
213
- ...
258
+ def get_pattern(self) -> str:
259
+ database_type = {
260
+ Dialect.ORACLE: SQL_CONST_SYSDATE,
261
+ Dialect.POSTGRESQL: SQL_CONST_CURR_DATE,
262
+ Dialect.SQL_SERVER: 'getDate()'
263
+ }
264
+ if self.dialect in database_type:
265
+ return database_type[self.dialect]
266
+ return super().get_pattern()
267
+ # --------------------------------------------------------
214
268
 
215
269
  class Aggregate:
216
270
  break_lines: bool = True
@@ -225,7 +279,7 @@ class Aggregate:
225
279
  )
226
280
  if keywords and self.break_lines:
227
281
  keywords += '\n\t'
228
- self.pattern = '{}({})' + f' OVER({keywords})'
282
+ self.pattern = self.get_pattern() + f' OVER({keywords})'
229
283
  return self
230
284
 
231
285
 
@@ -324,6 +378,12 @@ def quoted(value) -> str:
324
378
  return str(value)
325
379
 
326
380
 
381
+ class Position(Enum):
382
+ Middle = 0
383
+ StartsWith = 1
384
+ EndsWith = 2
385
+
386
+
327
387
  class Where:
328
388
  prefix = ''
329
389
 
@@ -339,8 +399,14 @@ class Where:
339
399
  return cls.__constructor('=', value)
340
400
 
341
401
  @classmethod
342
- def contains(cls, value: str):
343
- return cls(f"LIKE '%{value}%'")
402
+ def contains(cls, content: str, pos: Position = Position.Middle):
403
+ return cls(
404
+ "LIKE '{}{}{}'".format(
405
+ '%' if pos != Position.StartsWith else '',
406
+ content,
407
+ '%' if pos != Position.EndsWith else ''
408
+ )
409
+ )
344
410
 
345
411
  @classmethod
346
412
  def gt(cls, value):
@@ -420,9 +486,8 @@ class Options:
420
486
  self.__children: dict = values
421
487
 
422
488
  def add(self, logical_separator: str, main: SQLObject):
423
- """
424
- `logical_separator` must be AND or OR
425
- """
489
+ if logical_separator not in ('AND', 'OR'):
490
+ raise ValueError('`logical_separator` must be AND or OR')
426
491
  conditions: list[str] = []
427
492
  child: Where
428
493
  for field, child in self.__children.items():
@@ -1320,6 +1385,31 @@ class RuleDateFuncReplace(Rule):
1320
1385
  target.values[WHERE][i] = ' AND '.join(temp.values[WHERE])
1321
1386
 
1322
1387
 
1388
+ class RuleReplaceJoinBySubselect(Rule):
1389
+ @classmethod
1390
+ def apply(cls, target: Select):
1391
+ main, *others = Select.parse( str(target) )
1392
+ modified = False
1393
+ for query in others:
1394
+ fk_field, primary_k = ForeignKey.find(main, query)
1395
+ more_relations = any([
1396
+ ref[0] == query.table_name for ref in ForeignKey.references
1397
+ ])
1398
+ invalid = any([
1399
+ len( query.values.get(SELECT, []) ) > 0,
1400
+ len( query.values.get(WHERE, []) ) == 0,
1401
+ not fk_field, more_relations
1402
+ ])
1403
+ if invalid:
1404
+ continue
1405
+ query.__class__ = SubSelect
1406
+ Field.add(primary_k, query)
1407
+ query.add(fk_field, main)
1408
+ modified = True
1409
+ if modified:
1410
+ target.values = main.values.copy()
1411
+
1412
+
1323
1413
  def parser_class(text: str) -> Parser:
1324
1414
  PARSER_REGEX = [
1325
1415
  (r'select.*from', SQLParser),
@@ -1355,11 +1445,15 @@ def detect(text: str) -> Select:
1355
1445
  result += query
1356
1446
  return result
1357
1447
 
1448
+
1358
1449
  if __name__ == "__main__":
1359
- OrderBy.sort = SortType.DESC
1360
1450
  query = Select(
1361
- 'order_Detail d',
1362
- customer_id=GroupBy,
1363
- _=Sum('d.unitPrice * d.quantity').As('total', OrderBy)
1451
+ 'Installments i', due_date=Field, customer=Select(
1452
+ 'Customer c', id=PrimaryKey,
1453
+ name=contains('Smith', Position.EndsWith)
1454
+ )
1364
1455
  )
1365
1456
  print(query)
1457
+ print('-----')
1458
+ query.optimize([RuleReplaceJoinBySubselect])
1459
+ print(query)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 1.25.13
3
+ Version: 1.25.111
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
@@ -97,8 +97,13 @@ query = Select('Movie m', title=Field,
97
97
  genre=eq("Sci-Fi"),
98
98
  awards=contains("Oscar")
99
99
  )
100
+ AND=Options(
101
+ ..., name=contains(
102
+ 'Chris',
103
+ Position.StartsWith
104
+ )
105
+ )
100
106
  ```
101
- > Could be AND=Options(...)
102
107
 
103
108
  3.4 -- Negative conditions use the _Not_ class instead of _Where_
104
109
  ```
@@ -363,6 +368,35 @@ m2 = Select(
363
368
 
364
369
  > The method allows you to select which rules you want to apply in the optimization...Or define your own rules!
365
370
 
371
+ >> NOTE: When a joined table is used only as a filter, it is possible that it can be changed to a sub-query:
372
+
373
+ query = Select(
374
+ 'Installments i', due_date=Field, customer=Select(
375
+ 'Customer c', id=PrimaryKey,
376
+ name=contains('Smith', Position.EndsWith)
377
+ )
378
+ )
379
+ print(query)
380
+ print('-----')
381
+ query.optimize([RuleReplaceJoinBySubselect])
382
+ print(query)
383
+ ```
384
+ SELECT
385
+ i.due_date
386
+ FROM
387
+ Installments i
388
+ JOIN Customer c ON (i.customer = c.id)
389
+ WHERE
390
+ c.name LIKE '%Smith'
391
+ -----
392
+ SELECT
393
+ i.due_date
394
+ FROM
395
+ Installments i
396
+ WHERE
397
+ i.customer IN (SELECT c.id FROM Customer c WHERE c.name LIKE '%Smith')
398
+ ```
399
+
366
400
  ---
367
401
 
368
402
  ### 12 - Adding multiple fields at once
@@ -567,3 +601,21 @@ GROUP BY
567
601
  ORDER BY
568
602
  customer_count
569
603
  ```
604
+ ---
605
+ ### 16 - Function classes
606
+ You may use this functions:
607
+ * SubString
608
+ * Round
609
+ * DateDiff
610
+ * Year
611
+ * Current_Date
612
+ * Avg
613
+ * Min
614
+ * Max
615
+ * Sum
616
+ * Count
617
+ > Some of these functions may vary in syntax depending on the database.
618
+ For example, if your query is going to run on Oracle, do the following:
619
+
620
+ `Function.dialect = Dialect.ORACLE`
621
+
File without changes
File without changes