sqlobjects 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1054 @@
1
+ """SQLObjects Expression System - Type-Safe and Performance-Optimized
2
+
3
+ This module provides a simplified expression system that directly uses SQLAlchemy
4
+ native expressions, offering type safety, high performance, and modern database
5
+ expression support without unnecessary abstraction layers.
6
+
7
+ Key Design Principles:
8
+ - Direct SQLAlchemy integration for zero-overhead abstraction
9
+ - Type safety through native SQLAlchemy field references
10
+ - Intelligent subquery support with automatic type inference
11
+ - Full compatibility with SQLAlchemy ecosystem
12
+
13
+ Usage Examples:
14
+ # Direct field references with type safety
15
+ User.name.upper() # Chain methods on fields
16
+ User.age >= 18 # Direct comparisons
17
+
18
+ # Database functions
19
+ func.concat(User.first_name, ' ', User.last_name)
20
+ func.extract('year', User.created_at)
21
+
22
+ # Complex expressions
23
+ condition = and_(
24
+ User.age >= 18,
25
+ or_(User.role == 'admin', User.is_staff == True)
26
+ )
27
+
28
+ # Subqueries with intelligent type inference
29
+ avg_salary = User.objects.aggregate(
30
+ avg_sal=func.avg(User.salary)
31
+ ).subquery(query_type="scalar")
32
+ """
33
+
34
+ from typing import Any, Literal
35
+
36
+ from sqlalchemy import ColumnElement, func
37
+ from sqlalchemy.ext.compiler import compiles
38
+ from sqlalchemy.sql import Select, and_, asc, desc, exists, literal, not_, nullsfirst, nullslast, or_, text
39
+ from sqlalchemy.types import Boolean, String
40
+
41
+ from .exceptions import ValidationError
42
+
43
+
44
+ __all__ = [
45
+ # Core expression building
46
+ "func",
47
+ "and_",
48
+ "or_",
49
+ "not_",
50
+ "exists",
51
+ "text",
52
+ "literal",
53
+ # Query ordering
54
+ "asc",
55
+ "desc",
56
+ "nullsfirst",
57
+ "nullslast",
58
+ # Subquery support
59
+ "SubqueryExpression",
60
+ # Function mixins and expression
61
+ "FunctionMixin",
62
+ "StringFunctionMixin",
63
+ "NumericFunctionMixin",
64
+ "DateTimeFunctionMixin",
65
+ "FunctionExpression",
66
+ ]
67
+
68
+
69
+ class SubqueryExpression(ColumnElement):
70
+ """Intelligent subquery expression supporting multiple SQLAlchemy subquery types.
71
+
72
+ This class provides a unified interface for creating and managing different types
73
+ of subqueries including table subqueries, scalar subqueries, and existence subqueries.
74
+ It automatically handles type conversion and provides operator overloading for
75
+ seamless integration with other expressions.
76
+
77
+ Examples:
78
+ >>> # Table subquery for JOIN operations
79
+ >>> subq = User.objects.filter(age__gte=18).subquery()
80
+ >>> # Scalar subquery for comparisons
81
+ >>> avg_age = User.objects.aggregate(avg_age=func.avg(User.age)).subquery("scalar")
82
+ >>> # Existence subquery for boolean conditions
83
+ >>> has_posts = Post.objects.filter(author_id=User.id).subquery("exists")
84
+ """
85
+
86
+ inherit_cache = True # Support SQLAlchemy caching
87
+
88
+ def __init__(
89
+ self, query: Select, name: str | None = None, query_type: Literal["auto", "table", "scalar", "exists"] = "auto"
90
+ ):
91
+ """Initialize subquery expression with intelligent type inference.
92
+
93
+ Args:
94
+ query: SQLAlchemy Select query to convert to subquery
95
+ name: Optional alias name for the subquery
96
+ query_type: Type of subquery ('auto', 'table', 'scalar', 'exists')
97
+
98
+ Raises:
99
+ ValidationError: If query_type is invalid
100
+ """
101
+ super().__init__()
102
+ valid_types = {"auto", "table", "scalar", "exists"}
103
+ if query_type not in valid_types:
104
+ raise ValidationError(f"Unknown query type: {query_type}. Available types: {', '.join(valid_types)}")
105
+
106
+ self.query = query
107
+ self.name = name
108
+ self.query_type = self._infer_type() if query_type == "auto" else query_type
109
+ self._subquery = None
110
+ self._scalar_subquery = None
111
+ self._exists_subquery = None
112
+
113
+ # SQLAlchemy type obtained dynamically through type attribute
114
+ def __getattribute__(self, name):
115
+ if name == "type":
116
+ return self._get_expression_type() or super().__getattribute__(name)
117
+ return super().__getattribute__(name)
118
+
119
+ def _infer_type(self) -> str:
120
+ """Automatically infer the appropriate subquery type based on query structure.
121
+
122
+ Analyzes query characteristics including column count, aggregate functions,
123
+ and LIMIT clauses to determine the most suitable subquery type.
124
+
125
+ Returns:
126
+ Inferred subquery type ('scalar', 'table', or 'exists')
127
+ """
128
+ try:
129
+ structure = self._analyze_query_structure()
130
+
131
+ # Rule 1: Clear scalar query characteristics
132
+ if (
133
+ structure["has_single_column"]
134
+ and structure["has_aggregates"]
135
+ and (structure["has_limit_one"] or structure["is_count_query"])
136
+ ):
137
+ return "scalar"
138
+
139
+ # Rule 2: Single column aggregate query (commonly used for comparisons)
140
+ if structure["has_single_column"] and structure["has_aggregates"]:
141
+ return "scalar"
142
+
143
+ # Rule 3: Multi-column queries default to table subquery
144
+ if structure["column_count"] > 1:
145
+ return "table"
146
+
147
+ # Rule 4: Single column non-aggregate query (e.g., ID lists)
148
+ if structure["has_single_column"] and not structure["has_aggregates"]:
149
+ return "table" # For IN conditions
150
+
151
+ # Default: table subquery
152
+ return "table"
153
+
154
+ except Exception: # noqa
155
+ # Default to table subquery when inference fails
156
+ return "table"
157
+
158
+ def _analyze_query_structure(self) -> dict:
159
+ """Analyze query structure to extract inference criteria.
160
+
161
+ Examines various aspects of the query including SELECT columns,
162
+ aggregate functions, LIMIT clauses, and annotations to provide
163
+ data for intelligent type inference.
164
+
165
+ Returns:
166
+ Dictionary containing query structure analysis results
167
+ """
168
+ analysis = {
169
+ "select_columns": [],
170
+ "has_aggregates": False,
171
+ "has_single_column": False,
172
+ "has_limit_one": False,
173
+ "has_annotations": False,
174
+ "column_count": 0,
175
+ "is_count_query": False,
176
+ }
177
+
178
+ try:
179
+ # Analyze SELECT columns
180
+ if hasattr(self.query, "selected_columns"):
181
+ analysis["select_columns"] = list(self.query.selected_columns) # noqa
182
+ analysis["column_count"] = len(analysis["select_columns"])
183
+ analysis["has_single_column"] = analysis["column_count"] == 1
184
+
185
+ # Analyze aggregate functions (simplified detection)
186
+ query_str = str(self.query).lower()
187
+ aggregate_keywords = ["count(", "sum(", "avg(", "max(", "min("]
188
+ analysis["has_aggregates"] = any(keyword in query_str for keyword in aggregate_keywords)
189
+
190
+ # Analyze LIMIT clause
191
+ analysis["has_limit_one"] = (
192
+ hasattr(self.query, "_limit") and self.query._limit is not None and self.query._limit == 1 # noqa
193
+ )
194
+
195
+ # Detect count queries
196
+ analysis["is_count_query"] = "count(" in query_str
197
+
198
+ except Exception: # noqa
199
+ # Return safe defaults when analysis fails
200
+ pass
201
+
202
+ return analysis
203
+
204
+ def _get_expression_type(self):
205
+ """Infer SQLAlchemy type based on subquery type
206
+
207
+ Returns:
208
+ SQLAlchemy type object or None for table subqueries
209
+ """
210
+ if self.query_type == "exists":
211
+ return Boolean()
212
+ elif self.query_type == "scalar":
213
+ return self._infer_scalar_type()
214
+ else: # "table"
215
+ return None
216
+
217
+ def _infer_scalar_type(self):
218
+ """Infer the return type of scalar subquery
219
+
220
+ Returns:
221
+ SQLAlchemy type object based on column analysis
222
+ """
223
+ try:
224
+ columns = list(self.query.selected_columns) # noqa
225
+
226
+ if len(columns) == 1:
227
+ # Single column query: use the column's type directly
228
+ return columns[0].type
229
+ elif len(columns) > 1:
230
+ # Multi-column query: find aggregate column (usually the last one)
231
+ return self._find_aggregate_column_type(columns)
232
+ else:
233
+ # No column info: use default type
234
+ return String()
235
+
236
+ except Exception: # noqa
237
+ return String()
238
+
239
+ @staticmethod
240
+ def _find_aggregate_column_type(columns):
241
+ """Find aggregate column type from multiple columns
242
+
243
+ Args:
244
+ columns: List of column objects to analyze
245
+
246
+ Returns:
247
+ SQLAlchemy type object of the aggregate column
248
+ """
249
+ # Strategy 1: Find columns with aggregate function labels
250
+ for col in reversed(columns): # Search from back to front
251
+ if hasattr(col, "name") and col.name:
252
+ # Check if column is generated by aggregate function
253
+ if any(agg in str(col).lower() for agg in ["count", "sum", "avg", "max", "min"]):
254
+ return col.type
255
+
256
+ # Strategy 2: Find annotate-generated column (usually the last one)
257
+ last_column = columns[-1]
258
+ if hasattr(last_column, "type"):
259
+ return last_column.type
260
+
261
+ # Strategy 3: Default type
262
+ return String()
263
+
264
+ def get_children(self, **kwargs):
265
+ """Return child expressions for SQLAlchemy visitor pattern
266
+
267
+ Args:
268
+ **kwargs: Additional keyword arguments
269
+
270
+ Returns:
271
+ List containing the query object
272
+ """
273
+ return [self.query]
274
+
275
+ def resolve(self, table_or_model=None) -> Any:
276
+ """Resolve to appropriate SQLAlchemy object based on subquery type.
277
+
278
+ Args:
279
+ table_or_model: Table object or model class for field resolution (unused for subqueries)
280
+
281
+ Returns:
282
+ SQLAlchemy subquery object (Subquery, ScalarSelect, or Exists)
283
+
284
+ Raises:
285
+ ValidationError: If subquery conversion fails
286
+ """
287
+ _ = table_or_model # use it to avoid unused argument warning
288
+
289
+ try:
290
+ if self.query_type == "scalar":
291
+ return self._get_scalar_subquery()
292
+ elif self.query_type == "exists":
293
+ return self._get_exists_subquery()
294
+ else: # 'table'
295
+ return self._get_table_subquery()
296
+ except Exception as e:
297
+ raise ValidationError(f"Subquery conversion failed: {e}") from e
298
+
299
+ def _get_table_subquery(self):
300
+ """Get table subquery (equivalent to SQLAlchemy subquery()).
301
+
302
+ Creates a table subquery that can be used in JOIN operations
303
+ and other table-level operations.
304
+
305
+ Returns:
306
+ SQLAlchemy Subquery object
307
+
308
+ Raises:
309
+ ValidationError: If subquery creation fails
310
+ """
311
+ if self._subquery is None:
312
+ try:
313
+ self._subquery = self.query.subquery(name=self.name)
314
+ except Exception as e:
315
+ raise ValidationError(f"Subquery build failed: {e}") from e
316
+ return self._subquery
317
+
318
+ def _get_scalar_subquery(self):
319
+ """Get scalar subquery (equivalent to SQLAlchemy scalar_subquery()).
320
+
321
+ Creates a scalar subquery that returns a single value and can be used
322
+ in comparisons and arithmetic operations.
323
+
324
+ IMPORTANT: This method handles multi-column queries (like from annotate())
325
+ by extracting only the aggregate column. This is necessary because:
326
+ 1. SQLAlchemy allows multi-column scalar_subquery() calls without error
327
+ 2. But databases reject multi-column scalar subqueries at runtime with "row value misused"
328
+ 3. We need to extract the intended aggregate column for proper SQL generation
329
+
330
+ Example problematic case:
331
+ User.objects.annotate(avg_sal=func.avg(User.salary)).subquery("scalar")
332
+ Original: SELECT users.id, users.name, avg(salary) AS avg_sal FROM users
333
+ Fixed: SELECT avg(salary) AS avg_sal FROM users WHERE ...
334
+
335
+ Returns:
336
+ SQLAlchemy ScalarSelect object
337
+
338
+ Raises:
339
+ ValidationError: If scalar subquery creation fails
340
+ """
341
+ if self._scalar_subquery is None:
342
+ try:
343
+ # Handle multi-column queries (e.g., from annotate()) by extracting aggregate columns
344
+ columns = list(self.query.selected_columns) # noqa
345
+ if len(columns) > 1:
346
+ # Multi-column query: extract the aggregate column (usually the last one)
347
+ # This prevents "row value misused" database errors
348
+ agg_column = columns[-1]
349
+ from sqlalchemy import select
350
+
351
+ # Create new query with only the aggregate column
352
+ scalar_query = select(agg_column)
353
+ # Copy WHERE clause if exists
354
+ if hasattr(self.query, "whereclause") and self.query.whereclause is not None:
355
+ scalar_query = scalar_query.where(self.query.whereclause)
356
+ # Copy FROM clause
357
+ if hasattr(self.query, "table") and self.query.table is not None: # type: ignore[reportAttributeAccessIssue]
358
+ scalar_query = scalar_query.select_from(self.query.table) # type: ignore[reportAttributeAccessIssue]
359
+ elif hasattr(self.query, "froms") and self.query.froms:
360
+ scalar_query = scalar_query.select_from(*self.query.froms)
361
+ self._scalar_subquery = scalar_query.scalar_subquery()
362
+ else:
363
+ # Single column query: use as-is (safe for scalar subquery)
364
+ self._scalar_subquery = self.query.scalar_subquery()
365
+ except Exception as e:
366
+ raise ValidationError(f"Scalar subquery build failed: {e}") from e
367
+ return self._scalar_subquery
368
+
369
+ def _get_exists_subquery(self):
370
+ """Get existence subquery (equivalent to SQLAlchemy exists()).
371
+
372
+ Creates an existence subquery that returns a boolean value indicating
373
+ whether any rows match the subquery conditions.
374
+
375
+ Returns:
376
+ SQLAlchemy Exists object
377
+
378
+ Raises:
379
+ ValidationError: If existence subquery creation fails
380
+ """
381
+ if self._exists_subquery is None:
382
+ try:
383
+ self._exists_subquery = exists(self.query)
384
+ except Exception as e:
385
+ raise ValidationError(f"Exists subquery build failed: {e}") from e
386
+ return self._exists_subquery
387
+
388
+ @property
389
+ def c(self):
390
+ """Access subquery columns (only applicable to table subqueries).
391
+
392
+ Provides access to the columns of a table subquery, similar to
393
+ SQLAlchemy's subquery.c attribute.
394
+
395
+ Returns:
396
+ Column collection for the table subquery
397
+
398
+ Raises:
399
+ ValidationError: If called on non-table subquery types
400
+ """
401
+ if self.query_type != "table":
402
+ raise ValidationError(f"Column access not supported on {self.query_type} subquery")
403
+ return self._get_table_subquery().c
404
+
405
+ def alias(self, name: str) -> "SubqueryExpression":
406
+ """Create an alias for the subquery.
407
+
408
+ Args:
409
+ name: Alias name for the subquery
410
+
411
+ Returns:
412
+ New SubqueryExpression with the specified alias
413
+ """
414
+ return SubqueryExpression(self.query, name, self.query_type) # type: ignore[arg-type]
415
+
416
+ def as_scalar(self) -> "SubqueryExpression":
417
+ """Convert to scalar subquery type.
418
+
419
+ Returns:
420
+ New SubqueryExpression configured as scalar subquery
421
+ """
422
+ return SubqueryExpression(self.query, self.name, "scalar")
423
+
424
+ def as_exists(self) -> "SubqueryExpression":
425
+ """Convert to existence subquery type.
426
+
427
+ Returns:
428
+ New SubqueryExpression configured as existence subquery
429
+ """
430
+ return SubqueryExpression(self.query, self.name, "exists")
431
+
432
+ def as_table(self) -> "SubqueryExpression":
433
+ """Convert to table subquery type.
434
+
435
+ Returns:
436
+ New SubqueryExpression configured as table subquery
437
+ """
438
+ return SubqueryExpression(self.query, self.name, "table")
439
+
440
+
441
+ @compiles(SubqueryExpression)
442
+ def visit_subquery_expression(element, compiler, **kw):
443
+ """SQLAlchemy compiler: compile SubqueryExpression to SQL
444
+
445
+ Args:
446
+ element: SubqueryExpression instance to compile
447
+ compiler: SQLAlchemy compiler instance
448
+ **kw: Additional compilation keywords
449
+
450
+ Returns:
451
+ Compiled SQL string
452
+ """
453
+ return compiler.process(element.resolve(), **kw)
454
+
455
+
456
+ # === Function Mixin System ===
457
+
458
+
459
+ class FunctionMixin:
460
+ """Function method mixin class to reduce code duplication
461
+
462
+ Provides common database function methods that can be mixed into
463
+ field classes and expression classes.
464
+ """
465
+
466
+ def _get_expression(self):
467
+ """Get current expression - defaults to using expr attribute
468
+
469
+ Returns:
470
+ The expression object to apply functions to
471
+ """
472
+ return self.expr # type: ignore[reportAttributeAccessIssue]
473
+
474
+ def _create_result(self, func_call): # noqa
475
+ """Create FunctionExpression object
476
+
477
+ Args:
478
+ func_call: SQLAlchemy function call result
479
+
480
+ Returns:
481
+ FunctionExpression wrapping the function call
482
+ """
483
+ return FunctionExpression(func_call)
484
+
485
+ # === General functions ===
486
+ def cast(self, type_: str, **kwargs) -> "FunctionExpression":
487
+ """Cast expression to specified type
488
+
489
+ Args:
490
+ type_: Target type name
491
+ **kwargs: Additional type parameters
492
+
493
+ Returns:
494
+ FunctionExpression with cast operation
495
+ """
496
+ from .fields import create_type_instance
497
+
498
+ sqlalchemy_type = create_type_instance(type_, kwargs)
499
+ return self._create_result(func.cast(self._get_expression(), sqlalchemy_type))
500
+
501
+ def is_null(self) -> ColumnElement[bool]:
502
+ """Check if expression is NULL
503
+
504
+ Returns:
505
+ Boolean expression for NULL check
506
+ """
507
+ return self._get_expression().is_(None)
508
+
509
+ def is_not_null(self) -> ColumnElement[bool]:
510
+ """Check if expression is NOT NULL
511
+
512
+ Returns:
513
+ Boolean expression for NOT NULL check
514
+ """
515
+ return self._get_expression().is_not(None)
516
+
517
+ def case(self, *conditions, else_=None) -> "FunctionExpression":
518
+ """Create CASE expression
519
+
520
+ Args:
521
+ *conditions: Condition tuples or dictionary
522
+ else_: Default value for ELSE clause
523
+
524
+ Returns:
525
+ FunctionExpression with CASE operation
526
+ """
527
+ if len(conditions) == 1 and isinstance(conditions[0], dict):
528
+ cases = list(conditions[0].items())
529
+ else:
530
+ cases = conditions
531
+ return self._create_result(func.case(*cases, else_=else_))
532
+
533
+ def coalesce(self, *values) -> "FunctionExpression":
534
+ """Return first non-NULL value
535
+
536
+ Args:
537
+ *values: Values to check for non-NULL
538
+
539
+ Returns:
540
+ FunctionExpression with COALESCE operation
541
+ """
542
+ return self._create_result(func.coalesce(self._get_expression(), *values))
543
+
544
+ def nullif(self, value) -> "FunctionExpression":
545
+ """Return NULL if expression equals value
546
+
547
+ Args:
548
+ value: Value to compare against
549
+
550
+ Returns:
551
+ FunctionExpression with NULLIF operation
552
+ """
553
+ return self._create_result(func.nullif(self._get_expression(), value))
554
+
555
+
556
+ class StringFunctionMixin(FunctionMixin):
557
+ """String function mixin for text operations
558
+
559
+ Provides string manipulation functions like upper, lower, trim, etc.
560
+ """
561
+
562
+ def upper(self) -> "FunctionExpression":
563
+ """Convert string to uppercase"""
564
+ return self._create_result(func.upper(self._get_expression()))
565
+
566
+ def lower(self) -> "FunctionExpression":
567
+ """Convert string to lowercase"""
568
+ return self._create_result(func.lower(self._get_expression()))
569
+
570
+ def trim(self) -> "FunctionExpression":
571
+ """Remove leading and trailing whitespace"""
572
+ return self._create_result(func.trim(self._get_expression()))
573
+
574
+ def length(self) -> "FunctionExpression":
575
+ """Get string length"""
576
+ return self._create_result(func.length(self._get_expression()))
577
+
578
+ def substring(self, start: int, length: int | None = None) -> "FunctionExpression":
579
+ """Extract substring from string
580
+
581
+ Args:
582
+ start: Starting position (1-based)
583
+ length: Optional length of substring
584
+ """
585
+ expr = self._get_expression()
586
+ if length is not None:
587
+ return self._create_result(func.substring(expr, start, length))
588
+ return self._create_result(func.substring(expr, start))
589
+
590
+ def regexp_replace(self, pattern: str, replacement: str) -> "FunctionExpression":
591
+ """Replace text using regular expression
592
+
593
+ Args:
594
+ pattern: Regular expression pattern
595
+ replacement: Replacement string
596
+ """
597
+ return self._create_result(func.regexp_replace(self._get_expression(), pattern, replacement))
598
+
599
+ def split_part(self, delimiter: str, field: int) -> "FunctionExpression":
600
+ """Split string and return specified part
601
+
602
+ Args:
603
+ delimiter: String delimiter
604
+ field: Field number to return (1-based)
605
+ """
606
+ return self._create_result(func.split_part(self._get_expression(), delimiter, field))
607
+
608
+ def position(self, substring: str) -> "FunctionExpression":
609
+ """Find position of substring
610
+
611
+ Args:
612
+ substring: Substring to find
613
+ """
614
+ return self._create_result(func.position(substring, self._get_expression()))
615
+
616
+ def reverse(self) -> "FunctionExpression":
617
+ """Reverse string"""
618
+ return self._create_result(func.reverse(self._get_expression()))
619
+
620
+ def md5(self) -> "FunctionExpression":
621
+ """Calculate MD5 hash of string"""
622
+ return self._create_result(func.md5(self._get_expression()))
623
+
624
+
625
+ class NumericFunctionMixin(FunctionMixin):
626
+ """Numeric function mixin for mathematical operations
627
+
628
+ Provides mathematical functions like abs, round, sqrt, and aggregates.
629
+ """
630
+
631
+ def abs(self) -> "FunctionExpression":
632
+ """Get absolute value"""
633
+ return self._create_result(func.abs(self._get_expression()))
634
+
635
+ def round(self, precision: int = 0) -> "FunctionExpression":
636
+ """Round to specified precision
637
+
638
+ Args:
639
+ precision: Number of decimal places
640
+ """
641
+ return self._create_result(func.round(self._get_expression(), precision))
642
+
643
+ def ceil(self) -> "FunctionExpression":
644
+ """Round up to nearest integer"""
645
+ return self._create_result(func.ceil(self._get_expression()))
646
+
647
+ def floor(self) -> "FunctionExpression":
648
+ """Round down to nearest integer"""
649
+ return self._create_result(func.floor(self._get_expression()))
650
+
651
+ def sqrt(self) -> "FunctionExpression":
652
+ """Calculate square root"""
653
+ return self._create_result(func.sqrt(self._get_expression()))
654
+
655
+ def power(self, exponent) -> "FunctionExpression":
656
+ """Raise to power
657
+
658
+ Args:
659
+ exponent: Exponent value
660
+ """
661
+ return self._create_result(func.power(self._get_expression(), exponent))
662
+
663
+ def mod(self, divisor) -> "FunctionExpression":
664
+ """Calculate modulo
665
+
666
+ Args:
667
+ divisor: Divisor value
668
+ """
669
+ return self._create_result(func.mod(self._get_expression(), divisor))
670
+
671
+ def sign(self) -> "FunctionExpression":
672
+ """Get sign of number (-1, 0, or 1)"""
673
+ return self._create_result(func.sign(self._get_expression()))
674
+
675
+ # Aggregate functions
676
+ def sum(self) -> "FunctionExpression":
677
+ """Calculate sum aggregate"""
678
+ return self._create_result(func.sum(self._get_expression()))
679
+
680
+ def avg(self) -> "FunctionExpression":
681
+ """Calculate average aggregate"""
682
+ return self._create_result(func.avg(self._get_expression()))
683
+
684
+ def max(self) -> "FunctionExpression":
685
+ """Calculate maximum aggregate"""
686
+ return self._create_result(func.max(self._get_expression()))
687
+
688
+ def min(self) -> "FunctionExpression":
689
+ """Calculate minimum aggregate"""
690
+ return self._create_result(func.min(self._get_expression()))
691
+
692
+ def count(self) -> "FunctionExpression":
693
+ """Calculate count aggregate"""
694
+ return self._create_result(func.count(self._get_expression()))
695
+
696
+
697
+ class DateTimeFunctionMixin(FunctionMixin):
698
+ """DateTime function mixin for date/time operations
699
+
700
+ Provides date and time manipulation functions like extract, age, etc.
701
+ """
702
+
703
+ def extract(self, field: str) -> "FunctionExpression":
704
+ """Extract date/time component
705
+
706
+ Args:
707
+ field: Component to extract (year, month, day, etc.)
708
+ """
709
+ return self._create_result(func.extract(field, self._get_expression()))
710
+
711
+ def year(self) -> "FunctionExpression":
712
+ """Extract year component"""
713
+ return self._create_result(func.extract("year", self._get_expression()))
714
+
715
+ def month(self) -> "FunctionExpression":
716
+ """Extract month component"""
717
+ return self._create_result(func.extract("month", self._get_expression()))
718
+
719
+ def day(self) -> "FunctionExpression":
720
+ """Extract day component"""
721
+ return self._create_result(func.extract("day", self._get_expression()))
722
+
723
+ def hour(self) -> "FunctionExpression":
724
+ """Extract hour component"""
725
+ return self._create_result(func.extract("hour", self._get_expression()))
726
+
727
+ def minute(self) -> "FunctionExpression":
728
+ """Extract minute component"""
729
+ return self._create_result(func.extract("minute", self._get_expression()))
730
+
731
+ def age_in_years(self) -> "FunctionExpression":
732
+ """Calculate age in years from current date"""
733
+ expr = self._get_expression()
734
+ return self._create_result(func.extract("year", func.age(func.now(), expr)))
735
+
736
+ def age_in_months(self) -> "FunctionExpression":
737
+ """Calculate age in months from current date"""
738
+ expr = self._get_expression()
739
+ return self._create_result(func.extract("month", func.age(func.now(), expr)))
740
+
741
+ def days_between(self, end_date) -> "FunctionExpression":
742
+ """Calculate days between dates
743
+
744
+ Args:
745
+ end_date: End date for calculation
746
+ """
747
+ expr = self._get_expression()
748
+ return self._create_result(func.extract("day", func.age(end_date, expr)))
749
+
750
+ def date_trunc(self, precision: str) -> "FunctionExpression":
751
+ """Truncate date to specified precision
752
+
753
+ Args:
754
+ precision: Precision level (day, month, year, etc.)
755
+ """
756
+ return self._create_result(func.date_trunc(precision, self._get_expression()))
757
+
758
+ def to_char(self, format_str: str) -> "FunctionExpression":
759
+ """Format date as string
760
+
761
+ Args:
762
+ format_str: Format string
763
+ """
764
+ return self._create_result(func.to_char(self._get_expression(), format_str))
765
+
766
+ def add_days(self, days: int) -> "FunctionExpression":
767
+ """Add days to date
768
+
769
+ Args:
770
+ days: Number of days to add
771
+ """
772
+ expr = self._get_expression()
773
+ return self._create_result(expr + func.interval(f"{days} days"))
774
+
775
+
776
+ class FunctionExpression:
777
+ """Function call result supporting continued method chaining
778
+
779
+ Wraps SQLAlchemy function expressions and provides chainable methods
780
+ for building complex database expressions.
781
+ """
782
+
783
+ # === Core Infrastructure ===
784
+
785
+ def __init__(self, expression):
786
+ """Initialize function expression
787
+
788
+ Args:
789
+ expression: SQLAlchemy expression object to wrap
790
+ """
791
+ self.expression = expression
792
+
793
+ def __getattr__(self, name):
794
+ """Proxy attribute access to underlying expression
795
+
796
+ Args:
797
+ name: Attribute name to access
798
+
799
+ Returns:
800
+ Attribute value from the underlying expression
801
+ """
802
+ return getattr(self.expression, name)
803
+
804
+ # === String Functions ===
805
+
806
+ def upper(self) -> "FunctionExpression":
807
+ return FunctionExpression(func.upper(self.expression))
808
+
809
+ def lower(self) -> "FunctionExpression":
810
+ return FunctionExpression(func.lower(self.expression))
811
+
812
+ def trim(self) -> "FunctionExpression":
813
+ return FunctionExpression(func.trim(self.expression))
814
+
815
+ def length(self) -> "FunctionExpression":
816
+ return FunctionExpression(func.length(self.expression))
817
+
818
+ def substring(self, start: int, length: int | None = None) -> "FunctionExpression":
819
+ if length is not None:
820
+ return FunctionExpression(func.substring(self.expression, start, length))
821
+ return FunctionExpression(func.substring(self.expression, start))
822
+
823
+ def regexp_replace(self, pattern: str, replacement: str) -> "FunctionExpression":
824
+ return FunctionExpression(func.regexp_replace(self.expression, pattern, replacement))
825
+
826
+ def position(self, substring: str) -> "FunctionExpression":
827
+ return FunctionExpression(func.position(substring, self.expression))
828
+
829
+ def split_part(self, delimiter: str, field: int) -> "FunctionExpression":
830
+ return FunctionExpression(func.split_part(self.expression, delimiter, field))
831
+
832
+ def reverse(self) -> "FunctionExpression":
833
+ return FunctionExpression(func.reverse(self.expression))
834
+
835
+ def md5(self) -> "FunctionExpression":
836
+ return FunctionExpression(func.md5(self.expression))
837
+
838
+ def concat(self, *args) -> "FunctionExpression":
839
+ return FunctionExpression(func.concat(self.expression, *args))
840
+
841
+ def left(self, length: int) -> "FunctionExpression":
842
+ return FunctionExpression(func.left(self.expression, length))
843
+
844
+ def right(self, length: int) -> "FunctionExpression":
845
+ return FunctionExpression(func.right(self.expression, length))
846
+
847
+ def lpad(self, length: int, fill_char: str = " ") -> "FunctionExpression":
848
+ return FunctionExpression(func.lpad(self.expression, length, fill_char))
849
+
850
+ def rpad(self, length: int, fill_char: str = " ") -> "FunctionExpression":
851
+ return FunctionExpression(func.rpad(self.expression, length, fill_char))
852
+
853
+ def ltrim(self, chars: str | None = None) -> "FunctionExpression":
854
+ if chars:
855
+ return FunctionExpression(func.ltrim(self.expression, chars))
856
+ return FunctionExpression(func.ltrim(self.expression))
857
+
858
+ def rtrim(self, chars: str | None = None) -> "FunctionExpression":
859
+ if chars:
860
+ return FunctionExpression(func.rtrim(self.expression, chars))
861
+ return FunctionExpression(func.rtrim(self.expression))
862
+
863
+ def replace(self, old: str, new: str) -> "FunctionExpression":
864
+ return FunctionExpression(func.replace(self.expression, old, new))
865
+
866
+ # === Numeric Functions ===
867
+
868
+ def abs(self) -> "FunctionExpression":
869
+ return FunctionExpression(func.abs(self.expression))
870
+
871
+ def round(self, precision: int = 0) -> "FunctionExpression":
872
+ return FunctionExpression(func.round(self.expression, precision))
873
+
874
+ def ceil(self) -> "FunctionExpression":
875
+ return FunctionExpression(func.ceil(self.expression))
876
+
877
+ def floor(self) -> "FunctionExpression":
878
+ return FunctionExpression(func.floor(self.expression))
879
+
880
+ def sqrt(self) -> "FunctionExpression":
881
+ return FunctionExpression(func.sqrt(self.expression))
882
+
883
+ def power(self, exponent) -> "FunctionExpression":
884
+ return FunctionExpression(func.power(self.expression, exponent))
885
+
886
+ def mod(self, divisor) -> "FunctionExpression":
887
+ return FunctionExpression(func.mod(self.expression, divisor))
888
+
889
+ def sign(self) -> "FunctionExpression":
890
+ return FunctionExpression(func.sign(self.expression))
891
+
892
+ def trunc(self, precision: int = 0) -> "FunctionExpression":
893
+ return FunctionExpression(func.trunc(self.expression, precision))
894
+
895
+ def exp(self) -> "FunctionExpression":
896
+ return FunctionExpression(func.exp(self.expression))
897
+
898
+ def ln(self) -> "FunctionExpression":
899
+ return FunctionExpression(func.ln(self.expression))
900
+
901
+ def log(self, base: int = 10) -> "FunctionExpression":
902
+ return FunctionExpression(func.log(base, self.expression))
903
+
904
+ # === Aggregate Functions ===
905
+
906
+ def sum(self) -> "FunctionExpression":
907
+ return FunctionExpression(func.sum(self.expression))
908
+
909
+ def avg(self) -> "FunctionExpression":
910
+ return FunctionExpression(func.avg(self.expression))
911
+
912
+ def max(self) -> "FunctionExpression":
913
+ return FunctionExpression(func.max(self.expression))
914
+
915
+ def min(self) -> "FunctionExpression":
916
+ return FunctionExpression(func.min(self.expression))
917
+
918
+ def count(self) -> "FunctionExpression":
919
+ return FunctionExpression(func.count(self.expression))
920
+
921
+ def count_distinct(self) -> "FunctionExpression":
922
+ return FunctionExpression(func.count(func.distinct(self.expression)))
923
+
924
+ def distinct(self) -> "FunctionExpression":
925
+ return FunctionExpression(func.distinct(self.expression))
926
+
927
+ # === Date/Time Functions ===
928
+
929
+ def year(self) -> "FunctionExpression":
930
+ return FunctionExpression(func.extract("year", self.expression))
931
+
932
+ def month(self) -> "FunctionExpression":
933
+ return FunctionExpression(func.extract("month", self.expression))
934
+
935
+ def day(self) -> "FunctionExpression":
936
+ return FunctionExpression(func.extract("day", self.expression))
937
+
938
+ def hour(self) -> "FunctionExpression":
939
+ return FunctionExpression(func.extract("hour", self.expression))
940
+
941
+ def minute(self) -> "FunctionExpression":
942
+ return FunctionExpression(func.extract("minute", self.expression))
943
+
944
+ def extract(self, field: str) -> "FunctionExpression":
945
+ return FunctionExpression(func.extract(field, self.expression))
946
+
947
+ def date_trunc(self, precision: str) -> "FunctionExpression":
948
+ return FunctionExpression(func.date_trunc(precision, self.expression))
949
+
950
+ def age_in_years(self) -> "FunctionExpression":
951
+ return FunctionExpression(func.extract("year", func.age(func.now(), self.expression)))
952
+
953
+ def age_in_months(self) -> "FunctionExpression":
954
+ return FunctionExpression(func.extract("month", func.age(func.now(), self.expression)))
955
+
956
+ def days_between(self, end_date) -> "FunctionExpression":
957
+ return FunctionExpression(func.extract("day", func.age(end_date, self.expression)))
958
+
959
+ def to_char(self, format_str: str) -> "FunctionExpression":
960
+ return FunctionExpression(func.to_char(self.expression, format_str))
961
+
962
+ def add_days(self, days: int) -> "FunctionExpression":
963
+ return FunctionExpression(self.expression + func.interval(f"{days} days"))
964
+
965
+ # === General Functions ===
966
+
967
+ def cast(self, type_: str, **kwargs) -> "FunctionExpression":
968
+ from .fields import create_type_instance
969
+
970
+ sqlalchemy_type = create_type_instance(type_, kwargs)
971
+ return FunctionExpression(func.cast(self.expression, sqlalchemy_type))
972
+
973
+ def coalesce(self, *values) -> "FunctionExpression":
974
+ return FunctionExpression(func.coalesce(self.expression, *values))
975
+
976
+ def nullif(self, value) -> "FunctionExpression":
977
+ return FunctionExpression(func.nullif(self.expression, value))
978
+
979
+ def case(self, *conditions, else_=None) -> "FunctionExpression": # noqa
980
+ if len(conditions) == 1 and isinstance(conditions[0], dict):
981
+ cases = list(conditions[0].items())
982
+ else:
983
+ cases = conditions
984
+ return FunctionExpression(func.case(*cases, else_=else_))
985
+
986
+ def greatest(self, *args) -> "FunctionExpression":
987
+ return FunctionExpression(func.greatest(self.expression, *args))
988
+
989
+ def least(self, *args) -> "FunctionExpression":
990
+ return FunctionExpression(func.least(self.expression, *args))
991
+
992
+ # === SQLAlchemy Integration ===
993
+
994
+ def label(self, name: str):
995
+ return self.expression.label(name)
996
+
997
+ def asc(self):
998
+ return self.expression.asc()
999
+
1000
+ def desc(self):
1001
+ return self.expression.desc()
1002
+
1003
+ # === Comparison Operators ===
1004
+
1005
+ def __eq__(self, other) -> ColumnElement[bool]: # type: ignore[reportIncompatibleMethodOverride]
1006
+ return self.expression == other # noqa
1007
+
1008
+ def __ne__(self, other) -> ColumnElement[bool]: # type: ignore[reportIncompatibleMethodOverride]
1009
+ return self.expression != other # noqa
1010
+
1011
+ def __lt__(self, other) -> ColumnElement[bool]:
1012
+ return self.expression < other # noqa
1013
+
1014
+ def __le__(self, other) -> ColumnElement[bool]:
1015
+ return self.expression <= other # noqa
1016
+
1017
+ def __gt__(self, other) -> ColumnElement[bool]:
1018
+ return self.expression > other # noqa
1019
+
1020
+ def __ge__(self, other) -> ColumnElement[bool]:
1021
+ return self.expression >= other # noqa
1022
+
1023
+ def like(self, pattern: str) -> ColumnElement[bool]:
1024
+ return self.expression.like(pattern)
1025
+
1026
+ def ilike(self, pattern: str) -> ColumnElement[bool]:
1027
+ return self.expression.ilike(pattern)
1028
+
1029
+ def not_like(self, pattern: str) -> ColumnElement[bool]:
1030
+ return ~self.expression.like(pattern)
1031
+
1032
+ def not_ilike(self, pattern: str) -> ColumnElement[bool]:
1033
+ return ~self.expression.ilike(pattern)
1034
+
1035
+ def between(self, min_val, max_val) -> ColumnElement[bool]:
1036
+ return self.expression.between(min_val, max_val)
1037
+
1038
+ def in_(self, values) -> ColumnElement[bool]:
1039
+ # Auto-wrap SubqueryExpression in list
1040
+ if isinstance(values, SubqueryExpression):
1041
+ values = [values]
1042
+ return self.expression.in_(values)
1043
+
1044
+ def not_in(self, values) -> ColumnElement[bool]:
1045
+ # Auto-wrap SubqueryExpression in list
1046
+ if isinstance(values, SubqueryExpression):
1047
+ values = [values]
1048
+ return ~self.expression.in_(values)
1049
+
1050
+ def is_(self, other) -> ColumnElement[bool]:
1051
+ return self.expression.is_(other)
1052
+
1053
+ def is_not(self, other) -> ColumnElement[bool]:
1054
+ return self.expression.is_not(other)