oxyde-postgres 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.
Files changed (66) hide show
  1. oxyde/__init__.py +151 -0
  2. oxyde/_msgpack.py +3 -0
  3. oxyde/cli/__init__.py +7 -0
  4. oxyde/cli/app.py +107 -0
  5. oxyde/cli/codegen.py +66 -0
  6. oxyde/cli/config.py +84 -0
  7. oxyde/cli/migrations.py +536 -0
  8. oxyde/codegen/__init__.py +15 -0
  9. oxyde/codegen/stub_generator.py +1229 -0
  10. oxyde/core/__init__.py +90 -0
  11. oxyde/core/column_types.py +367 -0
  12. oxyde/core/ir.py +380 -0
  13. oxyde/core/types.py +69 -0
  14. oxyde/core/wrapper.py +242 -0
  15. oxyde/db/__init__.py +230 -0
  16. oxyde/db/pool.py +305 -0
  17. oxyde/db/registry.py +113 -0
  18. oxyde/db/schema.py +51 -0
  19. oxyde/db/transaction.py +392 -0
  20. oxyde/exceptions.py +89 -0
  21. oxyde/migrations/__init__.py +49 -0
  22. oxyde/migrations/config.py +187 -0
  23. oxyde/migrations/context.py +598 -0
  24. oxyde/migrations/executor.py +456 -0
  25. oxyde/migrations/extract.py +316 -0
  26. oxyde/migrations/generator.py +534 -0
  27. oxyde/migrations/replay.py +367 -0
  28. oxyde/migrations/squash.py +109 -0
  29. oxyde/migrations/tracker.py +162 -0
  30. oxyde/migrations/utils.py +145 -0
  31. oxyde/models/__init__.py +83 -0
  32. oxyde/models/base.py +871 -0
  33. oxyde/models/decorators.py +111 -0
  34. oxyde/models/field.py +204 -0
  35. oxyde/models/lookups.py +667 -0
  36. oxyde/models/metadata.py +174 -0
  37. oxyde/models/registry.py +170 -0
  38. oxyde/models/serializers.py +144 -0
  39. oxyde/models/utils.py +116 -0
  40. oxyde/py.typed +0 -0
  41. oxyde/queries/__init__.py +79 -0
  42. oxyde/queries/aggregates.py +145 -0
  43. oxyde/queries/base.py +249 -0
  44. oxyde/queries/conditions.py +92 -0
  45. oxyde/queries/expressions.py +171 -0
  46. oxyde/queries/insert.py +124 -0
  47. oxyde/queries/joins.py +66 -0
  48. oxyde/queries/manager.py +409 -0
  49. oxyde/queries/mixins/__init__.py +68 -0
  50. oxyde/queries/mixins/aggregation.py +335 -0
  51. oxyde/queries/mixins/debug.py +154 -0
  52. oxyde/queries/mixins/execution.py +701 -0
  53. oxyde/queries/mixins/filtering.py +144 -0
  54. oxyde/queries/mixins/joining.py +160 -0
  55. oxyde/queries/mixins/mutation.py +508 -0
  56. oxyde/queries/mixins/pagination.py +145 -0
  57. oxyde/queries/q.py +250 -0
  58. oxyde/queries/raw.py +114 -0
  59. oxyde/queries/select.py +345 -0
  60. oxyde/queries/typed.py +213 -0
  61. oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
  62. oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
  63. oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
  64. oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
  65. oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,145 @@
