Skip to content

Add DirectoryNameLock model and enhance name conflict handling#71

Open
Creeper19472 wants to merge 5 commits into
devfrom
fix-name-conflict
Open

Add DirectoryNameLock model and enhance name conflict handling#71
Creeper19472 wants to merge 5 commits into
devfrom
fix-name-conflict

Conversation

@Creeper19472

@Creeper19472 Creeper19472 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Introduce a locking-based mechanism to prevent directory/document name conflicts and align handlers/tests with the new conflict semantics.

Bug Fixes:

  • Harden cursor decoding to reject malformed base64 tokens instead of silently accepting them.

Enhancements:

  • Treat directory and document names as a shared namespace with normalized names and explicit validation for required titles/names.
  • Handle name conflicts through a structured NameConflict response object and centralized helpers, improving error reporting and reuse across handlers.
  • Clean up stale inactive documents only when caller has sufficient permissions, while ensuring they no longer mask name conflicts.

Tests:

  • Add comprehensive tests for directory/document name conflict scenarios, including rename, move, restore, inactive documents, and name-lock lifetime.

Chores:

  • Add DirectoryNameLock model, Alembic migration, and wiring into SQLAlchemy models to support per-parent name reservations.

@sourcery-ai

sourcery-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a DirectoryNameLock model and name-locking workflow so document and directory operations share a single normalized name namespace per parent, centralizes name conflict detection into reusable helpers returning structured NameConflict responses, and updates handlers and tests to enforce non-empty names and correct conflict handling for create, rename, move, and restore operations.

Sequence diagram for name-locked document creation

sequenceDiagram
    actor Client
    participant RequestCreateDocumentHandler as CreateDocumentHandler
    participant Session
    participant NameConflicts as name_conflicts
    participant DirectoryNameLock

    Client->>CreateDocumentHandler: handle
    CreateDocumentHandler->>CreateDocumentHandler: require_object_name(title)
    alt invalid title
        CreateDocumentHandler-->>Client: conclude_request(400, ..., DOCUMENT_TITLE_REQUIRED)
    else valid title
        CreateDocumentHandler->>Session: begin
        CreateDocumentHandler->>name_conflicts: reserve_name_for_write(session, folder_id, title)
        alt lock or name conflict
            name_conflicts-->>CreateDocumentHandler: conflict(NameConflict)
            CreateDocumentHandler-->>Client: conclude_request(conflict.code, conflict.public_data(), conflict.message)
        else no conflict
            name_conflicts-->>CreateDocumentHandler: (name_lock, None)
            CreateDocumentHandler->>Session: create Document
            CreateDocumentHandler->>name_conflicts: release_name_write_lock(session, name_lock)
            Session-->>CreateDocumentHandler: commit
            CreateDocumentHandler-->>Client: conclude_request(200, ..., SUCCESS)
        end
    end
Loading

Sequence diagram for directory rename with shared name namespace

sequenceDiagram
    actor Client
    participant RequestRenameDirectoryHandler as RenameDirectoryHandler
    participant Session
    participant NameConflicts as name_conflicts

    Client->>RenameDirectoryHandler: handle
    RenameDirectoryHandler->>RenameDirectoryHandler: require_object_name(new_name)
    alt invalid name
        RenameDirectoryHandler-->>Client: conclude_request(400, ..., DIRECTORY_NAME_REQUIRED)
    else valid name
        RenameDirectoryHandler->>Session: load Folder
        RenameDirectoryHandler->>name_conflicts: reserve_name_for_write(session, parent_id, new_name)
        alt conflict with Folder or Document
            name_conflicts-->>RenameDirectoryHandler: conflict(NameConflict)
            RenameDirectoryHandler-->>Client: conclude_request(conflict.code, conflict.public_data(), conflict.message)
        else no conflict
            name_conflicts-->>RenameDirectoryHandler: (name_lock, None)
            RenameDirectoryHandler->>Session: update Folder.name
            RenameDirectoryHandler->>name_conflicts: release_name_write_lock(session, name_lock)
            Session-->>RenameDirectoryHandler: commit
            RenameDirectoryHandler-->>Client: conclude_request(200, ..., DIRECTORY_RENAMED)
        end
    end
Loading

Entity relationship diagram for DirectoryNameLock and shared name namespace

