This release is a pre-release and may not be stable for production use.
alchemy-utils
An executable research spike for a subset of the
sqlite-utils Python API backed by
SQLAlchemy Core, built using GPT-5.6 Sol Ultra and Codex.
It demonstrates the same style of table-first API across SQLite, PostgreSQL, and DuckDB:
from alchemy_utils import Database
db = Database("sqlite:///:memory:")
people = db["people"].insert(
{"id": 1, "name": "Ada", "profile": {"language": "Python"}},
pk="id",
)
people.upsert({"id": 1, "name": "Ada Lovelace"})
people.insert_all(
[
{"id": 2, "name": "Grace"},
{"id": 3, "name": "Katherine"},
]
)
people.update(2, {"name": "Grace Hopper"})
assert people.pks == ["id"]
assert people.columns_dict["profile"] is dict
assert people.get(1)["name"] == "Ada Lovelace"
Swap only the URL to use another engine:
postgres = Database("postgresql+psycopg://user:password@localhost/app")
duckdb = Database("duckdb:///analytics.duckdb")
This is a spike, not a published compatibility promise. See RESEARCH.md for the conclusion, design trade-offs, and a rough production estimate.
Implemented API
Database supports:
- construction from a SQLAlchemy
Engine,URL, URL string, or SQLite path; db[name],db.table(), anddb.create_table();table_names(),view_names(),tables, and normalizedschema;- context-manager cleanup and
close().
Table supports:
create()with Python or SQLAlchemy types, single/compound primary keys, partial column ordering,NOT NULL, server defaults, single/compound foreign keys, and existing-table options;insert(),insert_all(),upsert(),upsert_all(), andupdate();- generated integer primary keys on all three engines;
alter=Truefor new nullable columns, plus insertignore=Trueandreplace=Trueconflict modes;exists(),count,rows, andget();columns,columns_dict,pks,foreign_keys,indexes,schema,default_values, anduse_rowid.
All mutation methods return the same Table object for chaining. A one-record
write sets last_pk; bulk writes leave it as None.
Engine-specific architecture
Calling Database(...) selects an independent engine implementation:
Database factory
├── SQLiteDatabase SQLite ON CONFLICT and rowid capability
├── PostgreSQLDatabase PostgreSQL ON CONFLICT
└── DuckDBDatabase DuckDB ON CONFLICT, sequences, catalog fallbacks
│
└── PK / JSON / index / DDL reflection repairs
Table shared API and orchestration only
The shared Table class does not inspect dialect names or build dialect SQL.
It delegates conflict statements, generated-key DDL, table lifecycle, and
reflection to its Database instance.
Install
The base package needs Click and SQLAlchemy and works with Python 3.10 or later. Engine drivers are extras:
pip install alchemy-utils
pip install 'alchemy-utils[postgresql]'
pip install 'alchemy-utils[duckdb]'
For this checkout, uv sync installs the development group, including both
drivers and the test tools.
Command-line interface
Installing the package adds a collision-safe alchemy-utils command.
It follows the relevant sqlite-utils command shapes, but its DATABASE
argument can be either a SQLite filename or any installed SQLAlchemy URL:
# SQLite: a bare path
alchemy-utils create-table data.db people \
id integer name text profile json --pk id --not-null name
# PostgreSQL and DuckDB: SQLAlchemy URLs
alchemy-utils tables \
postgresql+psycopg://user@localhost/app --json
alchemy-utils schema duckdb:///analytics.duckdb
For PostgreSQL credentials, prefer libpq environment variables or a password file instead of putting a password in the command-line URL, where it could be recorded in shell history.
insert and upsert each handle either one record or many records, mapping to
the corresponding single-record or *_all() API. The default input is a JSON
object or array. Use --nl, --csv, or --tsv for other formats; - reads
standard input, and those formats are also detected from file extensions.
CSV and TSV inputs are streamed in batches of 100 records; use --batch-size
to tune the batch size. --alter scans all records first so it can add every
missing column before writing.
echo '{"id": 1, "name": "Ada"}' \
| alchemy-utils insert data.db people -
printf '%s\n' \
'{"id": 1, "name": "Ada Lovelace"}' \
'{"id": 2, "name": "Grace Hopper"}' \
| alchemy-utils upsert data.db people - --nl
echo '{"name": "Amazing Grace", "active": true}' \
| alchemy-utils update data.db people 2 - --alter
Repeat --pk for compound keys. get and update accept a JSON array for a
compound key, for example '["acme", 7]'. Binary JSON values use sqlite-utils'
portable shape, {"$base64": true, "encoded": "AP8="}.
Available inspection and read commands are tables, views, schema,
columns, indexes, foreign-keys, rows, get, and count. Metadata and
rows use normalized JSON (or JSON lines with --nl where offered); schema is
engine-shaped reflected DDL. tables and views use JSON by default, with
--plain for one name per line. Mutations are silent on success, as in
sqlite-utils. Run any command with --help for its options. The same interface
is also available as python -m alchemy_utils.
Test
uv sync
uv run ruff check src tests
uv run pytest
PostgreSQL tests use testing.postgresql to start one disposable server and a
unique database per test. They never use an existing application database. The
fixture finds postgres and initdb from environment variables, PATH, or
pg_config; if it cannot find them, PostgreSQL cases are skipped.
Homebrew example:
PG_BIN="$(brew --prefix postgresql@18)/bin"
POSTGRESQL_PATH="$PG_BIN/postgres" \
INITDB_PATH="$PG_BIN/initdb" \
uv run pytest
The library and CLI suites run on all three engines and have been exercised on both Python 3.10 and 3.14.3, with SQLAlchemy 2.0.52, SQLite 3.50.4, PostgreSQL 18.3, DuckDB 1.5.5, duckdb-engine 0.17.0, and psycopg 3.3.4.
Deliberate spike limitations
- Bulk inputs are materialized in memory;
batch_sizeis accepted but not yet used for streaming chunks. - Bulk input supports mappings, not sqlite-utils' header-plus-sequence mode.
alter=Trueonly adds nullable columns. Full transforms are out of scope.- Exact SQLite DDL text, implicit indexes, triggers, checks, FTS, and STRICT metadata are not portable and are not emulated.
hash_id, extracts, conversions,analyze, and schema transforms are not implemented.- Reflected type names and raw server-default SQL vary by engine. The stable portable fields are names, Python types, nullability, key ordering, foreign key shape, and explicit indexes.
- A hidden
rowidis never synthesized as a primary key.use_rowidreports the SQLite capability, whilepksreturns only declared keys on every engine. - Mixing explicit integer primary keys with later generated keys may require synchronizing the PostgreSQL or DuckDB sequence; this spike does not do that.
- DuckDB expression-index parsing is intentionally best-effort; ordinary column indexes are covered.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file alchemy_utils-0.1a1.tar.gz.
File metadata
- Download URL: alchemy_utils-0.1a1.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78ba8ee725c9d6b7f3621d2355131f32c88d17483595674b4e4f34fd65f9901f
|
|
| MD5 |
5d6ab555cc4103ff489c0d56331365e8
|
|
| BLAKE2b-256 |
edc8e89a75392c9926a42a1ad9c023a7c18ebb83d3599f608e863d1547bc9d25
|
Provenance
The following attestation bundles were made for alchemy_utils-0.1a1.tar.gz:
Publisher:
publish.yml on simonw/alchemy-utils
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
alchemy_utils-0.1a1.tar.gz -
Subject digest:
78ba8ee725c9d6b7f3621d2355131f32c88d17483595674b4e4f34fd65f9901f - Sigstore transparency entry: 2445640296
- Sigstore integration time:
-
Permalink:
simonw/alchemy-utils@c2432fb580317dd6a8cdf3e3451a2fc6a85631f7 -
Branch / Tag:
refs/tags/0.1a1 - Owner: https://github.com/simonw
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c2432fb580317dd6a8cdf3e3451a2fc6a85631f7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file alchemy_utils-0.1a1-py3-none-any.whl.
File metadata
- Download URL: alchemy_utils-0.1a1-py3-none-any.whl
- Upload date:
- Size: 27.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3239b56d3375ce2270577657ab4938fedd245cef1a5312bfae31be99472d7c3
|
|
| MD5 |
4e056385d424225dd753cc8ed7f2cb5b
|
|
| BLAKE2b-256 |
8bd4700efee252bac851774e1109a8b2f2d056f7d73684bb6868385231784d0e
|
Provenance
The following attestation bundles were made for alchemy_utils-0.1a1-py3-none-any.whl:
Publisher:
publish.yml on simonw/alchemy-utils
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
alchemy_utils-0.1a1-py3-none-any.whl -
Subject digest:
d3239b56d3375ce2270577657ab4938fedd245cef1a5312bfae31be99472d7c3 - Sigstore transparency entry: 2445640421
- Sigstore integration time:
-
Permalink:
simonw/alchemy-utils@c2432fb580317dd6a8cdf3e3451a2fc6a85631f7 -
Branch / Tag:
refs/tags/0.1a1 - Owner: https://github.com/simonw
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c2432fb580317dd6a8cdf3e3451a2fc6a85631f7 -
Trigger Event:
release
-
Statement type: