Skip to content

Feat/file quota - #245

Merged
Ahmath-Gadji merged 8 commits into
devfrom
feat/file_quota
Feb 18, 2026
Merged

Feat/file quota#245
Ahmath-Gadji merged 8 commits into
devfrom
feat/file_quota

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

PR reopen for additional commits before merge. See original PR: #234

Summary by CodeRabbit

  • New Features

    • Per-user file quota system with enforcement (counts include pending tasks), admin/unlimited handling, and endpoints to view/update quotas; user payloads include file_quota, file_count, pending_files
  • Documentation

    • Expanded docs for file quotas, env vars (DEFAULT_FILE_QUOTA, Ray setting), schema changes, migrations, and migration troubleshooting
  • Tests

    • Comprehensive quota enforcement and user quota management tests added
  • Tools

    • DB check-and-repair utility for file_count consistency and new migrations included

@Ahmath-Gadji Ahmath-Gadji added breaking-change Change of behavior after upgrade feat Add a new feature labels Feb 12, 2026
@coderabbitai

coderabbitai Bot commented Feb 12, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a per-user file quota system: DB schema (users.file_quota, users.file_count, files.created_by), Alembic migrations and merge, DEFAULT_FILE_QUOTA config/env, quota enforcement dependency applied to upload endpoints, API for viewing/updating quotas, vectordb/ORM changes to track counts, CLI checker, and tests.

Changes

Cohort / File(s) Summary
CI / Config / Submodule
\.github/workflows/api_tests.yml, \.github/workflows/api_tests/docker-compose.yaml, \.gitmodules, \.hydra_config/config.yaml
Add DEFAULT_FILE_QUOTA in CI/docker-compose and hydra config; add branch = main for extern/indexer-ui.
DB Migrations
openrag/scripts/migrations/alembic/versions/c224d4befe71_add_file_count_and_file_quota.py, openrag/scripts/migrations/alembic/versions/cd9b84278028_merge_heads.py
New migration adding users.file_quota, users.file_count, files.created_by (FK/index) and a merge-head no-op migration.
ORM & VectorDB manager
openrag/components/indexer/vectordb/utils.py, openrag/components/indexer/vectordb/vectordb.py
Add File.created_by, User.file_quota, User.file_count; maintain increments/decrements of file_count on add/remove/partition-delete; expose file_quota/file_count in user payloads; add update_user_quota and extend relevant method signatures to accept quota/user_id.
Task state
openrag/components/indexer/indexer.py
Add TaskStateManager.get_user_pending_task_count(user_id) and expose it under queue_info for pending-task counts.
Quota enforcement & router changes
openrag/routers/utils.py, openrag/routers/indexer.py
Add DEFAULT_FILE_QUOTA constant and async dependency check_user_file_quota (considers indexed + pending files, admin bypass); apply as dependency to add/copy file endpoints.
User API
openrag/routers/users.py
Return enriched user info including file_count, pending_files, file_quota; add admin-protected PATCH /users/{user_id}/quota; accept file_quota in user creation.
Docs & Guides
docs/content/docs/documentation/data_model.md, docs/.../env_vars.md, docs/.../sql_migration.mdx, CLAUDE.md
Document schema additions, DEFAULT_FILE_QUOTA and semantics, Ray env var, and Alembic merge troubleshooting.
Utilities / Scripts
openrag/scripts/check_file_counts.py
New CLI to compare and optionally repair users.file_count against actual per-uploader file totals.
Tests
tests/api_tests/test_indexer.py, tests/api_tests/test_users.py
Add TestUserQuotaEnforcement suite, extend wait_for_task to accept headers, add tests for quota update/default and enforcement scenarios.
Submodule update
extern/indexer-ui
Update submodule commit reference (no repo code changes).

Sequence Diagram

