Using LD_PRELOAD to run any version of SQLite with Python
I’ve been wanting to figure this out for ages. This thread on Hacker News plus this Stackoverflow post gave me some initial clues.
The challenge: someone reports a bug in Datasette with a specific version of SQLite. How can I run Datasette against that specific version?
Steps:
- Download the source code for that specific version
- Build a
libsqlite3.so
file using that source code - Run Datasette prefixed with
LD_PRELOAD=path/to/libsqlite3.so
Obtaining source code for old SQLite versions
This took a while to figure out. https://www.sqlite.org/chronology.html has a list of links to different releases. From there clicking on one of the dates takes you to a page like this: https://www.sqlite.org/src/timeline?c=cf538e2783&y=ci
From there clicking the “version-X” link takes you through to the timeline page like this: https://www.sqlite.org/src/timeline?r=version-3.8.11.1 - then click on e.g. “check-in: cf538e27” to get to a page like https://www.sqlite.org/src/info/cf538e2783e468bb which has a link to a Tarball and a Zip archive. Download one of those!
Hopefully shorter version: decide which version you want, then navigate to https://www.sqlite.org/src/timeline?t=version-3.8.11.1 and click the check-in link to get to the page that links to the Zip / Tarball.
Here’s the URL to the 3.8.11.1 Tarball: https://www.sqlite.org/src/tarball/cf538e27/SQLite-cf538e27.tar.gz
Building the library
I first tried this on macOS but I couldn’t get the LD_PRELOAD
trick to work. I think this is because of “system integrity protection” (clue in this comment) - so I decided to do it in a Docker Ubuntu container instead.
To start a fresh Ubuntu container:
docker run -it -p 8001:8001 ubuntu
I’m using -p 8001:8001
here to allow me to access Datasette running on port 8001 later on.
Now we need python3 and the various build tools (SQLite also needs tcl
for the build):
apt-get install -y python3 build-essential tcl wget
Annoyingly installing tcl
requires us to interactively set the timezone data - I enter 2
for America and then 85
for Los Angeles.
Download and extract the SQLite code:
cd /tmpwget https://www.sqlite.org/src/tarball/cf538e27/SQLite-cf538e27.tar.gztar -xzvf SQLite-cf538e27.tar.gzcd SQLite-cf538e27
Now we can build the extension. The CPPFLAGS
are optional but I found I needed them to get the full Datasette test suite to pass later on:
CPPFLAGS="-DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_RTREE=1" ./configuremake
There is no need to run make install
here, we just need the compiled library.
This puts the libraries in a .libs
directory:
root@11b79085483e:/tmp/SQLite-cf538e27# ls -l .libs/total 22660-rw-r--r-- 1 root root 9622164 Jun 17 12:52 libsqlite3.alrwxrwxrwx 1 root root 16 Jun 17 12:52 libsqlite3.la -> ../libsqlite3.la-rw-r--r-- 1 root root 955 Jun 17 12:52 libsqlite3.lailrwxrwxrwx 1 root root 19 Jun 17 12:52 libsqlite3.so -> libsqlite3.so.0.8.6lrwxrwxrwx 1 root root 19 Jun 17 12:52 libsqlite3.so.0 -> libsqlite3.so.0.8.6-rwxr-xr-x 1 root root 4021816 Jun 17 12:52 libsqlite3.so.0.8.6-rwxr-xr-x 1 root root 314160 Jun 17 12:52 sqlite3-rw-r--r-- 1 root root 9232752 Jun 17 12:52 sqlite3.o
We can confirm that this worked using the following one-liner:
LD_PRELOAD=.libs/libsqlite3.so python3 -c \ 'import sqlite3; print(sqlite3.connect(":memory").execute("select sqlite_version()").fetchone())'
Which for me now outputs:
('3.8.11.1',)
Try that without the LD_PRELOAD
to see the difference:
root@11b79085483e:/tmp/SQLite-cf538e27# python3 -c \> 'import sqlite3; print(sqlite3.connect(":memory").execute("select sqlite_version()").fetchone())'('3.31.1',)
Running Datasette
No need for a virtual environent since we are in a Docker container. We need to install pip
:
apt-get install -y python3-pip
Then we can install Datasette:
python3 -mpip install datasette
Now run it like this:
datasette -h 0.0.0.0INFO: Started server process [10266]INFO: Waiting for application startup.INFO: Application startup complete.INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
We need -h 0.0.0.0
to allow access from outside. On our machine we can now visit it here:
http://0.0.0.0:8001/-/versions
And confirm that the default SQLite version is "version": "3.31.1"
.
Alternatively, run this to see the versions output without needing to run the server:
datasette --get /-/versions.json
Quit Datasette and start it again with the LD_PRELOAD
:
LD_PRELOAD=.libs/libsqlite3.so datasette -h 0.0.0.0INFO: Started server process [10274]INFO: Waiting for application startup.INFO: Application startup complete.INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
And now http://0.0.0.0:8001/-/versions reports the following:
{ "python": { "version": "3.8.2", "full": "3.8.2 (default, Apr 27 2020, 15:53:34) \n[GCC 9.3.0]" }, "datasette": { "version": "0.44" }, "asgi": "3.0", "uvicorn": "0.11.5", "sqlite": { "version": "3.8.11.1", "fts_versions": [], "extensions": {}, "compile_options": [ "HAVE_ISNAN", "SYSTEM_MALLOC", "TEMP_STORE=1", "THREADSAFE=1" ] }}
Running the Datasette tests
To run Datasette’s test suite I needed to install a few extra dependencies:
apt-get install -y python3-pip git python3.8-venvcd /tmpgit clone https://github.com/simonw/datasettecd datasettepython3 -m venv venvsource venv/bin/activatepip install wheel # So bdist_wheel works in next steppip install -e '.[test]'LD_PRELOAD=/tmp/SQLite-cf538e27/.libs/libsqlite3.so python3 -c
I used this process to track down a bug that was introduced between SQLite 3.36.0 and 3.37.0 in this issue.