Skip to content

Add RAG ingestion with vector embeddings#5318

Merged
renecannao merged 1 commit into
sysown:v4.0_rag_ingest_2from
rahim-kanji:v4.0_rag_ingest
Jan 23, 2026
Merged

Add RAG ingestion with vector embeddings#5318
renecannao merged 1 commit into
sysown:v4.0_rag_ingest_2from
rahim-kanji:v4.0_rag_ingest

Conversation

@rahim-kanji

@rahim-kanji rahim-kanji commented Jan 23, 2026

Copy link
Copy Markdown
Collaborator

Implements RAG document ingestion with:

  • Vector embeddings via sqlite-vec (stub/OpenAI providers)
  • FTS5 full-text search index
  • Incremental sync with watermark tracking (rag_sync_state)
  • Character-based chunking with overlap
  • Comprehensive test suite (test_rag_ingest.sh)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added RAG ingestion workflow supporting document ingestion from MySQL sources with incremental syncing capabilities
    • Introduced embeddings support with configurable providers (stub and OpenAI-compatible APIs)
    • Added vector search capabilities for ingested documents
    • Enhanced catalog search with full-text search (FTS5) ranking for improved relevance
  • Improvements

    • Optimized search results ordering by relevance and recency

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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.

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

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Introduces a RAG ingestion system that synchronizes MySQL data to SQLite with optional embedding generation, incremental sync tracking, and vector extension support. Updates catalog search to use FTS5 with BM25 ranking. Adds build orchestration for RAG components and comprehensive end-to-end test workflow.

Changes

Cohort / File(s) Summary
Build Orchestration
Makefile
Added two PHONY targets: rag_ingest (depends on build_deps, invokes make in RAG_POC with CC/CXX/CXXFLAGS) and rag_ingest_clean (runs make clean in RAG_POC).
RAG Ingestion Build
RAG_POC/Makefile
New C++ project Makefile with g++ compiler settings (C++17 standard, optimization), include paths for JSON/MariaDB/SQLite, and build rules for rag_ingest binary linking against MariaDB client, OpenSSL, crypt, dl, and pthread.
RAG Ingestion Core
RAG_POC/rag_ingest.cpp
Major new implementation introducing: (1) abstract EmbeddingProvider interface with StubEmbeddingProvider and OpenAIEmbeddingProvider (curl-based REST); (2) SyncCursor struct for incremental sync state; (3) EmbeddingConfig expansion with provider, api_base, api_key, batch_size, timeout_ms; (4) incremental sync functions (load/parse cursor, build filter, update cursor); (5) curl initialization/cleanup; (6) vec extension loading hook; (7) embedding generation and insertion into rag_vec_chunks per chunk.
Test Data & Configuration
RAG_POC/sample_mysql.sql
RAG_POC/sample_sqlite.sql
MySQL test dataset defining rag_test database with posts table (10 rows); SQLite configuration inserting single rag_sources row with MySQL connection details, JSON doc_map, chunking, and embedding settings.
End-to-End Testing
RAG_POC/test_rag_ingest.sh
Comprehensive shell test script with environment setup, helper functions (run_sqlite, apply_schema_and_source, import_mysql_seed, etc.), and five phased tests covering: chunking off/on, FTS assertions, watermark filtering, incremental sync behaviors, where-clause filtering, and optional OpenAI-compatible embeddings validation.
Catalog Search Refactor
lib/MySQL_Catalog.cpp
Refactored search() function to use FTS5-based MATCH query with dedicated catalog_fts join; sorts by BM25 relevance then updated_at; introduces quote-escaping helper; returns structured JSON with explicit column mapping (schema, kind, key, document, tags, links); adds robust empty-query handling.
Resource Cleanup
lib/ProxySQL_MCP_Server.cpp
Added MySQL Tool Handler cleanup in destructor: logs message, deletes handler, sets to NULL (before existing Config Tool Handler cleanup).

Sequence Diagram

sequenceDiagram
    participant RAG as RAG Ingest
    participant MySQL as MySQL<br/>(Source)
    participant Chunk as Chunker
    participant Embed as Embedding<br/>Provider
    participant SQLite as SQLite<br/>(rag_vec_chunks)

    RAG->>SQLite: Load sync cursor
    RAG->>MySQL: SELECT with incremental filter
    MySQL-->>RAG: Row batch
    RAG->>Chunk: Create document chunks
    Chunk-->>RAG: Chunk list
    RAG->>Embed: embed(chunks)
    alt OpenAI Provider
        Embed->>Embed: Build HTTP request
        Embed->>Embed: Call curl OpenAI API
        Embed-->>Embed: Parse embeddings
    else Stub Provider
        Embed-->>Embed: Generate pseudo embeddings
    end
    Embed-->>RAG: Vector embeddings
    RAG->>SQLite: INSERT rag_vec_chunks<br/>(doc_id, chunk_text, embedding)
    RAG->>SQLite: UPDATE sync cursor
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A RAG hops in with vectors bright,
Embedding MySQL chunks just right,
FTS5 hops search with BM25 flair,
Sync cursors track what's fair,
OpenAI or stubs—the choice is there! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add RAG ingestion with vector embeddings' accurately summarizes the main feature added across multiple files, including RAG ingest implementation, vector embeddings support, and related infrastructure.

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


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

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @rahim-kanji, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new RAG ingestion mechanism, enabling the system to ingest documents, generate vector embeddings, and create a full-text search index. This enhancement significantly improves the system's ability to perform semantic search and retrieve relevant information from large document collections. The implementation includes support for incremental synchronization, chunking, and various embedding providers, along with a comprehensive test suite.

Highlights

  • RAG Ingestion Implementation: Implements RAG (Retrieval-Augmented Generation) document ingestion with vector embeddings and full-text search capabilities.
  • Vector Embeddings: Utilizes sqlite-vec for vector embeddings, with support for stub and OpenAI providers.
  • Full-Text Search: Includes FTS5 full-text search index for efficient document retrieval.
  • Incremental Sync: Features incremental sync with watermark tracking (rag_sync_state) to efficiently update the index.
  • Chunking: Employs character-based chunking with overlap to improve retrieval accuracy.
  • Comprehensive Testing: Includes a comprehensive test suite (test_rag_ingest.sh) to ensure functionality and stability.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a RAG (Retrieval-Augmented Generation) ingester PoC, including changes to the Makefile to build the RAG ingester, a new Makefile and source file (rag_ingest.cpp) in the RAG_POC directory, sample SQL files for MySQL and SQLite, and a test script (test_rag_ingest.sh). The code changes in rag_ingest.cpp involve including necessary headers, defining functions for SQLite interaction, JSON parsing, and embedding handling (including stub and OpenAI embedding providers), and implementing the core ingestion logic with watermark support. Review comments highlight the importance of using $(CXXFLAGS) directly in the Makefile, potential portability issues with the MySQL header path, the need for libcrypt availability for the sha256_crypt_r weak alias, potential performance improvements by batching embedding inputs, providing more explicit error feedback for missing query parameters in catalog searches, verifying the ranking behavior of the bm25 function, and simplifying parameter binding in the MySQL_Catalog::search function by removing unnecessary variables. Additionally, the cleanup of mysql_tool_handler in the destructor and the correct implementation of escape_sql lambda for preventing SQL injection are noted as crucial improvements.

