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/functions.py
ADDED
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Generic, Literal, Type, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from iceaxe.base import (
|
|
8
|
+
DBFieldClassDefinition,
|
|
9
|
+
)
|
|
10
|
+
from iceaxe.comparison import (
|
|
11
|
+
ComparisonBase,
|
|
12
|
+
ComparisonType,
|
|
13
|
+
FieldComparison,
|
|
14
|
+
)
|
|
15
|
+
from iceaxe.queries_str import QueryLiteral
|
|
16
|
+
from iceaxe.sql_types import get_python_to_sql_mapping
|
|
17
|
+
from iceaxe.typing import is_column, is_function_metadata
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
DATE_PART_FIELD = Literal[
|
|
22
|
+
"century",
|
|
23
|
+
"day",
|
|
24
|
+
"decade",
|
|
25
|
+
"dow",
|
|
26
|
+
"doy",
|
|
27
|
+
"epoch",
|
|
28
|
+
"hour",
|
|
29
|
+
"isodow",
|
|
30
|
+
"isoyear",
|
|
31
|
+
"microseconds",
|
|
32
|
+
"millennium",
|
|
33
|
+
"milliseconds",
|
|
34
|
+
"minute",
|
|
35
|
+
"month",
|
|
36
|
+
"quarter",
|
|
37
|
+
"second",
|
|
38
|
+
"timezone",
|
|
39
|
+
"timezone_hour",
|
|
40
|
+
"timezone_minute",
|
|
41
|
+
"week",
|
|
42
|
+
"year",
|
|
43
|
+
]
|
|
44
|
+
DATE_PRECISION = Literal[
|
|
45
|
+
"microseconds",
|
|
46
|
+
"milliseconds",
|
|
47
|
+
"second",
|
|
48
|
+
"minute",
|
|
49
|
+
"hour",
|
|
50
|
+
"day",
|
|
51
|
+
"week",
|
|
52
|
+
"month",
|
|
53
|
+
"quarter",
|
|
54
|
+
"year",
|
|
55
|
+
"decade",
|
|
56
|
+
"century",
|
|
57
|
+
"millennium",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FunctionMetadata(ComparisonBase):
|
|
62
|
+
"""
|
|
63
|
+
Represents metadata for SQL aggregate functions and other SQL function operations.
|
|
64
|
+
This class bridges the gap between Python function calls and their SQL representations,
|
|
65
|
+
maintaining type information and original field references.
|
|
66
|
+
|
|
67
|
+
```python {{sticky: True}}
|
|
68
|
+
# Internal representation of function calls:
|
|
69
|
+
metadata = FunctionMetadata(
|
|
70
|
+
literal=QueryLiteral("count(users.id)"),
|
|
71
|
+
original_field=User.id,
|
|
72
|
+
local_name="user_count"
|
|
73
|
+
)
|
|
74
|
+
# Used in query: SELECT count(users.id) AS user_count
|
|
75
|
+
```
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
literal: QueryLiteral
|
|
79
|
+
"""
|
|
80
|
+
The SQL representation of the function call
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
original_field: DBFieldClassDefinition
|
|
84
|
+
"""
|
|
85
|
+
The database field this function operates on
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
local_name: str | None = None
|
|
89
|
+
"""
|
|
90
|
+
Optional alias for the function result in the query
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
literal: QueryLiteral,
|
|
96
|
+
original_field: DBFieldClassDefinition,
|
|
97
|
+
local_name: str | None = None,
|
|
98
|
+
):
|
|
99
|
+
self.literal = literal
|
|
100
|
+
self.original_field = original_field
|
|
101
|
+
self.local_name = local_name
|
|
102
|
+
|
|
103
|
+
def to_query(self):
|
|
104
|
+
"""
|
|
105
|
+
Converts the function metadata to its SQL representation.
|
|
106
|
+
|
|
107
|
+
:return: A tuple of the SQL literal and an empty list of variables
|
|
108
|
+
"""
|
|
109
|
+
return self.literal, []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TSQueryFunctionMetadata(FunctionMetadata):
|
|
113
|
+
"""
|
|
114
|
+
Represents metadata specifically for tsquery operations in PostgreSQL.
|
|
115
|
+
This class provides methods that are only applicable to tsquery results.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def matches(self, vector: TSVectorFunctionMetadata) -> FieldComparison:
|
|
119
|
+
"""
|
|
120
|
+
Creates a text search match operation (@@) between this tsquery and a tsvector.
|
|
121
|
+
|
|
122
|
+
:param vector: The tsvector to match against
|
|
123
|
+
:return: A field comparison object that resolves to a boolean
|
|
124
|
+
|
|
125
|
+
```python {{sticky: True}}
|
|
126
|
+
# Match a tsvector against this tsquery
|
|
127
|
+
matches = func.to_tsquery('english', 'python').matches(
|
|
128
|
+
func.to_tsvector('english', Article.content)
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
"""
|
|
132
|
+
metadata = FunctionBuilder._column_to_metadata(vector)
|
|
133
|
+
|
|
134
|
+
# Create a new FunctionMetadata for the @@ operation
|
|
135
|
+
match_metadata = FunctionMetadata(
|
|
136
|
+
literal=QueryLiteral(f"{metadata.literal} @@ {self.literal}"),
|
|
137
|
+
original_field=self.original_field,
|
|
138
|
+
)
|
|
139
|
+
# Return a FieldComparison that will be accepted by where()
|
|
140
|
+
return FieldComparison(
|
|
141
|
+
left=match_metadata, comparison=ComparisonType.EQ, right=True
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TSVectorFunctionMetadata(FunctionMetadata):
|
|
146
|
+
"""
|
|
147
|
+
Represents metadata specifically for tsvector operations in PostgreSQL.
|
|
148
|
+
This class provides methods that are only applicable to tsvector results.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def matches(self, query: TSQueryFunctionMetadata) -> FieldComparison:
|
|
152
|
+
"""
|
|
153
|
+
Creates a text search match operation (@@) between this tsvector and a tsquery.
|
|
154
|
+
|
|
155
|
+
:param query: The tsquery to match against
|
|
156
|
+
:return: A field comparison object that resolves to a boolean
|
|
157
|
+
|
|
158
|
+
```python {{sticky: True}}
|
|
159
|
+
# Match this tsvector against a tsquery
|
|
160
|
+
matches = func.to_tsvector('english', Article.content).matches(
|
|
161
|
+
func.to_tsquery('english', 'python')
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
"""
|
|
165
|
+
metadata = FunctionBuilder._column_to_metadata(query)
|
|
166
|
+
|
|
167
|
+
# Create a new FunctionMetadata for the @@ operation
|
|
168
|
+
match_metadata = FunctionMetadata(
|
|
169
|
+
literal=QueryLiteral(f"{self.literal} @@ {metadata.literal}"),
|
|
170
|
+
original_field=self.original_field,
|
|
171
|
+
)
|
|
172
|
+
# Return a FieldComparison that will be accepted by where()
|
|
173
|
+
return FieldComparison(
|
|
174
|
+
left=match_metadata, comparison=ComparisonType.EQ, right=True
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def concat(self, other: TSVectorFunctionMetadata) -> TSVectorFunctionMetadata:
|
|
178
|
+
"""
|
|
179
|
+
Concatenates two tsvectors.
|
|
180
|
+
|
|
181
|
+
:param other: The tsvector to concatenate with
|
|
182
|
+
:return: A TSVectorFunctionMetadata object preserving the input type
|
|
183
|
+
|
|
184
|
+
```python {{sticky: True}}
|
|
185
|
+
# Concatenate two tsvectors
|
|
186
|
+
combined = func.to_tsvector('english', Article.title).concat(
|
|
187
|
+
func.to_tsvector('english', Article.content)
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
"""
|
|
191
|
+
metadata = FunctionBuilder._column_to_metadata(other)
|
|
192
|
+
self.literal = QueryLiteral(f"{self.literal} || {metadata.literal}")
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class BooleanExpression(FieldComparison):
|
|
197
|
+
"""
|
|
198
|
+
A FieldComparison that represents a complete boolean expression.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(self, expression: FunctionMetadata):
|
|
202
|
+
# Initialize with dummy values since we override to_query
|
|
203
|
+
super().__init__(left=expression, comparison=ComparisonType.EQ, right=True)
|
|
204
|
+
self.expression = expression
|
|
205
|
+
|
|
206
|
+
def to_query(self, start: int = 1) -> tuple[QueryLiteral, list[Any]]:
|
|
207
|
+
"""
|
|
208
|
+
Return the expression directly without additional comparison.
|
|
209
|
+
"""
|
|
210
|
+
return self.expression.literal, []
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ArrayComparison(Generic[T], ComparisonBase[bool]):
|
|
214
|
+
"""
|
|
215
|
+
Provides comparison methods for SQL array operations (ANY/ALL).
|
|
216
|
+
This class enables ergonomic syntax for array comparisons.
|
|
217
|
+
|
|
218
|
+
```python {{sticky: True}}
|
|
219
|
+
# Using ArrayComparison for ANY operations:
|
|
220
|
+
func.any(Article.tags) == 'python'
|
|
221
|
+
func.any(User.follower_ids) == current_user_id
|
|
222
|
+
|
|
223
|
+
# Using ArrayComparison for ALL operations:
|
|
224
|
+
func.all(Project.member_statuses) == 'active'
|
|
225
|
+
func.all(Student.test_scores) >= 70
|
|
226
|
+
```
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self, array_metadata: FunctionMetadata, operation: Literal["ANY", "ALL"]
|
|
231
|
+
):
|
|
232
|
+
"""
|
|
233
|
+
Initialize an ArrayComparison object.
|
|
234
|
+
|
|
235
|
+
:param array_metadata: The metadata for the array field
|
|
236
|
+
:param operation: The SQL operation (ANY or ALL)
|
|
237
|
+
"""
|
|
238
|
+
self.array_metadata = array_metadata
|
|
239
|
+
self.operation = operation
|
|
240
|
+
|
|
241
|
+
def __eq__(self, other: T) -> BooleanExpression: # type: ignore
|
|
242
|
+
"""
|
|
243
|
+
Creates an equality comparison with the array elements.
|
|
244
|
+
|
|
245
|
+
:param other: The value to compare with array elements
|
|
246
|
+
:return: A boolean expression object that resolves to a boolean
|
|
247
|
+
"""
|
|
248
|
+
return self._create_comparison(other, "=")
|
|
249
|
+
|
|
250
|
+
def __ne__(self, other: T) -> BooleanExpression: # type: ignore
|
|
251
|
+
"""
|
|
252
|
+
Creates a not-equal comparison with the array elements.
|
|
253
|
+
|
|
254
|
+
:param other: The value to compare with array elements
|
|
255
|
+
:return: A boolean expression object that resolves to a boolean
|
|
256
|
+
"""
|
|
257
|
+
return self._create_comparison(other, "!=")
|
|
258
|
+
|
|
259
|
+
def __lt__(self, other: T) -> BooleanExpression:
|
|
260
|
+
"""
|
|
261
|
+
Creates a less-than comparison with the array elements.
|
|
262
|
+
|
|
263
|
+
:param other: The value to compare with array elements
|
|
264
|
+
:return: A boolean expression object that resolves to a boolean
|
|
265
|
+
"""
|
|
266
|
+
return self._create_comparison(other, "<")
|
|
267
|
+
|
|
268
|
+
def __le__(self, other: T) -> BooleanExpression:
|
|
269
|
+
"""
|
|
270
|
+
Creates a less-than-or-equal comparison with the array elements.
|
|
271
|
+
|
|
272
|
+
:param other: The value to compare with array elements
|
|
273
|
+
:return: A boolean expression object that resolves to a boolean
|
|
274
|
+
"""
|
|
275
|
+
return self._create_comparison(other, "<=")
|
|
276
|
+
|
|
277
|
+
def __gt__(self, other: T) -> BooleanExpression:
|
|
278
|
+
"""
|
|
279
|
+
Creates a greater-than comparison with the array elements.
|
|
280
|
+
|
|
281
|
+
:param other: The value to compare with array elements
|
|
282
|
+
:return: A boolean expression object that resolves to a boolean
|
|
283
|
+
"""
|
|
284
|
+
return self._create_comparison(other, ">")
|
|
285
|
+
|
|
286
|
+
def __ge__(self, other: T) -> BooleanExpression:
|
|
287
|
+
"""
|
|
288
|
+
Creates a greater-than-or-equal comparison with the array elements.
|
|
289
|
+
|
|
290
|
+
:param other: The value to compare with array elements
|
|
291
|
+
:return: A boolean expression object that resolves to a boolean
|
|
292
|
+
"""
|
|
293
|
+
return self._create_comparison(other, ">=")
|
|
294
|
+
|
|
295
|
+
def _create_comparison(self, value: T, operator: str) -> BooleanExpression:
|
|
296
|
+
"""
|
|
297
|
+
Internal method to create the comparison SQL.
|
|
298
|
+
|
|
299
|
+
:param value: The value to compare with array elements
|
|
300
|
+
:param operator: The SQL comparison operator
|
|
301
|
+
:return: A boolean expression object that resolves to a boolean
|
|
302
|
+
"""
|
|
303
|
+
# Handle simple values
|
|
304
|
+
if isinstance(value, (str, int, float, bool)):
|
|
305
|
+
value_literal = f"'{value}'" if isinstance(value, str) else str(value)
|
|
306
|
+
else:
|
|
307
|
+
# For complex values, use the metadata
|
|
308
|
+
value_metadata = FunctionBuilder._column_to_metadata(value)
|
|
309
|
+
value_literal = str(value_metadata.literal)
|
|
310
|
+
|
|
311
|
+
# Create the comparison as a FunctionMetadata
|
|
312
|
+
result = FunctionMetadata(
|
|
313
|
+
literal=QueryLiteral(
|
|
314
|
+
f"{value_literal} {operator} {self.operation}({self.array_metadata.literal})"
|
|
315
|
+
),
|
|
316
|
+
original_field=self.array_metadata.original_field,
|
|
317
|
+
)
|
|
318
|
+
# Return a BooleanExpression that generates the SQL directly
|
|
319
|
+
return BooleanExpression(result)
|
|
320
|
+
|
|
321
|
+
def to_query(self) -> tuple[QueryLiteral, list[Any]]:
|
|
322
|
+
"""
|
|
323
|
+
Converts the array comparison to its SQL representation.
|
|
324
|
+
|
|
325
|
+
:return: A tuple of the SQL query string and list of parameter values
|
|
326
|
+
"""
|
|
327
|
+
return self.array_metadata.literal, []
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class FunctionBuilder:
|
|
331
|
+
"""
|
|
332
|
+
Builder class for SQL aggregate functions and other SQL operations.
|
|
333
|
+
Provides a Pythonic interface for creating SQL function calls with proper type hints.
|
|
334
|
+
|
|
335
|
+
This class is typically accessed through the global `func` instance:
|
|
336
|
+
```python {{sticky: True}}
|
|
337
|
+
from iceaxe import func
|
|
338
|
+
|
|
339
|
+
query = select((
|
|
340
|
+
User.name,
|
|
341
|
+
func.count(User.id),
|
|
342
|
+
func.max(User.age)
|
|
343
|
+
))
|
|
344
|
+
```
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def count(self, field: Any) -> int:
|
|
348
|
+
"""
|
|
349
|
+
Creates a COUNT aggregate function call.
|
|
350
|
+
|
|
351
|
+
:param field: The field to count. Can be a column or another function result
|
|
352
|
+
:return: A function metadata object that resolves to an integer count
|
|
353
|
+
|
|
354
|
+
```python {{sticky: True}}
|
|
355
|
+
# Count all users
|
|
356
|
+
total = await conn.execute(select(func.count(User.id)))
|
|
357
|
+
|
|
358
|
+
# Count distinct values
|
|
359
|
+
unique = await conn.execute(
|
|
360
|
+
select(func.count(func.distinct(User.status)))
|
|
361
|
+
)
|
|
362
|
+
```
|
|
363
|
+
"""
|
|
364
|
+
metadata = self._column_to_metadata(field)
|
|
365
|
+
metadata.literal = QueryLiteral(f"count({metadata.literal})")
|
|
366
|
+
return cast(int, metadata)
|
|
367
|
+
|
|
368
|
+
def distinct(self, field: T) -> T:
|
|
369
|
+
"""
|
|
370
|
+
Creates a DISTINCT function call that removes duplicate values.
|
|
371
|
+
|
|
372
|
+
:param field: The field to get distinct values from
|
|
373
|
+
:return: A function metadata object preserving the input type
|
|
374
|
+
|
|
375
|
+
```python {{sticky: True}}
|
|
376
|
+
# Get distinct status values
|
|
377
|
+
statuses = await conn.execute(select(func.distinct(User.status)))
|
|
378
|
+
|
|
379
|
+
# Count distinct values
|
|
380
|
+
unique_count = await conn.execute(
|
|
381
|
+
select(func.count(func.distinct(User.status)))
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
"""
|
|
385
|
+
metadata = self._column_to_metadata(field)
|
|
386
|
+
metadata.literal = QueryLiteral(f"distinct {metadata.literal}")
|
|
387
|
+
return cast(T, metadata)
|
|
388
|
+
|
|
389
|
+
def sum(self, field: T) -> T:
|
|
390
|
+
"""
|
|
391
|
+
Creates a SUM aggregate function call.
|
|
392
|
+
|
|
393
|
+
:param field: The numeric field to sum
|
|
394
|
+
:return: A function metadata object preserving the input type
|
|
395
|
+
|
|
396
|
+
```python {{sticky: True}}
|
|
397
|
+
# Get total of all salaries
|
|
398
|
+
total = await conn.execute(select(func.sum(Employee.salary)))
|
|
399
|
+
|
|
400
|
+
# Sum with grouping
|
|
401
|
+
by_dept = await conn.execute(
|
|
402
|
+
select((Department.name, func.sum(Employee.salary)))
|
|
403
|
+
.group_by(Department.name)
|
|
404
|
+
)
|
|
405
|
+
```
|
|
406
|
+
"""
|
|
407
|
+
metadata = self._column_to_metadata(field)
|
|
408
|
+
metadata.literal = QueryLiteral(f"sum({metadata.literal})")
|
|
409
|
+
return cast(T, metadata)
|
|
410
|
+
|
|
411
|
+
def avg(self, field: T) -> T:
|
|
412
|
+
"""
|
|
413
|
+
Creates an AVG aggregate function call.
|
|
414
|
+
|
|
415
|
+
:param field: The numeric field to average
|
|
416
|
+
:return: A function metadata object preserving the input type
|
|
417
|
+
|
|
418
|
+
```python {{sticky: True}}
|
|
419
|
+
# Get average age of all users
|
|
420
|
+
avg_age = await conn.execute(select(func.avg(User.age)))
|
|
421
|
+
|
|
422
|
+
# Average with grouping
|
|
423
|
+
by_dept = await conn.execute(
|
|
424
|
+
select((Department.name, func.avg(Employee.salary)))
|
|
425
|
+
.group_by(Department.name)
|
|
426
|
+
)
|
|
427
|
+
```
|
|
428
|
+
"""
|
|
429
|
+
metadata = self._column_to_metadata(field)
|
|
430
|
+
metadata.literal = QueryLiteral(f"avg({metadata.literal})")
|
|
431
|
+
return cast(T, metadata)
|
|
432
|
+
|
|
433
|
+
def max(self, field: T) -> T:
|
|
434
|
+
"""
|
|
435
|
+
Creates a MAX aggregate function call.
|
|
436
|
+
|
|
437
|
+
:param field: The field to get the maximum value from
|
|
438
|
+
:return: A function metadata object preserving the input type
|
|
439
|
+
|
|
440
|
+
```python {{sticky: True}}
|
|
441
|
+
# Get highest salary
|
|
442
|
+
highest = await conn.execute(select(func.max(Employee.salary)))
|
|
443
|
+
|
|
444
|
+
# Max with grouping
|
|
445
|
+
by_dept = await conn.execute(
|
|
446
|
+
select((Department.name, func.max(Employee.salary)))
|
|
447
|
+
.group_by(Department.name)
|
|
448
|
+
)
|
|
449
|
+
```
|
|
450
|
+
"""
|
|
451
|
+
metadata = self._column_to_metadata(field)
|
|
452
|
+
metadata.literal = QueryLiteral(f"max({metadata.literal})")
|
|
453
|
+
return cast(T, metadata)
|
|
454
|
+
|
|
455
|
+
def min(self, field: T) -> T:
|
|
456
|
+
"""
|
|
457
|
+
Creates a MIN aggregate function call.
|
|
458
|
+
|
|
459
|
+
:param field: The field to get the minimum value from
|
|
460
|
+
:return: A function metadata object preserving the input type
|
|
461
|
+
|
|
462
|
+
```python {{sticky: True}}
|
|
463
|
+
# Get lowest salary
|
|
464
|
+
lowest = await conn.execute(select(func.min(Employee.salary)))
|
|
465
|
+
|
|
466
|
+
# Min with grouping
|
|
467
|
+
by_dept = await conn.execute(
|
|
468
|
+
select((Department.name, func.min(Employee.salary)))
|
|
469
|
+
.group_by(Department.name)
|
|
470
|
+
)
|
|
471
|
+
```
|
|
472
|
+
"""
|
|
473
|
+
metadata = self._column_to_metadata(field)
|
|
474
|
+
metadata.literal = QueryLiteral(f"min({metadata.literal})")
|
|
475
|
+
return cast(T, metadata)
|
|
476
|
+
|
|
477
|
+
def abs(self, field: T) -> T:
|
|
478
|
+
"""
|
|
479
|
+
Creates an ABS function call to get the absolute value.
|
|
480
|
+
|
|
481
|
+
:param field: The numeric field to get the absolute value of
|
|
482
|
+
:return: A function metadata object preserving the input type
|
|
483
|
+
|
|
484
|
+
```python {{sticky: True}}
|
|
485
|
+
# Get absolute value of balance
|
|
486
|
+
abs_balance = await conn.execute(select(func.abs(Account.balance)))
|
|
487
|
+
```
|
|
488
|
+
"""
|
|
489
|
+
metadata = self._column_to_metadata(field)
|
|
490
|
+
metadata.literal = QueryLiteral(f"abs({metadata.literal})")
|
|
491
|
+
return cast(T, metadata)
|
|
492
|
+
|
|
493
|
+
def date_trunc(self, precision: DATE_PRECISION, field: T) -> T:
|
|
494
|
+
"""
|
|
495
|
+
Truncates a timestamp or interval value to specified precision.
|
|
496
|
+
|
|
497
|
+
:param precision: The precision to truncate to ('microseconds', 'milliseconds', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade', 'century', 'millennium')
|
|
498
|
+
:param field: The timestamp or interval field to truncate
|
|
499
|
+
:return: A function metadata object preserving the input type
|
|
500
|
+
|
|
501
|
+
```python {{sticky: True}}
|
|
502
|
+
# Truncate timestamp to month
|
|
503
|
+
monthly = await conn.execute(select(func.date_trunc('month', User.created_at)))
|
|
504
|
+
```
|
|
505
|
+
"""
|
|
506
|
+
metadata = self._column_to_metadata(field)
|
|
507
|
+
metadata.literal = QueryLiteral(
|
|
508
|
+
f"date_trunc('{precision}', {metadata.literal})"
|
|
509
|
+
)
|
|
510
|
+
return cast(T, metadata)
|
|
511
|
+
|
|
512
|
+
def date_part(self, field: DATE_PART_FIELD, source: Any) -> float:
|
|
513
|
+
"""
|
|
514
|
+
Extracts a subfield from a date/time value.
|
|
515
|
+
|
|
516
|
+
:param field: The subfield to extract ('century', 'day', 'decade', 'dow', 'doy', 'epoch', 'hour', 'isodow', 'isoyear', 'microseconds', 'millennium', 'milliseconds', 'minute', 'month', 'quarter', 'second', 'timezone', 'timezone_hour', 'timezone_minute', 'week', 'year')
|
|
517
|
+
:param source: The date/time field to extract from
|
|
518
|
+
:return: A function metadata object that resolves to an integer
|
|
519
|
+
|
|
520
|
+
```python {{sticky: True}}
|
|
521
|
+
# Get month from timestamp
|
|
522
|
+
month = await conn.execute(select(func.date_part('month', User.created_at)))
|
|
523
|
+
```
|
|
524
|
+
"""
|
|
525
|
+
metadata = self._column_to_metadata(source)
|
|
526
|
+
metadata.literal = QueryLiteral(f"date_part('{field}', {metadata.literal})")
|
|
527
|
+
return cast(float, metadata)
|
|
528
|
+
|
|
529
|
+
def extract(self, field: DATE_PART_FIELD, source: Any) -> int:
|
|
530
|
+
"""
|
|
531
|
+
Extracts a subfield from a date/time value using SQL standard syntax.
|
|
532
|
+
|
|
533
|
+
:param field: The subfield to extract ('century', 'day', 'decade', 'dow', 'doy', 'epoch', 'hour', 'isodow', 'isoyear', 'microseconds', 'millennium', 'milliseconds', 'minute', 'month', 'quarter', 'second', 'timezone', 'timezone_hour', 'timezone_minute', 'week', 'year')
|
|
534
|
+
:param source: The date/time field to extract from
|
|
535
|
+
:return: A function metadata object that resolves to an integer
|
|
536
|
+
|
|
537
|
+
```python {{sticky: True}}
|
|
538
|
+
# Get year from timestamp
|
|
539
|
+
year = await conn.execute(select(func.extract('year', User.created_at)))
|
|
540
|
+
```
|
|
541
|
+
"""
|
|
542
|
+
metadata = self._column_to_metadata(source)
|
|
543
|
+
metadata.literal = QueryLiteral(f"extract({field} from {metadata.literal})")
|
|
544
|
+
return cast(int, metadata)
|
|
545
|
+
|
|
546
|
+
def age(self, timestamp: T, reference: T | None = None) -> T:
|
|
547
|
+
"""
|
|
548
|
+
Calculates the difference between two timestamps.
|
|
549
|
+
If reference is not provided, current_date is used.
|
|
550
|
+
|
|
551
|
+
:param timestamp: The timestamp to calculate age from
|
|
552
|
+
:param reference: Optional reference timestamp (defaults to current_date)
|
|
553
|
+
:return: A function metadata object preserving the input type
|
|
554
|
+
|
|
555
|
+
```python {{sticky: True}}
|
|
556
|
+
# Get age of a timestamp
|
|
557
|
+
age = await conn.execute(select(func.age(User.birth_date)))
|
|
558
|
+
|
|
559
|
+
# Get age between two timestamps
|
|
560
|
+
age_diff = await conn.execute(select(func.age(Event.end_time, Event.start_time)))
|
|
561
|
+
```
|
|
562
|
+
"""
|
|
563
|
+
metadata = self._column_to_metadata(timestamp)
|
|
564
|
+
if reference is not None:
|
|
565
|
+
ref_metadata = self._column_to_metadata(reference)
|
|
566
|
+
metadata.literal = QueryLiteral(
|
|
567
|
+
f"age({metadata.literal}, {ref_metadata.literal})"
|
|
568
|
+
)
|
|
569
|
+
else:
|
|
570
|
+
metadata.literal = QueryLiteral(f"age({metadata.literal})")
|
|
571
|
+
return cast(T, metadata)
|
|
572
|
+
|
|
573
|
+
def date(self, field: T) -> T:
|
|
574
|
+
"""
|
|
575
|
+
Converts a timestamp to a date by dropping the time component.
|
|
576
|
+
|
|
577
|
+
:param field: The timestamp field to convert
|
|
578
|
+
:return: A function metadata object that resolves to a date
|
|
579
|
+
|
|
580
|
+
```python {{sticky: True}}
|
|
581
|
+
# Get just the date part
|
|
582
|
+
event_date = await conn.execute(select(func.date(Event.timestamp)))
|
|
583
|
+
```
|
|
584
|
+
"""
|
|
585
|
+
metadata = self._column_to_metadata(field)
|
|
586
|
+
metadata.literal = QueryLiteral(f"date({metadata.literal})")
|
|
587
|
+
return cast(T, metadata)
|
|
588
|
+
|
|
589
|
+
# String Functions
|
|
590
|
+
def lower(self, field: T) -> T:
|
|
591
|
+
"""
|
|
592
|
+
Converts string to lowercase.
|
|
593
|
+
|
|
594
|
+
:param field: The string field to convert
|
|
595
|
+
:return: A function metadata object preserving the input type
|
|
596
|
+
"""
|
|
597
|
+
metadata = self._column_to_metadata(field)
|
|
598
|
+
metadata.literal = QueryLiteral(f"lower({metadata.literal})")
|
|
599
|
+
return cast(T, metadata)
|
|
600
|
+
|
|
601
|
+
def upper(self, field: T) -> T:
|
|
602
|
+
"""
|
|
603
|
+
Converts string to uppercase.
|
|
604
|
+
|
|
605
|
+
:param field: The string field to convert
|
|
606
|
+
:return: A function metadata object preserving the input type
|
|
607
|
+
"""
|
|
608
|
+
metadata = self._column_to_metadata(field)
|
|
609
|
+
metadata.literal = QueryLiteral(f"upper({metadata.literal})")
|
|
610
|
+
return cast(T, metadata)
|
|
611
|
+
|
|
612
|
+
def length(self, field: Any) -> int:
|
|
613
|
+
"""
|
|
614
|
+
Returns length of string.
|
|
615
|
+
|
|
616
|
+
:param field: The string field to measure
|
|
617
|
+
:return: A function metadata object that resolves to an integer
|
|
618
|
+
"""
|
|
619
|
+
metadata = self._column_to_metadata(field)
|
|
620
|
+
metadata.literal = QueryLiteral(f"length({metadata.literal})")
|
|
621
|
+
return cast(int, metadata)
|
|
622
|
+
|
|
623
|
+
def trim(self, field: T) -> T:
|
|
624
|
+
"""
|
|
625
|
+
Removes whitespace from both ends of string.
|
|
626
|
+
|
|
627
|
+
:param field: The string field to trim
|
|
628
|
+
:return: A function metadata object preserving the input type
|
|
629
|
+
"""
|
|
630
|
+
metadata = self._column_to_metadata(field)
|
|
631
|
+
metadata.literal = QueryLiteral(f"trim({metadata.literal})")
|
|
632
|
+
return cast(T, metadata)
|
|
633
|
+
|
|
634
|
+
def substring(self, field: T, start: int, length: int) -> T:
|
|
635
|
+
"""
|
|
636
|
+
Extracts substring.
|
|
637
|
+
|
|
638
|
+
:param field: The string field to extract from
|
|
639
|
+
:param start: Starting position (1-based)
|
|
640
|
+
:param length: Number of characters to extract
|
|
641
|
+
:return: A function metadata object preserving the input type
|
|
642
|
+
"""
|
|
643
|
+
metadata = self._column_to_metadata(field)
|
|
644
|
+
metadata.literal = QueryLiteral(
|
|
645
|
+
f"substring({metadata.literal} from {start} for {length})"
|
|
646
|
+
)
|
|
647
|
+
return cast(T, metadata)
|
|
648
|
+
|
|
649
|
+
# Mathematical Functions
|
|
650
|
+
def round(self, field: T) -> T:
|
|
651
|
+
"""
|
|
652
|
+
Rounds to nearest integer.
|
|
653
|
+
|
|
654
|
+
:param field: The numeric field to round
|
|
655
|
+
:return: A function metadata object preserving the input type
|
|
656
|
+
"""
|
|
657
|
+
metadata = self._column_to_metadata(field)
|
|
658
|
+
metadata.literal = QueryLiteral(f"round({metadata.literal})")
|
|
659
|
+
return cast(T, metadata)
|
|
660
|
+
|
|
661
|
+
def ceil(self, field: T) -> T:
|
|
662
|
+
"""
|
|
663
|
+
Rounds up to nearest integer.
|
|
664
|
+
|
|
665
|
+
:param field: The numeric field to round up
|
|
666
|
+
:return: A function metadata object preserving the input type
|
|
667
|
+
"""
|
|
668
|
+
metadata = self._column_to_metadata(field)
|
|
669
|
+
metadata.literal = QueryLiteral(f"ceil({metadata.literal})")
|
|
670
|
+
return cast(T, metadata)
|
|
671
|
+
|
|
672
|
+
def floor(self, field: T) -> T:
|
|
673
|
+
"""
|
|
674
|
+
Rounds down to nearest integer.
|
|
675
|
+
|
|
676
|
+
:param field: The numeric field to round down
|
|
677
|
+
:return: A function metadata object preserving the input type
|
|
678
|
+
"""
|
|
679
|
+
metadata = self._column_to_metadata(field)
|
|
680
|
+
metadata.literal = QueryLiteral(f"floor({metadata.literal})")
|
|
681
|
+
return cast(T, metadata)
|
|
682
|
+
|
|
683
|
+
def power(self, field: T, exponent: int | float) -> T:
|
|
684
|
+
"""
|
|
685
|
+
Raises a number to the specified power.
|
|
686
|
+
|
|
687
|
+
:param field: The numeric field to raise
|
|
688
|
+
:param exponent: The power to raise to
|
|
689
|
+
:return: A function metadata object preserving the input type
|
|
690
|
+
"""
|
|
691
|
+
metadata = self._column_to_metadata(field)
|
|
692
|
+
metadata.literal = QueryLiteral(f"power({metadata.literal}, {exponent})")
|
|
693
|
+
return cast(T, metadata)
|
|
694
|
+
|
|
695
|
+
def sqrt(self, field: T) -> T:
|
|
696
|
+
"""
|
|
697
|
+
Calculates square root.
|
|
698
|
+
|
|
699
|
+
:param field: The numeric field to calculate square root of
|
|
700
|
+
:return: A function metadata object preserving the input type
|
|
701
|
+
"""
|
|
702
|
+
metadata = self._column_to_metadata(field)
|
|
703
|
+
metadata.literal = QueryLiteral(f"sqrt({metadata.literal})")
|
|
704
|
+
return cast(T, metadata)
|
|
705
|
+
|
|
706
|
+
# Aggregate Functions
|
|
707
|
+
def array_agg(self, field: T) -> list[T]:
|
|
708
|
+
"""
|
|
709
|
+
Collects values into an array.
|
|
710
|
+
|
|
711
|
+
:param field: The field to aggregate
|
|
712
|
+
:return: A function metadata object that resolves to a list
|
|
713
|
+
"""
|
|
714
|
+
metadata = self._column_to_metadata(field)
|
|
715
|
+
metadata.literal = QueryLiteral(f"array_agg({metadata.literal})")
|
|
716
|
+
return cast(list[T], metadata)
|
|
717
|
+
|
|
718
|
+
def string_agg(self, field: Any, delimiter: str) -> str:
|
|
719
|
+
"""
|
|
720
|
+
Concatenates values with delimiter.
|
|
721
|
+
|
|
722
|
+
:param field: The field to aggregate
|
|
723
|
+
:param delimiter: The delimiter to use between values
|
|
724
|
+
:return: A function metadata object that resolves to a string
|
|
725
|
+
"""
|
|
726
|
+
metadata = self._column_to_metadata(field)
|
|
727
|
+
metadata.literal = QueryLiteral(
|
|
728
|
+
f"string_agg({metadata.literal}, '{delimiter}')"
|
|
729
|
+
)
|
|
730
|
+
return cast(str, metadata)
|
|
731
|
+
|
|
732
|
+
def unnest(self, field: list[T]) -> T:
|
|
733
|
+
"""
|
|
734
|
+
Expands an array into a set of rows.
|
|
735
|
+
|
|
736
|
+
:param field: The array field to unnest
|
|
737
|
+
:return: A function metadata object that resolves to the element type
|
|
738
|
+
|
|
739
|
+
```python {{sticky: True}}
|
|
740
|
+
# Unnest an array column
|
|
741
|
+
tags = await conn.execute(select(func.unnest(Article.tags)))
|
|
742
|
+
|
|
743
|
+
# Use with joins
|
|
744
|
+
result = await conn.execute(
|
|
745
|
+
select((User.name, func.unnest(User.favorite_colors)))
|
|
746
|
+
.where(User.id == user_id)
|
|
747
|
+
)
|
|
748
|
+
```
|
|
749
|
+
"""
|
|
750
|
+
metadata = self._column_to_metadata(field)
|
|
751
|
+
metadata.literal = QueryLiteral(f"unnest({metadata.literal})")
|
|
752
|
+
return cast(T, metadata)
|
|
753
|
+
|
|
754
|
+
# Array Operators and Functions
|
|
755
|
+
def any(self, array_field: list[T]) -> ArrayComparison[T]:
|
|
756
|
+
"""
|
|
757
|
+
Creates an ANY array comparison that can be used with comparison operators.
|
|
758
|
+
|
|
759
|
+
:param array_field: The array field to check against
|
|
760
|
+
:return: An ArrayComparison object that supports comparison operators
|
|
761
|
+
|
|
762
|
+
```python {{sticky: True}}
|
|
763
|
+
# Check if 'python' is in the tags array
|
|
764
|
+
has_python = await conn.execute(
|
|
765
|
+
select(Article)
|
|
766
|
+
.where(func.any(Article.tags) == 'python')
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Check if user id is in the follower_ids array
|
|
770
|
+
is_follower = await conn.execute(
|
|
771
|
+
select(User)
|
|
772
|
+
.where(func.any(User.follower_ids) == current_user_id)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Check if any score is above threshold
|
|
776
|
+
has_passing = await conn.execute(
|
|
777
|
+
select(Student)
|
|
778
|
+
.where(func.any(Student.test_scores) >= 70)
|
|
779
|
+
)
|
|
780
|
+
```
|
|
781
|
+
"""
|
|
782
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
783
|
+
return ArrayComparison(array_metadata, "ANY")
|
|
784
|
+
|
|
785
|
+
def all(self, array_field: list[T]) -> ArrayComparison[T]:
|
|
786
|
+
"""
|
|
787
|
+
Creates an ALL array comparison that can be used with comparison operators.
|
|
788
|
+
|
|
789
|
+
:param array_field: The array field to check against
|
|
790
|
+
:return: An ArrayComparison object that supports comparison operators
|
|
791
|
+
|
|
792
|
+
```python {{sticky: True}}
|
|
793
|
+
# Check if all elements in status array are 'active'
|
|
794
|
+
all_active = await conn.execute(
|
|
795
|
+
select(Project)
|
|
796
|
+
.where(func.all(Project.member_statuses) == 'active')
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
# Check if all scores are above threshold
|
|
800
|
+
all_passing = await conn.execute(
|
|
801
|
+
select(Student)
|
|
802
|
+
.where(func.all(Student.test_scores) >= 70)
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# Check if all values are non-null
|
|
806
|
+
all_present = await conn.execute(
|
|
807
|
+
select(Survey)
|
|
808
|
+
.where(func.all(Survey.responses) != None)
|
|
809
|
+
)
|
|
810
|
+
```
|
|
811
|
+
"""
|
|
812
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
813
|
+
return ArrayComparison(array_metadata, "ALL")
|
|
814
|
+
|
|
815
|
+
def array_contains(
|
|
816
|
+
self, array_field: list[T], contained: list[T]
|
|
817
|
+
) -> FunctionMetadata:
|
|
818
|
+
"""
|
|
819
|
+
Creates an array contains comparison using the @> operator.
|
|
820
|
+
Checks if the array field contains all elements of the contained array.
|
|
821
|
+
|
|
822
|
+
:param array_field: The array field to check
|
|
823
|
+
:param contained: The array that should be contained
|
|
824
|
+
:return: A function metadata object that resolves to a boolean
|
|
825
|
+
|
|
826
|
+
```python {{sticky: True}}
|
|
827
|
+
# Check if tags contain both 'python' and 'django'
|
|
828
|
+
has_both = await conn.execute(
|
|
829
|
+
select(Article)
|
|
830
|
+
.where(func.array_contains(Article.tags, ['python', 'django']) == True)
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# Check if permissions contain required permissions
|
|
834
|
+
has_perms = await conn.execute(
|
|
835
|
+
select(User)
|
|
836
|
+
.where(func.array_contains(User.permissions, ['read', 'write']) == True)
|
|
837
|
+
)
|
|
838
|
+
```
|
|
839
|
+
"""
|
|
840
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
841
|
+
|
|
842
|
+
# Convert the contained list to PostgreSQL array syntax
|
|
843
|
+
if all(isinstance(x, str) for x in contained):
|
|
844
|
+
contained_literal = "ARRAY[" + ",".join(f"'{x}'" for x in contained) + "]"
|
|
845
|
+
else:
|
|
846
|
+
contained_literal = "ARRAY[" + ",".join(str(x) for x in contained) + "]"
|
|
847
|
+
|
|
848
|
+
# Create the @> comparison as a FunctionMetadata
|
|
849
|
+
array_metadata.literal = QueryLiteral(
|
|
850
|
+
f"{array_metadata.literal} @> {contained_literal}"
|
|
851
|
+
)
|
|
852
|
+
return array_metadata
|
|
853
|
+
|
|
854
|
+
def array_contained_by(
|
|
855
|
+
self, array_field: list[T], container: list[T]
|
|
856
|
+
) -> FunctionMetadata:
|
|
857
|
+
"""
|
|
858
|
+
Creates an array contained by comparison using the <@ operator.
|
|
859
|
+
Checks if all elements of the array field are contained in the container array.
|
|
860
|
+
|
|
861
|
+
:param array_field: The array field to check
|
|
862
|
+
:param container: The array that should contain all elements
|
|
863
|
+
:return: A function metadata object that resolves to a boolean
|
|
864
|
+
|
|
865
|
+
```python {{sticky: True}}
|
|
866
|
+
# Check if user's skills are all from allowed skills
|
|
867
|
+
valid_skills = await conn.execute(
|
|
868
|
+
select(User)
|
|
869
|
+
.where(func.array_contained_by(User.skills, ['python', 'java', 'go', 'rust']) == True)
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Check if selected options are all from valid options
|
|
873
|
+
valid_selection = await conn.execute(
|
|
874
|
+
select(Survey)
|
|
875
|
+
.where(func.array_contained_by(Survey.selected, [1, 2, 3, 4, 5]) == True)
|
|
876
|
+
)
|
|
877
|
+
```
|
|
878
|
+
"""
|
|
879
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
880
|
+
|
|
881
|
+
# Convert the container list to PostgreSQL array syntax
|
|
882
|
+
if all(isinstance(x, str) for x in container):
|
|
883
|
+
container_literal = "ARRAY[" + ",".join(f"'{x}'" for x in container) + "]"
|
|
884
|
+
else:
|
|
885
|
+
container_literal = "ARRAY[" + ",".join(str(x) for x in container) + "]"
|
|
886
|
+
|
|
887
|
+
# Create the <@ comparison as a FunctionMetadata
|
|
888
|
+
array_metadata.literal = QueryLiteral(
|
|
889
|
+
f"{array_metadata.literal} <@ {container_literal}"
|
|
890
|
+
)
|
|
891
|
+
return array_metadata
|
|
892
|
+
|
|
893
|
+
def array_overlaps(self, array_field: list[T], other: list[T]) -> FunctionMetadata:
|
|
894
|
+
"""
|
|
895
|
+
Creates an array overlap comparison using the && operator.
|
|
896
|
+
Checks if the arrays have any elements in common.
|
|
897
|
+
|
|
898
|
+
:param array_field: The first array field
|
|
899
|
+
:param other: The second array to check for overlap
|
|
900
|
+
:return: A function metadata object that resolves to a boolean
|
|
901
|
+
|
|
902
|
+
```python {{sticky: True}}
|
|
903
|
+
# Check if article tags overlap with user interests
|
|
904
|
+
matching_interests = await conn.execute(
|
|
905
|
+
select(Article)
|
|
906
|
+
.where(func.array_overlaps(Article.tags, ['python', 'data-science', 'ml']) == True)
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Check if available times overlap
|
|
910
|
+
has_common_time = await conn.execute(
|
|
911
|
+
select(Meeting)
|
|
912
|
+
.where(func.array_overlaps(Meeting.available_slots, [1, 2, 3]) == True)
|
|
913
|
+
)
|
|
914
|
+
```
|
|
915
|
+
"""
|
|
916
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
917
|
+
|
|
918
|
+
# Convert the other list to PostgreSQL array syntax
|
|
919
|
+
if all(isinstance(x, str) for x in other):
|
|
920
|
+
other_literal = "ARRAY[" + ",".join(f"'{x}'" for x in other) + "]"
|
|
921
|
+
else:
|
|
922
|
+
other_literal = "ARRAY[" + ",".join(str(x) for x in other) + "]"
|
|
923
|
+
|
|
924
|
+
# Create the && comparison as a FunctionMetadata
|
|
925
|
+
array_metadata.literal = QueryLiteral(
|
|
926
|
+
f"{array_metadata.literal} && {other_literal}"
|
|
927
|
+
)
|
|
928
|
+
return array_metadata
|
|
929
|
+
|
|
930
|
+
def array_append(self, array_field: list[T], element: T) -> list[T]:
|
|
931
|
+
"""
|
|
932
|
+
Appends an element to the end of an array.
|
|
933
|
+
|
|
934
|
+
:param array_field: The array field to append to
|
|
935
|
+
:param element: The element to append
|
|
936
|
+
:return: A function metadata object that resolves to an array
|
|
937
|
+
|
|
938
|
+
```python {{sticky: True}}
|
|
939
|
+
# Append a tag to the array
|
|
940
|
+
updated = await conn.execute(
|
|
941
|
+
update(Article)
|
|
942
|
+
.set(tags=func.array_append(Article.tags, 'new-tag'))
|
|
943
|
+
.where(Article.id == article_id)
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
# Select with appended element
|
|
947
|
+
with_extra = await conn.execute(
|
|
948
|
+
select(func.array_append(User.skills, 'python'))
|
|
949
|
+
)
|
|
950
|
+
```
|
|
951
|
+
"""
|
|
952
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
953
|
+
|
|
954
|
+
# Handle element literal
|
|
955
|
+
if isinstance(element, str):
|
|
956
|
+
element_literal = f"'{element}'"
|
|
957
|
+
elif isinstance(element, (int, float, bool)):
|
|
958
|
+
element_literal = str(element)
|
|
959
|
+
else:
|
|
960
|
+
element_metadata = self._column_to_metadata(element)
|
|
961
|
+
element_literal = str(element_metadata.literal)
|
|
962
|
+
|
|
963
|
+
array_metadata.literal = QueryLiteral(
|
|
964
|
+
f"array_append({array_metadata.literal}, {element_literal})"
|
|
965
|
+
)
|
|
966
|
+
return cast(list[T], array_metadata)
|
|
967
|
+
|
|
968
|
+
def array_prepend(self, element: T, array_field: list[T]) -> list[T]:
|
|
969
|
+
"""
|
|
970
|
+
Prepends an element to the beginning of an array.
|
|
971
|
+
|
|
972
|
+
:param element: The element to prepend
|
|
973
|
+
:param array_field: The array field to prepend to
|
|
974
|
+
:return: A function metadata object that resolves to an array
|
|
975
|
+
|
|
976
|
+
```python {{sticky: True}}
|
|
977
|
+
# Prepend a tag to the array
|
|
978
|
+
updated = await conn.execute(
|
|
979
|
+
update(Article)
|
|
980
|
+
.set(tags=func.array_prepend('featured', Article.tags))
|
|
981
|
+
.where(Article.id == article_id)
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
# Select with prepended element
|
|
985
|
+
with_prefix = await conn.execute(
|
|
986
|
+
select(func.array_prepend('beginner', User.skill_levels))
|
|
987
|
+
)
|
|
988
|
+
```
|
|
989
|
+
"""
|
|
990
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
991
|
+
|
|
992
|
+
# Handle element literal
|
|
993
|
+
if isinstance(element, str):
|
|
994
|
+
element_literal = f"'{element}'"
|
|
995
|
+
elif isinstance(element, (int, float, bool)):
|
|
996
|
+
element_literal = str(element)
|
|
997
|
+
else:
|
|
998
|
+
element_metadata = self._column_to_metadata(element)
|
|
999
|
+
element_literal = str(element_metadata.literal)
|
|
1000
|
+
|
|
1001
|
+
array_metadata.literal = QueryLiteral(
|
|
1002
|
+
f"array_prepend({element_literal}, {array_metadata.literal})"
|
|
1003
|
+
)
|
|
1004
|
+
return cast(list[T], array_metadata)
|
|
1005
|
+
|
|
1006
|
+
def array_cat(self, array1: list[T], array2: list[T]) -> list[T]:
|
|
1007
|
+
"""
|
|
1008
|
+
Concatenates two arrays.
|
|
1009
|
+
|
|
1010
|
+
:param array1: The first array
|
|
1011
|
+
:param array2: The second array
|
|
1012
|
+
:return: A function metadata object that resolves to an array
|
|
1013
|
+
|
|
1014
|
+
```python {{sticky: True}}
|
|
1015
|
+
# Concatenate two arrays
|
|
1016
|
+
combined_tags = await conn.execute(
|
|
1017
|
+
select(func.array_cat(Article.tags, Article.categories))
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Merge user permissions
|
|
1021
|
+
all_perms = await conn.execute(
|
|
1022
|
+
update(User)
|
|
1023
|
+
.set(permissions=func.array_cat(User.permissions, ['admin', 'superuser']))
|
|
1024
|
+
.where(User.id == user_id)
|
|
1025
|
+
)
|
|
1026
|
+
```
|
|
1027
|
+
"""
|
|
1028
|
+
array1_metadata = self._column_to_metadata(array1)
|
|
1029
|
+
|
|
1030
|
+
# Handle array2 - could be a field or a literal list
|
|
1031
|
+
if isinstance(array2, list):
|
|
1032
|
+
# Convert literal list to PostgreSQL array
|
|
1033
|
+
if all(isinstance(x, str) for x in array2):
|
|
1034
|
+
array2_literal = "ARRAY[" + ",".join(f"'{x}'" for x in array2) + "]"
|
|
1035
|
+
else:
|
|
1036
|
+
array2_literal = "ARRAY[" + ",".join(str(x) for x in array2) + "]"
|
|
1037
|
+
else:
|
|
1038
|
+
array2_metadata = self._column_to_metadata(array2)
|
|
1039
|
+
array2_literal = str(array2_metadata.literal)
|
|
1040
|
+
|
|
1041
|
+
array1_metadata.literal = QueryLiteral(
|
|
1042
|
+
f"array_cat({array1_metadata.literal}, {array2_literal})"
|
|
1043
|
+
)
|
|
1044
|
+
return cast(list[T], array1_metadata)
|
|
1045
|
+
|
|
1046
|
+
def array_position(self, array_field: list[T], element: T) -> int:
|
|
1047
|
+
"""
|
|
1048
|
+
Returns the position of the first occurrence of an element in an array (1-based).
|
|
1049
|
+
Returns NULL if the element is not found.
|
|
1050
|
+
|
|
1051
|
+
:param array_field: The array field to search in
|
|
1052
|
+
:param element: The element to find
|
|
1053
|
+
:return: A function metadata object that resolves to an integer
|
|
1054
|
+
|
|
1055
|
+
```python {{sticky: True}}
|
|
1056
|
+
# Find position of a tag
|
|
1057
|
+
position = await conn.execute(
|
|
1058
|
+
select(func.array_position(Article.tags, 'python'))
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
# Find position in a list of ids
|
|
1062
|
+
rank = await conn.execute(
|
|
1063
|
+
select(func.array_position(Contest.winner_ids, User.id))
|
|
1064
|
+
.where(Contest.id == contest_id)
|
|
1065
|
+
)
|
|
1066
|
+
```
|
|
1067
|
+
"""
|
|
1068
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
1069
|
+
|
|
1070
|
+
# Handle element literal
|
|
1071
|
+
if isinstance(element, str):
|
|
1072
|
+
element_literal = f"'{element}'"
|
|
1073
|
+
elif isinstance(element, (int, float, bool)):
|
|
1074
|
+
element_literal = str(element)
|
|
1075
|
+
else:
|
|
1076
|
+
element_metadata = self._column_to_metadata(element)
|
|
1077
|
+
element_literal = str(element_metadata.literal)
|
|
1078
|
+
|
|
1079
|
+
array_metadata.literal = QueryLiteral(
|
|
1080
|
+
f"array_position({array_metadata.literal}, {element_literal})"
|
|
1081
|
+
)
|
|
1082
|
+
return cast(int, array_metadata)
|
|
1083
|
+
|
|
1084
|
+
def array_remove(self, array_field: list[T], element: T) -> list[T]:
|
|
1085
|
+
"""
|
|
1086
|
+
Removes all occurrences of an element from an array.
|
|
1087
|
+
|
|
1088
|
+
:param array_field: The array field to remove from
|
|
1089
|
+
:param element: The element to remove
|
|
1090
|
+
:return: A function metadata object that resolves to an array
|
|
1091
|
+
|
|
1092
|
+
```python {{sticky: True}}
|
|
1093
|
+
# Remove a tag from the array
|
|
1094
|
+
updated = await conn.execute(
|
|
1095
|
+
update(Article)
|
|
1096
|
+
.set(tags=func.array_remove(Article.tags, 'deprecated'))
|
|
1097
|
+
.where(Article.id == article_id)
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Remove a skill from user's skill list
|
|
1101
|
+
cleaned = await conn.execute(
|
|
1102
|
+
update(User)
|
|
1103
|
+
.set(skills=func.array_remove(User.skills, 'obsolete-skill'))
|
|
1104
|
+
.where(User.id == user_id)
|
|
1105
|
+
)
|
|
1106
|
+
```
|
|
1107
|
+
"""
|
|
1108
|
+
array_metadata = self._column_to_metadata(array_field)
|
|
1109
|
+
|
|
1110
|
+
# Handle element literal
|
|
1111
|
+
if isinstance(element, str):
|
|
1112
|
+
element_literal = f"'{element}'"
|
|
1113
|
+
elif isinstance(element, (int, float, bool)):
|
|
1114
|
+
element_literal = str(element)
|
|
1115
|
+
else:
|
|
1116
|
+
element_metadata = self._column_to_metadata(element)
|
|
1117
|
+
element_literal = str(element_metadata.literal)
|
|
1118
|
+
|
|
1119
|
+
array_metadata.literal = QueryLiteral(
|
|
1120
|
+
f"array_remove({array_metadata.literal}, {element_literal})"
|
|
1121
|
+
)
|
|
1122
|
+
return cast(list[T], array_metadata)
|
|
1123
|
+
|
|
1124
|
+
# Type Conversion Functions
|
|
1125
|
+
def cast(self, field: Any, type_name: Type[T]) -> T:
|
|
1126
|
+
"""
|
|
1127
|
+
Converts value to specified type.
|
|
1128
|
+
|
|
1129
|
+
:param field: The field to convert
|
|
1130
|
+
:param type_name: The target Python type to cast to
|
|
1131
|
+
:return: A function metadata object with the new type
|
|
1132
|
+
|
|
1133
|
+
```python {{sticky: True}}
|
|
1134
|
+
# Cast a string to integer
|
|
1135
|
+
int_value = await conn.execute(select(func.cast(User.string_id, int)))
|
|
1136
|
+
|
|
1137
|
+
# Cast a float to string
|
|
1138
|
+
str_value = await conn.execute(select(func.cast(Account.balance, str)))
|
|
1139
|
+
|
|
1140
|
+
# Cast a string to enum
|
|
1141
|
+
status = await conn.execute(select(func.cast(User.status_str, UserStatus)))
|
|
1142
|
+
```
|
|
1143
|
+
"""
|
|
1144
|
+
|
|
1145
|
+
metadata = self._column_to_metadata(field)
|
|
1146
|
+
|
|
1147
|
+
# Special handling for enums
|
|
1148
|
+
if issubclass(type_name, Enum):
|
|
1149
|
+
metadata.literal = QueryLiteral(
|
|
1150
|
+
f"cast({metadata.literal} as {type_name.__name__.lower()})"
|
|
1151
|
+
)
|
|
1152
|
+
else:
|
|
1153
|
+
sql_type = get_python_to_sql_mapping().get(type_name) # type: ignore
|
|
1154
|
+
if not sql_type:
|
|
1155
|
+
raise ValueError(f"Unsupported type for casting: {type_name}")
|
|
1156
|
+
metadata.literal = QueryLiteral(f"cast({metadata.literal} as {sql_type})")
|
|
1157
|
+
|
|
1158
|
+
return cast(T, metadata)
|
|
1159
|
+
|
|
1160
|
+
def to_char(self, field: Any, format: str) -> str:
|
|
1161
|
+
"""
|
|
1162
|
+
Converts value to string with format.
|
|
1163
|
+
|
|
1164
|
+
:param field: The field to convert
|
|
1165
|
+
:param format: The format string
|
|
1166
|
+
:return: A function metadata object that resolves to a string
|
|
1167
|
+
"""
|
|
1168
|
+
metadata = self._column_to_metadata(field)
|
|
1169
|
+
metadata.literal = QueryLiteral(f"to_char({metadata.literal}, '{format}')")
|
|
1170
|
+
return cast(str, metadata)
|
|
1171
|
+
|
|
1172
|
+
def to_number(self, field: Any, format: str) -> float:
|
|
1173
|
+
"""
|
|
1174
|
+
Converts string to number with format.
|
|
1175
|
+
|
|
1176
|
+
:param field: The string field to convert
|
|
1177
|
+
:param format: The format string
|
|
1178
|
+
:return: A function metadata object that resolves to a float
|
|
1179
|
+
"""
|
|
1180
|
+
metadata = self._column_to_metadata(field)
|
|
1181
|
+
metadata.literal = QueryLiteral(f"to_number({metadata.literal}, '{format}')")
|
|
1182
|
+
return cast(float, metadata)
|
|
1183
|
+
|
|
1184
|
+
def to_timestamp(self, field: Any, format: str) -> datetime:
|
|
1185
|
+
"""
|
|
1186
|
+
Converts string to timestamp with format.
|
|
1187
|
+
|
|
1188
|
+
:param field: The string field to convert
|
|
1189
|
+
:param format: The format string
|
|
1190
|
+
:return: A function metadata object that resolves to a timestamp
|
|
1191
|
+
"""
|
|
1192
|
+
metadata = self._column_to_metadata(field)
|
|
1193
|
+
metadata.literal = QueryLiteral(f"to_timestamp({metadata.literal}, '{format}')")
|
|
1194
|
+
return cast(datetime, metadata)
|
|
1195
|
+
|
|
1196
|
+
def to_tsvector(
|
|
1197
|
+
self, language: str, field: T | list[T]
|
|
1198
|
+
) -> TSVectorFunctionMetadata:
|
|
1199
|
+
"""
|
|
1200
|
+
Creates a tsvector from one or more text fields for full-text search.
|
|
1201
|
+
|
|
1202
|
+
:param language: The language to use for text search (e.g., 'english')
|
|
1203
|
+
:param field: A single text field or list of text fields to convert to tsvector
|
|
1204
|
+
:return: A TSVectorFunctionMetadata object that resolves to a tsvector
|
|
1205
|
+
|
|
1206
|
+
```python {{sticky: True}}
|
|
1207
|
+
# Create a tsvector from a single text field
|
|
1208
|
+
vector = func.to_tsvector('english', Article.content)
|
|
1209
|
+
|
|
1210
|
+
# Create a tsvector from multiple text fields
|
|
1211
|
+
vector = func.to_tsvector('english', [Article.title, Article.content, Article.summary])
|
|
1212
|
+
```
|
|
1213
|
+
"""
|
|
1214
|
+
if isinstance(field, list):
|
|
1215
|
+
if not field:
|
|
1216
|
+
raise ValueError("Cannot create tsvector from empty list of fields")
|
|
1217
|
+
|
|
1218
|
+
# Start with the first field
|
|
1219
|
+
result = self._column_to_metadata(field[0])
|
|
1220
|
+
result.literal = QueryLiteral(
|
|
1221
|
+
f"to_tsvector('{language}', {result.literal})"
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
# Concatenate remaining fields
|
|
1225
|
+
for f in field[1:]:
|
|
1226
|
+
metadata = self._column_to_metadata(f)
|
|
1227
|
+
metadata.literal = QueryLiteral(
|
|
1228
|
+
f"to_tsvector('{language}', {metadata.literal})"
|
|
1229
|
+
)
|
|
1230
|
+
result.literal = QueryLiteral(f"{result.literal} || {metadata.literal}")
|
|
1231
|
+
|
|
1232
|
+
return TSVectorFunctionMetadata(
|
|
1233
|
+
literal=result.literal,
|
|
1234
|
+
original_field=result.original_field,
|
|
1235
|
+
local_name=result.local_name,
|
|
1236
|
+
)
|
|
1237
|
+
else:
|
|
1238
|
+
metadata = self._column_to_metadata(field)
|
|
1239
|
+
metadata.literal = QueryLiteral(
|
|
1240
|
+
f"to_tsvector('{language}', {metadata.literal})"
|
|
1241
|
+
)
|
|
1242
|
+
return TSVectorFunctionMetadata(
|
|
1243
|
+
literal=metadata.literal,
|
|
1244
|
+
original_field=metadata.original_field,
|
|
1245
|
+
local_name=metadata.local_name,
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
def to_tsquery(self, language: str, query: str) -> TSQueryFunctionMetadata:
|
|
1249
|
+
"""
|
|
1250
|
+
Creates a tsquery for full-text search.
|
|
1251
|
+
|
|
1252
|
+
:param language: The language to use for text search (e.g., 'english')
|
|
1253
|
+
:param query: The search query string
|
|
1254
|
+
:return: A TSQueryFunctionMetadata object that resolves to a tsquery
|
|
1255
|
+
|
|
1256
|
+
```python {{sticky: True}}
|
|
1257
|
+
# Create a tsquery from a search string
|
|
1258
|
+
query = func.to_tsquery('english', 'python & programming')
|
|
1259
|
+
```
|
|
1260
|
+
"""
|
|
1261
|
+
return TSQueryFunctionMetadata(
|
|
1262
|
+
literal=QueryLiteral(f"to_tsquery('{language}', '{query}')"),
|
|
1263
|
+
original_field=None, # type: ignore
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
def setweight(self, field: Any, weight: str) -> TSVectorFunctionMetadata:
|
|
1267
|
+
"""
|
|
1268
|
+
Sets the weight of a tsvector.
|
|
1269
|
+
|
|
1270
|
+
:param field: The tsvector to set weight for
|
|
1271
|
+
:param weight: The weight to set (A, B, C, or D)
|
|
1272
|
+
:return: A TSVectorFunctionMetadata object for the weighted tsvector
|
|
1273
|
+
|
|
1274
|
+
```python {{sticky: True}}
|
|
1275
|
+
# Set weight for a tsvector
|
|
1276
|
+
weighted = func.setweight(func.to_tsvector('english', Article.title), 'A')
|
|
1277
|
+
```
|
|
1278
|
+
"""
|
|
1279
|
+
metadata = self._column_to_metadata(field)
|
|
1280
|
+
metadata.literal = QueryLiteral(f"setweight({metadata.literal}, '{weight}')")
|
|
1281
|
+
return TSVectorFunctionMetadata(
|
|
1282
|
+
literal=metadata.literal,
|
|
1283
|
+
original_field=metadata.original_field,
|
|
1284
|
+
local_name=metadata.local_name,
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
def ts_rank(self, vector: Any, query: Any) -> int:
|
|
1288
|
+
"""
|
|
1289
|
+
Ranks search results.
|
|
1290
|
+
|
|
1291
|
+
:param vector: The tsvector to rank
|
|
1292
|
+
:param query: The tsquery to rank against
|
|
1293
|
+
:return: A function metadata object that resolves to a float
|
|
1294
|
+
|
|
1295
|
+
```python {{sticky: True}}
|
|
1296
|
+
# Rank search results
|
|
1297
|
+
rank = func.ts_rank(
|
|
1298
|
+
func.to_tsvector('english', Article.content),
|
|
1299
|
+
func.to_tsquery('english', 'python')
|
|
1300
|
+
)
|
|
1301
|
+
```
|
|
1302
|
+
"""
|
|
1303
|
+
vector_metadata = self._column_to_metadata(vector)
|
|
1304
|
+
query_metadata = self._column_to_metadata(query)
|
|
1305
|
+
metadata = FunctionMetadata(
|
|
1306
|
+
literal=QueryLiteral(
|
|
1307
|
+
f"ts_rank({vector_metadata.literal}, {query_metadata.literal})"
|
|
1308
|
+
),
|
|
1309
|
+
original_field=vector_metadata.original_field,
|
|
1310
|
+
)
|
|
1311
|
+
return cast(int, metadata)
|
|
1312
|
+
|
|
1313
|
+
def ts_headline(
|
|
1314
|
+
self, language: str, field: T, query: T, options: str | None = None
|
|
1315
|
+
) -> str:
|
|
1316
|
+
"""
|
|
1317
|
+
Generates search result highlights.
|
|
1318
|
+
|
|
1319
|
+
:param language: The language to use for text search
|
|
1320
|
+
:param field: The text field to generate highlights for
|
|
1321
|
+
:param query: The tsquery to highlight
|
|
1322
|
+
:param options: Optional configuration string
|
|
1323
|
+
:return: A function metadata object that resolves to a string
|
|
1324
|
+
|
|
1325
|
+
```python {{sticky: True}}
|
|
1326
|
+
# Generate search result highlights
|
|
1327
|
+
headline = func.ts_headline(
|
|
1328
|
+
'english',
|
|
1329
|
+
Article.content,
|
|
1330
|
+
func.to_tsquery('english', 'python'),
|
|
1331
|
+
'StartSel=<mark>, StopSel=</mark>'
|
|
1332
|
+
)
|
|
1333
|
+
```
|
|
1334
|
+
"""
|
|
1335
|
+
field_metadata = self._column_to_metadata(field)
|
|
1336
|
+
query_metadata = self._column_to_metadata(query)
|
|
1337
|
+
if options:
|
|
1338
|
+
metadata = FunctionMetadata(
|
|
1339
|
+
literal=QueryLiteral(
|
|
1340
|
+
f"ts_headline('{language}', {field_metadata.literal}, {query_metadata.literal}, '{options}')"
|
|
1341
|
+
),
|
|
1342
|
+
original_field=field_metadata.original_field,
|
|
1343
|
+
)
|
|
1344
|
+
else:
|
|
1345
|
+
metadata = FunctionMetadata(
|
|
1346
|
+
literal=QueryLiteral(
|
|
1347
|
+
f"ts_headline('{language}', {field_metadata.literal}, {query_metadata.literal})"
|
|
1348
|
+
),
|
|
1349
|
+
original_field=field_metadata.original_field,
|
|
1350
|
+
)
|
|
1351
|
+
return cast(str, metadata)
|
|
1352
|
+
|
|
1353
|
+
@staticmethod
|
|
1354
|
+
def _column_to_metadata(field: Any) -> FunctionMetadata:
|
|
1355
|
+
"""
|
|
1356
|
+
Internal helper method to convert a field to FunctionMetadata.
|
|
1357
|
+
Handles both raw columns and nested function calls.
|
|
1358
|
+
|
|
1359
|
+
:param field: The field to convert
|
|
1360
|
+
:return: A FunctionMetadata instance
|
|
1361
|
+
:raises ValueError: If the field cannot be converted to a column
|
|
1362
|
+
"""
|
|
1363
|
+
if is_function_metadata(field):
|
|
1364
|
+
return field
|
|
1365
|
+
elif is_column(field):
|
|
1366
|
+
return FunctionMetadata(literal=field.to_query()[0], original_field=field)
|
|
1367
|
+
else:
|
|
1368
|
+
raise ValueError(
|
|
1369
|
+
f"Unable to cast this type to a column: {field} ({type(field)})"
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
func = FunctionBuilder()
|
|
1374
|
+
"""
|
|
1375
|
+
A global instance of FunctionBuilder that provides SQL function operations for use in queries.
|
|
1376
|
+
This instance offers a comprehensive set of SQL functions including aggregates, string operations,
|
|
1377
|
+
mathematical functions, date/time manipulations, and type conversions.
|
|
1378
|
+
|
|
1379
|
+
Available function categories:
|
|
1380
|
+
- Aggregate Functions: count, sum, avg, min, max, array_agg, string_agg
|
|
1381
|
+
- String Functions: lower, upper, length, trim, substring
|
|
1382
|
+
- Mathematical Functions: abs, round, ceil, floor, power, sqrt
|
|
1383
|
+
- Date/Time Functions: date_trunc, date_part, extract, age, date
|
|
1384
|
+
- Type Conversion: cast, to_char, to_number, to_timestamp
|
|
1385
|
+
|
|
1386
|
+
```python {{sticky: True}}
|
|
1387
|
+
from iceaxe import func, select
|
|
1388
|
+
|
|
1389
|
+
# Aggregate functions
|
|
1390
|
+
total_users = await conn.execute(select(func.count(User.id)))
|
|
1391
|
+
avg_salary = await conn.execute(select(func.avg(Employee.salary)))
|
|
1392
|
+
unique_statuses = await conn.execute(select(func.distinct(User.status)))
|
|
1393
|
+
|
|
1394
|
+
# String operations
|
|
1395
|
+
users = await conn.execute(select((
|
|
1396
|
+
User.id,
|
|
1397
|
+
func.lower(User.name),
|
|
1398
|
+
func.upper(User.email),
|
|
1399
|
+
func.length(User.bio)
|
|
1400
|
+
)))
|
|
1401
|
+
|
|
1402
|
+
# Date/time operations
|
|
1403
|
+
monthly_stats = await conn.execute(select((
|
|
1404
|
+
func.date_trunc('month', Event.created_at),
|
|
1405
|
+
func.count(Event.id)
|
|
1406
|
+
)).group_by(func.date_trunc('month', Event.created_at)))
|
|
1407
|
+
|
|
1408
|
+
# Mathematical operations
|
|
1409
|
+
account_stats = await conn.execute(select((
|
|
1410
|
+
Account.id,
|
|
1411
|
+
func.abs(Account.balance),
|
|
1412
|
+
func.ceil(Account.interest_rate)
|
|
1413
|
+
)))
|
|
1414
|
+
|
|
1415
|
+
# Type conversions
|
|
1416
|
+
converted = await conn.execute(select((
|
|
1417
|
+
func.cast(User.string_id, int),
|
|
1418
|
+
func.to_char(User.created_at, 'YYYY-MM-DD'),
|
|
1419
|
+
func.cast(User.status_str, UserStatus)
|
|
1420
|
+
)))
|
|
1421
|
+
|
|
1422
|
+
# Complex aggregations
|
|
1423
|
+
department_stats = await conn.execute(
|
|
1424
|
+
select((
|
|
1425
|
+
Department.name,
|
|
1426
|
+
func.array_agg(Employee.name),
|
|
1427
|
+
func.string_agg(Employee.email, ','),
|
|
1428
|
+
func.sum(Employee.salary)
|
|
1429
|
+
)).group_by(Department.name)
|
|
1430
|
+
)
|
|
1431
|
+
```
|
|
1432
|
+
"""
|