Files
xarmian 5cb2297f67 fix(store): export soft-deleted collections so their live items survive a round trip (BUG-2884) (#1255)
* test(store): BUG-2884 regression tests — items under a soft-deleted collection

Measured against 5c4fa229 + the CollectionExport.DeletedAt field alone (no
behaviour change): export shape and both round-trip tests fail for their
designed reasons; the routing guard and the legacy-bundle compat lock pass.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR

* fix(store): export soft-deleted collections so their live items survive a round trip (BUG-2884)

DeleteCollection soft-deletes only the collection row; its items keep
deleted_at IS NULL and stay reachable by ref, by id, and through search, since
no item-bearing read joins collection liveness. Export filtered collections on
deleted_at IS NULL but items only on the item's own mark, so the bundle named a
collection it did not carry and ImportWorkspace dropped those items on its
orphan gate with no log line. pad db migrate is ExportWorkspace piped into
ImportWorkspace, so this lost live rows on a SQLite to Postgres migration.

Filtering items to live collections instead was rejected on measurement: it
would make the migration DELETE reachable rows.

Also fixes a second defect the regression tests surfaced: an orphaned item with
a comment aborted the ENTIRE import on a foreign-key violation. The comment
loop guarded on itemMap[...] == "", which cannot fire for an item the bundle
contains, since itemMap is written before the orphan skip for parent
resolution. It now checks insertedItems, as the reminder loop already does.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR

* test(store): discriminate the two archived-collection skips (BUG-2884)

The routing test asserted on ListTraitedCollections, which filters deleted rows
anyway, so it would have passed with both skips reverted. It now captures slog
and fails on a duplicate-declaration warning, and a new test locks that
inference does not stamp canonical traits onto an archived collection.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR

* fix(store): refuse an orphan's dependents before the database sees them (BUG-2884)

Found by the Postgres gate: the links and versions loops answered an orphaned
item's foreign-key failure with continue, which recovers on SQLite and cannot
on Postgres, where a failed statement aborts the transaction and COMMIT reports
"commit unexpectedly resulted in rollback". A single skippable row failed the
entire workspace restore.

Both loops now check insertedItems, as the comment and reminder loops do, so
the row is refused before the database sees it. The reminder loop's own note
that skip-on-error survives an FK failure is now qualified with the driver it
is true on.

The regression test is one subtest per dependent kind, with the bundle stripped
to exactly that kind: creating and editing an item mints rows of its own, so an
un-isolated fixture failed at whichever loop ran first and discriminated none
of them.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR

* fix(store): resolve an import's parent_id through insertedItems, and dedupe links (BUG-2884)

Both from codex round 3, both verified in the code before accepting.

Parent resolution was the half my own sweep of the itemMap consumers missed:
the guard I added to the second pass protects the item being updated, not the
parent VALUE it writes. A live child of an archived-collection parent wrote a
nonexistent id into items.parent_id — fatal on the first pass on both drivers,
since SQLite runs with foreign_keys ON. Both sites now go through one helper
that drops the edge, which is the only thing a bundle missing the parent can
express.

Links are deduped before the write rather than after the error. The source's
UNIQUE constraint bounds what pad wrote, not what arrives, and on Postgres a
unique violation aborts the transaction, so the continue could never ignore it.

Also swept the itemMap note that still said links and versions survive by
skipping on error — true on SQLite only, and it is the sentence that would talk
the next reader out of the fix.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR
2026-09-05 13:20:23 -04:00
..
2026-03-26 01:52:36 +00:00