lacing.store.postgres#
PostgreSQL-backed annotation store using int8range + GiST.
Maps lacing’s interval model to Postgres native range types (BACK-DOC §4.2):
Allen relation |
Postgres operator (int8range / tstzrange) |
|---|---|
overlaps (any) |
|
during |
|
contains |
|
meets / met_by |
|
equals |
|
before |
|
after |
|
strict overlap |
|
We store intervals as int8range(start_value, end_value, '[)') — half-open,
matching lacing’s TimeInterval semantics — at a **per-(owner, project)
rate** recorded in the projects table. All inserts must be at that rate;
re-quantizing happens on insert if necessary.
GiST index on the range column gives sub-millisecond overlap queries at
million-row scale. Optional EXCLUDE USING GIST constraints are added
per tier for stereotypes like TIME_SUBDIVISION that forbid overlap.
Multi-tenancy (Phase 4, reelee#177)#
Multiple logical stores (one per nw/reelee project) coexist in a single
Postgres database via tenant columns on shared tables (the decision in
reelee/docs/storage_migration_plan.md §”Postgres tenancy”): every
annotations and tiers row carries an owner_id and a project_id,
and every query is scoped by (owner_id, project_id). A PostgresStore
instance is bound to one (owner_id, project_id) at construction and behaves
exactly like a single-tenant store; two stores with different project ids over
the same database never see each other’s rows.
owner_id is a forward seam for the multi-tenant access layer (reelee#174):
the column is carried and scoped now, but its enforcement (the policy
decision point) is deferred — today every store defaults owner_id="default".
The per-tier EXCLUDE USING GIST no-overlap constraint is likewise scoped by
(owner_id, project_id, tier) so one project’s non-overlap rule cannot block
another project’s annotations.
Connection pooling (Phase 4)#
nw’s ProjectGraph opens and closes the store per
operation. That is fine for a SQLite file but pathological for a network DB, so
this module keeps a process-wide psycopg_pool.ConnectionPool per
connection string (see get_pool()); a PostgresStore borrows a
connection from the pool for its lifetime and returns it on close() instead
of opening a fresh TCP/auth round-trip each time. When psycopg_pool is not
installed (or a caller passes use_pool=False) we fall back to a dedicated
long-lived connection.
Schema versioning lives in meta; mismatched versions raise on open.
Status: Phase 4. Tested via pytest-postgresql sandbox (no live server
needed for development).
Module Attributes
Current schema version stored in the |
|
Owner placeholder until the multi-tenant access layer (reelee#174) lands. |
|
Project placeholder for single-project / legacy use. |
Functions
Close every registered connection pool (test teardown / shutdown). |
|
|
Replicate an in-memory store into a Postgres database (one tenant). |
|
Return a process-wide |
|
Snapshot a |
Classes
|
PostgreSQL-backed |
Exceptions
Raised on opening a database whose schema_version differs. |
|
Raised when an annotation's interval cannot be represented at the project rate. |
|
Raised when an insert would violate a per-tier no-overlap constraint. |
- lacing.store.postgres.DEFAULT_OWNER_ID = 'default'#
Owner placeholder until the multi-tenant access layer (reelee#174) lands.
The
owner_idcolumn is carried and scoped now; its enforcement is deferred.
- lacing.store.postgres.DEFAULT_PROJECT_ID = 'default'#
Project placeholder for single-project / legacy use.
- exception lacing.store.postgres.PgSchemaMismatchError[source]#
Bases:
RuntimeErrorRaised on opening a database whose schema_version differs.
- class lacing.store.postgres.PostgresStore(connection_string, *, rate=24000, owner_id='default', project_id='default', autocommit=True, use_pool=True)[source]#
Bases:
objectPostgreSQL-backed
IntervalAnnotationStore, scoped to one tenant.A store instance is bound to one
(owner_id, project_id)pair; multiple instances over the same database (different project ids) coexist without seeing each other’s annotations or tiers (see the module docstring on multi-tenancy). The mapping/Allen/tier surface is unchanged — the tenant scoping is transparent.- Parameters:
connection_string (
str|dict) – A psycopg-compatible conninfo URL, or a kwargs dict (host/port/user/password/dbname).rate (
int) – Project-wide rate for this(owner, project). Set on first init for the pair; re-opening the same pair with a different rate raisesPgSchemaMismatchError. Different projects in the same DB may have different rates.owner_id (
str) – Tenant owner. Defaults toDEFAULT_OWNER_ID— the forward seam for the access layer (reelee#174); enforcement deferred.project_id (
str) – Logical project key. Defaults toDEFAULT_PROJECT_ID.nwpasses the project’sproject_asset_idhere.autocommit (
bool) – If True (default), statements run in their own transaction.use_pool (
bool) – When True (default) and the connection is given as a string, borrow a connection from a process-wide pool keyed by the conninfo (amortizes per-op connect cost; seeget_pool()). Falls back to a dedicated connection whenpsycopg_poolis unavailable, when the connection is given as a dict, or whenuse_pool=False.
- lock(key)[source]#
Exclusive lock named
keyfor this(owner, project), across hosts.A session-level
pg_advisory_lockkeyed on a 64-bit hash ofowner_id+project_id+key, taken on a dedicated connection (never the store’s own, which other threads and transactions share) and released infinally. If the holder crashes, Postgres drops the lock when its connection dies, so nothing can go stale. Blocks until available; not re-entrant.
- update(annotation)[source]#
Replace the row with this id in ONE
UPDATEstatement (lacing#66).A single statement is atomic in any isolation mode, so a concurrent reader sees the old row or the new one — unlike
removethenadd, which commit twice. RaisesKeyErrorif the id is not in this(owner, project)andTierOverlapErrorlikeadd().- Return type:
- exception lacing.store.postgres.RateMismatchError[source]#
Bases:
LossyTimeConversionErrorRaised when an annotation’s interval cannot be represented at the project rate.
- lacing.store.postgres.SCHEMA_VERSION = 3#
Current schema version stored in the
metatable.v2 (Phase 4, reelee#177) added tenant columns (
owner_id/project_id) totiersandannotations, and a per-(owner, project)projectstable holding the rate (was a singlemetarow in v1).v3 (lacing#14, defect D5):
prov_was_derived_frommay contain 64-hex artifactasset_idstrings alongside annotation UUIDs. Layout and rows unchanged — the bump makes pre-v3 builds (whose read path eagerlyUUID()-parses the column) refuse rather than crash mid-read.The migration ladder (
lacing.store.migrations,store_kind="postgres") is deliberately empty: no Postgres database at any version ever held real data — the backend is config-selectable and was never enabled in any environment; production has always been the per-caller sqlite.annotlayout (verified 2026-08-15, lacing#15). Dev databases at v1/v2 are recreated, not migrated. Register steps from v3 forward once a deployed Postgres exists.
- exception lacing.store.postgres.TierOverlapError[source]#
Bases:
RuntimeErrorRaised when an insert would violate a per-tier no-overlap constraint.
- lacing.store.postgres.close_all_pools()[source]#
Close every registered connection pool (test teardown / shutdown).
- Return type:
- lacing.store.postgres.from_memory(memory_store, connection_string, *, rate=24000, owner_id='default', project_id='default')[source]#
Replicate an in-memory store into a Postgres database (one tenant).
- Return type:
- lacing.store.postgres.get_pool(connection_string, **pool_kwargs)[source]#
Return a process-wide
psycopg_pool.ConnectionPoolfor a conninfo.One pool per distinct
connection_string(the keying happens here, not at the call-site), created lazily on first request. RaisesImportErrorifpsycopg_poolis not installed — callers that want a graceful fallback catch it (seePostgresStore).- Parameters:
connection_string (
str) – psycopg conninfo URL (the registry key).**pool_kwargs – forwarded to
ConnectionPoolon first creation (e.g.min_size,max_size); ignored on reuse.
- Returns:
The shared pool for this conninfo.