iceaxe 0.7.1__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 +1264 -0
  20. iceaxe/__tests__/schemas/test_cli.py +25 -0
  21. iceaxe/__tests__/schemas/test_db_memory_serializer.py +1525 -0
  22. iceaxe/__tests__/schemas/test_db_serializer.py +398 -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 +605 -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 +350 -0
  36. iceaxe/comparison.py +560 -0
  37. iceaxe/field.py +250 -0
  38. iceaxe/functions.py +906 -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 +1455 -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 +705 -0
  63. iceaxe/schemas/db_serializer.py +346 -0
  64. iceaxe/schemas/db_stubs.py +525 -0
  65. iceaxe/session.py +860 -0
  66. iceaxe/session_optimized.c +12035 -0
  67. iceaxe/session_optimized.cpython-313-darwin.so +0 -0
  68. iceaxe/session_optimized.pyx +212 -0
  69. iceaxe/sql_types.py +148 -0
  70. iceaxe/typing.py +73 -0
  71. iceaxe-0.7.1.dist-info/METADATA +261 -0
  72. iceaxe-0.7.1.dist-info/RECORD +75 -0
  73. iceaxe-0.7.1.dist-info/WHEEL +6 -0
  74. iceaxe-0.7.1.dist-info/licenses/LICENSE +21 -0
  75. iceaxe-0.7.1.dist-info/top_level.txt +1 -0
