| """core/store_backend.py β which store is active: `STORE_BACKEND=hf|pg` (X4 / EXIT-2b). |
| |
| import core.store_backend as store # new code |
| store.get('users') # goes to whichever backend is selected |
| |
| β **C-4 HAPPENED β WAVE 20, 2026-08-05 (owner ruling R1, DEBT D-4).** This header used to say, |
| in bold, that flipping `STORE_BACKEND=pg` did NOT redirect the ~28 callers that say |
| `import core.store as store`, and that "the tempting shortcut β have `core/store.py` itself |
| delegate" would be worse because there was "no dual-read window and no way to compare the two |
| stores' contents first". |
| |
| Both halves of that objection were ANSWERED rather than ignored, which is the only reason the |
| shortcut became the design: |
| * the comparison exists β `ops/seed_pg_from_hf.py --verify` diffs the two stores key-by-key |
| (and value-by-value) and is run BEFORE any environment flips; |
| * the "silently changes backend the moment an env var is set in some shell" risk is why the |
| flip is fail-closed and loud: no `DATABASE_URL` under `pg` RAISES, it never falls back to the |
| file store. A misconfigured process refuses to serve instead of quietly writing to the wrong |
| place β which is the failure mode the original warning actually cared about. |
| |
| So **`core/store.py::handle()` is now the seam**, and it is the one every caller crosses: |
| module-level functions resolve it per call via `_d()`, and `harness.runtime.get_runtime` binds a |
| per-TENANT handle (a dataset repo on `hf`, a `t_<slug>` schema on `pg`). THIS module remains the |
| by-name selector for code that wants a specific backend's module rather than the active handle β |
| the migration tooling, and `verify_store_pg`, which asserts `store.backend()` and `name()` agree |
| on every value so the two entry points cannot drift. |
| |
| An unrecognised value is a configuration error and RAISES rather than falling back β a typo'd |
| backend name that quietly served the old store would be discovered by data going missing. |
| """ |
| import os |
|
|
| import core.store as _hf |
|
|
| _NAMES = ('available', 'get', 'exists', 'put', 'update', 'upload_bytes', 'download_bytes', |
| 'delete_path', 'flush') |
|
|
|
|
| def name(): |
| """The selected backend name, validated. `hf` unless explicitly told otherwise.""" |
| raw = (os.environ.get('STORE_BACKEND') or 'hf').strip().lower() |
| if raw not in ('hf', 'pg'): |
| raise RuntimeError( |
| f"STORE_BACKEND={raw!r} is not a backend. Use 'hf' (the HF Dataset store, the " |
| f"default) or 'pg' (Postgres, needs DATABASE_URL). Refusing to guess: a typo that " |
| f"silently served the other store is how data ends up in two places.") |
| return raw |
|
|
|
|
| def active(): |
| """The backend MODULE. Resolved per call, so a test can flip the env var without a reimport.""" |
| if name() == 'pg': |
| import core.store_pg as _pg |
| return _pg |
| return _hf |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| def available(): |
| return active().available() |
|
|
|
|
| def get(name_, fresh=False): |
| return active().get(name_, fresh=fresh) |
|
|
|
|
| def exists(name_): |
| return active().exists(name_) |
|
|
|
|
| def put(name_, data): |
| return active().put(name_, data) |
|
|
|
|
| def update(name_, fn, flush='sync'): |
| return active().update(name_, fn, flush=flush) |
|
|
|
|
| def upload_bytes(path_in_repo, data, message=None): |
| return active().upload_bytes(path_in_repo, data, message=message) |
|
|
|
|
| def download_bytes(path_in_repo): |
| return active().download_bytes(path_in_repo) |
|
|
|
|
| def delete_path(path_in_repo): |
| return active().delete_path(path_in_repo) |
|
|
|
|
| def flush(name_=None, timeout=30.0): |
| return active().flush(name_, timeout=timeout) |
|
|
|
|
| def get_projection(name_, drop=()): |
| """A read of `name_` with `drop`'s keys removed from every top-level value (W32-T01 / D-185). |
| |
| The saving is the COPY, not the wire: on tenant #0's `user_tables` a whole read deep-copies |
| 28.6 MB / 81,192 rows, and **99.89% of those bytes are `rows`** that no nav render or |
| permission check reads. Measured warm on that document: 1,823 ms whole vs 2.2 ms projected. |
| |
| β Both backends implement it, which `verify_store_pg`'s IFACE forces β and this delegate is the |
| THIRD place that has to know, after `core/store.py` and `core/store_pg.py`. The gate found it |
| missing here the first time round; without it the selector falls through to nothing and a |
| `STORE_BACKEND` caller gets `AttributeError` instead of a projection. |
| """ |
| return active().get_projection(name_, drop=drop) |
|
|
|
|
| def revision(name_): |
| """The bucket's change token (wave 29, C6) β `{'rev', 'updated_at', 'token'}`. |
| |
| Both backends answer, and they answer with the same keys and the same units (`updated_at` is |
| epoch seconds on each). That parity is the reason the change-token endpoint needs no branch on |
| the backend, and it is asserted rather than assumed by `verify_store_pg`'s interface section. |
| """ |
| return active().revision(name_) |
|
|