psycodict¶
A dictionary-based Python interface to PostgreSQL, extracted from the
L-functions and Modular Forms Database (LMFDB) so that
other projects can use the SQL interface built for it. Queries are Python
dictionaries and results are Python dictionaries: you describe what you want
as a dict, psycodict turns it into SQL, runs it, and hands the rows back as
dicts. On top of that query language it provides the machinery a large,
mostly read-only research database needs — bulk loading from files, cached
statistics and counts, schema management, and tools for keeping copies of a
database in sync.
Install¶
psycodict runs on psycopg 3. psycopg is an optional dependency so that you can choose between the binary and pure-Python builds; install psycodict with one of the two extras:
pip install "psycodict[pgbinary]" # pulls in psycopg[binary]; no system libpq needed
pip install "psycodict[pgsource]" # pulls in pure-Python psycopg, which uses your system libpq
Quickstart¶
psycodict reads its connection settings from a config.ini: it uses
$PSYCODICT_CONFIG if set, then config.ini in the working directory if one
exists, and otherwise ~/.psycodict/config.ini (created with default values
on first run). Point it at your server — here the user and database from
Setting up PostgreSQL below:
[postgresql]
host = localhost
port = 5432
user = myuser
password = a good password
dbname = mydb
Then create a table, insert some rows, and search — a query is a dict, and each result is a dict:
from psycodict.database import PostgresDatabase
# create=True bootstraps the meta tables the first time you connect to a fresh database
db = PostgresDatabase(create=True)
db.create_table(
"demo_primes",
[("n", "integer"), ("label", "text"), ("factors", "jsonb"), ("is_prime", "boolean")],
label_col="label",
sort=["n"],
)
db.demo_primes.insert_many([
{"n": 6, "label": "6", "factors": {"2": 1, "3": 1}, "is_prime": False},
{"n": 7, "label": "7", "factors": {"7": 1}, "is_prime": True},
{"n": 10, "label": "10", "factors": {"2": 1, "5": 1}, "is_prime": False},
{"n": 12, "label": "12", "factors": {"2": 2, "3": 1}, "is_prime": False},
])
for row in db.demo_primes.search(
{"is_prime": False, "n": {"$lte": 10}},
projection=["n", "factors"],
):
print(row)
{'n': 6, 'factors': {'2': 1, '3': 1}}
{'n': 10, 'factors': {'2': 1, '5': 1}}
search returns every matching row; lucky returns the first match (or a
single column of it), and count counts them:
db.demo_primes.count({"is_prime": False}) # 3
db.demo_primes.lucky({"n": 7}, projection="factors") # {'7': 1}
Features¶
Dictionary query language. Equality and null tests, ranges (
$lte,$gte,$lt,$gt), membership and containment ($in,$nin,$contains), array and jsonb path access ("ainvs.2"), disjunction with$or, cardinality with$size, comparisons between columns with$col, and a raw-SQL escape hatch ($raw). Multi-table joins viajoin=onsearch,countandlucky(inner, left, right or full), with"table.column"qualification usable in queries, projections, sorts,$coland$raw.Bulk data management. Load and dump whole tables to and from files (
copy_from/copy_to), reload a table from a file andreload_revertback to the previous contents if something is wrong, and group writes intostaged()transactional uploads with write exclusion and drift detection.Cached statistics and counts. Counts and statistics tables let search pages report totals and distributions without re-scanning a dataset that rarely changes.
Schema management. Meta tables record every table, column, index and constraint; their layout carries a versioned format stamp checked on connect (an older-format database keeps working, with a warning, until migrated in place with
PostgresDatabase(upgrade=True)); partial indexes are supported, anddb.refresh_tables()picks up schema changes without a restart.Introspection. Inspect running and blocked queries (
show_queries,show_blocked) and analyze the slow-query log (psycodict.slowlog).Change notifications. LISTEN/NOTIFY-based schema-change notifications and a small publish/subscribe layer.
Database diffing.
db.compareanddb.show_differencesdetect drift between two databases.
Documentation¶
QueryLanguage — how Python dictionaries become SQL
WHEREclauses.Searching — the read-side API:
search,lucky,lookup,count, projections, sorts and joins.DataManagement — the write side: creating tables, loading data, reloading and reverting, statistics.
MetadataFormats — how the layout of the meta tables is versioned, cross-format compatibility, and the checklist for changing it.
Versioning — what the version number promises: the public API, metadata compatibility, and the deprecation policy.
The same documents live at the repository root as Markdown, next to an API reference generated from the docstrings.
See the CHANGELOG for the release history.
Supported versions¶
Python 3.9 or newer.
PostgreSQL 13 through 18.
psycopg 3.2.4 or newer (installed through the
pgbinaryorpgsourceextra above).
Python 3.9 and PostgreSQL 13 are already past their upstream end of life; psycodict keeps supporting them as legacy compatibility for downstream deployments. As Versioning spells out, dropping an end-of-life interpreter or server version is not a breaking change and can happen in a minor release.
Setting up PostgreSQL¶
If you do not already have a server, install
PostgreSQL and create a
user and a
database.
For example, in psql:
CREATE USER myuser WITH PASSWORD 'a good password';
CREATE DATABASE mydb OWNER myuser;
Making myuser the database owner matters on PostgreSQL 15 and later:
GRANT ALL PRIVILEGES ON DATABASE no longer implies the right to create
objects in the public schema, where psycodict puts its (unqualified) tables.
If the user is not the owner, grant that privilege explicitly — connected to
mydb — instead:
GRANT USAGE, CREATE ON SCHEMA public TO myuser;
PostgresDatabase(create=True) bootstraps the meta tables on first connection,
so the database only needs to exist and let myuser create tables in it — you
do not have to create any schema by hand.
Running the tests¶
Install the test dependencies and point the standard PostgreSQL environment variables at a database you do not mind being written to:
pip install -e ".[pgbinary,test]"
createdb psycodict_test
PGDATABASE=psycodict_test pytest
The connection is configured through PGHOST, PGPORT, PGUSER,
PGPASSWORD and PGDATABASE, defaulting to postgres@localhost:5432 and a
database named psycodict_test. The database only needs to be empty: the meta
tables are bootstrapped on first connection, and every test creates its own
randomly named tables and drops them afterwards.
If no server is reachable the tests that need one are skipped, so
pytest tests/test_encoding.py tests/test_utils.py tests/test_config.py
works with no database at all.
Two further sets of tests are opt-in:
PSYCODICT_TEST_DEVMIRROR=1 pytest tests/test_devmirror.py
runs read-only queries against LMFDB’s public mirror at devmirror.lmfdb.xyz,
which checks psycodict against a real 190-table schema, and
PSYCODICT_TEST_DB_REQUIRED=1 pytest
turns “no database, skip” into a hard failure — which is what continuous integration wants, so that a misconfigured job cannot report success by quietly skipping everything.
Provenance¶
psycodict was split out of the LMFDB, where it grew as
the project’s db interface. It is written and maintained by David Roe and
Edgar Costa, with contributions from the wider LMFDB community, and is
distributed under the GNU General Public License, version 2 or later (see
LICENSE).