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.

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.1a0.tar.gz (22.1 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.1a0-py3-none-any.whl (26.4 kB view details)

Uploaded Python 3

File details

Details for the file alchemy_utils-0.1a0.tar.gz.

File metadata

  • Download URL: alchemy_utils-0.1a0.tar.gz
  • Upload date:
  • Size: 22.1 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.1a0.tar.gz
Algorithm Hash digest
SHA256 e58a9a9169adf06e8ac7317a846912ba942507a739c760741e756a2eaf565fc9
MD5 b928a61345d4ebf03c5c63c82ad53e2f
BLAKE2b-256 bf26f73d1b05bc24de693f9b0d7dc8542f138034abb2f1c200ee15d56254f683

See more details on using hashes here.

Provenance

The following attestation bundles were made for alchemy_utils-0.1a0.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.1a0-py3-none-any.whl.

File metadata

  • Download URL: alchemy_utils-0.1a0-py3-none-any.whl
  • Upload date:
  • Size: 26.4 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.1a0-py3-none-any.whl
Algorithm Hash digest
SHA256 05185c9ecc02efb798654a01c2d0305658507f5980adcc81ab01c8d23b1f68b3
MD5 bbd83d5819930bc3da77025223922774
BLAKE2b-256 b5caf303ebd333534f45c2379d62aa1ccf7f5da43fc90727c7abcbcd2be82131

See more details on using hashes here.

Provenance

The following attestation bundles were made for alchemy_utils-0.1a0-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