iceaxe 0.8.3__cp313-cp313-macosx_11_0_arm64.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.
Potentially problematic release.
This version of iceaxe might be problematic. Click here for more details.
- iceaxe/__init__.py +20 -0
- iceaxe/__tests__/__init__.py +0 -0
- iceaxe/__tests__/benchmarks/__init__.py +0 -0
- iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
- iceaxe/__tests__/benchmarks/test_select.py +114 -0
- iceaxe/__tests__/conf_models.py +133 -0
- iceaxe/__tests__/conftest.py +204 -0
- iceaxe/__tests__/docker_helpers.py +208 -0
- iceaxe/__tests__/helpers.py +268 -0
- iceaxe/__tests__/migrations/__init__.py +0 -0
- iceaxe/__tests__/migrations/conftest.py +36 -0
- iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
- iceaxe/__tests__/migrations/test_generator.py +140 -0
- iceaxe/__tests__/migrations/test_generics.py +91 -0
- iceaxe/__tests__/mountaineer/__init__.py +0 -0
- iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
- iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
- iceaxe/__tests__/schemas/__init__.py +0 -0
- iceaxe/__tests__/schemas/test_actions.py +1265 -0
- iceaxe/__tests__/schemas/test_cli.py +25 -0
- iceaxe/__tests__/schemas/test_db_memory_serializer.py +1571 -0
- iceaxe/__tests__/schemas/test_db_serializer.py +435 -0
- iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
- iceaxe/__tests__/test_alias.py +83 -0
- iceaxe/__tests__/test_base.py +52 -0
- iceaxe/__tests__/test_comparison.py +383 -0
- iceaxe/__tests__/test_field.py +11 -0
- iceaxe/__tests__/test_helpers.py +9 -0
- iceaxe/__tests__/test_modifications.py +151 -0
- iceaxe/__tests__/test_queries.py +764 -0
- iceaxe/__tests__/test_queries_str.py +173 -0
- iceaxe/__tests__/test_session.py +1511 -0
- iceaxe/__tests__/test_text_search.py +287 -0
- iceaxe/alias_values.py +67 -0
- iceaxe/base.py +351 -0
- iceaxe/comparison.py +560 -0
- iceaxe/field.py +263 -0
- iceaxe/functions.py +1432 -0
- iceaxe/generics.py +140 -0
- iceaxe/io.py +107 -0
- iceaxe/logging.py +91 -0
- iceaxe/migrations/__init__.py +5 -0
- iceaxe/migrations/action_sorter.py +98 -0
- iceaxe/migrations/cli.py +228 -0
- iceaxe/migrations/client_io.py +62 -0
- iceaxe/migrations/generator.py +404 -0
- iceaxe/migrations/migration.py +86 -0
- iceaxe/migrations/migrator.py +101 -0
- iceaxe/modifications.py +176 -0
- iceaxe/mountaineer/__init__.py +10 -0
- iceaxe/mountaineer/cli.py +74 -0
- iceaxe/mountaineer/config.py +46 -0
- iceaxe/mountaineer/dependencies/__init__.py +6 -0
- iceaxe/mountaineer/dependencies/core.py +67 -0
- iceaxe/postgres.py +133 -0
- iceaxe/py.typed +0 -0
- iceaxe/queries.py +1459 -0
- iceaxe/queries_str.py +294 -0
- iceaxe/schemas/__init__.py +0 -0
- iceaxe/schemas/actions.py +864 -0
- iceaxe/schemas/cli.py +30 -0
- iceaxe/schemas/db_memory_serializer.py +711 -0
- iceaxe/schemas/db_serializer.py +347 -0
- iceaxe/schemas/db_stubs.py +529 -0
- iceaxe/session.py +860 -0
- iceaxe/session_optimized.c +12207 -0
- iceaxe/session_optimized.cpython-313-darwin.so +0 -0
- iceaxe/session_optimized.pyx +212 -0
- iceaxe/sql_types.py +149 -0
- iceaxe/typing.py +73 -0
- iceaxe-0.8.3.dist-info/METADATA +262 -0
- iceaxe-0.8.3.dist-info/RECORD +75 -0
- iceaxe-0.8.3.dist-info/WHEEL +6 -0
- iceaxe-0.8.3.dist-info/licenses/LICENSE +21 -0
- iceaxe-0.8.3.dist-info/top_level.txt +1 -0
iceaxe/queries_str.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from iceaxe.typing import is_base_table, is_column
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class QueryElementBase(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for SQL query elements that require special string processing.
|
|
11
|
+
This class provides the foundation for handling different types of SQL elements
|
|
12
|
+
(like identifiers and literals) with their specific escaping and formatting rules.
|
|
13
|
+
|
|
14
|
+
The class implements equality comparisons, hashing, and sorting based on the processed
|
|
15
|
+
string representation, making it suitable for comparing query elements in test assertions,
|
|
16
|
+
caching, using elements as dictionary keys or in sets, and sorting collections.
|
|
17
|
+
|
|
18
|
+
```python {{sticky: True}}
|
|
19
|
+
# Base class is not used directly, but through its subclasses:
|
|
20
|
+
table_name = QueryIdentifier("users") # -> "users"
|
|
21
|
+
raw_sql = QueryLiteral("COUNT(*)") # -> COUNT(*)
|
|
22
|
+
```
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, value: str):
|
|
26
|
+
"""
|
|
27
|
+
:param value: The raw string value to be processed
|
|
28
|
+
"""
|
|
29
|
+
self._value = self.process_value(value)
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def process_value(self, value: str) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Process the input value according to the specific rules of the query element type.
|
|
35
|
+
Must be implemented by subclasses.
|
|
36
|
+
|
|
37
|
+
:param value: The raw string value to process
|
|
38
|
+
:return: The processed string value
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def __eq__(self, compare):
|
|
43
|
+
return str(self) == str(compare)
|
|
44
|
+
|
|
45
|
+
def __ne__(self, compare):
|
|
46
|
+
return str(self) != str(compare)
|
|
47
|
+
|
|
48
|
+
def __lt__(self, other):
|
|
49
|
+
"""
|
|
50
|
+
Enable sorting of query elements based on their string representation.
|
|
51
|
+
This makes QueryElementBase instances sortable using sorted() or list.sort().
|
|
52
|
+
|
|
53
|
+
:param other: Another QueryElementBase instance to compare with
|
|
54
|
+
:return: True if this element's string representation comes before the other's
|
|
55
|
+
"""
|
|
56
|
+
return str(self) < str(other)
|
|
57
|
+
|
|
58
|
+
def __str__(self):
|
|
59
|
+
return self._value
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f"{self.__class__.__name__}({self._value})"
|
|
63
|
+
|
|
64
|
+
def __hash__(self):
|
|
65
|
+
"""
|
|
66
|
+
Generate a hash based on the string representation of the query element.
|
|
67
|
+
This makes QueryElementBase instances usable as dictionary keys and in sets.
|
|
68
|
+
|
|
69
|
+
:return: Hash value of the processed string
|
|
70
|
+
"""
|
|
71
|
+
return hash(str(self))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class QueryIdentifier(QueryElementBase):
|
|
75
|
+
"""
|
|
76
|
+
Represents a SQL identifier (table name, column name, etc.) that needs to be
|
|
77
|
+
properly quoted to prevent SQL injection and handle special characters.
|
|
78
|
+
|
|
79
|
+
When used, the identifier is automatically wrapped in double quotes, making it
|
|
80
|
+
safe for use in queries even if it contains special characters or SQL keywords.
|
|
81
|
+
|
|
82
|
+
```python {{sticky: True}}
|
|
83
|
+
# In a query builder context:
|
|
84
|
+
table = QueryIdentifier("user_data")
|
|
85
|
+
column = QueryIdentifier("email_address")
|
|
86
|
+
print(f"SELECT {column} FROM {table}")
|
|
87
|
+
# -> SELECT "email_address" FROM "user_data"
|
|
88
|
+
|
|
89
|
+
# Handles special characters and keywords safely:
|
|
90
|
+
reserved = QueryIdentifier("group")
|
|
91
|
+
print(str(reserved)) # -> "group"
|
|
92
|
+
```
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def process_value(self, value: str):
|
|
96
|
+
return f'"{value}"'
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class QueryLiteral(QueryElementBase):
|
|
100
|
+
"""
|
|
101
|
+
Represents a raw SQL literal that should be included in the query exactly as provided,
|
|
102
|
+
without any additional processing or escaping.
|
|
103
|
+
|
|
104
|
+
This class is used for parts of the query that are already properly formatted and
|
|
105
|
+
should not be modified, such as SQL functions, operators, or pre-processed strings.
|
|
106
|
+
|
|
107
|
+
Warning:
|
|
108
|
+
Be careful when using QueryLiteral with user input, as it bypasses SQL escaping.
|
|
109
|
+
It should primarily be used for trusted, programmatically generated SQL components.
|
|
110
|
+
|
|
111
|
+
```python {{sticky: True}}
|
|
112
|
+
# Safe usage with SQL functions:
|
|
113
|
+
count = QueryLiteral("COUNT(*)")
|
|
114
|
+
print(f"SELECT {count} FROM users")
|
|
115
|
+
# -> SELECT COUNT(*) FROM users
|
|
116
|
+
|
|
117
|
+
# Complex SQL expressions:
|
|
118
|
+
case = QueryLiteral("CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END")
|
|
119
|
+
print(f"SELECT name, {case} FROM users")
|
|
120
|
+
# -> SELECT name, CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END FROM users
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def process_value(self, value: str):
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SQLGenerator:
|
|
129
|
+
"""
|
|
130
|
+
The SQLGenerator class provides a convenient way to generate SQL-safe strings for various
|
|
131
|
+
query elements. It acts as a singleton that handles the proper formatting and escaping
|
|
132
|
+
of table names, column names, and other SQL elements.
|
|
133
|
+
|
|
134
|
+
The class provides three main methods:
|
|
135
|
+
- __call__: For generating table-qualified column names
|
|
136
|
+
- select: For generating SELECT clause elements with proper aliases
|
|
137
|
+
- raw: For generating raw identifiers without table qualification
|
|
138
|
+
|
|
139
|
+
Each method handles both column and table inputs, applying appropriate formatting rules
|
|
140
|
+
to prevent SQL injection and ensure proper identifier quoting.
|
|
141
|
+
|
|
142
|
+
```python {{sticky: True}}
|
|
143
|
+
# Basic usage with columns
|
|
144
|
+
sql(User.name) # -> "users"."name"
|
|
145
|
+
sql.select(User.name) # -> "users"."name" AS "users_name"
|
|
146
|
+
sql.raw(User.name) # -> "name"
|
|
147
|
+
|
|
148
|
+
# Basic usage with tables
|
|
149
|
+
sql(User) # -> "users"
|
|
150
|
+
sql.select(User) # -> "users"."id" as "users_id", "users"."name" as "users_name"
|
|
151
|
+
sql.raw(User) # -> "users"
|
|
152
|
+
|
|
153
|
+
# Usage in query building
|
|
154
|
+
query = f"SELECT {sql.select(User.name)} FROM {sql(User)}"
|
|
155
|
+
# -> SELECT "users"."name" AS "users_name" FROM "users"
|
|
156
|
+
```
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __call__(self, obj) -> QueryElementBase:
|
|
160
|
+
"""
|
|
161
|
+
Generate a table-qualified column name or table identifier. This is the default
|
|
162
|
+
format used in most SQL contexts where a column or table reference is needed.
|
|
163
|
+
|
|
164
|
+
For columns, the output includes the table name to prevent ambiguity in JOINs
|
|
165
|
+
and complex queries. For tables, it returns just the quoted table name.
|
|
166
|
+
|
|
167
|
+
:param obj: A column or table object
|
|
168
|
+
:return: A SQL-safe string representation
|
|
169
|
+
|
|
170
|
+
```python {{sticky: True}}
|
|
171
|
+
# Column usage
|
|
172
|
+
sql(User.name)
|
|
173
|
+
# -> "users"."name"
|
|
174
|
+
|
|
175
|
+
sql(Post.title)
|
|
176
|
+
# -> "posts"."title"
|
|
177
|
+
|
|
178
|
+
# Table usage
|
|
179
|
+
sql(User)
|
|
180
|
+
# -> "users"
|
|
181
|
+
|
|
182
|
+
# In a query context
|
|
183
|
+
query = f"UPDATE {sql(User)} SET {sql(User.name)} = 'John'"
|
|
184
|
+
# -> UPDATE "users" SET "users"."name" = 'John'
|
|
185
|
+
|
|
186
|
+
# In a JOIN context
|
|
187
|
+
query = f"SELECT {sql(User.name)} FROM {sql(User)} JOIN {sql(Post)} ON {sql(User.id)} = {sql(Post.user_id)}"
|
|
188
|
+
# -> SELECT "users"."name" FROM "users" JOIN "posts" ON "users"."id" = "posts"."user_id"
|
|
189
|
+
```
|
|
190
|
+
"""
|
|
191
|
+
if is_column(obj):
|
|
192
|
+
table = QueryIdentifier(obj.root_model.get_table_name())
|
|
193
|
+
column = QueryIdentifier(obj.key)
|
|
194
|
+
return QueryLiteral(f"{table}.{column}")
|
|
195
|
+
elif is_base_table(obj):
|
|
196
|
+
return QueryIdentifier(obj.get_table_name())
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError(f"Invalid type for sql: {type(obj)}")
|
|
199
|
+
|
|
200
|
+
def select(self, obj) -> QueryElementBase:
|
|
201
|
+
"""
|
|
202
|
+
Generate a SQL-safe string for selecting fields with proper aliases. This format
|
|
203
|
+
is specifically designed for SELECT clauses where unique column aliases are needed
|
|
204
|
+
to prevent name collisions.
|
|
205
|
+
|
|
206
|
+
For columns, generates a table-qualified column with an alias that includes both
|
|
207
|
+
table and column names. For tables, generates a comma-separated list of all
|
|
208
|
+
columns with their aliases.
|
|
209
|
+
|
|
210
|
+
:param obj: A column or table object
|
|
211
|
+
:return: A SQL-safe string with proper SELECT clause formatting
|
|
212
|
+
|
|
213
|
+
```python {{sticky: True}}
|
|
214
|
+
# Single column selection
|
|
215
|
+
sql.select(User.name)
|
|
216
|
+
# -> "users"."name" AS "users_name"
|
|
217
|
+
|
|
218
|
+
# Full table selection
|
|
219
|
+
sql.select(User)
|
|
220
|
+
# -> "users"."id" as "users_id", "users"."name" as "users_name", "users"."email" as "users_email"
|
|
221
|
+
|
|
222
|
+
# In a complex query with multiple tables
|
|
223
|
+
query = f'''
|
|
224
|
+
SELECT
|
|
225
|
+
{sql.select(User)},
|
|
226
|
+
{sql.select(Post.title)}
|
|
227
|
+
FROM {sql(User)}
|
|
228
|
+
JOIN {sql(Post)} ON {sql(User.id)} = {sql(Post.user_id)}
|
|
229
|
+
'''
|
|
230
|
+
# -> SELECT
|
|
231
|
+
# "users"."id" as "users_id", "users"."name" as "users_name",
|
|
232
|
+
# "posts"."title" AS "posts_title"
|
|
233
|
+
# FROM "users"
|
|
234
|
+
# JOIN "posts" ON "users"."id" = "posts"."user_id"
|
|
235
|
+
```
|
|
236
|
+
"""
|
|
237
|
+
if is_column(obj):
|
|
238
|
+
table = QueryIdentifier(obj.root_model.get_table_name())
|
|
239
|
+
column = QueryIdentifier(obj.key)
|
|
240
|
+
alias = QueryIdentifier(f"{obj.root_model.get_table_name()}_{obj.key}")
|
|
241
|
+
return QueryLiteral(f"{table}.{column} AS {alias}")
|
|
242
|
+
elif is_base_table(obj):
|
|
243
|
+
table_token = QueryIdentifier(obj.get_table_name())
|
|
244
|
+
select_fields: list[str] = []
|
|
245
|
+
for field_name in obj.get_client_fields():
|
|
246
|
+
field_token = QueryIdentifier(field_name)
|
|
247
|
+
return_field = QueryIdentifier(f"{obj.get_table_name()}_{field_name}")
|
|
248
|
+
select_fields.append(f"{table_token}.{field_token} AS {return_field}")
|
|
249
|
+
return QueryLiteral(", ".join(select_fields))
|
|
250
|
+
else:
|
|
251
|
+
raise ValueError(f"Invalid type for select: {type(obj)}")
|
|
252
|
+
|
|
253
|
+
def raw(self, obj) -> QueryElementBase:
|
|
254
|
+
"""
|
|
255
|
+
Generate a raw identifier without table qualification. This is useful in specific
|
|
256
|
+
contexts where you need just the column or table name without any additional
|
|
257
|
+
qualification.
|
|
258
|
+
|
|
259
|
+
For columns, returns just the column name without the table prefix. For tables,
|
|
260
|
+
returns just the table name. All identifiers are still properly quoted.
|
|
261
|
+
|
|
262
|
+
:param obj: A column or table object
|
|
263
|
+
:return: A SQL-safe raw identifier
|
|
264
|
+
|
|
265
|
+
```python {{sticky: True}}
|
|
266
|
+
# Column usage
|
|
267
|
+
sql.raw(User.name)
|
|
268
|
+
# -> "name"
|
|
269
|
+
|
|
270
|
+
sql.raw(Post.title)
|
|
271
|
+
# -> "title"
|
|
272
|
+
|
|
273
|
+
# Table usage
|
|
274
|
+
sql.raw(User)
|
|
275
|
+
# -> "users"
|
|
276
|
+
|
|
277
|
+
# Useful in specific contexts like ORDER BY or GROUP BY
|
|
278
|
+
query = f"SELECT {sql.select(User.name)} FROM {sql(User)} ORDER BY {sql.raw(User.name)}"
|
|
279
|
+
# -> SELECT "users"."name" AS "users_name" FROM "users" ORDER BY "name"
|
|
280
|
+
|
|
281
|
+
# Or in UPDATE SET clauses where table qualification isn't needed
|
|
282
|
+
query = f"UPDATE {sql(User)} SET {sql.raw(User.name)} = 'John'"
|
|
283
|
+
# -> UPDATE "users" SET "name" = 'John'
|
|
284
|
+
```
|
|
285
|
+
"""
|
|
286
|
+
if is_column(obj):
|
|
287
|
+
return QueryIdentifier(obj.key)
|
|
288
|
+
elif is_base_table(obj):
|
|
289
|
+
return QueryIdentifier(obj.get_table_name())
|
|
290
|
+
else:
|
|
291
|
+
raise ValueError(f"Invalid type for raw: {type(obj)}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
sql = SQLGenerator()
|
|
File without changes
|