File size: 5,483 Bytes
c14ceee
 
 
 
 
ea2c336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcdb685
c14ceee
 
dcdb685
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcdb685
 
609fb78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcdb685
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""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


# --- the TEN delegates. Written out rather than generated with setattr so that reading this file
# tells you the whole interface, and so a new store operation cannot be added to core/store.py
# without something here failing to match (verify_store_pg.py compares the signatures).
#
# ⭐ That guard did its job in wave 29: `revision()` landed in `core/store.py` and `core/store_pg.py`
# and the gate went red HERE, on the one file of the four that the change had missed. A generated
# delegate list would have covered it silently and left this file unable to describe itself.
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_)