sql-blocks 0.0.2__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 ADDED
@@ -0,0 +1 @@
1
+ from sql_blocks import *
@@ -0,0 +1,434 @@
1
+ from enum import Enum
2
+
3
+ KEYWORD = {
4
+ 'SELECT': (',{}', 'SELECT *'),
5
+ 'FROM': ('{}', ''),
6
+ 'WHERE': ('{}AND ', ''),
7
+ 'GROUP BY': (',{}', ''),
8
+ 'ORDER BY': (',{}', ''),
9
+ 'LIMIT': (' ', ''),
10
+ }
11
+ SELECT, FROM, WHERE, GROUP_BY, ORDER_BY, LIMIT = KEYWORD.keys()
12
+ USUAL_KEYS = [SELECT, WHERE, GROUP_BY, ORDER_BY]
13
+
14
+
15
+ class SQLObject:
16
+ def __init__(self, table_name: str=''):
17
+ self.alias = ''
18
+ self.values = {}
19
+ self.key_field = ''
20
+ self.set_table(table_name)
21
+
22
+ def set_table(self, table_name: str):
23
+ if not table_name:
24
+ return
25
+ if ' ' in table_name:
26
+ table_name, self.alias = table_name.split()
27
+ elif '_' in table_name:
28
+ self.alias = ''.join(
29
+ word[0].lower()
30
+ for word in table_name.split('_')
31
+ )
32
+ else:
33
+ self.alias = table_name.lower()[:3]
34
+ self.values.setdefault(FROM, []).append(f'{table_name} {self.alias}')
35
+
36
+ @property
37
+ def table_name(self) -> str:
38
+ return self.values[FROM][0].split()[0]
39
+
40
+ def delete(self, search: str, keys: list=USUAL_KEYS):
41
+ for key in keys:
42
+ result = []
43
+ for item in self.values.get(key, []):
44
+ if search not in item:
45
+ result.append(item)
46
+ self.values[key] = result
47
+
48
+
49
+ class Function:
50
+ ...
51
+
52
+ class Field:
53
+ prefix = ''
54
+
55
+ @classmethod
56
+ def format(cls, name: str, main: SQLObject) -> str:
57
+ if name == '_':
58
+ name = '*'
59
+ else:
60
+ name = f'{main.alias}.{name}'
61
+ if Function in cls.__bases__:
62
+ name = f'{cls.__name__}({name})'
63
+ return f'{cls.prefix}{name}'
64
+
65
+ @classmethod
66
+ def add(cls, name: str, main: SQLObject):
67
+ main.values.setdefault(SELECT, []).append(
68
+ cls.format(name, main)
69
+ )
70
+
71
+
72
+ class Avg(Function, Field):
73
+ ...
74
+ class Min(Function, Field):
75
+ ...
76
+ class Max(Function, Field):
77
+ ...
78
+ class Sum(Function, Field):
79
+ ...
80
+ class Count(Function, Field):
81
+ ...
82
+
83
+ class Distinct(Field):
84
+ prefix = 'DISTINCT '
85
+
86
+
87
+ class NamedField:
88
+ def __init__(self, alias: str, class_type = Field):
89
+ self.alias = alias
90
+ if class_type not in [Field] + Field.__subclasses__():
91
+ raise TypeError('class_type must be a Field (sub)class.')
92
+ self.class_type = class_type
93
+
94
+ def add(self, name: str, main: SQLObject):
95
+ main.values.setdefault(SELECT, []).append(
96
+ '{} as {}'.format(
97
+ self.class_type.format(name, main),
98
+ self.alias
99
+ )
100
+ )
101
+
102
+
103
+ class Table:
104
+ def __init__(self, fields: list | str=[]):
105
+ if isinstance(fields, str):
106
+ fields = [f.strip() for f in fields.split(',')]
107
+ self.fields = fields
108
+
109
+ def add(self, name: str, main: SQLObject):
110
+ main.set_table(name)
111
+ for field in self.fields:
112
+ Field.add(field, main)
113
+
114
+
115
+ class PrimaryKey:
116
+ @staticmethod
117
+ def add(name: str, main: SQLObject):
118
+ main.key_field = name
119
+
120
+
121
+ class ForeignKey:
122
+ references = {}
123
+
124
+ def __init__(self, table_name: str):
125
+ self.table_name = table_name
126
+
127
+ def add(self, name: str, main: SQLObject):
128
+ key = f'{main.table_name}_{self.table_name}'
129
+ ForeignKey.references[key] = name
130
+
131
+ @classmethod
132
+ def find(cls, obj1: SQLObject, obj2: SQLObject) -> list:
133
+ key = f'{obj1.table_name}_{obj2.table_name}'
134
+ return cls.references.get(key, '').split('_')
135
+
136
+
137
+ def quoted(value) -> str:
138
+ if isinstance(value, str):
139
+ value = f"'{value}'"
140
+ return str(value)
141
+
142
+
143
+ class Where:
144
+ prefix = ''
145
+
146
+ def __init__(self, expr: str):
147
+ self.expr = f'{self.prefix}{expr}'
148
+
149
+ @classmethod
150
+ def __constructor(cls, operator: str, value):
151
+ return cls(expr=f'{operator} {quoted(value)}')
152
+
153
+ @classmethod
154
+ def eq(cls, value):
155
+ return cls.__constructor('=', value)
156
+
157
+ @classmethod
158
+ def like(cls, value: str):
159
+ return cls(f"LIKE '%{value}%'")
160
+
161
+ @classmethod
162
+ def gt(cls, value):
163
+ return cls.__constructor('>', value)
164
+
165
+ @classmethod
166
+ def gte(cls, value):
167
+ return cls.__constructor('>=', value)
168
+
169
+ @classmethod
170
+ def lt(cls, value):
171
+ return cls.__constructor('<', value)
172
+
173
+ @classmethod
174
+ def lte(cls, value):
175
+ return cls.__constructor('<=', value)
176
+
177
+ @classmethod
178
+ def is_null(cls):
179
+ return cls('IS NULL')
180
+
181
+ @classmethod
182
+ def list(cls, values):
183
+ if isinstance(values, list):
184
+ values = ','.join(quoted(v) for v in values)
185
+ return cls(f'IN ({values})')
186
+
187
+ def add(self, name: str, main: SQLObject):
188
+ main.values.setdefault(WHERE, []).append('{} {}'.format(
189
+ Field.format(name, main), self.expr
190
+ ))
191
+
192
+
193
+ class Not(Where):
194
+ prefix = 'NOT '
195
+
196
+ @classmethod
197
+ def eq(cls, value):
198
+ return Where.__constructor('<>', value)
199
+
200
+
201
+ class Case:
202
+ def __init__(self, field: str):
203
+ self.__conditions = {}
204
+ self.default = None
205
+ self.field = field
206
+
207
+ def when(self, condition: Where, result: str):
208
+ self.__conditions[result] = condition
209
+ return self
210
+
211
+ def else_value(self, default: str):
212
+ self.default = default
213
+ return self
214
+
215
+ def add(self, name: str, main: SQLObject):
216
+ field = Field.format(self.field, main)
217
+ default = quoted(self.default)
218
+ name = 'CASE \n{}\n\tEND AS {}'.format(
219
+ '\n'.join(
220
+ f'\t\tWHEN {field} {cond.expr} THEN {quoted(res)}'
221
+ for res, cond in self.__conditions.items()
222
+ ) + f'\n\t\tELSE {default}' if default else '',
223
+ name
224
+ )
225
+ main.values.setdefault(SELECT, []).append(name)
226
+
227
+
228
+ class Options:
229
+ def __init__(self, **values):
230
+ self.__children: dict = values
231
+
232
+ def add(self, logical_separator: str, main: SQLObject):
233
+ """
234
+ `logical_separator` must be AND or OR
235
+ """
236
+ conditions: list[str] = []
237
+ child: Where
238
+ for field, child in self.__children.items():
239
+ conditions.append(' {} {} '.format(
240
+ Field.format(field, main), child.expr
241
+ ))
242
+ main.values.setdefault(WHERE, []).append(
243
+ '(' + logical_separator.join(conditions) + ')'
244
+ )
245
+
246
+
247
+ class Between:
248
+ def __init__(self, start, end):
249
+ if start > end:
250
+ start, end = end, start
251
+ self.start = start
252
+ self.end = end
253
+
254
+ def add(self, name: str, main:SQLObject):
255
+ Where.gte(self.start).add(name, main),
256
+ Where.lte(self.end).add(name, main)
257
+
258
+
259
+ class SortType(Enum):
260
+ ASC = ''
261
+ DESC = ' DESC'
262
+
263
+ class OrderBy:
264
+ sort: SortType = SortType.ASC
265
+ @classmethod
266
+ def add(cls, name: str, main: SQLObject):
267
+ if main.alias:
268
+ name = f'{main.alias}.{name}'
269
+ main.values.setdefault(ORDER_BY, []).append(name + cls.sort.value)
270
+
271
+
272
+ class GroupBy:
273
+ @staticmethod
274
+ def add(name: str, main: SQLObject):
275
+ main.values.setdefault(GROUP_BY, []).append(f'{main.alias}.{name}')
276
+
277
+
278
+ class Having:
279
+ def __init__(self, function: Function, condition: Where):
280
+ self.function = function
281
+ self.condition = condition
282
+
283
+ def add(self, name: str, main:SQLObject):
284
+ main.values[GROUP_BY][-1] += ' HAVING {} {}'.format(
285
+ self.function.format(name, main), self.condition.expr
286
+ )
287
+
288
+ @classmethod
289
+ def avg(cls, condition: Where):
290
+ return cls(Avg, condition)
291
+
292
+ @classmethod
293
+ def min(cls, condition: Where):
294
+ return cls(Min, condition)
295
+
296
+ @classmethod
297
+ def max(cls, condition: Where):
298
+ return cls(Max, condition)
299
+
300
+ @classmethod
301
+ def sum(cls, condition: Where):
302
+ return cls(Sum, condition)
303
+
304
+ @classmethod
305
+ def count(cls, condition: Where):
306
+ return cls(Count, condition)
307
+
308
+
309
+ class JoinType(Enum):
310
+ INNER = ''
311
+ LEFT = 'LEFT '
312
+ RIGHT = 'RIGHT '
313
+ FULL = 'FULL '
314
+
315
+ class Select(SQLObject):
316
+ join_type: JoinType = JoinType.INNER
317
+ REGEX = None
318
+
319
+ def __init__(self, table_name: str='', **values):
320
+ super().__init__(table_name)
321
+ self.__call__(**values)
322
+ self.break_lines = True
323
+
324
+ def add(self, name: str, main: SQLObject):
325
+ def update_values(key: str, new_values: list):
326
+ for value in new_values:
327
+ old_values = main.values.get(key, [])
328
+ if value not in old_values:
329
+ main.values[key] = old_values + [value]
330
+ update_values(
331
+ FROM, [
332
+ '{jt}JOIN {tb} {a2} ON ({a1}.{f1} = {a2}.{f2})'.format(
333
+ jt=self.join_type.value,
334
+ tb=self.table_name,
335
+ a1=main.alias, f1=name,
336
+ a2=self.alias, f2=self.key_field
337
+ )
338
+ ] + self.values[FROM][1:]
339
+ )
340
+ for key in USUAL_KEYS:
341
+ update_values(key, self.values.get(key, []))
342
+
343
+ def __add__(self, other: SQLObject):
344
+ foreign_field, *primary_key = ForeignKey.find(self, other)
345
+ if not foreign_field:
346
+ foreign_field, *primary_key = ForeignKey.find(other, self)
347
+ if foreign_field:
348
+ if primary_key:
349
+ PrimaryKey.add(primary_key[0], self)
350
+ self.add(foreign_field, other)
351
+ return other
352
+ raise ValueError(f'No relationship found between {self.table_name} and {other.table_name}.')
353
+ elif primary_key:
354
+ PrimaryKey.add(primary_key[0], other)
355
+ other.add(foreign_field, self)
356
+ return self
357
+
358
+ def __str__(self) -> str:
359
+ TABULATION = '\n\t' if self.break_lines else ' '
360
+ LINE_BREAK = '\n' if self.break_lines else ' '
361
+ DEFAULT = lambda key: KEYWORD[key][1]
362
+ FMT_SEP = lambda key: KEYWORD[key][0].format(TABULATION)
363
+ select, _from, where, groupBy, orderBy, limit = [
364
+ DEFAULT(key) if not self.values.get(key) else "{}{}{}{}".format(
365
+ LINE_BREAK, key, TABULATION, FMT_SEP(key).join(self.values[key])
366
+ ) for key in KEYWORD
367
+ ]
368
+ return f'{select}{_from}{where}{groupBy}{orderBy}{limit}'.strip()
369
+
370
+ def __call__(self, **values):
371
+ to_list = lambda x: x if isinstance(x, list) else [x]
372
+ for name, params in values.items():
373
+ for obj in to_list(params):
374
+ obj.add(name, self)
375
+ return self
376
+
377
+ def __eq__(self, other: SQLObject) -> bool:
378
+ def sorted_values(obj: SQLObject, key: str) -> list:
379
+ return sorted(obj.values.get(key, []))
380
+ for key in KEYWORD:
381
+ if sorted_values(self, key) != sorted_values(other, key):
382
+ return False
383
+ return True
384
+
385
+ def limit(self, row_count: int, offset: int=0):
386
+ result = [str(row_count)]
387
+ if offset > 0:
388
+ result.append(f'OFFSET {offset}')
389
+ self.values.setdefault(LIMIT, result)
390
+ return self
391
+
392
+ @classmethod
393
+ def parse(cls, txt: str) -> list[SQLObject]:
394
+ import re
395
+ txt = re.sub(' +', ' ', re.sub('\n|\t', ' ', txt))
396
+ if not cls.REGEX:
397
+ keywords = '|'.join(KEYWORD)
398
+ cls.REGEX = re.compile(f'({keywords})', re.IGNORECASE)
399
+ tokens = [t.strip() for t in cls.REGEX.split(txt) if t.strip()]
400
+ values = {k: v for k, v in zip(tokens[::2], tokens[1::2])}
401
+ tables = [t.strip() for t in re.split('JOIN|LEFT|RIGHT|ON', values[FROM]) if t.strip()]
402
+ result = {}
403
+ for item in tables:
404
+ if '=' in item:
405
+ a1, f1, a2, f2 = [r.strip() for r in re.split('[().=]', item) if r]
406
+ obj1: SQLObject = result[a1]
407
+ obj2:SQLObject = result[a2]
408
+ PrimaryKey.add(f2, obj2)
409
+ ForeignKey(obj2.table_name).add(f1, obj1)
410
+ else:
411
+ obj = cls(item)
412
+ for key in USUAL_KEYS:
413
+ if not key in values:
414
+ continue
415
+ separator = KEYWORD[key][0].format(
416
+ ' ' if key == WHERE else ''
417
+ )
418
+ fields = [
419
+ fld.strip() for fld in re.split(
420
+ separator, values[key]
421
+ ) if len(tables) == 1
422
+ or re.findall(f'^[( ]*{obj.alias}\.', fld)
423
+ ]
424
+ obj.values[key] = [
425
+ f'{obj.alias}.{f}' if not '.' in f else f for f in fields
426
+ ]
427
+ result[obj.alias] = obj
428
+ return list( result.values() )
429
+
430
+
431
+ class SubSelect(Select):
432
+ def add(self, name: str, main: SQLObject):
433
+ self.break_lines = False
434
+ Where.list(self).add(name, main)
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,296 @@
1
+ Metadata-Version: 2.1
2
+ Name: sql_blocks
3
+ Version: 0.0.2
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
+ Home-page: https://github.com/julio-cascalles/sql_blocks
6
+ Author: Júlio Cascalles
7
+ Author-email: Julio Cascalles <julio.cascalles@outlook.com>
8
+ Project-URL: Homepage, https://github.com/julio-cascalles/sql_blocks
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+
16
+ # SQL_Blocks
17
+
18
+ ### 1 - You can assemble a simple object that will then be converted into an SQL command:
19
+
20
+ > a = Select('Actor') # --> SELECT * FROM Actor act
21
+
22
+ _Note that an alias "act" has been added._
23
+
24
+ You can specify your own alias: `a = Select('Actor a')`
25
+
26
+ ---
27
+ ### 2 - You can also add a field, like this...
28
+
29
+ * a = Select('Actor a', **name=Field**)
30
+
31
+ * Here are another ways to add a field:
32
+ - Select('Actor a', name=Distinct )
33
+
34
+ - Select('Actor a', name=NamedField('actors_name'))
35
+
36
+ - Select(
37
+ 'Actor a',
38
+ name=NamedField('actors_name', Distinct)
39
+ )
40
+
41
+ ---
42
+
43
+ ### 3 - To set conditions, use **Where**:
44
+ * For example, `a = Select(... age=Where.gt(45) )`
45
+
46
+ Some possible conditions:
47
+ * field=Where.eq(value) - ...the field is EQUAL to the value;
48
+ * field=Where.gt(value) - ...the field is GREATER than the value;
49
+ * field=Where.lt(value) - ...the field is LESS than the value;
50
+
51
+ 3.1 -- If you want to filter the field on a range of values:
52
+
53
+ `a = Select( 'Actor a', age=Between(45, 69) )`
54
+
55
+ 3.2 -- Sub-queries:
56
+
57
+ ```
58
+ query = Select('Movie m', title=Field,
59
+ id=SubSelect(
60
+ 'Review r',
61
+ rate=Where.gt(4.5),
62
+ movie_id=Distinct
63
+ )
64
+ )
65
+ ```
66
+
67
+ **>> print(query)**
68
+
69
+ SELECT
70
+ m.title
71
+ FROM
72
+ Movie m
73
+ WHERE
74
+ m.id IN (
75
+ SELECT DISTINCT r.movie
76
+ FROM Review r WHERE r.rate > 4.5
77
+ )
78
+
79
+ 3.3 -- Optional conditions:
80
+ ```
81
+ OR=Options(
82
+ genre=Where.eq("Sci-Fi"),
83
+ awards=Where.like("Oscar")
84
+ )
85
+ ```
86
+ > Could be AND=Options(...)
87
+
88
+ ---
89
+ ### 4 - A field can be two things at the same time:
90
+
91
+ * m = Select('Movie m' release_date=[Field, OrderBy])
92
+ - This means that the field will appear in the results and also that the query will be ordered by that field.
93
+ * Applying **GROUP BY** to item 3.2, it would look like this:
94
+ ```
95
+ SubSelect(
96
+ 'Review r', movie=[GroupBy, Distinct],
97
+ rate=Having.avg(Where.gt(4.5))
98
+ )
99
+ ```
100
+ ---
101
+ ### 5 - Relationships:
102
+ ```
103
+ query = Select('Actor a', name=Field,
104
+ cast=Select('Cast c', id=PrimaryKey)
105
+ )
106
+ ```
107
+ **>> print(query)**
108
+ ```
109
+ SELECT
110
+ a.name
111
+ FROM
112
+ Actor a
113
+ JOIN Cast c ON (a.cast = c.id)
114
+ ```
115
+
116
+ ---
117
+ ### 6 - The reverse process (parse):
118
+ ```
119
+ text = """
120
+ SELECT
121
+ cas.role,
122
+ m.title,
123
+ m.release_date,
124
+ a.name as actors_name
125
+ FROM
126
+ Actor a
127
+ LEFT JOIN Cast cas ON (a.cast = cas.id)
128
+ LEFT JOIN Movie m ON (cas.movie = m.id)
129
+ WHERE
130
+ (
131
+ m.genre = 'Sci-Fi'
132
+ OR
133
+ m.awards LIKE '%Oscar%'
134
+ )
135
+ AND a.age <= 69 AND a.age >= 45
136
+ ORDER BY
137
+ m.release_date DESC
138
+ """
139
+ ```
140
+
141
+ `a, c, m = Select.parse(text)`
142
+
143
+ **6.1 --- print(a)**
144
+ ```
145
+ SELECT
146
+ a.name as actors_name
147
+ FROM
148
+ Actor a
149
+ WHERE
150
+ a.age <= 69
151
+ AND a.age >= 45
152
+ ```
153
+
154
+ **6.2 --- print(c)**
155
+
156
+ SELECT
157
+ c.role
158
+ FROM
159
+ Cast c
160
+
161
+ **6.3 --- print(m)**
162
+
163
+ SELECT
164
+ m.title,
165
+ m.release_date
166
+ FROM
167
+ Movie m
168
+ WHERE
169
+ ( m.genre = 'Sci-Fi' OR m.awards LIKE '%Oscar%' )
170
+ ORDER BY
171
+ m.release_date DESC
172
+
173
+
174
+
175
+ **6.4 --- print(a+c)**
176
+
177
+ SELECT
178
+ a.name as actors_name,
179
+ cas.role
180
+ FROM
181
+ Actor a
182
+ JOIN Cast cas ON (a.cast = cas.id)
183
+ WHERE
184
+ a.age >= 45
185
+ AND a.age <= 69
186
+
187
+ **6.5 --- print(c+m)**
188
+ > `... or print(m+c)`
189
+
190
+ SELECT
191
+ cas.role,
192
+ m.title,
193
+ m.release_date,
194
+ m.director
195
+ FROM
196
+ Cast cas
197
+ JOIN Movie m ON (cas.movie = m.id)
198
+ WHERE
199
+ ( m.genre = 'Sci-Fi' OR m.awards LIKE '%Oscar%' )
200
+ AND m.director LIKE '%Coppola%'
201
+ ORDER BY
202
+ m.release_date,
203
+ m.director
204
+ ---
205
+
206
+ ### 7 - You can add or delete attributes directly in objects:
207
+ * a(gender=Field)
208
+ * m.delete('director')
209
+
210
+ ---
211
+
212
+ ### 8 - Defining relationship on separate objects:
213
+ ```
214
+ a = Select...
215
+ c = Select...
216
+ m = Select...
217
+ ```
218
+ `a + c => ERROR: "No relationship found between Actor and Cast"`
219
+
220
+ 8.1 - But...
221
+
222
+ a( cast=ForeignKey('Cast') )
223
+ c(id=PrimaryKey)
224
+
225
+ **a + c => Ok!**
226
+
227
+ 8.2
228
+
229
+ c( movie=ForeignKey('Movie') )
230
+ m(id=PrimaryKey)
231
+
232
+ > **c + m => Ok!**
233
+ >> **m + c => Ok!**
234
+
235
+
236
+ ---
237
+
238
+ ### 9 - Comparing objects
239
+
240
+ 9.1
241
+ ```
242
+ a1 = Select.parse('''
243
+ SELECT gender, max(age) FROM Actor act
244
+ WHERE act.age <= 69 AND act.age >= 45
245
+ GROUP BY gender
246
+ ''')[0]
247
+
248
+ a2 = Select('Actor',
249
+ age=Between(45, 69), gender=GroupBy,
250
+ gender=[GroupBy, Field]
251
+ )
252
+ ```
253
+ > **a1 == a2 # --- True!**
254
+
255
+
256
+
257
+ 9.2
258
+ ```
259
+ m1 = Select.parse("""
260
+ SELECT title, release_date FROM Movie m ORDER BY release_date
261
+ WHERE m.genre = 'Sci-Fi' AND m.awards LIKE '%Oscar%'
262
+ """)[0]
263
+
264
+ m2 = Select.parse("""
265
+ SELECT release_date, title
266
+ FROM Movie m
267
+ WHERE m.awards LIKE '%Oscar%' AND m.genre = 'Sci-Fi'
268
+ ORDER BY release_date
269
+ """)[0]
270
+ ```
271
+
272
+ **m1 == m2 # --- True!**
273
+
274
+
275
+ 9.3
276
+ ```
277
+ best_movies = SubSelect(
278
+ Review=Table('role'),
279
+ rate=[GroupBy, Having.avg(Where.gt(4.5))]
280
+ )
281
+ m1 = Select(
282
+ Movie=Table('title,release_date),
283
+ id=best_movies
284
+ )
285
+
286
+ sql = "SELECT rev.role FROM Review rev GROUP BY rev.rate HAVING Avg(rev.rate) > 4.5"
287
+ m2 = Select(
288
+ 'Movie', release_date=Field, title=Field,
289
+ id=Where(f"IN ({sql})")
290
+ )
291
+ ```
292
+ **m1 == m2 # --- True!**
293
+
294
+ ---
295
+ ---
296
+
@@ -0,0 +1,7 @@
1
+ sql_blocks/__init__.py,sha256=TodC5q-UEdYEz9v1RRoogVqqRcsKnZRY1WDGinrI2zo,26
2
+ sql_blocks/sql_blocks.py,sha256=V5WKBZg90emr0-jPpFEyX9rtU4VR3JlQKKu1qhlaiH0,13193
3
+ sql_blocks-0.0.2.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
4
+ sql_blocks-0.0.2.dist-info/METADATA,sha256=stJ7wQ6iRmHNvf4nJBVr_r5W13Z-ey1ZAllXd8kV9bw,6604
5
+ sql_blocks-0.0.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
+ sql_blocks-0.0.2.dist-info/top_level.txt,sha256=57AbUvUjYNy4m1EqDaU3WHeP-uyIAfV0n8GAUp1a1YQ,11
7
+ sql_blocks-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sql_blocks