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.

Files changed (75) hide show
  1. iceaxe/__init__.py +20 -0
  2. iceaxe/__tests__/__init__.py +0 -0
  3. iceaxe/__tests__/benchmarks/__init__.py +0 -0
  4. iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
  5. iceaxe/__tests__/benchmarks/test_select.py +114 -0
  6. iceaxe/__tests__/conf_models.py +133 -0
  7. iceaxe/__tests__/conftest.py +204 -0
  8. iceaxe/__tests__/docker_helpers.py +208 -0
  9. iceaxe/__tests__/helpers.py +268 -0
  10. iceaxe/__tests__/migrations/__init__.py +0 -0
  11. iceaxe/__tests__/migrations/conftest.py +36 -0
  12. iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
  13. iceaxe/__tests__/migrations/test_generator.py +140 -0
  14. iceaxe/__tests__/migrations/test_generics.py +91 -0
  15. iceaxe/__tests__/mountaineer/__init__.py +0 -0
  16. iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
  17. iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
  18. iceaxe/__tests__/schemas/__init__.py +0 -0
  19. iceaxe/__tests__/schemas/test_actions.py +1265 -0
  20. iceaxe/__tests__/schemas/test_cli.py +25 -0
  21. iceaxe/__tests__/schemas/test_db_memory_serializer.py +1571 -0
  22. iceaxe/__tests__/schemas/test_db_serializer.py +435 -0
  23. iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
  24. iceaxe/__tests__/test_alias.py +83 -0
  25. iceaxe/__tests__/test_base.py +52 -0
  26. iceaxe/__tests__/test_comparison.py +383 -0
  27. iceaxe/__tests__/test_field.py +11 -0
  28. iceaxe/__tests__/test_helpers.py +9 -0
  29. iceaxe/__tests__/test_modifications.py +151 -0
  30. iceaxe/__tests__/test_queries.py +764 -0
  31. iceaxe/__tests__/test_queries_str.py +173 -0
  32. iceaxe/__tests__/test_session.py +1511 -0
  33. iceaxe/__tests__/test_text_search.py +287 -0
  34. iceaxe/alias_values.py +67 -0
  35. iceaxe/base.py +351 -0
  36. iceaxe/comparison.py +560 -0
  37. iceaxe/field.py +263 -0
  38. iceaxe/functions.py +1432 -0
  39. iceaxe/generics.py +140 -0
  40. iceaxe/io.py +107 -0
  41. iceaxe/logging.py +91 -0
  42. iceaxe/migrations/__init__.py +5 -0
  43. iceaxe/migrations/action_sorter.py +98 -0
  44. iceaxe/migrations/cli.py +228 -0
  45. iceaxe/migrations/client_io.py +62 -0
  46. iceaxe/migrations/generator.py +404 -0
  47. iceaxe/migrations/migration.py +86 -0
  48. iceaxe/migrations/migrator.py +101 -0
  49. iceaxe/modifications.py +176 -0
  50. iceaxe/mountaineer/__init__.py +10 -0
  51. iceaxe/mountaineer/cli.py +74 -0
  52. iceaxe/mountaineer/config.py +46 -0
  53. iceaxe/mountaineer/dependencies/__init__.py +6 -0
  54. iceaxe/mountaineer/dependencies/core.py +67 -0
  55. iceaxe/postgres.py +133 -0
  56. iceaxe/py.typed +0 -0
  57. iceaxe/queries.py +1459 -0
  58. iceaxe/queries_str.py +294 -0
  59. iceaxe/schemas/__init__.py +0 -0
  60. iceaxe/schemas/actions.py +864 -0
  61. iceaxe/schemas/cli.py +30 -0
  62. iceaxe/schemas/db_memory_serializer.py +711 -0
  63. iceaxe/schemas/db_serializer.py +347 -0
  64. iceaxe/schemas/db_stubs.py +529 -0
  65. iceaxe/session.py +860 -0
  66. iceaxe/session_optimized.c +12207 -0
  67. iceaxe/session_optimized.cpython-313-darwin.so +0 -0
  68. iceaxe/session_optimized.pyx +212 -0
  69. iceaxe/sql_types.py +149 -0
  70. iceaxe/typing.py +73 -0
  71. iceaxe-0.8.3.dist-info/METADATA +262 -0
  72. iceaxe-0.8.3.dist-info/RECORD +75 -0
  73. iceaxe-0.8.3.dist-info/WHEEL +6 -0
  74. iceaxe-0.8.3.dist-info/licenses/LICENSE +21 -0
  75. 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
+ """