Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

alchemy-utils

PyPI Tests Changelog License

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(), and db.create_table();
  • table_names(), view_names(), tables, and normalized schema;
  • 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(), and update();
  • generated integer primary keys on all three engines;
  • alter=True for new nullable columns, plus insert ignore=True and replace=True conflict modes;
  • exists(), count, rows, and get();
  • columns, columns_dict, pks, foreign_keys, indexes, schema, default_values, and use_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_size is accepted but not yet used for streaming chunks.
  • Bulk input supports mappings, not sqlite-utils' header-plus-sequence mode.
  • alter=True only 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 rowid is never synthesized as a primary key. use_rowid reports the SQLite capability, while pks returns 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

alchemy_utils-0.1a1.tar.gz (23.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

alchemy_utils-0.1a1-py3-none-any.whl (27.7 kB view details)

Uploaded Python 3

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

Hashes for alchemy_utils-0.1a1.tar.gz
Algorithm Hash digest
SHA256 78ba8ee725c9d6b7f3621d2355131f32c88d17483595674b4e4f34fd65f9901f
MD5 5d6ab555cc4103ff489c0d56331365e8
BLAKE2b-256 edc8e89a75392c9926a42a1ad9c023a7c18ebb83d3599f608e863d1547bc9d25

See more details on using hashes here.

Provenance

The following attestation bundles were made for alchemy_utils-0.1a1.tar.gz:

Publisher: publish.yml on simonw/alchemy-utils

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for alchemy_utils-0.1a1-py3-none-any.whl
Algorithm Hash digest
SHA256 d3239b56d3375ce2270577657ab4938fedd245cef1a5312bfae31be99472d7c3
MD5 4e056385d424225dd753cc8ed7f2cb5b
BLAKE2b-256 8bd4700efee252bac851774e1109a8b2f2d056f7d73684bb6868385231784d0e

See more details on using hashes here.

Provenance

The following attestation bundles were made for alchemy_utils-0.1a1-py3-none-any.whl:

Publisher: publish.yml on simonw/alchemy-utils

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page