Skip to content

refactor(phase-7): decompose MilvusDB god object into VectorStore + CatalogStore adapters - #393

Merged
Ahmath-Gadji merged 24 commits into
refactor/hexagonalfrom
refactor/phase-7-storage-persistence-adapters
May 18, 2026
Merged

refactor(phase-7): decompose MilvusDB god object into VectorStore + CatalogStore adapters#393
Ahmath-Gadji merged 24 commits into
refactor/hexagonalfrom
refactor/phase-7-storage-persistence-adapters

Conversation

@Ahmath-Gadji

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

Copy link
Copy Markdown
Collaborator

Phase 7 — Storage & Persistence Adapters

Decomposes the MilvusDB Ray god object (~2,850 LOC: vector search + all PostgreSQL metadata + embedding, in one @ray.remote class) into two clean, single-responsibility adapters behind the Phase 4 ports:

  • MilvusVectorStore (services/storage/milvus_store.py) — vector operations only, implements the VectorStore ABC
  • PostgresStore (services/storage/postgres_store.py) — all metadata, implements the CatalogStore ABC, composing 6 asyncpg repositories

The old MilvusDB actor is not removed — it is reduced to a thin shim that delegates to the new stores, so every one of the ~76 existing .remote() call sites keeps working unchanged. Full removal is deferred to Phase 9.

What's in this PR

7A — PostgreSQL persistence layer (SQLAlchemy ORM → asyncpg)

  • connection.pyConnectionManager with pooled asyncpg, retry/backoff, idempotent migration guard
  • schema.py — 7 ORM models rewritten as metadata-only sa.Table defs (Alembic autogenerate only, no ORM)
  • 6 real repositories decomposed from PartitionFileManager (51 methods): document, user, partition, partition_membership, oidc_session, workspace
  • 6 stub repositories for planned post-refactor features (job, chunk, prompt, conversation, audit_log, idempotency) — signatures only, from the port ABCs
  • PostgresStore composite owns the ConnectionManager lifecycle and exposes each repo as a property
  • Alembic + Milvus migrations relocated under services/persistence/migrations/ (8 existing scripts carried forward unchanged)

7B — Milvus vector store

  • MilvusVectorStore extracted from MilvusDB's vector ops, rewritten against the VectorStore ABC
  • Receives pre-embedded chunks (embedding is no longer a store concern)
  • Preserves the existing one-collection / many-partitions Milvus schema and native hybrid search (dense HNSW + BM25 sparse fused by RRFRanker)
  • Own reconnect logic for ConnectionNotExistException (distinct from the Phase 6 circuit breaker)

7C — God-object shim

  • MilvusDB.__init__ now builds the new stores internally; pure-PG and pure-Milvus methods delegate; the 6 cross-cutting methods (async_add_documents, delete_file, delete_partition, async_search, upsert_file_metadata, add_documents_for_existing_file) call both stores inline until Phase 8 orchestrators land
  • initialize() made idempotent to stop a per-request migration storm

7E — DI wiring

  • di/vector_stores.py, di/repositories.py factories; ServiceContainer wires both stores and exposes individual repos for Phase 8 orchestrator injection

7F — Tests

  • Integration suite for the asyncpg persistence layer (per-repo) + MilvusVectorStore unit + live integration tests, with tests/integration/ conftest and docker-compose

Scope & risk

This is the highest-risk refactoring phase (god-object coupling, dual database, sync ORM → async driver rewrite). The mitigation is the shim: the system runs at every commit and no caller is rewritten here — that is Phase 8's job.

Out of scope (deferred by design)

  • 7D S3/object store — OpenRAG has no S3; skipped
  • Stub repos remain stubs (post-refactor feature hooks)
  • Rewiring callers to use stores via DI → Phase 8
  • Deleting the shim / vectordb.py → Phase 9

Verification

  • New stores instantiate standalone; existing test suite passes through the shim
  • No SQLAlchemy ORM in services/persistence/ or services/storage/ (metadata-only)
  • asyncpg used consistently via the shared pool_getter; no module-level config

72 files changed, ~7.4k insertions. Base: refactor/hexagonal.

@coderabbitai

coderabbitai Bot commented May 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

This PR introduces an async PostgreSQL ConnectionManager with Alembic-compatible metadata, SQLAlchemy schema definitions, and a Milvus 2.6-backed MilvusVectorStore supporting dense/hybrid search, upsert, delete, collection administration, and related unit + integration tests. Config, env mappings, migration docs, and decision-log entries were also added.

Changes

PostgreSQL persistence, migrations, and configuration

Layer / File(s) Summary
Configuration extensions and decision documentation
openrag/core/config/infrastructure.py, openrag/core/config/loader.py, REFACTORING_DECISION_LOG.md
RDBConfig gains an optional database and pool/timeout settings; loader maps POSTGRES_DATABASE, POSTGRES_POOL_MIN_SIZE, POSTGRES_POOL_MAX_SIZE, POSTGRES_COMMAND_TIMEOUT; Phase 7A.1 decision log documents migrations-directory move and schema acceptance checks.
Persistence package entrypoint
openrag/services/persistence/__init__.py
Package entrypoint defines the persistence package and re-exports ConnectionManager and metadata.
ConnectionManager and pool lifecycle
openrag/services/persistence/connection.py
Async ConnectionManager validates RDBConfig.database, constructs DSNs with password-safe logging, creates asyncpg.Pool with bounded exponential-backoff retry, manages shutdown/reinitialization, and runs Alembic migrations in a background thread.
SQLAlchemy schema definitions
openrag/services/persistence/schema.py
Metadata-only SQLAlchemy Table objects for partitions, files, users, oidc_sessions, partition_memberships, workspaces, workspace_files with columns, constraints, foreign keys, and indexes; exported with shared MetaData() for Alembic and raw SQL usage.
Alembic target metadata wiring
openrag/services/persistence/migrations/alembic/env.py
Alembic env.py imports target_metadata from services.persistence.schema instead of the legacy ORM metadata.
Migration documentation path updates
docs/content/docs/documentation/sql_migration.mdx, docs/content/docs/documentation/milvus_migration.mdx
SQL and Milvus migration docs updated to reference services/persistence/migrations/ for Alembic config and migration runners across command examples and troubleshooting.
Milvus migration runner schema version source
openrag/services/persistence/migrations/milvus/...
Milvus migration headers and runner now import SCHEMA_VERSION_PROPERTY_KEY from services.storage.milvus_store and use the new migrations path in examples.
Placeholder Postgres repo tests
openrag/services/persistence/test_*.py
Adds skipped placeholder pytest modules reserving test locations for upcoming Postgres repository implementations.

Milvus vector store implementation

Layer / File(s) Summary
Module docstring, constants, and class initialization
openrag/services/storage/milvus_store.py
Module docstring describes Milvus adapter semantics; defines constants and analyzer params; adds MilvusVectorStore with sync/async Milvus clients and idempotent async initialize(embedding_dimension).
Collection initialization, schema, and index creation
openrag/services/storage/milvus_store.py
_ensure_loaded() creates/loads collections with schema-version checking; _create_schema() builds fields (text, partition, file_id, dense vector, optional sparse BM25); _create_index() configures HNSW/COSINE, inverted and STL_SORT indexes; schema-version persistence and migration checks are enforced.
Filter expression building and sync query pagination
openrag/services/storage/milvus_store.py
_resolve_collection() enforces bound collection names; _build_filter_expr() constructs Milvus filter expressions with partition wildcard and empty IN semantics plus raw expr passthrough; _iter_query() drains query_iterator for sync pagination.
Upsert: data preparation and insert
openrag/services/storage/milvus_store.py
ID conversion helpers and _chunk_to_entity build typed Milvus entities (typed optional fields only when set) and _gen_chunk_order_metadata; upsert() validates embeddings, inserts entities asynchronously, and returns insert counts with domain-specific error translation.
Dense and hybrid search
openrag/services/storage/milvus_store.py
_parse_search_response() normalizes search/hybrid results; search() performs dense ANN over the vector field with constructed filters; hybrid_search() (guarded by config) constructs dense + sparse BM25 requests and fuses results via RRF with normalized responses and error handling.
Deletion, admin, and query utilities
openrag/services/storage/milvus_store.py
delete() coerces numeric Chunk.id to Milvus _id; delete_by_filter() enforces non-empty expressions and rejects tautologies; ensure_collection()/drop_collection() handle admin; collection_exists(), query_ids_by_filter(), and query_chunks_by_filter() provide query utilities.
Tests and integration
openrag/services/storage/test_milvus_store.py, tests/integration/test_milvus_store_integration.py, tests/integration/docker-compose.yaml
Adds unit tests with mocked Milvus clients for pure-logic coverage and an integration suite exercising real Milvus round trips (skipped when unreachable), plus Docker Compose for integration services.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • linagora/openrag#259: Modifies Milvus schema versioning and temporal-field migration logic, including SCHEMA_VERSION_PROPERTY_KEY usage.

Suggested reviewers

  • paultranvan

Poem

🐰 A Milvus meadow blooms with schema bright,
Async pools hum through the migration night,
Partitions and vectors tucked in neat rows—
A rabbit hops where the RRF wind blows.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.33% 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.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title accurately describes the main change: refactoring Phase 7 to decompose a god object into specialized VectorStore and CatalogStore adapters.

✏️ 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 refactor/phase-7-storage-persistence-adapters

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 (1)
openrag/services/persistence/migrations/env.py (1)

34-41: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

env.py always derives the database name from the collection name, but ConnectionManager requires an explicitly set POSTGRES_DATABASE environment variable, risking a mismatch between migration and runtime databases.

Lines 27–32 of env.py use rag_config.rdb.user, .password, .port, .host but explicitly skip .database, instead deriving the database name as f"partitions_for_collection_{rag_config.vectordb.collection_name}". Meanwhile, ConnectionManager.__init__() (connection.py:46–50) raises ValueError if config.database is falsy, requiring POSTGRES_DATABASE to be set via environment.

The infrastructure.py comments (37–40) claim the database name is "wired by the caller," but no such wiring code exists. If POSTGRES_DATABASE is not set or differs from the formula-derived name, migrations and runtime operations will target different databases, breaking the catalog.

Either:

  1. env.py should respect config.rdb.database if set, falling back to the collection-derived name only if None, or
  2. The initialization code must explicitly wire the collection name to rdb.database before ConnectionManager construction.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/persistence/migrations/env.py` around lines 34 - 41, env.py
currently always builds database_url using
f"partitions_for_collection_{rag_config.vectordb.collection_name}" which can
mismatch ConnectionManager (ConnectionManager.__init__) that requires
config.rdb.database/POSTGRES_DATABASE; change env.py to first check for an
explicit database value (config.rdb.database or rag_config.rdb.database) and use
it if present, otherwise fall back to the collection-derived name
(f"partitions_for_collection_{rag_config.vectordb.collection_name}"); ensure the
resultant database name is assigned to the config used by ConnectionManager so
migrations and runtime share the same config.
🧹 Nitpick comments (6)
openrag/services/persistence/connection.py (3)

79-84: ⚡ Quick win

Consider adding server_settings parameter for application_name.

Setting application_name in the connection pool helps with database monitoring and identifying connections in pg_stat_activity. This is especially useful in multi-service deployments.

Add application_name
                self._pool = await asyncpg.create_pool(
                    self._dsn,
                    min_size=self._min_size,
                    max_size=self._max_size,
                    command_timeout=self._command_timeout,
+                   server_settings={"application_name": "openrag-persistence"},
                )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/persistence/connection.py` around lines 79 - 84, The
connection pool creation using asyncpg.create_pool (where self._pool is
assigned) should include server_settings to set application_name for better DB
observability; update the asyncpg.create_pool call to pass
server_settings={'application_name': <name>} (preferably using an
injected/config value such as a new self._application_name or a parameter added
to the class initializer) and ensure any callers/config loading are updated to
provide that value so the pool creation uses the configured application name.

114-118: 💤 Low value

Pool shutdown should log closure.

For operational observability, log when the pool is closed, similar to the log on successful connection (line 85-90). This helps trace connection lifecycle in production logs.

Add shutdown logging
     async def shutdown(self) -> None:
         if self._pool is None:
             return
         await self._pool.close()
         self._pool = None
+        logger.info("Closed Postgres connection pool for %s", self._dsn_log)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/persistence/connection.py` around lines 114 - 118, The
shutdown method currently closes the pool but does not log the closure; update
the async def shutdown(self) to log an informational message when the pool is
closed (use the same logger used by the connection setup/logging in this class)
before setting self._pool = None, e.g., detect self._pool is not None, await
self._pool.close(), call the class logger (e.g., self._logger.info or the module
logger used in connect) with a clear message that the connection pool was
closed, then set self._pool = None.

67-112: ⚡ Quick win

Retry logic should catch only transient connection errors, not permanent configuration errors.

The current retry catches (OSError, asyncpg.PostgresError), which is too broad. asyncpg.PostgresError includes permanent configuration errors like InvalidAuthorizationSpecificationError (auth/SSL mismatches) and InvalidCatalogNameError (database doesn't exist) that should fail immediately instead of retrying.

Replace with only the transient errors:

  • OSError (network-level issues)
  • asyncpg.CannotConnectNowError (database not ready, SQLSTATE 57P03)
  • asyncpg.TooManyConnectionsError (temporary resource exhaustion, SQLSTATE 53300)
Refined exception handling
-            except (OSError, asyncpg.PostgresError) as exc:
+            except (
+                OSError,
+                asyncpg.CannotConnectNowError,
+                asyncpg.TooManyConnectionsError,
+            ) as exc:

This prevents unnecessary retry delays during startup when configuration is invalid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/persistence/connection.py` around lines 67 - 112, The
initialize method currently retries on any asyncpg.PostgresError which can hide
permanent config issues; update the exception handling in initialize to catch
only transient errors: OSError, asyncpg.CannotConnectNowError, and
asyncpg.TooManyConnectionsError (instead of asyncpg.PostgresError) so permanent
errors like InvalidAuthorizationSpecificationError or InvalidCatalogNameError
bubble up immediately; keep the existing retry/backoff logic using
_RETRY_ATTEMPTS and _RETRY_BASE_DELAY_SECONDS/_RETRY_MAX_DELAY_SECONDS, preserve
setting last_exc and the final RuntimeError raise-from behavior, and only
log/retry when the exception is one of those transient types.
openrag/core/config/infrastructure.py (1)

42-44: 💤 Low value

Consider documenting connection pool sizing rationale.

The pool defaults (min=5, max=20, command_timeout=30s) are reasonable for moderate loads, but consider adding inline comments explaining the sizing rationale or linking to performance testing results—especially important for production deployments where these may need tuning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/core/config/infrastructure.py` around lines 42 - 44, Add brief inline
comments next to the pool configuration variables (pool_min_size, pool_max_size,
command_timeout) explaining the rationale for the chosen defaults (e.g.,
expected concurrent connections, memory/CPU tradeoffs, and typical workload
these values target), state units (seconds for command_timeout), and include a
pointer or TODO to link performance test results or tuning guidance for
production deployments so operators know when and how to adjust them.
openrag/services/persistence/schema.py (2)

166-168: 💤 Low value

Workspace created_by uses ondelete="SET NULL".

Similar to files, when a user is deleted, workspaces they created will have created_by set to NULL. Confirm this matches the intended audit/ownership semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/persistence/schema.py` around lines 166 - 168, The workspace
column definition uses ForeignKey("users.id", ondelete="SET NULL") with
nullable=True for created_by; confirm desired audit/ownership behavior and
update the schema accordingly: if workspaces must retain a creator reference
make created_by non-nullable and change ondelete to "RESTRICT" (or remove
ondelete), or if losing creator is acceptable keep "SET NULL" and nullable=True;
update the created_by Column/ForeignKey in the Workspace model (the created_by
column / ForeignKey("users.id", ondelete="SET NULL")) to reflect the chosen
semantics and run migrations.

62-64: ⚡ Quick win

Review ondelete="SET NULL" behavior for created_by foreign key.

The created_by column uses ondelete="SET NULL", which means if a user is deleted, file records will have created_by set to NULL. Verify this is the intended behavior for audit trail purposes—you may want to preserve the user ID or use a different cascade strategy if user deletion should be restricted when files exist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/persistence/schema.py` around lines 62 - 64, The created_by
column in openrag/services/persistence/schema.py currently uses
ForeignKey("users.id", ondelete="SET NULL"); confirm whether you want deleted
users to null out file.created_by or to prevent deletion/preserve the id for
auditing. If you need to preserve the user id for audit trails, change the
ForeignKey to ondelete="RESTRICT" (or remove ondelete entirely) and make
created_by non-nullable if appropriate; if you prefer to keep an immutable audit
copy, add a separate created_by_name/created_by_id audit column and stop relying
on the FK for historical attribution. Update the ForeignKey definition and any
related model constraints (e.g., nullable flag) in the schema accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openrag/services/persistence/connection.py`:
- Line 31: Replace the stdlib logger in connection.py (the variable logger
created via logging.getLogger(__name__)) with the project's Loguru wrapper by
importing get_logger from utils.logger and calling get_logger(__name__) to
obtain the logger; update any existing log statements to use that Loguru logger
and where appropriate bind context like file_id and partition (e.g.,
logger.bind(file_id=..., partition=...)) before logging so structured fields are
included.
- Around line 139-145: Wrap the call to command.upgrade(cfg, "head") in a
try/except around the migration execution in connection.py (the block that
builds cfg, sets "script_location" and "sqlalchemy.url" and logs using
self._dsn_log) to catch Alembic and SQL errors; on exception, log a clear error
via the same logger including self._dsn_log and the caught exception (and its
traceback if desired), then re-raise or exit so failure is visible instead of
producing a cryptic stacktrace. Use broad Exception to ensure
alembic.command.upgrade failures are captured but keep the log message specific
(e.g., "Alembic migration failed for %s: %s" with self._dsn_log and exception).

In `@openrag/services/persistence/schema.py`:
- Around line 129-131: The partition FK on the files table is missing an
explicit ondelete strategy, causing asymmetry with partition_memberships and
workspace which use ForeignKey(..., ondelete="CASCADE"); update the files table
ForeignKey("partitions.partition") to include ondelete="CASCADE" (or
alternatively document and enforce that files must be removed before deleting a
partition) so partition deletions behave consistently; locate and modify the
ForeignKey usage in the files table definition and any other partition FKs
(e.g., in partition_memberships and workspace) to ensure consistent cascade
semantics.
- Line 170: The workspaces table's created_at Column declaration
(Column("created_at", DateTime, default=datetime.now)) is missing the nullable
constraint; update that Column in openrag/services/persistence/schema.py to
include nullable=False (i.e., Column("created_at", DateTime,
default=datetime.now, nullable=False)) so it matches the other created_at
definitions and prevents NULL workspace creation timestamps.

---

Outside diff comments:
In `@openrag/services/persistence/migrations/env.py`:
- Around line 34-41: env.py currently always builds database_url using
f"partitions_for_collection_{rag_config.vectordb.collection_name}" which can
mismatch ConnectionManager (ConnectionManager.__init__) that requires
config.rdb.database/POSTGRES_DATABASE; change env.py to first check for an
explicit database value (config.rdb.database or rag_config.rdb.database) and use
it if present, otherwise fall back to the collection-derived name
(f"partitions_for_collection_{rag_config.vectordb.collection_name}"); ensure the
resultant database name is assigned to the config used by ConnectionManager so
migrations and runtime share the same config.

---

Nitpick comments:
In `@openrag/core/config/infrastructure.py`:
- Around line 42-44: Add brief inline comments next to the pool configuration
variables (pool_min_size, pool_max_size, command_timeout) explaining the
rationale for the chosen defaults (e.g., expected concurrent connections,
memory/CPU tradeoffs, and typical workload these values target), state units
(seconds for command_timeout), and include a pointer or TODO to link performance
test results or tuning guidance for production deployments so operators know
when and how to adjust them.

In `@openrag/services/persistence/connection.py`:
- Around line 79-84: The connection pool creation using asyncpg.create_pool
(where self._pool is assigned) should include server_settings to set
application_name for better DB observability; update the asyncpg.create_pool
call to pass server_settings={'application_name': <name>} (preferably using an
injected/config value such as a new self._application_name or a parameter added
to the class initializer) and ensure any callers/config loading are updated to
provide that value so the pool creation uses the configured application name.
- Around line 114-118: The shutdown method currently closes the pool but does
not log the closure; update the async def shutdown(self) to log an informational
message when the pool is closed (use the same logger used by the connection
setup/logging in this class) before setting self._pool = None, e.g., detect
self._pool is not None, await self._pool.close(), call the class logger (e.g.,
self._logger.info or the module logger used in connect) with a clear message
that the connection pool was closed, then set self._pool = None.
- Around line 67-112: The initialize method currently retries on any
asyncpg.PostgresError which can hide permanent config issues; update the
exception handling in initialize to catch only transient errors: OSError,
asyncpg.CannotConnectNowError, and asyncpg.TooManyConnectionsError (instead of
asyncpg.PostgresError) so permanent errors like
InvalidAuthorizationSpecificationError or InvalidCatalogNameError bubble up
immediately; keep the existing retry/backoff logic using _RETRY_ATTEMPTS and
_RETRY_BASE_DELAY_SECONDS/_RETRY_MAX_DELAY_SECONDS, preserve setting last_exc
and the final RuntimeError raise-from behavior, and only log/retry when the
exception is one of those transient types.

In `@openrag/services/persistence/schema.py`:
- Around line 166-168: The workspace column definition uses
ForeignKey("users.id", ondelete="SET NULL") with nullable=True for created_by;
confirm desired audit/ownership behavior and update the schema accordingly: if
workspaces must retain a creator reference make created_by non-nullable and
change ondelete to "RESTRICT" (or remove ondelete), or if losing creator is
acceptable keep "SET NULL" and nullable=True; update the created_by
Column/ForeignKey in the Workspace model (the created_by column /
ForeignKey("users.id", ondelete="SET NULL")) to reflect the chosen semantics and
run migrations.
- Around line 62-64: The created_by column in
openrag/services/persistence/schema.py currently uses ForeignKey("users.id",
ondelete="SET NULL"); confirm whether you want deleted users to null out
file.created_by or to prevent deletion/preserve the id for auditing. If you need
to preserve the user id for audit trails, change the ForeignKey to
ondelete="RESTRICT" (or remove ondelete entirely) and make created_by
non-nullable if appropriate; if you prefer to keep an immutable audit copy, add
a separate created_by_name/created_by_id audit column and stop relying on the FK
for historical attribution. Update the ForeignKey definition and any related
model constraints (e.g., nullable flag) in the schema accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b5b97fc2-60fd-4553-8fed-7db772fd278e

📥 Commits

Reviewing files that changed from the base of the PR and between ec5a528 and 81804bc.

📒 Files selected for processing (20)
  • REFACTORING_DECISION_LOG.md
  • docs/content/docs/documentation/sql_migration.mdx
  • openrag/core/config/infrastructure.py
  • openrag/core/config/loader.py
  • openrag/services/persistence/__init__.py
  • openrag/services/persistence/connection.py
  • openrag/services/persistence/migrations/README
  • openrag/services/persistence/migrations/alembic.ini
  • openrag/services/persistence/migrations/env.py
  • openrag/services/persistence/migrations/schema_helpers.py
  • openrag/services/persistence/migrations/script.py.mako
  • openrag/services/persistence/migrations/versions/4add4d260575_initial_migration.py
  • openrag/services/persistence/migrations/versions/a1b2c3d4e5f6_add_document_relationships.py
  • openrag/services/persistence/migrations/versions/c224d4befe71_add_file_count_and_file_quota.py
  • openrag/services/persistence/migrations/versions/cd642e4502d8_create_users_memberships_tables.py
  • openrag/services/persistence/migrations/versions/cd9b84278028_merge_heads.py
  • openrag/services/persistence/migrations/versions/e7f8a9b0c1d2_add_workspaces.py
  • openrag/services/persistence/migrations/versions/f1a2b3c4d5e6_add_workspace_files_file_id_fk.py
  • openrag/services/persistence/migrations/versions/f5b6c918f741_add_oidc_auth.py
  • openrag/services/persistence/schema.py
✅ Files skipped from review due to trivial changes (4)
  • openrag/core/config/loader.py
  • openrag/services/persistence/init.py
  • docs/content/docs/documentation/sql_migration.mdx
  • REFACTORING_DECISION_LOG.md

Comment thread openrag/services/persistence/connection.py Outdated
Comment thread openrag/services/persistence/connection.py Outdated
Comment thread openrag/services/persistence/schema.py
Comment thread openrag/services/persistence/schema.py
@EnjoyBacon7
EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from 3c48a9d to 4bb7109 Compare May 12, 2026 15:11

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@openrag/services/persistence/migrations/milvus/1.add_created_at_temporal_fields.py`:
- Around line 21-29: Update the example command lines in the migration doc block
to reference the correct script name: replace all occurrences of
"1.add_temporal_fields.py" with "1.add_created_at_temporal_fields.py" so the
dry-run, apply, and downgrade examples point to the actual file; modify the
three example lines in the header of 1.add_created_at_temporal_fields.py that
build the docker compose commands accordingly.

In `@openrag/services/storage/milvus_store.py`:
- Around line 616-620: The inserted entities are being given an indexed_at
timestamp but the schema expects created_at for temporal queries; change the
code that builds entities (uses indexed_at variable and calls _chunk_to_entity)
to set created_at = datetime.now(UTC).isoformat() and pass created_at into
_chunk_to_entity (and into any other entity construction sites at the earlier
block around lines referencing the same pattern, e.g., the other occurrence at
576-577), optionally keeping indexed_at if you still need it but ensure
created_at is populated on each entity so temporal filtering works correctly.
- Around line 283-289: The code adds the "sparse" field in _create_schema() only
when self._hybrid is true but _create_index() always creates an index for
"sparse", causing failures for dense-only configs; update _create_index() to
guard any logic that creates or configures the "sparse" index (index_type
"SPARSE_INVERTED_INDEX" / field_name "sparse") behind the same condition
(self._hybrid or hybrid_search flag) used in _create_schema(), and apply the
same guard to the second index block referenced around the other occurrence so
that indexing of the sparse field only runs when hybrid mode is enabled.
- Around line 872-876: The delete_by_filter guard in delete_by_filter currently
only checks for empty expr but allows tautological raw expressions like "1==1"
or "true" which will delete the whole collection; update delete_by_filter to
detect and reject simple tautologies by inspecting the incoming filters/expr
(e.g., the expr string passed to delete_by_filter) and raising the same
ValueError for expressions that match common tautologies (case-insensitive
"true", numeric tautologies like "1==1", or equivalent whitespace variations) so
callers must explicitly call drop_collection() to remove all rows.
- Around line 451-456: The current partition-list handling in milvus_store.py
lets a wildcard (e.g. "all") in the list disable partition filtering even when
explicit partitions are present, widening scope; update the logic in the block
that inspects partition (the branch that checks isinstance(partition, (list,
tuple)), using _PARTITION_WILDCARDS and _format_value) to detect a mixed list
containing both wildcards and explicit partitions and reject or sanitize it:
either raise a ValueError (or similar) when a mix is provided, or strip
wildcards and only use explicit entries when building parts (i.e. use [p for p
in partition if p not in _PARTITION_WILDCARDS] and only append "partition in
[...]" if that resulting list is non-empty). Ensure the change references
partition, _PARTITION_WILDCARDS, _format_value and parts so callers cannot
accidentally widen scope.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 928d3a6d-f896-4837-9636-0446b4ae9a79

📥 Commits

Reviewing files that changed from the base of the PR and between 81804bc and 3c48a9d.

📒 Files selected for processing (22)
  • REFACTORING_DECISION_LOG.md
  • docs/content/docs/documentation/milvus_migration.mdx
  • docs/content/docs/documentation/sql_migration.mdx
  • openrag/services/persistence/migrations/alembic/README
  • openrag/services/persistence/migrations/alembic/__init__.py
  • openrag/services/persistence/migrations/alembic/alembic.ini
  • openrag/services/persistence/migrations/alembic/env.py
  • openrag/services/persistence/migrations/alembic/schema_helpers.py
  • openrag/services/persistence/migrations/alembic/script.py.mako
  • openrag/services/persistence/migrations/alembic/versions/4add4d260575_initial_migration.py
  • openrag/services/persistence/migrations/alembic/versions/__init__.py
  • openrag/services/persistence/migrations/alembic/versions/a1b2c3d4e5f6_add_document_relationships.py
  • openrag/services/persistence/migrations/alembic/versions/c224d4befe71_add_file_count_and_file_quota.py
  • openrag/services/persistence/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py
  • openrag/services/persistence/migrations/alembic/versions/cd9b84278028_merge_heads.py
  • openrag/services/persistence/migrations/alembic/versions/e7f8a9b0c1d2_add_workspaces.py
  • openrag/services/persistence/migrations/alembic/versions/f1a2b3c4d5e6_add_workspace_files_file_id_fk.py
  • openrag/services/persistence/migrations/alembic/versions/f5b6c918f741_add_oidc_auth.py
  • openrag/services/persistence/migrations/milvus/1.add_created_at_temporal_fields.py
  • openrag/services/persistence/migrations/milvus/__init__.py
  • openrag/services/persistence/migrations/milvus/migrate.py
  • openrag/services/storage/milvus_store.py
✅ Files skipped from review due to trivial changes (2)
  • REFACTORING_DECISION_LOG.md
  • docs/content/docs/documentation/sql_migration.mdx

Comment thread openrag/services/storage/milvus_store.py
Comment thread openrag/services/storage/milvus_store.py
Comment thread openrag/services/storage/milvus_store.py
Comment thread openrag/services/storage/milvus_store.py Outdated
@coderabbitai coderabbitai Bot added the feat Add a new feature label May 13, 2026

@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 (2)
tests/integration/test_milvus_store_integration.py (2)

286-286: 💤 Low value

Consider using hybrid_config.collection_name instead of accessing _collection_name.

Lines 286, 301, and 311 access the private _collection_name attribute. Since query_ids_by_filter and query_chunks_by_filter accept a collection parameter, using hybrid_config.collection_name would avoid accessing private attributes and make the tests cleaner.

♻️ Refactor to use config value
-        remaining_p1 = await hybrid_store.query_ids_by_filter(hybrid_store._collection_name, {"partition": "p1"})
+        remaining_p1 = await hybrid_store.query_ids_by_filter(hybrid_config.collection_name, {"partition": "p1"})

Apply similar changes to lines 301, 311, 342, and 346.

Also applies to: 301-301, 311-311

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_milvus_store_integration.py` at line 286, Tests are
accessing the private attribute hybrid_store._collection_name; replace those
usages with the public config value hybrid_config.collection_name when calling
hybrid_store.query_ids_by_filter and hybrid_store.query_chunks_by_filter (e.g.,
change calls like
hybrid_store.query_ids_by_filter(hybrid_store._collection_name, ...) to
hybrid_store.query_ids_by_filter(hybrid_config.collection_name, ...)), and apply
the same replacement for all occurrences referenced (lines around usages at 286,
301, 311, 342, 346) so tests no longer depend on private attributes.

315-321: Vector field leakage documented as known issue.

The NOTE comment correctly documents that query_chunks_by_filter currently leaks the dense vector field when using default output_fields, which contradicts the method docstring and is asymmetric with search() behavior. This is good practice to document current behavior while tracking the fix.

Do you want me to open a tracking issue for the vector field stripping in query_chunks_by_filter?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_milvus_store_integration.py` around lines 315 - 321,
Open a tracking issue titled something like "Strip vector field in
query_chunks_by_filter to match search() and docstring" and include: a short
summary of the bug (query_chunks_by_filter leaks dense vector when called with
default output_fields), reproduction steps referencing the test that
demonstrates current behavior
(tests/integration/test_milvus_store_integration.py which shows _iter_query
leaking the vector), the intended behavior (strip vector like search() using
_SEARCH_RESULT_DROPPED_KEYS), and link to the relevant symbols/functions to
change: query_chunks_by_filter, _iter_query, and the constant
_SEARCH_RESULT_DROPPED_KEYS; assign it to the milvus store owner and mark as
bug/regression for follow-up work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/integration/test_milvus_store_integration.py`:
- Line 286: Tests are accessing the private attribute
hybrid_store._collection_name; replace those usages with the public config value
hybrid_config.collection_name when calling hybrid_store.query_ids_by_filter and
hybrid_store.query_chunks_by_filter (e.g., change calls like
hybrid_store.query_ids_by_filter(hybrid_store._collection_name, ...) to
hybrid_store.query_ids_by_filter(hybrid_config.collection_name, ...)), and apply
the same replacement for all occurrences referenced (lines around usages at 286,
301, 311, 342, 346) so tests no longer depend on private attributes.
- Around line 315-321: Open a tracking issue titled something like "Strip vector
field in query_chunks_by_filter to match search() and docstring" and include: a
short summary of the bug (query_chunks_by_filter leaks dense vector when called
with default output_fields), reproduction steps referencing the test that
demonstrates current behavior
(tests/integration/test_milvus_store_integration.py which shows _iter_query
leaking the vector), the intended behavior (strip vector like search() using
_SEARCH_RESULT_DROPPED_KEYS), and link to the relevant symbols/functions to
change: query_chunks_by_filter, _iter_query, and the constant
_SEARCH_RESULT_DROPPED_KEYS; assign it to the milvus store owner and mark as
bug/regression for follow-up work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 95959b08-8ec7-4ad7-9791-8000eb3b2f70

📥 Commits

Reviewing files that changed from the base of the PR and between 3c48a9d and 4ca4f89.

📒 Files selected for processing (30)
  • REFACTORING_DECISION_LOG.md
  • docs/content/docs/documentation/milvus_migration.mdx
  • docs/content/docs/documentation/sql_migration.mdx
  • openrag/services/persistence/connection.py
  • openrag/services/persistence/migrations/alembic/README
  • openrag/services/persistence/migrations/alembic/__init__.py
  • openrag/services/persistence/migrations/alembic/alembic.ini
  • openrag/services/persistence/migrations/alembic/env.py
  • openrag/services/persistence/migrations/alembic/schema_helpers.py
  • openrag/services/persistence/migrations/alembic/script.py.mako
  • openrag/services/persistence/migrations/alembic/versions/4add4d260575_initial_migration.py
  • openrag/services/persistence/migrations/alembic/versions/__init__.py
  • openrag/services/persistence/migrations/alembic/versions/a1b2c3d4e5f6_add_document_relationships.py
  • openrag/services/persistence/migrations/alembic/versions/c224d4befe71_add_file_count_and_file_quota.py
  • openrag/services/persistence/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py
  • openrag/services/persistence/migrations/alembic/versions/cd9b84278028_merge_heads.py
  • openrag/services/persistence/migrations/alembic/versions/e7f8a9b0c1d2_add_workspaces.py
  • openrag/services/persistence/migrations/alembic/versions/f1a2b3c4d5e6_add_workspace_files_file_id_fk.py
  • openrag/services/persistence/migrations/alembic/versions/f5b6c918f741_add_oidc_auth.py
  • openrag/services/persistence/migrations/milvus/1.add_created_at_temporal_fields.py
  • openrag/services/persistence/migrations/milvus/__init__.py
  • openrag/services/persistence/migrations/milvus/migrate.py
  • openrag/services/persistence/test_document_repo.py
  • openrag/services/persistence/test_oidc_session_repo.py
  • openrag/services/persistence/test_partition_membership_repo.py
  • openrag/services/persistence/test_partition_repo.py
  • openrag/services/persistence/test_user_repo.py
  • openrag/services/persistence/test_workspace_repo.py
  • openrag/services/storage/test_milvus_store.py
  • tests/integration/test_milvus_store_integration.py
✅ Files skipped from review due to trivial changes (6)
  • openrag/services/persistence/test_partition_membership_repo.py
  • openrag/services/persistence/test_document_repo.py
  • openrag/services/persistence/test_oidc_session_repo.py
  • openrag/services/persistence/test_partition_repo.py
  • docs/content/docs/documentation/milvus_migration.mdx
  • docs/content/docs/documentation/sql_migration.mdx
🚧 Files skipped from review as they are similar to previous changes (4)
  • openrag/services/persistence/migrations/milvus/1.add_created_at_temporal_fields.py
  • openrag/services/persistence/migrations/milvus/migrate.py
  • openrag/services/persistence/migrations/alembic/env.py
  • openrag/services/persistence/connection.py

Ahmath-Gadji and others added 5 commits May 13, 2026 08:14
ConnectionManager (services/persistence/connection.py) owns the asyncpg
pool lifecycle: initialize() opens the pool with 5-attempt exponential
backoff (1s-16s), shutdown() closes it, run_migrations() invokes Alembic
synchronously via asyncio.to_thread. Repositories will consume the pool
via cm.pool through a lambda pool_getter wrapper (7A.2 onwards).

schema.py declares the 7 existing tables (partitions, files, users,
oidc_sessions, partition_memberships, workspaces, workspace_files) as
metadata-only sa.Table definitions on a shared MetaData. Column types,
indexes, unique constraints and check constraints match the legacy
SQLAlchemy ORM models exactly so Alembic autogenerate sees zero drift.
ORM relationship() calls are dropped - relationships are reconstructed
via raw SQL joins in the new repos.

RDBConfig gains database, pool_min_size, pool_max_size, command_timeout
fields (with sensible defaults). database stays optional - the 7E DI
wiring will derive it from VectorDBConfig.collection_name. The new
fields are also wired in core/config/loader.py via POSTGRES_DATABASE,
POSTGRES_POOL_MIN_SIZE, POSTGRES_POOL_MAX_SIZE, POSTGRES_COMMAND_TIMEOUT
environment variables.
…tions

Pulled forward from the 7A.4 subsection to keep env.py's metadata import
aligned with the new schema.py target. All 8 existing version scripts
(initial through OIDC sessions) carry forward unchanged; env.py imports
target_metadata from services.persistence.schema instead of the legacy
ORM Base.metadata. Old scripts/migrations/alembic/ directory is removed.

Updates docs/content/docs/documentation/sql_migration.mdx in a follow-up.
…ations

Mechanical path swap after the 7A.4 migration directory move - all
references to openrag/scripts/migrations/alembic/ now point at the
new openrag/services/persistence/migrations/ location.
Records four decisions from the 7A.1 implementation: pulling the 7A.4
migration move forward, extending RDBConfig with pool/database fields,
keeping the database name optional with a construction-time guard, and
the schema-vs-ORM diff used as a one-time acceptance check.
@EnjoyBacon7
EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch 2 times, most recently from dd0ef95 to 0605969 Compare May 13, 2026 10:24

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openrag/core/vector_stores/vector_store.py`:
- Around line 30-42: The abstract hybrid_search method on VectorStore
contradicts the design decision to keep hybrid search Milvus-specific; remove
the abstract async def hybrid_search(...) from the VectorStore ABC and add a
public async hybrid_search(self, embedding, query_text, top_k=10,
collection="default", filters=None) implementation to MilvusVectorStore (or the
Milvus-specific subclass) with the same signature and behavior, ensuring other
stores (Pinecone/Weaviate/Qdrant) are not forced to implement it; update
references/tests that called VectorStore.hybrid_search to target
MilvusVectorStore.hybrid_search instead and optionally add a short note in
REFACTORING_DECISION_LOG.md if you change the design.

In `@REFACTORING_DECISION_LOG.md`:
- Around line 726-729: The VectorStore ABC currently declares an abstract
hybrid_search, contradicting the Phase 7B decision that hybrid_search should be
Milvus-specific; change the implementation so VectorStore does NOT declare
hybrid_search and instead implement hybrid_search only on MilvusVectorStore
(remove `@abstractmethod` hybrid_search from VectorStore in
openrag/core/vector_stores/vector_store.py and add/ensure a public
hybrid_search(embedding, query_text, ...) on the MilvusVectorStore concrete
class), or if you intended to keep it on the ABC update the decision log to
state why VectorStore.hybrid_search remains required by all stores (reference
symbols: VectorStore, hybrid_search, MilvusVectorStore).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e4db651e-5634-49a4-bff7-6885e5c8a2ca

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca4f89 and 0605969.

📒 Files selected for processing (37)
  • REFACTORING_DECISION_LOG.md
  • docs/content/docs/documentation/milvus_migration.mdx
  • docs/content/docs/documentation/sql_migration.mdx
  • openrag/core/config/infrastructure.py
  • openrag/core/config/loader.py
  • openrag/core/vector_stores/vector_store.py
  • openrag/services/persistence/__init__.py
  • openrag/services/persistence/connection.py
  • openrag/services/persistence/migrations/alembic/README
  • openrag/services/persistence/migrations/alembic/__init__.py
  • openrag/services/persistence/migrations/alembic/alembic.ini
  • openrag/services/persistence/migrations/alembic/env.py
  • openrag/services/persistence/migrations/alembic/schema_helpers.py
  • openrag/services/persistence/migrations/alembic/script.py.mako
  • openrag/services/persistence/migrations/alembic/versions/4add4d260575_initial_migration.py
  • openrag/services/persistence/migrations/alembic/versions/__init__.py
  • openrag/services/persistence/migrations/alembic/versions/a1b2c3d4e5f6_add_document_relationships.py
  • openrag/services/persistence/migrations/alembic/versions/c224d4befe71_add_file_count_and_file_quota.py
  • openrag/services/persistence/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py
  • openrag/services/persistence/migrations/alembic/versions/cd9b84278028_merge_heads.py
  • openrag/services/persistence/migrations/alembic/versions/e7f8a9b0c1d2_add_workspaces.py
  • openrag/services/persistence/migrations/alembic/versions/f1a2b3c4d5e6_add_workspace_files_file_id_fk.py
  • openrag/services/persistence/migrations/alembic/versions/f5b6c918f741_add_oidc_auth.py
  • openrag/services/persistence/migrations/milvus/1.add_created_at_temporal_fields.py
  • openrag/services/persistence/migrations/milvus/__init__.py
  • openrag/services/persistence/migrations/milvus/migrate.py
  • openrag/services/persistence/schema.py
  • openrag/services/persistence/test_document_repo.py
  • openrag/services/persistence/test_oidc_session_repo.py
  • openrag/services/persistence/test_partition_membership_repo.py
  • openrag/services/persistence/test_partition_repo.py
  • openrag/services/persistence/test_user_repo.py
  • openrag/services/persistence/test_workspace_repo.py
  • openrag/services/storage/milvus_store.py
  • openrag/services/storage/test_milvus_store.py
  • tests/integration/docker-compose.yaml
  • tests/integration/test_milvus_store_integration.py
✅ Files skipped from review due to trivial changes (7)
  • openrag/services/persistence/init.py
  • openrag/services/persistence/test_partition_repo.py
  • openrag/services/persistence/test_user_repo.py
  • docs/content/docs/documentation/milvus_migration.mdx
  • docs/content/docs/documentation/sql_migration.mdx
  • openrag/services/persistence/test_workspace_repo.py
  • openrag/services/persistence/test_oidc_session_repo.py
🚧 Files skipped from review as they are similar to previous changes (11)
  • openrag/core/config/infrastructure.py
  • openrag/services/persistence/test_partition_membership_repo.py
  • openrag/services/persistence/migrations/milvus/1.add_created_at_temporal_fields.py
  • openrag/services/persistence/migrations/milvus/migrate.py
  • openrag/core/config/loader.py
  • openrag/services/storage/test_milvus_store.py
  • openrag/services/persistence/connection.py
  • openrag/services/persistence/schema.py
  • openrag/services/persistence/migrations/alembic/env.py
  • tests/integration/test_milvus_store_integration.py
  • openrag/services/storage/milvus_store.py

Comment thread openrag/core/vector_stores/vector_store.py Outdated
Comment thread REFACTORING_DECISION_LOG.md Outdated
@Ahmath-Gadji
Ahmath-Gadji force-pushed the refactor/phase-7-storage-persistence-adapters branch 2 times, most recently from 1011d14 to e0c893f Compare May 13, 2026 11:01
@EnjoyBacon7
EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch 2 times, most recently from f7262de to 67b7aa5 Compare May 13, 2026 16:18
EnjoyBacon7 added a commit that referenced this pull request May 18, 2026
…ager

Switch connection.py from stdlib logging to the project Loguru wrapper
(get_logger) and convert the %-style log calls to f-strings. Wrap
command.upgrade(cfg, head) in a try/except so a failed migration logs a
clear message and raises a RuntimeError instead of a bare Alembic
stacktrace.

Addresses CodeRabbit review on PR #393.
EnjoyBacon7 added a commit that referenced this pull request May 18, 2026
…ager

Switch connection.py from stdlib logging to the project Loguru wrapper
(get_logger) and convert the %-style log calls to f-strings. Wrap
command.upgrade(cfg, head) in a try/except so a failed migration logs a
clear message and raises a RuntimeError instead of a bare Alembic
stacktrace.

Addresses CodeRabbit review on PR #393.
@EnjoyBacon7
EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from 9376b9d to fadd6d5 Compare May 18, 2026 10:14
@EnjoyBacon7 EnjoyBacon7 changed the title chore: quick start phase 7 refactor(phase-7): decompose MilvusDB god object into VectorStore + CatalogStore adapters May 18, 2026
MilvusVectorStore is a Milvus 2.6 implementation of the narrow
VectorStore ABC — pure vector ops, no embedding, no PG, no Ray. Bound
to one Milvus collection from VectorDBConfig with a lazy
initialize(embedding_dimension) so the DI container can wire it
without pulling the embedder into construction.

hybrid_search is on the ABC — its query_text argument carries the
raw query that backends with a server-side lexical path (Milvus's
BM25 Function, Qdrant sparse, …) need alongside the dense embedding,
so the contract covers the realistic backend set instead of
artificially restricting itself to embedding-only. delete_by_filter
stays Milvus-specific (partition-row deletion used by the 7C shim's
delete_partition flow) — no need to widen the cross-store contract
for it yet.

Five design decisions deviated from / extended the phase 7 plan
(hybrid as ABC method, lazy dim wiring, strict collection-vs-partition
separation aimed at the SaaS end-state, dropping the unjustified
reconnect logic against pymilvus 2.6, and a deliberately narrow
store surface) — all recorded in REFACTORING_DECISION_LOG.md so the
rationale survives future passes.
EnjoyBacon7 added a commit that referenced this pull request May 18, 2026
…ager

Switch connection.py from stdlib logging to the project Loguru wrapper
(get_logger) and convert the %-style log calls to f-strings. Wrap
command.upgrade(cfg, head) in a try/except so a failed migration logs a
clear message and raises a RuntimeError instead of a bare Alembic
stacktrace.

Addresses CodeRabbit review on PR #393.
@EnjoyBacon7
EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from e1a7a06 to 7bad4bf Compare May 18, 2026 15:16
…ations/alembic

Carves out a backend-specific subdirectory so future migration sets
(notably Milvus, landing in the next commit) can sit beside alembic
in the same namespace — services/persistence/migrations/alembic for
Postgres, services/persistence/migrations/milvus for Milvus.

Pure structural move: env.py, alembic.ini, schema_helpers.py,
script.py.mako, README, and the eight version scripts all relocate
unchanged. env.py's metadata import (services.persistence.schema)
keeps resolving via the existing pythonpath setup; alembic.ini's
%(here)s/ script_location and prepend_sys_path = . both remain
valid relative to their new location.

The SQL migration guide is updated in the same commit to point
callers at the new alembic.ini path.
…ions/milvus

Lands Milvus migration assets in the unified migrations namespace
alongside alembic (previous commit), so all schema-evolution scripts
live under a single services/persistence/migrations/ root with one
sub-directory per backend.

The generic runner (migrate.py) and the v0→1 temporal-fields
migration carry forward unchanged in behaviour. SCHEMA_VERSION_PROPERTY_KEY
now imports from services.storage.milvus_store (the new home for the
constant) instead of the legacy components.indexer.vectordb.vectordb;
both modules define an identical value today, and the legacy module
is on the Phase 9 deletion path.

The Milvus migration guide is updated in the same commit — all
references to openrag/scripts/migrations/milvus/ now point at the
new nested location.
Decision 1 captures the choice of nested backend subdirectories
over either the spec-by-symmetry split-root layout
(services/persistence/migrations/ + services/storage/migrations/)
or a flat single dir.

Decision 2 captures the import-source swap for
SCHEMA_VERSION_PROPERTY_KEY from the legacy
components.indexer.vectordb.vectordb to the new
services.storage.milvus_store, eliminating a backwards dependency
into the Phase-9 deletion path.
Two test modules:

* openrag/services/storage/test_milvus_store.py — 52 unit tests with both
  pymilvus clients monkeypatched. Cover _format_value escapes,
  _build_filter_expr partition/list/expr semantics, _resolve_collection
  ABC discipline, ID round-trip, chunk-order metadata linking,
  _chunk_to_entity layering (typed > metadata, omit-None policy), and
  the empty/wildcard guards on upsert/delete/delete_by_filter. Also
  asserts hybrid_search refuses on a store built with hybrid_search=False.
  Colocated with the SUT — runs by default with no infra.

* tests/integration/test_milvus_store_integration.py — 14 end-to-end
  tests against a real Milvus 2.6, gated by the integration marker and
  the OPENRAG_TEST_VDB_HOST env (default localhost:19530). Covers
  initialize idempotence, ensure_collection dimension-mismatch guard,
  upsert/dense-search/hybrid-search round trip, partition-filtered
  search, delete-by-filter (partition scope + wildcard guard),
  query_ids/query_chunks round trip, drop_collection + re-initialize.
  Lives under tests/integration/ per the Phase 13C target test layout
  (tests/{unit,integration,load}/) documented in
  docs/refactoring/REFACTORING_STRATEGY_v1.md. pytest.ini's
  testpaths=openrag means it stays out of the default unit run and only
  fires when explicitly targeted or invoked by CI.

Pure-logic tests use monkeypatch on the loaded module rather than
unittest.mock.patch string paths because the project's pythonpath
setup registers the same source file under two module names
(services.storage.milvus_store vs openrag.services.storage.milvus_store),
which makes string-based patch paths fragile.

The integration fixture deliberately reads OPENRAG_TEST_VDB_HOST rather
than VDB_HOST: pymilvus.settings auto-loads .env at import time, which
would inject the docker-network hostname "milvus" into a host-side test
run. The dedicated env keeps runtime config and test config independent.

One test documents (rather than asserts against) an asymmetry: the
_iter_query path in query_chunks_by_filter leaks the dense vector when
called with "*" output_fields, while the search path strips it via
_SEARCH_RESULT_DROPPED_KEYS. Tracked as a follow-up.

A dedicated GitHub Actions job (.github/workflows/integration_tests.yml)
brings up a minimal Milvus stack via tests/integration/docker-compose.yaml
(etcd + minio + milvus 2.6, with 19530 and 9091 exposed to the host)
and runs pytest tests/integration/ -m integration. The compose file
deliberately does not pull in openrag/mock-vllm/rdb — only what the
integration suite touches — so the job stays fast and independent of
the heavier api-tests stack.
EnjoyBacon7 and others added 14 commits May 18, 2026 15:29
Decomposes PartitionFileManager (utils.py, 1095 LOC, 51 methods) into
fifteen asyncpg-backed Pg*Repository classes under services/persistence/.
Five are real (document, user, partition, oidc_session, workspace) and
cover every method moving over from the legacy god-object; ten are stubs
(job, chunk, prompt, conversation, audit_log, idempotency, entity,
topic_tag, model_endpoint, preset) that raise StubRepositoryError until
their tables exist.

Each real repo exposes a dual surface: the typed port ABC methods used
by future orchestrators, plus the legacy method names with the original
positional signatures the 7C shim will call. Repos receive a pool_getter
callable rather than a pool reference so they always see the live pool
when the connection manager is reinitialised in tests.

Supporting changes:

  * Add a Workspace domain model and WorkspaceRepository port (omitted in
    phase 4) plus a workspace_repo property on CatalogStore.
  * Extend OIDCSession with three optional bytes fields for the Fernet
    encrypted IdP tokens so create_session can carry them through the
    port without a side channel.
  * Register json/jsonb codecs in ConnectionManager._init_connection so
    repositories read JSON columns as Python dicts and write dicts back
    without per-call json.dumps boilerplate.
  * Log the six 7A.2 design decisions in REFACTORING_DECISION_LOG.md
    (new port, OIDC token bytes, dual surface, user-model field
    reconciliation, stub-repo error class, connection-level codec).
PostgresStore composes the Phase 7A.1 connection manager with every
Phase 7A.2 repository implementation into a single CatalogStore root.
Orchestrators and the upcoming 7C shim consume it through the port
ABC; the fifteen repos are wired once in __init__ and share one
pool_getter callable so a shutdown/initialize cycle in tests rebuilds
the pool without touching the repos.

The class lives in services/storage/ alongside MilvusStore rather than
inside services/persistence/, matching the layer split the rest of the
phase 7 plan assumes: storage is the high-level adapter surface,
persistence owns the row-level implementations.

initialize() opens the pool, then runs Alembic to head; a
run_migrations=False flag is exposed for fast unit tests against an
already-migrated database. A pool property gives Phase 8 orchestrators
a transactional escape hatch without polluting the CatalogStore ABC
with asyncpg specifics.

The four 7A.3 design decisions (placement, eager repo construction,
single-entry-point lifecycle, ABC-free pool escape hatch) are
recorded in REFACTORING_DECISION_LOG.md.
ServiceContainer gains an optional Settings argument that, when provided,
builds the Phase 7A.3 PostgresStore via a dedicated factory and exposes
every catalog repository as a typed property on the container. The
existing no-arg construction path is preserved so the registry-only
inference tests keep working unchanged; storage accessors raise a clear
RuntimeError when reached without Settings.

di/repositories.create_catalog_store centralises the legacy
"partitions_for_collection_<collection>" database-name fallback so DI
wiring code never has to mention the prefix; an explicit rdb.database
still wins. di/vector_stores.create_vector_store is a fail-loud Phase
7B stub with the right signature, so Phase 8 code can already import
the symbol — only the body changes when MilvusVectorStore lands.

The container exposes async initialize() and shutdown() that open and
close the asyncpg pool plus Alembic migrations, all behind the
CatalogStore ABC. Per-repo accessors return the same instance the
store holds, so orchestrator injection in Phase 8 sees a single source
of truth.

A new di/test_container.py covers the wiring end to end: legacy no-arg
container, Settings-driven container, catalog-store ABC conformance,
database-name derivation and override, all fifteen repo properties
returning port-typed instances that identity-match the store's, and the
vector store factory + property raising the Phase 7B message.

Also aligns services/persistence/ and services/storage/ imports to the
project's short-form convention (from core.X, not from openrag.core.X),
fixing a dual-import isinstance bug surfaced by the new container test:
pytest sets pythonpath=./openrag so the same module is reachable under
two names and Python sees the two as distinct classes. CLAUDE.md
already mandates the short form.

The four 7E design decisions (optional Settings, centralised database
fallback, fail-loud vector-store stub, import-convention alignment)
are recorded in REFACTORING_DECISION_LOG.md.
Phase 7F lands a real-Postgres integration suite under
tests/integration/ for the Phase 7A.3 PostgresStore composite plus the
five real repositories decomposed from PartitionFileManager. The suite
is kept outside pytest.ini's testpaths so the default unit run stays
infra-free; operators invoke it with uv run pytest tests/integration/.

Fixtures (conftest.py):

  * Discover a Postgres DSN via POSTGRES_TEST_ADMIN_DSN, falling back
    to the local docker-compose rdb container at 172.21.0.4. Auto-skip
    the whole suite when neither is reachable.
  * Session-scoped: drop+recreate openrag_phase7_test, build a
    PostgresStore, run Alembic to head.
  * Autouse function-scoped: TRUNCATE the seven user-modifiable tables
    with RESTART IDENTITY CASCADE so each test starts at id=1.
  * loop_scope=session on every async fixture and test marker so the
    pool stays on the one event loop pytest-asyncio creates per
    session.

Tests:

  * test_postgres_store.py — 20 cases: CatalogStore ABC conformance,
    pool lifecycle, all 15 repo properties returning the expected
    PgX class, migration idempotency, initialise/shutdown round-trip
    on an isolated store.
  * test_partition_repo.py — create/list/exists/delete/idempotent
    create.
  * test_document_repo.py — CRUD, partition-scoped list, status fold
    into metadata JSON, metadata-merge semantics, delete-by-partition
    count.
  * test_user_repo.py — CRUD, email lowercasing, by-email/by-external-
    id lookup, legacy token mint/regenerate/hash-lookup, partition
    membership assign/idempotent-upsert/remove.
  * test_oidc_session_repo.py — round-trip encrypted token bytes,
    revoked/expired rows hidden from get_by_token_hash, revoke-by-sid
    and revoke-by-user counts, 7-day retention window before
    delete_expired removes a row.
  * test_workspace_repo.py — workspace CRUD, file membership
    (idempotent add, partition-scoped get_file_workspaces, orphan
    detection on delete, remove_file_from_all_workspaces).
  * test_stores.py — cross-store full-cycle marked xfail(strict=True)
    until Phase 7B's MilvusVectorStore lands.

73 integration tests pass; the unit suite is unchanged at 655 passed.

Two real bugs surfaced while writing the suite are fixed in the same
commit so the new tests land green:

  * PgOIDCSessionRepository.delete_expired built a tz-aware cutoff and
    bound it against the tz-naive session_expires_at column, which
    asyncpg rejects at encode time. Strip tzinfo before the query.
  * services/persistence/migrations/env.py unconditionally clobbered
    sqlalchemy.url with one derived from load_config(), ignoring the
    DSN ConnectionManager.run_migrations() had just set. The first run
    of the test suite therefore tried to resolve docker's rdb
    hostname from the host. Defer to the preset URL when it's not the
    alembic.ini placeholder.

The five 7F design decisions (placement under tests/integration/,
per-test truncate vs per-test DB, session-scoped event loop, strict
xfail for the cross-store test, fixing the two surfaced bugs in this
commit rather than splitting them out) are recorded in
REFACTORING_DECISION_LOG.md.
Person A's 7E commit was drafted while Phase 7B was still pending, so
the di/vector_stores.create_vector_store factory was a fail-loud stub
("Phase 7B deliverable") and the di/container.ServiceContainer.vector_store
property raised NotImplementedError at access time. Phase 7B has since
landed on this branch (commit 5617cb3), so the factory swaps to its
final body — return MilvusVectorStore(settings.vectordb) — and the
container caches the instance at construction so repeated property reads
do not open a new pymilvus gRPC channel each time.

services/storage/__init__.py now exports MilvusVectorStore alongside
PostgresStore and the docstring drops the "Phase 7B placeholder"
language.

The di/test_container.TestVectorStoreNotYetWired class renames to
TestVectorStoreWiring and asserts isinstance against MilvusVectorStore.
An autouse module-level fixture patches MilvusClient / AsyncMilvusClient
with MagicMock so the unit suite does not need Milvus reachable — the
real coverage lives in tests/integration/test_milvus_store_integration.py.

tests/integration/test_stores.py keeps its strict xfail because the
cross-store fixture still needs a Milvus-store companion in the
postgres_store conftest — that lands in Phase 7C. The xfail reason and
the matching REFACTORING_DECISION_LOG.md 7F#4 entry are updated so
"Phase 7C handoff" is the named blocker rather than "Phase 7B not
landed".

Decision log entries 7E#3 and 7F#4 reflect the post-rebase reality.
…e ABC

The Phase 7B decision log still recorded the original "ABC stays
embedding-only; hybrid lives on MilvusVectorStore" choice, but the
hybrid_search abstract method has since been added to the
VectorStore ABC (see the 7B refactor commit). Rewrite §1 to capture
the reversal and the SaaS-backend reasoning that drove it (Qdrant /
Weaviate / Pinecone / OpenSearch all have a hybrid path; treating it
as a Milvus quirk would force an isinstance leak in the Phase-8
retrieval orchestrator). Trim §5's "store surface" list to reflect
that hybrid_search is no longer Milvus-only — the only remaining
Milvus-only public method on the store is delete_by_filter.

Resolves the CodeRabbit pull-request comments flagging the
"abstract method contradicts documented decision" inconsistency in
vector_store.py:42 and REFACTORING_DECISION_LOG.md:909.
CI's ruff format --check caught five files that hadn't gone through the
formatter: openrag/di/container.py, openrag/services/persistence/document_repo.py,
openrag/services/persistence/user_repo.py, tests/integration/conftest.py,
tests/integration/test_oidc_session_repo.py. Whitespace and line-wrap
adjustments only — no behaviour change. Lint job was the only red on the
PR; tests/api-tests/milvus-integration were still in progress.
Replace the legacy PartitionFileManager+MilvusDB actor body in
vectordb.py with a thin shim that composes MilvusVectorStore and
PostgresStore behind the existing Ray actor surface. Wire dependencies
to initialise the actor's stores inside its own asyncio loop so the
asyncpg pool is bound to the actor loop, and override the restart hook
so a recreated actor goes through the same initialize step.
…he shim

Record the trade-off: the per-collection database bootstrap stayed on
the MilvusDB shim (not in PostgresStore.initialize) to honour Phase 7C's
"vectordb.py only" scope contract. Flagged as a follow-up to move into
PostgresStore / ConnectionManager during 7E DI wiring so the helper can
be deleted in Phase 9 without regression.
PgPartitionRepository.delete_partition assumed files.partition_name
cascaded at the DB level, but the FK has no ON DELETE CASCADE — the
legacy ORM relied on SQLAlchemy's Python-side cascade. Issue an explicit
DELETE FROM files inside the same transaction before dropping the
partition row, matching the legacy bookkeeping. Add an integration test
that creates files under an uploader and asserts the cascade plus the
per-uploader file_count decrement.
`ray.get()` is synchronous and returns the resolved value, so wrapping
it with `await` tried to await a `str` and raised TypeError. Drop the
`ray.get()` wrapper and await the ObjectRef directly — the async-actor
pattern used elsewhere in this module.
…ager

Switch connection.py from stdlib logging to the project Loguru wrapper
(get_logger) and convert the %-style log calls to f-strings. Wrap
command.upgrade(cfg, head) in a try/except so a failed migration logs a
clear message and raises a RuntimeError instead of a bare Alembic
stacktrace.

Addresses CodeRabbit review on PR #393.
… migration storm

get_vectordb() is a FastAPI Depends used by 34 endpoints; it re-invokes
the Vectordb actor's initialize on every request. The shim delegates to
PostgresStore.initialize(), which re-ran a full Alembic command.upgrade
each time because, unlike MilvusVectorStore (guarded by _loaded),
PostgresStore had no idempotency guard. Under concurrent api-tests load
the repeated migrations starved the Postgres pool and surfaced as
intermittent 500s on indexer/workspace endpoints.

Guard initialize() with an _initialized flag (reset on shutdown) so
re-running on a hot actor is a no-op, matching the documented contract
in get_vectordb(). Add an integration regression test asserting three
initialize() calls trigger run_migrations exactly once.
Restores the 7A.2 one-repo-per-entity layout: partition memberships
get their own port and asyncpg implementation instead of riding on
UserRepository.

- new core/ports/partition_membership_repo.py (PartitionMembershipRepository ABC)
- new services/persistence/partition_membership_repo.py (PgPartitionMembershipRepository)
- moved the 6 clean methods + 6 legacy shim methods + _row_to_user_partition
  off PgUserRepository
- UserRepository keeps reading partition_memberships via _fetch_memberships
  only to hydrate the User aggregate's partitions field (read-only
  denormalisation, not membership management)
- expose membership_repo on CatalogStore, PostgresStore and the DI container
- repoint the 7 membership call sites in the vectordb.py shim
- split the membership tests into tests/integration/test_partition_membership_repo.py

Unit suite unchanged (714 passed); no caller outside the shim touched.
@EnjoyBacon7
EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from 7bad4bf to 79afed0 Compare May 18, 2026 15:35
@Ahmath-Gadji
Ahmath-Gadji merged commit b091957 into refactor/hexagonal May 18, 2026
10 checks passed
@Ahmath-Gadji
Ahmath-Gadji deleted the refactor/phase-7-storage-persistence-adapters branch May 18, 2026 15:45
EnjoyBacon7 added a commit that referenced this pull request May 26, 2026
…ager

Switch connection.py from stdlib logging to the project Loguru wrapper
(get_logger) and convert the %-style log calls to f-strings. Wrap
command.upgrade(cfg, head) in a try/except so a failed migration logs a
clear message and raises a RuntimeError instead of a bare Alembic
stacktrace.

Addresses CodeRabbit review on PR #393.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Add a new feature refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants