sql-blocks 1.20250710__tar.gz → 1.20250712__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.20250710
3
+ Version: 1.20250712
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
@@ -948,7 +948,7 @@ R2 = Recursive.create(
948
948
  >> Note: Comments added later.
949
949
  ---
950
950
 
951
- ### CTEFactory class
951
+ ### 17.3 - CTEFactory class
952
952
  CTEFactory exchanges subqueries for CTEs, simply by passing the text of the "dirty" query:
953
953
 
954
954
  *Example*:
@@ -992,4 +992,27 @@ results...
992
992
  ORDER BY
993
993
  u001.name
994
994
  ```
995
+
996
+ #### 17.3.1 - You can also pass a Cypher script like in the example below:
997
+
998
+ ![image](assets/CTEFactory.png)
999
+
1000
+ results...
1001
+ ```
1002
+ WITH Annual_Sales_per_Vendor AS (
1003
+ SELECT ven.name as vendors_name, Year(sal.ref_date) as ref_year
1004
+ , Sum(sal.quantity) as qty_sold FROM Vendor ven LEFT JOIN Sales sal ON (ven.id = sal.vendor)
1005
+ GROUP BY ven.name, ref_year
1006
+ )
1007
+ SELECT
1008
+ aspv.ref_year,
1009
+ aspv.qty_sold,
1010
+ aspv.vendors_name,
1011
+ goa.target
1012
+ FROM
1013
+ Annual_Sales_per_Vendor aspv
1014
+ RIGHT JOIN Goal goa ON (aspv.ref_year = goa.year)
1015
+ ```
1016
+
1017
+
995
1018
  ---
@@ -933,7 +933,7 @@ R2 = Recursive.create(
933
933
  >> Note: Comments added later.
934
934
  ---
935
935
 
936
- ### CTEFactory class
936
+ ### 17.3 - CTEFactory class
937
937
  CTEFactory exchanges subqueries for CTEs, simply by passing the text of the "dirty" query:
938
938
 
939
939
  *Example*:
@@ -977,4 +977,27 @@ results...
977
977
  ORDER BY
978
978
  u001.name
979
979
  ```
980
- ---
980
+
981
+ #### 17.3.1 - You can also pass a Cypher script like in the example below:
982
+
983
+ ![image](assets/CTEFactory.png)
984
+
985
+ results...
986
+ ```
987
+ WITH Annual_Sales_per_Vendor AS (
988
+ SELECT ven.name as vendors_name, Year(sal.ref_date) as ref_year
989
+ , Sum(sal.quantity) as qty_sold FROM Vendor ven LEFT JOIN Sales sal ON (ven.id = sal.vendor)
990
+ GROUP BY ven.name, ref_year
991
+ )
992
+ SELECT
993
+ aspv.ref_year,
994
+ aspv.qty_sold,
995
+ aspv.vendors_name,
996
+ goa.target
997
+ FROM
998
+ Annual_Sales_per_Vendor aspv
999
+ RIGHT JOIN Goal goa ON (aspv.ref_year = goa.year)
1000
+ ```
1001
+
1002
+
1003
+ ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql_blocks"
3
- version = "1.20250710"
3
+ version = "1.20250712"
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.20250710',
6
+ version = '1.20250712',
7
7
  author = 'Júlio Cascalles',
8
8
  author_email = 'julio.cascalles@outlook.com',
9
9
  packages = ['sql_blocks'],
@@ -221,11 +221,12 @@ class Code:
221
221
  def As(self, field_alias: str, modifiers=None):
222
222
  if modifiers:
223
223
  self.extra[field_alias] = TO_LIST(modifiers)
224
- self.field_class = NamedField(field_alias)
224
+ if field_alias:
225
+ self.field_class = NamedField(field_alias)
225
226
  return self
226
227
 
227
228
  def format(self, name: str, main: SQLObject) -> str:
228
- raise NotImplementedError('Use child classes instead of this one')
229
+ return Field.format(name, main)
229
230
 
230
231
  def __add(self, name: str, main: SQLObject):
231
232
  name = self.format(name, main)
@@ -335,6 +336,10 @@ class Round(Function):
335
336
  inputs = [FLOAT]
336
337
  output = FLOAT
337
338
 
339
+ class Trunc(Function):
340
+ inputs = [FLOAT]
341
+ output = FLOAT
342
+
338
343
  # --- Date Functions: ------------------------------------
339
344
  class DateDiff(Function):
340
345
  inputs = [DATE]
@@ -1674,38 +1679,58 @@ class CypherParser(Parser):
1674
1679
  Where(' '.join(condition)).add(field, query)
1675
1680
 
1676
1681
  def add_order(self, token: str):
1677
- self.add_field(token, [OrderBy])
1682
+ self.add_field(token, sorted=True)
1678
1683
 
1679
- def add_field(self, token: str, extra_classes: list['type']=[]):
1684
+ def add_field(self, token: str, sorted: bool = False):
1680
1685
  if token in self.TOKEN_METHODS:
1681
1686
  return
1682
- class_list = [Field]
1683
1687
  if '*' in token:
1684
- token = token.replace('*', '')
1685
- self.queries[-1].key_field = token
1688
+ pk_field = token.replace('*', '')
1689
+ if not pk_field.isidentifier():
1690
+ pos = int(pk_field or '1')-1
1691
+ pk_field = self.queries[-1].values[SELECT][pos]
1692
+ self.queries[-1].key_field = pk_field.split('.')[-1]
1686
1693
  return
1687
- elif '$' in token:
1688
- func_name, token = token.split('$')
1689
- if func_name == 'count':
1690
- if not token:
1691
- token = 'count_1'
1692
- pk_field = self.queries[-1].key_field or 'id'
1693
- Count().As(token, extra_classes).add(pk_field, self.queries[-1])
1694
- return
1695
- else:
1696
- class_type = FUNCTION_CLASS.get(func_name)
1694
+ # -------------------------------------------------------
1695
+ def field_params() -> dict:
1696
+ ROLE_OF_SEPARATOR = {
1697
+ '$': 'function',
1698
+ ':': 'alias',
1699
+ '@': 'group',
1700
+ '!': 'field',
1701
+ }
1702
+ REGEX_FIELD = r'([{}])'.format(''.join(ROLE_OF_SEPARATOR))
1703
+ elements = re.split(REGEX_FIELD, token+'!')
1704
+ return {
1705
+ ROLE_OF_SEPARATOR[k]: v
1706
+ for k, v in zip(elements[1::2], elements[::2])
1707
+ }
1708
+ def run(function: str='', alias: str='', group: str='', field: str=''):
1709
+ is_count = function == 'count'
1710
+ if alias or is_count:
1711
+ field, alias = alias, field
1712
+ extra_classes = [OrderBy] if sorted else []
1713
+ if group:
1714
+ if not field:
1715
+ field = group
1716
+ elif not alias:
1717
+ alias = group
1718
+ extra_classes += [GroupBy]
1719
+ if function:
1720
+ if is_count and not field:
1721
+ field = self.queries[-1].key_field or 'id'
1722
+ class_type = FUNCTION_CLASS.get(function)
1697
1723
  if not class_type:
1698
- raise ValueError(f'Unknown function `{func_name}`.')
1699
- if ':' in token:
1700
- token, field_alias = token.split(':')
1701
- if extra_classes == [OrderBy]:
1702
- class_type = class_type().As(field_alias, OrderBy)
1703
- extra_classes = []
1704
- else:
1705
- class_type = class_type().As(field_alias)
1706
- class_list = [class_type]
1707
- class_list += extra_classes
1708
- FieldList(token, class_list).add('', self.queries[-1])
1724
+ raise ValueError(f'Unknown function `{function}`.')
1725
+ class_list = [ class_type().As(alias, extra_classes) ]
1726
+ elif alias:
1727
+ class_list = [NamedField(alias)] + extra_classes
1728
+ else:
1729
+ class_list = [Field] + extra_classes
1730
+ FieldList(field, class_list).add('', self.queries[-1])
1731
+ # -------------------------------------------------------
1732
+ run( **field_params() )
1733
+ # -------------------------------------------------------
1709
1734
 
1710
1735
  def left_ftable(self, token: str):
1711
1736
  if self.queries:
@@ -2117,7 +2142,7 @@ class CTE(Select):
2117
2142
  else:
2118
2143
  count = len(fields)
2119
2144
  queries = detect(
2120
- pattern*count, join_queries=False, format=format
2145
+ pattern*count, join_method=None, format=format
2121
2146
  )
2122
2147
  FieldList(fields, queries, ziped=True).add('', self)
2123
2148
  self.break_lines = True
@@ -2138,7 +2163,7 @@ class Recursive(CTE):
2138
2163
  def get_field(obj: SQLObject, pos: int) -> str:
2139
2164
  return obj.values[SELECT][pos].split('.')[-1]
2140
2165
  t1, t2 = detect(
2141
- pattern*2, join_queries=False, format=format
2166
+ pattern*2, join_method=None, format=format
2142
2167
  )
2143
2168
  pk_field = get_field(t1, 0)
2144
2169
  foreign_key = ''
@@ -2167,12 +2192,31 @@ MAIN_TAG = '__main__'
2167
2192
  class CTEFactory:
2168
2193
  def __init__(self, txt: str):
2169
2194
  """
2170
- Syntax:
2195
+ SQL syntax:
2171
2196
  ---
2172
- **SELECT ...
2197
+ **SELECT field, field
2173
2198
  FROM** ( `sub_query1` ) **AS** `alias_1`
2174
2199
  JOIN ( `sub_query2` ) **AS** `alias_2` **ON** `__join__`
2200
+
2201
+ Cypher syntax:
2202
+ ---
2203
+ Table1(field, `function$`field`:alias`, `group@`) <- Table2(field)
2204
+ `...`MainTable(field)
2175
2205
  """
2206
+ self.main = None
2207
+ if parser_class(txt) == CypherParser:
2208
+ if '...' in txt:
2209
+ txt, other = txt.split('...')
2210
+ self.main = detect(other)
2211
+ alias = self.main.table_name
2212
+ query_list = Select.parse(txt, CypherParser)
2213
+ if not self.main:
2214
+ alias = '_'.join(query.table_name for query in query_list)
2215
+ self.main = Select(alias)
2216
+ self.main.break_lines = False
2217
+ query = join_queries(query_list)
2218
+ self.cte_list = [CTE(alias, [query])]
2219
+ return
2176
2220
  summary = self.extract_subqueries(txt)
2177
2221
  self.main = detect( summary.pop(MAIN_TAG) )
2178
2222
  self.cte_list = [
@@ -2351,8 +2395,13 @@ def parser_class(text: str) -> Parser:
2351
2395
  return class_type
2352
2396
  return None
2353
2397
 
2398
+ def join_queries(query_list: list) -> Select:
2399
+ result = query_list[0]
2400
+ for query in query_list[1:]:
2401
+ result += query
2402
+ return result
2354
2403
 
2355
- def detect(text: str, join_queries: bool = True, format: str='') -> Select | list[Select]:
2404
+ def detect(text: str, join_method = join_queries, format: str='') -> Select | list[Select]:
2356
2405
  from collections import Counter
2357
2406
  parser = parser_class(text)
2358
2407
  if not parser:
@@ -2367,34 +2416,29 @@ def detect(text: str, join_queries: bool = True, format: str='') -> Select | lis
2367
2416
  Select.EQUIVALENT_NAMES[new_name] = table
2368
2417
  text = text[:begin] + new_name + '(' + text[end:]
2369
2418
  count -= 1
2370
- query_list = Select.parse(text, parser)
2419
+ result = Select.parse(text, parser)
2371
2420
  if format:
2372
- for query in query_list:
2421
+ for query in result:
2373
2422
  query.set_file_format(format)
2374
- if not join_queries:
2375
- return query_list
2376
- result = query_list[0]
2377
- for query in query_list[1:]:
2378
- result += query
2423
+ if join_method:
2424
+ result = join_method(result)
2379
2425
  return result
2380
2426
  # ===========================================================================================//
2381
2427
 
2382
2428
  if __name__ == "__main__":
2383
- query = Select(
2384
- 'Sales s', #quantity=Sum().As('qty_sold'),
2385
- ref_date=GroupBy(
2386
- ref_year=Year, qty_sold=Sum('quantity'),
2387
- vendor=Select(
2388
- 'Vendor v', id=[PrimaryKey, NamedField('vendor_id')], name=Field
2389
- ),
2390
- prod_id=Field
2391
- )
2392
- )
2393
- cte = CTE('Sales_by_year', [query])(
2394
- _=Where.join(
2395
- Select('Goal G'), dict(
2396
- prod_id='product', ref_year='year', vendor_id='vendor'
2397
- )
2398
- )
2429
+ cte = CTEFactory(
2430
+ "Sales(year$ref_date:ref_year@, sum$quantity:qty_sold, vendor) <- Vendor(id, name:vendors_name@)"
2431
+ # ^^^ ^^^ ^^^
2432
+ # | | | ^^^ ^^^
2433
+ # | | | | |
2434
+ # | | | Relate Sales to Vendor --------+ |
2435
+ # | | | |
2436
+ # | | +---- Call it `ref_year` and group it |
2437
+ # | | |
2438
+ # | +-- Extracts the year from the `ref_date` field |
2439
+ # | |
2440
+ # +--- The Sales table |
2441
+ # Also groups by vendor´s name ------------------+
2442
+ "...Annual_Sales_per_Vendor(ref_year, qty_sold, vendors_name, *) -> Goal(year, target)"
2399
2443
  )
2400
- print(cte)
2444
+ print(cte)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 1.20250710
3
+ Version: 1.20250712
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
@@ -948,7 +948,7 @@ R2 = Recursive.create(
948
948
  >> Note: Comments added later.
949
949
  ---
950
950
 
951
- ### CTEFactory class
951
+ ### 17.3 - CTEFactory class
952
952
  CTEFactory exchanges subqueries for CTEs, simply by passing the text of the "dirty" query:
953
953
 
954
954
  *Example*:
@@ -992,4 +992,27 @@ results...
992
992
  ORDER BY
993
993
  u001.name
994
994
  ```
995
+
996
+ #### 17.3.1 - You can also pass a Cypher script like in the example below:
997
+
998
+ ![image](assets/CTEFactory.png)
999
+
1000
+ results...
1001
+ ```
1002
+ WITH Annual_Sales_per_Vendor AS (
1003
+ SELECT ven.name as vendors_name, Year(sal.ref_date) as ref_year
1004
+ , Sum(sal.quantity) as qty_sold FROM Vendor ven LEFT JOIN Sales sal ON (ven.id = sal.vendor)
1005
+ GROUP BY ven.name, ref_year
1006
+ )
1007
+ SELECT
1008
+ aspv.ref_year,
1009
+ aspv.qty_sold,
1010
+ aspv.vendors_name,
1011
+ goa.target
1012
+ FROM
1013
+ Annual_Sales_per_Vendor aspv
1014
+ RIGHT JOIN Goal goa ON (aspv.ref_year = goa.year)
1015
+ ```
1016
+
1017
+
995
1018
  ---
File without changes