Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use transactions with Azure Synapse Analytics #8231

Closed
gordthompson opened this issue Jul 8, 2022 Discussed in #8223 · 52 comments
Closed

Unable to use transactions with Azure Synapse Analytics #8231

gordthompson opened this issue Jul 8, 2022 Discussed in #8223 · 52 comments
Labels
external driver issues the issue involves a misbehavior on the part of the DBAPI itself, probably not SQLAlchemy SQL Server Microsoft SQL Server, e.g. mssql
Milestone

Comments

@gordthompson
Copy link
Member

Discussed in #8223

Originally posted by ww2406 July 7, 2022

Describe the bug

Hello!

Problem: Unable to use transactions with Azure Synapse Analytics

Description: ODBC Error 111214 is returned: An attempt to complete a transaction has failed. No corresponding transaction found.

Other notes: Transactions function properly when using pyodbc directly [see MCVE]. I saw this post by gordthompson in response to a question on StackOverflow (and since added to the documentation) that

Azure SQL Data Warehouse does not support transactions

but this is outdated. Azure Synapse Analytics does support transactions in most cases; see this Microsoft documentation:

As you would expect, SQL pool supports transactions as part of the data warehouse workload. However, to ensure SQL pool is maintained at scale, some features are limited when compared to SQL Server. This article highlights the differences.

To Reproduce

import pyodbc 
import sqlalchemy as sa 
import os 

p_con_string = os.getenv("synapse_conn_pyodbc")
sa_eng_string = os.getenv("synapse_conn_sa")

p_con = pyodbc.connect(p_con_string)
engine = sa.create_engine(sa_eng_string)

p_cur = p_con.cursor()
p_cur.execute("INSERT INTO test VALUES ('test')")
p_con.commit()
p_con.close()
# No error

with engine.begin() as s_con:
    stmt = sa.Text("INSERT INTO test VALUES ('test')")
    s_con.execute(stmt)

# Traceback (most recent call last):
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3280, in _wrap_pool_connect
#     return fn()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 310, in connect
#     return _ConnectionFairy._checkout(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 868, in _checkout
#     fairy = _ConnectionRecord.checkout(pool)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 476, in checkout
#     rec = pool._do_get()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 145, in _do_get
#     with util.safe_reraise():
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 70, in __exit__
#     compat.raise_(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
#     raise exception
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 143, in _do_get
#     return self._create_connection()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 256, in _create_connection
#     return _ConnectionRecord(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 371, in __init__
#     self.__connect()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 679, in __connect
#     )._exec_w_sync_on_first_run(self.dbapi_connection, self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 329, in _exec_w_sync_on_first_run
#     self(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 343, in __call__
#     fn(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 1695, in go
#     return once_fn(*arg, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\create.py", line 694, in first_connect
#     dialect.do_rollback(c.connection)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\default.py", line 682, in do_rollback
#     dbapi_connection.rollback()
# pyodbc.ProgrammingError: ('42000', '[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]111214;An attempt to complete a transaction has failed. No corresponding transaction found. (111214) (SQLEndTran)')

# The above exception was the direct cause of the following exception:

# Traceback (most recent call last):
#   File "synapse_issue.py", line 15, in <module>
#     with engine.begin() as s_con:
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3062, in begin
#     conn = self.connect(close_with_result=close_with_result)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3234, in connect
#     return self._connection_cls(self, close_with_result=close_with_result)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 96, in __init__
#     else engine.raw_connection()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3313, in raw_connection
#     return self._wrap_pool_connect(self.pool.connect, _connection)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3283, in _wrap_pool_connect
#     Connection._handle_dbapi_exception_noconnection(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 2117, in _handle_dbapi_exception_noconnection
#     util.raise_(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
#     raise exception
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3280, in _wrap_pool_connect
#     return fn()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 310, in connect
#     return _ConnectionFairy._checkout(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 868, in _checkout
#     fairy = _ConnectionRecord.checkout(pool)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 476, in checkout
#     rec = pool._do_get()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 145, in _do_get
#     with util.safe_reraise():
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 70, in __exit__
#     compat.raise_(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
#     raise exception
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 143, in _do_get
#     return self._create_connection()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 256, in _create_connection
#     return _ConnectionRecord(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 371, in __init__
#     self.__connect()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 679, in __connect
#     )._exec_w_sync_on_first_run(self.dbapi_connection, self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 329, in _exec_w_sync_on_first_run
#     self(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 343, in __call__
#     fn(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 1695, in go
#     return once_fn(*arg, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\create.py", line 694, in first_connect
#     dialect.do_rollback(c.connection)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\default.py", line 682, in do_rollback
#     dbapi_connection.rollback()
# sqlalchemy.exc.ProgrammingError: (pyodbc.ProgrammingError) ('42000', '[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]111214;An attempt to complete a transaction has failed. No 
# corresponding transaction found. (111214) (SQLEndTran)')
# (Background on this error at: https://sqlalche.me/e/14/f405)

