Thanks for the information! Indeed, the workaround solved my problem.

Is there a downside to unconditionally activating the workaround, or should 
I check for use of pysqlite?

from sqlite3 import Connection as _sqlite3_Connection

from sqlalchemy import event as _event
from sqlalchemy.engine import Engine as _Engine

@_event.listens_for(_Engine, 'connect')
def do_connect(dbapi_connection, connection_record):
    if isinstance(dbapi_connection, _sqlite3_Connection):
        # disable pysqlite's emitting of the BEGIN statement entirely.
        # also stops it from emitting COMMIT before any DDL.
        logger.debug('setting connection isolation level to `None` to work 
around pysqlite bug')
        dbapi_connection.isolation_level = None

@_event.listens_for(_Engine, 'begin')
def do_begin(connection):
    if isinstance(connection._Connection__connection.connection, 
_sqlite3_Connection):
        # emit our own BEGIN
        logger.debug('emitting our own BEGIN to work around pysqlite bug')
        connection.execute('BEGIN')

My only other question is, for future debugging, where should I log to get 
*all* the SQL that is ultimately passed to the database?

Thanks,
Scott


On Monday, May 28, 2018 at 6:29:14 PM UTC-7, Mike Bayer wrote:
>
>
>
> On Mon, May 28, 2018, 8:00 PM Mike Bayer <mik...@zzzcomputing.com 
> <javascript:>> wrote:
>
>> On Mon, May 28, 2018 at 7:23 PM, Scott Colby <scol...@gmail.com 
>> <javascript:>> wrote:
>> > Hello all,
>> >
>> > I am working on testing a project that uses SQLalchemy with the pytest
>> > testing framework. I have adapted the code from the docs to pytest as 
>> shown.
>> >
>> > logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
>> >
>> > cursor_handler = logging.FileHandler('cursor.log')
>> > cursor_log = logging.getLogger('cursor_log')
>> > cursor_log.addHandler(cursor_handler)
>> > cursor_log.setLevel(logging.DEBUG)
>> > cursor_log.propagate = false
>> >
>> >
>> > @pytest.fixture(scope='session')
>> > def created_database_path(tmpdir_factory):
>> >  db_path = tmpdir_factory.mktemp('ocspdash').join('ocspdash.db')
>> >
>> >  engine = create_engine(f'sqlite:///{db_path}')
>> >  Base.metadata.create_all(engine)
>> >
>> >  yield db_path
>> >
>> > @pytest.fixture(scope='session')
>> > def manager_session(created_database_path):
>> >  engine = create_engine(f'sqlite:///{created_database_path}')
>> >
>> >  @event.listens_for(engine, 'before_cursor_execute')
>> >  def receive_before_cursor_execute(connection, cursor, statement,
>> > parameters, context, executemany):
>> >  cursor_log.debug(statement)
>> >  cursor_log.debug(parameters)
>> >
>> >  connection = engine.connect()
>> >
>> >  session_maker = sessionmaker(bind=connection)
>> >  session = scoped_session(session_maker)
>> >
>> >  @event.listens_for(session, 'after_transaction_end')
>> >  def restart_savepoint(session, transaction):
>> >  if transaction.nested and not transaction._parent.nested:
>> >  # ensure that state is expired the way
>> >  # session.commit() normally does
>> >  session.expire_all()
>> >
>> >  session.begin_nested()
>> >
>> >  transaction = connection.begin()
>> >  session.begin_nested()
>> >
>> >  manager = Manager(
>> >  engine=engine,
>> >  session=session,
>> >  )
>> >
>> >  yield manager, connection
>> >
>> >  session.close()
>> >  transaction.rollback()
>> >
>> >  connection.close()
>> >
>> >
>> > Pytest fixtures are sort of like context managers: everything before the
>> > `yield` is the set up and after the `yield` is the tear down.
>> >
>> > I have separated the `create_all` part because SQLalchemy seems to issue
>> > unnecessary COMMITs in the process of creating the tables:
>>
>>
>> the create_all() needs to be called with the connection that you've
>> started the transaction within:
>>
>> trans = connection.begin()
>> metadata.create_all(connection)
>>
>> then no COMMITs will be emitted.
>>
>>
>> >
>> >
>> > These commits would prematurely end the transaction created by
>> > `connection.begin()`.
>>
>> is that when using a file-based SQLIte database or :memory: ?   if a
>> file based SQLite database, it would be using a separate connection.
>>
>>
>> >
>> > But here is the real problem. Consider the output in `cursor.log` of one
>> > test function:
>> > The ROLLBACK is missing in the 'before_cursor_execute' events. Why is 
>> this?
>>
>> When using the Python DBAPI, you don't emit the string "ROLLBACK", you
>> call connection.rollback() where the DBAPI does the actual
>> implementation, in the case of SQLite it would be the ROLLBACK string.
>> See https://www.python.org/dev/peps/pep-0249/#rollback
>>
>> >
>> > Adding an extra `engine.execute('ROLLBACK')` at the end raises an
>> > appropriate error about there being no transaction to end.
>>
>> that's correct because it was already rolled back.
>>
>> >
>> > However, if I open up the test database:
>> >
>> > $ sqlite3 test.db
>> > SQLite version 3.19.3 2017-06-27 16:48:08
>> > Enter ".help" for usage hints.
>> > sqlite> select * from authority;
>> > 1|Test Authority|2345|2018-05-28 22:59:28
>> > sqlite>
>> >
>> > It's still in there!
>>
>> you're using SAVEPOINT with the pysqlite driver which requires this 
>> workaround:
>>
>>
>> http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl
>>
>> that section overall should provide some perspective about how the
>> driver gets involved, and pysqlite's is particualrly intervening in an
>> effort to reduce file lock contention.
>>
>
> That workaround also allows transactional ddl to work, the pysqlite driver 
> is otherwise also emitting COMMIT for every ddl statement so perhaps that's 
> what you were seeing since it looks like you were logging from the driver 
> side.
>
>
>
>
>>
>> >
>> > So here are my questions:
>> >
>> > why does `engine.create_all` issue unnecessary COMMITs?
>>
>> you mean "metadata.create_all()". if you hand it the engine, then you
>> are using connectionless execution which only runs in "autocommit"
>> mode, e.g. commit for every DDL / DML statement.   See
>>
>> http://docs.sqlalchemy.org/en/latest/core/connections.html#understanding-autocommit
>> as well as the section that follows.  give it the connection that
>> you've begun the transaction upon.
>>
>> > why is the final ROLLBACK not showing up in the 'before_cursor_execute'
>> > event?
>>
>> SQLAlchemy calls the connection.rollback() and connection.commit()
>> pep-249 methods for transaction rollback/commit and no SQL is rendered
>> on the SQLAlchemy side.
>>
>> > what am I doing wrong that the transaction doesn't actually get rolled 
>> back?
>>
>> probably the SAVEPOINT workaround for pysqlite.
>>
>> >
>> > (I know that `scoped_session` might not be the most appropriate thing 
>> to use
>> > here, but that is how the real code works and I want to test in a 
>> similar
>> > environment; additionally, when I tried this whole exercise with a 
>> normal
>> > `sessionmaker`, all these problems happened in an identical manner.)
>>
>> if you're manipulating transactions, work with engines/connections and
>> transaction, and just have the Session hang on within the inside of
>> the transaction.    The connection/transaction
>> begin()/commit()/rollback() API has a nesting behavior such that only
>> the outermost begin()/commit() actually demarcate the transaction; a
>> rollback() at any level rolls back the transaction immediately.
>> This is illustrated at
>>
>> http://docs.sqlalchemy.org/en/latest/core/connections.html#nesting-of-transaction-blocks
>> .
>>
>>
>> >
>> > Thanks,
>> > Scott Colby
>> >
>> > --
>> > SQLAlchemy -
>> > The Python SQL Toolkit and Object Relational Mapper
>> >
>> > http://www.sqlalchemy.org/
>> >
>> > To post example code, please provide an MCVE: Minimal, Complete, and
>> > Verifiable Example. See http://stackoverflow.com/help/mcve for a full
>> > description.
>> > ---
>> > You received this message because you are subscribed to the Google 
>> Groups
>> > "sqlalchemy" group.
>> > To unsubscribe from this group and stop receiving emails from it, send 
>> an
>> > email to sqlalchemy+...@googlegroups.com <javascript:>.
>> > To post to this group, send email to sqlal...@googlegroups.com 
>> <javascript:>.
>> > Visit this group at https://groups.google.com/group/sqlalchemy.
>> > For more options, visit https://groups.google.com/d/optout.
>>
>

-- 
SQLAlchemy - 
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to