Comment thread lib/MySQL_Catalog.cpp
Comment on lines +297 to +307
auto escape_sql = [](const std::string& str) -> std::string {
std::string result;
result.reserve(str.length() * 2); // Reserve space for potential escaping
for (char c : str) {
if (c == '\'') {
result += '\''; // Escape single quote by doubling it
}
result += c;
}
return result;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The escape_sql lambda is correctly implemented to prevent SQL injection for FTS5 MATCH clauses. This is a critical security improvement, ensuring that user-provided queries are safely handled before being incorporated into the SQL statement.

Comment on lines +220 to +224
// Clean up MySQL Tool Handler
if (handler->mysql_tool_handler) {
proxy_info("Cleaning up MySQL Tool Handler...\n");
delete handler->mysql_tool_handler;
handler->mysql_tool_handler = 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.

critical

Adding cleanup for mysql_tool_handler in the destructor is a crucial improvement for resource management, preventing potential memory leaks when the ProxySQL_MCP_Server is destroyed.

Comment thread Makefile Outdated
# RAG ingester (PoC)
.PHONY: rag_ingest
rag_ingest: build_deps
cd RAG_POC && ${MAKE} CC=${CC} CXX=${CXX} OPTZ="${O2} -ggdb" CXXFLAGS="${O2} -ggdb"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Consider using $(CXXFLAGS) directly here instead of re-specifying OPTZ and CXXFLAGS with "${O2} -ggdb". This would make the build process more consistent with the overall Makefile's philosophy of setting CXXFLAGS once and reusing it, and easier to change build flags globally.

For example, if CXXFLAGS is already defined as "-std=c++17 -O2 -ggdb", then CXXFLAGS="${O2} -ggdb" would override it unnecessarily or potentially lead to unexpected behavior if O2 is later changed.

cd RAG_POC && ${MAKE} CC=${CC} CXX=${CXX} CXXFLAGS="${CXXFLAGS}"

Comment thread RAG_POC/rag_ingest.cpp Outdated

#include <sqlite3.h>
#include <mysql/mysql.h>
#include <mysql.h>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Changing from <mysql/mysql.h> to <mysql.h> might cause portability issues on systems where the MySQL client library headers are not directly in the default include path but rather in a subdirectory like mysql/. It's generally safer to use the more specific path if it's known to work across different environments or ensure the build system correctly adds the parent directory to the include paths.

Comment thread RAG_POC/rag_ingest.cpp
Comment on lines +114 to +134
extern "C" __attribute__((weak)) char *sha256_crypt_r(
const char *key,
const char *salt,
char *buffer,
int buflen) {
if (!key || !salt || !buffer || buflen <= 0) {
return nullptr;
}
struct crypt_data data;
std::memset(&data, 0, sizeof(data));
char *res = crypt_r(key, salt, &data);
if (!res) {
return nullptr;
}
size_t len = std::strlen(res);
if (len + 1 > static_cast<size_t>(buflen)) {
return nullptr;
}
std::memcpy(buffer, res, len + 1);
return buffer;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The sha256_crypt_r weak alias is a good workaround for potential library inconsistencies. However, it's worth noting that crypt_r is part of libcrypt, which might not be available on all systems or might require specific linking flags. This could lead to build issues on some platforms. Ensure the build environment (e.g., Docker images, CI/CD) consistently provides libcrypt or a compatible alternative.

Comment thread RAG_POC/rag_ingest.cpp
Comment on lines +1272 to +1274
std::vector<std::string> batch_inputs = {emb_input};
std::vector<std::vector<float>> vecs = embedder->embed(batch_inputs, ecfg.dim);
sqlite_insert_vec(ss, vecs[0], chunk_id, doc.doc_id, src.source_id, now_epoch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Currently, embedder->embed is called for each chunk individually, passing a batch_inputs vector with a single element. The EmbeddingConfig includes a batch_size parameter, suggesting that the embedding provider might support batching multiple inputs for efficiency. Consider refactoring the ingestion loop to collect multiple emb_input strings and call embedder->embed once per batch, especially if ecfg.batch_size is greater than 1. This could significantly improve performance when processing many small chunks.

Comment thread lib/MySQL_Catalog.cpp Outdated
Comment on lines +291 to +293
if (query.empty()) {
proxy_error("Catalog search requires a query parameter\n");
return "[]";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The proxy_error message Catalog search requires a query parameter is good for indicating a missing required parameter. However, returning an empty JSON array "[]" might mask the error from the caller. Consider throwing an exception or returning a JSON object with an error message to provide more explicit feedback to the client about the failed request.

Comment thread lib/MySQL_Catalog.cpp

sql << " ORDER BY updated_at DESC LIMIT ? OFFSET ?";
// Order by relevance (BM25) and recency
sql << " ORDER BY bm25(f) ASC, c.updated_at DESC LIMIT ? OFFSET ?";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ORDER BY bm25(f) ASC, c.updated_at DESC clause is a good choice for hybrid relevance and recency. However, bm25 scores are typically lower for more relevant results, so ASC ordering would prioritize more relevant results. This seems correct for FTS, but ensure this aligns with the desired ranking behavior.

Comment thread lib/MySQL_Catalog.cpp
Comment on lines +347 to +348
if (!schema.empty()) {
(*proxy_sqlite3_bind_text)(stmt, param_idx++, schema.c_str(), -1, SQLITE_TRANSIENT);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The schema_pattern and kind_pattern variables are no longer needed since the schema and kind parameters are directly bound without wildcard wrapping. The tags_pattern still requires wrapping with % for LIKE operator.

Suggested change
if (!schema.empty()) {
(*proxy_sqlite3_bind_text)(stmt, param_idx++, schema.c_str(), -1, SQLITE_TRANSIENT);
if (!schema.empty()) {
(*proxy_sqlite3_bind_text)(stmt, param_idx++, schema.c_str(), -1, SQLITE_TRANSIENT);
}
if (!kind.empty()) {
(*proxy_sqlite3_bind_text)(stmt, param_idx++, kind.c_str(), -1, SQLITE_TRANSIENT);
}
if (!tags.empty()) {

renecannao added a commit to rahim-kanji/proxysql that referenced this pull request Jan 23, 2026
- Fix Makefile: Use $(CXXFLAGS) directly for consistency with build philosophy
- Fix MySQL_Catalog: Return proper error JSON instead of empty array on missing query
@renecannao

Copy link
Copy Markdown
Contributor

@coderabbitai review

@renecannao
renecannao requested a review from Copilot January 23, 2026 13:22
@coderabbitai

coderabbitai Bot commented Jan 23, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (3)
RAG_POC/rag_ingest.cpp (2)

993-1005: Potential NULL pointer dereference from sqlite3_column_text.

If any of these columns contain NULL values, sqlite3_column_text returns NULL, and assigning NULL to std::string is undefined behavior. The where_sql column uses COALESCE, but others don't.

🐛 Suggested fix
-    s.name      = (const char*)sqlite3_column_text(st, 1);
+    s.name      = str_or_empty((const char*)sqlite3_column_text(st, 1));
     s.enabled   = sqlite3_column_int(st, 2);

-    s.backend_type = (const char*)sqlite3_column_text(st, 3);
-    s.host         = (const char*)sqlite3_column_text(st, 4);
+    s.backend_type = str_or_empty((const char*)sqlite3_column_text(st, 3));
+    s.host         = str_or_empty((const char*)sqlite3_column_text(st, 4));
     s.port         = sqlite3_column_int(st, 5);
-    s.user         = (const char*)sqlite3_column_text(st, 6);
-    s.pass         = (const char*)sqlite3_column_text(st, 7);
-    s.db           = (const char*)sqlite3_column_text(st, 8);
+    s.user         = str_or_empty((const char*)sqlite3_column_text(st, 6));
+    s.pass         = str_or_empty((const char*)sqlite3_column_text(st, 7));
+    s.db           = str_or_empty((const char*)sqlite3_column_text(st, 8));

-    s.table_name   = (const char*)sqlite3_column_text(st, 9);
-    s.pk_column    = (const char*)sqlite3_column_text(st, 10);
+    s.table_name   = str_or_empty((const char*)sqlite3_column_text(st, 9));
+    s.pk_column    = str_or_empty((const char*)sqlite3_column_text(st, 10));

1361-1368: Missing curl_global_cleanup on error exit path.

When the transaction fails and the function returns at line 1364, curl_global_cleanup() is not called. While this is a minor issue for a CLI tool (resources are freed on process exit), it's good practice to ensure proper cleanup.

♻️ Suggested fix
   } else {
     sqlite_exec(db, "ROLLBACK;");
     sqlite3_close(db);
+    curl_global_cleanup();
     return 1;
   }
lib/MySQL_Catalog.cpp (1)

387-388: Guard NULL tags/links to avoid std::string(nullptr) crash.

tags and links are nullable columns. If a row has NULL, sqlite3_column_text returns nullptr and constructing std::string is undefined behavior. Add a null guard or COALESCE.

🐛 Suggested fix (null-safe extraction)
-    entry["tags"] = std::string((const char*)(*proxy_sqlite3_column_text)(stmt, 4));
-    entry["links"] = std::string((const char*)(*proxy_sqlite3_column_text)(stmt, 5));
+    const char* tags_str = (const char*)(*proxy_sqlite3_column_text)(stmt, 4);
+    entry["tags"] = tags_str ? std::string(tags_str) : nullptr;
+    const char* links_str = (const char*)(*proxy_sqlite3_column_text)(stmt, 5);
+    entry["links"] = links_str ? std::string(links_str) : nullptr;
🤖 Fix all issues with AI agents
In `@RAG_POC/Makefile`:
- Line 16: The Makefile's LIBS variable is missing the libcurl linker flag
causing link errors when rag_ingest.cpp (which includes <curl/curl.h> and uses
libcurl for the OpenAI embedding provider) is built; update the LIBS variable to
add -lcurl (e.g., append -lcurl to LIBS) so the linker includes libcurl when
linking targets that depend on rag_ingest.cpp.

In `@RAG_POC/rag_ingest.cpp`:
- Around line 1214-1226: The code uses std::stoll on strings validated by
is_integer_string but doesn't guard against std::out_of_range; wrap both places
where std::stoll is called (the initial assignment that sets max_num and the
comparison path that computes nv) in try/catch blocks catching std::out_of_range
(and std::invalid_argument for safety), and on catch switch to the
string-comparison branch by setting max_numeric = false and max_str = v (and
preserve max_set = true) so huge integers won't throw and will be compared
lexicographically instead; update both uses (the assignment setting max_num and
the nv comparison) to follow this pattern.

In `@RAG_POC/test_rag_ingest.sh`:
- Around line 109-122: The functions import_mysql_seed and run_mysql_sql
currently pass the password via -p"${MYSQL_PASS}", which leaks credentials;
change both to avoid the -p flag and instead supply the password via the
MYSQL_PWD environment variable or a temporary defaults file: invoke the mysql
binary with MYSQL_PWD="${MYSQL_PASS}" "${MYSQL_BIN}" -h"${MYSQL_HOST}"
-P"${MYSQL_PORT}" -u"${MYSQL_USER}" ... (or create a secure
--defaults-extra-file and point mysql at it), and ensure you unset or clear
MYSQL_PWD immediately after the command to avoid lingering secrets in the
environment or logs.
- Around line 31-36: The script always exports OPENAI_API_BASE and
OPENAI_API_KEY which forces the OpenAI-compatible path to run with placeholder
credentials; update test_rag_ingest.sh to stop unconditionally exporting
OPENAI_API_BASE and OPENAI_API_KEY and instead gate those exports behind an
explicit opt-in (e.g. check an ENABLE_OPENAI_TEST or similar env var) or keep
them commented out by default so Phase 5 only runs when the user opt-ins; ensure
references to OPENAI_API_BASE, OPENAI_API_KEY (and optionally
OPENAI_MODEL/EMBEDDING_PROVIDER) are only set when the opt-in flag is true.
♻️ Duplicate comments (1)
RAG_POC/rag_ingest.cpp (1)

1270-1275: Embedding calls are not batched despite batch_size config.

The EmbeddingConfig.batch_size parameter is parsed but unused. Each chunk triggers a separate API call, which is inefficient for external providers like OpenAI. Consider collecting chunks and calling embedder->embed in batches.

🧹 Nitpick comments (6)
RAG_POC/rag_ingest.cpp (3)

173-181: Consider using parameterized queries instead of manual escaping.

The sql_escape_single_quotes function only escapes single quotes by doubling them. While the values come from trusted sources (sync cursor stored in SQLite), this escaping approach doesn't handle all edge cases (e.g., backslash escapes in certain MySQL modes).

For more robust SQL construction, consider using MySQL prepared statements for the incremental filter instead of string concatenation.

Also applies to: 626-633


936-939: Wrap JSON parsing in try-catch to provide clearer error messages.

If the embedding API returns malformed JSON or an unexpected response format, json::parse will throw a generic exception. Wrapping this in a try-catch would allow providing a more descriptive error message including the response status and partial body for debugging.

♻️ Suggested improvement
-    json resp = json::parse(buf.data);
+    json resp;
+    try {
+      resp = json::parse(buf.data);
+    } catch (const json::parse_error& e) {
+      throw std::runtime_error("Failed to parse embedding response: " + std::string(e.what()));
+    }

1249-1253: updated_at is always set to 0 in vector chunks.

As noted in the comments, this stores a placeholder value. For production use, consider querying SELECT unixepoch() once at the start of ingestion and reusing that value for all inserts, ensuring consistent timestamps within a single run.

RAG_POC/Makefile (1)

21-21: Consider adding a test phony target.

Static analysis suggests adding a test target for consistency. For a PoC Makefile, this is optional but useful if you want to integrate with make test workflows.

RAG_POC/test_rag_ingest.sh (2)

59-107: Escape JSON/where_sql before injecting into SQL.

chunking_json_value, embedding_json_value, and where_sql are interpolated into SQL literals. A single quote in any override will break the statement and can make the test brittle. Consider escaping single quotes before injecting.

♻️ Proposed fix (escape single quotes)
 apply_schema_and_source() {
   local db="$1"
   local where_sql="$2"
   local load_schema="$3"
   local chunking_json_override="${4:-}"
   local embedding_json_override="${5:-}"
   local schema_override_path="${6:-}"
+  local sql_escape
+  sql_escape() { printf "%s" "$1" | sed "s/'/''/g"; }

   local schema_cmd=""
   if [[ "${load_schema}" == "true" ]]; then
@@
-  local embedding_json_value='{"enabled":false}'
+  local embedding_json_value='{"enabled":false}'
   if [[ -n "${embedding_json_override}" ]]; then
     embedding_json_value="${embedding_json_override}"
   fi
   echo "==> embedding_json: ${embedding_json_value}"
 
+  local chunking_json_sql
+  local embedding_json_sql
+  local where_sql_sql
+  chunking_json_sql="$(sql_escape "${chunking_json_value}")"
+  embedding_json_sql="$(sql_escape "${embedding_json_value}")"
+  where_sql_sql="$(sql_escape "${where_sql}")"
+
   "${SQLITE_BIN}" "${db}" <<SQL
 .load ${VEC_EXT}
@@
-  UPDATE rag_sources
-  SET chunking_json='${chunking_json_value}'
+  UPDATE rag_sources
+  SET chunking_json='${chunking_json_sql}'
 WHERE source_id=1;
 UPDATE rag_sources
-SET embedding_json='${embedding_json_value}'
+SET embedding_json='${embedding_json_sql}'
 WHERE source_id=1;
 UPDATE rag_sources
-SET where_sql='${where_sql}'
+SET where_sql='${where_sql_sql}'
 WHERE source_id=1;
 SQL
 }

357-359: Sed replacement is brittle against schema formatting changes.

The substitution assumes an exact string (embedding float[1536]). If spacing or the default dimension changes, the replacement silently fails. Consider a more flexible pattern.

♻️ Proposed fix (regex-based)
-  sed "s/embedding  float\[1536\]/embedding  float[${OPENAI_EMBEDDING_DIM}]/" "${ROOT_DIR}/schema.sql" > "${OPENAI_SCHEMA_TMP}"
+  sed -E "s/embedding[[:space:]]+float\\[[0-9]+\\]/embedding float[${OPENAI_EMBEDDING_DIM}]/" "${ROOT_DIR}/schema.sql" > "${OPENAI_SCHEMA_TMP}"

Comment thread RAG_POC/Makefile Outdated

SQLITE3_OBJ := $(ROOT_DIR)/deps/sqlite3/sqlite-amalgamation-3500400/sqlite3.o

LIBS := -lmariadbclient -lssl -lcrypto -lcrypt -ldl -lpthread

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 | 🔴 Critical

Missing -lcurl in LIBS.

The rag_ingest.cpp file includes <curl/curl.h> and uses libcurl for the OpenAI embedding provider. The -lcurl library flag is missing from the LIBS variable, which will cause linker errors when embedding is enabled.

🐛 Fix
-LIBS := -lmariadbclient -lssl -lcrypto -lcrypt -ldl -lpthread
+LIBS := -lmariadbclient -lssl -lcrypto -lcrypt -lcurl -ldl -lpthread
📝 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
LIBS := -lmariadbclient -lssl -lcrypto -lcrypt -ldl -lpthread
LIBS := -lmariadbclient -lssl -lcrypto -lcrypt -lcurl -ldl -lpthread
🤖 Prompt for AI Agents
In `@RAG_POC/Makefile` at line 16, The Makefile's LIBS variable is missing the
libcurl linker flag causing link errors when rag_ingest.cpp (which includes
<curl/curl.h> and uses libcurl for the OpenAI embedding provider) is built;
update the LIBS variable to add -lcurl (e.g., append -lcurl to LIBS) so the
linker includes libcurl when linking targets that depend on rag_ingest.cpp.

Comment thread RAG_POC/rag_ingest.cpp
Comment on lines +1214 to +1226
if (cursor.numeric || is_integer_string(v)) {
max_numeric = true;
max_num = std::stoll(v);
} else {
max_numeric = false;
max_str = v;
}
max_set = true;
} else if (max_numeric) {
if (is_integer_string(v)) {
std::int64_t nv = std::stoll(v);
if (nv > max_num) max_num = nv;
}

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

std::stoll can throw std::out_of_range for values exceeding int64 limits.

While is_integer_string validates the format, it doesn't check if the value fits in an int64_t. If the watermark column contains very large numeric strings, std::stoll will throw an exception that isn't caught here, causing the ingestion to abort.

♻️ Suggested improvement
           if (cursor.numeric || is_integer_string(v)) {
+            try {
               max_numeric = true;
               max_num = std::stoll(v);
+            } catch (const std::out_of_range&) {
+              // Fall back to string comparison for very large values
+              max_numeric = false;
+              max_str = v;
+            }
           } else {
🤖 Prompt for AI Agents
In `@RAG_POC/rag_ingest.cpp` around lines 1214 - 1226, The code uses std::stoll on
strings validated by is_integer_string but doesn't guard against
std::out_of_range; wrap both places where std::stoll is called (the initial
assignment that sets max_num and the comparison path that computes nv) in
try/catch blocks catching std::out_of_range (and std::invalid_argument for
safety), and on catch switch to the string-comparison branch by setting
max_numeric = false and max_str = v (and preserve max_set = true) so huge
integers won't throw and will be compared lexicographically instead; update both
uses (the assignment setting max_num and the nv comparison) to follow this
pattern.

Comment on lines +31 to +36
# Uncomment to test OpenAI-compatible embeddings
# export EMBEDDING_PROVIDER=openai
# export EMBEDDING_DIM=1536
export OPENAI_API_BASE="https://api.synthetic.new/openai/v1"
export OPENAI_API_KEY="your_api_key_here"
# export OPENAI_MODEL="hf:nomic-ai/nomic-embed-text-v1.5"

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

OpenAI test is always enabled due to unconditional exports.

These exports make the Phase 5 guard always true, so the script will attempt an OpenAI-compatible call with placeholder credentials. This is likely to fail in CI and may unintentionally hit external services. Keep these commented or gate them behind an explicit opt-in flag.

🐛 Proposed fix (opt-in only)
-# export EMBEDDING_PROVIDER=openai
-# export EMBEDDING_DIM=1536
-export OPENAI_API_BASE="https://api.synthetic.new/openai/v1"
-export OPENAI_API_KEY="your_api_key_here"
-# export OPENAI_MODEL="hf:nomic-ai/nomic-embed-text-v1.5"
+# export EMBEDDING_PROVIDER=openai
+# export EMBEDDING_DIM=1536
+# export OPENAI_API_BASE="https://api.synthetic.new/openai/v1"
+# export OPENAI_API_KEY="your_api_key_here"
+# export OPENAI_MODEL="hf:nomic-ai/nomic-embed-text-v1.5"
🤖 Prompt for AI Agents
In `@RAG_POC/test_rag_ingest.sh` around lines 31 - 36, The script always exports
OPENAI_API_BASE and OPENAI_API_KEY which forces the OpenAI-compatible path to
run with placeholder credentials; update test_rag_ingest.sh to stop
unconditionally exporting OPENAI_API_BASE and OPENAI_API_KEY and instead gate
those exports behind an explicit opt-in (e.g. check an ENABLE_OPENAI_TEST or
similar env var) or keep them commented out by default so Phase 5 only runs when
the user opt-ins; ensure references to OPENAI_API_BASE, OPENAI_API_KEY (and
optionally OPENAI_MODEL/EMBEDDING_PROVIDER) are only set when the opt-in flag is
true.

Comment on lines +109 to +122
import_mysql_seed() {
"${MYSQL_BIN}" \
-h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
< "${ROOT_DIR}/sample_mysql.sql"
}

run_mysql_sql() {
local sql="$1"
"${MYSQL_BIN}" \
-h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
-e "${sql}"
}

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

Avoid passing MySQL passwords on the command line.

-p"${MYSQL_PASS}" exposes credentials via process listings and CI logs. Prefer MYSQL_PWD (or a defaults file) to avoid leakage.

🔐 Proposed fix (MYSQL_PWD)
-import_mysql_seed() {
-  "${MYSQL_BIN}" \
+import_mysql_seed() {
+  MYSQL_PWD="${MYSQL_PASS}" "${MYSQL_BIN}" \
     -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-    -u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
+    -u"${MYSQL_USER}" \
     < "${ROOT_DIR}/sample_mysql.sql"
 }
 
 run_mysql_sql() {
   local sql="$1"
-  "${MYSQL_BIN}" \
+  MYSQL_PWD="${MYSQL_PASS}" "${MYSQL_BIN}" \
     -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-    -u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
+    -u"${MYSQL_USER}" \
     -e "${sql}"
 }
📝 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
import_mysql_seed() {
"${MYSQL_BIN}" \
-h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
< "${ROOT_DIR}/sample_mysql.sql"
}
run_mysql_sql() {
local sql="$1"
"${MYSQL_BIN}" \
-h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-u"${MYSQL_USER}" -p"${MYSQL_PASS}" \
-e "${sql}"
}
import_mysql_seed() {
MYSQL_PWD="${MYSQL_PASS}" "${MYSQL_BIN}" \
-h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-u"${MYSQL_USER}" \
< "${ROOT_DIR}/sample_mysql.sql"
}
run_mysql_sql() {
local sql="$1"
MYSQL_PWD="${MYSQL_PASS}" "${MYSQL_BIN}" \
-h"${MYSQL_HOST}" -P"${MYSQL_PORT}" \
-u"${MYSQL_USER}" \
-e "${sql}"
}
🤖 Prompt for AI Agents
In `@RAG_POC/test_rag_ingest.sh` around lines 109 - 122, The functions
import_mysql_seed and run_mysql_sql currently pass the password via
-p"${MYSQL_PASS}", which leaks credentials; change both to avoid the -p flag and
instead supply the password via the MYSQL_PWD environment variable or a
temporary defaults file: invoke the mysql binary with MYSQL_PWD="${MYSQL_PASS}"
"${MYSQL_BIN}" -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" -u"${MYSQL_USER}" ... (or
create a secure --defaults-extra-file and point mysql at it), and ensure you
unset or clear MYSQL_PWD immediately after the command to avoid lingering
secrets in the environment or logs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

renecannao added a commit that referenced this pull request Jan 23, 2026
Merge changes from PR #5318 (RAG ingestion feature) into the new
development branch v4.0_rag_ingest_2.

Changes include:
- RAG ingestion tool (rag_ingest) with chunking and embeddings
- MySQL_Catalog fixes for NULL pointer handling
- MCP server updates for RAG tools
- Comprehensive documentation and test scripts
@renecannao
renecannao changed the base branch from v4.0 to v4.0_rag_ingest_2 January 23, 2026 15:27
@renecannao
renecannao merged commit 6166ada into sysown:v4.0_rag_ingest_2 Jan 23, 2026
3 of 4 checks passed
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants