Visitar URL original
SQLite rowcount is corrupted when combining UPDATE RETURNING w/ table that is dropped and recreated · Issue #93421 · python/cpython · GitHub
Skip to content

SQLite rowcount is corrupted when combining UPDATE RETURNING w/ table that is dropped and recreated  #93421

Description

@zzzeek

version info:

$ python
Python 3.10.0 (default, Nov  5 2021, 17:23:47) [GCC 11.2.1 20210728 (Red Hat 11.2.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.36.0'

we have a test suite that creates a table, runs some SQL, then drops it. if multiple tests run that each perform this task, if the same SQLite connection is used, rowcount starts returning "0". Seems to also require RETURNING to be used. Full demonstration:

import os
import sqlite3



def go():
    """function creates a new table, runs INSERT/UPDATE, drops table,
    commits connection.

    """

    # create table
    cursor = conn.cursor()
    cursor.execute(
        """CREATE TABLE some_table (
        id INTEGER NOT NULL,
        value VARCHAR(40) NOT NULL,
        PRIMARY KEY (id)
    )
    """
    )
    cursor.close()
    conn.commit()

    # run operation
    cursor = conn.cursor()
    cursor.execute(
        "INSERT INTO some_table (id, value) VALUES (1, 'v1')"
    )
    ident = 1

    cursor.execute(
        "UPDATE some_table SET value='v2' "
        "WHERE id=? RETURNING id",
        (ident,),
    )
    new_ident = cursor.fetchone()[0]
    assert ident == new_ident
    assert cursor.rowcount == 1, cursor.rowcount
    cursor.close()

    # drop table
    cursor = conn.cursor()
    cursor.execute("DROP TABLE some_table")
    cursor.close()

    conn.commit()

if os.path.exists("file.db"):
    os.unlink("file.db")

# passes
conn = sqlite3.connect("file.db")
go()

# run again w/ new connection (same DB), passes
conn = sqlite3.connect("file.db")
go()

print("FAILURE NOW OCCURS")
# run again w/ same connection, fails
go()

on the third run, where we ran the "test" on the same connection twice, it fails:

$  python test3.py 
FAILURE NOW OCCURS
Traceback (most recent call last):
  File "/home/classic/dev/sqlalchemy/test3.py", line 62, in <module>
    go()
  File "/home/classic/dev/sqlalchemy/test3.py", line 39, in go
    assert cursor.rowcount == 1, cursor.rowcount
AssertionError: 0

it would appear there's some internal caching of table state that needs to be cleared.

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Jun 1, 2022
  2. AA-Turner commented on Jun 1, 2022

    @AA-Turner
    Member
  3. erlend-aasland commented on Jun 2, 2022

    @erlend-aasland
    Contributor

    Thanks for the report!

    Internally, we simply use the function sqlite3_changes to set rowcount1, implying that we do not calculate rowcount ourself; the value comes straight from the underlying SQLite library. If sqlite3_changes returns zero, rowcount will be zero.

    I've verified that this only happens with UPDATE ... RETURNING statements, which also implies that the behaviour we are observing comes from the underlying SQLite library. I suggest to report this on the SQLite forum (they do not have a bug tracker).

    Note: I did some tests where I used sqlite3_total_changes() in addition to sqlite3_changes(), and they revealed that the number of total changes on the database connection did not increase whith UPDATE ... RETURNING statements, only with normal UPDATE statements.

    Closing this as not-a-bug.

    See also:

    Footnotes

    1. Assuming that you've supplied a valid statement, and that no database errors were raised by SQLite. In case of errors, rowcount will normally be set to -1. ↩

  4. moved this from Done to Discarded in sqlite3 issueson Jun 2, 2022
  5. removed
    type-bugAn unexpected behavior, bug, or error
    3.11only security fixes
    on Jun 2, 2022
  6. 25 remaining items

  7. Repository owner moved this from In Progress to Done in sqlite3 issueson Jun 8, 2022
  8. added a commit that references this issue on Jun 8, 2022
  9. added a commit that references this issue on Jun 8, 2022
  10. added a commit that references this issue on Jun 8, 2022
  11. erlend-aasland commented on Jun 8, 2022

    @erlend-aasland
    Contributor

    Thanks for the report, and for pushing this through, Mike. Thanks for reviewing, Ma Lin!

  12. added a commit that references this issue on Jun 8, 2022
  13. added a commit that references this issue on Jun 8, 2022
  14. erlend-aasland commented on Jun 8, 2022

    @erlend-aasland
    Contributor

    Just thought, if the next row is cached (like pre 3df0fc8), the .rowcount will be more correct?

    We'd still only update .rowcount after SQLITE_DONE, so when the statement is stepped through, you'll have the correct row count, caching or not.

    For example, in these conditions: [...]

    That is a separate issue (and it has always been). It is not very large or complex fix; we just need to step through a statement for each loop iteration in _pysqlite_query_execute if multiple is true, kinda like we do in executescript. It is a bug, though, so it would be quite ok to backport it through 3.10.

    However, as you point out, such code is probably rare; it does not make sense with UPDATE ... RETURNING in executemany.

  15. rt121212121 commented on Dec 1, 2022

    @rt121212121

    As a data point, I have a MacOS user who is using 3.10.8, and getting the rowcount 0 problem. From what I understand the 3.10 backport was merged in June and 3.10.8 was tagged as of October, so should have the fix from what I understand.

    This is an UPDATE ... RETURNING execute that asserts rowcount == 1, which fails because it is unexpectedly 0.

  16. erlend-aasland commented on Dec 2, 2022

    @erlend-aasland
    Contributor

    As a data point, I have a MacOS user who is using 3.10.8, and getting the rowcount 0 problem. From what I understand the 3.10 backport was merged in June and 3.10.8 was tagged as of October, so should have the fix from what I understand.

    This is an UPDATE ... RETURNING execute that asserts rowcount == 1, which fails because it is unexpectedly 0.

    Would you mind posting a minimal reproducer?

    Python 3.10.8 should have the fix, yes.

  17. rt121212121 commented on Dec 5, 2022

    @rt121212121

    The reproducer from the top does not work for him. It'll happen in the course of operation of the larger application repeatedly however. No idea how to reproduce it outside of that.

  18. rt121212121 commented on Jan 18, 2023

    @rt121212121

    Finally had the problem happen myself. It appears to be a different problem than tables being dropped or recreated. Repro on the above linked issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

3.11only security fixes3.12only security fixestopic-sqlite3type-bugAn unexpected behavior, bug, or error

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions