surrealdb-orm 0.1.3__py3-none-any.whl → 0.5.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.
- surreal_orm/__init__.py +78 -3
- surreal_orm/aggregations.py +164 -0
- surreal_orm/auth/__init__.py +15 -0
- surreal_orm/auth/access.py +167 -0
- surreal_orm/auth/mixins.py +302 -0
- surreal_orm/cli/__init__.py +15 -0
- surreal_orm/cli/commands.py +369 -0
- surreal_orm/connection_manager.py +58 -18
- surreal_orm/fields/__init__.py +36 -0
- surreal_orm/fields/encrypted.py +166 -0
- surreal_orm/fields/relation.py +465 -0
- surreal_orm/migrations/__init__.py +51 -0
- surreal_orm/migrations/executor.py +380 -0
- surreal_orm/migrations/generator.py +272 -0
- surreal_orm/migrations/introspector.py +305 -0
- surreal_orm/migrations/migration.py +188 -0
- surreal_orm/migrations/operations.py +531 -0
- surreal_orm/migrations/state.py +406 -0
- surreal_orm/model_base.py +594 -135
- surreal_orm/py.typed +0 -0
- surreal_orm/query_set.py +609 -34
- surreal_orm/relations.py +645 -0
- surreal_orm/surreal_function.py +95 -0
- surreal_orm/surreal_ql.py +113 -0
- surreal_orm/types.py +86 -0
- surreal_sdk/README.md +79 -0
- surreal_sdk/__init__.py +151 -0
- surreal_sdk/connection/__init__.py +17 -0
- surreal_sdk/connection/base.py +516 -0
- surreal_sdk/connection/http.py +421 -0
- surreal_sdk/connection/pool.py +244 -0
- surreal_sdk/connection/websocket.py +519 -0
- surreal_sdk/exceptions.py +71 -0
- surreal_sdk/functions.py +607 -0
- surreal_sdk/protocol/__init__.py +13 -0
- surreal_sdk/protocol/rpc.py +218 -0
- surreal_sdk/py.typed +0 -0
- surreal_sdk/pyproject.toml +49 -0
- surreal_sdk/streaming/__init__.py +31 -0
- surreal_sdk/streaming/change_feed.py +278 -0
- surreal_sdk/streaming/live_query.py +265 -0
- surreal_sdk/streaming/live_select.py +369 -0
- surreal_sdk/transaction.py +386 -0
- surreal_sdk/types.py +346 -0
- surrealdb_orm-0.5.0.dist-info/METADATA +465 -0
- surrealdb_orm-0.5.0.dist-info/RECORD +52 -0
- {surrealdb_orm-0.1.3.dist-info → surrealdb_orm-0.5.0.dist-info}/WHEEL +1 -1
- surrealdb_orm-0.5.0.dist-info/entry_points.txt +2 -0
- {surrealdb_orm-0.1.3.dist-info → surrealdb_orm-0.5.0.dist-info}/licenses/LICENSE +1 -1
- surrealdb_orm-0.1.3.dist-info/METADATA +0 -184
- surrealdb_orm-0.1.3.dist-info/RECORD +0 -11
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transaction support for SurrealDB SDK.
|
|
3
|
+
|
|
4
|
+
Provides atomic transaction handling for both HTTP and WebSocket connections.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
10
|
+
|
|
11
|
+
from .exceptions import TransactionError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .connection.base import BaseSurrealConnection
|
|
15
|
+
from .types import (
|
|
16
|
+
DeleteResponse,
|
|
17
|
+
QueryResponse,
|
|
18
|
+
RecordResponse,
|
|
19
|
+
RecordsResponse,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TransactionStatement:
|
|
25
|
+
"""A single statement queued in a transaction."""
|
|
26
|
+
|
|
27
|
+
sql: str
|
|
28
|
+
vars: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseTransaction(ABC):
|
|
32
|
+
"""
|
|
33
|
+
Abstract base class for transaction handling.
|
|
34
|
+
|
|
35
|
+
Provides a unified interface for atomic operations that works
|
|
36
|
+
across both HTTP and WebSocket connections.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
async with conn.transaction() as tx:
|
|
40
|
+
await tx.update("players:abc", {"is_ready": True})
|
|
41
|
+
await tx.update("game_tables:xyz", {"ready_count": 1})
|
|
42
|
+
# Auto-commit on success, auto-rollback on exception
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, connection: "BaseSurrealConnection"):
|
|
46
|
+
self._connection = connection
|
|
47
|
+
self._statements: list[TransactionStatement] = []
|
|
48
|
+
self._committed = False
|
|
49
|
+
self._rolled_back = False
|
|
50
|
+
self._active = False
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_active(self) -> bool:
|
|
54
|
+
"""Check if transaction is active."""
|
|
55
|
+
return self._active and not self._committed and not self._rolled_back
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_committed(self) -> bool:
|
|
59
|
+
"""Check if transaction was committed."""
|
|
60
|
+
return self._committed
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_rolled_back(self) -> bool:
|
|
64
|
+
"""Check if transaction was rolled back."""
|
|
65
|
+
return self._rolled_back
|
|
66
|
+
|
|
67
|
+
async def __aenter__(self) -> Self:
|
|
68
|
+
"""Begin transaction on context entry."""
|
|
69
|
+
await self._begin()
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def __aexit__(
|
|
73
|
+
self,
|
|
74
|
+
exc_type: type[BaseException] | None,
|
|
75
|
+
exc_val: BaseException | None,
|
|
76
|
+
exc_tb: Any,
|
|
77
|
+
) -> bool:
|
|
78
|
+
"""Commit on success, rollback on exception."""
|
|
79
|
+
if exc_type is not None:
|
|
80
|
+
await self.rollback()
|
|
81
|
+
return False # Re-raise exception
|
|
82
|
+
await self.commit()
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def _begin(self) -> None:
|
|
87
|
+
"""Begin the transaction."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
async def commit(self) -> "QueryResponse":
|
|
92
|
+
"""Commit the transaction."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
async def rollback(self) -> None:
|
|
97
|
+
"""Rollback the transaction."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
# Transaction operations (to be implemented by subclasses)
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def query(self, sql: str, vars: dict[str, Any] | None = None) -> "QueryResponse":
|
|
104
|
+
"""Execute a query within the transaction."""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
async def create(self, thing: str, data: dict[str, Any] | None = None) -> "RecordResponse":
|
|
109
|
+
"""Create a record within the transaction."""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
async def insert(self, table: str, data: list[dict[str, Any]] | dict[str, Any]) -> "RecordsResponse":
|
|
114
|
+
"""Insert records within the transaction."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def update(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
119
|
+
"""Update records within the transaction."""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
124
|
+
"""Merge data into records within the transaction."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
async def delete(self, thing: str) -> "DeleteResponse":
|
|
129
|
+
"""Delete records within the transaction."""
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
async def relate(
|
|
134
|
+
self,
|
|
135
|
+
from_thing: str,
|
|
136
|
+
relation: str,
|
|
137
|
+
to_thing: str,
|
|
138
|
+
data: dict[str, Any] | None = None,
|
|
139
|
+
) -> "RecordResponse":
|
|
140
|
+
"""Create a relation within the transaction."""
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class HTTPTransaction(BaseTransaction):
|
|
145
|
+
"""
|
|
146
|
+
HTTP-based transaction that batches statements.
|
|
147
|
+
|
|
148
|
+
Since HTTP is stateless, all statements are collected and
|
|
149
|
+
executed as a single atomic query on commit.
|
|
150
|
+
|
|
151
|
+
The statements are wrapped in BEGIN TRANSACTION / COMMIT TRANSACTION
|
|
152
|
+
and sent as a single request.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
async def _begin(self) -> None:
|
|
156
|
+
"""Mark transaction as active (no server call needed for HTTP)."""
|
|
157
|
+
if self._active:
|
|
158
|
+
raise TransactionError("Transaction already active")
|
|
159
|
+
self._active = True
|
|
160
|
+
self._statements = []
|
|
161
|
+
|
|
162
|
+
async def commit(self) -> "QueryResponse":
|
|
163
|
+
"""Execute all queued statements atomically."""
|
|
164
|
+
from .types import QueryResponse
|
|
165
|
+
|
|
166
|
+
if not self.is_active:
|
|
167
|
+
raise TransactionError("Transaction not active")
|
|
168
|
+
|
|
169
|
+
if not self._statements:
|
|
170
|
+
# Empty transaction, just mark as committed
|
|
171
|
+
self._committed = True
|
|
172
|
+
self._active = False
|
|
173
|
+
return QueryResponse(results=[], raw=[])
|
|
174
|
+
|
|
175
|
+
# Build batched query with BEGIN/COMMIT wrapper
|
|
176
|
+
sql_parts = ["BEGIN TRANSACTION;"]
|
|
177
|
+
all_vars: dict[str, Any] = {}
|
|
178
|
+
|
|
179
|
+
for i, stmt in enumerate(self._statements):
|
|
180
|
+
sql_parts.append(stmt.sql)
|
|
181
|
+
# Namespace variables to avoid conflicts between statements
|
|
182
|
+
for key, val in stmt.vars.items():
|
|
183
|
+
namespaced_key = f"tx_{i}_{key}"
|
|
184
|
+
all_vars[namespaced_key] = val
|
|
185
|
+
# Replace variable reference in SQL
|
|
186
|
+
stmt.sql = stmt.sql.replace(f"${key}", f"${namespaced_key}")
|
|
187
|
+
sql_parts[i + 1] = stmt.sql # Update with namespaced vars
|
|
188
|
+
|
|
189
|
+
sql_parts.append("COMMIT TRANSACTION;")
|
|
190
|
+
full_sql = "\n".join(sql_parts)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
result = await self._connection.query(full_sql, all_vars)
|
|
194
|
+
self._committed = True
|
|
195
|
+
self._active = False
|
|
196
|
+
return result
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self._active = False
|
|
199
|
+
raise TransactionError(f"Transaction commit failed: {e}")
|
|
200
|
+
|
|
201
|
+
async def rollback(self) -> None:
|
|
202
|
+
"""Discard queued statements (no server call needed for HTTP)."""
|
|
203
|
+
self._statements = []
|
|
204
|
+
self._rolled_back = True
|
|
205
|
+
self._active = False
|
|
206
|
+
|
|
207
|
+
def _queue_statement(self, sql: str, vars: dict[str, Any] | None = None) -> None:
|
|
208
|
+
"""Queue a statement for later execution."""
|
|
209
|
+
if not self.is_active:
|
|
210
|
+
raise TransactionError("Transaction not active")
|
|
211
|
+
self._statements.append(TransactionStatement(sql=sql, vars=vars or {}))
|
|
212
|
+
|
|
213
|
+
async def query(self, sql: str, vars: dict[str, Any] | None = None) -> "QueryResponse":
|
|
214
|
+
"""Queue a query for execution on commit."""
|
|
215
|
+
from .types import QueryResponse
|
|
216
|
+
|
|
217
|
+
self._queue_statement(sql, vars)
|
|
218
|
+
# Return empty response since actual execution happens on commit
|
|
219
|
+
return QueryResponse(results=[], raw=[])
|
|
220
|
+
|
|
221
|
+
async def create(self, thing: str, data: dict[str, Any] | None = None) -> "RecordResponse":
|
|
222
|
+
"""Queue a create operation."""
|
|
223
|
+
from .types import RecordResponse
|
|
224
|
+
|
|
225
|
+
if data:
|
|
226
|
+
# Build CREATE statement with data
|
|
227
|
+
fields = ", ".join(f"{k} = ${k}" for k in data.keys())
|
|
228
|
+
sql = f"CREATE {thing} SET {fields};"
|
|
229
|
+
self._queue_statement(sql, data)
|
|
230
|
+
else:
|
|
231
|
+
sql = f"CREATE {thing};"
|
|
232
|
+
self._queue_statement(sql)
|
|
233
|
+
return RecordResponse(record=None, raw=None)
|
|
234
|
+
|
|
235
|
+
async def insert(self, table: str, data: list[dict[str, Any]] | dict[str, Any]) -> "RecordsResponse":
|
|
236
|
+
"""Queue an insert operation."""
|
|
237
|
+
from .types import RecordsResponse
|
|
238
|
+
|
|
239
|
+
if isinstance(data, dict):
|
|
240
|
+
data = [data]
|
|
241
|
+
|
|
242
|
+
for i, record in enumerate(data):
|
|
243
|
+
fields = ", ".join(f"{k} = $r{i}_{k}" for k in record.keys())
|
|
244
|
+
sql = f"CREATE {table} SET {fields};"
|
|
245
|
+
vars_with_prefix = {f"r{i}_{k}": v for k, v in record.items()}
|
|
246
|
+
self._queue_statement(sql, vars_with_prefix)
|
|
247
|
+
|
|
248
|
+
return RecordsResponse(records=[], raw=[])
|
|
249
|
+
|
|
250
|
+
async def update(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
251
|
+
"""Queue an update operation."""
|
|
252
|
+
from .types import RecordsResponse
|
|
253
|
+
|
|
254
|
+
fields = ", ".join(f"{k} = ${k}" for k in data.keys())
|
|
255
|
+
sql = f"UPDATE {thing} SET {fields};"
|
|
256
|
+
self._queue_statement(sql, data)
|
|
257
|
+
return RecordsResponse(records=[], raw=[])
|
|
258
|
+
|
|
259
|
+
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
260
|
+
"""Queue a merge operation."""
|
|
261
|
+
from .types import RecordsResponse
|
|
262
|
+
|
|
263
|
+
fields = ", ".join(f"{k} = ${k}" for k in data.keys())
|
|
264
|
+
sql = f"UPDATE {thing} MERGE {{ {fields} }};"
|
|
265
|
+
self._queue_statement(sql, data)
|
|
266
|
+
return RecordsResponse(records=[], raw=[])
|
|
267
|
+
|
|
268
|
+
async def delete(self, thing: str) -> "DeleteResponse":
|
|
269
|
+
"""Queue a delete operation."""
|
|
270
|
+
from .types import DeleteResponse
|
|
271
|
+
|
|
272
|
+
sql = f"DELETE {thing};"
|
|
273
|
+
self._queue_statement(sql)
|
|
274
|
+
return DeleteResponse(deleted=[], raw=[])
|
|
275
|
+
|
|
276
|
+
async def relate(
|
|
277
|
+
self,
|
|
278
|
+
from_thing: str,
|
|
279
|
+
relation: str,
|
|
280
|
+
to_thing: str,
|
|
281
|
+
data: dict[str, Any] | None = None,
|
|
282
|
+
) -> "RecordResponse":
|
|
283
|
+
"""Queue a relate operation."""
|
|
284
|
+
from .types import RecordResponse
|
|
285
|
+
|
|
286
|
+
if data:
|
|
287
|
+
fields = ", ".join(f"{k} = ${k}" for k in data.keys())
|
|
288
|
+
sql = f"RELATE {from_thing}->{relation}->{to_thing} SET {fields};"
|
|
289
|
+
self._queue_statement(sql, data)
|
|
290
|
+
else:
|
|
291
|
+
sql = f"RELATE {from_thing}->{relation}->{to_thing};"
|
|
292
|
+
self._queue_statement(sql)
|
|
293
|
+
return RecordResponse(record=None, raw=None)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class WebSocketTransaction(BaseTransaction):
|
|
297
|
+
"""
|
|
298
|
+
WebSocket-based transaction with server-side state.
|
|
299
|
+
|
|
300
|
+
Uses actual BEGIN/COMMIT/ROLLBACK commands since
|
|
301
|
+
WebSocket maintains session state across requests.
|
|
302
|
+
|
|
303
|
+
Operations are executed immediately within the transaction context.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
async def _begin(self) -> None:
|
|
307
|
+
"""Send BEGIN TRANSACTION to server."""
|
|
308
|
+
if self._active:
|
|
309
|
+
raise TransactionError("Transaction already active")
|
|
310
|
+
await self._connection.query("BEGIN TRANSACTION;")
|
|
311
|
+
self._active = True
|
|
312
|
+
|
|
313
|
+
async def commit(self) -> "QueryResponse":
|
|
314
|
+
"""Send COMMIT to server."""
|
|
315
|
+
if not self.is_active:
|
|
316
|
+
raise TransactionError("Transaction not active")
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
result = await self._connection.query("COMMIT TRANSACTION;")
|
|
320
|
+
self._committed = True
|
|
321
|
+
self._active = False
|
|
322
|
+
return result
|
|
323
|
+
except Exception as e:
|
|
324
|
+
self._active = False
|
|
325
|
+
raise TransactionError(f"Commit failed: {e}")
|
|
326
|
+
|
|
327
|
+
async def rollback(self) -> None:
|
|
328
|
+
"""Send ROLLBACK to server."""
|
|
329
|
+
if not self.is_active:
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
await self._connection.query("CANCEL TRANSACTION;")
|
|
334
|
+
except Exception:
|
|
335
|
+
pass # Best effort rollback
|
|
336
|
+
finally:
|
|
337
|
+
self._rolled_back = True
|
|
338
|
+
self._active = False
|
|
339
|
+
|
|
340
|
+
async def query(self, sql: str, vars: dict[str, Any] | None = None) -> "QueryResponse":
|
|
341
|
+
"""Execute query immediately within transaction."""
|
|
342
|
+
if not self.is_active:
|
|
343
|
+
raise TransactionError("Transaction not active")
|
|
344
|
+
return await self._connection.query(sql, vars)
|
|
345
|
+
|
|
346
|
+
async def create(self, thing: str, data: dict[str, Any] | None = None) -> "RecordResponse":
|
|
347
|
+
"""Execute create immediately within transaction."""
|
|
348
|
+
if not self.is_active:
|
|
349
|
+
raise TransactionError("Transaction not active")
|
|
350
|
+
return await self._connection.create(thing, data)
|
|
351
|
+
|
|
352
|
+
async def insert(self, table: str, data: list[dict[str, Any]] | dict[str, Any]) -> "RecordsResponse":
|
|
353
|
+
"""Execute insert immediately within transaction."""
|
|
354
|
+
if not self.is_active:
|
|
355
|
+
raise TransactionError("Transaction not active")
|
|
356
|
+
return await self._connection.insert(table, data)
|
|
357
|
+
|
|
358
|
+
async def update(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
359
|
+
"""Execute update immediately within transaction."""
|
|
360
|
+
if not self.is_active:
|
|
361
|
+
raise TransactionError("Transaction not active")
|
|
362
|
+
return await self._connection.update(thing, data)
|
|
363
|
+
|
|
364
|
+
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
365
|
+
"""Execute merge immediately within transaction."""
|
|
366
|
+
if not self.is_active:
|
|
367
|
+
raise TransactionError("Transaction not active")
|
|
368
|
+
return await self._connection.merge(thing, data)
|
|
369
|
+
|
|
370
|
+
async def delete(self, thing: str) -> "DeleteResponse":
|
|
371
|
+
"""Execute delete immediately within transaction."""
|
|
372
|
+
if not self.is_active:
|
|
373
|
+
raise TransactionError("Transaction not active")
|
|
374
|
+
return await self._connection.delete(thing)
|
|
375
|
+
|
|
376
|
+
async def relate(
|
|
377
|
+
self,
|
|
378
|
+
from_thing: str,
|
|
379
|
+
relation: str,
|
|
380
|
+
to_thing: str,
|
|
381
|
+
data: dict[str, Any] | None = None,
|
|
382
|
+
) -> "RecordResponse":
|
|
383
|
+
"""Execute relate immediately within transaction."""
|
|
384
|
+
if not self.is_active:
|
|
385
|
+
raise TransactionError("Transaction not active")
|
|
386
|
+
return await self._connection.relate(from_thing, relation, to_thing, data)
|