| """core/store_pg.py β `core/store.py`'s interface, backed by Postgres (X4 / EXIT-2b, 2026-07-30). |
| |
| THE INTERFACE IS THE CONTRACT, and it is `core/store.py`'s nine functions: |
| |
| available() Β· get(name, fresh=False) Β· exists(name) Β· put(name, data) |
| update(name, fn, flush='sync') Β· upload_bytes(path, data, message=None) |
| download_bytes(path) Β· delete_path(path) Β· flush(name=None, timeout=30.0) |
| |
| Same names, same signatures, same return types, same failure semantics β so switching backends is |
| an env var (`STORE_BACKEND=hf|pg`) and not a rewrite of every caller. `core/store_backend.py` |
| does the selection; nothing above it needs to know which store it is talking to. |
| |
| β
VERIFIED against a real managed Postgres 2026-08-04 (W19: Neon us-east-2; `verify_store_pg.py` |
| 80/80 INCLUDING the integration half β schema apply, tenant provisioning, jsonb/bytea round-trips, |
| 80 concurrent FOR-UPDATE writes β run from HF egress; the owner's local network resets TLS:5432). |
| The CUTOVER remains parked behind C1e's triggers and this module is not the default backend. |
| Without `DATABASE_URL` the gate still SKIPS its integration half LOUDLY, never silently. |
| The schema is `harness/pg/schema.sql`. Sequenced around the blocker, never stubbed past it. |
| |
| WHAT POSTGRES FIXES, precisely (C1c β these are the reasons, not a preference): |
| * **`get()` is cache-first on the HF store**, so a write from another process is invisible to a |
| running app until restart. Observed live in wave 11. Here a read is a SELECT: cross-process |
| coherence is the default rather than a thing to remember. |
| * **`update()` was read-modify-write with no concurrency control** β two writers raced and the |
| loser vanished silently. Here it runs inside ONE transaction with `SELECT β¦ FOR UPDATE`, so |
| the second writer waits and then sees the first writer's value. |
| * **A write was an HTTP commit** against a 256-commits/hour repo budget, which is why the hot |
| path needed a coalescing async flusher at all. A Postgres write is a write; `flush='async'` |
| stays in the signature and becomes a no-op, because there is nothing to coalesce. |
| |
| β ONE DELIBERATE DIFFERENCE FROM THE HF BACKEND, and it is a fix. `store.get()` swallows transient |
| read failures and hands back a cached-or-empty dict, because a display read must not break a page. |
| That leniency is exactly what wiped the user registry once (an empty read merged into a write), and |
| `_read_strict` exists to opt out of it. Postgres has no equivalent "the network blipped" state |
| worth hiding: a connection failure RAISES from `get()` here. Callers that must not break on a read |
| should catch it β silently returning `{}` from a database that is simply unreachable is how an |
| empty dict gets written back over real data. |
| """ |
| import json |
| import os |
| import threading |
|
|
| _URL_ENV = 'DATABASE_URL' |
| _LOCK = threading.RLock() |
| _POOL = {'pool': None, 'url': None, 'schema': None} |
|
|
|
|
| def _url(): |
| return os.environ.get(_URL_ENV) or None |
|
|
|
|
| def _schema_for(tenant_slug=None): |
| """`t_<slug>` with `-` normalised to `_` (a hyphen is not legal in an unquoted identifier). |
| |
| Mirrors `control.provision_tenant_schema` exactly β the two MUST agree or a write lands in a |
| schema the DDL never created. `AIOS_TENANT` names the tenant this process serves; it defaults |
| to tenant #0 so the default behaviour is unchanged, the same rule `harness.datastore.path_for` |
| follows for the DuckDB filename. |
| """ |
| slug = (tenant_slug or os.environ.get('AIOS_TENANT') or 'royal-imports').strip().lower() |
| return 't_' + slug.replace('-', '_') |
|
|
|
|
| def _pool(): |
| """The psycopg connection pool, opened once per process. |
| |
| psycopg is imported LAZILY and only here: `STORE_BACKEND=hf` must not require the dependency |
| to be installed at all, which is what keeps this file safe to commit before B-3 unblocks. |
| """ |
| with _LOCK: |
| url = _url() |
| if not url: |
| raise RuntimeError( |
| f'{_URL_ENV} is not set β the Postgres store backend has nothing to connect to.') |
| if _POOL['pool'] is not None and _POOL['url'] == url: |
| return _POOL['pool'] |
| try: |
| from psycopg_pool import ConnectionPool |
| except ImportError as e: |
| raise RuntimeError( |
| 'STORE_BACKEND=pg needs psycopg[binary,pool]. It is deliberately NOT in ' |
| 'requirements.txt until the database exists (owner blocker B-3), so that the ' |
| 'default hf backend installs nothing it does not use.') from e |
| if _POOL['pool'] is not None: |
| try: |
| _POOL['pool'].close() |
| except Exception: |
| pass |
| |
| |
| _POOL['pool'] = ConnectionPool(url, min_size=0, max_size=8, open=True, timeout=10.0) |
| _POOL['url'] = url |
| return _POOL['pool'] |
|
|
|
|
| def available(): |
| """True when a connection can actually be MADE, not merely when a URL is configured. |
| |
| `core.store.available()` only checks for a token, which is all it can cheaply do. Here the |
| honest answer needs a round-trip, so the result is memoised per process: callers use this to |
| decide whether to degrade the UI, and a `SELECT 1` per render would be absurd. |
| """ |
| if not _url(): |
| return False |
| if _POOL.get('ok'): |
| return True |
| try: |
| with _pool().connection() as con: |
| con.execute('SELECT 1') |
| _POOL['ok'] = True |
| return True |
| except Exception: |
| return False |
|
|
|
|
| def _table(kind, tenant_slug=None): |
| """`"t_slug"."store_kv"` β the qualified table, safely quoted.""" |
| from psycopg import sql |
| return sql.SQL('{}.{}').format(sql.Identifier(_schema_for(tenant_slug)), |
| sql.Identifier(kind)) |
|
|
|
|
| def _q(template, kind, tenant_slug=None): |
| """Compose `template` with `{tbl}` = the QUALIFIED table and `{bare}` = the table name alone. |
| |
| β THE TWO ARE NOT INTERCHANGEABLE, and getting it wrong is a runtime syntax error nothing |
| local would catch (no database here β owner blocker B-3). Inside `ON CONFLICT β¦ DO UPDATE`, |
| Postgres refers to the conflicting row by the table's ALIAS, which defaults to the bare |
| table name: `store_kv.rev` is valid, `t_royal_imports.store_kv.rev` is not. So the FROM |
| position takes `{tbl}` and the DO-UPDATE position takes `{bare}`. |
| |
| Identifiers go through `psycopg.sql.Identifier`, so the schema name β derived from a tenant |
| slug β cannot be an injection vector even though it is interpolated. The slug is ALSO |
| regex-constrained at its source (`control.tenants`), which is the belt to this brace. |
| """ |
| from psycopg import sql |
| return sql.SQL(template).format(tbl=_table(kind, tenant_slug), |
| bare=sql.Identifier(kind)) |
|
|
|
|
| def get(name, fresh=False, tenant_slug=None): |
| """The value stored under `name`, or `{}` when the key is genuinely absent. |
| |
| `fresh` is accepted and IGNORED: it exists in the HF signature to bypass a process cache, and |
| there is no cache here β every read is a SELECT. Keeping the parameter means callers do not |
| branch on the backend. |
| """ |
| with _pool().connection() as con: |
| row = con.execute( |
| _q('SELECT value FROM {tbl} WHERE key = %s', 'store_kv', tenant_slug), |
| (str(name),)).fetchone() |
| return dict(row[0]) if row and isinstance(row[0], dict) else (row[0] if row else {}) |
|
|
|
|
| def get_projection(name, drop=(), tenant_slug=None): |
| """`core.store.get_projection`'s twin β see `PgStore.get_projection` for why the saving does |
| NOT transfer and why that is stated rather than implied. |
| |
| β Present because `verify_store_pg.IFACE` names it, which is the mechanism working as designed: |
| a capability only the HF backend had would 500 on `/nav` at the cutover instead of failing here, |
| at review, with no database attached. |
| """ |
| import core.store as _hf |
| return _hf._project(get(name, tenant_slug=tenant_slug), |
| frozenset(str(d) for d in drop)) |
|
|
|
|
| def exists(name, tenant_slug=None): |
| """True/False β and unlike the HF backend this is NEVER "True on uncertainty". |
| |
| `core.store.exists` returns True on any error so a caller that would seed the store cannot |
| clobber a registry it merely failed to reach. That guard exists because a FILE HOST cannot |
| distinguish "absent" from "unreachable". Postgres can: an absent row is a fact, and an |
| unreachable server raises instead of answering. |
| """ |
| with _pool().connection() as con: |
| row = con.execute( |
| _q('SELECT 1 FROM {tbl} WHERE key = %s', 'store_kv', tenant_slug), |
| (str(name),)).fetchone() |
| return row is not None |
|
|
|
|
| def put(name, data, tenant_slug=None): |
| with _pool().connection() as con: |
| con.execute( |
| _q('INSERT INTO {tbl} (key, value) VALUES (%s, %s::jsonb) ' |
| 'ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, ' |
| 'updated_at = now(), rev = {bare}.rev + 1', 'store_kv', tenant_slug), |
| (str(name), json.dumps(data))) |
| return data |
|
|
|
|
| def update(name, fn, flush='sync', tenant_slug=None): |
| """Read-modify-write inside ONE transaction, with the row LOCKED for the duration. |
| |
| This is the operation the HF backend could not make safe. There, `update` read over HTTP, |
| applied `fn`, and uploaded β so two concurrent writers both read the old value and the second |
| upload silently discarded the first writer's change. `SELECT β¦ FOR UPDATE` makes the second |
| writer WAIT and then apply `fn` to the first writer's result, which is what "read-modify-write" |
| was always supposed to mean. |
| |
| `flush` is accepted and ignored (see the module docstring): there is no commit budget to |
| coalesce against, so 'async' has nothing to defer. |
| """ |
| with _pool().connection() as con: |
| with con.transaction(): |
| row = con.execute( |
| _q('SELECT value FROM {tbl} WHERE key = %s FOR UPDATE', |
| 'store_kv', tenant_slug), |
| (str(name),)).fetchone() |
| data = dict(row[0]) if row and isinstance(row[0], dict) else (row[0] if row else {}) |
| result = fn(data) |
| data = result if result is not None else data |
| con.execute( |
| _q('INSERT INTO {tbl} (key, value) VALUES (%s, %s::jsonb) ' |
| 'ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, ' |
| 'updated_at = now(), rev = {bare}.rev + 1', 'store_kv', tenant_slug), |
| (str(name), json.dumps(data))) |
| return data |
|
|
|
|
| def upload_bytes(path_in_repo, data, message=None, tenant_slug=None): |
| with _pool().connection() as con: |
| con.execute( |
| _q('INSERT INTO {tbl} (path, bytes, message) VALUES (%s, %s, %s) ' |
| 'ON CONFLICT (path) DO UPDATE SET bytes = EXCLUDED.bytes, ' |
| 'message = EXCLUDED.message, updated_at = now()', 'store_blobs', tenant_slug), |
| (str(path_in_repo), bytes(data), message)) |
|
|
|
|
| def download_bytes(path_in_repo, tenant_slug=None): |
| """Raw bytes, or None when absent β the same contract as the HF backend: a missing attachment |
| degrades to "that file is gone", never to a broken page.""" |
| with _pool().connection() as con: |
| row = con.execute( |
| _q('SELECT bytes FROM {tbl} WHERE path = %s', 'store_blobs', tenant_slug), |
| (str(path_in_repo),)).fetchone() |
| return bytes(row[0]) if row else None |
|
|
|
|
| def delete_path(path_in_repo, tenant_slug=None): |
| """Absent is SUCCESS β deleting what is already gone is the goal (HF backend's contract).""" |
| with _pool().connection() as con: |
| con.execute(_q('DELETE FROM {tbl} WHERE path = %s', 'store_blobs', tenant_slug), |
| (str(path_in_repo),)) |
| return True |
|
|
|
|
| def revision(name, tenant_slug=None): |
| """`{'rev', 'updated_at', 'token'}` for ONE bucket β wave 29, item 20 / contract C6. |
| |
| β THIS BACKEND HAS MAINTAINED THE ANSWER SINCE X4 AND NEVER PUBLISHED IT. Every `put` and |
| `update` above already ends `updated_at = now(), rev = {bare}.rev + 1`; all this adds is the |
| read door. Which is why C6 insisted the HF side use these two column NAMES for its in-memory |
| counter: the wire shape does not change at the cutover, so the client keeps polling the same |
| endpoint and the token simply stops being per-process. |
| |
| β AND HERE THE TOKEN IS DURABLE, which is the real upgrade. `core.store`'s counter lives in |
| one process, so it cannot see a write by a second container and it restarts at zero β hence |
| the process stamp in its token. `rev` is a column: it survives a restart, and it is shared by |
| every process pointed at this schema. The prefix below is therefore a constant rather than a |
| per-process value, and a backend flip changes the token exactly once (one refetch, correct). |
| |
| An absent key is `rev 0`, not an error: a bucket nobody has written yet has changed zero times, |
| which is a fact a client can hold a baseline against. |
| """ |
| with _pool().connection() as con: |
| row = con.execute( |
| _q('SELECT rev, updated_at FROM {tbl} WHERE key = %s', 'store_kv', tenant_slug), |
| (str(name),)).fetchone() |
| rev = int(row[0]) if row and row[0] is not None else 0 |
| stamp = row[1] if row else None |
| |
| |
| |
| at = stamp.timestamp() if hasattr(stamp, 'timestamp') else None |
| return {'rev': rev, 'updated_at': at, 'token': f'pg:{rev}'} |
|
|
|
|
| def flush(name=None, timeout=30.0): |
| """A no-op that returns True: every write above is already committed when it returns. Kept in |
| the interface because QA gates and shutdown hooks call it and must not branch on the backend.""" |
| return True |
|
|
|
|
| def close(): |
| """Release the pool (tests, and a process that is shutting down cleanly).""" |
| with _LOCK: |
| if _POOL['pool'] is not None: |
| try: |
| _POOL['pool'].close() |
| finally: |
| _POOL['pool'] = None |
| _POOL['url'] = None |
| _POOL.pop('ok', None) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class PgStore: |
| """`core.store.Store`'s interface for ONE tenant's schema. |
| |
| Deliberately a thin binder over the module functions rather than a reimplementation: every |
| behaviour argument (FOR UPDATE in `update`, absent-is-success in `delete_path`, `fresh` |
| ignored because a SELECT has no cache to bypass) is stated once, up there, and cannot drift |
| between the two entry points. |
| """ |
|
|
| def __init__(self, tenant_slug): |
| self.tenant_slug = str(tenant_slug or '').strip().lower() or 'royal-imports' |
| |
| |
| self.repo = f'pg:{_schema_for(self.tenant_slug)}' |
|
|
| def __repr__(self): |
| return f'<PgStore {self.repo}>' |
|
|
| def available(self): |
| return available() |
|
|
| def get(self, name, fresh=False): |
| return get(name, fresh=fresh, tenant_slug=self.tenant_slug) |
|
|
| def get_projection(self, name, drop=()): |
| """`Store.get_projection`'s contract on Postgres β same ANSWER, different economics. |
| |
| ββ W32-T01, AND IT IS HERE FOR A REASON THE GATE CANNOT STATE. `get_projection` is a |
| METHOD, not a module function, so `verify_store_pg`'s IFACE list does not force it (that |
| list pins the ten module-level operations, and a public module function here would fail its |
| `extra:` check). But `harness.runtime.TenantRuntime.get_projection` resolves through |
| `store.handle()`, which returns THIS class when `STORE_BACKEND=pg` β so a method only the HF |
| store had would `AttributeError` at the cutover, on `/nav`, for every tenant at once. That |
| is exactly what `revision()`'s docstring means by *"a capability only the HF store had would |
| have 500'd at the cutover instead of at review"*, and this is the review. |
| |
| β **The saving does NOT transfer, and pretending otherwise would be the lie.** On HF the |
| whole tenant document is one JSON blob in memory and the cost is the 28.6 MB deep copy this |
| skips. Here `get` is a SELECT that has already materialised the row, so projecting after the |
| fact saves the copy of the dropped keys and nothing on the wire. It is correct, not fast. |
| A cheaper pg projection is a `jsonb` column list in the SELECT itself β a real optimisation, |
| for the day D-4 actually flips, and not something to build blind against a database that |
| does not exist yet (blocker B-3). |
| """ |
| import core.store as _hf |
| return _hf._project(get(name, tenant_slug=self.tenant_slug), |
| frozenset(str(d) for d in drop)) |
|
|
| def _read_strict(self, name): |
| """Interface parity with the HF Store. There, `get` is lenient (swallows a transient |
| failure) and `_read_strict` opts out so a failed read ABORTS a read-modify-write instead |
| of merging into `{}`. Here `get` ALREADY raises β the leniency it opts out of does not |
| exist β so the strict read is the ordinary one, and saying so beats a second code path.""" |
| return get(name, tenant_slug=self.tenant_slug) |
|
|
| def exists(self, name): |
| return exists(name, tenant_slug=self.tenant_slug) |
|
|
| def put(self, name, data): |
| return put(name, data, tenant_slug=self.tenant_slug) |
|
|
| def update(self, name, fn, flush='sync'): |
| return update(name, fn, flush=flush, tenant_slug=self.tenant_slug) |
|
|
| def revision(self, name): |
| return revision(name, tenant_slug=self.tenant_slug) |
|
|
| def upload_bytes(self, path_in_repo, data, message=None): |
| return upload_bytes(path_in_repo, data, message=message, tenant_slug=self.tenant_slug) |
|
|
| def download_bytes(self, path_in_repo): |
| return download_bytes(path_in_repo, tenant_slug=self.tenant_slug) |
|
|
| def delete_path(self, path_in_repo): |
| return delete_path(path_in_repo, tenant_slug=self.tenant_slug) |
|
|
| def flush(self, name=None, timeout=30.0): |
| return True |
|
|
| def _flush_now_at_exit(self): |
| """The atexit hook `core.store` registers for its instances. Nothing is buffered here β |
| every write above committed before it returned β so this is honestly a no-op rather than |
| an unimplemented method that would raise during interpreter shutdown.""" |
| return True |
|
|