Skip to content

Commit 227c21c

Browse files
vistartclaude
andcommitted
fix(sqlserver): fix async backend fetch operations and transaction manager
- Add AsyncSQLOperationsMixin to async backend class hierarchy so fetch_one/fetch_all are async and properly await execute() - Add AsyncTransactionManagementMixin for async transaction context manager support - Fix AsyncSQLServerTransactionManager.savepoint() to return name (str) instead of None, matching base class signature - Change savepoints from property to method for API consistency - Add isolation_level property to async transaction manager with proper SQLServerIsolationLevel resolution - Add @asynccontextmanager decorator to transaction() method - Separate _sqlserver_isolation_level from base IsolationLevel to prevent enum value being used as SQL string Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 00c3a8a commit 227c21c

2 files changed

Lines changed: 95 additions & 57 deletions

File tree

src/rhosocial/activerecord/backend/impl/sqlserver/async_backend.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from typing import Any, Dict, List, Optional, Tuple
1313

1414
from rhosocial.activerecord.backend.base import StorageBackend
15+
from rhosocial.activerecord.backend.base.operations import AsyncSQLOperationsMixin
16+
from rhosocial.activerecord.backend.base.transaction_management import AsyncTransactionManagementMixin
1517
from rhosocial.activerecord.backend.errors import (
1618
ConnectionError,
1719
DatabaseError,
@@ -34,6 +36,8 @@
3436

3537

3638
class AsyncSQLServerBackend(
39+
AsyncSQLOperationsMixin,
40+
AsyncTransactionManagementMixin,
3741
IntrospectorBackendMixin,
3842
SQLServerBackendMixin,
3943
StorageBackend

src/rhosocial/activerecord/backend/impl/sqlserver/async_transaction.py

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"""
55

66
import logging
7-
from typing import List, Optional
7+
from contextlib import asynccontextmanager
8+
from typing import AsyncGenerator, List, Optional
89

910
from rhosocial.activerecord.backend.transaction import (
1011
TransactionState,
@@ -20,18 +21,38 @@
2021

2122
class AsyncSQLServerTransactionManager:
2223
"""Asynchronous SQL Server transaction manager.
23-
24+
2425
Provides async transaction management with savepoint support.
2526
"""
26-
27+
2728
def __init__(self, backend, logger=None):
2829
self._backend = backend
2930
self._logger = logger
3031
self._state = TransactionState.INACTIVE
3132
self._savepoints: List[str] = []
32-
self._isolation_level: Optional[SQLServerIsolationLevel] = None
33+
self._sqlserver_isolation_level: Optional[SQLServerIsolationLevel] = None
3334
self._name: Optional[str] = None
34-
35+
36+
@property
37+
def is_active(self) -> bool:
38+
"""Check if a transaction is currently active."""
39+
return self._state == TransactionState.ACTIVE
40+
41+
@property
42+
def state(self) -> TransactionState:
43+
"""Get the current transaction state."""
44+
return self._state
45+
46+
@property
47+
def isolation_level(self) -> Optional[IsolationLevel]:
48+
"""Get the current isolation level."""
49+
return self._isolation_level
50+
51+
@isolation_level.setter
52+
def isolation_level(self, level: Optional[IsolationLevel]):
53+
"""Set the isolation level for the next transaction."""
54+
self._isolation_level = level
55+
3556
async def begin(
3657
self,
3758
isolation_level: Optional[IsolationLevel] = None,
@@ -41,7 +62,7 @@ async def begin(
4162
**kwargs
4263
) -> None:
4364
"""Begin a new async transaction.
44-
65+
4566
Args:
4667
isolation_level: Transaction isolation level
4768
mode: Transaction mode (READ ONLY not supported)
@@ -50,145 +71,158 @@ async def begin(
5071
"""
5172
if self._state == TransactionState.ACTIVE:
5273
raise TransactionError("Transaction already active")
53-
74+
5475
if mode == TransactionMode.READ_ONLY:
5576
raise UnsupportedTransactionModeError(
5677
feature="READ ONLY transactions",
5778
backend="SQL Server",
5879
message="SQL Server does not support READ ONLY transactions."
5980
)
60-
81+
6182
self._name = name
62-
83+
84+
# Resolve isolation level from parameter, base class property, or snapshot flag
85+
effective_iso = isolation_level
86+
if effective_iso is None and isinstance(getattr(self, '_isolation_level', None), IsolationLevel):
87+
effective_iso = self._isolation_level
88+
6389
if snapshot:
64-
self._isolation_level = SQLServerIsolationLevel.SNAPSHOT
65-
elif isolation_level:
66-
self._isolation_level = ISOLATION_LEVEL_MAP.get(isolation_level)
67-
90+
self._sqlserver_isolation_level = SQLServerIsolationLevel.SNAPSHOT
91+
elif effective_iso:
92+
self._sqlserver_isolation_level = ISOLATION_LEVEL_MAP.get(effective_iso)
93+
6894
await self._do_begin()
69-
95+
7096
self._state = TransactionState.ACTIVE
7197
self._savepoints = []
72-
73-
self._log(logging.DEBUG, f"Async transaction started (isolation={self._isolation_level})")
74-
98+
99+
self._log(logging.DEBUG, f"Async transaction started (isolation={self._sqlserver_isolation_level})")
100+
75101
async def _do_begin(self) -> None:
76102
"""Execute the BEGIN TRANSACTION statement."""
77103
parts = ["BEGIN TRANSACTION"]
78-
104+
79105
if self._name:
80106
parts.append(self._name)
81-
107+
82108
sql = " ".join(parts)
83109
await self._backend.execute(sql)
84-
85-
if self._isolation_level:
86-
set_iso_sql = f"SET TRANSACTION ISOLATION LEVEL {self._isolation_level.value}"
110+
111+
if self._sqlserver_isolation_level:
112+
set_iso_sql = f"SET TRANSACTION ISOLATION LEVEL {self._sqlserver_isolation_level.value}"
87113
await self._backend.execute(set_iso_sql)
88-
114+
89115
async def commit(self) -> None:
90116
"""Commit the current async transaction."""
91117
if self._state != TransactionState.ACTIVE:
92118
raise TransactionError("No active transaction to commit")
93-
119+
94120
self._log(logging.DEBUG, "Committing async transaction")
95-
121+
96122
try:
97123
await self._do_commit()
98124
self._state = TransactionState.COMMITTED
99125
self._savepoints = []
100-
126+
101127
except Exception as e:
102128
self._state = TransactionState.FAILED
103129
self._log(logging.ERROR, f"Failed to commit async transaction: {str(e)}")
104130
raise
105-
131+
106132
async def _do_commit(self) -> None:
107133
"""Execute the COMMIT TRANSACTION statement."""
108134
sql = "COMMIT TRANSACTION"
109135
if self._name:
110136
sql += f" {self._name}"
111137
await self._backend.execute(sql)
112-
138+
113139
async def rollback(self, savepoint: Optional[str] = None) -> None:
114140
"""Rollback the async transaction or to a savepoint.
115-
141+
116142
Args:
117143
savepoint: If provided, rollback to this savepoint
118144
"""
119145
if self._state != TransactionState.ACTIVE:
120146
raise TransactionError("No active transaction to rollback")
121-
147+
122148
if savepoint:
123149
await self._rollback_to_savepoint(savepoint)
124150
else:
125151
await self._do_rollback()
126152
self._state = TransactionState.ROLLED_BACK
127153
self._savepoints = []
128-
154+
129155
async def _do_rollback(self) -> None:
130156
"""Execute the ROLLBACK TRANSACTION statement."""
131157
sql = "ROLLBACK TRANSACTION"
132158
if self._name:
133159
sql += f" {self._name}"
134160
await self._backend.execute(sql)
135-
136-
async def savepoint(self, name: str) -> None:
161+
162+
async def savepoint(self, name: Optional[str] = None) -> str:
137163
"""Create a savepoint within the current async transaction.
138-
164+
139165
Args:
140-
name: Savepoint name
166+
name: Savepoint name. If None, auto-generated.
167+
168+
Returns:
169+
str: The name of the created savepoint
141170
"""
142171
if self._state != TransactionState.ACTIVE:
143172
raise TransactionError("No active transaction for savepoint")
144-
173+
174+
if name is None:
175+
name = f"sp_{len(self._savepoints) + 1}"
176+
145177
if name in self._savepoints:
146178
self._log(logging.WARNING, f"Savepoint '{name}' already exists")
147-
179+
148180
sql = f"SAVE TRANSACTION {name}"
149181
await self._backend.execute(sql)
150182
self._savepoints.append(name)
151-
183+
152184
self._log(logging.DEBUG, f"Async savepoint created: {name}")
153-
185+
return name
186+
154187
async def _rollback_to_savepoint(self, name: str) -> None:
155188
"""Rollback to a specific savepoint."""
156189
if name not in self._savepoints:
157190
raise TransactionError(f"Savepoint '{name}' not found")
158-
191+
159192
sql = f"ROLLBACK TRANSACTION {name}"
160193
await self._backend.execute(sql)
161-
194+
162195
idx = self._savepoints.index(name) + 1
163196
self._savepoints = self._savepoints[:idx]
164-
197+
165198
self._log(logging.DEBUG, f"Rolled back to async savepoint: {name}")
166-
199+
167200
async def release_savepoint(self, name: str) -> None:
168201
"""Release a savepoint (no-op in SQL Server).
169-
202+
170203
Args:
171204
name: Savepoint name
172205
"""
173206
if name in self._savepoints:
174207
self._savepoints.remove(name)
175208
self._log(logging.DEBUG, f"Async savepoint released: {name}")
176-
177-
@property
178-
def is_active(self) -> bool:
179-
"""Check if a transaction is currently active."""
180-
return self._state == TransactionState.ACTIVE
181-
182-
@property
183-
def state(self) -> TransactionState:
184-
"""Get the current transaction state."""
185-
return self._state
186-
187-
@property
209+
188210
def savepoints(self) -> List[str]:
189211
"""Get list of current savepoints."""
190212
return self._savepoints.copy()
191-
213+
214+
@asynccontextmanager
215+
async def transaction(self):
216+
"""Async context manager for transaction."""
217+
await self.begin()
218+
try:
219+
yield self
220+
except Exception:
221+
await self.rollback()
222+
raise
223+
else:
224+
await self.commit()
225+
192226
def _log(self, level: int, message: str) -> None:
193227
"""Log a message."""
194228
if self._logger:

0 commit comments

Comments
 (0)