I am confident this is a bug in CPython, not a bug in a third-party project
I have searched the CPython issue tracker,
and am confident this bug has not been reported before
CPython versions tested on:
3.11
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.11.5 (main, Aug 25 2023, 13:19:53) [GCC 9.4.0]
A clear and concise description of the bug:
Apologies if I'm misunderstanding. Please advice if I should post elsewhere. But shouldn't iterdump() properly detect VARCHAR columns with binary data and output X'' strings instead of throwing an error? This is what sqlite3 .dump does.
File "foo.py", line 79, in dump_sqlite_db
for line in conn.iterdump():
File "/usr/lib/python3.11/sqlite3/dump.py", line 63, in _iterdump
for row in query_res:
sqlite3.OperationalError: Could not decode to UTF-8 column ''INSERT INTO "sync_entities_metadata" VALUES('||quote("storage_key")||','||quote("metadata")||')'' with text 'INSERT INTO "sync_entities_metadata" VALUES(1,'v10����
I tried enabling conn.text_factory = bytes as a workaround, but now get a different error.
File "foo.py", line 79, in dump_sqlite_db
for line in conn.iterdump():
File "/usr/lib/python3.11/sqlite3/dump.py", line 43, in _iterdump
elif table_name.startswith('sqlite_'):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: startswith first arg must be bytes or a tuple of bytes, not str
Notice a single non-ASCII/UTF byte in the data VARCHAR column.
Then do this to build the database:
sqlite3 foo.db <foo.sql
Everything works fine! The sqlite3 database is apparently restored and consistent.
But then in Python...
#! /usr/bin/env python3.11"""https://github.com/python/cpython/issues/108590"""importsqlite3conn=sqlite3.connect('foo.db')
curs=conn.cursor()
curs.execute('SELECT * FROM foo')
forlineinconn.iterdump():
pass
It fails thusly:
./cpython-108590.py
Traceback (most recent call last):
File "cpython-108590.py", line 9, in <module>
for line in conn.iterdump():
File "/usr/lib/python3.11/sqlite3/dump.py", line 63, in _iterdump
for row in query_res:
sqlite3.OperationalError: Could not decode to UTF-8 column ''INSERT INTO "foo" VALUES('||quote("id")||','||quote("data")||')'' with text 'INSERT INTO "foo" VALUES(42,'a�')'
Apologies if I'm misunderstanding. Please advice if I should post elsewhere. But shouldn't iterdump() properly detect VARCHAR columns with binary data and output X'' strings instead of throwing an error? This is what sqlite3 .dump does.
The SQLite shell does not special case VARCHAR (take a look at shell.c in the SQLite sources); it simply ignores the unprintable character. On my computer, that is also the behaviour of iterdump.
BTW, @dotysan, I see you are using the connection context manager. Note that the connection context manager does not close the database, it only makes sure that the transaction in the with body is committed; __exit__ implicitly executes COMMIT or ROLLBACK, depending on if an exception happened. So, from your use case, it seems to me you would be better off by using itertools.closing.
I am able to reproduce using @dotysan's foo.sql (recreated from hex dump, my terminal/editor was giving problems trying to "fix" the invalid encoding). The sqlite3 CLI both accepts it and reproduces it in a dump so I do believe its a python sqlite bug (rather than a corrupt db). Usually there is not supposed to be invalid encoded characters in a VARCHAR/TEXT field but it seems sqlite takes the garbage in -> garbage out policy. Working on a patch for this.
Heres an alternative hexdump of foo.sql that can be imported a bit easier (with xxd -r):
Unfortunately I don't think I can make it that pretty unless there is a API function that lets us send a query of bytes instead of str that I'm not aware of. The encode() down the chain is causing a problem as its turning the '\x9f' into a '\xc2\x9f' before being handed to sqlite.
Let see. It is a complex issue, like every encoding issue.
The SQLite database can contain non UTF-8 sequences of bytes as a text. To work it around, there is a special mechanism: you can set text_factory to bytes, bytearray or custom factory which calls str() with other encoding or errors handler. What do you do with the result -- is your problem. If you get bytes or bytearray, you are responsible of decoding it. If you have a non-standard decoded string, you should use corresponding encoding when write it to a file or handle special characters in some way.
iterdump() has its own issues. It is not compatible with bytes and bytearray. When you use non-standard string decoding method, you can write it to file (using the correct encoding method) and feed to sqlite3 command, but you cannot feed it to execute().
So what can we do?
First of all, document the limitation of iterdump(), the workaround (seems, it is not well known) and the limitation of the workaround.
We can make execute() accepting non-UTF-8-encodable SQL statement and values. It should not be enabled by default, because it is easy to produce by accident a DB which breaks other programs. We can even use the same option to use surrogateescapes in results, it is more efficient than text_factory = lambda x: str(x, errors='surrogateescape').
We can make iterdump() converting all surrogateescapes into CAST(X'...' AS TEXT). Additional checks and transformations have a cost, so perhaps it should not be enabled by default. We can make iterdump() compatible with text_factory = bytes, but it will convert all TEXTs into BLOBs.
(2) and (3) are complex tasks and may require separate discussions about details. For example, should text_factory and new options be Connection or Cursor attributes, or parameters of execute()?
Let see. It is a complex issue, like every encoding issue.
+1
So what can we do?
First of all, document the limitation of iterdump(), the workaround (seems, it is not well known) and the limitation of the workaround.
IMO, this is the preferred solution. See #108699 for a draft docs update.
We can make execute() accepting non-UTF-8-encodable SQL statement and values. It should not be enabled by default, because it is easy to produce by accident a DB which breaks other programs. We can even use the same option to use surrogateescapes in results, it is more efficient than text_factory = lambda x: str(x, errors='surrogateescape').
I'm not sure this is a good idea. The sqlite3 module is a DB API (PEP-249) implementation, and the DB API dictates the param spec of execute(). If we change that, we'd be drifting further away from the DB API, which IMO is not desirable.
We can make iterdump() converting all surrogateescapes into CAST(X'...' AS TEXT). Additional checks and transformations have a cost, so perhaps it should not be enabled by default. We can make iterdump() compatible with text_factory = bytes, but it will convert all TEXTs into BLOBs.
Using CAST(X'...' AS TEXT) in the dump is a possibility. I'm not sure it is worth the added complexity, though.
The sqlite3 docs now includes a section about encoding issues, so it should hopefully be easier to avoid issues like this. However, @serhiy-storchaka is not satisfied with how the docs turned out and has flagged that he wants to improve them further. Perhaps those improvements should be done in a separate issue, though.
Activity
CorvinM commented on Aug 29, 2023
I am not able to reproduce this on 3.11.5, could you provide an example database or the script/sql to create one that demonstrates the issue?
dotysan commented on Aug 29, 2023
Hi @CorvinM, thanks for taking the time to attempt to reproduce.
My use case is fiddling around with existing settings in a Google Chrome profile. Your mileage may vary.
But here's a simplified/synthesized test that should be reproducible...
Notice a single non-ASCII/UTF byte in the data VARCHAR column.
Then do this to build the database:
sqlite3 foo.db <foo.sql
Everything works fine! The sqlite3 database is apparently restored and consistent.
But then in Python...
It fails thusly:
./cpython-108590.py Traceback (most recent call last): File "cpython-108590.py", line 9, in <module> for line in conn.iterdump(): File "/usr/lib/python3.11/sqlite3/dump.py", line 63, in _iterdump for row in query_res: sqlite3.OperationalError: Could not decode to UTF-8 column ''INSERT INTO "foo" VALUES('||quote("id")||','||quote("data")||')'' with text 'INSERT INTO "foo" VALUES(42,'a�')'erlend-aasland commented on Aug 29, 2023
I'm unable to reproduce this using the following:
erlend-aasland commented on Aug 29, 2023
The SQLite shell does not special case VARCHAR (take a look at shell.c in the SQLite sources); it simply ignores the unprintable character. On my computer, that is also the behaviour of iterdump.
erlend-aasland commented on Aug 29, 2023
Unable to reproduce on Debian or Ubuntu as well.
erlend-aasland commented on Aug 29, 2023
BTW, @dotysan, I see you are using the connection context manager. Note that the connection context manager does not close the database, it only makes sure that the transaction in the with body is committed; __exit__ implicitly executes COMMIT or ROLLBACK, depending on if an exception happened. So, from your use case, it seems to me you would be better off by using itertools.closing.
CorvinM commented on Aug 29, 2023
I am able to reproduce using @dotysan's foo.sql (recreated from hex dump, my terminal/editor was giving problems trying to "fix" the invalid encoding). The sqlite3 CLI both accepts it and reproduces it in a dump so I do believe its a python sqlite bug (rather than a corrupt db). Usually there is not supposed to be invalid encoded characters in a VARCHAR/TEXT field but it seems sqlite takes the garbage in -> garbage out policy. Working on a patch for this.
Heres an alternative hexdump of foo.sql that can be imported a bit easier (with xxd -r):
erlend-aasland commented on Aug 29, 2023
We can't use a hexdump for a regression test; please provide a Python only reproducer.
erlend-aasland commented on Aug 29, 2023
@CorvinM, something like this should suffice:
CorvinM commented on Aug 29, 2023
Unfortunately I don't think I can make it that pretty unless there is a API function that lets us send a query of bytes instead of str that I'm not aware of. The encode() down the chain is causing a problem as its turning the '\x9f' into a '\xc2\x9f' before being handed to sqlite.
To show the encode() problem:
Regardless, this works to show the original issue (albeit nasty):
erlend-aasland commented on Aug 29, 2023
Hm, yes I noticed this, @CorvinM. I can reproduce using the hexdump.
33 remaining items
erlend-aasland commented on Aug 30, 2023
@serhiy-storchaka, @CorvinM: As an alternative, I created #108699 in order to try to solve this in documentation only.
serhiy-storchaka commented on Aug 31, 2023
Let see. It is a complex issue, like every encoding issue.
The SQLite database can contain non UTF-8 sequences of bytes as a text. To work it around, there is a special mechanism: you can set text_factory to bytes, bytearray or custom factory which calls str() with other encoding or errors handler. What do you do with the result -- is your problem. If you get bytes or bytearray, you are responsible of decoding it. If you have a non-standard decoded string, you should use corresponding encoding when write it to a file or handle special characters in some way.
iterdump() has its own issues. It is not compatible with bytes and bytearray. When you use non-standard string decoding method, you can write it to file (using the correct encoding method) and feed to sqlite3 command, but you cannot feed it to execute().
So what can we do?
(2) and (3) are complex tasks and may require separate discussions about details. For example, should text_factory and new options be Connection or Cursor attributes, or parameters of execute()?
erlend-aasland commented on Aug 31, 2023
+1
IMO, this is the preferred solution. See #108699 for a draft docs update.
I'm not sure this is a good idea. The sqlite3 module is a DB API (PEP-249) implementation, and the DB API dictates the param spec of execute(). If we change that, we'd be drifting further away from the DB API, which IMO is not desirable.
Using CAST(X'...' AS TEXT) in the dump is a possibility. I'm not sure it is worth the added complexity, though.
erlend-aasland commented on Aug 31, 2023
I suggest we focus on 1. for now, the docs update. Well, in my opinion, docs are as hard to get right as encoding issues, so feel free to chime in 😄
serhiy-storchaka commented on Aug 31, 2023
At least it works with older versions of Python.
gh-108590: Improve sqlite3 docs on encoding issues and how to handle …
pythongh-108590: Improve sqlite3 docs on encoding issues and how to h…
pythongh-108590: Improve sqlite3 docs on encoding issues and how to h…
[3.11] gh-108590: Improve sqlite3 docs on encoding issues and how to …
[3.12] gh-108590: Improve sqlite3 docs on encoding issues and how to …
hugovk commented on Nov 9, 2023
I see lots of merged PRs for this issue. Can it be closed or is there more to be done?
erlend-aasland commented on Nov 10, 2023
The sqlite3 docs now includes a section about encoding issues, so it should hopefully be easier to avoid issues like this. However, @serhiy-storchaka is not satisfied with how the docs turned out and has flagged that he wants to improve them further. Perhaps those improvements should be done in a separate issue, though.
I'm fine with closing this.
pythongh-108590: Improve sqlite3 docs on encoding issues and how to h…
pythongh-108590: Improve sqlite3 docs on encoding issues and how to h…