erDiagram
    FOLDER {
        string id
        string parent_id
        string name
    }
    DOCUMENT {
        string id
        string folder_id
        string title
    }
    DIRECTORYNAMELOCK {
        string parent_id
        string name
    }

    FOLDER ||--o{ DOCUMENT : contains
    FOLDER ||--o{ FOLDER : contains

    DIRECTORYNAMELOCK ||--o| FOLDER : parent_id_ref
    DOCUMENT }o--|| DIRECTORYNAMELOCK : shares_name_namespace
Loading

File-Level Changes

Change Details Files
Introduce DirectoryNameLock model and Alembic migration to support per-parent name locks for documents/directories.
  • Add DirectoryNameLock SQLAlchemy model with (parent_id, name) composite primary key.
  • Register DirectoryNameLock in ORM models all and metadata exports.
  • Add Alembic migration creating the directory_name_locks table with appropriate primary key.
  • Add unit test verifying DirectoryNameLock lifecycle using an in-memory SQLite database.
src/include/database/models/documents.py
src/include/database/models/__init__.py
src/alembic/versions/2edcba9e076a_directory_name_locks.py
tests/test_directories.py
Refactor name conflict logic into reusable helpers that normalize names, enforce required names, and coordinate with DirectoryNameLock.
  • Add NameConflict dataclass for structured conflict responses with helper to filter internal fields.
  • Introduce normalize_object_name and normalize_parent_id helpers for consistent whitespace trimming and ROOT_DIRECTORY_ID fallback.
  • Add require_object_name to enforce non-empty names and return NameConflict on validation failure.
  • Replace handle_name_duplicate with find_name_conflict and reserve_name_for_write, which integrate DirectoryNameLock acquisition and release and handle inactive documents and shared directory/document namespaces.
  • Implement acquire_name_write_lock and release_name_write_lock using the new DirectoryNameLock model and IntegrityError handling to report duplicate-name conflicts.
src/include/domains/documents/commands/name_conflicts.py
Update directory handlers to use new name validation and locking/NameConflict APIs for create, rename, move, and restore operations.
  • Wire RequestCreateDirectoryHandler to require_object_name and reserve_name_for_write, including exists_ok handling based on structured NameConflict data and ensuring locks are released before commit.
  • Update directory rename, move, and restore handlers to call reserve_name_for_write instead of inline queries, return conflict.code/message, and release locks upon success.
  • Use normalize_parent_id and NameConflict.public_data to standardize error payloads and avoid leaking internal entity objects in responses.
src/include/domains/documents/handlers/directories.py
Update document handlers to use new name validation and locking/NameConflict APIs for create, rename, move, and restore operations, and ensure shared directory/document namespaces.
  • Change RequestCreateDocumentHandler to validate title via require_object_name, then use reserve_name_for_write and release_name_write_lock when creating documents.
  • Update document rename handler to enforce non-empty new_title and use reserve_name_for_write/NameConflict for conflict reporting and metrics.
  • Update move and restore handlers to replace ad-hoc conflict queries with reserve_name_for_write, returning standardized conflict payloads and releasing locks on success.
  • Ensure restore operations respect existing name conflicts but ignore self-conflicts where the restored entity is the same logical object.
src/include/domains/documents/handlers/documents.py
Extend tests and messages to cover new conflict semantics, inactive document behavior, and cursor validation.
  • Add TestDocumentDirectoryNameConflicts test suite covering shared namespace between documents and directories, inactive document masking behavior, and conflict handling for rename/move/restore and self-restore scenarios.
  • Add helper methods in tests for creating and deleting inactive documents directly in the DB to simulate edge cases.
  • Add DIRECTORY_NAME_REQUIRED message constant and use it where directory names are validated.
  • Harden pagination cursor decoding by verifying that re-encoding the decoded bytes produces the exact original token, raising CursorError on mismatch.
tests/test_directories.py
src/include/messages.py
src/include/domains/pagination.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The tests in TestDocumentDirectoryNameConflicts directly open src/app.db with sqlite3, hardcoding the path and bypassing the existing SQLAlchemy/session setup; consider routing these inserts/deletes through the same DB config/fixtures used by the rest of the test suite to avoid coupling to a specific file path and schema layout.
  • The new name validation logic introduces several ad‑hoc "Directory name is required" and DOCUMENT_TITLE_REQUIRED responses; it may be worth centralizing this validation (e.g., a small helper around normalize_object_name) so the behavior and error messages for empty/whitespace-only names stay consistent across create/rename/move/restore handlers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The tests in `TestDocumentDirectoryNameConflicts` directly open `src/app.db` with `sqlite3`, hardcoding the path and bypassing the existing SQLAlchemy/session setup; consider routing these inserts/deletes through the same DB config/fixtures used by the rest of the test suite to avoid coupling to a specific file path and schema layout.
- The new name validation logic introduces several ad‑hoc `"Directory name is required"` and `DOCUMENT_TITLE_REQUIRED` responses; it may be worth centralizing this validation (e.g., a small helper around `normalize_object_name`) so the behavior and error messages for empty/whitespace-only names stay consistent across create/rename/move/restore handlers.

## Individual Comments

### Comment 1
<location path="src/include/domains/documents/commands/name_conflicts.py" line_range="91-57" />
<code_context>
-        .filter_by(folder_id=folder_id, title=title)
-        .first()
-    )
     existing_folder = (
         session.query(Folder)
         .with_for_update()
         .filter_by(parent_id=folder_id, name=title)
         .first()
     )
