diff --git a/.github/actions/setup-sqlite-version/action.yml b/.github/actions/setup-sqlite-version/action.yml
new file mode 100644
index 0000000..fdbc71c
--- /dev/null
+++ b/.github/actions/setup-sqlite-version/action.yml
@@ -0,0 +1,39 @@
+name: "Setup SQLite version"
+description: "Build and activate a specific SQLite version from its amalgamation archive"
+inputs:
+ version:
+ description: "The SQLite version to install"
+ required: true
+ cflags:
+ description: "CFLAGS to use when compiling SQLite"
+ required: false
+ default: ""
+ skip-activate:
+ description: "Set to true to skip modifying the library path"
+ required: false
+ default: "false"
+ fallback-urls:
+ description: "Whitespace-separated fallback download URLs to try after sqlite.org"
+ required: false
+ default: ""
+outputs:
+ sqlite-location:
+ description: "Directory containing the compiled SQLite library"
+ value: ${{ steps.build.outputs.sqlite-location }}
+runs:
+ using: "composite"
+ steps:
+ - shell: bash
+ run: mkdir -p "$RUNNER_TEMP/sqlite-versions/downloads"
+ - uses: actions/cache@v6
+ with:
+ path: ${{ runner.temp }}/sqlite-versions/downloads
+ key: setup-sqlite-version-${{ inputs.version }}-amalgamation-v1
+ - id: build
+ shell: bash
+ run: bash "$GITHUB_ACTION_PATH/setup-sqlite-version.sh"
+ env:
+ SQLITE_VERSION: ${{ inputs.version }}
+ SQLITE_CFLAGS: ${{ inputs.cflags }}
+ SQLITE_SKIP_ACTIVATE: ${{ inputs.skip-activate }}
+ SQLITE_EXTRA_FALLBACK_URLS: ${{ inputs.fallback-urls }}
diff --git a/.github/actions/setup-sqlite-version/setup-sqlite-version.sh b/.github/actions/setup-sqlite-version/setup-sqlite-version.sh
new file mode 100644
index 0000000..0df7290
--- /dev/null
+++ b/.github/actions/setup-sqlite-version/setup-sqlite-version.sh
@@ -0,0 +1,144 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+version_spec="${SQLITE_VERSION:?SQLITE_VERSION is required}"
+cflags="${SQLITE_CFLAGS:-}"
+skip_activate="${SQLITE_SKIP_ACTIVATE:-false}"
+extra_fallback_urls="${SQLITE_EXTRA_FALLBACK_URLS:-}"
+
+case "$version_spec" in
+ 3.46 | 3.46.0)
+ sqlite_version="3.46.0"
+ sqlite_year="2024"
+ amalgamation_id="3460000"
+ builtin_fallback_urls="https://static.simonwillison.net/static/2026/sqlite-amalgamation-3460000.zip"
+ ;;
+ 3.23.1)
+ sqlite_version="3.23.1"
+ sqlite_year="2018"
+ amalgamation_id="3230100"
+ builtin_fallback_urls="https://static.simonwillison.net/static/2026/sqlite-amalgamation-3230100.zip"
+ ;;
+ *)
+ echo "::error::Unsupported SQLite version '$version_spec'. Add its release year and amalgamation id to $GITHUB_ACTION_PATH/setup-sqlite-version.sh."
+ exit 1
+ ;;
+esac
+
+case "$(uname -s)" in
+ Linux)
+ library_name="libsqlite3.so.0"
+ library_path_var="LD_LIBRARY_PATH"
+ ;;
+ Darwin)
+ library_name="libsqlite3.dylib"
+ library_path_var="DYLD_LIBRARY_PATH"
+ ;;
+ *)
+ echo "::error::Unsupported platform $(uname -s)"
+ exit 1
+ ;;
+esac
+
+runner_temp="${RUNNER_TEMP:-}"
+if [ -z "$runner_temp" ]; then
+ runner_temp="$(mktemp -d)"
+fi
+
+filename="sqlite-amalgamation-${amalgamation_id}"
+official_url="https://www.sqlite.org/${sqlite_year}/${filename}.zip"
+download_dir="${runner_temp}/sqlite-versions/downloads"
+source_root="${runner_temp}/sqlite-versions/source"
+source_dir="${source_root}/${filename}"
+build_dir="${runner_temp}/sqlite-versions/build/${sqlite_version}"
+archive_path="${download_dir}/${filename}.zip"
+
+mkdir -p "$download_dir" "$source_root" "$build_dir"
+
+download_archive() {
+ local url
+ local candidate_path="${archive_path}.tmp"
+ local urls=("$official_url")
+
+ for url in $builtin_fallback_urls $extra_fallback_urls; do
+ urls+=("$url")
+ done
+
+ rm -f "$candidate_path"
+ for url in "${urls[@]}"; do
+ echo "Downloading SQLite ${sqlite_version} amalgamation from ${url}"
+ if curl \
+ --fail \
+ --location \
+ --show-error \
+ --retry 5 \
+ --retry-delay 2 \
+ --retry-max-time 180 \
+ --retry-all-errors \
+ --connect-timeout 20 \
+ --max-time 240 \
+ --output "$candidate_path" \
+ "$url"; then
+ mv "$candidate_path" "$archive_path"
+ return 0
+ fi
+
+ echo "::warning::Download failed from ${url}"
+ rm -f "$candidate_path"
+ done
+
+ echo "::error::Could not download SQLite ${sqlite_version} amalgamation"
+ return 1
+}
+
+if [ ! -f "${source_dir}/sqlite3.c" ]; then
+ if [ ! -f "$archive_path" ]; then
+ download_archive
+ fi
+
+ rm -rf "$source_dir"
+ unzip -q "$archive_path" -d "$source_root"
+fi
+
+if [ ! -f "${source_dir}/sqlite3.c" ]; then
+ echo "::error::Expected ${source_dir}/sqlite3.c after extracting ${archive_path}"
+ exit 1
+fi
+
+read -r -a cflag_args <<< "$cflags"
+
+echo "Compiling SQLite ${sqlite_version} to ${build_dir}/${library_name}"
+gcc \
+ -fPIC \
+ -shared \
+ "${cflag_args[@]}" \
+ "${source_dir}/sqlite3.c" \
+ "-I${source_dir}" \
+ -o "${build_dir}/${library_name}"
+
+if [ "$library_name" = "libsqlite3.so.0" ]; then
+ ln -sf "$library_name" "${build_dir}/libsqlite3.so"
+fi
+
+if [ -n "${GITHUB_OUTPUT:-}" ]; then
+ echo "sqlite-location=${build_dir}" >> "$GITHUB_OUTPUT"
+else
+ echo "sqlite-location=${build_dir}"
+fi
+
+case "$(printf '%s' "$skip_activate" | tr '[:upper:]' '[:lower:]')" in
+ true | 1 | yes)
+ echo "Skipping ${library_path_var} activation"
+ ;;
+ *)
+ existing_value="${!library_path_var:-}"
+ if [ -n "${GITHUB_ENV:-}" ]; then
+ if [ -n "$existing_value" ]; then
+ echo "${library_path_var}=${build_dir}:${existing_value}" >> "$GITHUB_ENV"
+ else
+ echo "${library_path_var}=${build_dir}" >> "$GITHUB_ENV"
+ fi
+ fi
+ echo "Added ${build_dir} to ${library_path_var}"
+ ;;
+esac
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index a37907a..23b23bd 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -9,24 +9,19 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- - uses: actions/cache@v3
- name: Configure pip caching
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
- restore-keys: |
- ${{ runner.os }}-pip-
+ cache: pip
+ cache-dependency-path: pyproject.toml
- name: Install dependencies
run: |
- pip install -e '.[test]'
+ pip install . --group dev
- name: Run tests
run: |
pytest
@@ -34,25 +29,20 @@ jobs:
runs-on: ubuntu-latest
needs: [test]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v6
with:
- python-version: '3.11'
- - uses: actions/cache@v3
- name: Configure pip caching
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }}
- restore-keys: |
- ${{ runner.os }}-publish-pip-
+ python-version: '3.14'
+ cache: pip
+ cache-dependency-path: pyproject.toml
- name: Install dependencies
run: |
- pip install setuptools wheel twine
+ pip install build twine
- name: Publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
- python setup.py sdist bdist_wheel
+ python -m build
twine upload dist/*
diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
index 8a86cd2..2afa7b7 100644
--- a/.github/workflows/spellcheck.yml
+++ b/.github/workflows/spellcheck.yml
@@ -6,21 +6,16 @@ jobs:
spellcheck:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
with:
- python-version: 3.9
- - uses: actions/cache@v2
- name: Configure pip caching
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
- restore-keys: |
- ${{ runner.os }}-pip-
+ python-version: "3.12"
+ cache: pip
+ cache-dependency-path: pyproject.toml
- name: Install dependencies
run: |
- pip install -e '.[docs]'
+ pip install . --group docs
- name: Check spelling
run: |
codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt
diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml
index 0681f66..7668f1b 100644
--- a/.github/workflows/test-coverage.yml
+++ b/.github/workflows/test-coverage.yml
@@ -12,24 +12,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v7
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v6
with:
- python-version: 3.9
- - uses: actions/cache@v2
- name: Configure pip caching
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
- restore-keys: |
- ${{ runner.os }}-pip-
+ python-version: "3.11"
+ cache: pip
+ cache-dependency-path: pyproject.toml
- name: Install SpatiaLite
run: sudo apt-get install libsqlite3-mod-spatialite
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
- python -m pip install -e .[test]
+ python -m pip install . --group dev
python -m pip install pytest-cov
- name: Run tests
run: |-
diff --git a/.github/workflows/test-sqlite-support.yml b/.github/workflows/test-sqlite-support.yml
new file mode 100644
index 0000000..f195cba
--- /dev/null
+++ b/.github/workflows/test-sqlite-support.yml
@@ -0,0 +1,41 @@
+name: Test SQLite versions
+
+on: [push, pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ${{ matrix.platform }}
+ continue-on-error: true
+ strategy:
+ matrix:
+ platform: [ubuntu-latest]
+ python-version: ["3.10"]
+ sqlite-version: [
+ "3.46",
+ "3.23.1", # 2018-04-10, before UPSERT
+ ]
+ steps:
+ - uses: actions/checkout@v7
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
+ cache: pip
+ cache-dependency-path: pyproject.toml
+ - name: Set up SQLite ${{ matrix.sqlite-version }}
+ uses: ./.github/actions/setup-sqlite-version
+ with:
+ version: ${{ matrix.sqlite-version }}
+ cflags: "-DSQLITE_ENABLE_DESERIALIZE -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_JSON1"
+ - run: python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
+ - name: Install dependencies
+ run: |
+ pip install . --group dev
+ pip freeze
+ - name: Run tests
+ run: |
+ python -m pytest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2e706df..923de2e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,25 +10,21 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"]
numpy: [0, 1]
- os: [ubuntu-latest, macos-latest, windows-latest]
+ os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- - uses: actions/cache@v3
- name: Configure pip caching
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
- restore-keys: |
- ${{ runner.os }}-pip-
+ allow-prereleases: true
+ cache: pip
+ cache-dependency-path: pyproject.toml
- name: Install dependencies
run: |
- pip install -e '.[test,mypy,flake8]'
+ pip install . --group dev
- name: Optionally install numpy
if: matrix.numpy == 1
run: pip install numpy
@@ -42,12 +38,20 @@ jobs:
- name: Run tests
run: |
pytest -v
+ - name: Run autocommit tests just on 3.14/Ubuntu
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
+ run: pytest --sqlite-autocommit
- name: run mypy
run: mypy sqlite_utils tests
- name: run flake8
run: flake8
+ - name: run ty
+ if: matrix.os != 'windows-latest' && matrix.python-version == '3.14'
+ run: |
+ pip install uv
+ uv run ty check sqlite_utils
- name: Check formatting
run: black . --check
- name: Check if cog needs to be run
run: |
- cog --check README.md docs/*.rst
+ cog --check --diff README.md docs/*.rst
diff --git a/.gitignore b/.gitignore
index 7947f33..5b5d2c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.venv
+dist
build
*.db
__pycache__/
@@ -14,9 +15,10 @@ venv
.schema
.vscode
.hypothesis
+.claude/
Pipfile
Pipfile.lock
-pyproject.toml
+uv.lock
tests/*.dylib
tests/*.so
tests/*.dll
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index fe8ba1e..0000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-tasks:
-- init: pip install '.[test]'
\ No newline at end of file
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index aa15c7c..08c2f81 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -4,13 +4,14 @@ sphinx:
configuration: docs/conf.py
build:
- os: ubuntu-22.04
+ os: ubuntu-24.04
tools:
- python: "3.11"
+ python: "3.13"
+ jobs:
+ install:
+ - pip install --upgrade pip
+ - pip install . --group docs
-python:
- install:
- - method: pip
- path: .
- extra_requirements:
- - docs
+formats:
+- pdf
+- epub
diff --git a/Justfile b/Justfile
index 66863f6..be41523 100644
--- a/Justfile
+++ b/Justfile
@@ -1,29 +1,34 @@
# Run tests and linters
@default: test lint
-# Setup project
-@init:
- pipenv run pip install -e '.[test,docs,mypy,flake8]'
-
# Run pytest with supplied options
@test *options:
- pipenv run pytest {{options}}
+ uv run pytest {{options}}
-# Run linters: black, flake8, mypy, cog
+@run *options:
+ uv run -- {{options}}
+
+# Run linters: black, flake8, mypy, ty, cog
@lint:
- pipenv run black . --check
- pipenv run flake8
- pipenv run mypy sqlite_utils tests
- pipenv run cog --check README.md docs/*.rst
+ just run black . --check
+ uv run flake8
+ uv run mypy sqlite_utils tests
+ uv run ty check sqlite_utils
+ uv run cog --check README.md docs/*.rst
+ uv run --group docs codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt
+ uv run --group docs codespell sqlite_utils --ignore-words docs/codespell-ignore-words.txt
# Rebuild docs with cog
@cog:
- pipenv run cog -r README.md docs/*.rst
+ uv run --group docs cog -r README.md docs/*.rst
# Serve live docs on localhost:8000
@docs: cog
- cd docs && pipenv run make livehtml
+ #!/usr/bin/env bash
+ cd docs
+ uv run --group docs make livehtml
+
# Apply Black
@black:
- pipenv run black .
+ uv run black .
diff --git a/README.md b/README.md
index 49a26ed..c444c64 100644
--- a/README.md
+++ b/README.md
@@ -18,8 +18,12 @@ Python CLI utility and library for manipulating SQLite databases.
- [Configure SQLite full-text search](https://sqlite-utils.datasette.io/en/stable/cli.html#configuring-full-text-search) against your database tables and run search queries against them, ordered by relevance
- Run [transformations against your tables](https://sqlite-utils.datasette.io/en/stable/cli.html#transforming-tables) to make schema changes that SQLite `ALTER TABLE` does not directly support, such as changing the type of a column
- [Extract columns](https://sqlite-utils.datasette.io/en/stable/cli.html#extracting-columns-into-a-separate-table) into separate tables to better normalize your existing data
+- [Manage database migrations](https://sqlite-utils.datasette.io/en/stable/migrations.html) using Python migration files and the `sqlite-utils migrate` command
+- [Install plugins](https://sqlite-utils.datasette.io/en/stable/plugins.html) to add custom SQL functions and additional features
-Read more on my blog, in this series of posts on [New features in sqlite-utils](https://simonwillison.net/series/sqlite-utils-features/) and other [entries tagged sqliteutils](https://simonwillison.net/tags/sqliteutils/).
+Upgrading from sqlite-utils 3.x? See the [4.0 upgrade guide](https://sqlite-utils.datasette.io/en/stable/upgrading.html#upgrading-from-3-x-to-4-0).
+
+Read more on my blog, in this series of posts on [New features in sqlite-utils](https://simonwillison.net/series/sqlite-utils-features/) and other [entries tagged sqlite-utils](https://simonwillison.net/tags/sqlite-utils/).
## Installation
diff --git a/docs/_templates/base.html b/docs/_templates/base.html
index 43c2003..a253a46 100644
--- a/docs/_templates/base.html
+++ b/docs/_templates/base.html
@@ -7,6 +7,11 @@
{% block scripts %}
{{ super() }}
+