Skip to content

feat(firestore): support firestore mongodb dialect - #177

Draft
pl04351820 wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
pl04351820:feat/firestore-mongodb-support
Draft

feat(firestore): support firestore mongodb dialect#177
pl04351820 wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
pl04351820:feat/firestore-mongodb-support

Conversation

@pl04351820

Copy link
Copy Markdown

Firestore Enterprise Edition (MongoDB Compatible API) Support with context bootstraping


The PR adds the architectural changes for dynamic tool loading with built-in Firestore tools.

With this enhancement, developers and data agents can:

  1. Generate, evaluate, and iteratively optimize ContextSet artifacts (Templates, Facets, Value Searches) for Firestore collections with MongoDB Compatible API.
  2. Translate Natural Language Questions (NLQ) to MongoDB Query Language (MQL) using the Gemini Data Analytics (GDA) REST API (queryData).
  3. Execute and score generated MQL queries against native Firestore Enterprise Edition instances.

System Architecture & High-Level Flow

flowchart TD
    subgraph Local MCP Server [db-context-enrichment FastMCP]
        DynamicLoader[load_dynamic_mcp_tools] -- Reads --> ToolsYAML[autoctx/tools.yaml]
        DynamicLoader -- Registers --> LocalDriver[custom_tools/firestore_driver.py]
    end

    subgraph Evaluation & Generation Lifecycle
        NLQ[Natural Language Question] --> LocalCtx[Local ContextSet Injector]
        LocalCtx --> GDAPayload[GDA QueryData Payload Construction]
        GDAPayload --> GDAAPI[GDA REST API /queryData]
        GDAAPI --> GenMQL[Generated MongoDB MQL Query]
        GenMQL --> LocalDriver
        LocalDriver -- Executes MQL --> FirestoreDB[(Firestore Enterprise Edition)]
        FirestoreDB -- Returns Results --> Scorer[EvalBench Scorer & LLMRater]
    end
Loading

Key Lifecycle Phases

  1. Context Injection: Local ContextSet (Templates, Facets) are loaded and formatted into prompt guidelines.
  2. GDA REST Routing: Request is formatted with firestore_reference datasource metadata and dispatched to GDA REST API.
  3. Execution & Evaluation: Generated MQL (db.collection.aggregate(...) or db.collection.find(...)) is executed against the target Firestore database, and results are evaluated by SetMatcher & LLMRater.

Built-in Firestore MCP tool

Rather than depending on an external Toolbox MCP proxy or server binary, tool loading and execution for Firestore Enterprise Edition are handled in-process via a built-in dynamic tool loader (load_dynamic_mcp_tools()) in src/google/cloud/db_context_enrichment/main.py.

On startup, this loader scans autoctx/tools.yaml for tool definitions annotated with provider: local_mcp and dynamically constructs and registers native execution and schema inspection tools (such as firestore-list-collections and firestore-execute-mongodb) directly onto the FastMCP server instance using the local driver in custom_tools/firestore_driver.py.

This self-contained architecture allows data agents and evaluation runners to inspect collections and execute MongoDB MQL queries directly against Firestore Enterprise Edition via pymongo or google-cloud-firestore without requiring external MCP infrastructure.

Firestore Remote MCP tool could eventually replace the built-in tools.


Context Engineering & NoSQL Dialect Specification

NoSQL document databases differ significantly from relational SQL. This specification defines the NoSQL-specific rules for authoring ContextSet items.

Templates (references/template/mongodb.md)

Templates map end-to-end natural language questions to executable NoSQL MQL queries.

  • Structure:
{
  "nl_query": "What is the total revenue for completed orders?",
  "sql": "db.orders.aggregate([{ $match: { status: 'completed' } }, { $group: { _id: null, totalRevenue: { $sum: '$total_amount' } } }, { $project: { _id: 0, totalRevenue: 1 } }])",
  "intent": "Total revenue for completed orders",
  "manifest": "Total revenue for orders with a given status",
  "parameterized": {
    "parameterized_sql": "db.orders.aggregate([{ $match: { status: '$1' } }, { $group: { _id: null, totalRevenue: { $sum: '$total_amount' } } }, { $project: { _id: 0, totalRevenue: 1 } }])",
    "parameterized_intent": "Total revenue for orders with status $1"
  }
}

Facets (references/facet/mongodb.md)

Facets provide modular, reusable filter snippets and field-value predicates.

  • Structure:
{
  "sql_snippet": "orders.payment_method = 'credit_card'",
  "intent": "credit card payment method is literal string 'credit_card'",
  "manifest": "Credit card payment method",
  "parameterized": {
    "parameterized_sql_snippet": "orders.payment_method = '$1'",
    "parameterized_intent": "payment method is $1"
  }
}