iceaxe/functions.py ADDED
@@ -0,0 +1,906 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, 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 FunctionBuilder:
197
+ """
198
+ Builder class for SQL aggregate functions and other SQL operations.
199
+ Provides a Pythonic interface for creating SQL function calls with proper type hints.
200
+
201
+ This class is typically accessed through the global `func` instance:
202
+ ```python {{sticky: True}}
203
+ from iceaxe import func
204
+
205
+ query = select((
206
+ User.name,
207
+ func.count(User.id),
208
+ func.max(User.age)
209
+ ))
210
+ ```
211
+ """
212
+
213
+ def count(self, field: Any) -> int:
214
+ """
215
+ Creates a COUNT aggregate function call.
216
+
217
+ :param field: The field to count. Can be a column or another function result
218
+ :return: A function metadata object that resolves to an integer count
219
+
220
+ ```python {{sticky: True}}
221
+ # Count all users
222
+ total = await conn.execute(select(func.count(User.id)))
223
+
224
+ # Count distinct values
225
+ unique = await conn.execute(
226
+ select(func.count(func.distinct(User.status)))
227
+ )
228
+ ```
229
+ """
230
+ metadata = self._column_to_metadata(field)
231
+ metadata.literal = QueryLiteral(f"count({metadata.literal})")
232
+ return cast(int, metadata)
233
+
234
+ def distinct(self, field: T) -> T:
235
+ """
236
+ Creates a DISTINCT function call that removes duplicate values.
237
+
238
+ :param field: The field to get distinct values from
239
+ :return: A function metadata object preserving the input type
240
+
241
+ ```python {{sticky: True}}
242
+ # Get distinct status values
243
+ statuses = await conn.execute(select(func.distinct(User.status)))
244
+
245
+ # Count distinct values
246
+ unique_count = await conn.execute(
247
+ select(func.count(func.distinct(User.status)))
248
+ )
249
+ ```
250
+ """
251
+ metadata = self._column_to_metadata(field)
252
+ metadata.literal = QueryLiteral(f"distinct {metadata.literal}")
253
+ return cast(T, metadata)
254
+
255
+ def sum(self, field: T) -> T:
256
+ """
257
+ Creates a SUM aggregate function call.
258
+
259
+ :param field: The numeric field to sum
260
+ :return: A function metadata object preserving the input type
261
+
262
+ ```python {{sticky: True}}
263
+ # Get total of all salaries
264
+ total = await conn.execute(select(func.sum(Employee.salary)))
265
+
266
+ # Sum with grouping
267
+ by_dept = await conn.execute(
268
+ select((Department.name, func.sum(Employee.salary)))
269
+ .group_by(Department.name)
270
+ )
271
+ ```
272
+ """
273
+ metadata = self._column_to_metadata(field)
274
+ metadata.literal = QueryLiteral(f"sum({metadata.literal})")
275
+ return cast(T, metadata)
276
+
277
+ def avg(self, field: T) -> T:
278
+ """
279
+ Creates an AVG aggregate function call.
280
+
281
+ :param field: The numeric field to average
282
+ :return: A function metadata object preserving the input type
283
+
284
+ ```python {{sticky: True}}
285
+ # Get average age of all users
286
+ avg_age = await conn.execute(select(func.avg(User.age)))
287
+
288
+ # Average with grouping
289
+ by_dept = await conn.execute(
290
+ select((Department.name, func.avg(Employee.salary)))
291
+ .group_by(Department.name)
292
+ )
293
+ ```
294
+ """
295
+ metadata = self._column_to_metadata(field)
296
+ metadata.literal = QueryLiteral(f"avg({metadata.literal})")
297
+ return cast(T, metadata)
298
+
299
+ def max(self, field: T) -> T:
300
+ """
301
+ Creates a MAX aggregate function call.
302
+
303
+ :param field: The field to get the maximum value from
304
+ :return: A function metadata object preserving the input type
305
+
306
+ ```python {{sticky: True}}
307
+ # Get highest salary
308
+ highest = await conn.execute(select(func.max(Employee.salary)))
309
+
310
+ # Max with grouping
311
+ by_dept = await conn.execute(
312
+ select((Department.name, func.max(Employee.salary)))
313
+ .group_by(Department.name)
314
+ )
315
+ ```
316
+ """
317
+ metadata = self._column_to_metadata(field)
318
+ metadata.literal = QueryLiteral(f"max({metadata.literal})")
319
+ return cast(T, metadata)
320
+
321
+ def min(self, field: T) -> T:
322
+ """
323
+ Creates a MIN aggregate function call.
324
+
325
+ :param field: The field to get the minimum value from
326
+ :return: A function metadata object preserving the input type
327
+
328
+ ```python {{sticky: True}}
329
+ # Get lowest salary
330
+ lowest = await conn.execute(select(func.min(Employee.salary)))
331
+
332
+ # Min with grouping
333
+ by_dept = await conn.execute(
334
+ select((Department.name, func.min(Employee.salary)))
335
+ .group_by(Department.name)
336
+ )
337
+ ```
338
+ """
339
+ metadata = self._column_to_metadata(field)
340
+ metadata.literal = QueryLiteral(f"min({metadata.literal})")
341
+ return cast(T, metadata)
342
+
343
+ def abs(self, field: T) -> T:
344
+ """
345
+ Creates an ABS function call to get the absolute value.
346
+
347
+ :param field: The numeric field to get the absolute value of
348
+ :return: A function metadata object preserving the input type
349
+
350
+ ```python {{sticky: True}}
351
+ # Get absolute value of balance
352
+ abs_balance = await conn.execute(select(func.abs(Account.balance)))
353
+ ```
354
+ """
355
+ metadata = self._column_to_metadata(field)
356
+ metadata.literal = QueryLiteral(f"abs({metadata.literal})")
357
+ return cast(T, metadata)
358
+
359
+ def date_trunc(self, precision: DATE_PRECISION, field: T) -> T:
360
+ """
361
+ Truncates a timestamp or interval value to specified precision.
362
+
363
+ :param precision: The precision to truncate to ('microseconds', 'milliseconds', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade', 'century', 'millennium')
364
+ :param field: The timestamp or interval field to truncate
365
+ :return: A function metadata object preserving the input type
366
+
367
+ ```python {{sticky: True}}
368
+ # Truncate timestamp to month
369
+ monthly = await conn.execute(select(func.date_trunc('month', User.created_at)))
370
+ ```
371
+ """
372
+ metadata = self._column_to_metadata(field)
373
+ metadata.literal = QueryLiteral(
374
+ f"date_trunc('{precision}', {metadata.literal})"
375
+ )
376
+ return cast(T, metadata)
377
+
378
+ def date_part(self, field: DATE_PART_FIELD, source: Any) -> float:
379
+ """
380
+ Extracts a subfield from a date/time value.
381
+
382
+ :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')
383
+ :param source: The date/time field to extract from
384
+ :return: A function metadata object that resolves to an integer
385
+
386
+ ```python {{sticky: True}}
387
+ # Get month from timestamp
388
+ month = await conn.execute(select(func.date_part('month', User.created_at)))
389
+ ```
390
+ """
391
+ metadata = self._column_to_metadata(source)
392
+ metadata.literal = QueryLiteral(f"date_part('{field}', {metadata.literal})")
393
+ return cast(float, metadata)
394
+
395
+ def extract(self, field: DATE_PART_FIELD, source: Any) -> int:
396
+ """
397
+ Extracts a subfield from a date/time value using SQL standard syntax.
398
+
399
+ :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')
400
+ :param source: The date/time field to extract from
401
+ :return: A function metadata object that resolves to an integer
402
+
403
+ ```python {{sticky: True}}
404
+ # Get year from timestamp
405
+ year = await conn.execute(select(func.extract('year', User.created_at)))
406
+ ```
407
+ """
408
+ metadata = self._column_to_metadata(source)
409
+ metadata.literal = QueryLiteral(f"extract({field} from {metadata.literal})")
410
+ return cast(int, metadata)
411
+
412
+ def age(self, timestamp: T, reference: T | None = None) -> T:
413
+ """
414
+ Calculates the difference between two timestamps.
415
+ If reference is not provided, current_date is used.
416
+
417
+ :param timestamp: The timestamp to calculate age from
418
+ :param reference: Optional reference timestamp (defaults to current_date)
419
+ :return: A function metadata object preserving the input type
420
+
421
+ ```python {{sticky: True}}
422
+ # Get age of a timestamp
423
+ age = await conn.execute(select(func.age(User.birth_date)))
424
+
425
+ # Get age between two timestamps
426
+ age_diff = await conn.execute(select(func.age(Event.end_time, Event.start_time)))
427
+ ```
428
+ """
429
+ metadata = self._column_to_metadata(timestamp)
430
+ if reference is not None:
431
+ ref_metadata = self._column_to_metadata(reference)
432
+ metadata.literal = QueryLiteral(
433
+ f"age({metadata.literal}, {ref_metadata.literal})"
434
+ )
435
+ else:
436
+ metadata.literal = QueryLiteral(f"age({metadata.literal})")
437
+ return cast(T, metadata)
438
+
439
+ def date(self, field: T) -> T:
440
+ """
441
+ Converts a timestamp to a date by dropping the time component.
442
+
443
+ :param field: The timestamp field to convert
444
+ :return: A function metadata object that resolves to a date
445
+
446
+ ```python {{sticky: True}}
447
+ # Get just the date part
448
+ event_date = await conn.execute(select(func.date(Event.timestamp)))
449
+ ```
450
+ """
451
+ metadata = self._column_to_metadata(field)
452
+ metadata.literal = QueryLiteral(f"date({metadata.literal})")
453
+ return cast(T, metadata)
454
+
455
+ # String Functions
456
+ def lower(self, field: T) -> T:
457
+ """
458
+ Converts string to lowercase.
459
+
460
+ :param field: The string field to convert
461
+ :return: A function metadata object preserving the input type
462
+ """
463
+ metadata = self._column_to_metadata(field)
464
+ metadata.literal = QueryLiteral(f"lower({metadata.literal})")
465
+ return cast(T, metadata)
466
+
467
+ def upper(self, field: T) -> T:
468
+ """
469
+ Converts string to uppercase.
470
+
471
+ :param field: The string field to convert
472
+ :return: A function metadata object preserving the input type
473
+ """
474
+ metadata = self._column_to_metadata(field)
475
+ metadata.literal = QueryLiteral(f"upper({metadata.literal})")
476
+ return cast(T, metadata)
477
+
478
+ def length(self, field: Any) -> int:
479
+ """
480
+ Returns length of string.
481
+
482
+ :param field: The string field to measure
483
+ :return: A function metadata object that resolves to an integer
484
+ """
485
+ metadata = self._column_to_metadata(field)
486
+ metadata.literal = QueryLiteral(f"length({metadata.literal})")
487
+ return cast(int, metadata)
488
+
489
+ def trim(self, field: T) -> T:
490
+ """
491
+ Removes whitespace from both ends of string.
492
+
493
+ :param field: The string field to trim
494
+ :return: A function metadata object preserving the input type
495
+ """
496
+ metadata = self._column_to_metadata(field)
497
+ metadata.literal = QueryLiteral(f"trim({metadata.literal})")
498
+ return cast(T, metadata)
499
+
500
+ def substring(self, field: T, start: int, length: int) -> T:
501
+ """
502
+ Extracts substring.
503
+
504
+ :param field: The string field to extract from
505
+ :param start: Starting position (1-based)
506
+ :param length: Number of characters to extract
507
+ :return: A function metadata object preserving the input type
508
+ """
509
+ metadata = self._column_to_metadata(field)
510
+ metadata.literal = QueryLiteral(
511
+ f"substring({metadata.literal} from {start} for {length})"
512
+ )
513
+ return cast(T, metadata)
514
+
515
+ # Mathematical Functions
516
+ def round(self, field: T) -> T:
517
+ """
518
+ Rounds to nearest integer.
519
+
520
+ :param field: The numeric field to round
521
+ :return: A function metadata object preserving the input type
522
+ """
523
+ metadata = self._column_to_metadata(field)
524
+ metadata.literal = QueryLiteral(f"round({metadata.literal})")
525
+ return cast(T, metadata)
526
+
527
+ def ceil(self, field: T) -> T:
528
+ """
529
+ Rounds up to nearest integer.
530
+
531
+ :param field: The numeric field to round up
532
+ :return: A function metadata object preserving the input type
533
+ """
534
+ metadata = self._column_to_metadata(field)
535
+ metadata.literal = QueryLiteral(f"ceil({metadata.literal})")
536
+ return cast(T, metadata)
537
+
538
+ def floor(self, field: T) -> T:
539
+ """
540
+ Rounds down to nearest integer.
541
+
542
+ :param field: The numeric field to round down
543
+ :return: A function metadata object preserving the input type
544
+ """
545
+ metadata = self._column_to_metadata(field)
546
+ metadata.literal = QueryLiteral(f"floor({metadata.literal})")
547
+ return cast(T, metadata)
548
+
549
+ def power(self, field: T, exponent: int | float) -> T:
550
+ """
551
+ Raises a number to the specified power.
552
+
553
+ :param field: The numeric field to raise
554
+ :param exponent: The power to raise to
555
+ :return: A function metadata object preserving the input type
556
+ """
557
+ metadata = self._column_to_metadata(field)
558
+ metadata.literal = QueryLiteral(f"power({metadata.literal}, {exponent})")
559
+ return cast(T, metadata)
560
+
561
+ def sqrt(self, field: T) -> T:
562
+ """
563
+ Calculates square root.
564
+
565
+ :param field: The numeric field to calculate square root of
566
+ :return: A function metadata object preserving the input type
567
+ """
568
+ metadata = self._column_to_metadata(field)
569
+ metadata.literal = QueryLiteral(f"sqrt({metadata.literal})")
570
+ return cast(T, metadata)
571
+
572
+ # Aggregate Functions
573
+ def array_agg(self, field: T) -> list[T]:
574
+ """
575
+ Collects values into an array.
576
+
577
+ :param field: The field to aggregate
578
+ :return: A function metadata object that resolves to a list
579
+ """
580
+ metadata = self._column_to_metadata(field)
581
+ metadata.literal = QueryLiteral(f"array_agg({metadata.literal})")
582
+ return cast(list[T], metadata)
583
+
584
+ def string_agg(self, field: Any, delimiter: str) -> str:
585
+ """
586
+ Concatenates values with delimiter.
587
+
588
+ :param field: The field to aggregate
589
+ :param delimiter: The delimiter to use between values
590
+ :return: A function metadata object that resolves to a string
591
+ """
592
+ metadata = self._column_to_metadata(field)
593
+ metadata.literal = QueryLiteral(
594
+ f"string_agg({metadata.literal}, '{delimiter}')"
595
+ )
596
+ return cast(str, metadata)
597
+
598
+ # Type Conversion Functions
599
+ def cast(self, field: Any, type_name: Type[T]) -> T:
600
+ """
601
+ Converts value to specified type.
602
+
603
+ :param field: The field to convert
604
+ :param type_name: The target Python type to cast to
605
+ :return: A function metadata object with the new type
606
+
607
+ ```python {{sticky: True}}
608
+ # Cast a string to integer
609
+ int_value = await conn.execute(select(func.cast(User.string_id, int)))
610
+
611
+ # Cast a float to string
612
+ str_value = await conn.execute(select(func.cast(Account.balance, str)))
613
+
614
+ # Cast a string to enum
615
+ status = await conn.execute(select(func.cast(User.status_str, UserStatus)))
616
+ ```
617
+ """
618
+
619
+ metadata = self._column_to_metadata(field)
620
+
621
+ # Special handling for enums
622
+ if issubclass(type_name, Enum):
623
+ metadata.literal = QueryLiteral(
624
+ f"cast({metadata.literal} as {type_name.__name__.lower()})"
625
+ )
626
+ else:
627
+ sql_type = get_python_to_sql_mapping().get(type_name) # type: ignore
628
+ if not sql_type:
629
+ raise ValueError(f"Unsupported type for casting: {type_name}")
630
+ metadata.literal = QueryLiteral(f"cast({metadata.literal} as {sql_type})")
631
+
632
+ return cast(T, metadata)
633
+
634
+ def to_char(self, field: Any, format: str) -> str:
635
+ """
636
+ Converts value to string with format.
637
+
638
+ :param field: The field to convert
639
+ :param format: The format string
640
+ :return: A function metadata object that resolves to a string
641
+ """
642
+ metadata = self._column_to_metadata(field)
643
+ metadata.literal = QueryLiteral(f"to_char({metadata.literal}, '{format}')")
644
+ return cast(str, metadata)
645
+
646
+ def to_number(self, field: Any, format: str) -> float:
647
+ """
648
+ Converts string to number with format.
649
+
650
+ :param field: The string field to convert
651
+ :param format: The format string
652
+ :return: A function metadata object that resolves to a float
653
+ """
654
+ metadata = self._column_to_metadata(field)
655
+ metadata.literal = QueryLiteral(f"to_number({metadata.literal}, '{format}')")
656
+ return cast(float, metadata)
657
+
658
+ def to_timestamp(self, field: Any, format: str) -> datetime:
659
+ """
660
+ Converts string to timestamp with format.
661
+
662
+ :param field: The string field to convert
663
+ :param format: The format string
664
+ :return: A function metadata object that resolves to a timestamp
665
+ """
666
+ metadata = self._column_to_metadata(field)
667
+ metadata.literal = QueryLiteral(f"to_timestamp({metadata.literal}, '{format}')")
668
+ return cast(datetime, metadata)
669
+
670
+ def to_tsvector(
671
+ self, language: str, field: T | list[T]
672
+ ) -> TSVectorFunctionMetadata:
673
+ """
674
+ Creates a tsvector from one or more text fields for full-text search.
675
+
676
+ :param language: The language to use for text search (e.g., 'english')
677
+ :param field: A single text field or list of text fields to convert to tsvector
678
+ :return: A TSVectorFunctionMetadata object that resolves to a tsvector
679
+
680
+ ```python {{sticky: True}}
681
+ # Create a tsvector from a single text field
682
+ vector = func.to_tsvector('english', Article.content)
683
+
684
+ # Create a tsvector from multiple text fields
685
+ vector = func.to_tsvector('english', [Article.title, Article.content, Article.summary])
686
+ ```
687
+ """
688
+ if isinstance(field, list):
689
+ if not field:
690
+ raise ValueError("Cannot create tsvector from empty list of fields")
691
+
692
+ # Start with the first field
693
+ result = self._column_to_metadata(field[0])
694
+ result.literal = QueryLiteral(
695
+ f"to_tsvector('{language}', {result.literal})"
696
+ )
697
+
698
+ # Concatenate remaining fields
699
+ for f in field[1:]:
700
+ metadata = self._column_to_metadata(f)
701
+ metadata.literal = QueryLiteral(
702
+ f"to_tsvector('{language}', {metadata.literal})"
703
+ )
704
+ result.literal = QueryLiteral(f"{result.literal} || {metadata.literal}")
705
+
706
+ return TSVectorFunctionMetadata(
707
+ literal=result.literal,
708
+ original_field=result.original_field,
709
+ local_name=result.local_name,
710
+ )
711
+ else:
712
+ metadata = self._column_to_metadata(field)
713
+ metadata.literal = QueryLiteral(
714
+ f"to_tsvector('{language}', {metadata.literal})"
715
+ )
716
+ return TSVectorFunctionMetadata(
717
+ literal=metadata.literal,
718
+ original_field=metadata.original_field,
719
+ local_name=metadata.local_name,
720
+ )
721
+
722
+ def to_tsquery(self, language: str, query: str) -> TSQueryFunctionMetadata:
723
+ """
724
+ Creates a tsquery for full-text search.
725
+
726
+ :param language: The language to use for text search (e.g., 'english')
727
+ :param query: The search query string
728
+ :return: A TSQueryFunctionMetadata object that resolves to a tsquery
729
+
730
+ ```python {{sticky: True}}
731
+ # Create a tsquery from a search string
732
+ query = func.to_tsquery('english', 'python & programming')
733
+ ```
734
+ """
735
+ return TSQueryFunctionMetadata(
736
+ literal=QueryLiteral(f"to_tsquery('{language}', '{query}')"),
737
+ original_field=None, # type: ignore
738
+ )
739
+
740
+ def setweight(self, field: Any, weight: str) -> TSVectorFunctionMetadata:
741
+ """
742
+ Sets the weight of a tsvector.
743
+
744
+ :param field: The tsvector to set weight for
745
+ :param weight: The weight to set (A, B, C, or D)
746
+ :return: A TSVectorFunctionMetadata object for the weighted tsvector
747
+
748
+ ```python {{sticky: True}}
749
+ # Set weight for a tsvector
750
+ weighted = func.setweight(func.to_tsvector('english', Article.title), 'A')
751
+ ```
752
+ """
753
+ metadata = self._column_to_metadata(field)
754
+ metadata.literal = QueryLiteral(f"setweight({metadata.literal}, '{weight}')")
755
+ return TSVectorFunctionMetadata(
756
+ literal=metadata.literal,
757
+ original_field=metadata.original_field,
758
+ local_name=metadata.local_name,
759
+ )
760
+
761
+ def ts_rank(self, vector: Any, query: Any) -> int:
762
+ """
763
+ Ranks search results.
764
+
765
+ :param vector: The tsvector to rank
766
+ :param query: The tsquery to rank against
767
+ :return: A function metadata object that resolves to a float
768
+
769
+ ```python {{sticky: True}}
770
+ # Rank search results
771
+ rank = func.ts_rank(
772
+ func.to_tsvector('english', Article.content),
773
+ func.to_tsquery('english', 'python')
774
+ )
775
+ ```
776
+ """
777
+ vector_metadata = self._column_to_metadata(vector)
778
+ query_metadata = self._column_to_metadata(query)
779
+ metadata = FunctionMetadata(
780
+ literal=QueryLiteral(
781
+ f"ts_rank({vector_metadata.literal}, {query_metadata.literal})"
782
+ ),
783
+ original_field=vector_metadata.original_field,
784
+ )
785
+ return cast(int, metadata)
786
+
787
+ def ts_headline(
788
+ self, language: str, field: T, query: T, options: str | None = None
789
+ ) -> str:
790
+ """
791
+ Generates search result highlights.
792
+
793
+ :param language: The language to use for text search
794
+ :param field: The text field to generate highlights for
795
+ :param query: The tsquery to highlight
796
+ :param options: Optional configuration string
797
+ :return: A function metadata object that resolves to a string
798
+
799
+ ```python {{sticky: True}}
800
+ # Generate search result highlights
801
+ headline = func.ts_headline(
802
+ 'english',
803
+ Article.content,
804
+ func.to_tsquery('english', 'python'),
805
+ 'StartSel=<mark>, StopSel=</mark>'
806
+ )
807
+ ```
808
+ """
809
+ field_metadata = self._column_to_metadata(field)
810
+ query_metadata = self._column_to_metadata(query)
811
+ if options:
812
+ metadata = FunctionMetadata(
813
+ literal=QueryLiteral(
814
+ f"ts_headline('{language}', {field_metadata.literal}, {query_metadata.literal}, '{options}')"
815
+ ),
816
+ original_field=field_metadata.original_field,
817
+ )
818
+ else:
819
+ metadata = FunctionMetadata(
820
+ literal=QueryLiteral(
821
+ f"ts_headline('{language}', {field_metadata.literal}, {query_metadata.literal})"
822
+ ),
823
+ original_field=field_metadata.original_field,
824
+ )
825
+ return cast(str, metadata)
826
+
827
+ @staticmethod
828
+ def _column_to_metadata(field: Any) -> FunctionMetadata:
829
+ """
830
+ Internal helper method to convert a field to FunctionMetadata.
831
+ Handles both raw columns and nested function calls.
832
+
833
+ :param field: The field to convert
834
+ :return: A FunctionMetadata instance
835
+ :raises ValueError: If the field cannot be converted to a column
836
+ """
837
+ if is_function_metadata(field):
838
+ return field
839
+ elif is_column(field):
840
+ return FunctionMetadata(literal=field.to_query()[0], original_field=field)
841
+ else:
842
+ raise ValueError(
843
+ f"Unable to cast this type to a column: {field} ({type(field)})"
844
+ )
845
+
846
+
847
+ func = FunctionBuilder()
848
+ """
849
+ A global instance of FunctionBuilder that provides SQL function operations for use in queries.
850
+ This instance offers a comprehensive set of SQL functions including aggregates, string operations,
851
+ mathematical functions, date/time manipulations, and type conversions.
852
+
853
+ Available function categories:
854
+ - Aggregate Functions: count, sum, avg, min, max, array_agg, string_agg
855
+ - String Functions: lower, upper, length, trim, substring
856
+ - Mathematical Functions: abs, round, ceil, floor, power, sqrt
857
+ - Date/Time Functions: date_trunc, date_part, extract, age, date
858
+ - Type Conversion: cast, to_char, to_number, to_timestamp
859
+
860
+ ```python {{sticky: True}}
861
+ from iceaxe import func, select
862
+
863
+ # Aggregate functions
864
+ total_users = await conn.execute(select(func.count(User.id)))
865
+ avg_salary = await conn.execute(select(func.avg(Employee.salary)))
866
+ unique_statuses = await conn.execute(select(func.distinct(User.status)))
867
+
868
+ # String operations
869
+ users = await conn.execute(select((
870
+ User.id,
871
+ func.lower(User.name),
872
+ func.upper(User.email),
873
+ func.length(User.bio)
874
+ )))
875
+
876
+ # Date/time operations
877
+ monthly_stats = await conn.execute(select((
878
+ func.date_trunc('month', Event.created_at),
879
+ func.count(Event.id)
880
+ )).group_by(func.date_trunc('month', Event.created_at)))
881
+
882
+ # Mathematical operations
883
+ account_stats = await conn.execute(select((
884
+ Account.id,
885
+ func.abs(Account.balance),
886
+ func.ceil(Account.interest_rate)
887
+ )))
888
+
889
+ # Type conversions
890
+ converted = await conn.execute(select((
891
+ func.cast(User.string_id, int),
892
+ func.to_char(User.created_at, 'YYYY-MM-DD'),
893
+ func.cast(User.status_str, UserStatus)
894
+ )))
895
+
896
+ # Complex aggregations
897
+ department_stats = await conn.execute(
898
+ select((
899
+ Department.name,
900
+ func.array_agg(Employee.name),
901
+ func.string_agg(Employee.email, ','),
902
+ func.sum(Employee.salary)
903
+ )).group_by(Department.name)
904
+ )
905
+ ```
906
+ """