Skip to content

sqlite3 module breaks transactions and potentially corrupts data #54949

Closed

Description

@scotturban
BPO 10740
Nosy @pitrou, @ned-deily, @bitdancer, @asvetlov, @florentx, @dholth, @Bluehorn, @RonnyPfannschmidt, @berkerpeksag, @JimJJewett, @zooba, @animalize, @jan-hudec, @carlwgeorge, @maggyero
Files
  • pysql-transactions.2.diff: proposed patch
  • test_sqlite_ddl.py: test script
  • sqlite_transaction_config_py27.diff: Patch against Python 2.7
  • sqlite_transaction_config_py3.diff: Patch for Python 3 default branch
  • sqlite_transaction_config_py27.diff: Patch against Python 2.7, take 2
  • sqlite_transaction_config_v2.diff: Updated patch: Added docs
  • transaction-api-proposal-issues-8145-9924-10740-16958.patch: API improvement proposal (as doc patch)
  • sqlite_sanity_check.rst
  • issue10740-aaugustin.patch
  • test_sqlite.py
  • issue10740_upstream.diff
  • issue10740_upstream_v2.diff
  • cant_vacuum.py
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2016-09-11.11:56:25.732>
    created_at = <Date 2010-12-20.18:02:11.675>
    labels = ['type-feature', 'library']
    title = 'sqlite3 module breaks transactions and potentially corrupts data'
    updated_at = <Date 2020-01-26.19:27:00.405>
    user = 'https://bugs.python.org/scotturban'

    bugs.python.org fields:

    activity = <Date 2020-01-26.19:27:00.405>
    actor = 'maggyero'
    assignee = 'none'
    closed = True
    closed_date = <Date 2016-09-11.11:56:25.732>
    closer = 'berker.peksag'
    components = ['Library (Lib)']
    creation = <Date 2010-12-20.18:02:11.675>
    creator = 'scott.urban'
    dependencies = []
    files = ['20118', '20119', '21417', '21418', '21419', '21479', '35498', '35780', '40252', '42251', '42316', '44437', '45949']
    hgrepos = []
    issue_num = 10740
    keywords = ['patch']
    message_count = 69.0
    messages = ['124392', '124393', '124398', '124407', '128815', '132285', '132287', '132414', '132470', '132612', '136362', '144197', '207789', '207796', '207800', '209900', '209901', '209904', '209917', '209924', '216715', '216732', '216734', '216738', '216740', '219757', '219758', '219759', '219761', '219806', '219818', '219884', '220478', '220491', '220500', '220501', '220519', '220533', '220555', '220566', '220602', '220615', '221568', '248816', '248830', '248833', '248839', '248915', '248917', '249111', '262241', '262254', '262268', '262280', '262281', '262573', '274810', '274816', '274935', '275760', '283528', '283529', '283533', '283919', '283921', '360665', '360703', '360729', '360733']
    nosy_count = 26.0
    nosy_names = ['ghaering', 'pitrou', 'ned.deily', 'Jeremy Banks', 'r.david.murray', 'zzzeek', 'asvetlov', 'flox', 'adamtj', 'dholth', 'torsten', 'monsanto', 'scott.urban', 'aymeric.augustin', 'tshepang', 'python-dev', 'Ronny.Pfannschmidt', 'berker.peksag', 'Mark.Bucciarelli', 'Jim.Jewett', 'steve.dower', 'malin', 'bulb', 'bkircher', 'carlwgeorge', 'maggyero']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue10740'
    versions = ['Python 3.6']

    Activity

    1. scotturban commented on Dec 20, 2010

      scotturbanmannequin
      MannequinAuthor

      The python sqlite module automatically commits open transactions
      when it encounters a DDL statement. This is unnecessary; DDL is
      transactional in my testing (see attached).

      Attached patch addresses the issue. Patch is against 2.6.1, but
      looking at Trunk in svn, it seems like the patch is needed and
      would apply. One issue I could foresee is that this behavior might
      depend on the sqlite version in use (I'm on 3.6.10).

      Patch also allows pragma statement.

    2. added
      type-bugAn unexpected behavior, bug, or error
      stdlibStandard Library Python modules in the Lib/ directory
      on Dec 20, 2010
    3. scotturban commented on Dec 20, 2010

      scotturbanmannequin
      MannequinAuthor

      Here are some tests.

    4. bitdancer commented on Dec 20, 2010

      @bitdancer
      Member

      See also bpo-8145. It would be nice if someone could sort all this out, but I'm not knowledgeable enough to do so.

      For this patch, it would be a significant change it behaviour. Therefore it would have to be a new feature controlled by a flag of some sort (which is part of the reason for the reference to bpo-8145).

    5. added
      type-featureA feature request or enhancement
      and removed
      type-bugAn unexpected behavior, bug, or error
      on Dec 20, 2010
    6. scotturban commented on Dec 20, 2010

      scotturbanmannequin
      MannequinAuthor

      I find the way that the sqlite3 module handles transactions pretty
      surprising in general, but I agree that someone who got used
      to DDL not rolling back could in theory find this patch surprising.

      We will apply this patch to our python build because having DDL
      covered by a transactions is convenient for certain tasks. If anyone
      can think of a problem with this simple patch, I'd like to hear it.

      Thanks
      Scott

    7. dholth commented on Feb 18, 2011

      dholthmannequin
      Mannequin

      I want transactional DDL too. I was tremendously surprised that I could not duplicate the way sqlite3 behaves on the command line from witin pysqlite.

      Instead of this patch, I would rather be able to instruct pysqlite to always begin a transaction for any kind of statement (I hear this is a requirement for DB-API compliance) and never perform an implicit commit for any reason. For example, someone on the google code project had a complaint that 'SAVEPOINT' (create a subtransaction) automatically commits because pysqlite doesn't know about it.

      I tried to trick pysqlite into doing what I wanted by prepending /* update */ to every CREATE TABLE statement but it didn't seem to quite work. Instead, I would prefer a pysqlite that does not strcmp(statement) at all.

    8. Bluehorn commented on Mar 27, 2011

      Bluehornmannequin
      Mannequin

      The attached patch is my take on this issue. I ran into the problem that during schema upgrades dropping a table was not rolled back. In another instance, renaming a table was not rolled back. This greatly increases the risk of data loss for our application.

      Because I do not currently foresee which commands might need dropping out of a transaction and because of backwards compatibility woes, I added a new field to the sqlite3.Connection: operation_needs_transaction_callback

      This function is called to decide if a running transaction should be implicitly committed (I'd consider this dangerous), if a transaction has to be started if not running (should normally always hold) or if the transaction state should be left alone.

      For example, the "pragma foreign_keys = ..." is a no-op inside a transaction, therefore an implicit begin would be possibly harmful. In our application, we enable foreign keys when getting the connection out of the SQLAlchemy pool via a pool listener, which would be disabled if there is an implicit begin triggered.

      The patch also adds a bunch of unit tests to cover the new behaviour.

    9. Bluehorn commented on Mar 27, 2011

      Bluehornmannequin
      Mannequin

      Same patch for Python 3. In fact, this also adds a missing Py_XDECREF.

    10. dholth commented on Mar 28, 2011

      dholthmannequin
      Mannequin

      Torsten basically you are suggesting that PRAGMA would never work at all with my 'do not strcmp() the sql at all, always begin a transaction' approach?

    11. Bluehorn commented on Mar 29, 2011

      Bluehornmannequin
      Mannequin

      Torsten basically you are suggesting that PRAGMA would never work at all with my 'do not strcmp() the sql at all, always begin a transaction' approach?

      No. Most pragmas should still work and getting the current setting of a pragma should also work.

      I was only referring to http://www.sqlite.org/pragma.html#pragma_foreign_keys

      Quote:

      This pragma is a no-op within a transaction; foreign key constraint enforcement may only be enabled or disabled when there is no pending BEGIN or SAVEPOINT.

      I did not see such a restriction for other pragmas though I admit that I did not reread the list in full.

    12. Bluehorn commented on Mar 30, 2011

      Bluehornmannequin
      Mannequin

      The attached patch is an updated version which adds a bit of documentation.

    13. Bluehorn commented on May 20, 2011

      Bluehornmannequin
      Mannequin

      I updated the patch for upstream pysqlite2. Available at

      http://code.google.com/p/pysqlite/issues/detail?id=24

      Patch over there is for Python 2 (tested with our production Python 2.6).

    14. 52 remaining items

    15. aymericaugustin commented on Sep 7, 2016

      aymericaugustinmannequin
      Mannequin

      The latest patch removes the current statement parsing and unexpected implicit commits. It looks good to me.

      Unfortunately it also introduces a new kind of statement parsing that detects DDL statements and doesn't open a transaction in that case, while it should. See ghaering/pysqlite#105 for details.

      As a consequence, this change will allow Django to remove one of the two special cases for transaction in SQLite:
      https://github.com/django/django/blob/master/django/db/transaction.py#L159-L166

      but not the other:
      https://github.com/django/django/blob/271581df606b307d89c141e8b1a50ace763bea81/django/db/backends/base/base.py#L375-L404

      It's still a step forward so I support merging it.

    16. zooba commented on Sep 8, 2016

      @zooba
      Member

      I'm in favor of merging Berker's patch, once the string comparison is made a little more robust. Right now there's a risk of matching a prefix rather than a whole word, and potentially overrunning the buffer (unless I've forgotten how stricmp works).

    17. python-dev commented on Sep 11, 2016

      python-devmannequin
      Mannequin

      New changeset 284676cf2ac8 by Berker Peksag in branch 'default':
      Issue bpo-10740: sqlite3 no longer implicitly commit an open transaction before DDL statements
      https://hg.python.org/cpython/rev/284676cf2ac8

    18. animalize commented on Dec 18, 2016

      animalizemannequin
      Mannequin

      I'm using Python 3.6.0 RC2, I don't know if it's a bug.

      When I try to run VACUUM command, an exception raised:

      conn.execute('begin')  # <- remove this line get the same result
      conn.execute('VACUUM')

      sqlite3.OperationalError: cannot VACUUM from within a transaction

      ~~~~~~~~~~~~~~~~~~~
      On Python 3.5, everything is fine.
      Attached file cant_vacuum.py is the full example code.

    19. bitdancer commented on Dec 18, 2016

      @bitdancer
      Member

      It is an unexpected change in behavior and is therefore probably a bug. It will work if you set isolation_level=None. Because of that I don't think it is a release critical bug, though one could wish we'd discovered it earlier. Please open a new issue for this.

    20. animalize commented on Dec 18, 2016

      animalizemannequin
      Mannequin

      bpo-29003 created

    21. carlwgeorge commented on Dec 24, 2016

      carlwgeorgemannequin
      Mannequin

      While attempting to build a Python 3.6 RPM for RHEL/CentOS 6, I noticed the following warning.

      *** WARNING: renaming "_sqlite3" since importing it failed: build/lib.linux-i686-3.6-pydebug/_sqlite3.cpython-36dm-i386-linux-gnu.so: undefined symbol: sqlite3_stmt_readonly

      The resolution of this issue introduced usage of the sqlite3_stmt_readonly interface. That interface wasn't added to sqlite until 3.7.4 (http://www.sqlite.org/releaselog/3_7_4.html). My RPM build failed because RHEL/CentOS 6 only has sqlite 3.6.20. I understand that Python can't support old libraries forever, but can this minimum sqlite version be noted somewhere in the documentation?

    22. bitdancer commented on Dec 24, 2016

      @bitdancer
      Member

      Please open a new issue for this request.

    23. maggyero commented on Jan 25, 2020

      maggyeromannequin
      Mannequin

      With Berker Peksag's patch merged in 2016, in the default non auto-commit mode, sqlite3 implicitly issues a BEGIN statement before any non SELECT statement not already in a transaction, EXCEPT DDL statements, for backwards compatibility reasons:

      + /* For backwards compatibility reasons, do not start a transaction if a
      + DDL statement is encountered. If anybody wants transactional DDL,
      + they can issue a BEGIN statement manually. */
      + if (self->connection->begin_statement && !sqlite3_stmt_readonly(self->statement->st) && !self->statement->is_ddl) {
      + if (sqlite3_get_autocommit(self->connection->db)) {
      + result = _pysqlite_connection_begin(self->connection);
      + if (!result) {
      + goto error;
      + }
      + Py_DECREF(result);
      }
      }

      Is there any plan to cover DDL statements as well in a future Python version and deprecate the old behaviour? That would avoid having to insert BEGIN statements manually for getting transactional DDL statements, like in psycopg2 for PostgreSQL databases.

    24. bitdancer commented on Jan 25, 2020

      @bitdancer
      Member

      Please open a new issue for this question.

    25. maggyero commented on Jan 26, 2020

      maggyeromannequin
      Mannequin

      @Aymeric Augustin

      While you're there, it would be cool to provide "connection.autocommit = True" as an API to enable autocommit, because "connection.isolation_level = None" isn't a good API at all -- it's very obscure and has nothing to do with isolation level whatsoever.

      +1. We could use this new autocommit property to enable the new full transactional mode (that is to say with transactional DDL):

      connection.autocommit = True  # enable the autocommit mode
      connection.autocommit = False  # disable the autocommit mode (enable the full transactional mode)
      connection.autocommit = None  # fallback to connection.isolation_level
      

      To transition from the old partial transactional mode (without transactional DDL) by default to the new full transactional mode (with transactional DDL) by default, we could use the following migration strategy:

      1. During the deprecation period:
      • Add the new autocommit property with the value None by default, so that the old partial transactional mode is still the default.
      • Add a deprecation warning for the value None of the autocommit property, in favor of the other values True and False. It will prompt users who enabled the autocommit mode with connection.isolation_level = None to use connection.autocommit = True instead, and users who disabled the autocommit mode (that is to say users who enabled the old partial transactional mode) with connection.isolation_level = DEFERRED/IMMEDIATE/EXCLUSIVE to use connection.autocommit = False instead AND add to their code the potentially missing connection.commit() calls required by the new full transactional mode for committing DDL statements.
      1. After the deprecation period:
      • Set the value of the autocommit property to False by default, so that the new full transactional mode becomes the new default.
      • Remove the value None of the autocommit property and its deprecation warning.
      • Remove the value None of the isolation_level property, so that the old partial transactional mode disappears.
    26. maggyero commented on Jan 26, 2020

      maggyeromannequin
      Mannequin

      @r. David Murray

      Please open a new issue for this request.

      Done here: https://bugs.python.org/issue39457

    27. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      stdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions