Skip to content

fix(workspaces): exclude indexed files from orphan check on workspace delete - #285

Merged
EnjoyBacon7 merged 4 commits into
devfrom
fix/workspace-delete-orphan-check
Mar 19, 2026
Merged

fix(workspaces): exclude indexed files from orphan check on workspace delete#285
EnjoyBacon7 merged 4 commits into
devfrom
fix/workspace-delete-orphan-check

Conversation

@EnjoyBacon7

@EnjoyBacon7 EnjoyBacon7 commented Mar 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #275.

delete_workspace() determined which files to delete from Milvus by checking whether a file appeared in any other workspace. It did not consult the files table, so a file that was independently indexed into the partition and then added to a workspace would be permanently deleted from Milvus when that workspace was removed — irreversible data loss.

Root cause

# before — only checks other workspaces
subq = select(WorkspaceFile.file_id).where(WorkspaceFile.workspace_id != workspace_id).subquery()

Fix

Add a second exclusion subquery against files so that a file is only considered orphaned if it is both absent from every other workspace and not present in the files table (i.e. was never independently indexed).

subq_other_ws = select(WorkspaceFile.file_id).where(WorkspaceFile.workspace_id != workspace_id).subquery()
subq_indexed  = select(File.file_id).subquery()
# file must pass both exclusions to be orphaned

File was already imported in utils.py — no new imports needed.

Tests

5 new unit tests in test_delete_workspace.py (SQLite in-memory, no Ray):

  • independently-indexed file is not returned as orphan
  • workspace-only file is returned as orphan
  • file shared between two workspaces is not returned as orphan
  • mixed scenario: only the true orphan is returned
  • empty workspace returns no orphans

227 passed, 3 skipped (pre-existing)

Summary by CodeRabbit

  • Bug Fixes

    • Improved workspace deletion to more accurately identify truly orphaned files, preserving files shared across workspaces or independently indexed.
    • Deletion now returns a precise list of orphaned file IDs and safely removes the workspace and its associations.
  • Tests

    • Added unit tests covering orphan detection for various scenarios (shared files, indexed files, empty workspaces, and mixed cases).

… delete

delete_workspace() determined orphaned files by checking whether a file
appeared in any other workspace. It did not check the files table, so
any file that was independently indexed into the partition and then added
to a workspace would be permanently deleted from Milvus when that
workspace was removed.

Fix: add a second exclusion subquery against the files table so that
only files that are truly workspace-only (not independently indexed and
not in any other workspace) are returned as orphans for deletion.
Cover: independently-indexed file not returned as orphan, workspace-only
file returned as orphan, file shared across workspaces not returned,
mixed scenario where only the true orphan is returned, empty workspace.
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@EnjoyBacon7 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 57 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a57bf9e2-48cf-4f48-b3bb-720d7faafa91

📥 Commits

Reviewing files that changed from the base of the PR and between 5585cc8 and 3f8a24e.

📒 Files selected for processing (2)
  • openrag/components/indexer/vectordb/test_delete_workspace.py
  • openrag/components/indexer/vectordb/utils.py
📝 Walkthrough

Walkthrough

Updated delete_workspace logic to avoid deleting files that are independently indexed by excluding File entries in the same partition; added a comprehensive pytest module validating orphan detection behavior across multiple scenarios.

Changes

Cohort / File(s) Summary
Tests
openrag/components/indexer/vectordb/test_delete_workspace.py
Adds a new pytest module (in-memory SQLite) with helpers and tests covering orphan detection: independently-indexed files, workspace-only files, shared files, mixed cases, empty workspace, and partition-scoped indexing edge cases.
Core Logic
openrag/components/indexer/vectordb/utils.py
Modifies delete_workspace to load the workspace, derive partition_name, and compute orphaned file_ids by excluding IDs present in other workspaces and IDs present in the File table for the same partition. Deletes workspace rows and returns orphan IDs.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Utils as "delete_workspace()"
    participant DB as "Database (Workspace, WorkspaceFile, File)"
    Caller->>Utils: delete_workspace(workspace_id)
    Utils->>DB: SELECT Workspace WHERE id=workspace_id
    alt workspace exists
      Utils->>DB: SELECT WorkspaceFile.file_id WHERE workspace_id == target
      Utils->>DB: SELECT WorkspaceFile.file_id WHERE workspace_id != target (subq_other_ws)
      Utils->>DB: SELECT File.file_id WHERE partition_name == target.partition (subq_indexed)
      Utils->>Utils: compute orphan_ids = target_files - subq_other_ws - subq_indexed
      Utils->>DB: DELETE FROM WorkspaceFile WHERE workspace_id == target
      Utils->>DB: DELETE FROM Workspace WHERE id == target
      Utils->>DB: COMMIT
      Utils->>Caller: return orphan_ids
    else workspace missing
      Utils->>Caller: return []
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through rows of files and names,
Excluding friends with independent claims.
Ghosts that only lived in one small space,
Now found and freed without a trace.
Hooray — no indexed friend will lose their place! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: excluding indexed files from the orphan check during workspace deletion, which directly addresses the bug fix.
Linked Issues check ✅ Passed The code changes fully address the requirements from issue #275: the orphan detection now excludes files present in the files table AND filters by partition, preventing data loss.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the workspace deletion orphan detection logic and its comprehensive testing; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/workspace-delete-orphan-check
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
openrag/components/indexer/vectordb/test_delete_workspace.py (1)

142-154: Consider adding a cross-partition test case.

The mixed scenario test is comprehensive, but all tests use a single partition "p1". Consider adding a test case with files in multiple partitions to verify the subq_indexed behavior doesn't incorrectly exclude orphans due to same-named file_ids in different partitions.

💡 Optional: Cross-partition test case
def test_file_indexed_in_different_partition_does_not_affect_orphan(db):
    """A file_id indexed in partition p2 should not prevent orphan detection in p1."""
    db.add_file("p2", "file-x")  # Indexed in different partition
    db.add_workspace("ws1", "p1")
    db.add_file_to_workspace("ws1", "file-x")  # Same file_id, but not indexed in p1

    orphans = db.delete_workspace("ws1")

    # If file_ids are globally unique (UUIDs), this would return []
    # If file_ids can be reused across partitions, this should return ["file-x"]
    # Update assertion based on expected behavior
    assert orphans == []  # or ["file-x"] depending on design decision
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openrag/components/indexer/vectordb/test_delete_workspace.py` around lines
142 - 154, Add a cross-partition unit test to ensure delete_workspace's orphan
detection doesn't treat a file indexed in another partition as indexed in this
partition: create a new test (e.g.,
test_file_indexed_in_different_partition_does_not_affect_orphan) that calls
db.add_file("p2", "file-x"), db.add_workspace("ws1", "p1"),
db.add_file_to_workspace("ws1", "file-x"), then call db.delete_workspace("ws1")
and assert the expected orphans list based on your partitioning semantics; this
will exercise the same subquery/logic used by delete_workspace and subq_indexed
to verify partition-scoped indexing behavior.
openrag/components/indexer/vectordb/utils.py (1)

636-660: LGTM! The orphan detection logic correctly addresses the data loss issue.

The fix properly excludes both files in other workspaces and independently-indexed files from the orphan set, which aligns with the PR objectives for issue #275.

Minor optional simplification: The subqueries can be used directly without wrapping in another select():

♻️ Optional: Simplify subquery usage
         with self.Session() as session:
-            # Files present in at least one other workspace
-            subq_other_ws = select(WorkspaceFile.file_id).where(WorkspaceFile.workspace_id != workspace_id).subquery()
-            # Files that were independently indexed (present in the files table)
-            subq_indexed = select(File.file_id).subquery()
+            # Files present in at least one other workspace
+            subq_other_ws = select(WorkspaceFile.file_id).where(WorkspaceFile.workspace_id != workspace_id)
+            # Files that were independently indexed (present in the files table)
+            subq_indexed = select(File.file_id)
             result = session.execute(
                 select(WorkspaceFile.file_id)
                 .where(WorkspaceFile.workspace_id == workspace_id)
-                .where(WorkspaceFile.file_id.notin_(select(subq_other_ws.c.file_id)))
-                .where(WorkspaceFile.file_id.notin_(select(subq_indexed.c.file_id)))
+                .where(WorkspaceFile.file_id.notin_(subq_other_ws))
+                .where(WorkspaceFile.file_id.notin_(subq_indexed))
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openrag/components/indexer/vectordb/utils.py` around lines 636 - 660, The
current delete_workspace implementation builds subq_other_ws and subq_indexed
then wraps them in extra select(...) calls when using .notin_; simplify by
passing the subquery objects directly to the .notin_ predicates. Update the
.where clauses in delete_workspace that reference
WorkspaceFile.file_id.notin_(select(subq_other_ws.c.file_id)) and
WorkspaceFile.file_id.notin_(select(subq_indexed.c.file_id)) to use
WorkspaceFile.file_id.notin_(subq_other_ws) and
WorkspaceFile.file_id.notin_(subq_indexed) respectively, keeping the same
semantics and leaving other logic (session handling, deletion, commit)
untouched.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openrag/components/indexer/vectordb/utils.py`:
- Around line 647-648: subq_indexed currently selects all File.file_id without
partition filtering which breaks orphan detection across partitions; update the
logic to fetch the target Workspace's partition (from the Workspace row used in
this routine) and restrict subq_indexed to that partition (e.g. replace
select(File.file_id).subquery() with select(File.file_id).where(File.partition
== workspace_partition).subquery()), ensuring you reference the
Workspace.partition field and the unique constraint uix_file_id_partition when
applying the filter.

---

Nitpick comments:
In `@openrag/components/indexer/vectordb/test_delete_workspace.py`:
- Around line 142-154: Add a cross-partition unit test to ensure
delete_workspace's orphan detection doesn't treat a file indexed in another
partition as indexed in this partition: create a new test (e.g.,
test_file_indexed_in_different_partition_does_not_affect_orphan) that calls
db.add_file("p2", "file-x"), db.add_workspace("ws1", "p1"),
db.add_file_to_workspace("ws1", "file-x"), then call db.delete_workspace("ws1")
and assert the expected orphans list based on your partitioning semantics; this
will exercise the same subquery/logic used by delete_workspace and subq_indexed
to verify partition-scoped indexing behavior.

In `@openrag/components/indexer/vectordb/utils.py`:
- Around line 636-660: The current delete_workspace implementation builds
subq_other_ws and subq_indexed then wraps them in extra select(...) calls when
using .notin_; simplify by passing the subquery objects directly to the .notin_
predicates. Update the .where clauses in delete_workspace that reference
WorkspaceFile.file_id.notin_(select(subq_other_ws.c.file_id)) and
WorkspaceFile.file_id.notin_(select(subq_indexed.c.file_id)) to use
WorkspaceFile.file_id.notin_(subq_other_ws) and
WorkspaceFile.file_id.notin_(subq_indexed) respectively, keeping the same
semantics and leaving other logic (session handling, deletion, commit)
untouched.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 988c052a-220c-47ef-9cef-f827b27edf5f

📥 Commits

Reviewing files that changed from the base of the PR and between c1fe53a and 0edca41.

📒 Files selected for processing (2)
  • openrag/components/indexer/vectordb/test_delete_workspace.py
  • openrag/components/indexer/vectordb/utils.py

Comment thread openrag/components/indexer/vectordb/utils.py Outdated
…workspace

subq_indexed was selecting all file_ids from the files table without
filtering by partition. Since file_id is only unique per (file_id,
partition_name), the same filename in a different partition could
incorrectly prevent a file from being identified as orphaned.

Now fetches the workspace's partition_name and filters File.file_id
to that partition only.

Also adds a cross-partition regression test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
openrag/components/indexer/vectordb/test_delete_workspace.py (1)

166-171: Consider adding a test for non-existent workspace.

The helper's delete_workspace returns [] when the workspace doesn't exist (lines 80-81), mirroring the production early-return behavior. Adding explicit coverage would be a good-to-have:

def test_nonexistent_workspace_returns_empty_list(db):
    """Deleting a workspace that doesn't exist returns an empty list."""
    orphans = db.delete_workspace("nonexistent-ws")
    assert orphans == []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openrag/components/indexer/vectordb/test_delete_workspace.py` around lines
