sql-blocks 1.20250715__py3-none-any.whl → 1.20250719__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/sql_blocks.py CHANGED
@@ -1994,7 +1994,7 @@ class Select(SQLObject):
1994
1994
  from copy import deepcopy
1995
1995
  return deepcopy(self)
1996
1996
 
1997
- def no_relation_error(self, other: SQLObject):
1997
+ def relation_error(self, other: SQLObject):
1998
1998
  raise ValueError(f'No relationship found between {self.table_name} and {other.table_name}.')
1999
1999
 
2000
2000
  def __add__(self, other: SQLObject):
@@ -2011,7 +2011,7 @@ class Select(SQLObject):
2011
2011
  PrimaryKey.add(primary_key, query)
2012
2012
  query.add(foreign_field, other)
2013
2013
  return other
2014
- self.no_relation_error(other) # === raise ERROR ... ===
2014
+ self.relation_error(other) # === raise ERROR ... ===
2015
2015
  elif primary_key:
2016
2016
  PrimaryKey.add(primary_key, other)
2017
2017
  other.add(foreign_field, query)
@@ -2040,7 +2040,7 @@ class Select(SQLObject):
2040
2040
  else:
2041
2041
  fk_field, primary_k = ForeignKey.find(other, self)
2042
2042
  if not fk_field:
2043
- self.no_relation_error(other) # === raise ERROR ... ===
2043
+ self.relation_error(other) # === raise ERROR ... ===
2044
2044
  query = other.copy()
2045
2045
  other = self.copy()
2046
2046
  query.__class__ = NotSelectIN
@@ -2127,8 +2127,8 @@ class CTE(Select):
2127
2127
  query.break_lines = False
2128
2128
  result, line = [], ''
2129
2129
  keywords = '|'.join(KEYWORD)
2130
- for word in re.split(fr'({keywords}|AND|OR|,)', str(query)):
2131
- if len(line) >= 50:
2130
+ for word in re.split(fr'({keywords}|AND|OR|JOIN|,)', str(query)):
2131
+ if len(line) >= 30:
2132
2132
  result.append(line)
2133
2133
  line = ''
2134
2134
  line += word
@@ -2197,7 +2197,8 @@ class Recursive(CTE):
2197
2197
  MAIN_TAG = '__main__'
2198
2198
 
2199
2199
  class CTEFactory:
2200
-
2200
+ TEMPLATE_FIELD_FUNC = lambda t: t.lower()[:3] + '_id'
2201
+
2201
2202
  def __init__(self, txt: str, template: str = ''):
2202
2203
  """
2203
2204
  SQL syntax:
@@ -2208,35 +2209,61 @@ class CTEFactory:
2208
2209
 
2209
2210
  Cypher syntax:
2210
2211
  ---
2211
- Table1(field, `function$`field`:alias`, `group@`) <- Table2(field)
2212
- `...`MainTable(field)
2212
+ `cte_name`[
2213
+ Table1(field, `function$`field`:alias`, `group@`) <- Table2(field)
2214
+ ]
2213
2215
  """
2216
+ def put_parentheses():
2217
+ result = txt
2218
+ for found in re.findall(r'\[\d+\][^(]', result):
2219
+ item = found.strip()[:3]
2220
+ result = result.replace(item, f'{item}()')
2221
+ return result
2222
+ txt, *negative = re.split(r'(\[-\d+\])', txt)
2223
+ txt = put_parentheses()
2224
+ txt, *suffix = re.split(r'(\[\d+\])', txt, maxsplit=1)
2214
2225
  if template:
2215
2226
  for table in re.findall(r'[#](\w+)', txt):
2216
- txt = txt.replace(f'#{table}', template.format(t=table))
2217
- if parser_class(txt) == CypherParser:
2218
- txt, *main_script = txt.split('...')
2219
- query_list = Select.parse(txt, CypherParser)
2220
- if main_script:
2221
- main_script = self.replace_wildcards(
2222
- ''.join(main_script), query_list
2223
- )
2224
- self.main = detect(main_script)
2225
- alias = self.main.table_name
2226
- else:
2227
+ txt = txt.replace( f'#{table}', template.format(
2228
+ t=table, f=CTEFactory.TEMPLATE_FIELD_FUNC(table)
2229
+ ) )
2230
+ self.cte_list = []
2231
+ self.main = None
2232
+ for script in txt.split(']'):
2233
+ if '(' not in script:
2234
+ script += self.replace_wildcards(''.join(suffix))
2235
+ suffix = []
2236
+ self.build_ctes(script)
2237
+ if not self.cte_list:
2238
+ return
2239
+ if not suffix and negative:
2240
+ suffix = negative
2241
+ if suffix:
2242
+ self.main = detect( self.replace_wildcards(''.join(suffix)) )
2243
+ elif not self.main:
2244
+ self.main = Select(self.cte_list[0].table_name)
2245
+ self.main.break_lines = False
2246
+
2247
+ def build_ctes(self, script: str):
2248
+ alias, *body = script.split('[')
2249
+ if body:
2250
+ script = ''.join(body)
2251
+ if not re.sub( r'\s+', '', script):
2252
+ return
2253
+ if parser_class(script) == CypherParser:
2254
+ query_list = Select.parse(script, CypherParser)
2255
+ if not body:
2227
2256
  alias = '_'.join(query.table_name for query in query_list)
2228
- self.main = Select(alias)
2229
- self.main.break_lines = False
2230
2257
  related_tables = any([
2231
2258
  query.join_type.value for query in query_list
2232
2259
  ])
2233
2260
  if related_tables:
2234
2261
  query_list = [ join_queries(query_list) ]
2235
- self.cte_list = [CTE(alias, query_list)]
2262
+ self.cte_list += [CTE(alias, query_list)]
2236
2263
  return
2237
- summary = self.extract_subqueries(txt)
2264
+ summary = self.extract_subqueries(script)
2238
2265
  self.main = detect( summary.pop(MAIN_TAG) )