+    existing_docs = (
</code_context>
<issue_to_address>
**issue (bug_risk):** find_name_conflict incorrectly treats non-active / self entities as conflicts, which breaks restore flows and can delete the object being restored

Reusing `reserve_name_for_write` / `find_name_conflict` in the restore handlers changes semantics:

- `existing_folder` no longer filters by `status`, so the folder being restored is treated as a conflicting sibling and can incorrectly yield `DIRECTORY_NAME_DUPLICATE`.
- `existing_docs` also does not filter by status; the non-active doc being restored is included in `inactive_docs`, and the cleanup loop can call `delete_all_revisions` and `session.delete(existing_doc)` on it.

To match the previous behavior and avoid self-conflicts/deleting the target of the restore, `find_name_conflict` should either:
- Filter to `status == EntityStatus.OK` (and potentially only active entities), or
- Take the ID of the entity being restored and explicitly exclude it from conflict queries.

Without this, restore operations can behave incorrectly compared to the prior status-filtered checks.
</issue_to_address>

### Comment 2
<location path="src/include/domains/documents/handlers/documents.py" line_range="304" />
<code_context>
     def handle(self, handler: ConnectionHandler):
         folder_id = handler.data.get("folder_id") or ROOT_DIRECTORY_ID
-        title = (handler.data.get("title") or "").strip()
+        title = normalize_object_name(handler.data.get("title") or "")
         access_rules = handler.data.get("access_rules") or {}
         inherit_parent = handler.data.get("inherit_parent", True)
</code_context>
<issue_to_address>
**suggestion:** Create document still allows empty titles while other handlers now reject empty names/titles

Here `title` is normalized but not validated, so documents with empty titles can still be created while other operations now reject empty names/titles. For consistency and to avoid edge cases with blank titles (especially given centralized name locking/conflict detection), please add an `if not title` check here and return a 400 with `DOCUMENT_TITLE_REQUIRED` (or equivalent).

Suggested implementation:

```python
        folder_id = handler.data.get("folder_id") or ROOT_DIRECTORY_ID
        title = normalize_object_name(handler.data.get("title") or "")
        if not title:
            handler.write_error(
                status_code=400,
                message=smsg.DOCUMENT_TITLE_REQUIRED,
            )
            return

        access_rules = handler.data.get("access_rules") or {}
        inherit_parent = handler.data.get("inherit_parent", True)

```

1. Ensure `Messages` (`smsg`) contains `DOCUMENT_TITLE_REQUIRED`; if the constant name differs, replace `smsg.DOCUMENT_TITLE_REQUIRED` with the correct one used elsewhere for document title validation.
2. If your `ConnectionHandler` uses a different API for emitting HTTP errors (e.g. `handler.error(...)`, `handler.write_response(...)`, or raising a specific exception), adjust the `handler.write_error(...)` call to match the existing error-handling pattern used by sibling handlers that validate names/titles.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/include/domains/documents/commands/name_conflicts.py
Comment thread src/include/domains/documents/handlers/documents.py Outdated
…nction and updating validation in directory and document operations
@Creeper19472

Copy link
Copy Markdown
Collaborator Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The new name locking flow only calls release_name_write_lock on the happy path; in several handlers (e.g., document/directory create, rename, move, restore) any early return or exception after acquiring a lock will leave a persistent row in directory_name_locks, so it would be safer to wrap the critical sections in a try/finally and always release the lock.
  • In reserve_name_for_write, when find_name_conflict returns a conflict, the previously inserted DirectoryNameLock isn’t released, which means the lock row will linger even though the caller receives a conflict; consider deleting the lock before returning the conflict to keep the lock table from accumulating unnecessary rows.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new name locking flow only calls `release_name_write_lock` on the happy path; in several handlers (e.g., document/directory create, rename, move, restore) any early return or exception after acquiring a lock will leave a persistent row in `directory_name_locks`, so it would be safer to wrap the critical sections in a `try/finally` and always release the lock.
- In `reserve_name_for_write`, when `find_name_conflict` returns a conflict, the previously inserted `DirectoryNameLock` isn’t released, which means the lock row will linger even though the caller receives a conflict; consider deleting the lock before returning the conflict to keep the lock table from accumulating unnecessary rows.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant