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.
- oxyde/__init__.py +151 -0
- oxyde/_msgpack.py +3 -0
- oxyde/cli/__init__.py +7 -0
- oxyde/cli/app.py +107 -0
- oxyde/cli/codegen.py +66 -0
- oxyde/cli/config.py +84 -0
- oxyde/cli/migrations.py +536 -0
- oxyde/codegen/__init__.py +15 -0
- oxyde/codegen/stub_generator.py +1229 -0
- oxyde/core/__init__.py +90 -0
- oxyde/core/column_types.py +367 -0
- oxyde/core/ir.py +380 -0
- oxyde/core/types.py +69 -0
- oxyde/core/wrapper.py +242 -0
- oxyde/db/__init__.py +230 -0
- oxyde/db/pool.py +305 -0
- oxyde/db/registry.py +113 -0
- oxyde/db/schema.py +51 -0
- oxyde/db/transaction.py +392 -0
- oxyde/exceptions.py +89 -0
- oxyde/migrations/__init__.py +49 -0
- oxyde/migrations/config.py +187 -0
- oxyde/migrations/context.py +598 -0
- oxyde/migrations/executor.py +456 -0
- oxyde/migrations/extract.py +316 -0
- oxyde/migrations/generator.py +534 -0
- oxyde/migrations/replay.py +367 -0
- oxyde/migrations/squash.py +109 -0
- oxyde/migrations/tracker.py +162 -0
- oxyde/migrations/utils.py +145 -0
- oxyde/models/__init__.py +83 -0
- oxyde/models/base.py +871 -0
- oxyde/models/decorators.py +111 -0
- oxyde/models/field.py +204 -0
- oxyde/models/lookups.py +667 -0
- oxyde/models/metadata.py +174 -0
- oxyde/models/registry.py +170 -0
- oxyde/models/serializers.py +144 -0
- oxyde/models/utils.py +116 -0
- oxyde/py.typed +0 -0
- oxyde/queries/__init__.py +79 -0
- oxyde/queries/aggregates.py +145 -0
- oxyde/queries/base.py +249 -0
- oxyde/queries/conditions.py +92 -0
- oxyde/queries/expressions.py +171 -0
- oxyde/queries/insert.py +124 -0
- oxyde/queries/joins.py +66 -0
- oxyde/queries/manager.py +409 -0
- oxyde/queries/mixins/__init__.py +68 -0
- oxyde/queries/mixins/aggregation.py +335 -0
- oxyde/queries/mixins/debug.py +154 -0
- oxyde/queries/mixins/execution.py +701 -0
- oxyde/queries/mixins/filtering.py +144 -0
- oxyde/queries/mixins/joining.py +160 -0
- oxyde/queries/mixins/mutation.py +508 -0
- oxyde/queries/mixins/pagination.py +145 -0
- oxyde/queries/q.py +250 -0
- oxyde/queries/raw.py +114 -0
- oxyde/queries/select.py +345 -0
- oxyde/queries/typed.py +213 -0
- oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
- oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
- oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
- oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
- oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
- oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
oxyde/db/transaction.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""Transaction management with Django-style atomic() context manager.
|
|
2
|
+
|
|
3
|
+
This module provides transaction support with automatic commit/rollback
|
|
4
|
+
and nested transaction handling via savepoints.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from oxyde.db import transaction
|
|
8
|
+
|
|
9
|
+
# Basic transaction
|
|
10
|
+
async with transaction.atomic():
|
|
11
|
+
await User.objects.create(name="Alice")
|
|
12
|
+
await Order.objects.create(user_id=1)
|
|
13
|
+
# Commits on exit, rolls back on exception
|
|
14
|
+
|
|
15
|
+
# Explicit rollback
|
|
16
|
+
async with transaction.atomic() as tx:
|
|
17
|
+
await User.objects.create(name="Alice")
|
|
18
|
+
tx.set_rollback() # Force rollback even without exception
|
|
19
|
+
|
|
20
|
+
# Nested transactions (savepoints)
|
|
21
|
+
async with transaction.atomic():
|
|
22
|
+
await User.objects.create(name="Alice")
|
|
23
|
+
try:
|
|
24
|
+
async with transaction.atomic():
|
|
25
|
+
await User.objects.create(name="Bob")
|
|
26
|
+
raise ValueError("oops")
|
|
27
|
+
except ValueError:
|
|
28
|
+
pass # Bob rolled back to savepoint, Alice still pending
|
|
29
|
+
# Alice commits
|
|
30
|
+
|
|
31
|
+
Classes:
|
|
32
|
+
AsyncTransaction:
|
|
33
|
+
Low-level transaction wrapper. Holds tx_id from Rust.
|
|
34
|
+
execute() sends queries through the transaction.
|
|
35
|
+
|
|
36
|
+
AtomicTransactionContext:
|
|
37
|
+
High-level context manager for atomic(). Manages nesting
|
|
38
|
+
via savepoints. Tracks depth per connection alias.
|
|
39
|
+
|
|
40
|
+
TransactionTimeoutError:
|
|
41
|
+
Raised when transaction exceeds configured timeout.
|
|
42
|
+
|
|
43
|
+
Functions:
|
|
44
|
+
atomic(using="default", timeout=None):
|
|
45
|
+
Create atomic context manager. Django-compatible API.
|
|
46
|
+
|
|
47
|
+
get_active_transaction(using="default"):
|
|
48
|
+
Get current transaction for connection (or None).
|
|
49
|
+
Used by QueryManager to route queries through active tx.
|
|
50
|
+
|
|
51
|
+
get_transaction_depth(using="default"):
|
|
52
|
+
Get atomic() depth for the connection alias and current task.
|
|
53
|
+
Returns 0 outside atomic(), 1 in the outermost block, and 2+ when nested.
|
|
54
|
+
|
|
55
|
+
ContextVar Storage:
|
|
56
|
+
_ACTIVE_TRANSACTIONS stores {alias: {transaction, depth, force_rollback, task}}
|
|
57
|
+
per async context. This allows concurrent requests to have separate
|
|
58
|
+
transaction state. The 'task' field tracks ownership via asyncio.current_task()
|
|
59
|
+
to prevent child tasks from inheriting parent transactions through ContextVar.
|
|
60
|
+
|
|
61
|
+
Savepoint Handling:
|
|
62
|
+
Nested atomic() calls create savepoints (sp_1, sp_2, ...).
|
|
63
|
+
Inner exception rolls back to savepoint, not entire transaction.
|
|
64
|
+
This matches Django/PostgreSQL behavior.
|
|
65
|
+
|
|
66
|
+
Rust Integration:
|
|
67
|
+
_begin_transaction(pool_name) → tx_id
|
|
68
|
+
_execute_in_transaction(pool_name, tx_id, ir_bytes) → bytes
|
|
69
|
+
_commit_transaction(tx_id)
|
|
70
|
+
_rollback_transaction(tx_id)
|
|
71
|
+
_create_savepoint(tx_id, name)
|
|
72
|
+
_rollback_to_savepoint(tx_id, name)
|
|
73
|
+
_release_savepoint(tx_id, name)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import asyncio
|
|
79
|
+
from contextvars import ContextVar
|
|
80
|
+
from datetime import timedelta
|
|
81
|
+
from typing import TYPE_CHECKING, Any
|
|
82
|
+
|
|
83
|
+
from oxyde._msgpack import msgpack
|
|
84
|
+
from oxyde.db.pool import _msgpack_encoder, _normalize_duration
|
|
85
|
+
from oxyde.db.registry import get_connection
|
|
86
|
+
|
|
87
|
+
if TYPE_CHECKING:
|
|
88
|
+
from oxyde.db.pool import AsyncDatabase
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
from oxyde.core import begin_transaction as _begin_transaction
|
|
92
|
+
from oxyde.core import commit_transaction as _commit_transaction
|
|
93
|
+
from oxyde.core import create_savepoint as _create_savepoint
|
|
94
|
+
from oxyde.core import execute_in_transaction as _execute_in_transaction
|
|
95
|
+
from oxyde.core import release_savepoint as _release_savepoint
|
|
96
|
+
from oxyde.core import rollback_to_savepoint as _rollback_to_savepoint
|
|
97
|
+
from oxyde.core import rollback_transaction as _rollback_transaction
|
|
98
|
+
except ImportError:
|
|
99
|
+
# Stub for when the Rust module is not built
|
|
100
|
+
async def _execute_in_transaction(
|
|
101
|
+
pool_name: str, tx_id: int, ir_bytes: bytes
|
|
102
|
+
) -> bytes:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def _begin_transaction(pool_name: str) -> int:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def _commit_transaction(tx_id: int) -> None:
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def _rollback_transaction(tx_id: int) -> None:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def _create_savepoint(tx_id: int, savepoint_name: str) -> None:
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def _rollback_to_savepoint(tx_id: int, savepoint_name: str) -> None:
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def _release_savepoint(tx_id: int, savepoint_name: str) -> None:
|
|
133
|
+
raise RuntimeError(
|
|
134
|
+
"Rust core module not available. Please install oxyde-postgres-core."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Active transactions per connection (ContextVar for async safety)
|
|
139
|
+
_ACTIVE_TRANSACTIONS: ContextVar[dict[str, dict[str, Any]]] = ContextVar(
|
|
140
|
+
"oxyde_active_transactions",
|
|
141
|
+
default={},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _get_owned_entry(using: str) -> dict[str, Any] | None:
|
|
146
|
+
"""Return the transaction entry only if it belongs to the current task."""
|
|
147
|
+
entry = _ACTIVE_TRANSACTIONS.get().get(using)
|
|
148
|
+
if not entry:
|
|
149
|
+
return None
|
|
150
|
+
try:
|
|
151
|
+
task = asyncio.current_task()
|
|
152
|
+
except RuntimeError:
|
|
153
|
+
# No running event loop (e.g. a worker thread with a copied context)
|
|
154
|
+
return None
|
|
155
|
+
if entry.get("task") is task:
|
|
156
|
+
return entry
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TransactionTimeoutError(TimeoutError):
|
|
161
|
+
"""Raised when a transaction exceeds the configured timeout."""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class AsyncTransaction:
|
|
165
|
+
"""Wrapper for executing queries within a database transaction."""
|
|
166
|
+
|
|
167
|
+
def __init__(self, database: AsyncDatabase, timeout: float | None = None):
|
|
168
|
+
self._database = database
|
|
169
|
+
self._tx_id: int | None = None
|
|
170
|
+
self._timeout = timeout
|
|
171
|
+
self._deadline: float | None = None
|
|
172
|
+
self._timed_out = False
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def id(self) -> int:
|
|
176
|
+
if self._tx_id is None:
|
|
177
|
+
raise RuntimeError("Transaction not started")
|
|
178
|
+
return self._tx_id
|
|
179
|
+
|
|
180
|
+
def _remaining_timeout(self) -> float | None:
|
|
181
|
+
if self._deadline is None:
|
|
182
|
+
return None
|
|
183
|
+
loop = asyncio.get_running_loop()
|
|
184
|
+
return self._deadline - loop.time()
|
|
185
|
+
|
|
186
|
+
async def execute(self, ir: dict[str, Any]) -> bytes:
|
|
187
|
+
if self._tx_id is None:
|
|
188
|
+
raise RuntimeError("Transaction not started")
|
|
189
|
+
if self._timed_out:
|
|
190
|
+
raise TransactionTimeoutError(
|
|
191
|
+
"Transaction can no longer be used because its timeout elapsed"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
ir_bytes = msgpack.packb(ir, default=_msgpack_encoder)
|
|
195
|
+
coro = _execute_in_transaction(self._database.name, self._tx_id, ir_bytes)
|
|
196
|
+
|
|
197
|
+
remaining = self._remaining_timeout()
|
|
198
|
+
if remaining is not None:
|
|
199
|
+
if remaining <= 0:
|
|
200
|
+
self._timed_out = True
|
|
201
|
+
msg = f"Transaction {self._tx_id} exceeded timeout"
|
|
202
|
+
raise TransactionTimeoutError(msg)
|
|
203
|
+
try:
|
|
204
|
+
return await asyncio.wait_for(coro, remaining)
|
|
205
|
+
except asyncio.TimeoutError as exc:
|
|
206
|
+
self._timed_out = True
|
|
207
|
+
msg = f"Transaction {self._tx_id} exceeded timeout"
|
|
208
|
+
raise TransactionTimeoutError(msg) from exc
|
|
209
|
+
|
|
210
|
+
result: bytes = await coro
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
async def __aenter__(self) -> AsyncTransaction:
|
|
214
|
+
await self._database.ensure_connected()
|
|
215
|
+
self._tx_id = await _begin_transaction(self._database.name)
|
|
216
|
+
if self._timeout is not None:
|
|
217
|
+
loop = asyncio.get_running_loop()
|
|
218
|
+
self._deadline = loop.time() + self._timeout
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
222
|
+
tx_id = self._tx_id
|
|
223
|
+
self._tx_id = None
|
|
224
|
+
self._deadline = None
|
|
225
|
+
|
|
226
|
+
if tx_id is None:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
if self._timed_out or exc_type is not None:
|
|
231
|
+
await _rollback_transaction(tx_id)
|
|
232
|
+
else:
|
|
233
|
+
await _commit_transaction(tx_id)
|
|
234
|
+
finally:
|
|
235
|
+
self._timed_out = False
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class AtomicTransactionContext:
|
|
239
|
+
"""Context manager that reuses or creates transactions per connection alias."""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
using: str = "default",
|
|
244
|
+
timeout: float | None = None,
|
|
245
|
+
database: AsyncDatabase | None = None,
|
|
246
|
+
):
|
|
247
|
+
self.using = using
|
|
248
|
+
self.timeout = timeout
|
|
249
|
+
self._database = database
|
|
250
|
+
self._transaction: AsyncTransaction | None = None
|
|
251
|
+
self._savepoint_name: str | None = None
|
|
252
|
+
|
|
253
|
+
async def __aenter__(self) -> AtomicTransactionContext:
|
|
254
|
+
state = dict(_ACTIVE_TRANSACTIONS.get())
|
|
255
|
+
entry = _get_owned_entry(self.using)
|
|
256
|
+
if entry:
|
|
257
|
+
# Nested transaction - create savepoint
|
|
258
|
+
depth = entry["depth"]
|
|
259
|
+
self._transaction = entry["transaction"]
|
|
260
|
+
|
|
261
|
+
# Create savepoint BEFORE incrementing depth —
|
|
262
|
+
# if this fails, __aexit__ won't be called and depth must stay unchanged
|
|
263
|
+
self._savepoint_name = f"sp_{depth + 1}"
|
|
264
|
+
await _create_savepoint(self._transaction.id, self._savepoint_name)
|
|
265
|
+
|
|
266
|
+
entry["depth"] += 1
|
|
267
|
+
state[self.using] = entry
|
|
268
|
+
_ACTIVE_TRANSACTIONS.set(state)
|
|
269
|
+
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
# Use provided database instance or get from registry
|
|
273
|
+
database = (
|
|
274
|
+
self._database
|
|
275
|
+
if self._database is not None
|
|
276
|
+
else await get_connection(self.using)
|
|
277
|
+
)
|
|
278
|
+
tx = AsyncTransaction(database, timeout=self.timeout)
|
|
279
|
+
await tx.__aenter__()
|
|
280
|
+
state[self.using] = {
|
|
281
|
+
"transaction": tx,
|
|
282
|
+
"depth": 1,
|
|
283
|
+
"force_rollback": False,
|
|
284
|
+
"task": asyncio.current_task(),
|
|
285
|
+
}
|
|
286
|
+
_ACTIVE_TRANSACTIONS.set(state)
|
|
287
|
+
self._transaction = tx
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
291
|
+
state = dict(_ACTIVE_TRANSACTIONS.get())
|
|
292
|
+
entry = _get_owned_entry(self.using)
|
|
293
|
+
if entry is None:
|
|
294
|
+
raise RuntimeError("transaction.atomic context mismatch")
|
|
295
|
+
|
|
296
|
+
entry["depth"] -= 1
|
|
297
|
+
force_rollback = entry.get("force_rollback", False)
|
|
298
|
+
|
|
299
|
+
if entry["depth"] == 0:
|
|
300
|
+
# Top-level transaction - commit or rollback entire transaction
|
|
301
|
+
state.pop(self.using, None)
|
|
302
|
+
_ACTIVE_TRANSACTIONS.set(state)
|
|
303
|
+
tx = entry["transaction"]
|
|
304
|
+
if exc_type is None and force_rollback:
|
|
305
|
+
exc_val = RuntimeError("transaction marked for rollback")
|
|
306
|
+
exc_type = RuntimeError
|
|
307
|
+
await tx.__aexit__(exc_type, exc_val, exc_tb)
|
|
308
|
+
else:
|
|
309
|
+
# Nested transaction - handle savepoint
|
|
310
|
+
if self._savepoint_name:
|
|
311
|
+
if exc_type:
|
|
312
|
+
# Rollback to savepoint - this is the end of this nested transaction
|
|
313
|
+
# Don't mark outer transaction for rollback, savepoint handles it
|
|
314
|
+
await _rollback_to_savepoint(
|
|
315
|
+
entry["transaction"].id, self._savepoint_name
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
# Release savepoint - nested transaction succeeded
|
|
319
|
+
await _release_savepoint(
|
|
320
|
+
entry["transaction"].id, self._savepoint_name
|
|
321
|
+
)
|
|
322
|
+
# Don't modify force_rollback - savepoint already handled the rollback
|
|
323
|
+
else:
|
|
324
|
+
# No savepoint (shouldn't happen with correct nesting)
|
|
325
|
+
if exc_type:
|
|
326
|
+
entry["force_rollback"] = True
|
|
327
|
+
|
|
328
|
+
state[self.using] = entry
|
|
329
|
+
_ACTIVE_TRANSACTIONS.set(state)
|
|
330
|
+
|
|
331
|
+
def set_rollback(self, rollback: bool = True) -> None:
|
|
332
|
+
entry = _get_owned_entry(self.using)
|
|
333
|
+
if entry is None:
|
|
334
|
+
raise RuntimeError("No active transaction to mark for rollback")
|
|
335
|
+
state = dict(_ACTIVE_TRANSACTIONS.get())
|
|
336
|
+
entry["force_rollback"] = bool(rollback)
|
|
337
|
+
state[self.using] = entry
|
|
338
|
+
_ACTIVE_TRANSACTIONS.set(state)
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def transaction(self) -> AsyncTransaction:
|
|
342
|
+
if self._transaction is None:
|
|
343
|
+
raise RuntimeError("transaction.atomic context not entered")
|
|
344
|
+
return self._transaction
|
|
345
|
+
|
|
346
|
+
async def execute(self, ir: dict[str, Any]) -> bytes:
|
|
347
|
+
return await self.transaction.execute(ir)
|
|
348
|
+
|
|
349
|
+
def __getattr__(self, item: str) -> Any:
|
|
350
|
+
return getattr(self.transaction, item)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def atomic(
|
|
354
|
+
*,
|
|
355
|
+
using: str = "default",
|
|
356
|
+
timeout: float | int | timedelta | None = None,
|
|
357
|
+
) -> AtomicTransactionContext:
|
|
358
|
+
"""Create an atomic transaction context."""
|
|
359
|
+
normalized = _normalize_duration(timeout)
|
|
360
|
+
return AtomicTransactionContext(using=using, timeout=normalized)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def get_active_transaction(using: str = "default") -> AsyncTransaction | None:
|
|
364
|
+
"""Get the currently active transaction for a connection alias."""
|
|
365
|
+
entry = _get_owned_entry(using)
|
|
366
|
+
if entry:
|
|
367
|
+
tx: AsyncTransaction = entry["transaction"]
|
|
368
|
+
return tx
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_transaction_depth(using: str = "default") -> int:
|
|
373
|
+
"""Return the atomic() depth for a connection alias in the calling task.
|
|
374
|
+
|
|
375
|
+
Returns 0 when no atomic() block is active, 1 in the outermost block,
|
|
376
|
+
and 2+ in nested blocks. Transactions owned by other tasks are excluded.
|
|
377
|
+
"""
|
|
378
|
+
entry = _get_owned_entry(using)
|
|
379
|
+
if entry:
|
|
380
|
+
depth: int = entry["depth"]
|
|
381
|
+
return depth
|
|
382
|
+
return 0
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
__all__ = [
|
|
386
|
+
"AsyncTransaction",
|
|
387
|
+
"TransactionTimeoutError",
|
|
388
|
+
"AtomicTransactionContext",
|
|
389
|
+
"atomic",
|
|
390
|
+
"get_active_transaction",
|
|
391
|
+
"get_transaction_depth",
|
|
392
|
+
]
|
oxyde/exceptions.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Exception hierarchy for Oxyde ORM.
|
|
2
|
+
|
|
3
|
+
All Oxyde exceptions inherit from OxydeError, allowing catch-all handling:
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
user = await User.objects.get(id=999)
|
|
7
|
+
except OxydeError as e:
|
|
8
|
+
print(f"ORM error: {e}")
|
|
9
|
+
|
|
10
|
+
Exception hierarchy:
|
|
11
|
+
OxydeError (base)
|
|
12
|
+
├── FieldError - Invalid field definition or access
|
|
13
|
+
├── FieldLookupError - Unknown lookup operator (e.g., __xyz)
|
|
14
|
+
│ └── FieldLookupValueError - Invalid value for lookup (e.g., __in=None)
|
|
15
|
+
└── ManagerError - Query execution errors
|
|
16
|
+
├── NotFoundError - get() returned no rows
|
|
17
|
+
├── MultipleObjectsReturned - get() returned multiple rows
|
|
18
|
+
└── IntegrityError - Constraint violation
|
|
19
|
+
├── UniqueViolationError - UNIQUE / PK constraint
|
|
20
|
+
├── ForeignKeyViolationError - FOREIGN KEY constraint
|
|
21
|
+
├── NotNullViolationError - NOT NULL constraint
|
|
22
|
+
└── CheckViolationError - CHECK constraint
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OxydeError(Exception):
|
|
29
|
+
"""Base exception for all Oxyde-related errors."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FieldError(OxydeError):
|
|
33
|
+
"""Raised when a model field is invalid or missing."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FieldLookupError(OxydeError):
|
|
37
|
+
"""Raised when an unsupported field lookup is requested."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FieldLookupValueError(FieldLookupError):
|
|
41
|
+
"""Raised when the lookup value is not compatible with the operator."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ManagerError(OxydeError):
|
|
45
|
+
"""Raised for issues inside the ORM manager layer."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NotFoundError(ManagerError):
|
|
49
|
+
"""Raised when a query expecting a single row finds none."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MultipleObjectsReturned(ManagerError):
|
|
53
|
+
"""Raised when a query expecting a single row finds more than one."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IntegrityError(ManagerError):
|
|
57
|
+
"""Raised when database integrity constraints are violated."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UniqueViolationError(IntegrityError):
|
|
61
|
+
"""Raised when a UNIQUE (or PRIMARY KEY) constraint is violated."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ForeignKeyViolationError(IntegrityError):
|
|
65
|
+
"""Raised when a FOREIGN KEY constraint is violated."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NotNullViolationError(IntegrityError):
|
|
69
|
+
"""Raised when a NOT NULL constraint is violated."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CheckViolationError(IntegrityError):
|
|
73
|
+
"""Raised when a CHECK constraint is violated."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"OxydeError",
|
|
78
|
+
"FieldError",
|
|
79
|
+
"FieldLookupError",
|
|
80
|
+
"FieldLookupValueError",
|
|
81
|
+
"ManagerError",
|
|
82
|
+
"NotFoundError",
|
|
83
|
+
"MultipleObjectsReturned",
|
|
84
|
+
"IntegrityError",
|
|
85
|
+
"UniqueViolationError",
|
|
86
|
+
"ForeignKeyViolationError",
|
|
87
|
+
"NotNullViolationError",
|
|
88
|
+
"CheckViolationError",
|
|
89
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Oxyde migrations system.
|
|
2
|
+
|
|
3
|
+
This module provides database migration functionality:
|
|
4
|
+
- Schema extraction from models
|
|
5
|
+
- Migration replay in memory
|
|
6
|
+
- MigrationContext for upgrade/downgrade operations
|
|
7
|
+
- Migration file generation
|
|
8
|
+
- Migration tracking and execution
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from oxyde.migrations.context import MigrationContext
|
|
14
|
+
from oxyde.migrations.executor import (
|
|
15
|
+
apply_migrations,
|
|
16
|
+
rollback_migration,
|
|
17
|
+
rollback_migrations,
|
|
18
|
+
)
|
|
19
|
+
from oxyde.migrations.extract import extract_current_schema
|
|
20
|
+
from oxyde.migrations.generator import generate_migration_file
|
|
21
|
+
from oxyde.migrations.replay import SchemaState, replay_migrations
|
|
22
|
+
from oxyde.migrations.squash import SquashResult, squash_migrations
|
|
23
|
+
from oxyde.migrations.tracker import (
|
|
24
|
+
ensure_migrations_table,
|
|
25
|
+
get_applied_migrations,
|
|
26
|
+
get_migration_files,
|
|
27
|
+
get_pending_migrations,
|
|
28
|
+
record_migration,
|
|
29
|
+
remove_migration,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SquashResult",
|
|
34
|
+
"squash_migrations",
|
|
35
|
+
"extract_current_schema",
|
|
36
|
+
"SchemaState",
|
|
37
|
+
"replay_migrations",
|
|
38
|
+
"MigrationContext",
|
|
39
|
+
"generate_migration_file",
|
|
40
|
+
"apply_migrations",
|
|
41
|
+
"rollback_migration",
|
|
42
|
+
"rollback_migrations",
|
|
43
|
+
"ensure_migrations_table",
|
|
44
|
+
"get_applied_migrations",
|
|
45
|
+
"get_migration_files",
|
|
46
|
+
"get_pending_migrations",
|
|
47
|
+
"record_migration",
|
|
48
|
+
"remove_migration",
|
|
49
|
+
]
|