Error

# Traceback (most recent call last):
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3280, in _wrap_pool_connect
#     return fn()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 310, in connect
#     return _ConnectionFairy._checkout(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 868, in _checkout
#     fairy = _ConnectionRecord.checkout(pool)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 476, in checkout
#     rec = pool._do_get()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 145, in _do_get
#     with util.safe_reraise():
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 70, in __exit__
#     compat.raise_(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
#     raise exception
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 143, in _do_get
#     return self._create_connection()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 256, in _create_connection
#     return _ConnectionRecord(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 371, in __init__
#     self.__connect()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 679, in __connect
#     )._exec_w_sync_on_first_run(self.dbapi_connection, self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 329, in _exec_w_sync_on_first_run
#     self(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 343, in __call__
#     fn(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 1695, in go
#     return once_fn(*arg, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\create.py", line 694, in first_connect
#     dialect.do_rollback(c.connection)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\default.py", line 682, in do_rollback
#     dbapi_connection.rollback()
# pyodbc.ProgrammingError: ('42000', '[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]111214;An attempt to complete a transaction has failed. No corresponding transaction found. (111214) (SQLEndTran)')

# The above exception was the direct cause of the following exception:

# Traceback (most recent call last):
#   File "synapse_issue.py", line 15, in <module>
#     with engine.begin() as s_con:
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3062, in begin
#     conn = self.connect(close_with_result=close_with_result)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3234, in connect
#     return self._connection_cls(self, close_with_result=close_with_result)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 96, in __init__
#     else engine.raw_connection()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3313, in raw_connection
#     return self._wrap_pool_connect(self.pool.connect, _connection)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3283, in _wrap_pool_connect
#     Connection._handle_dbapi_exception_noconnection(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 2117, in _handle_dbapi_exception_noconnection
#     util.raise_(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
#     raise exception
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\base.py", line 3280, in _wrap_pool_connect
#     return fn()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 310, in connect
#     return _ConnectionFairy._checkout(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 868, in _checkout
#     fairy = _ConnectionRecord.checkout(pool)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 476, in checkout
#     rec = pool._do_get()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 145, in _do_get
#     with util.safe_reraise():
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 70, in __exit__
#     compat.raise_(
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
#     raise exception
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\impl.py", line 143, in _do_get
#     return self._create_connection()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 256, in _create_connection
#     return _ConnectionRecord(self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 371, in __init__
#     self.__connect()
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\pool\base.py", line 679, in __connect
#     )._exec_w_sync_on_first_run(self.dbapi_connection, self)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 329, in _exec_w_sync_on_first_run
#     self(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\event\attr.py", line 343, in __call__
#     fn(*args, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\util\langhelpers.py", line 1695, in go
#     return once_fn(*arg, **kw)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\create.py", line 694, in first_connect
#     dialect.do_rollback(c.connection)
#   File "C:\Users\user\AppData\Roaming\Python\Python310\site-packages\sqlalchemy\engine\default.py", line 682, in do_rollback
#     dbapi_connection.rollback()
# sqlalchemy.exc.ProgrammingError: (pyodbc.ProgrammingError) ('42000', '[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]111214;An attempt to complete a transaction has failed. No 
# corresponding transaction found. (111214) (SQLEndTran)')
# (Background on this error at: https://sqlalche.me/e/14/f405)

Versions

  • OS: Windows 10
  • Python: 3.10
  • SQLAlchemy: 1.4.37
  • Database: Azure Synapse Analytics Dedicated Pool
  • DBAPI (eg: psycopg, cx_oracle, mysqlclient): pyodbc

Additional context

No response

@gordthompson gordthompson added SQL Server Microsoft SQL Server, e.g. mssql external driver issues the issue involves a misbehavior on the part of the DBAPI itself, probably not SQLAlchemy labels Jul 8, 2022
@zzzeek
Copy link
Member

