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.
- sqlobjects/__init__.py +38 -0
- sqlobjects/config.py +519 -0
- sqlobjects/database.py +586 -0
- sqlobjects/exceptions.py +538 -0
- sqlobjects/expressions.py +1054 -0
- sqlobjects/fields.py +1866 -0
- sqlobjects/history.py +101 -0
- sqlobjects/metadata.py +1130 -0
- sqlobjects/model.py +1009 -0
- sqlobjects/objects.py +812 -0
- sqlobjects/queries.py +1059 -0
- sqlobjects/relations.py +843 -0
- sqlobjects/session.py +389 -0
- sqlobjects/signals.py +464 -0
- sqlobjects/utils/__init__.py +5 -0
- sqlobjects/utils/naming.py +53 -0
- sqlobjects/utils/pattern.py +644 -0
- sqlobjects/validators.py +294 -0
- sqlobjects-0.1.0.dist-info/METADATA +29 -0
- sqlobjects-0.1.0.dist-info/RECORD +23 -0
- sqlobjects-0.1.0.dist-info/WHEEL +5 -0
- sqlobjects-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlobjects-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|