mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 11:26:34 +00:00
c6ebe5a3e3
Three accessors read a full collection row and each carried a verbatim copy of the same 15-column projection and scan/hydration block: GetCollection, GetCollectionAnyState, and the transactional getCollectionInWorkspaceTx used by the cross-workspace copy. A column added to the model had to be added in three places, and the copy path drifted silently if only GetCollection was updated. Extract collectionColumns plus scanCollectionRow, parameterized over rowQueryer (the uniqueSlugQ / validateAssignmentScopeQ pattern from TASK-2362) so the same read runs against *sql.DB or inside a caller's *sql.Tx. Each accessor's full statement is assembled from constants, so the WHERE predicate is the only per-caller difference, the SQL is built at compile time rather than per call, and no runtime-assembled fragment is ever handed to s.q. Preserved deliberately: s.q placeholder rewriting (applied once, inside the helper, so no call site can skip it); nil-on-sql.ErrNoRows at every accessor -- the helper returns real errors unwrapped so each keeps its own distinct prefix; the transactional lookup stays workspace-scoped and active-only, which is the security boundary that makes a foreign collection a not-found rather than a cross-workspace write. lockCollectionRows is untouched: its SELECT id ... FOR UPDATE is a locking primitive that duplicates nothing, and its sorted acquisition is load-bearing. ListCollections is deliberately left out and documented as such: it is an aggregate multi-row query with aliased columns, a trailing COUNT and no deleted_at, so sharing a projection would need a second count-aware scanner and would reshape a hot query for no correctness gain. TestCollectionAccessorsShareOneHydration pins all three to one hydration. Every scanned column except deleted_at is asserted against a literal, distinct value rather than against another accessor's output, since cross-accessor equality alone cannot catch a mutation in the shared projection; created_at and updated_at are set to different instants so transposing them fails, and deleted_at is pinned by the soft-delete branch, the only state in which it is non-nil. Verified by mutation: a transposed slug/prefix projection, a transposed created_at/updated_at projection, a dropped workspace scope on the transactional read, a flattened deleted-state predicate, and a miss turned into an error each fail the test.