zzzeek commented Jul 8, 2022

@gordthompson can you please elaborate what the actual SQLAlchemy bug is? the trace shows pyodbc.connection.rollback() failing. how is that our issue ?

@zzzeek zzzeek added awaiting info waiting for the submitter to give more information question issue where a "fix" on the SQLAlchemy side is unlikely, hence more of a usage question labels Jul 8, 2022
@gordthompson
Copy link
Member Author

When we get_isolation_level() we have to look in two places:

views = ("sys.dm_exec_sessions", "sys.dm_pdw_nodes_exec_sessions")

Plain old SQL Server uses the former, while the Data Warehouse products use the latter.

@ww2406 discovered that if they removed the first entry and just queried sys.dm_pdw_nodes_exec_sessions then the error from Azure Synapse Analytics went away. Subsequent testing showed that simply swapping the order of the views made Synapse happy and SQL Server 2019 was cool with it. (I ran tox -e py38-mssql just to be sure.)

So this is just a workaround for the difference in behaviour between SQL Server and Synapse.

@zzzeek
Copy link
Member

zzzeek commented Jul 9, 2022

OK I need more detail than that:

  1. has this code ever worked for data warehouse?
  2. if so, what has changed?
  3. is it the case that both sys.dm_exec_sessions and sys.dm_pdw_nodes_exec_sessions both exist in data warehouse products?
  4. if so, what's the difference between them?
  5. does the sys.dm_pdw_nodes_exec_sessions view exist in plain SQL server ? because if it does, and returns garbage data as seems to be suggested by number 3 above, then it would be a disaster for us to reverse these
  6. can we change the entire approach and look at SQL server version string, or some other reliable indicator, rather than just querying both tables and waiting for a failure?

tl;dr; reversing them is a band aid, lets please use real server telemetry to choose the correct view up front

@zzzeek
Copy link
Member

zzzeek commented Jul 9, 2022

if the "reversing" helps only because it avoids an error condition, that otherwise somehow corrupts the connection, then that also is kind of a bigger problem.

@ww2406
Copy link

ww2406 commented Jul 9, 2022

@zzzeek
1-- @gordthompson may know more, but as far as I've been able to find based on various bug reports, stack overflow posts, etc., no; it has always been required to use autocommit=True to avoid the transaction issue

3-- No, sys.dm_exec_sessions does not exist in the PDW products. This is what causes this issue in the first place, I believe, and why pure pyodbc does not experience issues. SQLAlchemy fails when it attempts to query sys.dm_exec_sessions (which does not happen in the underlying pyodbc implmentation). There may be another latent issue here in how the code is structured, because from my investigation it appears that the second call is successful but the error from the first run is still thrown.