sequenceDiagram
    participant Client as Client API
    participant Router as Router (add_file)
    participant QuotaCheck as check_user_file_quota
    participant VectorDB as VectorDB / PartitionFileManager
    participant TaskMgr as TaskStateManager
    participant DB as Database

    Client->>Router: POST /add_file
    Router->>QuotaCheck: Depends(check_user_file_quota)
    QuotaCheck->>VectorDB: get_user_by_id(user_id)
    VectorDB->>DB: query user.file_quota, user.file_count
    DB-->>VectorDB: user data
    VectorDB-->>QuotaCheck: user data
    QuotaCheck->>TaskMgr: get_user_pending_task_count(user_id)
    TaskMgr-->>QuotaCheck: pending_count
    QuotaCheck->>QuotaCheck: total = file_count + pending_count
    alt total < file_quota (or unlimited)
        QuotaCheck-->>Router: allow
        Router->>VectorDB: add_file_to_partition(..., user_id)
        VectorDB->>DB: insert file (created_by), increment file_count
        DB-->>VectorDB: success
        VectorDB-->>Router: file added
        Router-->>Client: 200 OK
    else total >= file_quota
        QuotaCheck-->>Router: deny (403)
        Router-->>Client: 403 Quota exceeded
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/file quota #234: Implements the same per-user file-quota feature (DEFAULT_FILE_QUOTA, DB migration, quota checks, vectordb changes, tests).
  • Revert "Feat/add ruff linting" #213: Modifies vectordb utilities (create_user/add_file signatures) that overlap with this PR's vectordb/ORM changes.

Suggested reviewers

  • dodekapod

Poem

🐰 I hopped through schemas, counted with care,
Files and quotas sorted fair,
I nudged migrations, tests, and route,
Kept counts in tune without a doubt,
Now uploads rest easy in my lair.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (17 files):

⚔️ .github/workflows/api_tests.yml (content)
⚔️ .github/workflows/api_tests/docker-compose.yaml (content)
⚔️ .gitmodules (content)
⚔️ .hydra_config/config.yaml (content)
⚔️ CLAUDE.md (content)
⚔️ docs/content/docs/documentation/data_model.md (content)
⚔️ docs/content/docs/documentation/env_vars.md (content)
⚔️ docs/content/docs/documentation/sql_migration.mdx (content)
⚔️ extern/indexer-ui (content)
⚔️ openrag/components/indexer/indexer.py (content)
⚔️ openrag/components/indexer/vectordb/utils.py (content)
⚔️ openrag/components/indexer/vectordb/vectordb.py (content)
⚔️ openrag/routers/indexer.py (content)
⚔️ openrag/routers/users.py (content)
⚔️ openrag/routers/utils.py (content)
⚔️ tests/api_tests/test_indexer.py (content)
⚔️ tests/api_tests/test_users.py (content)

These conflicts must be resolved before merging into dev.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/file quota' directly summarizes the main feature added in this pull request: file quota functionality. It is concise and clearly indicates the primary change.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/file_quota

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openrag/components/indexer/vectordb/vectordb.py (1)

886-893: ⚠️ Potential issue | 🔴 Critical

delete_user calls delete_partition without the now-required user_id, causing a TypeError at runtime.

delete_partition in PartitionFileManager (utils.py line 302) now requires user_id: int, but line 892 calls it without that argument. Any attempt to delete a user who owns partitions will crash.

Proposed fix
     async def delete_user(self, user_id: int):
         self._check_user_exists(user_id)
         user_partitions = [
             p["partition"] for p in self.partition_file_manager.list_user_partitions(user_id) if p["role"] == "owner"
         ]
         for partition in user_partitions:
-            self.partition_file_manager.delete_partition(partition)
+            self.partition_file_manager.delete_partition(partition, user_id=user_id)
         self.partition_file_manager.delete_user(user_id)
openrag/components/indexer/indexer.py (1)

164-189: ⚠️ Potential issue | 🟠 Major

user=None will crash delete_file which requires user: dict.

update_file_metadata declares user: dict | None = None (line 170), but calls self.delete_file at line 183 expecting user: dict (non-optional, required parameter at line 149). If update_file_metadata is ever called without a user argument, delete_file will invoke user.get("id") on None, raising an AttributeError.

