mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
bf14c1168a
Phase 1 of PLAN-2357. Durable record of "this item was copied/moved from workspace A to workspace B", backing the forward redirect (TASK-2359) and the destination's back-pointer. Implements DR-2 / DR-2a. Paired, dual-dialect, forward-only migrations (migrations/077 + pgmigrations/055). archived_source distinguishes a move from a plain copy (INTEGER on SQLite, BOOLEAN on Postgres, written through dialect.BoolToInt). source_seq is a NULLABLE per-source move ordinal that exists solely so two moves inside the same second are orderable — created_at is second-precision RFC3339, so archive -> restore -> move again would otherwise resolve to an arbitrary destination. Partial index (source_item_id, source_seq DESC) WHERE archived_source, deliberately NOT unique: restore-then-move-again legitimately repeats. The back direction IS uniquely indexed — a destination item is created by exactly one copy, in the same transaction that writes its provenance row, so a duplicate there would silently change which source the back-pointer names. Cascade is asymmetric on purpose, inverting item_collection_moves: the archived source is precisely the row whose pointer must survive, so source_item_id carries no FK at all; target_item_id cascades, because a pointer at a vanished destination is worse than no pointer. Store accessors: a tx-taking insert helper (no self-committing variant — the row must land in the copy transaction), a forward lookup returning a SET newest-first, and a back lookup. The insert rejects an archived row with no seq and a copy row with one, so DR-2a's ordering invariant is enforced at the write boundary rather than assumed. NULL ordering is normalized with COALESCE because SQLite and Postgres disagree on DESC NULL placement. Also wires workspace purge, which the two-workspace shape requires: both workspace columns are RESTRICT references, so a purge clearing only one direction would fail outright when the purged workspace sits on the other end. Tests cover insert-in-tx, forward lookup with multiple destinations ordered newest-first and scoped to one source, back lookup, rollback leaving no row, and both DR-2a criteria. The ordering and scoping tests use fixed row IDs whose lexical order contradicts the expected answer, so deleting the ordering term or the WHERE clause under test fails them on every run rather than half the time; verified by mutating the production query. Claude-Session: https://claude.ai/code/session_01E2fRi12n8rARczvdEa2LYT
44 lines
1.9 KiB
Go
44 lines
1.9 KiB
Go
package models
|
|
|
|
// ItemWorkspaceMove is one durable record of an item being copied — or moved,
|
|
// which is a copy plus an archive of the source — from one workspace to
|
|
// another. Written in the SAME transaction as the copy, so the provenance can
|
|
// never disagree with the data (PLAN-2357 DR-2).
|
|
//
|
|
// It backs exactly two lookups:
|
|
//
|
|
// - forward, by SourceItemID: "where did this item go?" One source can be
|
|
// copied into several workspaces, so the forward lookup is a SET.
|
|
// - back, by TargetItemID: "where did this item come from?" A destination
|
|
// item is created by exactly one copy, so this is at most one row.
|
|
//
|
|
// ArchivedSource distinguishes a move from a plain copy, and only a move
|
|
// produces a "moved to" pointer on the archived source (DR-2a). A source
|
|
// copied three times and then moved has four rows, and only the archived one
|
|
// says where it *went*.
|
|
type ItemWorkspaceMove struct {
|
|
ID string `json:"id"`
|
|
SourceWorkspaceID string `json:"source_workspace_id"`
|
|
SourceItemID string `json:"source_item_id"`
|
|
TargetWorkspaceID string `json:"target_workspace_id"`
|
|
TargetItemID string `json:"target_item_id"`
|
|
|
|
// ArchivedSource is true when the source was archived as part of the
|
|
// operation (a move) and false for a plain copy.
|
|
ArchivedSource bool `json:"archived_source"`
|
|
|
|
// SourceSeq is the workspace-scoped `seq` the source workspace assigned
|
|
// when it archived the source. It exists only to order one source's
|
|
// moves deterministically: CreatedAt is second-precision RFC3339, so two
|
|
// moves inside the same second tie and the "moved to" lookup could
|
|
// otherwise return an arbitrary destination.
|
|
//
|
|
// nil for plain copies, which never archive. Since the "moved to" lookup
|
|
// reads only ArchivedSource rows, nil values never participate in the
|
|
// ordering.
|
|
SourceSeq *int64 `json:"source_seq,omitempty"`
|
|
|
|
CreatedBy string `json:"created_by"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|