You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c3c72a4
Browse filesBrowse the repository at this point in the historyBrowse files
fix(objectql): hydrate a tombstoned sys_file that still has a live holder (#11844)
* fix(objectql): hydrate a tombstoned sys_file that still has a live holder
#10246 stopped the two download endpoints treating a `sys_file` tombstone as
the last word: they ask the reap guard's own `findFileHolder` and serve the row
for as long as something still holds it. Record file-field hydration was not
part of that ruling and kept the older `status === 'committed'` rule, so one
`sys_file` row answered 200 at `GET /api/v1/storage/files/:id` and a bare id
inside a record payload — which UI and export render as "no attachment".
The predicate is not re-derived in the engine. "Still held" has ONE definition,
`findFileHolder`, a union of `sys_attachment` join rows and the `ref_*`
ownership columns, and it lives in a package objectql cannot depend on. So the
engine declares `registerHeldFileResolver` and the storage plugin fills it —
the same handover `resolveFileHolder` makes to the download routes.
Batched, because hydration runs over many rows per read: `findHeldFiles` takes
the whole tombstoned set, settles every row whose ownership columns answer it
with zero queries, and issues at most ONE `$in` read for the rest. A read with
no tombstone — every ordinary read — costs nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
* test(service-storage): resolve driver-memory from source in vitest
`check:test-source-alias` — the agreement pin drives a REAL driver, and one
read from dist/ would make it a verdict about build state rather than about
the source beside it. objectql stays unaliased on purpose: it is a registered
KNOWN_UNALIASED_TEST_IMPORTS entry, which is what lets an ablation of engine
source, rebuilt, actually change what these tests observe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
* test(service-storage): keep the new pin inside the type-check-debt ratchet
`check:type-check-debt --re-measure` counts raw tsc errors per package
against a shrink-only ledger; the new file added 6 (unused type import, the
required packageId argument to registry.registerObject, and two untyped mocks
whose calls[0][1] read as an empty tuple). Fixed rather than ledgered — the
package re-measures at its frozen 51.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
* docs(permissions): the tombstone question is asked by both readers, not just downloads
The `sys_file` bullet scoped its "same question" claim to the download
endpoints. That scoping was accurate only because record file-field hydration
was NOT asking it — the divergence this branch fixes, written down as if it
were the design, so a reader came away believing record reads do not
participate.
Widened to name both readers and to say why one predicate rather than two is
what makes them agree. The boundary clause is kept and extended rather than
softened: a file with no holder left still answers FILE_NOT_FOUND (404) and
still keeps its bare id in a record payload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
* test(service-storage): migrate the agreement pin to sqlite :memory:
`check:driver-memory-census` was red on this branch: the pin bound
@objectstack/driver-memory by import and by devDependency, and the ledger
covers neither.
Migrated rather than ledgered. The two `ruled-permanent` entries exist because
the in-memory driver's DISTINCT SEMANTICS are the subject — the schemaless arm
of #4271, and the engine-fallback arm of #6468 where `supports = {}` puts the
counter in the engine. This pin's subject is neither: it needs shared state
across two read surfaces so the download verdict and the hydration verdict come
from the same rows, and sqlite `:memory:` carries that identically. #5499 froze
investment in the driver and #5704 made sqlite the test backend, so the ruled
default already answers this — no ruling required, and no ledger row added by
hand (the census still reports 2 ruled consumers, not 3).
Checked before switching: every seeded key is declared, so there is no
undeclared-field write for the SQL family to refuse, and nothing in the fixture
is schemaless or leans on a driver-advertised capability.
The vitest alias moves with the import; @objectstack/objectql stays
ledgered-unaliased so an ablation of engine source, rebuilt, still changes what
these tests observe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
* chore(changeset): @objectstack/objectql is a minor — the public surface widens
Contract review rejected the changeset for disagreeing with the PR's own
clause-② declaration. Verified at this head rather than relayed: the type
`HeldFileResolver` is re-exported from the package entry
(packages/objectql/src/index.ts), and `registerHeldFileResolver` is a public
method on `ObjectQL`, which that entry exports too. Both are ADDITIVE — nothing
removed, no existing caller's behaviour changed — which is a minor, not a patch.
`@objectstack/service-storage` stays `patch` on purpose and is untouched:
`findHeldFiles` is deliberately absent from that package's index, so its public
surface does not move (positive control: the symbol resolves in 3 files on this
branch, none of them the entry).
Refs #11427.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
---------
Co-authored-by: Claude <noreply@anthropic.com>
Record file-field hydration now answers the same question about a `sys_file` tombstone that the download path answers (#11427). `#10246` stopped `GET /api/v1/storage/files/:id` treating a tombstone (`status: 'deleted'` + `deleted_at`) as the last word — it asks the reap guard's own `findFileHolder` and serves the row for as long as something still holds it — but the record read kept the older `status === 'committed'` rule. One `sys_file` row therefore answered `200` at the download endpoint and a bare id inside a record payload, which UI and export render as "this record has no attachment".
7
+
8
+
The population is narrow and unchanged in every other respect: `claimFile` already un-tombstones a field file synchronously when a record re-points at it, and attachments-scope files are never reached by field hydration, so what this closes is the residual the reap guard's sweep-time re-verification names — hook races, direct-driver writes, and future trash restore. A tombstone nothing holds still hydrates as a bare id, and a `pending` upload is untouched.
9
+
10
+
The predicate is not re-derived in the engine. `ObjectQL` gains `registerHeldFileResolver` (type `HeldFileResolver`), which the storage plugin fills with `findHeldFiles` — the batched form of `findFileHolder`, asking the same union of `sys_attachment` join rows and the `ref_*` ownership columns. Batched because hydration runs over many rows per read: a read with no tombstone costs nothing, and the residual case costs one extra query for the whole read rather than one per file. Engines with no storage plugin keep tombstones un-hydrated exactly as before.
0 commit comments