PostgreSQL operational ledger

Use PostgreSQL when a project needs a shared operational ledger. SQLite remains the portable archive format.

When to choose PostgreSQL

Choose the backend by how the ledger is accessed, not by its size. A SQLite ledger stays small and fast well beyond typical project sizes: 5,000 structures of 108 atoms with three evaluations each (energy, forces, one scalar) take about 100 MB, roughly 20 kB per structure including its evaluations.

Access pattern Backend
One user or ingest process, ledger on a local disk SQLite
Several processes or agents on one machine, ledger on a local disk, occasional batch writes SQLite
Processes on different machines or cluster nodes, or a ledger file on NFS, Lustre, or another network filesystem PostgreSQL
Frequent or long write transactions, writers failing with database is locked PostgreSQL
Access control or remote access for several users PostgreSQL

SQLite allows one writer at a time. Content-aware structure ingestion takes the write lock before checking for duplicates, so concurrent writers on one machine cannot create duplicate structures; they queue instead, each waiting up to SQLiteRepository(timeout=30.0) seconds before failing with database is locked. In the default DELETE journal mode, a commit also waits for active readers, so long streaming queries can delay writers. For several concurrent local processes, open the ledger in WAL mode, where readers and the writer do not block each other:

from stoms.atomsledger import AtomsLedger, SQLiteRepository

ledger = AtomsLedger(
    SQLiteRepository("project.atomsledger", journal_mode="WAL")
)

Every process sharing the file must use the same journal mode, because each connection sets it. In WAL mode, call ledger.repository.checkpoint() from time to time if long-lived readers keep the -wal file growing.

SQLite file locking, and WAL in particular, is not reliable on network filesystems, so processes on several machines or nodes must not share one SQLite file. Use PostgreSQL there. There is no need to switch early: migration with import_sqlite (below) preserves IDs, provenance, tags, and aggregations. Keep exporting SQLite snapshots from PostgreSQL for archiving and for browsing with stoms serve.

Install and connect

In the project that uses STOMS, install the PostgreSQL extra:

uv add "stoms[postgres]"

Construct the ledger with the database DSN and a schema owned only by STOMS:

import os

from stoms.atomsledger import AtomsLedger, PostgresRepository

ledger = AtomsLedger(
    PostgresRepository(
        os.environ["PROJECT_LEDGER_DSN"],
        schema="project_ledger",
    )
)

PostgresRepository creates and validates its tables in that schema. Keep the DSN in the project environment, not in source control. Do not share the schema with unrelated applications.

Use this ledger exactly like a SQLite ledger for ingestion, queries, tags, stored queries, aggregations, and exports.

Synchronize SQLite and PostgreSQL

Transfers are additive: they copy records missing from the target and verify the fingerprint of records already present. They preserve IDs, provenance, post-ingestion tags, trajectories, stored queries, and aggregations. Transfers never delete records from either side.

Import a portable SQLite archive into PostgreSQL:

from stoms.atomsledger import import_sqlite

report = import_sqlite("project.atomsledger", ledger.repository)
print(report)

Export the PostgreSQL ledger as a fresh portable SQLite archive:

report = ledger.export_database("project-backup.atomsledger", overwrite=True)
print(report)

For an existing SQLite target, use transfer_repository directly:

from stoms.atomsledger import SQLiteRepository, transfer_repository

report = transfer_repository(
    ledger.repository,
    SQLiteRepository("project-copy.atomsledger"),
)
print(report)

The default conflict policy is "verify": a matching ID with different immutable content raises IntegrityError. Use conflict="error" to reject every pre-existing record, or conflict="skip" only when intentional.

Concurrency

PostgreSQL, not STOMS, provides the base guarantees for multiple simultaneous users: transactional isolation, row-level locking, and connection authentication/authorization through normal PostgreSQL roles and pg_hba.conf rules. STOMS adds no server, authorization layer, or connection pool of its own; provision and secure the database the way you would for any other PostgreSQL-backed application.

On top of that, PostgresRepository guarantees ledger-specific correctness under concurrent STOMS writers: content-aware structure ingestion is serialized with advisory locks so concurrent inserts of the same structure content cannot create duplicate records, batch writes are atomic and roll back together on failure, and aggregation creation with a repeated external_key is idempotent under concurrent retries rather than racing into duplicate or conflicting aggregations.