The current router call site always passes user, but the type signature creates a contract violation that could surface in future usage.

Either make user required in update_file_metadata or add a guard before calling delete_file.

Proposed fix — make user required to match delete_file
     `@ray.method`(concurrency_group="update")
     async def update_file_metadata(
         self,
         file_id: str,
         metadata: dict,
         partition: str,
-        user: dict | None = None,
+        user: dict,
     ):
🤖 Fix all issues with AI agents
In `@docs/content/docs/documentation/data_model.md`:
- Around line 67-71: The markdown table rows for `file_quota` and `file_count`
are malformed (inconsistent pipe spacing) and there's no blank line before the
`**Relationships**` header which causes MD table parsing errors; fix by
normalizing the table row formatting for the `file_quota` and `file_count` lines
so pipes and cell spacing are consistent, then insert a single blank line after
the end of the table and before the `**Relationships**` line so
`**Relationships**` is parsed as a normal heading/paragraph rather than a table
row.

In `@openrag/routers/partition.py`:
- Around line 71-72: The base abstract method delete_partition currently has the
signature async def delete_partition(self, partition: str) but the concrete
implementation expects async def delete_partition(self, partition: str, user_id:
int); update the abstract declaration in the vectordb base class to async def
delete_partition(self, partition: str, user_id: int) (include the user_id type
hint), adjust any abstractmethod decorator if present, and ensure all concrete
subclasses (e.g., the implementation that defines delete_partition(self,
partition: str, user_id: int)) continue to match this signature so the interface
is consistent.

In `@openrag/routers/users.py`:
- Around line 59-95: get_current_user_info currently treats DEFAULT_FILE_QUOTA <
0 as the only signal for "unlimited", which causes inconsistent UX when
DEFAULT_FILE_QUOTA == 0 because check_user_file_quota uses DEFAULT_FILE_QUOTA <=
0 to disable quota enforcement; update get_current_user_info (and its logic
around user_quota assignment) to use DEFAULT_FILE_QUOTA <= 0 when deciding to
set user_quota = float("inf") so the /info response reports unlimited
consistently with check_user_file_quota (refer to function get_current_user_info
and the global DEFAULT_FILE_QUOTA).

In `@openrag/routers/utils.py`:
- Around line 184-187: The check_user_file_quota function currently declares
vectordb=Depends(get_vectordb) but never uses it, causing unnecessary overhead;
remove the unused dependency by deleting the vectordb parameter from the
check_user_file_quota signature (and any corresponding parameter in
callers/route declarations if applicable) so only user=Depends(current_user)
remains, or alternatively if vectordb is intended to be used implement its usage
inside check_user_file_quota (referencing get_vectordb) — ensure to update any
imports/usages accordingly and run tests to confirm no broken dependency
injections.
🧹 Nitpick comments (3)
tests/api_tests/test_indexer.py (1)

486-518: Good enforcement test — verify the 403 error detail message too.

The test correctly validates that the 6th upload is rejected with 403. Consider also asserting on the response body to confirm it's a quota-related rejection (not some other 403 like a permissions issue).

💡 Optional enhancement
             # 6th file should be rejected due to quota
             response = self._upload_file(api_client, partition_name, "file-5", user_token, "Content 5")
             assert response.status_code == 403, (
                 f"6th file should be rejected with 403 Forbidden, got {response.status_code}: {response.text}"
             )
+            assert "quota" in response.json().get("detail", "").lower(), (
+                f"403 should be quota-related, got: {response.text}"
+            )
openrag/routers/users.py (1)

262-277: Consider returning the updated user quota details instead of just a message.

vectordb.update_user_quota returns a dict with the updated user details (including file_quota and file_count), but the response only includes a plain text message. Returning the actual updated state would let clients confirm the change without a follow-up GET request.

Proposed change
     await vectordb.update_user_quota.remote(user_id, file_quota)
