Skip to content

Phase 1: Storage and Schema#36

Open
sid-ak wants to merge 19 commits into
devfrom
2-phase-1-storage-and-schema
Open

Phase 1: Storage and Schema#36
sid-ak wants to merge 19 commits into
devfrom
2-phase-1-storage-and-schema

Conversation

@sid-ak

@sid-ak sid-ak commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the storage layer for the study assistant's shared retrieval library (rag_core). It sets up a Postgres (pgvector) schema for source documents and the text chunks split out of them, plus a small client that reads and writes them. The client sits behind an interface, so the later ingestion and retrieval work builds on that abstraction instead of talking to the database directly.

  • Schema: documents and chunks with an HNSW index for cosine similarity, a GIN index over a
    generated tsvector column for full-text search, and the single-user user_id seam (fc5fb2f).
  • Store client: the CRUD surface over the schema, bulk-inserting chunks via COPY ... FORMAT BINARY
    rather than per-row INSERT (ee64b04).
  • Interface seam: a StoreProtocol with two implementations — the pgvector-backed Store and an
    InMemoryStore fake — so callers bind to the interface and unit tests run without a database
    (5c3f016).
  • Tests and CI: a single parametrized contract suite run against both implementations, with
    genuinely DB-only behavior (vector round-tripping, schema DDL idempotency) split into a separate
    integration module; CI gains a pgvector service and a test database provisioned by init.sql
    (544b180, 467e0b7).
  • Lint: docstring presence enforced repo-wide through ruff's pydocstyle rules (a325e8f).

Motivation

Phase 1 (issue #2): the storage layer every later phase builds on, stood up behind the ADR 0006
interface boundary so ingestion and retrieval depend on a Protocol rather than raw pgvector.

Type

  • Bug
  • Feature
  • Improvement

Related issues

Closes #2.

Verification

  1. pre-commit run --all-files and uv run sphinx-build -b html docs site -W — the static gate and
    docs build: hygiene hooks, ruff lint + format, mypy, and the Sphinx build with warnings-as-errors.
    All green.
  2. docker compose up -d && uv run pytest — the full test suite (unit plus DB-backed integration);
    20 passed. The database must be up or the integration tests fail to connect.
  3. docker compose up -d && uv run pytest -m integration — 10 passed. The DB-backed tests round-trip
    a chunk through real pgvector (the embedding column reads back as a numpy array), confirm
    delete_document cascades to its chunks, and confirm initialize_schema() is idempotent
    (test_db_store.py plus the [db] leg of the contract suite).
  4. uv run pytest -m unit — 10 passed with no database: the parametrized StoreProtocol contract
    suite runs against InMemoryStore, and the conformance test asserts both Store and
    InMemoryStore satisfy StoreProtocol (test_store.py).

Decision records / ADRs

  • docs/decisions/0006-interface-first-architecture.md — store, embedder, reranker, and generator sit
    behind Protocols; the seam this branch implements for the store.
  • docs/decisions/0007-generation-backend.md — vendor-neutral generation adapter.
  • docs/decisions/0008-per-model-embedding-columns.md — additive per-model embedding columns;
    recorded here, implementation deferred to Phase 3 (Phase 3 — Embeddings + dense retrieval (rag_core/embed) #4).

@sid-ak sid-ak self-assigned this Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9280b565-6e7f-497d-b1f1-9cd1095a338a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a pgvector-backed storage layer with a shared protocol and in-memory fake, wires integration tests and Postgres test infrastructure, and updates contributor guidance, ADRs, docs navigation, and review automation.

Changes

Store implementation and test wiring

Layer / File(s) Summary
Contracts and schema
packages/rag_core/src/rag_core/config.py, packages/rag_core/src/rag_core/store/schema.py, packages/rag_core/src/rag_core/store/protocol.py, packages/rag_core/pyproject.toml
Adds application settings, storage dependencies, schema bootstrap SQL, and the storage protocol.
Store implementations
packages/rag_core/src/rag_core/store/client.py, packages/rag_core/src/rag_core/store/memory.py, packages/rag_core/src/rag_core/store/__init__.py
Adds the pgvector-backed store, the in-memory store, and package exports.
Fixtures and store tests
packages/rag_core/tests/conftest.py, packages/rag_core/tests/test_store.py, packages/rag_core/tests/test_db_store.py, packages/rag_core/tests/test_smoke.py
Adds shared fixtures, contract tests, DB-specific integration tests, and a smoke-test docstring update.
Test DB and CI tooling
.env.example, infra/postgres/init.sql, .github/workflows/ci.yaml, conftest.py, pyproject.toml, .pre-commit-config.yaml, .coderabbit.yaml
Adds test DB config, creates the test database, provisions CI Postgres, auto-marks unit tests, updates pytest/mypy config, changes the local mypy hook, and adjusts review settings.

Documentation and review workflow

Layer / File(s) Summary
Contributor guidance
AGENTS.md, packages/rag_core/AGENTS.md, CHEATSHEET.md, README.md, .github/PULL_REQUEST_TEMPLATE.md
Rewrites contributor guidance, adds store-specific conventions, adds a dev cheatsheet, updates README status text, and expands the PR template.
Architecture docs and ADRs
docs/architecture.md, docs/conf.py, docs/decisions/0000-0008.md, docs/decisions/README.md, docs/index.md, docs/reference/index.md
Revises architecture docs, updates ADR headings and listings, adds new ADRs, and expands the API reference navigation.
Review automation
.github/workflows/pr_agent.yaml, .coderabbit.yaml
Adds the PR Agent workflow and updates CodeRabbit review behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several unrelated docs, workflow, and review-tooling changes go beyond the Phase 1 storage/schema scope. Split PR-agent, review config, template, and broad docs/ADR edits into separate PRs, and keep this one focused on storage/schema.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the Phase 1 storage and schema work.
Description check ✅ Passed The description matches the storage, schema, test, and CI changes in this PR.
Linked Issues check ✅ Passed The PR implements the linked storage/schema work, including pgvector CRUD, the user_id seam, and DB-backed integration tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2-phase-1-storage-and-schema

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

@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

🧹 Nitpick comments (4)
packages/rag_core/src/rag_core/store/schema.py (1)

40-42: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider tuning HNSW build parameters.

Default HNSW m/ef_construction may be suboptimal for recall/latency at scale; consider setting explicit values once embedding volume is known.

🤖 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 `@packages/rag_core/src/rag_core/store/schema.py` around lines 40 - 42, The
HNSW index definition in the schema setup uses default build settings, which may
not be ideal at scale. Update the chunks embedding index creation in the schema
module to set explicit HNSW parameters for m and ef_construction once the
expected embedding volume and recall/latency target are known. Keep the change
localized to the index DDL used for idx_chunks_embedding.
packages/rag_core/src/rag_core/store/client.py (1)

89-118: 🎯 Functional Correctness | 🔵 Trivial

TODO may already be resolved by the transaction.

add_chunks runs inside the implicit transaction opened by get_connection(); with self.get_connection() as conn: rolls back on any exception raised during the COPY (e.g., a malformed chunk dict), so the batch is already all-or-nothing per call — matching the "treat batch writes as all-or-nothing" convention. Consider closing out the TODO or clarifying what additional "partial success" scenario it's tracking (e.g., across multiple add_chunks calls).

🤖 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 `@packages/rag_core/src/rag_core/store/client.py` around lines 89 - 118, The
TODO in add_chunks is likely redundant because get_connection() and the COPY
block already make each call transactional and all-or-nothing on errors. Update
add_chunks in the ChunkStore client to either remove or resolve the TODO by
clarifying that partial success is only a concern across multiple calls, not
within a single batch insert. If you keep the note, make it explicit around the
get_connection/copy flow and the add_chunks method so the intended behavior is
clear.
.github/workflows/ci.yaml (1)

10-52: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add explicit least-privilege permissions: block.

Static analysis flags the job as running with default (broad) GITHUB_TOKEN permissions since no permissions: block is declared. Scope it down, e.g. contents: read, especially now that the job runs a service container and integration tests.

🔒 Suggested fix
   checks:
     name: Lint, typecheck, test
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     # Integration tests (rag_core/store) run against a real pgvector instance.
🤖 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 @.github/workflows/ci.yaml around lines 10 - 52, The checks job in the CI
workflow is using the default broad GITHUB_TOKEN scope because no permissions
block is declared. Add an explicit least-privilege permissions section for this
job, keeping only the access needed for actions/checkout and the test steps (for
example, read-only repository contents). Locate the job under the checks name
and update the workflow configuration accordingly.

Source: Linters/SAST tools

.env.example (1)

8-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: env keys out of alphabetical/grouped order per linter.

dotenv-linter flags POSTGRES_TEST_DB/POSTGRES_HOST/POSTGRES_PORT as out of order. Purely cosmetic; reorder if you want a clean lint run.

🤖 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 @.env.example around lines 8 - 10, The PostgreSQL env entries in the
.env.example block are out of the linter’s expected order; reorder the
POSTGRES_* keys in that group to satisfy dotenv-linter. Keep the fix limited to
the env variable declarations so the existing values remain unchanged.

Source: Linters/SAST tools

🤖 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 `@docs/decisions/0005-documentation-tooling.md`:
- Around line 16-22: The ADR text has inconsistent MkDocs config filenames,
which makes the historical note ambiguous. Update the wording in this paragraph
so it matches the filename used elsewhere in the same decision and stays
consistent with the rest of the document. Use the existing ADR prose around the
MkDocs setup to locate the reference and normalize it everywhere it appears.

In `@docs/decisions/0008-per-model-embedding-columns.md`:
- Around line 8-12: The ADR text is out of sync with the implementation because
the embedding dimension is not hardcoded to 1024 in store/schema.py; it is
derived from settings.embedding_dimension at import time. Update the sentence in
this section to describe the actual behavior of the chunks.embedding vector
column and its HNSW index using the settings-derived dimension, so the docs
match the current schema logic.

In `@packages/rag_core/src/rag_core/store/memory.py`:
- Around line 63-68: The read methods in MemoryStore are returning only shallow
copies, so nested mutable fields like metadata and embedding can still be
mutated through the returned object. Update get_document_by_hash and the other
affected read path around the matching copy logic at the referenced section to
return a deep copy of each document instead of dict(doc)/dict(c), using the same
cloning behavior consistently across these accessors so callers cannot modify
the internal store through nested objects.
- Around line 70-85: InMemoryStore.add_chunks currently accepts chunks even when
chunk["document_id"] does not exist, which diverges from the real Store schema
behavior. Update add_chunks to validate the document_id against the in-memory
documents state before storing each chunk, and raise the same kind of integrity
failure the DB-backed store would surface when the document is missing. Keep the
behavior aligned with StoreProtocol and the DB Store implementation so both
stores reject orphan chunks consistently.

---

Nitpick comments:
In @.env.example:
- Around line 8-10: The PostgreSQL env entries in the .env.example block are out
of the linter’s expected order; reorder the POSTGRES_* keys in that group to
satisfy dotenv-linter. Keep the fix limited to the env variable declarations so
the existing values remain unchanged.

In @.github/workflows/ci.yaml:
- Around line 10-52: The checks job in the CI workflow is using the default
broad GITHUB_TOKEN scope because no permissions block is declared. Add an
explicit least-privilege permissions section for this job, keeping only the
access needed for actions/checkout and the test steps (for example, read-only
repository contents). Locate the job under the checks name and update the
workflow configuration accordingly.

In `@packages/rag_core/src/rag_core/store/client.py`:
- Around line 89-118: The TODO in add_chunks is likely redundant because
get_connection() and the COPY block already make each call transactional and
all-or-nothing on errors. Update add_chunks in the ChunkStore client to either
remove or resolve the TODO by clarifying that partial success is only a concern
across multiple calls, not within a single batch insert. If you keep the note,
make it explicit around the get_connection/copy flow and the add_chunks method
so the intended behavior is clear.

In `@packages/rag_core/src/rag_core/store/schema.py`:
- Around line 40-42: The HNSW index definition in the schema setup uses default
build settings, which may not be ideal at scale. Update the chunks embedding
index creation in the schema module to set explicit HNSW parameters for m and
ef_construction once the expected embedding volume and recall/latency target are
known. Keep the change localized to the index DDL used for idx_chunks_embedding.
🪄 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 Plus

Run ID: 0f6dd409-0206-4495-a125-e334537bc025

📥 Commits

Reviewing files that changed from the base of the PR and between 5e9e41e and a626708.

⛔ Files ignored due to path filters (2)
  • docs/assets/architecture.svg is excluded by !**/*.svg
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • .env.example
  • .github/workflows/ci.yaml
  • .pre-commit-config.yaml
  • AGENTS.md
  • CHEATSHEET.md
  • README.md
  • conftest.py
  • docs/architecture.md
  • docs/conf.py
  • docs/decisions/0000-stack.md
  • docs/decisions/0001-rag-retrieval-boundary.md
  • docs/decisions/0002-local-embedding-and-reranking.md
  • docs/decisions/0003-local-single-user-scope.md
  • docs/decisions/0004-cli-batch-ingestion.md
  • docs/decisions/0005-documentation-tooling.md
  • docs/decisions/0006-interface-first-architecture.md
  • docs/decisions/0007-generation-backend.md
  • docs/decisions/0008-per-model-embedding-columns.md
  • docs/decisions/README.md
  • docs/index.md
  • docs/reference/index.md
  • infra/postgres/init.sql
  • packages/rag_core/AGENTS.md
  • packages/rag_core/pyproject.toml
  • packages/rag_core/src/rag_core/config.py
  • packages/rag_core/src/rag_core/store/__init__.py
  • packages/rag_core/src/rag_core/store/client.py
  • packages/rag_core/src/rag_core/store/memory.py
  • packages/rag_core/src/rag_core/store/protocol.py
  • packages/rag_core/src/rag_core/store/schema.py
  • packages/rag_core/tests/conftest.py
  • packages/rag_core/tests/test_db_store.py
  • packages/rag_core/tests/test_smoke.py
  • packages/rag_core/tests/test_store.py
  • pyproject.toml

Comment on lines +16 to +22
`mkdocs.yaml`, a generated API reference page, and a deploy workflow. That work surfaced a
governance problem rather than a technical one: MkDocs 2.0 (announced by a maintainer who took over
the project mid-2024) removes the plugin system entirely, has no migration path for existing
projects, and is currently unlicensed. Since `mkdocstrings` — the entire reason to use this stack —
is itself a plugin, pinning `mkdocs<2` only defers the exposure; it does not remove it. For a
project being written from scratch with no legacy MkDocs investment to protect, that is a reason to
pick a foundation without that single point of failure, not to work around it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the MkDocs filename consistent.

This paragraph now says mkdocs.yaml, but the same ADR still refers to mkdocs.yml later on. Please normalize the filename so the historical note stays unambiguous.

🤖 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 `@docs/decisions/0005-documentation-tooling.md` around lines 16 - 22, The ADR
text has inconsistent MkDocs config filenames, which makes the historical note
ambiguous. Update the wording in this paragraph so it matches the filename used
elsewhere in the same decision and stays consistent with the rest of the
document. Use the existing ADR prose around the MkDocs setup to locate the
reference and normalize it everywhere it appears.

Comment on lines +8 to +12
[ADR 0002](0002-local-embedding-and-reranking.md) pins `bge-m3` and notes that its output dimension
is baked into the pgvector `vector(N)` column and its index, so changing embedders means a re-embed
plus a schema migration — the "main lock-in." Today `chunks` has a single `embedding vector(1024)`
column with one HNSW index, and the dimension is read from the global `settings.embedding_dimension`
at import time inside `store/schema.py`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Match the ADR to the actual schema.

store/schema.py does not hardcode 1024; it derives the dimension from settings.embedding_dimension. Please rewrite this sentence so the current implementation is documented accurately.

🤖 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 `@docs/decisions/0008-per-model-embedding-columns.md` around lines 8 - 12, The
ADR text is out of sync with the implementation because the embedding dimension
is not hardcoded to 1024 in store/schema.py; it is derived from
settings.embedding_dimension at import time. Update the sentence in this section
to describe the actual behavior of the chunks.embedding vector column and its
HNSW index using the settings-derived dimension, so the docs match the current
schema logic.

Comment on lines +63 to +68
def get_document_by_hash(self, file_hash: str) -> dict[str, Any] | None:
"""Return a copy of the document with this file_hash, or None if absent."""
for doc in self.documents.values():
if doc["file_hash"] == file_hash:
return dict(doc)
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Shallow copies leak mutable nested state (metadata, embedding) back to the store.

dict(doc) / dict(c) only shallow-copy the top level; a caller mutating the returned metadata dict or embedding list mutates the internal store directly, unlike the DB-backed Store where each read builds fresh Python objects from the wire.

🛠️ Proposed fix
-        for doc in self.documents.values():
-            if doc["file_hash"] == file_hash:
-                return dict(doc)
+        for doc in self.documents.values():
+            if doc["file_hash"] == file_hash:
+                return copy.deepcopy(doc)
-        return [dict(c) for c in self.chunks.values() if c["document_id"] == document_id]
+        return [copy.deepcopy(c) for c in self.chunks.values() if c["document_id"] == document_id]

Also applies to: 87-89

🤖 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 `@packages/rag_core/src/rag_core/store/memory.py` around lines 63 - 68, The
read methods in MemoryStore are returning only shallow copies, so nested mutable
fields like metadata and embedding can still be mutated through the returned
object. Update get_document_by_hash and the other affected read path around the
matching copy logic at the referenced section to return a deep copy of each
document instead of dict(doc)/dict(c), using the same cloning behavior
consistently across these accessors so callers cannot modify the internal store
through nested objects.

Comment on lines +70 to +85
def add_chunks(
self,
chunks: list[dict[str, Any]],
user_id: UUID | None = None,
) -> None:
"""Store each chunk by id: ``{id, document_id, content, metadata, embedding}``."""
uid = user_id or settings.default_user_id
for chunk in chunks:
self.chunks[chunk["id"]] = {
"id": chunk["id"],
"document_id": chunk["document_id"],
"content": chunk["content"],
"metadata": chunk["metadata"],
"embedding": chunk["embedding"],
"user_id": uid,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

add_chunks doesn't enforce the document_id FK that the real schema requires.

The DB schema declares document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE (schema.py, line 31), so inserting a chunk for a nonexistent document raises an integrity error against the real Store. InMemoryStore.add_chunks has no such check and will silently accept orphan chunks, so the two StoreProtocol implementations diverge on this behavior. Since the store conventions explicitly require both implementations to stay in lockstep for protocol-guaranteed behavior, this gap could let a bug pass the [memory] contract run while the [db] run fails (or vice versa masking a real integrity issue).

🛠️ Proposed fix
     def add_chunks(
         self,
         chunks: list[dict[str, Any]],
         user_id: UUID | None = None,
     ) -> None:
         """Store each chunk by id: ``{id, document_id, content, metadata, embedding}``."""
         uid = user_id or settings.default_user_id
         for chunk in chunks:
+            if chunk["document_id"] not in self.documents:
+                raise ValueError(f"Unknown document_id: {chunk['document_id']!r}")
             self.chunks[chunk["id"]] = {
                 "id": chunk["id"],
                 "document_id": chunk["document_id"],
                 "content": chunk["content"],
                 "metadata": chunk["metadata"],
                 "embedding": chunk["embedding"],
                 "user_id": uid,
             }

Based on path instructions: "when changing the protocol, keep both InMemoryStore and DB Store lockstep using the 3-pass approach."

📝 Committable suggestion

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

Suggested change
def add_chunks(
self,
chunks: list[dict[str, Any]],
user_id: UUID | None = None,
) -> None:
"""Store each chunk by id: ``{id, document_id, content, metadata, embedding}``."""
uid = user_id or settings.default_user_id
for chunk in chunks:
self.chunks[chunk["id"]] = {
"id": chunk["id"],
"document_id": chunk["document_id"],
"content": chunk["content"],
"metadata": chunk["metadata"],
"embedding": chunk["embedding"],
"user_id": uid,
}
def add_chunks(
self,
chunks: list[dict[str, Any]],
user_id: UUID | None = None,
) -> None:
"""Store each chunk by id: ``{id, document_id, content, metadata, embedding}``."""
uid = user_id or settings.default_user_id
for chunk in chunks:
if chunk["document_id"] not in self.documents:
raise ValueError(f"Unknown document_id: {chunk['document_id']!r}")
self.chunks[chunk["id"]] = {
"id": chunk["id"],
"document_id": chunk["document_id"],
"content": chunk["content"],
"metadata": chunk["metadata"],
"embedding": chunk["embedding"],
"user_id": uid,
}
🤖 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 `@packages/rag_core/src/rag_core/store/memory.py` around lines 70 - 85,
InMemoryStore.add_chunks currently accepts chunks even when chunk["document_id"]
does not exist, which diverges from the real Store schema behavior. Update
add_chunks to validate the document_id against the in-memory documents state
before storing each chunk, and raise the same kind of integrity failure the
DB-backed store would surface when the document is missing. Keep the behavior
aligned with StoreProtocol and the DB Store implementation so both stores reject
orphan chunks consistently.

Source: Path instructions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/PULL_REQUEST_TEMPLATE.md (1)

11-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make commit hashes optional in the walkthrough.

Requiring short hashes here makes the template brittle after rebases or squash merges and adds manual lookup work for authors. Keep the facet-based bullets, but treat hashes as optional metadata instead of a hard requirement.

♻️ Proposed tweak
- 1. Walkthrough: ... cites it traces to by short-hash ...
+ 1. Walkthrough: ... names the facet it changes; add a short-hash only when it helps tie the bullet back to a specific commit.
🤖 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 @.github/PULL_REQUEST_TEMPLATE.md around lines 11 - 15, Update the
walkthrough guidance in the PR template so short commit hashes are optional
rather than required. In the bullet describing how to cite commits, keep the
focus on the unique or non-obvious facets of the change and allow authors to
omit hashes when they are unavailable or unstable after rebases/squash merges.
Adjust the wording in the template text so it no longer instructs users to
always pull hashes from the commit log, while still permitting them as optional
metadata.
🤖 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 @.github/PULL_REQUEST_TEMPLATE.md:
- Around line 11-15: Update the walkthrough guidance in the PR template so short
commit hashes are optional rather than required. In the bullet describing how to
cite commits, keep the focus on the unique or non-obvious facets of the change
and allow authors to omit hashes when they are unavailable or unstable after
rebases/squash merges. Adjust the wording in the template text so it no longer
instructs users to always pull hashes from the commit log, while still
permitting them as optional metadata.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b4bf7e3c-691e-4219-9583-d8f511decaf9

📥 Commits

Reviewing files that changed from the base of the PR and between a626708 and c1d6ba8.

📒 Files selected for processing (2)
  • .github/PULL_REQUEST_TEMPLATE.md
  • packages/rag_core/src/rag_core/store/schema.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rag_core/src/rag_core/store/schema.py

@sid-ak

sid-ak commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (3) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 6 rules

Grey Divider


Action required

1. chunks.embedding lacks model id 📎 Requirement gap ⚙ Maintainability
Description
The new schema stores embeddings without any explicit embedder model identifier or recorded
dimension, so embeddings are not attributable to a model and future embedder swaps require in-place
interpretation/migration. This breaks the additive-swap requirement for embeddings.
Code

packages/rag_core/src/rag_core/store/schema.py[34]

+    embedding vector({settings.embedding_dimension}) NOT NULL,
Evidence
PR Compliance ID 1790485 requires explicitly recording embedder model identifier and vector
dimension. The new schema defines only embedding vector({settings.embedding_dimension}) with no
model identifier column/table and no explicit persisted dimension metadata.

Record embedder model identifier and vector dimension explicitly to support additive embedder swaps
packages/rag_core/src/rag_core/store/schema.py[28-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The schema stores embeddings in `chunks.embedding` but does not record an embedder model identifier or the embedding dimension explicitly alongside the stored vectors.

## Issue Context
Compliance requires embeddings be attributable to a model and dimension so embedders can be swapped additively without ambiguous interpretation of existing vectors.

## Fix Focus Areas
- packages/rag_core/src/rag_core/store/schema.py[28-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Single embedding column/index 📎 Requirement gap ⚙ Maintainability
Description
The schema creates only one embedding vector column and one HNSW index, so multiple embedders
cannot coexist additively. This makes embedder changes destructive (requires rewriting/reindexing
the shared column).
Code

packages/rag_core/src/rag_core/store/schema.py[R34-42]

+    embedding vector({settings.embedding_dimension}) NOT NULL,
+    tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
+    user_id UUID NOT NULL,
+    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- HNSW index for vector similarity search (cosine distance)
+CREATE INDEX IF NOT EXISTS idx_chunks_embedding ON chunks
+USING hnsw (embedding vector_cosine_ops);
Evidence
PR Compliance ID 1790486 requires per-embedder vector columns and indexes. The new schema defines
only one embedding vector(...) column and a single HNSW index idx_chunks_embedding over that
shared column.

Maintain per-embedder embedding columns and indexes (one vector(N) column + index per active model)
packages/rag_core/src/rag_core/store/schema.py[28-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The schema defines a single shared `embedding vector(N)` column and one vector index, rather than per-embedder columns + indexes.

## Issue Context
Compliance requires additive embedder swaps by maintaining one vector column + index per active model, keyed/tagged by a model identifier.

## Fix Focus Areas
- packages/rag_core/src/rag_core/store/schema.py[28-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. No embeddings table/migrations 📎 Requirement gap ≡ Correctness
Description
The Phase 1 storage layer is incomplete because it omits the required embeddings table and
explicitly defers a versioned migrations mechanism, while the store interface and implementations
only provide CRUD for documents and chunks with no embedding CRUD surface. This violates the Phase 1
storage foundation and compliance requirements.
Code

packages/rag_core/src/rag_core/store/schema.py[R1-38]

+"""SQL schema for documents and chunks with pgvector support.
+
+Schema evolution is handled by ``Store.initialize_schema`` running this idempotent
+``CREATE ... IF NOT EXISTS`` bootstrap. A versioned migration mechanism is deferred until the
+first breaking change actually needs one — most likely the re-embed / ``vector(N)`` dimension
+change tracked by issue #15. Until then there is no existing data to migrate.
+"""
+
+from rag_core.config import settings
+
+# Extension and Tables
+SCHEMA_SQL = f"""
+-- Ensure vector extension exists
+CREATE EXTENSION IF NOT EXISTS vector;
+
+-- Documents table: Tracks source files
+CREATE TABLE IF NOT EXISTS documents (
+    id UUID PRIMARY KEY,
+    filename TEXT NOT NULL,
+    -- UNIQUE makes re-ingesting the same file idempotent (upsert target below) and
+    -- doubles as the lookup index for get_document_by_hash. Single-user scope keys on
+    -- file_hash alone; multi-user would migrate this to (user_id, file_hash).
+    file_hash TEXT NOT NULL UNIQUE,
+    user_id UUID NOT NULL,
+    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Chunks table: Stores semantic segments and embeddings
+CREATE TABLE IF NOT EXISTS chunks (
+    id UUID PRIMARY KEY,
+    document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
+    content TEXT NOT NULL,
+    metadata JSONB NOT NULL DEFAULT '{{}}',
+    embedding vector({settings.embedding_dimension}) NOT NULL,
+    tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
+    user_id UUID NOT NULL,
+    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
Evidence
PR Compliance IDs 1790491 and 1790492 require a pgvector-backed schema including documents,
chunks, and embeddings tables with migrations, and CRUD operations for
documents/chunks/embeddings. The cited schema file defines only documents and chunks and notes
migrations are deferred (showing the missing embeddings table and migration mechanism), while the
new StoreProtocol and related store code define methods for documents and chunks but no APIs for
embedding CRUD, demonstrating the required embeddings functionality is absent across both schema and
storage interfaces/implementations.

Implement pgvector storage schema and migrations for documents/chunks/embeddings with a user_id seam
packages/rag_core/src/rag_core/store/schema.py[3-6]
packages/rag_core/src/rag_core/store/schema.py[16-38]
packages/rag_core/src/rag_core/store/protocol.py[17-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Phase 1 storage does not meet compliance requirements because the schema only defines `documents` and `chunks` while deferring a versioned migrations mechanism, and the storage seam (`StoreProtocol`) plus concrete store implementations expose CRUD only for documents/chunks with no corresponding CRUD APIs for embeddings.

## Issue Context
Compliance requires a pgvector-backed storage foundation that includes `documents`, `chunks`, and `embeddings` tables, supports migrations that apply cleanly, and provides CRUD operations for documents, chunks, and embeddings through the store interface and its implementations.

## Fix Focus Areas
- packages/rag_core/src/rag_core/store/schema.py[1-38]
- packages/rag_core/src/rag_core/store/client.py[28-32]
- packages/rag_core/src/rag_core/store/protocol.py[17-57]
- packages/rag_core/src/rag_core/store/client.py[39-128]
- packages/rag_core/src/rag_core/store/memory.py[26-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Orphan chunks allowed 🐞 Bug ≡ Correctness
Description
InMemoryStore.add_chunks() accepts chunks whose document_id doesn’t exist, but the Postgres
schema enforces a foreign key on chunks.document_id. This can let unit tests pass against
InMemoryStore while real ingestion fails against Store with FK violations.
Code

packages/rag_core/src/rag_core/store/memory.py[R70-85]

+    def add_chunks(
+        self,
+        chunks: list[dict[str, Any]],
+        user_id: UUID | None = None,
+    ) -> None:
+        """Store each chunk by id: ``{id, document_id, content, metadata, embedding}``."""
+        uid = user_id or settings.default_user_id
+        for chunk in chunks:
+            self.chunks[chunk["id"]] = {
+                "id": chunk["id"],
+                "document_id": chunk["document_id"],
+                "content": chunk["content"],
+                "metadata": chunk["metadata"],
+                "embedding": chunk["embedding"],
+                "user_id": uid,
+            }
Evidence
The DB schema enforces chunks.document_id as an FK to documents(id), so inserting a chunk for a
nonexistent document will fail in Store but currently succeeds in InMemoryStore because it
stores rows unconditionally.

packages/rag_core/src/rag_core/store/schema.py[29-33]
packages/rag_core/src/rag_core/store/memory.py[70-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`InMemoryStore.add_chunks()` currently stores chunks without verifying that the referenced `document_id` exists. The DB-backed `Store` will reject the same data because `chunks.document_id` has an FK to `documents(id)`.

### Issue Context
This mismatch reduces the value of DB-free unit tests: ingestion logic that forgets to create the document first will look correct in memory tests but fail in integration/prod.

### Fix Focus Areas
- packages/rag_core/src/rag_core/store/memory.py[70-85]
- packages/rag_core/src/rag_core/store/schema.py[29-33]

### Suggested fix
In `InMemoryStore.add_chunks`, for each chunk:
- Check `chunk["document_id"] in self.documents`.
- If missing, raise a `KeyError` or `ValueError` (pick one and document it) so unit tests catch the same class of bug the DB would.
Optionally add a contract test asserting both implementations reject orphan chunks (so they can’t drift again).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Root AGENTS.md leaks paths 📘 Rule violation ⚙ Maintainability
Description
Root-level AGENTS.md includes concrete, package-specific implementation references (e.g.,
rag_core and store/schema.py) rather than staying purely cross-cutting. This reduces clarity of
global guidance and should be moved to the package-scoped AGENTS.md.
Code

AGENTS.md[R87-89]

- Schema changes need a fresh DB: `initialize_schema()` is `CREATE ... IF NOT EXISTS` and will not
  retrofit constraints, so run `docker compose down -v && docker compose up -d` after editing
  `store/schema.py`.
Evidence
PR Compliance ID 1790481 prohibits package-specific implementation details in the root AGENTS.md.
The updated root file references the rag_core module as an example and includes schema-change
instructions pointing to store/schema.py, which are package-specific details.

Rule 1790481: Root AGENTS.md must not contain package-specific implementation details
AGENTS.md[39-40]
AGENTS.md[87-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The root `AGENTS.md` contains package-specific implementation details that should live in per-package `AGENTS.md` files.

## Issue Context
Compliance requires the root `AGENTS.md` to avoid concrete instructions scoped to a single package.

## Fix Focus Areas
- AGENTS.md[39-40]
- AGENTS.md[87-89]
- packages/rag_core/AGENTS.md[24-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Unordered chunk retrieval 🐞 Bug ☼ Reliability
Description
Store.get_chunks_by_document() does not ORDER BY, so Postgres can return chunks in an arbitrary
order, while InMemoryStore returns dict insertion order. This creates inconsistent behavior across
implementations and can lead to nondeterministic results if callers assume stable ordering.
Code

packages/rag_core/src/rag_core/store/client.py[R120-127]

+    def get_chunks_by_document(self, document_id: UUID) -> list[dict[str, Any]]:
+        """Return every chunk row belonging to ``document_id``, or ``[]`` if none exist."""
+        with self.get_connection() as conn:
+            cur = conn.execute(
+                "SELECT id, document_id, content, metadata, embedding, user_id "
+                "FROM chunks WHERE document_id = %s",
+                (document_id,),
+            )
Evidence
The DB query lacks ORDER BY, so result order is not guaranteed by SQL; the in-memory
implementation’s order is driven by Python dict iteration order, which is a different (and more
stable) behavior.

packages/rag_core/src/rag_core/store/client.py[120-128]
packages/rag_core/src/rag_core/store/memory.py[87-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`get_chunks_by_document()` currently returns rows with no defined ordering in the DB-backed implementation, while the in-memory fake returns insertion order. This can lead to nondeterminism and implementation-dependent behavior.

### Issue Context
Even if no current caller relies on ordering, adding deterministic ordering now prevents subtle future bugs and flaky tests.

### Fix Focus Areas
- packages/rag_core/src/rag_core/store/client.py[120-128]
- packages/rag_core/src/rag_core/store/memory.py[87-89]

### Suggested fix
Pick and document an ordering contract, then implement it in both stores:
- Minimal: `ORDER BY id` (stable but semantically weak).
- Better: add/return an explicit sequence field (e.g., `chunk_index` in metadata or a dedicated column in a later migration) and order by it.
Update `InMemoryStore.get_chunks_by_document()` to sort accordingly (not rely on dict iteration order).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
content TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{{}}',
embedding vector({settings.embedding_dimension}) NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. chunks.embedding lacks model id 📎 Requirement gap ⚙ Maintainability

The new schema stores embeddings without any explicit embedder model identifier or recorded
dimension, so embeddings are not attributable to a model and future embedder swaps require in-place
interpretation/migration. This breaks the additive-swap requirement for embeddings.
Agent Prompt
## Issue description
The schema stores embeddings in `chunks.embedding` but does not record an embedder model identifier or the embedding dimension explicitly alongside the stored vectors.

## Issue Context
Compliance requires embeddings be attributable to a model and dimension so embedders can be swapped additively without ambiguous interpretation of existing vectors.

## Fix Focus Areas
- packages/rag_core/src/rag_core/store/schema.py[28-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +34 to +42
embedding vector({settings.embedding_dimension}) NOT NULL,
tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- HNSW index for vector similarity search (cosine distance)
CREATE INDEX IF NOT EXISTS idx_chunks_embedding ON chunks
USING hnsw (embedding vector_cosine_ops);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Single embedding column/index 📎 Requirement gap ⚙ Maintainability

The schema creates only one embedding vector column and one HNSW index, so multiple embedders
cannot coexist additively. This makes embedder changes destructive (requires rewriting/reindexing
the shared column).
Agent Prompt
## Issue description
The schema defines a single shared `embedding vector(N)` column and one vector index, rather than per-embedder columns + indexes.

## Issue Context
Compliance requires additive embedder swaps by maintaining one vector column + index per active model, keyed/tagged by a model identifier.

## Fix Focus Areas
- packages/rag_core/src/rag_core/store/schema.py[28-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1 to +38
"""SQL schema for documents and chunks with pgvector support.

Schema evolution is handled by ``Store.initialize_schema`` running this idempotent
``CREATE ... IF NOT EXISTS`` bootstrap. A versioned migration mechanism is deferred until the
first breaking change actually needs one — most likely the re-embed / ``vector(N)`` dimension
change tracked by issue #15. Until then there is no existing data to migrate.
"""

from rag_core.config import settings

# Extension and Tables
SCHEMA_SQL = f"""
-- Ensure vector extension exists
CREATE EXTENSION IF NOT EXISTS vector;

-- Documents table: Tracks source files
CREATE TABLE IF NOT EXISTS documents (
id UUID PRIMARY KEY,
filename TEXT NOT NULL,
-- UNIQUE makes re-ingesting the same file idempotent (upsert target below) and
-- doubles as the lookup index for get_document_by_hash. Single-user scope keys on
-- file_hash alone; multi-user would migrate this to (user_id, file_hash).
file_hash TEXT NOT NULL UNIQUE,
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Chunks table: Stores semantic segments and embeddings
CREATE TABLE IF NOT EXISTS chunks (
id UUID PRIMARY KEY,
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
content TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{{}}',
embedding vector({settings.embedding_dimension}) NOT NULL,
tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT 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.

Action required

3. No embeddings table/migrations 📎 Requirement gap ≡ Correctness

The Phase 1 storage layer is incomplete because it omits the required embeddings table and
explicitly defers a versioned migrations mechanism, while the store interface and implementations
only provide CRUD for documents and chunks with no embedding CRUD surface. This violates the Phase 1
storage foundation and compliance requirements.
Agent Prompt
## Issue description
Phase 1 storage does not meet compliance requirements because the schema only defines `documents` and `chunks` while deferring a versioned migrations mechanism, and the storage seam (`StoreProtocol`) plus concrete store implementations expose CRUD only for documents/chunks with no corresponding CRUD APIs for embeddings.

## Issue Context
Compliance requires a pgvector-backed storage foundation that includes `documents`, `chunks`, and `embeddings` tables, supports migrations that apply cleanly, and provides CRUD operations for documents, chunks, and embeddings through the store interface and its implementations.

## Fix Focus Areas
- packages/rag_core/src/rag_core/store/schema.py[1-38]
- packages/rag_core/src/rag_core/store/client.py[28-32]
- packages/rag_core/src/rag_core/store/protocol.py[17-57]
- packages/rag_core/src/rag_core/store/client.py[39-128]
- packages/rag_core/src/rag_core/store/memory.py[26-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread AGENTS.md
Comment on lines 87 to 89
- Schema changes need a fresh DB: `initialize_schema()` is `CREATE ... IF NOT EXISTS` and will not
retrofit constraints, so run `docker compose down -v && docker compose up -d` after editing
`store/schema.py`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Root agents.md leaks paths 📘 Rule violation ⚙ Maintainability

Root-level AGENTS.md includes concrete, package-specific implementation references (e.g.,
rag_core and store/schema.py) rather than staying purely cross-cutting. This reduces clarity of
global guidance and should be moved to the package-scoped AGENTS.md.
Agent Prompt
## Issue description
The root `AGENTS.md` contains package-specific implementation details that should live in per-package `AGENTS.md` files.

## Issue Context
Compliance requires the root `AGENTS.md` to avoid concrete instructions scoped to a single package.

## Fix Focus Areas
- AGENTS.md[39-40]
- AGENTS.md[87-89]
- packages/rag_core/AGENTS.md[24-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +70 to +85
def add_chunks(
self,
chunks: list[dict[str, Any]],
user_id: UUID | None = None,
) -> None:
"""Store each chunk by id: ``{id, document_id, content, metadata, embedding}``."""
uid = user_id or settings.default_user_id
for chunk in chunks:
self.chunks[chunk["id"]] = {
"id": chunk["id"],
"document_id": chunk["document_id"],
"content": chunk["content"],
"metadata": chunk["metadata"],
"embedding": chunk["embedding"],
"user_id": uid,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Orphan chunks allowed 🐞 Bug ≡ Correctness

InMemoryStore.add_chunks() accepts chunks whose document_id doesn’t exist, but the Postgres
schema enforces a foreign key on chunks.document_id. This can let unit tests pass against
InMemoryStore while real ingestion fails against Store with FK violations.
Agent Prompt
### Issue description
`InMemoryStore.add_chunks()` currently stores chunks without verifying that the referenced `document_id` exists. The DB-backed `Store` will reject the same data because `chunks.document_id` has an FK to `documents(id)`.

### Issue Context
This mismatch reduces the value of DB-free unit tests: ingestion logic that forgets to create the document first will look correct in memory tests but fail in integration/prod.

### Fix Focus Areas
- packages/rag_core/src/rag_core/store/memory.py[70-85]
- packages/rag_core/src/rag_core/store/schema.py[29-33]

### Suggested fix
In `InMemoryStore.add_chunks`, for each chunk:
- Check `chunk["document_id"] in self.documents`.
- If missing, raise a `KeyError` or `ValueError` (pick one and document it) so unit tests catch the same class of bug the DB would.
Optionally add a contract test asserting both implementations reject orphan chunks (so they can’t drift again).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +120 to +127
def get_chunks_by_document(self, document_id: UUID) -> list[dict[str, Any]]:
"""Return every chunk row belonging to ``document_id``, or ``[]`` if none exist."""
with self.get_connection() as conn:
cur = conn.execute(
"SELECT id, document_id, content, metadata, embedding, user_id "
"FROM chunks WHERE document_id = %s",
(document_id,),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Unordered chunk retrieval 🐞 Bug ☼ Reliability

Store.get_chunks_by_document() does not ORDER BY, so Postgres can return chunks in an arbitrary
order, while InMemoryStore returns dict insertion order. This creates inconsistent behavior across
implementations and can lead to nondeterministic results if callers assume stable ordering.
Agent Prompt
### Issue description
`get_chunks_by_document()` currently returns rows with no defined ordering in the DB-backed implementation, while the in-memory fake returns insertion order. This can lead to nondeterminism and implementation-dependent behavior.

### Issue Context
Even if no current caller relies on ordering, adding deterministic ordering now prevents subtle future bugs and flaky tests.

### Fix Focus Areas
- packages/rag_core/src/rag_core/store/client.py[120-128]
- packages/rag_core/src/rag_core/store/memory.py[87-89]

### Suggested fix
Pick and document an ordering contract, then implement it in both stores:
- Minimal: `ORDER BY id` (stable but semantically weak).
- Better: add/return an explicit sequence field (e.g., `chunk_index` in metadata or a dedicated column in a later migration) and order by it.
Update `InMemoryStore.get_chunks_by_document()` to sort accordingly (not rely on dict iteration order).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@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

🧹 Nitpick comments (1)
.coderabbit.yaml (1)

8-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drafts + wildcard branches may increase review noise.

Combining auto_review.drafts: true with base_branches: [".*"] triggers full reviews on every push to draft PRs across all branches, which may generate reviews on early, unstable WIP commits.

🤖 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 @.coderabbit.yaml around lines 8 - 13, The auto_review configuration is too
broad because drafts are enabled together with a wildcard base branch match,
causing reviews on unstable WIP pushes. Update the .coderabbit.yaml auto_review
settings so the review trigger is narrower by either disabling drafts or
restricting base_branches in the auto_review block, keeping the change localized
to the auto_review.enabled, auto_review.drafts, and auto_review.base_branches
keys.
🤖 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 @.github/workflows/pr_agent.yaml:
- Around line 4-5: The pr_agent workflow trigger only covers opened, reopened,
and ready_for_review PR events, so updated PRs with new commits will not rerun.
Update the pull_request event in pr_agent.yaml to include synchronize so the
workflow matches the “new/updated PRs” behavior described in the job comments;
check the trigger block and any related workflow documentation text near the
auto-run description.
- Around line 6-10: The workflow trigger is too broad because pr_agent_job still
runs for all issue_comment events, not just pull request comments. Update the
github event filtering in the workflow so the job only starts when the comment
is on a PR, using the existing issue_comment trigger with a PR-specific
condition in pr_agent_job (or equivalent PR-only gate) while keeping the Bot
exclusion intact.
- Line 20: The workflow currently uses the third-party action
the-pr-agent/pr-agent via a mutable ref, which should be pinned to an immutable
version. Update the uses entry in the pr_agent workflow to reference a specific
release tag or commit SHA instead of main so the action version cannot change
unexpectedly. Keep the change limited to the existing action reference and
ensure the chosen ref is stable and reproducible.
- Around line 12-16: The GitHub Actions permissions block includes an unused
checks: write scope, which should be removed unless github.publish_as_check_run
is enabled. Update the permissions in pr_agent.yaml to keep only the required
scopes used by the workflow, and leave the existing issues, pull-requests, and
contents permissions intact.

---

Nitpick comments:
In @.coderabbit.yaml:
- Around line 8-13: The auto_review configuration is too broad because drafts
are enabled together with a wildcard base branch match, causing reviews on
unstable WIP pushes. Update the .coderabbit.yaml auto_review settings so the
review trigger is narrower by either disabling drafts or restricting
base_branches in the auto_review block, keeping the change localized to the
auto_review.enabled, auto_review.drafts, and auto_review.base_branches keys.
🪄 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 Plus

Run ID: 0882d275-180a-4cf5-bea8-04523b179aaf

📥 Commits

Reviewing files that changed from the base of the PR and between c1d6ba8 and a1410ac.

📒 Files selected for processing (2)
  • .coderabbit.yaml
  • .github/workflows/pr_agent.yaml

Comment thread .github/workflows/pr_agent.yaml
Comment thread .github/workflows/pr_agent.yaml Outdated
Comment on lines +6 to +10
issue_comment:

jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Gate comment runs to PR comments.

Line 6 triggers on every issue comment, while Line 10 only excludes bots. That starts the action for ordinary issue comments too, with write-scoped token access and the Gemini secret available to the action.

Suggested fix
-    if: ${{ github.event.sender.type != 'Bot' }}
+    if: ${{ github.event.sender.type != 'Bot' && (github.event_name != 'issue_comment' || github.event.issue.pull_request != null) }}
📝 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
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' && (github.event_name != 'issue_comment' || github.event.issue.pull_request != null) }}
🤖 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 @.github/workflows/pr_agent.yaml around lines 6 - 10, The workflow trigger is
too broad because pr_agent_job still runs for all issue_comment events, not just
pull request comments. Update the github event filtering in the workflow so the
job only starts when the comment is on a PR, using the existing issue_comment
trigger with a PR-specific condition in pr_agent_job (or equivalent PR-only
gate) while keeping the Bot exclusion intact.

Comment thread .github/workflows/pr_agent.yaml
name: Run PR Agent (Gemini free tier) on pull requests and comments
steps:
- name: PR Agent action step
uses: the-pr-agent/pr-agent@main

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect available immutable refs for PR Agent before choosing a pinned version.
git ls-remote --tags https://github.com/the-pr-agent/pr-agent.git | tail -n 20
git ls-remote https://github.com/the-pr-agent/pr-agent.git refs/heads/main

Repository: sid-ak/study_assistant

Length of output: 1372


🏁 Script executed:

sed -n '1,80p' .github/workflows/pr_agent.yaml | cat -n

Repository: sid-ak/study_assistant

Length of output: 1373


Pin the-pr-agent/pr-agent to an immutable ref. @main is mutable for a third-party action; use a release tag or commit SHA instead.

🤖 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 @.github/workflows/pr_agent.yaml at line 20, The workflow currently uses the
third-party action the-pr-agent/pr-agent via a mutable ref, which should be
pinned to an immutable version. Update the uses entry in the pr_agent workflow
to reference a specific release tag or commit SHA instead of main so the action
version cannot change unexpectedly. Keep the change limited to the existing
action reference and ensure the chosen ref is stable and reproducible.

@sid-ak

sid-ak commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

35 - Fully compliant

Compliant requirements:

  • Abandon Anthropic as the primary generation model.
  • Move towards open-source and vendor-neutral generation.

34 - Fully compliant

Compliant requirements:

  • Establish a documentation site.
  • Integrate API reference generation.

2 - Fully compliant

Compliant requirements:

  • Implement pgvector schema for documents, chunks, embeddings with user_id seam.
  • Implement schema migrations (idempotent creation).
  • Implement CRUD operations for documents and chunks.
  • Add integration tests against the compose DB for insert/query chunks and vector column round-trips.

15 - Not compliant

Non-compliant requirements:

  • Implement dimension as an explicit parameter for embedders.
  • Implement per-model embedding columns.
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Architectural Drift

The AGENTS.md file explicitly notes a "Known drift from ADRs" regarding ADR 0008 (Per-Model Embedding Columns). The config.py still reads a single global settings.embedding_dimension, and schema.py builds one shared embedding vector(N) column. This means the architectural decision to support additive embedder swaps is not yet reflected in the code, which could lead to a more complex migration when this feature is eventually implemented.

- **ADR 0008 (per-model embedding columns):** `config.py` still reads a single global
  `settings.embedding_dimension`, and `schema.py` still builds one shared `embedding vector(N)`
  column — the pre-0008 shape ADR 0008 exists to replace. Explicit per-call dimensions and
  per-model columns aren't implemented; tracked in

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 1 — Storage + schema (rag_core/store)

1 participant