44"""
55
66import logging
7- from typing import List , Optional
7+ from contextlib import asynccontextmanager
8+ from typing import AsyncGenerator , List , Optional
89
910from rhosocial .activerecord .backend .transaction import (
1011 TransactionState ,
2021
2122class 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