1
+ """SQL aggregate and scalar functions for SELECT queries.
2
+
3
+ This module provides Python wrappers for SQL aggregate functions
4
+ used with annotate() and direct aggregate queries.
5
+
6
+ Aggregate Functions:
7
+ Count(field="*", distinct=False) → COUNT(*) or COUNT(DISTINCT field)
8
+ Sum(field) → SUM(field)
9
+ Avg(field) → AVG(field)
10
+ Max(field) → MAX(field)
11
+ Min(field) → MIN(field)
12
+
13
+ Scalar Functions:
14
+ Concat(*fields, separator="") → Concatenate strings
15
+ Coalesce(*fields) → First non-NULL value
16
+
17
+ Raw SQL:
18
+ RawSQL(sql) → Inject raw SQL expression
19
+
20
+ Usage with annotate():
21
+ # Add computed columns to query results
22
+ query = User.objects.annotate(
23
+ post_count=Count("id"),
24
+ total_score=Sum("score"),
25
+ )
26
+
27
+ Usage with group_by():
28
+ query = Order.objects.annotate(
29
+ total=Sum("amount"),
30
+ ).group_by("customer_id")
31
+
32
+ Direct aggregates (execute immediately):
33
+ total = await User.objects.filter(active=True).count()
34
+ avg_age = await User.objects.avg("age")
35
+ max_score = await User.objects.max("score")
36
+
37
+ IR Format:
38
+ Count("id").to_ir() returns:
39
+ {"func": "count", "field": "id"}
40
+
41
+ Count("id", distinct=True).to_ir() returns:
42
+ {"func": "count", "field": "id", "distinct": True}
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from typing import Any
48
+
49
+
50
+ class Aggregate:
51
+ """Base class for aggregate functions."""
52
+
53
+ def __init__(self, field: str, *, distinct: bool = False):
54
+ self.field = field
55
+ self.distinct = distinct
56
+ self.func_name = self.__class__.__name__.lower()
57
+
58
+ def to_ir(self) -> dict[str, Any]:
59
+ """Convert to IR representation."""
60
+ ir: dict[str, Any] = {
61
+ "func": self.func_name,
62
+ "field": self.field,
63
+ }
64
+ if self.distinct:
65
+ ir["distinct"] = True
66
+ return ir
67
+
68
+
69
+ class Count(Aggregate):
70
+ """COUNT aggregate function."""
71
+
72
+ def __init__(self, field: str = "*", *, distinct: bool = False):
73
+ super().__init__(field, distinct=distinct)
74
+
75
+
76
+ class Sum(Aggregate):
77
+ """SUM aggregate function."""
78
+
79
+
80
+ class Avg(Aggregate):
81
+ """AVG aggregate function."""
82
+
83
+
84
+ class Max(Aggregate):
85
+ """MAX aggregate function."""
86
+
87
+
88
+ class Min(Aggregate):
89
+ """MIN aggregate function."""
90
+
91
+
92
+ class Concat(Aggregate):
93
+ """String concatenation."""
94
+
95
+ def __init__(self, *fields: str, separator: str = ""):
96
+ self.fields = fields
97
+ self.separator = separator
98
+ self.func_name = "concat"
99
+
100
+ def to_ir(self) -> dict[str, Any]:
101
+ return {
102
+ "func": "concat",
103
+ "fields": list(self.fields),
104
+ "separator": self.separator,
105
+ }
106
+
107
+
108
+ class Coalesce(Aggregate):
109
+ """COALESCE function - returns first non-NULL value."""
110
+
111
+ def __init__(self, *fields: str):
112
+ self.fields = fields
113
+ self.func_name = "coalesce"
114
+
115
+ def to_ir(self) -> dict[str, Any]:
116
+ return {
117
+ "func": "coalesce",
118
+ "fields": list(self.fields),
119
+ }
120
+
121
+
122
+ class RawSQL:
123
+ """Raw SQL expression."""
124
+
125
+ def __init__(self, sql: str):
126
+ self.sql = sql
127
+
128
+ def to_ir(self) -> dict[str, Any]:
129
+ return {
130
+ "type": "raw",
131
+ "sql": self.sql,
132
+ }
133
+
134
+
135
+ __all__ = [
136
+ "Aggregate",
137
+ "Count",
138
+ "Sum",
139
+ "Avg",
140
+ "Max",
141
+ "Min",
142
+ "Concat",
143
+ "Coalesce",
144
+ "RawSQL",
145
+ ]
oxyde/queries/base.py ADDED
@@ -0,0 +1,249 @@
1
+ """Shared utilities, protocols, and helpers for query builders.
2
+
3
+ This module provides common functionality used across all query types
4
+ (SELECT, INSERT, UPDATE, DELETE).
5
+
6
+ Protocol:
7
+ SupportsExecute: Interface for objects that can execute queries.
8
+ - AsyncDatabase implements this
9
+ - AsyncTransaction implements this
10
+ Any object with async execute(ir: dict) -> bytes works.
11
+
12
+ Type Variables:
13
+ TQuery: TypeVar bound to Query for method chaining with correct return types.
14
+
15
+ Helper Functions:
16
+ _model_key(model_class) -> str:
17
+ Returns "{module}.{qualname}" for model identification.
18
+
19
+ _resolve_registered_model(key) -> type[Model]:
20
+ Look up model by key or class name in registry.
21
+
22
+ _primary_key_meta(model_class) -> ColumnMeta:
23
+ Get metadata for primary key field.
24
+
25
+ _collect_model_columns(model_class) -> list[(field, column)]:
26
+ Get all (field_name, db_column) pairs.
27
+
28
+ _map_values_to_columns(model_class, values) -> dict:
29
+ Convert field names to database column names in a dict.
30
+
31
+ _resolve_execution_client(using, client) -> SupportsExecute:
32
+ Resolve connection from using="alias" or client object.
33
+ Checks active transaction first.
34
+
35
+ _resolve_pool_name(using, client) -> str:
36
+ Get pool name for schema/explain operations.
37
+
38
+ Caching:
39
+ _TYPE_ADAPTER_CACHE: Thread-safe cache for Pydantic TypeAdapter instances.
40
+ Used for efficient list[Model] validation in fetch_models().
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import threading
46
+ from typing import (
47
+ TYPE_CHECKING,
48
+ Any,
49
+ Protocol,
50
+ TypeVar,
51
+ runtime_checkable,
52
+ )
53
+
54
+ from pydantic import TypeAdapter
55
+
56
+ from oxyde.core import wrapper as _core
57
+ from oxyde.db.pool import AsyncDatabase
58
+ from oxyde.db.registry import get_connection
59
+ from oxyde.db.transaction import AsyncTransaction, get_active_transaction
60
+ from oxyde.exceptions import (
61
+ CheckViolationError,
62
+ FieldLookupError,
63
+ ForeignKeyViolationError,
64
+ IntegrityError,
65
+ ManagerError,
66
+ NotNullViolationError,
67
+ UniqueViolationError,
68
+ )
69
+ from oxyde.models.metadata import ColumnMeta
70
+ from oxyde.models.registry import registered_tables
71
+ from oxyde.models.serializers import _get_virtual_fields
72
+
73
+ if TYPE_CHECKING:
74
+ from oxyde.models.base import Model
75
+ from oxyde.queries.select import Query
76
+
77
+ # Global cache for TypeAdapter instances (thread-safe)
78
+ _TYPE_ADAPTER_CACHE: dict[type, TypeAdapter] = {}
79
+ _TYPE_ADAPTER_LOCK = threading.Lock()
80
+
81
+ # Core exception → oxyde exception, most specific first (core classes form
82
+ # the same hierarchy, so IntegrityError must be checked last).
83
+ _CORE_ERROR_MAP: tuple[tuple[type[BaseException], type[IntegrityError]], ...] = (
84
+ (_core.UniqueViolationError, UniqueViolationError),
85
+ (_core.ForeignKeyViolationError, ForeignKeyViolationError),
86
+ (_core.NotNullViolationError, NotNullViolationError),
87
+ (_core.CheckViolationError, CheckViolationError),
88
+ (_core.IntegrityError, IntegrityError),
89
+ )
90
+
91
+
92
+ def _translate_core_error(exc: BaseException) -> ManagerError | None:
93
+ """Map a Rust-core exception onto the oxyde hierarchy, if it is one.
94
+
95
+ Classification is done by the driver (sqlx ErrorKind) — no message-text
96
+ matching happens anywhere on the Python side.
97
+ """
98
+ for core_cls, oxyde_cls in _CORE_ERROR_MAP:
99
+ if isinstance(exc, core_cls):
100
+ return oxyde_cls(str(exc))
101
+ return None
102
+
103
+
104
+ def _model_key(model_class: type[Model]) -> str:
105
+ """Return fully qualified identifier for a model."""
106
+ return f"{model_class.__module__}.{model_class.__qualname__}"
107
+
108
+
109
+ @runtime_checkable
110
+ class SupportsExecute(Protocol):
111
+ """Protocol for objects that can execute queries."""
112
+
113
+ async def execute(self, ir: dict[str, Any]) -> bytes: ...
114
+
115
+
116
+ # TypeVar for query builders (covariant for subclassing)
117
+ TQuery = TypeVar("TQuery", bound="Query")
118
+
119
+
120
+ def _resolve_registered_model(model_key: str) -> type[Model]:
121
+ """Resolve a model by its fully qualified key or simple class name."""
122
+ tables = registered_tables()
123
+ # Try exact match first
124
+ model = tables.get(model_key)
125
+ if model is not None:
126
+ return model
127
+ # Fallback: search by simple class name (for forward refs and test classes)
128
+ for key, table_model in tables.items():
129
+ if key.endswith(f".{model_key}") or table_model.__name__ == model_key:
130
+ return table_model
131
+ raise FieldLookupError(f"Related model '{model_key}' is not registered")
132
+
133
+
134
+ def _primary_key_meta(model_class: type[Model]) -> ColumnMeta:
135
+ """Get primary key metadata from model (uses cached pk_field)."""
136
+ pk = model_class._db_meta.pk_field
137
+ if pk is not None:
138
+ return model_class._db_meta.field_metadata[pk]
139
+ raise FieldLookupError(f"{model_class.__name__} has no primary key field")
140
+
141
+
142
+ def _build_column_types(model_class: type[Model]) -> dict[str, dict] | None:
143
+ """Get cached column_types mapping (ColumnTypeSpec dicts) from model metadata."""
144
+ return model_class._db_meta.column_types
145
+
146
+
147
+ def _build_column_db_types(model_class: type[Model]) -> dict[str, str] | None:
148
+ """Get cached db_type overrides by column from model metadata."""
149
+ return model_class._db_meta.column_db_types
150
+
151
+
152
+ def _collect_model_columns(model_class: type[Model]) -> list[tuple[str, str]]:
153
+ """Collect all (field_name, db_column) pairs from model.
154
+
155
+ Excludes virtual fields (reverse FK, M2M, FK model fields) using
156
+ _get_virtual_fields() as the single source of truth.
157
+ """
158
+ virtual = _get_virtual_fields(model_class)
159
+ return [
160
+ (meta.name, meta.db_column)
161
+ for meta in model_class._db_meta.field_metadata.values()
162
+ if meta.name not in virtual
163
+ ]
164
+
165
+
166
+ def _map_values_to_columns(
167
+ model_class: type[Model],
168
+ values: dict[str, Any],
169
+ ) -> dict[str, Any]:
170
+ """Map field names to database column names."""
171
+ metadata = model_class._db_meta.field_metadata
172
+ mapped: dict[str, Any] = {}
173
+ for key, value in values.items():
174
+ meta = metadata.get(key)
175
+ column = meta.db_column if meta else key
176
+ mapped[column] = value
177
+ return mapped
178
+
179
+
180
+ async def _resolve_execution_client(
181
+ using: str | None,
182
+ client: SupportsExecute | None,
183
+ ) -> SupportsExecute:
184
+ """Resolve the execution client from using/client parameters."""
185
+ if client is not None and using is not None:
186
+ raise ManagerError("Provide either 'client' or 'using', not both")
187
+ if client is not None:
188
+ return client
189
+
190
+ alias = using or "default"
191
+ active_tx = get_active_transaction(alias)
192
+ if active_tx is not None:
193
+ return active_tx
194
+ return await get_connection(alias)
195
+
196
+
197
+ def _resolve_pool_name(
198
+ using: str | None,
199
+ client: SupportsExecute | None,
200
+ ) -> str:
201
+ """
202
+ Resolve pool name from using or client parameters.
203
+
204
+ Args:
205
+ using: Database alias
206
+ client: Database client (AsyncDatabase or AsyncTransaction)
207
+
208
+ Returns:
209
+ Pool name string
210
+
211
+ Raises:
212
+ ManagerError: If both using and client are provided
213
+ """
214
+ if client is not None and using is not None:
215
+ raise ManagerError("Provide either 'client' or 'using', not both")
216
+
217
+ if client is not None:
218
+ # Try to get pool name from client
219
+ if isinstance(client, AsyncDatabase):
220
+ return client.name
221
+ elif isinstance(client, AsyncTransaction):
222
+ return client._database.name
223
+ else:
224
+ # Unknown client type, try to get name attribute
225
+ if hasattr(client, "name"):
226
+ return str(client.name)
227
+ elif hasattr(client, "_database") and hasattr(client._database, "name"):
228
+ return str(client._database.name)
229
+ else:
230
+ ctype = type(client).__name__
231
+ raise ManagerError(f"Cannot determine pool name from client: {ctype}")
232
+
233
+ # Use alias
234
+ return using or "default"
235
+
236
+
237
+ __all__ = [
238
+ "SupportsExecute",
239
+ "TQuery",
240
+ "_model_key",
241
+ "_resolve_registered_model",
242
+ "_primary_key_meta",
243
+ "_collect_model_columns",
244
+ "_map_values_to_columns",
245
+ "_resolve_execution_client",
246
+ "_resolve_pool_name",
247
+ "_TYPE_ADAPTER_CACHE",
248
+ "_TYPE_ADAPTER_LOCK",
249
+ ]
@@ -0,0 +1,92 @@
1
+ """Condition class representing a single WHERE clause comparison.
2
+
3
+ Condition is the leaf node of the filter tree. It represents a single
4
+ comparison like "age >= 18" or "name LIKE '%alice%'".
5
+
6
+ Attributes:
7
+ field: Python field name (e.g., "age")
8
+ operator: SQL operator ("=", ">", "LIKE", "IN", "IS NULL", etc.)
9
+ value: Comparison value (can be None for IS NULL)
10
+ column: Database column name (may differ from field if db_column set)
11
+ escape: Optional LIKE escape character for pattern filters
12
+
13
+ Operators:
14
+ "=" → Equality
15
+ ">", ">=" → Greater than
16
+ "<", "<=" → Less than
17
+ "LIKE" → Pattern match (case-sensitive)
18
+ "ILIKE" → Pattern match (case-insensitive, PostgreSQL)
19
+ "IN" → Value in list
20
+ "BETWEEN" → Range (value is [low, high])
21
+ "IS NULL" → NULL check
22
+ "IS NOT NULL" → NOT NULL check
23
+ "ARRAY_CONTAINS" → Array holds every given element
24
+ "ARRAY_CONTAINED_BY" → Every stored element is among the given ones
25
+ "ARRAY_OVERLAP" → At least one element in common
26
+
27
+ Creation:
28
+ Conditions are typically created via Field comparison operators:
29
+ User.name == "Alice" → Condition("name", "=", "Alice")
30
+ User.age >= 18 → Condition("age", ">=", 18)
31
+
32
+ Or via lookups module:
33
+ _build_lookup_conditions(User, "name", "contains", "alice", meta)
34
+ → Condition("name", "LIKE", "%alice%")
35
+
36
+ IR Format:
37
+ to_ir() returns dict for Rust codec. For a LIKE condition with an
38
+ escape character:
39
+ {
40
+ "type": "condition",
41
+ "field": "name",
42
+ "op": "LIKE",
43
+ "value": "A\\_%",
44
+ "column": "name",
45
+ "escape": "\\"
46
+ }
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ from typing import Any
52
+
53
+ from oxyde.core import ir
54
+ from oxyde.core.ir import FilterNode
55
+ from oxyde.core.types import serialize_value
56
+
57
+
58
+ class Condition:
59
+ """Represents a filter condition."""
60
+
61
+ def __init__(
62
+ self,
63
+ field: str,
64
+ operator: str,
65
+ value: Any,
66
+ *,
67
+ column: str | None = None,
68
+ escape: str | None = None,
69
+ ):
70
+ self.field = field
71
+ self.operator = operator
72
+ self.value = value
73
+ self.column = column or field
74
+ self.escape = escape
75
+
76
+ def to_ir(self) -> FilterNode:
77
+ """Convert to IR format expected by the Rust codec.
78
+
79
+ Values are serialized via TYPE_REGISTRY so that date, UUID, Decimal
80
+ etc. are converted to msgpack-safe representations before hitting
81
+ the IR dict.
82
+ """
83
+ return ir.filter_condition(
84
+ field=self.field,
85
+ operator=self.operator,
86
+ value=serialize_value(self.value),
87
+ column=self.column,
88
+ escape=self.escape,
89
+ )
90
+
91
+
92
+ __all__ = ["Condition"]
@@ -0,0 +1,171 @@
1
+ """F-expressions for database-side field references and arithmetic.
2
+
3
+ F() allows referencing database columns in UPDATE statements for atomic
4
+ operations that execute entirely in the database, avoiding race conditions.
5
+
6
+ Basic Usage:
7
+ F("price") → Reference column "price"
8
+ F("price") * 1.1 → price * 1.1
9
+ F("count") + 1 → count + 1
10
+ F("a") + F("b") → a + b
11
+
12
+ Supported Operations:
13
+ F("x") + y → Addition
14
+ F("x") - y → Subtraction
15
+ F("x") * y → Multiplication
16
+ F("x") / y → Division
17
+ -F("x") → Negation
18
+
19
+ Operands can be: F(), numbers, or other _Expression objects.
20
+
21
+ Use Cases:
22
+ # Atomic increment (no race condition)
23
+ await User.objects.filter(id=1).update(login_count=F("login_count") + 1)
24
+
25
+ # Percentage increase
26
+ await Product.objects.filter(category="sale").update(price=F("price") * 0.9)
27
+
28
+ # Combine columns
29
+ await Order.objects.update(total=F("price") * F("quantity"))
30
+
31
+ Internal Structure:
32
+ F("name") creates:
33
+ _Expression({"type": "column", "name": "name"})
34
+
35
+ F("x") + 5 creates:
36
+ _Expression({
37
+ "type": "op",
38
+ "op": "add",
39
+ "lhs": {"type": "column", "name": "x"},
40
+ "rhs": {"type": "value", "value": 5}
41
+ })
42
+
43
+ _serialize_value_for_ir() wraps expressions in {"__expr__": ...}
44
+ for the Rust IR parser to recognize them. Literal nodes carry an
45
+ optional value_type ColumnTypeSpec dict ({"kind": "decimal"}).
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from typing import Any
51
+
52
+ from oxyde.core.column_types import spec_for_literal
53
+ from oxyde.core.types import serialize_value as _serialize_type_value
54
+
55
+
56
+ class _Expression:
57
+ """Represents an arithmetic expression usable in mutation statements."""
58
+
59
+ __slots__ = ("_expr",)
60
+
61
+ def __init__(self, expr: dict[str, Any]):
62
+ self._expr = expr
63
+
64
+ def _binary(self, op: str, other: Any) -> _Expression:
65
+ rhs = _coerce_expression(other)
66
+ return _Expression(
67
+ {
68
+ "type": "op",
69
+ "op": op,
70
+ "lhs": self._expr,
71
+ "rhs": rhs._expr,
72
+ }
73
+ )
74
+
75
+ def __add__(self, other: Any) -> _Expression:
76
+ return self._binary("add", other)
77
+
78
+ def __radd__(self, other: Any) -> _Expression:
79
+ return _coerce_expression(other)._binary("add", self)
80
+
81
+ def __sub__(self, other: Any) -> _Expression:
82
+ return self._binary("sub", other)
83
+
84
+ def __rsub__(self, other: Any) -> _Expression:
85
+ return _coerce_expression(other)._binary("sub", self)
86
+
87
+ def __mul__(self, other: Any) -> _Expression:
88
+ return self._binary("mul", other)
89
+
90
+ def __rmul__(self, other: Any) -> _Expression:
91
+ return _coerce_expression(other)._binary("mul", self)
92
+
93
+ def __truediv__(self, other: Any) -> _Expression:
94
+ return self._binary("div", other)
95
+
96
+ def __rtruediv__(self, other: Any) -> _Expression:
97
+ return _coerce_expression(other)._binary("div", self)
98
+
99
+ def __neg__(self) -> _Expression:
100
+ return _Expression({"type": "neg", "expr": self._expr})
101
+
102
+
103
+ def _coerce_expression(value: Any) -> _Expression:
104
+ """Coerce a value to an _Expression.
105
+
106
+ For known Python types, attach a `value_type` ColumnTypeSpec dict.
107
+ The Rust side uses it to recover typed bindings (Decimal, UUID,
108
+ datetime, ...) that msgpack-encode as strings via `serialize_value`.
109
+ """
110
+ if isinstance(value, _Expression):
111
+ return value
112
+ if isinstance(value, F):
113
+ return value._expression
114
+ node: dict[str, Any] = {"type": "value", "value": value}
115
+ spec = spec_for_literal(type(value))
116
+ if spec is not None:
117
+ node["value_type"] = spec
118
+ return _Expression(node)
119
+
120
+
121
+ class F:
122
+ """Field expression helper for arithmetic updates."""
123
+
124
+ __slots__ = ("name", "_expression")
125
+
126
+ def __init__(self, name: str):
127
+ self.name = name
128
+ self._expression = _Expression({"type": "column", "name": name})
129
+
130
+ def __add__(self, other: Any) -> _Expression:
131
+ return self._expression.__add__(other)
132
+
133
+ def __radd__(self, other: Any) -> _Expression:
134
+ return self._expression.__radd__(other)
135
+
136
+ def __sub__(self, other: Any) -> _Expression:
137
+ return self._expression.__sub__(other)
138
+
139
+ def __rsub__(self, other: Any) -> _Expression:
140
+ return self._expression.__rsub__(other)
141
+
142
+ def __mul__(self, other: Any) -> _Expression:
143
+ return self._expression.__mul__(other)
144
+
145
+ def __rmul__(self, other: Any) -> _Expression:
146
+ return self._expression.__rmul__(other)
147
+
148
+ def __truediv__(self, other: Any) -> _Expression:
149
+ return self._expression.__truediv__(other)
150
+
151
+ def __rtruediv__(self, other: Any) -> _Expression:
152
+ return self._expression.__rtruediv__(other)
153
+
154
+ def __neg__(self) -> _Expression:
155
+ return -self._expression
156
+
157
+
158
+ def _serialize_value_for_ir(value: Any) -> Any:
159
+ """Serialize values for IR, handling expressions and non-msgpack types."""
160
+ if isinstance(value, _Expression):
161
+ return {"__expr__": value._expr}
162
+ if isinstance(value, F):
163
+ return {"__expr__": value._expression._expr}
164
+ if isinstance(value, list):
165
+ return [_serialize_value_for_ir(item) for item in value]
166
+ if isinstance(value, dict):
167
+ return {key: _serialize_value_for_ir(val) for key, val in value.items()}
168
+ return _serialize_type_value(value)
169
+
170
+
171
+ __all__ = ["F", "_Expression", "_coerce_expression", "_serialize_value_for_ir"]