Key NoSQL Authoring Guidelines

  1. Dot Notation: Use dot-path notation (items.price, customer.satisfaction, orders.status) instead of SQL table joins.
  2. Array Unwinding: Replace multi-table JOINs with { $unwind: "$items" } stages when calculating metrics over array elements.
  3. Exact Value Matching: String filters in MQL are case/space-sensitive (e.g. 'credit_card', 'In store' with space vs 'In-store').
  4. Date Formatting: Use ISODate('YYYY-MM-DDTHH:MM:SSZ') for date range matching.

EvalBench Pipeline & Integration

In evalbench/generators/models/query_data_api.py, local ContextSet files are parsed and appended to the prompt payload:

if context_set_path:
    # Format templates and facets into prompt guidelines
    prompt_text = format_context_guidelines(context_set_path, nl_query)

In db_generators/firestore.py, build_custom_query_context() passes context_set_path and routes execution through the PyMongo / Firestore runner.


Verification & Evaluation Results

The implementation was validated using iterative hill-climbing context optimization across two benchmark datasets:

Benchmark Dataset Items Initial Score Final LLMRater Final Executable
DART Ecommerce Dataset 54 92.59% 100.0% (54/54) 100.0% (54/54)
Supplies Dataset 114 84.21% 100.0% (114/114) 100.0% (114/114)

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

Code Review

This pull request introduces MongoDB and Firestore support to the context generation guide and evaluation pipeline. It adds a new firestore_driver with custom MCP tools for listing collections and executing queries, integrates these tools dynamically, and implements a FirestoreConfigGenerator for EvalBench configurations. Feedback on these changes highlights a security vulnerability due to disabled SSL verification, bugs in schema flattening and dataset conversion, and logical issues such as ignoring query filters in the native client fallback and hardcoding default collection IDs.

}

json_data = json.dumps(payload).encode("utf-8")
ssl_ctx = ssl._create_unverified_context()

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.

security-high high

Disabling SSL certificate verification via ssl._create_unverified_context() makes the connection vulnerable to Man-in-the-Middle (MitM) attacks. Use ssl.create_default_context() instead to ensure secure communication.

Suggested change
ssl_ctx = ssl._create_unverified_context()
ssl_ctx = ssl.create_default_context()

Comment on lines +83 to +92
if isinstance(val, dict):
value_type = val.get("valueType") or (list(val.keys())[0] if val else "stringValue")
if value_type == "mapValue":
map_fields = val.get("mapValue", {}).get("fields", {})
columns.extend(_flatten_schema(map_fields, field_name))
else:
data_type = val.get("stringValue") or value_type.replace("Value", "").upper()
columns.append({"name": field_name, "type": data_type})
elif isinstance(val, (dict, list)):
columns.append({"name": field_name, "type": "JSON"})

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.

high

The _flatten_schema function assumes any dictionary is a Firestore Value proto (e.g., {"stringValue": "..."}). However, when standard document sampling is used, to_dict() returns native Python dictionaries where nested maps are also native dictionaries (e.g., {"director": "Christopher Nolan"}). This causes native nested dictionaries to be incorrectly flattened as a single column with a type derived from the first key name (e.g., type "DIRECTOR"). We should distinguish between Firestore Value protos and native dictionaries, and recursively flatten native dictionaries.

Suggested change
if isinstance(val, dict):
value_type = val.get("valueType") or (list(val.keys())[0] if val else "stringValue")
if value_type == "mapValue":
map_fields = val.get("mapValue", {}).get("fields", {})
columns.extend(_flatten_schema(map_fields, field_name))
else:
data_type = val.get("stringValue") or value_type.replace("Value", "").upper()
columns.append({"name": field_name, "type": data_type})
elif isinstance(val, (dict, list)):
columns.append({"name": field_name, "type": "JSON"})
if isinstance(val, dict):
# Check if it's a Firestore Value proto (REST API format)
is_proto = "valueType" in val or any(k.endswith("Value") for k in val.keys())
if is_proto:
value_type = val.get("valueType") or (list(val.keys())[0] if val else "stringValue")
if value_type == "mapValue":
map_fields = val.get("mapValue", {}).get("fields", {})
columns.extend(_flatten_schema(map_fields, field_name))
else:
data_type = val.get("stringValue") or value_type.replace("Value", "").upper()
columns.append({"name": field_name, "type": data_type})
else:
# Native nested dictionary: recursively flatten
columns.extend(_flatten_schema(val, field_name))
elif isinstance(val, list):
columns.append({"name": field_name, "type": "JSON"})

Comment on lines +222 to +228
query_obj = json.loads(query) if isinstance(query, str) else query
col_id = query_obj.get("find") or query_obj.get("collection") or "movies"
limit = query_obj.get("limit", 10)

col_ref = client.collection(col_id)
docs = list(col_ref.limit(limit).stream())
results = [doc.to_dict() for doc in docs]

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.

high

The native client fallback in firestore_execute_mongodb completely ignores the query filters, projections, and aggregation stages. It simply extracts the collection name and streams the first N documents. This will return incorrect/unfiltered results during evaluation or agent execution. If standard Firestore doesn't support MQL, we should either raise an error or implement actual execution via pymongo if a connection string is provided.

Comment on lines +224 to +228
if isinstance(golden_sql_raw, dict):
# Ensure dialect key matches the generator's expected dialect (e.g. mongodb)
golden_sql_dict = {}
for d_key, val in golden_sql_raw.items():
golden_sql_dict[dialect] = val

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.

medium

When golden_sql_raw is a dictionary, the loop blindly overwrites golden_sql_dict[dialect] with the value of every key. If the target dialect is already present in the dictionary, it might be overwritten by another dialect's query depending on the iteration order. We should explicitly check if the target dialect exists in golden_sql_raw first.

Suggested change
if isinstance(golden_sql_raw, dict):
# Ensure dialect key matches the generator's expected dialect (e.g. mongodb)
golden_sql_dict = {}
for d_key, val in golden_sql_raw.items():
golden_sql_dict[dialect] = val
if isinstance(golden_sql_raw, dict):
# Ensure dialect key matches the generator's expected dialect (e.g. mongodb)
golden_sql_dict = {}
if dialect in golden_sql_raw:
golden_sql_dict[dialect] = golden_sql_raw[dialect]
elif golden_sql_raw:
golden_sql_dict[dialect] = next(iter(golden_sql_raw.values()))

self.project = params.get("project")
self.database = params.get("database", "nl2sql-mflix")
self.connection_string = params.get("connection_string")
self.collection_ids = params.get("collection_ids") or params.get("table_ids") or ["orders", "products", "customers"]

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.

medium

Hardcoding default collection IDs ["orders", "products", "customers"] leaks domain-specific assumptions into the generic FirestoreConfigGenerator class. If no collections are specified, it is better to default to None or an empty list to avoid querying incorrect collections on other databases (like nl2sql-mflix).

Suggested change
self.collection_ids = params.get("collection_ids") or params.get("table_ids") or ["orders", "products", "customers"]
self.collection_ids = params.get("collection_ids") or params.get("table_ids")

@g-lynnzee g-lynnzee left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please remove files that are not intended for this PR.

@@ -0,0 +1,236 @@
import json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We talked about how this ideally belongs in firestore MCP? Why can't you make the code change there?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Upstreaming Firestore support directly to MCP Toolbox is currently in progress in googleapis/mcp-toolbox#3826. In the interim, this local driver allows us to run Firestore MongoDB evaluations and unblocks context engineering workflows until the new toolbox version is released and pinned here.

query_context_dict = MessageToDict(
query_context._pb, preserving_proto_field_name=True
)
if hasattr(self, "build_custom_query_context"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I dont' think you'll need this given https://github.com/Google...ntext-enrichment/pull/192

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated! Cleaned up BaseDBConfigGenerator to conform to the REST dictionary contract from #192, removing the custom query context extraction override.

) -> gda.DatasourceReferences:
return gda.DatasourceReferences()

def build_custom_query_context(self, context_set_id: str) -> dict[str, Any]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think you'll need this given #192

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated! Conformed FirestoreConfigGenerator.build_datasource_reference to return a dict[str, Any] matching the base class REST structure from #192, and removed hardcoded default collection IDs.


def _convert_dataset(dataset_path: str, dialect: str) -> str:
"""Reads simplified dataset and converts to EvalBench standard format."""
"""Reads simplified or standard EvalBench dataset and converts/normalizes to EvalBench standard format."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please refactor with use of helper functions for readability.

This isn't related specifically to firestore. should we put it into its own PR?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Refactored _convert_dataset to use clean, modular helper functions (_normalize_golden_sql, _convert_simplified_entry, _convert_standard_entry) and fixed validation.

@pl04351820 pl04351820 changed the title Feat/firestore mongodb support feat(firestore): support firestore mongodb dialect Aug 14, 2026
@pl04351820

Copy link
Copy Markdown
Author

Summary of Updates & Fixes:

  1. Scope / Workspace Cleanup: Removed all experimental runs and CSV artifacts under autoctx/experiments/ from git tracking and added autoctx/ to .gitignore.
  2. Security Fix: Replaced ssl._create_unverified_context() with ssl.create_default_context() in firestore_driver.py.
  3. Schema Flattening: Updated _flatten_schema to handle both Firestore Value proto format and recursively flattened native Python dictionaries.
  4. EvalBench REST Contract Alignment: Conformed FirestoreConfigGenerator and BaseDBConfigGenerator to the REST dictionary contract from feat(evaluate): Use REST transport to enable working for unreleased proto fields #192, removing obsolete custom extraction overrides and hardcoded default collection IDs.
  5. Dataset Conversion Refactoring: Decomposed _convert_dataset into modular helper functions (_normalize_golden_sql, _convert_simplified_entry, _convert_standard_entry) with proper required key validation.
  6. Dependencies & Tests: Added google-cloud-firestore to pyproject.toml dependencies. All 56 unit tests pass and code is formatted with ruff.

@pl04351820
pl04351820 force-pushed the feat/firestore-mongodb-support branch from 00addcc to 16cda8f Compare August 14, 2026 21:58
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.

2 participants