2239
- self.cte_list = [
2266
+ self.cte_list += [
2240
2267
  CTE(alias, [
2241
2268
  Select.parse(query)[0]
2242
2269
  for query in elements
@@ -2245,24 +2272,43 @@ class CTEFactory:
2245
2272
  ]
2246
2273
 
2247
2274
  def __str__(self):
2275
+ if not self.main:
2276
+ return ''
2248
2277
  CTE.show_query = False
2249
2278
  lines = [str(cte) for cte in self.cte_list]
2250
2279
  result = ',\n'.join(lines) + '\n' + str(self.main)
2251
2280
  CTE.show_query = True
2252
2281
  return result
2253
2282
 
2254
- @staticmethod
2255
- def replace_wildcards(txt: str, query_list: list) -> str:
2256
- if '(*)' in txt:
2257
- field_list = [
2258
- re.split(
2259
- r'\bas\b|\bAS\b', field
2260
- )[-1].strip()
2261
- for query in query_list
2262
- for field in query.values.get(SELECT, [])
2263
- ]
2264
- return txt.replace('(*)', '({}, *)'.format(
2265
- ','.join( set(field_list) )
2283
+ def replace_wildcards(self, txt: str) -> str:
2284
+ ALL_FIELDS_WILDCARD = '**'
2285
+ result = ''
2286
+ names = []
2287
+ query_list = []
2288
+ cte: CTE
2289
+ for cte in self.cte_list:
2290
+ names.append(cte.table_name)
2291
+ query_list += cte.query_list
2292
+ last = 0
2293
+ for found in re.finditer(r'\[[-]*\d+\]', txt):
2294
+ pos = int(re.sub(r'\[|\]', '', found.group()))
2295
+ if pos > 0:
2296
+ pos -= 1
2297
+ start = found.start()
2298
+ result += txt[last:start] + names[pos]
2299
+ last = found.end()
2300
+ txt = result + txt[last:]
2301
+ if ALL_FIELDS_WILDCARD in txt:
2302
+ field_list = []
2303
+ for query in query_list:
2304
+ for field in query.values.get(SELECT, []):
2305
+ new_item = re.split(
2306
+ r'\bas\b|\bAS\b|[.]', field
2307
+ )[-1].strip()
2308
+ if new_item not in field_list:
2309
+ field_list.append(new_item)
2310
+ return txt.replace(ALL_FIELDS_WILDCARD, '{}'.format(
2311
+ ','.join(field_list)
2266
2312
  ))
2267
2313
  return txt
2268
2314
 
@@ -2458,38 +2504,3 @@ def detect(text: str, join_method = join_queries, format: str='') -> Select | li
2458
2504
  # ===========================================================================================//
2459
2505
 
2460
2506
 
2461
- if __name__ == "__main__":
2462
- OrderBy.sort = SortType.DESC
2463
- cte = CTEFactory(
2464
- # "#Customer#Employee#Supplier...People_by_Type(*)",
2465
- # template = '{t}("{t[0]}":ptype, nome:person_name)'
2466
- """
2467
- SELECT u001.name, agg_sales.total
2468
- FROM (
2469
- SELECT * FROM Users u
2470
- WHERE u.status = 'active'
2471
- ) AS u001
2472
- JOIN (
2473
- SELECT s.user_id, Sum(s.value) as total
2474
- FROM Sales s
2475
- GROUP BY s.user_id
2476
- )
2477
- As agg_sales
2478
- ON u001.id = agg_sales.user_id
2479
- ORDER BY u001.name
2480
- """
2481
- # "Sales(year$ref_date:ref_year@, sum$quantity:qty_sold, vendor) <- Vendor(id, name:vendors_name@)"
2482
- # ^^^ ^^^ ^^^
2483
- # | | | ^^^ ^^^
2484
- # | | | | |
2485
- # | | | Relate Sales to Vendor --------+ |
2486
- # | | | |
2487
- # | | +---- Call it `ref_year` and group it |
2488
- # | | |
2489
- # | +-- Extracts the year from the `ref_date` field |
2490
- # | |
2491
- # +--- The Sales table |
2492
- # Also groups by vendor´s name ------------------+
2493
- # "...Annual_Sales_per_Vendor(*) -> Goal(^year, target)"
2494
- )
2495
- print(cte)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql_blocks
3
- Version: 1.20250715
3
+ Version: 1.20250719
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
@@ -573,16 +573,20 @@ ORDER BY
573
573
  mov.title
574
574
  ```
575
575
  ---
576
- > **Separators and meaning:**
576
+
577
+ #### Separators and meaning:
578
+ <a id="cypher_separators"></a>
579
+
577
580
  * `( )` Delimits a table and its fields
578
581
  * `,` Separate fields
579
582
  * `?` For simple conditions (> < = <>)
580
583
  * `<-` connects to the table on the left
581
584
  * `->` connects to the table on the right
582
585
  * `^` Put the field in the ORDER BY clause
583
- * `@` Immediately after the table name, it indicates the grouping field.
586
+ * `@` Immediately after the table/field name, it indicates the grouping field.
584
587
  * `$` For SQL functions like **avg**$_field_, **sum**$_field_, **count**$_field_...
585
588
  * `*` Sets the primary key field.
589
+ * `:` Allows you to assign an alias to the field or expression.
586
590
 
587
591
 
588
592
  ---
@@ -869,6 +873,7 @@ UNION ALL
869
873
  ```
870
874
 
871
875
  * **17.2.1 - The `create` method** ... parameters :
876
+ <a id="cte_create_method"></a>
872
877
  - name: The name of the CTE
873
878
  - pattern: A cypher script that defines the tables used
874
879
  - formula: The format for `Where.formula` method _(*)_
@@ -881,6 +886,7 @@ UNION ALL
881
886
  '[2] = R.[1]', 'JFK', format='.csv'
882
887
  ) # ^^^--- Flyghts from JFK airport
883
888
  ```
889
+
884
890
  _...Creates a recursive CTE called Route, using Flyght table, where the recursivity condition is Flyght.arrival equals to Route.departure_
885
891
  >> (*) -- Note that [1] and [2] refers to first field and second field. 😉
886
892
 
@@ -995,31 +1001,36 @@ results...
995
1001
 
996
1002
  #### 17.3.1 - You can also pass a Cypher script like in the example below:
997
1003
 
998
- cte = CTEFactory(
999
- "Sales(year$ref_date:ref_year@, sum$quantity:qty_sold, vendor)"
1000
- " <- Vendor(id, name:vendors_name@)"
1001
- "...Annual_Sales_per_Vendor(*) -> Goal(year, target)"
1002
- )
1003
- print(cte)
1004
+ cte = CTEFactory("""
1005
+ Annual_Sales_per_Vendor[
1006
+ Sales(
1007
+ year$ref_date:ref_year@, sum$quantity:qty_sold,
1008
+ vendor) <- Vendor(id, name:vendors_name@)
1009
+ ]
1004
1010
 
1005
- ![image](https://raw.githubusercontent.com/julio-cascalles/sql_blocks/refs/heads/master/assets/CTEFactory.png)
1011
+ [-1](**, ref_year) -> Goal(year, target)
1012
+ """)
1013
+ print(cte)
1006
1014
 
1007
1015
  results...
1008
1016
  ```
1009
1017
  WITH Annual_Sales_per_Vendor AS (
1010
- SELECT ven.name as vendors_name, Year(sal.ref_date) as ref_year
1011
- , Sum(sal.quantity) as qty_sold FROM Vendor ven LEFT JOIN Sales sal ON (ven.id = sal.vendor)
1018
+ SELECT ven.name as vendors_name
1019
+ , Year(sal.ref_date) as ref_year
1020
+ , Sum(sal.quantity) as qty_sold
1021
+ FROM Vendor ven LEFT JOIN Sales sal ON (ven.id = sal.vendor
1012
1022
  GROUP BY ven.name, ref_year
1013
1023
  )
1014
1024
  SELECT
1025
+ aspv.vendors_name,
1015
1026
  aspv.ref_year,
1016
1027
  aspv.qty_sold,
1017
- aspv.vendors_name,
1018
1028
  goa.target
1019
1029
  FROM
1020
1030
  Annual_Sales_per_Vendor aspv
1021
1031
  RIGHT JOIN Goal goa ON (aspv.ref_year = goa.year)
1022
1032
  ```
1033
+ For more details, see the [Cypher syntax](#cypher_separators) or [CTE create method](#cte_create_method)!
1023
1034
 
1024
1035
 
1025
1036
  ---
@@ -0,0 +1,7 @@
1
+ sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
2
+ sql_blocks/sql_blocks.py,sha256=F6MnDlqlBDnCZkN6t2v3Kx7Og5o7kDetRvMP3xdrBGg,85649
3
+ sql_blocks-1.20250719.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
+ sql_blocks-1.20250719.dist-info/METADATA,sha256=3Aggm19HCvCpFNf-Mw8NCqX38WRTyd-eeBfg7wvpLwQ,25592
5
+ sql_blocks-1.20250719.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
+ sql_blocks-1.20250719.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
+ sql_blocks-1.20250719.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- sql_blocks/__init__.py,sha256=5ItzGCyqqa6kwY8wvF9kapyHsAiWJ7KEXCcC-OtdXKg,37
2
- sql_blocks/sql_blocks.py,sha256=LNdU9qK7eROrpCzggnPxOOlTSKkuU-mVYJLNsZNjvdY,85746
3
- sql_blocks-1.20250715.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
- sql_blocks-1.20250715.dist-info/METADATA,sha256=pVsle1pwP1elpLwRZtpcXjL5uIi2iTiysunYaBj4N3I,25388
5
- sql_blocks-1.20250715.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
- sql_blocks-1.20250715.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
- sql_blocks-1.20250715.dist-info/RECORD,,