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
SQLite rowcount is corrupted when combining UPDATE RETURNING w/ table that is dropped and recreated #93421
Comments
cc: @erlend-aasland A |
Thanks for the report! Internally, we simply use the function I've verified that this only happens with Note: I did some tests where I used Closing this as not-a-bug. See also:
Footnotes
|
For the record; we explicitly clear |
post is up at https://sqlite.org/forum/forumpost/33ea0f369f |
just to keep track here, the equivalent commands in the sqlite3 shell should be (please review to make sure this is accurate): CREATE TABLE some_table (
id INTEGER NOT NULL,
value VARCHAR(40) NOT NULL,
PRIMARY KEY (id)
);
BEGIN;
INSERT INTO some_table (value) VALUES ('v1');
SELECT last_insert_rowid();
UPDATE some_table SET value='v2' WHERE id=1 RETURNING id;
SELECT changes();
DROP TABLE some_table;
COMMIT; these commands can be run repeatedly in the sqlite3 shell and the results are correct each time. |
hi there - I'd like to reopen this. per discussion at https://sqlite.org/forum/forumpost/397abefa91 this is still a bug in pysqlite. The incorrect cursor.rowcount value returned by sqlite does not match running the query manually on the failed run, indicating SQLite is in fact returning the correct value, pysqlite is somehow getting the wrong value. Can we please look into this? new demonstration below compares 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 (value) VALUES ('v1')")
ident = cursor.lastrowid
cursor.execute(
"UPDATE some_table SET value='v2' " "WHERE id=? RETURNING id",
(ident,),
)
new_ident = cursor.fetchone()[0]
assert ident == new_ident
cursor_rowcount = cursor.rowcount
manual_rowcount = cursor.execute("select changes()").fetchone()[0]
assert cursor_rowcount == manual_rowcount, (
f"Cursor rowcount {cursor_rowcount} does not match "
f"SELECTed rowcount {manual_rowcount}"
)
print(
f"Cursor rowcount {cursor_rowcount} matches SELECTed rowcount "
f"{manual_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")
print("------------------- pass one -----------------------")
# passes
conn = sqlite3.connect("file.db")
go()
print("------------------- pass two -----------------------")
# run again w/ new connection (same DB), passes
conn = sqlite3.connect("file.db")
go()
print("------------------- pass three -----------------------")
# run again w/ same connection, fails
go() output:
|
@erlend-aasland - I reproduced Mike's script locally and reopened for more discussion as it seems there might be something A |
Thanks for reopening and investigating this. I can reproduce it, and I've got a fix for it. I want to narrow down a C reproducer before I open a PR. FTR, we are calling the And, per the discussion over at the SQLite forum, yes there are some inconveniences in the sqlite3 API, but we cannot just change them, because people depend on the inconveniences. The SQLite C API also have some inconveniences1, but I would not call it names because of that. Footnotes
|
Here's a SQLite C API repro: https://gist.github.com/erlend-aasland/5e48b920d882e04f911fe7871fd5ccd9 As you can see, EDIT: |
If we change the implementation of An alternative could be to update Another solution can be to save the total change count ( I don't have a strong preference; but I feel the latter solution will result in the most sane behaviour for the Slightly related: we can fix @serhiy-storchaka, do you have an opinion here? |
Fun fact: changing Mike's reproducer to insert and update two or more values results in interesting values returned by Two rows, same update query$ ./test
Compiled with SQLite 3.26.0
Executing with SQLite 3.36.0
executing 'CREATE TABLE some_table ( id INTEGER NOT NULL, value VARCHAR(40) NOT NULL, PRIMARY KEY (id))'
- step, changes=0
executing 'BEGIN'
- step, changes=0
executing 'INSERT INTO some_table (id, value) VALUES (1, 'v1'), (2, 'v1')'
- step, changes=2
executing 'UPDATE some_table SET value='v2' WHERE id=1 RETURNING id'
- step, changes=2
- step, changes=1
executing 'DROP TABLE some_table'
- step, changes=1
executing 'COMMIT'
- step, changes=1
executing 'CREATE TABLE some_table ( id INTEGER NOT NULL, value VARCHAR(40) NOT NULL, PRIMARY KEY (id))' (cached)
- step, changes=1
executing 'BEGIN' (cached)
- step, changes=1
executing 'INSERT INTO some_table (id, value) VALUES (1, 'v1'), (2, 'v1')' (cached)
- step, changes=2
executing 'UPDATE some_table SET value='v2' WHERE id=1 RETURNING id' (cached)
- step, changes=0
- step, changes=1
executing 'DROP TABLE some_table' (cached)
- step, changes=1
executing 'COMMIT' (cached)
- step, changes=1 Three rows, loosened update query$ ./test
Compiled with SQLite 3.26.0
Executing with SQLite 3.36.0
executing 'CREATE TABLE some_table ( id INTEGER NOT NULL, value VARCHAR(40) NOT NULL, PRIMARY KEY (id))'
- step, changes=0
executing 'BEGIN'
- step, changes=0
executing 'INSERT INTO some_table (id, value) VALUES (1, 'v1'), (2, 'v1'), (3, 'v1')'
- step, changes=3
executing 'UPDATE some_table SET value='v2' WHERE id<3 RETURNING id'
- step, changes=3
- step, changes=3
- step, changes=2
executing 'DROP TABLE some_table'
- step, changes=2
executing 'COMMIT'
- step, changes=2
executing 'CREATE TABLE some_table ( id INTEGER NOT NULL, value VARCHAR(40) NOT NULL, PRIMARY KEY (id))' (cached)
- step, changes=2
executing 'BEGIN' (cached)
- step, changes=2
executing 'INSERT INTO some_table (id, value) VALUES (1, 'v1'), (2, 'v1'), (3, 'v1')' (cached)
- step, changes=3
executing 'UPDATE some_table SET value='v2' WHERE id<3 RETURNING id' (cached)
- step, changes=0
- step, changes=0
- step, changes=2
executing 'DROP TABLE some_table' (cached)
- step, changes=2
executing 'COMMIT' (cached)
- step, changes=2 I guess that rules out the update-rowcount-via-the-fetch-one-handler approach, so we are stuck with the total-changes solution, AFAICS. |
I'll create a competing PR for this approach. UPDATE: I created #93526 which updates |
I have no opinion and I think you are more expedient in this. Only a question. If |
According to the DB API (as I understand it), So either we use
Footnotes |
Ah, so |
Good observation. So until recently, I was leaning towards the total-changes solution (gh-93520). But I just found out that there are other differences.
Let me explain that test again; it came out all wrong:
Footnotes |
For the record, here's the test code I've been using; it's a variant of the SQLite C API reproducer for this issue: https://gist.github.com/erlend-aasland/98b41dab6cc51c1082c70eef3659d8d1 |
Regarding gh-93520, here's an excerpt from the SQLite docs for
The last part is troublesome, and makes me lean towards gh-93526, though 2^31 is a pretty large number. What speaks for gh-93520, is that it will make it possible to solve #79579 by using cc. @animalize |
…pythonGH-93526) (cherry picked from commit 875de61) Co-authored-by: Erlend Egeberg Aasland <erlend.aasland@protonmail.com>
…TE_DONE (pythonGH-93526) (cherry picked from commit 875de61)
Thanks for the report, and for pushing this through, Mike. Thanks for reviewing, Ma Lin! |
Just thought, if the next row is cached (like pre 3df0fc8), the For example, in these conditions:
edit: There may not be too many code with such condition combination, and the changes are relatively large, so it can be fixed in 3.12. |
We'd still only update
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 However, as you point out, such code is probably rare; it does not make sense with |
version info:
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:
on the third run, where we ran the "test" on the same connection twice, it fails:
it would appear there's some internal caching of table state that needs to be cleared.
The text was updated successfully, but these errors were encountered: