Compile and run a new SQLite version with the existing sqlite3 Python library on macOS

I've been trying to figure this out for years. Previous notes include Using LD_PRELOAD to run any version of SQLite with Python (Linux only), and Building a specific version of SQLite with pysqlite on macOS/Linux and Using pysqlite3 on macOS (both using the pysqlite3 package). But the dream was always to find a way to let me easily run a different SQLite version with the sqlite3 module from the Python standard library directly on my Mac.

Alex Garcia helped me find the solution I've been looking for this morning.

It's pretty simple: you compile a new libsqlite3.0.dylib module from the SQLite amalgamation release, then point DYLD_LIBRARY_PATH to it before loading Python and importing sqlite3.

⚠️ This works against Python 3 installed via Homebrew, but has been reported not to work against a Python 3 installed directly from www.python.org

The amalgamation builds are released as zip files on the SQLite downloads page - let's grab 3.42.0, the most recent stable release:

wget 'https://www.sqlite.org/2023/sqlite-amalgamation-3420000.zip'

Then:

unzip sqlite-amalgamation-3420000.zip
cd sqlite-amalgamation-3420000

Running ls -lah shows there's only four files in that zip file:

-rw-rw-r--  1 simon  wheel   847K May 16 06:45 shell.c
-rw-rw-r--  1 simon  wheel   8.3M May 16 06:45 sqlite3.c
-rw-rw-r--  1 simon  wheel   611K May 16 06:45 sqlite3.h
-rw-rw-r--  1 simon  wheel    37K May 16 06:45 sqlite3ext.h

How do I compile that? I asked GPT-4 and got this recipe:

gcc -dynamiclib sqlite3.c -o libsqlite3.0.dylib -lm -lpthread

On my machine that runs in less than a second and produces a 1.2MB libsqlite3.0.dylib file.

To use it, set DYLD_LIBRARY_PATH to the folder that contains that .dylib file. We can use $PWD for that:

DYLD_LIBRARY_PATH=$PWD python3 -c "import sqlite3; print(sqlite3.sqlite_version)"

Outputs:

3.42.0

If I run that without the environment variable I get the older version:

python3 -c "import sqlite3; print(sqlite3.sqlite_version)"

Outputs:

3.41.1

And this works to run Datasette as well:

DYLD_LIBRARY_PATH=$PWD datasette --get /-/versions.json | jq

Shows:

{
  "python": {
    "version": "3.10.10",
    "full": "3.10.10 (main, Mar 21 2023, 13:41:05) [Clang 14.0.6 ]"
  },
  "datasette": {
    "version": "1.0a4"
  },
  "asgi": "3.0",
  "uvicorn": "0.23.2",
  "sqlite": {
    "version": "3.42.0",
    "fts_versions": [],
    "extensions": {
      "json1": null
    },
    "compile_options": [
      "ATOMIC_INTRINSICS=1",
      "COMPILER=clang-14.0.3",
      "DEFAULT_AUTOVACUUM",
      "DEFAULT_CACHE_SIZE=-2000",
      "DEFAULT_FILE_FORMAT=4",
      "DEFAULT_JOURNAL_SIZE_LIMIT=-1",
      "DEFAULT_MMAP_SIZE=0",
      "DEFAULT_PAGE_SIZE=4096",
      "DEFAULT_PCACHE_INITSZ=20",
      "DEFAULT_RECURSIVE_TRIGGERS",
      "DEFAULT_SECTOR_SIZE=4096",
      "DEFAULT_SYNCHRONOUS=2",
      "DEFAULT_WAL_AUTOCHECKPOINT=1000",
      "DEFAULT_WAL_SYNCHRONOUS=2",
      "DEFAULT_WORKER_THREADS=0",
      "MALLOC_SOFT_LIMIT=1024",
      "MAX_ATTACHED=10",
      "MAX_COLUMN=2000",
      "MAX_COMPOUND_SELECT=500",
      "MAX_DEFAULT_PAGE_SIZE=8192",
      "MAX_EXPR_DEPTH=1000",
      "MAX_FUNCTION_ARG=127",
      "MAX_LENGTH=1000000000",
      "MAX_LIKE_PATTERN_LENGTH=50000",
      "MAX_MMAP_SIZE=0x7fff0000",
      "MAX_PAGE_COUNT=1073741823",
      "MAX_PAGE_SIZE=65536",
      "MAX_SQL_LENGTH=1000000000",
      "MAX_TRIGGER_DEPTH=1000",
      "MAX_VARIABLE_NUMBER=32766",
      "MAX_VDBE_OP=250000000",
      "MAX_WORKER_THREADS=8",
      "MUTEX_PTHREADS",
      "SYSTEM_MALLOC",
      "TEMP_STORE=1",
      "THREADSAFE=1"
    ]
  }
}

Without the environment variable I get this instead:

{
  "python": {
    "version": "3.10.10",
    "full": "3.10.10 (main, Mar 21 2023, 13:41:05) [Clang 14.0.6 ]"
  },
  "datasette": {
    "version": "1.0a4"
  },
  "asgi": "3.0",
  "uvicorn": "0.23.2",
  "sqlite": {
    "version": "3.41.1",
    "fts_versions": [
      "FTS5",
      "FTS4",
      "FTS3"
    ],
    "extensions": {
      "json1": null
    },
    "compile_options": [
      "ATOMIC_INTRINSICS=1",
      "COMPILER=clang-14.0.6",
      "DEFAULT_AUTOVACUUM",
      "DEFAULT_CACHE_SIZE=-2000",
      "DEFAULT_FILE_FORMAT=4",
      "DEFAULT_JOURNAL_SIZE_LIMIT=-1",
      "DEFAULT_MMAP_SIZE=0",
      "DEFAULT_PAGE_SIZE=4096",
      "DEFAULT_PCACHE_INITSZ=20",
      "DEFAULT_RECURSIVE_TRIGGERS",
      "DEFAULT_SECTOR_SIZE=4096",
      "DEFAULT_SYNCHRONOUS=2",
      "DEFAULT_WAL_AUTOCHECKPOINT=1000",
      "DEFAULT_WAL_SYNCHRONOUS=2",
      "DEFAULT_WORKER_THREADS=0",
      "ENABLE_COLUMN_METADATA",
      "ENABLE_DBSTAT_VTAB",
      "ENABLE_FTS3",
      "ENABLE_FTS3_TOKENIZER",
      "ENABLE_FTS4",
      "ENABLE_FTS5",
      "ENABLE_GEOPOLY",
      "ENABLE_MATH_FUNCTIONS",
      "ENABLE_RTREE",
      "ENABLE_UNLOCK_NOTIFY",
      "MALLOC_SOFT_LIMIT=1024",
      "MAX_ATTACHED=10",
      "MAX_COLUMN=2000",
      "MAX_COMPOUND_SELECT=500",
      "MAX_DEFAULT_PAGE_SIZE=8192",
      "MAX_EXPR_DEPTH=10000",
      "MAX_FUNCTION_ARG=127",
      "MAX_LENGTH=1000000000",
      "MAX_LIKE_PATTERN_LENGTH=50000",
      "MAX_MMAP_SIZE=0x7fff0000",
      "MAX_PAGE_COUNT=1073741823",
      "MAX_PAGE_SIZE=65536",
      "MAX_SQL_LENGTH=1000000000",
      "MAX_TRIGGER_DEPTH=1000",
      "MAX_VARIABLE_NUMBER=250000",
      "MAX_VDBE_OP=250000000",
      "MAX_WORKER_THREADS=8",
      "MUTEX_PTHREADS",
      "SECURE_DELETE",
      "SYSTEM_MALLOC",
      "TEMP_STORE=1",
      "THREADSAFE=1"
    ]
  }
}

Note that the dylib version appears to be missing the FTS extension.

Adding compiler options

Here's a modification that includes full-text search support:

gcc -dynamiclib sqlite3.c -o libsqlite3.0.dylib -lm -lpthread \
  -DSQLITE_ENABLE_FTS3 \
  -DSQLITE_ENABLE_FTS3_PARENTHESIS \
  -DSQLITE_ENABLE_FTS4 \
  -DSQLITE_ENABLE_FTS5

And now:

DYLD_LIBRARY_PATH=$PWD datasette --get /-/versions.json | jq .sqlite

Outputs:

{
  "version": "3.42.0",
  "fts_versions": [
    "FTS5",
    "FTS4",
    "FTS3"
  ],
  "extensions": {
    "json1": null
  },
  "compile_options": [
    "ATOMIC_INTRINSICS=1",
    "COMPILER=clang-14.0.3",
    "DEFAULT_AUTOVACUUM",
    "DEFAULT_CACHE_SIZE=-2000",
    "DEFAULT_FILE_FORMAT=4",
    "DEFAULT_JOURNAL_SIZE_LIMIT=-1",
    "DEFAULT_MMAP_SIZE=0",
    "DEFAULT_PAGE_SIZE=4096",
    "DEFAULT_PCACHE_INITSZ=20",
    "DEFAULT_RECURSIVE_TRIGGERS",
    "DEFAULT_SECTOR_SIZE=4096",
    "DEFAULT_SYNCHRONOUS=2",
    "DEFAULT_WAL_AUTOCHECKPOINT=1000",
    "DEFAULT_WAL_SYNCHRONOUS=2",
    "DEFAULT_WORKER_THREADS=0",
    "ENABLE_FTS3",
    "ENABLE_FTS3_PARENTHESIS",
    "ENABLE_FTS4",
    "ENABLE_FTS5",
    "MALLOC_SOFT_LIMIT=1024",
    "MAX_ATTACHED=10",
    "MAX_COLUMN=2000",
    "MAX_COMPOUND_SELECT=500",
    "MAX_DEFAULT_PAGE_SIZE=8192",
    "MAX_EXPR_DEPTH=1000",
    "MAX_FUNCTION_ARG=127",
    "MAX_LENGTH=1000000000",
    "MAX_LIKE_PATTERN_LENGTH=50000",
    "MAX_MMAP_SIZE=0x7fff0000",
    "MAX_PAGE_COUNT=1073741823",
    "MAX_PAGE_SIZE=65536",
    "MAX_SQL_LENGTH=1000000000",
    "MAX_TRIGGER_DEPTH=1000",
    "MAX_VARIABLE_NUMBER=32766",
    "MAX_VDBE_OP=250000000",
    "MAX_WORKER_THREADS=8",
    "MUTEX_PTHREADS",
    "SYSTEM_MALLOC",
    "TEMP_STORE=1",
    "THREADSAFE=1"
  ]
}

Created 2023-08-22T12:28:07-07:00, updated 2024-01-15T19:10:23-08:00 · History · Edit