-
+    updated_user = await vectordb.update_user_quota.remote(user_id, file_quota)
     logger.debug("Updated user quota", user_id=user_id, file_quota=file_quota)
     return JSONResponse(
         status_code=status.HTTP_200_OK,
-        content={"message": f"Quota for user {user_id} updated to {file_quota}"},
+        content=updated_user,
     )
openrag/components/indexer/vectordb/vectordb.py (1)

52-62: Abstract base class signatures are out of sync with MilvusDB implementations.

BaseVectorDB.delete_file (line 61) and BaseVectorDB.delete_partition (line 53) lack the user_id parameter that the concrete MilvusDB implementations now require (lines 581, 760). Any code relying on the abstract interface would call these methods with the wrong arity.

Proposed fix
     `@abstractmethod`
-    async def delete_partition(self, partition: str):
+    async def delete_partition(self, partition: str, user_id: int):
         pass
 
     ...
 
     `@abstractmethod`
-    async def delete_file(self, file_id: str, partition: str):
+    async def delete_file(self, file_id: str, partition: str, user_id: int):
         pass

Comment on lines +67 to 71
| `file_quota` | Integer (nullable) | Max files allowed for that user |
| `file_count`| Integer (default=0) | Number of uploaded files.|
**Relationships**
- `memberships`: one-to-many → `PartitionMembership`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing blank line before **Relationships** breaks markdown table rendering.

Static analysis (MD055, MD056, MD058) correctly flags that line 69 (**Relationships**) is being parsed as a malformed table row because there's no blank line separating it from the preceding table. Also, line 68 has inconsistent pipe spacing.

📝 Proposed fix
-| `file_count`| Integer (default=0) | Number of uploaded files.|
-**Relationships**
-  - `memberships`: one-to-many → `PartitionMembership`
+| `file_count`   | Integer (default=0) | Number of uploaded files. |
+
+**Relationships**
+- `memberships`: one-to-many → `PartitionMembership`
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 69-69: Table pipe style
Expected: leading_and_trailing; Actual: no_leading_or_trailing; Missing leading pipe

(MD055, table-pipe-style)


[warning] 69-69: Table pipe style
Expected: leading_and_trailing; Actual: no_leading_or_trailing; Missing trailing pipe

(MD055, table-pipe-style)


[warning] 69-69: Table column count
Expected: 3; Actual: 1; Too few cells, row will be missing data

(MD056, table-column-count)


[warning] 69-69: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
In `@docs/content/docs/documentation/data_model.md` around lines 67 - 71, The
markdown table rows for `file_quota` and `file_count` are malformed
(inconsistent pipe spacing) and there's no blank line before the
`**Relationships**` header which causes MD table parsing errors; fix by
normalizing the table row formatting for the `file_quota` and `file_count` lines
so pipes and cell spacing are consistent, then insert a single blank line after
the end of the table and before the `**Relationships**` line so
`**Relationships**` is parsed as a normal heading/paragraph rather than a table
row.

Comment thread openrag/routers/partition.py Outdated
Comment thread openrag/routers/users.py
Comment thread openrag/routers/utils.py
Comment on lines +184 to +187
async def check_user_file_quota(
user=Depends(current_user),
vectordb=Depends(get_vectordb),
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

vectordb dependency is injected but never used in check_user_file_quota.

The vectordb=Depends(get_vectordb) parameter at line 186 is never referenced in the function body. This adds unnecessary overhead on every quota-checked request (Ray actor lookup) with no benefit.

♻️ Proposed fix
 async def check_user_file_quota(
     user=Depends(current_user),
-    vectordb=Depends(get_vectordb),
 ):
🤖 Prompt for AI Agents
In `@openrag/routers/utils.py` around lines 184 - 187, The check_user_file_quota
function currently declares vectordb=Depends(get_vectordb) but never uses it,
causing unnecessary overhead; remove the unused dependency by deleting the
vectordb parameter from the check_user_file_quota signature (and any
corresponding parameter in callers/route declarations if applicable) so only
user=Depends(current_user) remains, or alternatively if vectordb is intended to
be used implement its usage inside check_user_file_quota (referencing
get_vectordb) — ensure to update any imports/usages accordingly and run tests to
confirm no broken dependency injections.


# Increment uploader's file_count
if user_id:
session.query(User).filter(User.id == user_id).update(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nice enhancement with the argument synchronize_session=False that allows fast updates.
But we are still doing the updates manually. If i'm not mistaken this is not the same as using SQL triggers.

user.file_count = func.greatest(0, User.file_count - file_count)

session.delete(partition_obj) # Will delete all files due to cascade
# Count files per uploader before cascade deletes them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I did miss this case in my implementation: discounting per user when a partition is deleted.
It would be nice to have a test that goes along with it

@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator Author

I've tested the migration script and got this error:

INFO [alembic.runtime.migration] Will assume transactional DDL.
ERROR [alembic.util.messaging] Multiple head revisions are present for given argument 'head'; please specify a specific target revision, '<branchname>@head' to narrow to a specific head, or 'heads' for all heads
FAILED: Multiple head revisions are present for given argument 'head'; please specify a specific target revision, '<branchname>@head'
to narrow to a specific head, or 'heads' for all heads
  • explanation:
    Our migration history has diverged into two or more branches. This happens when two migration scripts are created independently from the same base revision without being merged.

  • a1b2c3d4e5f6_add_document_relationships.py

  • c224d4befe71_add_file_count_and_file_quota.py

In fact, theses features relationships and file_quota have been developed independently.

I've created a new migration script that merges them. See my commit

@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: 2

🤖 Fix all issues with AI agents
In `@docs/content/docs/documentation/sql_migration.mdx`:
- Around line 90-94: The migration example is missing the database startup step
so applying migrations can fail; update the "Apply migrations after merge"
snippet to run the rdb service first by adding a `docker compose up -d rdb` step
before the `docker compose run --no-deps --build --rm --entrypoint "uv run
alembic -c /app/openrag/scripts/migrations/alembic/alembic.ini upgrade head"
openrag; docker compose down` command so the rdb is available for Alembic to
apply migrations.

In `@openrag/components/indexer/vectordb/utils.py`:
- Around line 582-603: The update_user_quota method lacks a null-guard for the
queried User which can cause an AttributeError if the user was deleted after a
prior existence check; inside update_user_quota (using self.Session and querying
User), verify that user is not None after s.query(...).first(), and if it is
None log a clear message via self.logger and raise or return a controlled "user
not found" error (e.g., raise a ValueError or a NotFoundError) instead of
proceeding to access attributes; ensure no commit/refresh is attempted when user
is None so callers receive a deterministic error rather than an AttributeError.
🧹 Nitpick comments (3)
openrag/components/indexer/vectordb/vectordb.py (1)

871-878: Consider using keyword arguments when forwarding to partition_file_manager.create_user.

Line 878 forwards all parameters positionally. If the signature of PartitionFileManager.create_user ever changes parameter order, this will silently break.

♻️ Suggested improvement
-        return self.partition_file_manager.create_user(display_name, external_user_id, is_admin, file_quota)
+        return self.partition_file_manager.create_user(
+            display_name=display_name,
+            external_user_id=external_user_id,
+            is_admin=is_admin,
+            file_quota=file_quota,
+        )
tests/api_tests/test_indexer.py (2)

409-456: Good test scaffolding with proper cleanup.

The helper methods and try/finally cleanup pattern are well-structured. Swallowing exceptions in cleanup helpers is appropriate for test teardown.

One minor nit: import io on Line 432 could be moved to the file-level imports since it's also used in test_file_count_stable_on_replace (Line 672).


670-714: Good test for file replacement stability.

Verifies that PUT (replace) doesn't double-count by checking file_count remains 1 after replacement.

Consider adding a test for file_quota=0 (blocks all uploads) as an edge case — currently only file_quota=-1 (unlimited) and file_quota=5 (bounded) are covered.

Comment thread docs/content/docs/documentation/sql_migration.mdx
Comment thread openrag/components/indexer/vectordb/utils.py
Comment thread CLAUDE.md

@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: 3

🤖 Fix all issues with AI agents
In `@openrag/scripts/check_file_counts.py`:
- Line 8: The import list includes an unused symbol 'text' from sqlalchemy which
is causing lint failures; remove 'text' from the import statement so it reads
only "from sqlalchemy import create_engine, func" (ensuring any usages of 'text'
elsewhere are not present or are handled), then run linters to confirm the
pipeline passes.
- Around line 16-23: The build_database_url function constructs a plain-text
PostgreSQL URL using raw user and password values which breaks when credentials
contain special characters; update build_database_url to percent-encode
credentials by importing urllib.parse.quote_plus and applying it to
args.user/user and args.password/password (encode both user and password before
interpolating into the returned f-string) so the generated
"postgresql://{user}:{password}@{host}:{port}/{db_name}" URL is safe for special
characters.

In `@tests/api_tests/test_indexer.py`:
- Around line 703-705: In test_indexer.py adjust the partition delete to use the
same user auth as the other test: replace the admin/default api_client.delete
call that deletes partition_a with a call that includes
headers={"Authorization": f"Bearer {user_token}"}, mirroring the behavior in
test_file_count_decrements_on_delete so the code path exercised is the
uploader-authenticated delete (look for the delete call around partition_a in
the test and the user_token variable used earlier).
🧹 Nitpick comments (2)
openrag/scripts/check_file_counts.py (2)

11-13: Consider using absolute imports per coding guidelines.

The sys.path.insert hack is fragile. If this script is meant to be run from the project root (or installed as part of the package), an absolute import would be more robust and consistent with the project's import conventions.

-# Add parent dirs so we can import the models
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
-from components.indexer.vectordb.utils import File, User
+from openrag.components.indexer.vectordb.utils import File, User

As per coding guidelines: "Use absolute imports from the openrag/ directory as the Python path root, avoiding relative imports across packages."


26-28: Engine is never disposed.

For a CLI script this is unlikely to cause issues, but it's good practice to dispose the engine to cleanly release connections, especially if the script is ever imported and called programmatically.

Proposed fix
 def check_file_counts(database_url, fix=False):
     engine = create_engine(database_url)
-    Session = sessionmaker(bind=engine)
-
-    with Session() as session:
-        ...
-
-    return 0 if not has_mismatch else 1
+    try:
+        Session = sessionmaker(bind=engine)
+        with Session() as session:
+            ...
+        return 0 if not has_mismatch else 1
+    finally:
+        engine.dispose()

Comment thread openrag/scripts/check_file_counts.py Outdated
Comment on lines +16 to +23
def build_database_url(args):
host = args.host or os.environ.get("POSTGRES_HOST", "localhost")
port = args.port or os.environ.get("POSTGRES_PORT", "5432")
user = args.user or os.environ.get("POSTGRES_USER", "root")
password = args.password or os.environ.get("POSTGRES_PASSWORD", "root_password")
collection = args.collection or os.environ.get("VDB_COLLECTION_NAME", "vdb_test")
db_name = f"partitions_for_collection_{collection}"
return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Password with special characters will break the connection URL.

If the PostgreSQL password contains characters like @, /, %, or :, the f-string interpolation on line 23 will produce a malformed URL. Use urllib.parse.quote_plus to percent-encode the credentials.

Proposed fix
+from urllib.parse import quote_plus
+
 def build_database_url(args):
     host = args.host or os.environ.get("POSTGRES_HOST", "localhost")
     port = args.port or os.environ.get("POSTGRES_PORT", "5432")
     user = args.user or os.environ.get("POSTGRES_USER", "root")
     password = args.password or os.environ.get("POSTGRES_PASSWORD", "root_password")
     collection = args.collection or os.environ.get("VDB_COLLECTION_NAME", "vdb_test")
     db_name = f"partitions_for_collection_{collection}"
-    return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
+    return f"postgresql://{quote_plus(user)}:{quote_plus(password)}@{host}:{port}/{db_name}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def build_database_url(args):
host = args.host or os.environ.get("POSTGRES_HOST", "localhost")
port = args.port or os.environ.get("POSTGRES_PORT", "5432")
user = args.user or os.environ.get("POSTGRES_USER", "root")
password = args.password or os.environ.get("POSTGRES_PASSWORD", "root_password")
collection = args.collection or os.environ.get("VDB_COLLECTION_NAME", "vdb_test")
db_name = f"partitions_for_collection_{collection}"
return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
from urllib.parse import quote_plus
def build_database_url(args):
host = args.host or os.environ.get("POSTGRES_HOST", "localhost")
port = args.port or os.environ.get("POSTGRES_PORT", "5432")
user = args.user or os.environ.get("POSTGRES_USER", "root")
password = args.password or os.environ.get("POSTGRES_PASSWORD", "root_password")
collection = args.collection or os.environ.get("VDB_COLLECTION_NAME", "vdb_test")
db_name = f"partitions_for_collection_{collection}"
return f"postgresql://{quote_plus(user)}:{quote_plus(password)}@{host}:{port}/{db_name}"
🤖 Prompt for AI Agents
In `@openrag/scripts/check_file_counts.py` around lines 16 - 23, The
build_database_url function constructs a plain-text PostgreSQL URL using raw
user and password values which breaks when credentials contain special
characters; update build_database_url to percent-encode credentials by importing
urllib.parse.quote_plus and applying it to args.user/user and
args.password/password (encode both user and password before interpolating into
the returned f-string) so the generated
"postgresql://{user}:{password}@{host}:{port}/{db_name}" URL is safe for special
characters.

Comment thread tests/api_tests/test_indexer.py Outdated
Comment thread tests/api_tests/test_indexer.py Outdated
self._cleanup_user(api_client, editor["id"])
self._cleanup_user(api_client, owner["id"])

def test_partition_delete_only_decrements_its_files(self, api_client):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's a nice test but in my comment the idea was more about testing the discounting per user: #245 (comment)

  • Test
    • user A uploads a 2 files. Likewise for user B.
    • Then we delete the partition and check the discounting per user.

@paultranvan paultranvan Feb 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Well that was discounting a user after a partition removal :)
But I see your point, I updated the test

return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"


def check_file_counts(database_url, fix=False):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I guess this is supposed to be scheduled to run periodically. I think that's the missing piece.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yes, but not by the script itself, for now let's keep it manual

@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

🤖 Fix all issues with AI agents
In `@tests/api_tests/test_indexer.py`:
- Around line 473-476: The test blocks currently only wait when
"task_status_url" is present, which misses responses that include only
"task_id"; update each upload handling block to always attempt to extract a task
id via get_task_id(response.json()) and then call wait_for_task(api_client,
task_id, headers={"Authorization": f"Bearer {user_token}"}), or wrap get_task_id
in a try/except to skip waiting only if no task info is present—apply this
change to every occurrence in the test class where the current code checks for
"task_status_url" so uploads that return {"task_id": "..."} are properly
awaited.
🧹 Nitpick comments (2)
tests/api_tests/test_indexer.py (2)

430-442: Move import io to the top of the file.

io is used in _upload_file (line 432) and again in test_file_count_stable_on_replace (line 728). Hoist it to the module-level imports alongside os, time, and Path for consistency.

Proposed fix

At the top of the file:

 import os
 import time
+import io
 from pathlib import Path

Then remove the local imports at lines 432 and 728.


458-458: Remove unused tmp_path fixture parameter.

Both test_unlimited_quota_user_can_upload and test_quota_limit_blocks_excess_uploads accept tmp_path but never use it. This creates unnecessary temp directories on each run.

Proposed fix
-    def test_unlimited_quota_user_can_upload(self, api_client, tmp_path):
+    def test_unlimited_quota_user_can_upload(self, api_client):
-    def test_quota_limit_blocks_excess_uploads(self, api_client, tmp_path):
+    def test_quota_limit_blocks_excess_uploads(self, api_client):

Also applies to: 482-482

Comment on lines +473 to +476
data = response.json()
if "task_status_url" in data:
task_id = get_task_id(data)
wait_for_task(api_client, task_id, headers={"Authorization": f"Bearer {user_token}"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Task wait is skipped when response contains task_id instead of task_status_url.

All quota tests only check if "task_status_url" in data, but get_task_id() (line 52‑58) also handles a bare "task_id" key. If the API ever returns {"task_id": "..."} without task_status_url, the test won't wait for indexing to finish, causing racy assertions on file_count. This pattern repeats across every upload in this class (~10 occurrences).

Consider aligning with the existing tests (e.g., line 157) that unconditionally call get_task_id + wait_for_task:

Proposed fix (apply to all similar blocks)
                 data = response.json()
-                if "task_status_url" in data:
-                    task_id = get_task_id(data)
+                task_id = get_task_id(data)
+                if task_id:
                     wait_for_task(api_client, task_id, headers={"Authorization": f"Bearer {user_token}"})

Or, if some responses genuinely have no task info, guard with a try/except around get_task_id.

🤖 Prompt for AI Agents
In `@tests/api_tests/test_indexer.py` around lines 473 - 476, The test blocks
currently only wait when "task_status_url" is present, which misses responses
that include only "task_id"; update each upload handling block to always attempt
to extract a task id via get_task_id(response.json()) and then call
wait_for_task(api_client, task_id, headers={"Authorization": f"Bearer
{user_token}"}), or wrap get_task_id in a try/except to skip waiting only if no
task info is present—apply this change to every occurrence in the test class
where the current code checks for "task_status_url" so uploads that return
{"task_id": "..."} are properly awaited.

Ahmath-Gadji and others added 8 commits February 18, 2026 15:25
requires database migration to add file_quota column to users table.

Features:
- Add file_quota field to users table (nullable integer)
- Add DEFAULT_FILE_QUOTA env var to set global default quota
- Add PATCH /users/{user_id}/quota endpoint to update user quotas
- Add quota enforcement on file upload (indexed files + pending tasks)
- Admins bypass quota checks; quota<=0 for a user means unlimited; quota > 0 to limit user's quota

API Changes:
- POST /users/ now accepts optional file_quota parameter
- GET /users/ and GET /users/{id} return file_quota in response
- New PATCH /users/{user_id}/quota endpoint for quota management
- GET /users/info now renders additional fields: file_count, pending_files, total_files and file_quota

Tests:
- test_update_user_quota: verify quota update from 10 to 12
- test_user_default_quota: verify None quota defaults to 10
- TestUserQuotaEnforcement class in test_indexer.py:
  - test_unlimited_quota_user_can_exceed_default: user with quota=0 uploads 11 files
  - test_quota_limit_blocks_excess_uploads: user with quota=5 blocked on 6th file

Perform migration: Refer to the doc
https://github.com/linagora/openrag/blob/0b5f84cd880e7c75db6f78d12a7681227721c2ad/docs/content/docs/documentation/sql_migration.mdx?plain=1#L47-L53
Replace SQL triggers with application-level file_count management.
Add created_by column to files table to track who uploaded each file.
Increment/decrement the uploader's file_count in app code, using
func.greatest to clamp decrements to zero for race-condition safety.

Remove user_id from delete_file/delete_partition signatures since the
uploader is now looked up from the file record. Fix delete_user to
also clean up Milvus data via self.delete_partition().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies that deleting a partition only decrements the uploader's
file_count by the number of files in that partition, leaving files
in other partitions unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compares each user's materialized file_count against the actual
count from the files table and displays results in a color-coded
table. Supports --fix to correct mismatched values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two users each upload files to the same partition. On deletion,
verify each user's file_count is decremented independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Ahmath-Gadji
Ahmath-Gadji merged commit 42d8638 into dev Feb 18, 2026
4 of 5 checks passed
@Ahmath-Gadji
Ahmath-Gadji deleted the feat/file_quota branch February 18, 2026 15:50
@coderabbitai coderabbitai Bot mentioned this pull request Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Change of behavior after upgrade feat Add a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants