Skip to content

chore: quick start phase 7#393

Open
Ahmath-Gadji wants to merge 24 commits into
refactor/hexagonalfrom
refactor/phase-7-storage-persistence-adapters
Open

chore: quick start phase 7#393
Ahmath-Gadji wants to merge 24 commits into
refactor/hexagonalfrom
refactor/phase-7-storage-persistence-adapters

Conversation

@Ahmath-Gadji
Copy link
Copy Markdown
Collaborator

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

Summary by CodeRabbit

  • New Features

    • Milvus-backed vector store: dense ANN search, optional hybrid (vector+BM25) search, upsert/delete, collection/partition-aware operations.
    • Async PostgreSQL ConnectionManager with pooled connections and built-in migration runner.
  • Refactor

    • Persistence reorganized: shared schema metadata, unified migrations layout, and extended DB config options.
  • Documentation

    • Migration docs updated for new SQL and Milvus migration paths.
  • Tests

    • Unit and integration tests for Milvus store; placeholder/skipped tests for upcoming Postgres repos; integration compose config added.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

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 | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'chore: quick start phase 7' is vague and generic. While it references 'phase 7', it does not convey specific information about the actual changes (Milvus vector store, PostgreSQL connection manager, schema definitions, migrations). Provide a more descriptive title that captures the primary change, such as 'feat: add Milvus vector store and PostgreSQL persistence layer (phase 7)' or 'feat: implement MilvusVectorStore and ConnectionManager adapters'.
✅ Passed checks (3 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.

✏️ 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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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

from openrag.core.config.infrastructure import RDBConfig


logger = logging.getLogger(__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 | 🟡 Minor | ⚡ Quick win

Use Loguru structured logging instead of stdlib logging.

Per coding guidelines: "Use Loguru structured logging via from utils.logger import get_logger with binding for context variables like file_id and partition."

This module uses import logging and logging.getLogger(__name__), which bypasses the centralized Loguru configuration.

Proposed fix
-import logging
+from utils.logger import get_logger

-logger = logging.getLogger(__name__)
+logger = get_logger()

Then update log calls to use Loguru's syntax (.info(), .warning(), etc. remain the same, but you can add .bind() for structured context).

As per coding guidelines: Use Loguru structured logging via from utils.logger import get_logger.

🤖 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` at 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.

Comment on lines +139 to +145
cfg = Config(str(_ALEMBIC_INI))
# ``script_location`` in alembic.ini is relative to the .ini file via
# %(here)s, so this resolves to services/persistence/migrations/.
cfg.set_main_option("script_location", str(_MIGRATIONS_DIR))
cfg.set_main_option("sqlalchemy.url", self._dsn)
logger.info("Running Alembic migrations against %s", self._dsn_log)
command.upgrade(cfg, "head")
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 | ⚡ Quick win

Migration execution should catch and log Alembic exceptions.

The command.upgrade(cfg, "head") call can raise various Alembic exceptions (e.g., revision conflicts, SQL errors). Consider wrapping this in a try-except to provide clearer error messages and prevent cryptic stacktraces.

Add exception handling
         cfg.set_main_option("script_location", str(_MIGRATIONS_DIR))
         cfg.set_main_option("sqlalchemy.url", self._dsn)
         logger.info("Running Alembic migrations against %s", self._dsn_log)
-        command.upgrade(cfg, "head")
+        try:
+            command.upgrade(cfg, "head")
+            logger.info("Migrations completed successfully")
+        except Exception as exc:
+            logger.error("Migration failed: %s", exc)
+            raise RuntimeError(
+                f"Failed to run Alembic migrations against {self._dsn_log}: {exc}"
+            ) from exc
🤖 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 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).

Comment on lines +129 to +131
ForeignKey("partitions.partition", ondelete="CASCADE"),
nullable=False,
index=True,
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 | ⚡ Quick win

Verify cascade strategy for partition deletion.

Both partition_memberships and workspace tables use ondelete="CASCADE" when a partition is deleted. This will:

  • Delete all memberships (line 129)
  • Delete all workspaces in that partition (line 159)
  • Cascade further to workspace_files (line 181)

However, the files table (line 52-54) uses a plain ForeignKey("partitions.partition") without an explicit ondelete strategy, which defaults to RESTRICT in most databases. This asymmetry could prevent partition deletion if files exist.

Recommended fix to align cascade behavior

Either add ondelete="CASCADE" to the files table foreign key:

 Column(
     "partition_name",
     String,
-    ForeignKey("partitions.partition"),
+    ForeignKey("partitions.partition", ondelete="CASCADE"),
     nullable=False,
     index=True,
 ),

Or explicitly document that partition deletion requires files to be deleted first.

Also applies to: 136-138

🤖 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 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.

nullable=True,
),
Column("display_name", String, nullable=True),
Column("created_at", DateTime, default=datetime.now),
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 | ⚡ Quick win

workspaces.created_at is missing nullable=False constraint.

Lines 42, 84, 115, 141 all specify nullable=False for created_at columns, but line 170 omits it. This inconsistency could allow NULL timestamps for workspace creation.

Proposed fix
-    Column("created_at", DateTime, default=datetime.now),
+    Column("created_at", DateTime, default=datetime.now, nullable=False),
📝 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
Column("created_at", DateTime, default=datetime.now),
Column("created_at", DateTime, default=datetime.now, nullable=False),
🤖 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` at 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.

@EnjoyBacon7 EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from 3c48a9d to 4bb7109 Compare May 12, 2026 15:11
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 from 4ca4f89 to dd0ef95 Compare May 13, 2026 08:41
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.
…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.
The nest move (commit 8d2b609) relocated alembic.ini under
services/persistence/migrations/alembic/ but left ConnectionManager
resolving the .ini at the old flat path
(services/persistence/migrations/alembic.ini), which would raise
FileNotFoundError the first time run_migrations() executes.

Re-point _MIGRATIONS_DIR at the nested subdirectory so both
_ALEMBIC_INI and the script_location alembic.Config setting resolve
to real files. Verified the resolved path now exists.
@EnjoyBacon7 EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from dd0ef95 to 0605969 Compare May 13, 2026 10:24
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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
Comment thread REFACTORING_DECISION_LOG.md Outdated
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.
Six skip-only placeholders, one per upcoming repo
(document_repo, user_repo, partition_repo, partition_membership_repo,
oidc_session_repo, workspace_repo). Each carries a docstring naming
the target ABC port plus a module-level pytest.skip(reason="awaiting
7A.2 <name> from Person A"). Colocated with where the implementation
will land, so Person A opens services/persistence/ to write the repo
and the test file is already sitting beside it.

Two of the six (partition_membership_repo, workspace_repo) flag that
the matching port ABC under core/ports/ does not exist yet — those
will land in the same 7A.2 commit as the implementation.

Style — unit, integration, or mixed — is Person A's call at 7A.2.
Phase 13C will sweep these into tests/{unit,integration}/ in bulk
once the implementations are in.
@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 and others added 6 commits May 13, 2026 11:02
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.
…istence

Six files under openrag/services/persistence/ — test_document_repo.py,
test_oidc_session_repo.py, test_partition_membership_repo.py,
test_partition_repo.py, test_user_repo.py, test_workspace_repo.py —
were placeholder slots Person B reserved before Phase 7A.2 merged,
each marked pytest.mark.skip with a reason saying "awaiting 7A.2 ...
from Person A". 7A.2 has now merged and real coverage lives in
tests/integration/test_*_repo.py against a live Postgres, so the
placeholders skip on every run with a no-longer-valid reason and a
no-op test_placeholder().

test_partition_membership_repo.py additionally referenced a
PartitionMembershipRepository port that does not exist in core/ports/
and never will — Phase 4 folded partition memberships into
UserRepository (assign_partition / list_user_partitions / etc), which
7A.2 implemented as PgUserRepository.

Skip count drops from 10 → 4 with no change to the 714 passing tests.
@EnjoyBacon7 EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from c1b19d1 to f7262de Compare May 13, 2026 16:03
Replace the legacy PartitionFileManager+MilvusDB actor body with a thin
shim in vectordb_shims.py 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.
@EnjoyBacon7 EnjoyBacon7 force-pushed the refactor/phase-7-storage-persistence-adapters branch from f7262de to 67b7aa5 Compare May 13, 2026 16:18
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