🐾 fix(api): cascade plain library delete; case-insensitive name conflict #9

Merged
r merged 1 commits from fix/library-api-delete-and-name-case into main 2026-08-03 16:45:48 +00:00
Owner

Follow-up to mnemosyne#7, addressing the two gaps confirmed while answering the Spelunker team's provenance handover.

1. Plain API delete now cascades

DELETE /library/api/libraries/{uid}/ called bare lib.delete() — the only delete path not using the shared cascade — orphaning Collections/Items/Chunks/Images and skipping orphan-Concept GC. It now delegates to delete_library_cascade() like the HTML delete view and the workspace DELETE endpoint, and logs uid/name/item count/orphans like they do. (This — not global-vs-workspace scoping — was the actual cause of "deletes don't cascade"; the cascade is keyed on uid and always worked for global libraries via the other two paths.)

2. Name conflicts are case-insensitive at create time

The Neo4j unique index is case-sensitive, so "amazon connect" could silently coexist with "Amazon Connect" — and clients (Spelunker, humans) treat names case-insensitively, so near-duplicates confuse every name matcher. Both create endpoints (plain + workspace) now pre-check via a new find_library_by_name_ci() and 409 name_conflict, reporting the existing spelling. Implementation notes:

  • Parameterised Cypher toLower() comparison, deliberately not neomodel's iexact — that operator embeds the value in a regex, which breaks on names containing metacharacters (e.g. "C++ Notes").
  • The plain create also gains a UniqueProperty catch for the pre-check/save race, mirroring the workspace path's existing backstop.
  • Existing exact-name behaviour is unchanged; only case-variant creates newly 409.

Pre-deploy check for existing case-variants (should return nothing; if it doesn't, rename before deploy so 409s reference the intended library):

MATCH (a:Library), (b:Library)
WHERE a.uid < b.uid AND toLower(a.name) = toLower(b.name)
RETURN a.name, a.uid, b.name, b.uid

Testing

40 tests green across test_managed_by (incl. new case-variant + save-race cases), new test_library_api (cascade called; 404 skips cascade), test_workspaces_api, test_views. Full library + mcp_server suite matches the known main baseline exactly (8F/1E pre-existing in test_search_api/test_tasks). Live Cypher behaviour of find_library_by_name_ci is on the manual e2e checklist per repo test policy.

Not touched (flagged only): the API collection and item DELETE endpoints also call bare .delete() and may deserve the same cascade treatment — separate decision.

🤖 Generated with Claude Code

Follow-up to mnemosyne#7, addressing the two gaps confirmed while answering the Spelunker team's provenance handover. ## 1. Plain API delete now cascades `DELETE /library/api/libraries/{uid}/` called bare `lib.delete()` — the only delete path not using the shared cascade — orphaning Collections/Items/Chunks/Images and skipping orphan-Concept GC. It now delegates to `delete_library_cascade()` like the HTML delete view and the workspace DELETE endpoint, and logs uid/name/item count/orphans like they do. (This — not global-vs-workspace scoping — was the actual cause of "deletes don't cascade"; the cascade is keyed on `uid` and always worked for global libraries via the other two paths.) ## 2. Name conflicts are case-insensitive at create time The Neo4j unique index is case-sensitive, so `"amazon connect"` could silently coexist with `"Amazon Connect"` — and clients (Spelunker, humans) treat names case-insensitively, so near-duplicates confuse every name matcher. Both create endpoints (plain + workspace) now pre-check via a new `find_library_by_name_ci()` and 409 `name_conflict`, reporting the **existing spelling**. Implementation notes: - Parameterised Cypher `toLower()` comparison, deliberately **not** neomodel's `iexact` — that operator embeds the value in a regex, which breaks on names containing metacharacters (e.g. "C++ Notes"). - The plain create also gains a `UniqueProperty` catch for the pre-check/save race, mirroring the workspace path's existing backstop. - Existing exact-name behaviour is unchanged; only case-variant creates newly 409. **Pre-deploy check for existing case-variants** (should return nothing; if it doesn't, rename before deploy so 409s reference the intended library): ```cypher MATCH (a:Library), (b:Library) WHERE a.uid < b.uid AND toLower(a.name) = toLower(b.name) RETURN a.name, a.uid, b.name, b.uid ``` ## Testing 40 tests green across `test_managed_by` (incl. new case-variant + save-race cases), new `test_library_api` (cascade called; 404 skips cascade), `test_workspaces_api`, `test_views`. Full `library` + `mcp_server` suite matches the known main baseline exactly (8F/1E pre-existing in `test_search_api`/`test_tasks`). Live Cypher behaviour of `find_library_by_name_ci` is on the manual e2e checklist per repo test policy. Not touched (flagged only): the API **collection** and **item** DELETE endpoints also call bare `.delete()` and may deserve the same cascade treatment — separate decision. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
r added 1 commit 2026-08-03 16:42:11 +00:00
DELETE /library/api/libraries/{uid}/ called bare lib.delete(), orphaning
Collections/Items/Chunks/Images and skipping Concept GC — the only delete
path not using the shared cascade. It now delegates to
delete_library_cascade like the HTML and workspace delete paths.

Name-conflict checks on both create endpoints are now case-insensitive
via find_library_by_name_ci (parameterised Cypher toLower comparison —
not neomodel iexact, which embeds the value in a regex and breaks on
names like "C++ Notes"). The Neo4j unique index is case-sensitive, so
"amazon connect" previously coexisted silently with "Amazon Connect",
confusing every name-matching client (Spelunker matches names
case-insensitively). The 409 reports the existing spelling. The plain
create also gains a UniqueProperty catch for the pre-check/save race,
mirroring the workspace path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
r merged commit c040e6f3dd into main 2026-08-03 16:45:48 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: r/mnemosyne#9