4-- sys.dm_pdw_nodes_exec_sessions` is the equivalent in PDW products. PDW is architected differently from traditional SQL Server -- it is more akin to Redshift/BigQuery with the concept of distributed nodes etc. Nodes_exec_sessions provides a little more info about what node a query is executing on etc. that is immaterial for normal SQL Server.

5-- No. Plain SQL Server only contains dm_exec_sessions.

6-- I can do a little digging on this, and if nothing sticks out, I can see if my MS rep knows anything.

As a user, a band-aid that fixes a long-standing restriction with no impact to existing functionality is preferable now as opposed to having nothing while waiting for a deeper fix with no additional functional benefits [assuming that no impact statement holds true; gordthompson ran a test suite and I personally tested against SQL Server 2019 and regular Azure SQL Database with positive results].

(Also, I use PDW here to loosely refer to SQL Server Parallel Data Warehouse, Azure Synapse Analytics, and Azure SQL Data Warehouse -- they are all similar)

@ww2406
Copy link

ww2406 commented Jul 9, 2022

Re: 6....

This looks like a way to differentiate between the two.

declare @EngineEdition as sql_variant
set @EngineEdition = (select SERVERPROPERTY('EngineEdition'))
select @EngineEdition as EngineEdition,
case @EngineEdition
when 1 then 'Personal or Desktop Engine'
when 2 then 'Standard'
when 3 then 'Enterprise'
when 4  then 'Express'
when 5  then ' SQL Database'
when 6 then 'SQL Data Warehouse'
end as 'Engine Edition Based on ID'

I tested against a dedicated pool on my personal Azure account and confirmed I received a response of 6. I also tested against an enterprise instance in docker and received a response of 3.

@CaselIT
Copy link
Member

CaselIT commented Jul 9, 2022

Are the tables in sys reported by the information tables, like sys.tables? Because if they are maybe we can select the table to use based on that?

@gordthompson
Copy link
Member Author

Are the tables in sys reported by the information tables, like sys.tables? Because if they are maybe we can select the table to use based on that?

Yes, at least on SQL Server 2019:

1> select count(*) as n from sys.system_views where name = 'dm_exec_sessions'
2> go
n          
-----------
          1

(1 rows affected)
1> select count(*) as n from sys.system_views where name = 'dm_pdw_nodes_exec_sessions'
2> go
n          
-----------
          0

(1 rows affected)

@zzzeek
Copy link
Member

zzzeek commented Jul 9, 2022

yes I like looking in system tables for it.

however, I really need to understand the actual problem.

is the problem the following?

  1. we try to query nonexistent dm_exec_sessions
  2. error is raised, we catch
  3. .... connection is corrupted? can't rollback ? isn't this a bigger problem for issues like determining connection invalidation? should we really just be fixing is_disconnect?

what's involved getting a connection to one of these DBs, is there some msft developer cloud thing we can try?

@ww2406
Copy link

ww2406 commented Jul 9, 2022

Not free AFAIK, but the lowest operating level of Synapse is “only” $1.x an hour which isn’t horrible for quick testing like this. If someone has a VS Enterprise subscription they get $150 in free credits monthly against Azure svcs

@ww2406
Copy link

ww2406 commented Jul 9, 2022

I’m willing to setup and pay for a dedicated pool for a few hours of testing if that would be helpful.

@zzzeek
Copy link
Member

zzzeek commented Jul 9, 2022

I need the answser to number 3 above, but also if someone wants to work on a "test just the correct table only" patch, that is fine as well

@zzzeek
Copy link
Member

zzzeek commented Jul 10, 2022

OK @ww2406 has added a comment showing the original, actual bug, which is what it looked like from the beginning, which is pyodbc is being put into an invalid state it does not expect:

import pyodbc

conn_string = ''
con = pyodbc.connect(conn_string)
cur = con.cursor()
cur.execute("SELECT * FROM sys.dm_exec_sessions")
# ProgrammingError -- catalog view is not supported in this version
con.rollback()
# 111214 -- attempt to complete transaction failed

Here's why this is important:

  1. it's a bug in pyodbc, or related drivers and should be reported, or noted, or somehow understood at the driver level.
  2. it implies that maybe all, or many more rollback calls will not work on this database / driver combination ?

@gordthompson
Copy link
Member Author

gordthompson commented Jul 10, 2022

@ww2406 - Perhaps Synapse might be deferring the start of the transaction?

If you create the stored procedure

CREATE PROCEDURE [dbo].[sqla_issue_8231] 
AS
DECLARE @n int;
BEGIN
    SET NOCOUNT ON;

    SELECT @n = COUNT(*) FROM sys.system_views WHERE name = 'dm_exec_sessions';
    -- purposely try hitting the view that does not exist
    IF @n = 1
        SELECT COUNT(*) AS i FROM sys.dm_pdw_nodes_exec_sessions;
    ELSE
        SELECT COUNT(*) AS i FROM sys.dm_exec_sessions;
END

and then run the following script in SSMS

BEGIN TRANSACTION
BEGIN TRY
    EXEC dbo.sqla_issue_8231;
END TRY
BEGIN CATCH
    -- ignore error
END CATCH
ROLLBACK

does Synapse throw a "No corresponding transaction found." sort of error?

@ww2406
Copy link

ww2406 commented Jul 10, 2022

@gordthompson it does not, no error whatsoever
@zzzeek if helpful, it does succeed in some cases, e.g.

import pyodbc

conn_string = ''
con = pyodbc.connect(conn_string)
cur = con.cursor()
cur.execute("SELECT * FROM sys.dm_pdw_nodes_exec_sessions")
con.rollback()
# success

@zzzeek
Copy link
Member

zzzeek commented Jul 11, 2022

well yes the failure is related to the fact that a SQL statement fails, which is exactly when youd want to call rollback, and then the rollback fails, because somehow that SQL statement's error condition has already rolled back the transaction, pyodbc doesnt know about it, and boom. this is all wrong on someone's end.

@ww2406
Copy link

ww2406 commented Jul 11, 2022

okay, just mentioned that because you said "maybe all ... rollback calls will not work on this database" and this is one where it works.

at any rate, I believe this is a database functionality. I tested the following C# code (dotnet 5.0, Microsoft.Data.SqlClient 4.1.0) and receive a similar result.

using System;
using Microsoft.Data.SqlClient;

namespace SynapseTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting test...");

            SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();.
            builder.DataSource = "SERVER";
            builder.UserID = "UID";
            builder.Password = "PWD";
            builder.InitialCatalog = "DATABASE";

            using SqlConnection conn = new SqlConnection(builder.ConnectionString);

            conn.Open();
            
            using var txn = conn.BeginTransaction();

            using var cmd = conn.CreateCommand();
            cmd.Transaction = txn;
            cmd.CommandText = "SELECT * FROM sys.dm_exec_sessions";
            try
            {
                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }

            txn.Rollback();
            Console.WriteLine("All done");
        }
    }
}
Starting test...
Catalog view 'dm_exec_sessions' is not supported in this version.
Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): 111214;An attempt to complete a transaction has failed. No corresponding transaction found.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.TdsParser.TdsExecuteTransactionManagerRequest(Byte[] buffer, TransactionManagerRequestType request, String transactionName, TransactionManagerIsolationLevel isoLevel, Int32 timeout, SqlInternalTransact
ion transaction, TdsParserStateObject stateObj, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransactionYukon(TransactionRequest transactionRequest, String transactionName, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControl
Request)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction(TransactionRequest transactionRequest, String name, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalTransaction.Rollback()
   at Microsoft.Data.SqlClient.SqlTransaction.Rollback()
   at SynapseTest.Program.Main(String[] args) in C:\Users\user\RiderProjects\SynapseTest\Program.cs:line 37
ClientConnectionId:6bcd9b04-a875-4a8c-a535-80f3cda02707
Error Number:111214,State:1,Class:16

@ww2406
Copy link

ww2406 commented Jul 11, 2022

A third-party site suggested XACT_ABORT may default to ON on DW but I haven't found any MS documentation to substantiate this

@ww2406
Copy link

ww2406 commented Jul 11, 2022

Actually, I have, just didn't read carefully before: MS

Error Handling

If COMMIT or ROLLBACK are run and there is no active transaction, an error is raised.

If a BEGIN TRANSACTION is run while a transaction is already in progress, an error is raised. This can occur if a BEGIN TRANSACTION occurs after a successful BEGIN TRANSACTION statement or when the session is under SET AUTOCOMMIT OFF.

If an error other than a run-time statement error prevents the successful completion of an explicit transaction, Azure Synapse Analytics automatically rolls back the transaction and frees all resources held by the transaction. For example, if the client's network connection to an instance of Azure Synapse Analytics is broken or the client logs off the application, any uncommitted transactions for the connection are rolled back when the network notifies the instance of the break.

If a run-time statement error occurs in a batch, Azure Synapse Analytics behaves consistent with SQL ServerXACT_ABORT set to ON and the entire transaction is rolled back. For more information about the XACT_ABORT setting, see SET XACT_ABORT (Transact-SQL).

@gordthompson
Copy link
Member Author

gordthompson commented Jul 11, 2022

I believe this is a database functionality

Agreed. The fact that .NET SqlClient can also reproduce the error shows that it is not a pyodbc bug.

Also, the repro code for Azure from this comment does not produce an error on SQL Server 2019.

>>> import pyodbc
>>> cnxn = pyodbc.connect("DSN=mssql_199;UID=scott;PWD=tiger^5HHH")
>>> crsr = cnxn.cursor()
>>> crsr.execute("SET XACT_ABORT ON")
<pyodbc.Cursor object at 0x00000046B66CCBB0>
>>> crsr.execute("SELECT COUNT(*) AS i FROM sys.dm_pdw_nodes_exec_sessions")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
pyodbc.ProgrammingError: ('42S02', "[42S02] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Invalid object name 'sys.dm_pdw_nodes_exec_sessions'. (208) (SQLExecDirectW)")
>>> cnxn.rollback()
>>>

The two database platforms just behave differently. One possibility might be to SELECT XACT_STATE() before attempting the rollback. However, that could alter the transaction state itself, although that might not necessarily be problematic. Another option would be to simply try/catch the rollback.

@zzzeek
Copy link
Member

zzzeek commented Jul 11, 2022

my point is the same as always, DBAPI is not supposed to have restrictions on rollback(), otherwise you'd never know when you can call it or not if ceratin error conditions "autorollback" and break the driver's knowledge of the sequence of events.

this is where pyodbc pretending to be "database agnostic" (95% of all pyodbc use is for SQL server) is a problem because it really should have logic to know that certain error conditions mean the driver just did an "autorollback", unless there's some way to configure this with the odbc driver or similar.

@ww2406
Copy link

ww2406 commented Jul 11, 2022

Of course, if only a certain Redmond-based company that knows these products inside and out (hopefully) would consider writing a Python client like, idk, virtually every other database vendor…. 🤷‍♂️

Re: XACT_STATE, in my ganders through Synapse documentation, I think I saw something that MS introduced a new -2 value for XACT_STATE in PDW that doesn’t exist normally

@gordthompson
Copy link
Member Author

because [pyodbc] really should have logic to know that certain error conditions mean the driver just did an "autorollback"

Ah, but the pyodbc team would argue (rightly, IMO) that it's not their job, either. If Synapse starts a transaction at the client's request and later rolls it back arbitrarily for whatever reason then it shouldn't raise an error if the client requests the rollback that has already occurred.

We can always override do_rollback() in MSDialect_pyodbc and do a try/except there.

@zzzeek
Copy link
Member

zzzeek commented Jul 11, 2022

I would not feel comfortable squashing an exception like that for something as critical as a database rollback.

better would be if there were logic that literally ran "select can_we_rollback_here", whatever that has to be, then does the rollback.

then, we make it a recipe for those using Azure Synapse. I dont want hardcoded logic in the dialect that adds latency to all MSSQL code everywhere to work around what is IMO a major bug in Synapse and/or drivers.

@zzzeek
Copy link
Member

zzzeek commented Jul 11, 2022

Except we don't at the moment have a DialectEvents.do_rollback() method that can conditionilalize whether rollback is called or not.

So to be clear we have fully confirmed there is aboslutely no way to get the DB or ODBC driver to not do this "autorollback", and what would pyodbc devs actually tell us to do ? catch all exceptions and regexp them ?

@gordthompson
Copy link
Member Author

gordthompson commented Jul 11, 2022

@zzzeek did you see this?

If a run-time statement error occurs in a batch, Azure Synapse Analytics behaves consistent with SQL Server XACT_ABORT set to ON and the entire transaction is rolled back.

but even that is (apparently) inconsistent between Synapse and SQL Server - ref: here

@zzzeek
Copy link
Member

zzzeek commented Jul 11, 2022

I would assume when we use ordinary SQL Server we dont have XACT_ABORT set to ON.

however, perhaps the standardized nature of XACT_ABORT allows us to add an option to the SQL Server dialect overall called "xact_abort=True", meaning, we assume the DB has XACT_ABORT turned on, that gives us an excuse to augment do_rollback() to check ahead of time for aborted trans, or try/catch the rollback but it's important that we catch a very specific error and make sure the code matches xact_abort.

@zzzeek
Copy link
Member

zzzeek commented Jul 11, 2022

hi here is what I would accept:

  1. we still try to improve the tranasction isoialtion qeury to not raise an error, because it would be better to detect the correct table ahead of time
  2. for the larger issues that a lot of rollbacks (not jut this one) will place the connection into what would currently be seen as a corrupted state where it can't be returned to the pool (since the pool has to call rollback()), introduce new paramter xact_abort=True to the dialect (the base mssql dialect, not pyodbc). do_rollback() then if this parameter is True will try/catch on the rollback and ensure that the error code is exactly 111214. otherwise, it must re-raise directly.

@ww2406
Copy link

ww2406 commented Jul 11, 2022

hey @zzzeek I made a commit last night on that PR towards 1. I think the base part of it is figured out unless there are changes you'd like to see. Would you be willing to take a look, make sure I went on an okay path with it? Then I'm feeling kinda unsure how to approach developing testing for it (or do you think no additional testing beyond the existing test infrastructure is necessary)?

I'll try to take a look at 2, too, but I feel less certain about how to make that work. It seemed like the place to start is dialects\mssql\pyodbc.py?

@zzzeek
Copy link
Member

zzzeek commented Jul 12, 2022

this doesnt need new tests as coverage of the code in question is intrinsic to the database in use.

for 2, the parameter would go here since i think we are saying it should apply to any backend (noting pyodbc is the only backend we test right now), then you add a do_rollback() method to MSCompiler as well which overrides this one.

this is the idea

diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py
index ed4139ad17..576d6c830d 100644
--- a/lib/sqlalchemy/dialects/mssql/base.py
+++ b/lib/sqlalchemy/dialects/mssql/base.py
@@ -2843,6 +2843,7 @@ class MSDialect(default.DefaultDialect):
     _supports_offset_fetch = False
     _supports_nvarchar_max = False
 
+    _xact_abort_enabled = False
     legacy_schema_aliasing = False
 
     server_version_info = ()
@@ -2870,6 +2871,7 @@ class MSDialect(default.DefaultDialect):
         deprecate_large_types=None,
         json_serializer=None,
         json_deserializer=None,
+        xact_abort_enabled=True,
         legacy_schema_aliasing=None,
         **opts,
     ):
@@ -2891,6 +2893,7 @@ class MSDialect(default.DefaultDialect):
 
         self._json_serializer = json_serializer
         self._json_deserializer = json_deserializer
+        self._xact_abort_enabled = xact_abort_enabled
 
     def do_savepoint(self, connection, name):
         # give the DBAPI a push
@@ -2901,6 +2904,14 @@ class MSDialect(default.DefaultDialect):
         # SQL Server does not support RELEASE SAVEPOINT
         pass
 
+    def do_rollback(self, dbapi_connection):
+        try:
+            dbapi_connection.rollback()
+        except self.dbapi.ProgrammingError as err:
+            if self._xact_abort_enabled and "111214" in str(err):
+                return
+            raise
+
     _isolation_lookup = set(
         [
             "SERIALIZABLE",

@zzzeek
Copy link
Member

zzzeek commented Jul 12, 2022

can you reference "Fixes: #8231" in your PR so it links here, thanks

@gordthompson
Copy link
Member Author

This is what I've been working on for xact_abort:

gordthompson@de959b3

I've tested it as much as I can without Synapse, but it looks like it should work.

@zzzeek
Copy link
Member

zzzeek commented Jul 12, 2022

This is what I've been working on for xact_abort:

gordthompson@de959b3

I've tested it as much as I can without Synapse, but it looks like it should work.

great minds think alike, nice job

@gordthompson
Copy link
Member Author

This is what I've been working on for xact_abort:
gordthompson@de959b3
I've tested it as much as I can without Synapse, but it looks like it should work.

great minds think alike, nice job

Thanks.

@ww2406 - Can you test that patch in isolation (i.e., without your select-o-matic patch to use the appropriate view) and verify that it works? That is

pip install git+https://github.com/gordthompson/sqlalchemy@xact_abort

(or equivalent) and then do something like

import sqlalchemy as sa

engine = sa.create_engine(connection_url, xact_abort=True)

with engine.begin() as conn:
    pass

@ww2406
Copy link

ww2406 commented Jul 12, 2022 via email

@ww2406
Copy link

ww2406 commented Jul 13, 2022

@gordthompson sadly 111214 is still raised :(

@gordthompson
Copy link
Member Author

@gordthompson sadly 111214 is still raised :(

Rats. Okay, thanks for checking.

@ww2406
Copy link

ww2406 commented Jul 13, 2022

Absolutely! If it'd be helpful, I'd be happy to get you credentials to my personal SQL DW dedicated pool I play with occasionally for you to test directly without going through a middle man. Also happy to keep testing for you!

@gordthompson
Copy link
Member Author

Thanks, @ww2406 . Having a real DW connection certainly would be helpful to try and figure out what the heck is going on with this 111214 thing.

@ww2406
Copy link

ww2406 commented Jul 21, 2022

@gordthompson sorry for the delay, life is a bit of an adventure right now, just sent you an email with some details!

@gordthompson
Copy link
Member Author

Okay, with big thanks to @ww2406

https://github.com/gordthompson/sqlalchemy/tree/xact_abort_0721a

now has a patch that works for me against a real Synapse instance.

After doing

pip install git+https://github.com/gordthompson/sqlalchemy@xact_abort_0721a

the following code

import logging

import sqlalchemy as sa

logging.basicConfig(level=logging.INFO)

connection_url = "<insert your connection URL here>"
engine = sa.create_engine(
    connection_url,
    xact_abort=True,
)

with engine.begin() as conn:
    pass

should not fail, but simply log

INFO:root:pyodbc.ProgrammingError 111214 'No corresponding transaction found.' has been suppressed via xact_abort=True

If this looks okay then I can either

@zzzeek
Copy link
Member

zzzeek commented Jul 21, 2022

let's keep this all on one PR / gerrit if possible.

@ww2406
Copy link

ww2406 commented Jul 22, 2022

Is my patch still needed then?

@zzzeek
Copy link
Member

zzzeek commented Jul 22, 2022

i thought gord could append to your patch. either way. sorry I just wait for the "it's done, here's our solution" patch to come out before I look too closely :)

@gordthompson gordthompson removed question issue where a "fix" on the SQLAlchemy side is unlikely, hence more of a usage question awaiting info waiting for the submitter to give more information labels Jul 22, 2022
@gordthompson gordthompson added this to the 1.4.x milestone Jul 22, 2022
@sqla-tester
Copy link
Collaborator

Gord Thompson has proposed a fix for this issue in the main branch:

Fix 'No … transaction found' error on Synapse. https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/4006

@sqla-tester
Copy link
Collaborator

Gord Thompson has proposed a fix for this issue in the rel_1_4 branch:

Fix 'No transaction found' error on Synapse. https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/4020

@cwegener
Copy link

cwegener commented Aug 3, 2022

Of course, if only a certain Redmond-based company that knows these products inside and out (hopefully) would consider writing a Python client like, idk, virtually every other database vendor…. man_shrugging

A bit OT: But there is at least a whole documentation section on docs.microsoft.com about pymssql/FreeTDS ... https://docs.microsoft.com/en-us/sql/connect/python/pymssql/step-1-configure-development-environment-for-pymssql-python-development?view=sql-server-ver16

sqlalchemy-bot pushed a commit that referenced this issue Aug 4, 2022
Fixed issue where the SQL Server dialect's query for the current isolation
level would fail on Azure Synapse Analytics, due to the way in which this
database handles transaction rollbacks after an error has occurred. The
initial query has been modified to no longer rely upon catching an error
when attempting to detect the appropriate system view. Additionally, to
better support this database's very specific "rollback" behavior,
implemented new parameter ``ignore_no_transaction_on_rollback`` indicating
that a rollback should ignore Azure Synapse error 'No corresponding
transaction found. (111214)', which is raised if no transaction is present
in conflict with the Python DBAPI.

Fixes: #8231
Closes: #8233
Pull-request: #8233
Pull-request-sha: c48bd44

Change-Id: I6407a03148f45cc9eba8fe1d31d4f59ebf9c7ef7
(cherry picked from commit 8fe3cd69c5f2d8f73e75fb19ae929273282fba57)
@iamshaikarshad
Copy link

Hi Guys,

I initially i was able to connect to the syanpse dedicated pool and perform GET request, However was not able to perform POST request and received the same error.

I followed the above approach of:
pip install git+https://github.com/gordthompson/sqlalchemy@xact_abort_0721a
and
params = urllib.parse.quote_plus("Driver={ODBC Driver 17 for SQL Server};Server=tcp:database.sql.azuresynapse.net,1433;Database=dedicatedpool;Uid=<Username>;Pwd=<Password>;Encrypt=yes;Connection Timeout=30;Authentication=ActiveDirectoryPassword;xact_abort=True")

But after performing this action I was not evern able to connect with the server.

With error same as earlier.
o_rollback dbapi_connection.rollback() sqlalchemy.exc.ProgrammingError: (pyodbc.ProgrammingError) ('42000', '[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]111214;An attempt to complete a transaction has failed. No corresponding transaction found. (111214) (SQLEndTran)') (Background on this error at: https://sqlalche.me/e/20/f405)

Can you please advise me on this, if there are any additional steps to be performed?

@zzzeek
Copy link
Member

zzzeek commented Oct 30, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
external driver issues the issue involves a misbehavior on the part of the DBAPI itself, probably not SQLAlchemy SQL Server Microsoft SQL Server, e.g. mssql
Projects
None yet
Development

No branches or pull requests

7 participants