166 - 171, Add a new unit test in
openrag/components/indexer/vectordb/test_delete_workspace.py that calls the
helper method delete_workspace with a workspace id that was never created and
asserts it returns an empty list; name the test
test_nonexistent_workspace_returns_empty_list and simply call orphans =
db.delete_workspace("nonexistent-ws") followed by assert orphans == [] to cover
the early-return behavior in delete_workspace.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openrag/components/indexer/vectordb/test_delete_workspace.py`:
- Around line 166-171: Add a new unit test in
openrag/components/indexer/vectordb/test_delete_workspace.py that calls the
helper method delete_workspace with a workspace id that was never created and
asserts it returns an empty list; name the test
test_nonexistent_workspace_returns_empty_list and simply call orphans =
db.delete_workspace("nonexistent-ws") followed by assert orphans == [] to cover
the early-return behavior in delete_workspace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66132cb3-0f90-46e6-8746-4bc6101b80c6

📥 Commits

Reviewing files that changed from the base of the PR and between 0edca41 and 5585cc8.

📒 Files selected for processing (2)
  • openrag/components/indexer/vectordb/test_delete_workspace.py
  • openrag/components/indexer/vectordb/utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openrag/components/indexer/vectordb/utils.py

…kspace test

- Drop unnecessary .subquery() + select() wrapping in delete_workspace;
  pass select objects directly to .notin_()
- Add test_nonexistent_workspace_returns_empty_list to cover the
  early-return path
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants