sqlite-utils now supports plugins
24th July 2023
sqlite-utils 3.34 is out with a major new feature: support for plugins.
sqlite-utils
is my combination Python library and command-line tool for manipulating SQLite databases. It recently celebrated its fifth birthday, and has had over 100 releases since it first launched back in 2018.
The new plugin system is inspired by similar mechanisms in Datasette and LLM. It lets developers add new features to sqlite-utils
without needing to get their changes accepted by the core project.
I love plugin systems. As an open source maintainer they are by far the best way to encourage people to contribute to my projects—I can genuinely wake up in the morning and my software has new features, and I didn’t even need to review a pull request.
Plugins also offer a fantastic medium for exploration and experimentation. I can try out new ideas without committing to supporting them in core, and without needing to tie improvements to them to the core release cycle.
Version 3.34 adds two initial plugin hooks: register_commands()
and prepare_connection()
. These are both based on the equivalent hooks in Datasette.
I planned to just ship register_commands()
, but Alex Garcia spotted my activity on the repo and submitted a PR adding prepare_connection()
literally minutes before I had intended to ship the release!
register_commands()
The register_commands()
hook lets you add new commands to the sqlite-utils
command-line tool—so users can run sqlite-utils your-new-command
to access your feature.
I’ve learned from past experience that you should never ship a plugin hook without also releasing at least one plugin that uses it. I’ve built two so far for register_commands()
:
-
sqlite-utils-shell adds a simply interactive shell, accessed using
sqlite-utils shell
for an in-memory database orsqlite-utils shell data.db
to run it against a specific database file. - sqlite-migrate is my first draft of a database migrations system for SQLite, loosely inspired by Django migrations and previewed by the migration mechanism I added to LLM.
Try out the shell plugin like this:
sqlite-utils install sqlite-utils-shell
sqlite-utils shell
The interface looks like this:
In-memory database, content will be lost on exit
Type 'exit' to exit.
sqlite-utils> select 3 + 5;
3 + 5
-------
8
sqlite-utils>
prepare_connection()
This hook, contributed by Alex, lets you modify the connection object before it is used to execute any SQL. Most importantly, this lets you register custom SQLite functions.
I expect this to be the most common category of plugin. I’ve built one so far: sqlite-utils-dateutil, which adds functions for parsing dates and times using the dateutil library.
It lets you do things like this:
sqlite-utils install sqlite-utils-dateutil
sqlite-utils memory "select dateutil_parse('3rd october')" -t
Output:
dateutil_parse('3rd october')
-------------------------------
2023-10-03T00:00:00
This works inside sqlite-shell
too.
Plugins that you install also become available in the Python API interface to sqlite-utils
:
>>> import sqlite_utils
>>> db = sqlite_utils.Database(memory=True)
>>> list(db.query("select dateutil_parse('3rd october')"))
[{"dateutil_parse('3rd october')": '2023-10-03T00:00:00'}]
You can opt out of executing installed plugins by passing execute_plugins=False
to the Database()
constructor:
>>> db = sqlite_utils.Database(memory=True, execute_plugins=False)
>>> list(db.query("select dateutil_parse('3rd october')"))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../site-packages/sqlite_utils/db.py", line 494, in query
cursor = self.execute(sql, params or tuple())
File ".../site-packages/sqlite_utils/db.py", line 512, in execute
return self.conn.execute(sql, parameters)
sqlite3.OperationalError: no such function: dateutil_parse
sqlite-ml by Romain Clement
I quietly released sqlite-utils 3.34
on Saturday. The community has already released several plugins for it!
Romain Clement built sqlite-utils-ml, a plugin wrapper for his sqlite-ml project.
This adds custom SQL functions for training machine learning models and running predictions, entirely within SQLite, using algorithms from scikit-learn.
Here’s what that looks like running inside sqlite-utils shell
:
sqlite-utils install sqlite-utils-shell sqlite-utils-ml
sqlite-utils shell ml.db
Attached to ml.db
Type 'exit' to exit.
sqlite-utils> select sqml_load_dataset('iris') as dataset;
dataset
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"table": "dataset_iris", "feature_names": ["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"], "target_names": ["setosa", "versicolor", "virginica"], "size": 150}
sqlite-utils> select sqml_train(
...> 'Iris prediction',
...> 'classification',
...> 'logistic_regression',
...> 'dataset_iris',
...> 'target'
...> ) as training;
training
--------------------------------------------------------------------------------------------------------------------------------------------------------------
{"experiment_name": "Iris prediction", "prediction_type": "classification", "algorithm": "logistic_regression", "deployed": true, "score": 0.9736842105263158}
sqlite-utils> select
...> dataset_iris.*,
...> sqml_predict(
...> 'Iris prediction',
...> json_object(
...> 'sepal length (cm)', [sepal length (cm)],
...> 'sepal width (cm)', [sepal width (cm)],
...> 'petal length (cm)', [petal length (cm)],
...> 'petal width (cm)', [petal width (cm)]
...> )
...> ) as prediction
...> from dataset_iris
...> limit 1;
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) target prediction
------------------- ------------------ ------------------- ------------------ -------- ------------
5.1 3.5 1.4 0.2 0 0
SQLite extensions by Alex Garcia
Alex Garcia has a growing collection of SQLite extensions, many of which are written in Rust but are packaged as wheels for ease of installation using Python.
Alex released five plugins for SQLite corresponding to five of his existing extensions:
sqlite-utils-sqlite-regex
sqlite-utils-sqlite-path
sqlite-utils-sqlite-url
sqlite-utils-sqlite-ulid
sqlite-utils-sqlite-lines
Here’s an example of sqlite-utils-sqlite-ulid
in action:
sqlite-utils install sqlite-utils-sqlite-ulid
sqlite-utils memory 'select ulid() u1, ulid() u2, ulid() u3' | jq
Output:
[
{
"u1": "01h64d1ysg1rx63z1gwy7nah4n",
"u2": "01h64d1ysgd7vx04sc9pncqh10",
"u3": "01h64d1ysgz1sy7njkqt86dkq9"
}
]
I’ve started a sqlite-utils plugin directory with a list of all of the plugins so far.
Building your own plugin
If you want to try building your own plugin, the documentation includes a simple step-by-step guide. A plugin can be built with as little as two files: a Python module implementing the hooks, and a pyproject.toml
module with metadata about how it should be installed.
I’ve also released a new cookiecutter template: simonw/sqlite-utils-plugin. Here’s how to use that to get started building a plugin:
cookiecutter gh:simonw/sqlite-utils-plugin
Answer the form fields like this:
plugin_name []: rot13
description []: select rot13('text') as a sqlite-utils plugin
hyphenated [rot13]:
underscored [rot13]:
github_username []: your-username
author_name []: your-name
Change directory into the new folder and use sqlite-utils install -e
to install an editable version of your plugin, so changes you make will be reflected when you run the tool:
cd sqlite-utils-rot13
sqlite-utils install -e .
Run this command to confirm the plugin has been installed:
sqlite-utils plugins
You should see this:
[
{
"name": "sqlite-utils-rot13",
"hooks": [
"prepare_connection"
],
"version": "0.1"
}
]
Now drop this code into the sqlite_utils_rot13.py
file:
import sqlite_utils def rot13(s): chars = [] for v in s: c = ord(v) if c >= ord("a") and c <= ord("z"): if c > ord("m"): c -= 13 else: c += 13 elif c >= ord("A") and c <= ord("Z"): if c > ord("M"): c -= 13 else: c += 13 chars.append(chr(c)) return "".join(chars) @sqlite_utils.hookimpl def prepare_connection(conn): conn.create_function("rot13", 1, rot13)
And try it out like this:
sqlite-utils memory "select rot13('hello world')"
Output:
[{"rot13('hello world')": "uryyb jbeyq"}]
And to reverse that:
sqlite-utils memory "select rot13('uryyb jbeyq')"
Output:
[{"rot13('uryyb jbeyq')": "hello world"}]
As you can see, building plugins can be done with very little code. I’m excited to see what else people build with this new capability!
More recent articles
- Qwen2.5-Coder-32B is an LLM that can code well that runs on my Mac - 12th November 2024
- Visualizing local election results with Datasette, Observable and MapLibre GL - 9th November 2024
- Project: VERDAD - tracking misinformation in radio broadcasts using Gemini 1.5 - 7th November 2024