Skip to content

refactor(sdk): make queryweaver_sdk pip-installable with shared core#544

Open
galshubeli wants to merge 18 commits intostagingfrom
sdk-package-cleanup
Open

refactor(sdk): make queryweaver_sdk pip-installable with shared core#544
galshubeli wants to merge 18 commits intostagingfrom
sdk-package-cleanup

Conversation

@galshubeli
Copy link
Copy Markdown
Collaborator

@galshubeli galshubeli commented Apr 20, 2026

Summary

Supersedes #384. Fixes the correctness bugs surfaced in review, eliminates the api.extensions.db global mutation, deduplicates orchestration between the streaming and SDK text2sql paths, and makes queryweaver_sdk structurally ready for pip install queryweaver. Bumps SDK to 0.2.0 (one breaking change: database_id is now un-prefixed).

What changed

Correctness

  • schema_loader.load_database_sync returns un-prefixed database_id — fixes double-namespacing when the value is passed back into query/delete/get_schema.
  • Healer except clause broadened to match streaming path, so psycopg2.Error/pymysql.Error from bad SQL actually trigger healing.
  • Removed the tables_loaded substring-counting heuristic (it was double-counting).
  • urllib.parse.urlparse for DB-name extraction instead of brittle split.
  • CI workflows restored to uv sync --locked --all-extras.

Architecture — no more hidden globals

  • New api/core/db_resolver.py::resolve_db() — lazy server-default fallback.
  • api/graph.py (find, get_db_description, get_user_rules, set_user_rules), api/memory/graphiti_tool.py::MemoryTool, api/loaders/{graph,postgres,mysql}_loader.py — all accept optional db=None param. Routes unchanged (use resolver fallback, keep from api.extensions import db).
  • QueryWeaver.__init__ no longer mutates api.extensions.db. Multiple SDK instances in one process are now independent.
  • SDK import no longer triggers api.extensions load (no side-effect FalkorDB connect at import time).

Deduplication — one text2sql implementation

  • api/core/text2sql_common.py gained 6 shared helpers: execute_with_healing, refresh_schema_if_modified, quote_identifiers_from_graph, save_memory_background, format_ai_response, build_destructive_confirmation_message.
  • MESSAGE_DELIMITER moved to the common module (was duplicated in 3 files).
  • text2sql_sync.py rewritten to call the helpers; streaming text2sql.py uses the leaf helpers (inline healing + schema-refresh remain for now — full unification needs queue-based emit, deferred).

Layering

  • api/core/result_models.py + api/core/request_models.py (new) now own the dataclasses. queryweaver_sdk.models is a thin re-export shim. Fixes the inverted api → queryweaver_sdk import.

SDK additions

  • QueryRequest.custom_api_key / custom_model — per-request LLM overrides (parity with streaming path).
  • QueryWeaver.close() awaits in-flight memory writes via a contextvar-scoped task sink.
  • Dead code removed (_graph_name, unused _ChatContext.use_user_rules).

Tests

  • Rewrote tests/test_sdk/test_queryweaver.py to use async with QueryWeaver(...) for proper teardown — no more skip-on-RuntimeError wrappers.
  • Added test_two_instances_isolated (regression guard for the global-mutation fix).
  • Un-skipped test_query_with_history.

Breaking changes (SDK 0.1.0 → 0.2.0)

  • DatabaseConnection.database_id is now the un-prefixed database name (e.g. "mydb", not "user_mydb"). Pass this value directly back into query, delete_database, get_schema, refresh_schema — the SDK applies namespacing internally.

Verification

  • 79/79 non-SDK unit tests pass; 14/14 SDK fast tests pass (including test_two_instances_isolated). Pylint 10.00/10.
  • uv build produces queryweaver-0.2.0-py3-none-any.whl; installed cleanly in a Python 3.13 fresh venv; import does not load api.extensions; two instances have independent _db handles.
  • End-to-end smoke test against live PostgreSQL via the SDK: connect_databaseget_schema → natural-language query → SQL (SELECT id, name FROM customers WHERE city = 'New York') → correct filtered results (2 NY customers) → delete_database. All 13 checks passed.
  • FastAPI server (uv run uvicorn api.index:app) boots cleanly; 21 routes registered; HSTS middleware active; unchanged behavior from the streaming path's perspective.

Test plan

  • CI unit-tests job passes on sdk-package-cleanup
  • CI sdk-tests job passes (requires OPENAI_API_KEY + PG/MySQL services)
  • CI playwright job passes
  • CI pylint job passes
  • Manual spot-check: streaming query via the web UI still works (backend behavior unchanged but not clicked through in a browser)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Python SDK for programmatic Text2SQL usage without running the web server.
    • Support for async database connections, schema management, and query execution through a unified client interface.
  • Documentation

    • Added comprehensive Python SDK documentation to README with installation instructions and usage examples.
  • Chores

    • Updated version to 0.2.0 and enhanced packaging metadata.

Dvir Dukhan and others added 18 commits February 4, 2026 22:49
- Add detailed assertions for query results (customer names, counts, etc.)
- Add tests for filter queries, count aggregation, and joins
- Validate SQL query structure and result data
- Add session-scoped event loop to fix pytest-asyncio issues
- Handle async event loop cleanup errors gracefully with skip
- Expand model serialization tests
Disable warnings that are intentional architectural choices:
- C0415: import-outside-toplevel (lazy imports for SDK)
- W0718: broad-exception-caught (error handling)
- R0902: too-many-instance-attributes (dataclasses)
- R0903: too-few-public-methods
- R0911: too-many-return-statements
- R0913/R0917: too-many-arguments (SDK API design)
- C0302: too-many-lines
- Extract SDK sync functions to new api/core/text2sql_sync.py module
- Split QueryResult into composition: QueryResult + QueryMetadata + QueryAnalysis
- Reduce local variables in query_database_sync with helper functions
- Fix broad exception handling - use specific Redis/Connection/OS errors
- Refactor query method to accept Union[str, QueryRequest]
- Add compatibility properties to QueryResult for backwards compatibility
- Document lazy imports in client.py module docstring

Pylint score improved from 9.81/10 to 9.91/10
Remaining E0401 errors are missing dependencies in venv, not code issues
Resolve merge conflicts in .github/workflows/tests.yml, Makefile, and
pyproject.toml. Standardize on uv (from staging migration) while
preserving SDK additions from the PR branch:

- tests.yml: Use uv instead of pipenv in sdk-tests job, keep SDK test
  exclusion in unit-tests, update action versions to v6
- Makefile: Use 'uv run' directly instead of pipenv/uv fallback,
  keep SDK targets (test-sdk, build-package, docker-test-services)
- pyproject.toml: Keep SDK optional-dependencies structure (server/dev/all),
  use staging's dependency versions (~= notation), merge pytest/pylint
  config from both sides, keep [tool.uv] and [dependency-groups]

Co-authored-by: Copilot <[email protected]>
- Use 'uv sync --all-extras' in tests, playwright, and pylint workflows
  to install server/dev optional dependencies (fastapi, uvicorn, etc.)
- Fix imports in api/routes/graphs.py: import GENERAL_PREFIX and
  graph_name from text2sql_common, errors from api.core.errors
- Remove unused variable 'last_message' in api/core/schema_loader.py
- Add pylint disable comments for too-many-arguments/locals in
  api/core/text2sql_sync.py

Co-authored-by: Copilot <[email protected]>
- Use uv sync --all-extras in CI (needed for SDK optional deps)
- Update dependency versions to staging's latest (litellm 1.82, falkordb 1.6, etc.)
- Keep SDK optional dependency structure (core vs server vs dev)
- Align playwright version across dependency groups (~=1.58.0)
- Merge pylint config (max-line-length, ignore-patterns, messages_control)
- Regenerate uv.lock

Co-authored-by: Copilot <[email protected]>
Bumps SDK to 0.2.0. Removes the api.extensions.db global mutation that
made two QueryWeaver instances collide, fixes the un-prefixed
database_id contract (breaking), and deduplicates orchestration
between the streaming and sync text2sql paths via shared helpers in
api.core.text2sql_common.

Key changes:
- api/core/db_resolver.py (new): lazy server-default fallback for db
- api/core/text2sql_common.py: 6 new shared helpers (execute_with_healing,
  refresh_schema_if_modified, quote_identifiers_from_graph,
  save_memory_background, format_ai_response,
  build_destructive_confirmation_message); MESSAGE_DELIMITER moved here
- api/core/{result,request}_models.py (new): dataclasses extracted so
  api.core no longer depends on queryweaver_sdk
- api/graph.py, loaders/*, memory/graphiti_tool.py: accept optional
  db param; routes unchanged (use resolver fallback)
- queryweaver_sdk/client.py: no more api.extensions.db mutation;
  close() awaits in-flight memory writes via contextvar-scoped sink
- queryweaver_sdk/models.py: re-export shim over api.core
- tests/test_sdk/: async-with fixtures, test_two_instances_isolated,
  no more skip-on-RuntimeError wrappers
- CI: restored --locked --all-extras in all three workflows

Breaking (SDK 0.1.0 -> 0.2.0):
- DatabaseConnection.database_id is now un-prefixed (fixes double-
  namespacing when passed back into query/delete/get_schema)

Fixes verified end-to-end: NL -> SQL -> results against live
PostgreSQL via SDK; FastAPI server boots cleanly with 21 routes and
HSTS middleware; two QueryWeaver instances coexist with independent
FalkorDB handles; uv build produces a working wheel; clean-venv
install succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@overcut-ai
Copy link
Copy Markdown

overcut-ai bot commented Apr 20, 2026

Completed Working on "Code Review"

✅ Review publishing completed successfully. Posted comments from all chunks and submitted final review: COMMENT. Total comments: 8 across 8 files.

✅ Workflow completed successfully.


👉 View complete log

@railway-app
Copy link
Copy Markdown

railway-app bot commented Apr 20, 2026

🚅 Deployed to the QueryWeaver-pr-544 environment in queryweaver

Service Status Web Updated (UTC)
QueryWeaver ❌ Build Failed (View Logs) Web Apr 20, 2026 at 3:18 pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

A Python SDK for Text2SQL is introduced alongside core refactoring that centralizes shared utilities, implements dependency injection for database connections, adds sync/async API functions for SDK usage, and includes integration test infrastructure for multi-service validation.

Changes

Cohort / File(s) Summary
SDK Package Core
queryweaver_sdk/__init__.py, queryweaver_sdk/client.py, queryweaver_sdk/connection.py, queryweaver_sdk/models.py
New async Python SDK client (QueryWeaver) with FalkorDB connection management, methods for schema/database/query operations, context manager support, and re-exported models from core API.
API Core Refactoring
api/core/__init__.py, api/core/db_resolver.py, api/core/request_models.py, api/core/result_models.py, api/core/text2sql_common.py, api/core/text2sql_sync.py
New request/result dataclasses, centralized utilities (MESSAGE_DELIMITER, graph/db helpers, sanitization, SQL analysis), shared execution/healing logic, background memory tasks, new SDK sync entrypoints (query_database_sync, execute_destructive_operation_sync, refresh_database_schema_sync), and dependency resolver for database handles.
Core Text2SQL Logic
api/core/text2sql.py, api/core/schema_loader.py
Refactored to use common utilities and accept optional db parameter for dependency injection; simplified control flow by delegating chat validation, SQL analysis, identifier quoting, and response formatting to text2sql_common helpers; updated schema loading with new sync function load_database_sync returning DatabaseConnection result.
Data Access Layer
api/graph.py, api/loaders/graph_loader.py, api/loaders/postgres_loader.py, api/loaders/mysql_loader.py, api/memory/graphiti_tool.py
Updated all signatures to accept optional db parameter; replaced direct api.extensions.db imports with resolve_db() for lazy/injected database resolution; graph selection and schema operations now use injected handles instead of module-level singletons.
API Routes
api/routes/database.py, api/routes/graphs.py
Removed local MESSAGE_DELIMITER definition and updated imports to reference common utilities; refactored graph ID namespacing to use graph_name() from text2sql_common.
Build & Dependency Configuration
pyproject.toml, docker-compose.test.yml, Makefile
Updated version to 0.2.0; split dependencies into minimal dependencies and optional server/dev groups; added console script entry queryweaver; added wheel packaging for queryweaver_sdk; added Docker services (FalkorDB, PostgreSQL, MySQL) with health checks for integration testing; added SDK-related make targets (test-sdk, docker-test-services, test-all, build-package).
CI/CD Workflows
.github/workflows/playwright.yml, .github/workflows/pylint.yml, .github/workflows/tests.yml
Updated to use uv sync --locked --all-extras; added new sdk-tests job in tests.yml that spins up multi-service containers, configures environment variables, and runs pytest on tests/test_sdk/.
SDK Integration Tests
tests/test_sdk/__init__.py, tests/test_sdk/conftest.py, tests/test_sdk/test_queryweaver.py
New test package with fixtures for FalkorDB/Postgres/MySQL database URLs, QueryWeaver client lifecycle management, and comprehensive end-to-end tests covering initialization, database operations, schema retrieval, query execution, chat history, deletion, and model serialization.
Documentation
README.md, .github/wordlist.txt
Added Python SDK section documenting installation, quick-start examples, async usage patterns, API surface, and advanced query configuration; updated wordlist.

Sequence Diagram

sequenceDiagram
    participant App as QueryWeaver SDK
    participant Core as text2sql_sync
    participant Analysis as Analysis Agents
    participant Exec as execute_with_healing
    participant Loader as BaseLoader
    participant DB as FalkorDB/SQL
    participant Memory as MemoryTool

    App->>Core: query_database_sync(user_id, graph_id, chat_data)
    Core->>Analysis: validate_relevancy + get_table_discovery
    Analysis-->>Core: relevance/tables result
    alt Off-topic
        Core-->>App: QueryResult(missing_information="...")
    else Needs confirmation
        Core-->>App: QueryResult(requires_confirmation=true)
    else Proceed
        Core->>Analysis: analyze_and_generate_sql
        Analysis-->>Core: sql_query + analysis_output
        Core->>Exec: execute_with_healing(sql_query, ...)
        Exec->>Loader: execute SQL
        Loader->>DB: run query
        DB-->>Loader: results
        Exec->>DB: check health/healing
        Exec-->>Core: (result, analysis)
        Core->>Memory: save_memory_background(question, sql, result, ...)
        Core-->>App: QueryResult(sql_query, results, analysis, metadata)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


🐰 A SDK is born with async/await grace,
Dependencies injected in their place,
From schemas loaded to queries refined,
Common utilities beautifully designed!
Tests in containers, services align,
Text2SQL now shines with Python design!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.74% 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 PR title accurately describes the main change: refactoring the SDK to be pip-installable with shared core logic. It directly relates to the primary objective of the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sdk-package-cleanup
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch sdk-package-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@overcut-ai overcut-ai bot left a comment

Choose a reason for hiding this comment

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

Thanks for the substantial refactor and test expansion. Consolidating the posted findings:

  • 8 MAJOR comments (no BLOCKER/CRITICAL/MINOR/SUGGESTION/PRAISE in this review set)
  • 8 files affected

Key themes

  1. SDK packaging/runtime boundary regressions: several findings indicate minimal pip install queryweaver may still fail due to transitive server-only imports and dependency coupling.
  2. Behavioral parity/correctness gaps: differences between SDK and streaming paths (memory persistence, error wrapping) can produce inconsistent outcomes and contracts.
  3. Test reliability/coverage risks: current fixture scoping and API-key gating reduce deterministic coverage for non-LLM SDK behaviors.

Recommended next steps

  1. Harden package boundaries so SDK imports and core SDK operations run on minimal install; add CI coverage for minimal-install smoke tests.
  2. Restore contract consistency by aligning error handling and memory-write behavior between SDK and streaming paths.
  3. Improve test determinism by removing unnecessary LLM-key gating for non-LLM tests and isolating mutable DB state per test.

Addressing these should materially reduce runtime surprises and improve confidence in the 0.2.0 SDK surface.

Comment thread api/core/text2sql_sync.py
analysis = _parse_analysis_result(answer_an)

if not analysis.is_valid:
return _invalid_sql_result(ctx, analysis, answer_an)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: When AnalysisAgent returns is_sql_translatable=false, the SDK path returns early via _invalid_sql_result without persisting query/memory, while the streaming path still saves memory for the turn. This breaks parity and loses conversation traces for failed/ambiguous questions when use_memory is enabled.

Before returning from the invalid-SQL branch, call save_memory_background(...) similarly to other exit paths (include success=false and the follow-up response payload).

Comment thread api/core/schema_loader.py
if success:
# SDK callers pass the un-prefixed database_id back into query/delete/etc.,
# where graph_name(user_id, db_name) re-applies the user_id prefix.
db_name = urlparse(url).path.lstrip("/")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: load_database_sync now derives database_id using urlparse(url).path.lstrip('/'), but for URLs like .../mydb?sslmode=require this can propagate malformed IDs back to SDK callers. Round-tripping this value into query/delete/get_schema/refresh_schema can target a non-existent graph and break connect→query flows.

Derive the DB name from parsed URL components (path only) and never include query/fragment; reject empty/invalid names explicitly instead of returning potentially malformed IDs.

Comment thread api/core/text2sql.py
except ResponseError as re:
raise GraphNotFoundError("Failed to delete graph, Graph not found") from re
except Exception as e: # pylint: disable=broad-exception-caught
except (RedisError, ConnectionError) as e:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: Replacing the previous broad fallback with only (RedisError, ConnectionError) can let other backend/driver failures escape unwrapped, causing inconsistent API/SDK error contracts on delete failures.

Restore a final broad catch that wraps unexpected failures into InternalError after logging, while preserving specific handling for known Redis exceptions.

Comment thread queryweaver_sdk/models.py
"""

from api.core.request_models import QueryRequest
from api.core.result_models import (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: queryweaver_sdk.models re-exports from api.core.*, and the api.core import chain can transitively require server extras (e.g., graph/fastapi-adjacent modules). This can break the documented minimal SDK install by failing at import time.

Decouple shared dataclasses from api package side effects (or make api.core.__init__ minimal/lazy) so importing SDK models does not require server extras.

Comment thread queryweaver_sdk/client.py
Raises:
ValueError: If the database URL format is invalid.
"""
from api.core.schema_loader import load_database_sync
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: SDK methods route through api.core.schema_loader / api.core.text2sql, which can transitively import loader code requiring graphiti_core. With graphiti-core moved to optional extras, normal SDK operations can raise ModuleNotFoundError under pip install queryweaver.

Either keep required runtime deps in base install or isolate SDK execution paths from server-only imports; add packaging CI that validates minimal install can run core SDK operations.

Comment thread README.md

```bash
# SDK only (minimal dependencies)
pip install queryweaver
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: README claims pip install queryweaver is sufficient, but current import/runtime paths indicate optional extras may still be required for documented SDK flows. This can produce runtime/import errors for users following quick start instructions.

Align docs with actual requirements, or preferably fix package boundaries so minimal install works as documented and verify this in CI.


@pytest.mark.asyncio
@pytest.mark.requires_postgres
async def test_connect_postgres(self, falkordb_url, postgres_url, has_llm_key):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: Deterministic SDK lifecycle tests (e.g., connect/get_schema/delete) are skipped when no LLM key is set, reducing regression coverage for non-LLM behavior and teardown reliability.

Remove LLM-key gating from tests that do not call query() and keep it only for truly LLM-dependent tests.

)


@pytest.fixture(scope="session")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[major]: Session-scoped mutable DB fixtures can make test outcomes order-dependent when tests write data, risking flaky counts/results across reruns or parallelization.

Use function-scoped isolation (or per-test schema/db names with teardown) for deterministic SDK integration assertions.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

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

⚠️ Outside diff range comments (5)
api/loaders/graph_loader.py (1)

19-32: ⚠️ Potential issue | 🟡 Minor

Annotate the new optional graph DB handle.

load_to_graph now exposes a dependency-injection parameter; keep the loader API typed. As per coding guidelines, api/**/*.py: Include type hints throughout Python code.

Proposed fix
 import json
+from typing import Any
 
 import tqdm
@@
-    db=None,
+    db: Any | None = None,
 ) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/loaders/graph_loader.py` around lines 19 - 32, The new optional DB handle
parameter on load_to_graph needs a type annotation; update the function
signature for load_to_graph to type the db parameter as Optional[...] (e.g.,
Optional[FalkorDB] or the concrete DB interface used by resolve_db) and import
Optional from typing (or use typing.Optional), keeping the existing -> None
return type; ensure any references to resolve_db(db) still type-check with the
chosen DB type and update any stub/type imports (FalkorDB or GraphDB interface)
so the annotation is valid.
api/graph.py (1)

337-350: ⚠️ Potential issue | 🟠 Major

Preserve column-only search results.

When the LLM returns only column descriptions, main_tasks contains one task, but tables_by_columns_des becomes []; those matches are silently discarded.

Proposed fix
     # Execute the main embedding-based searches in parallel
     results = await asyncio.gather(*main_tasks)
 
     # Unpack results based on what tasks we ran
-    tables_des = results[0] if table_embeddings else []
-    tables_by_columns_des = results[1] if (table_embeddings and column_embeddings) else []
+    result_index = 0
+    tables_des = []
+    tables_by_columns_des = []
+    if table_embeddings:
+        tables_des = results[result_index]
+        result_index += 1
+    if column_embeddings:
+        tables_by_columns_des = results[result_index]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/graph.py` around lines 337 - 350, The current unpacking logic assumes
fixed positions in results and drops column-only searches; instead iterate
results in insertion order and assign them based on which tasks were appended to
main_tasks: after building main_tasks with _find_tables and/or
_find_tables_by_columns, consume results sequentially (use an index or pop from
a list) and set tables_des when table_embeddings was requested and
tables_by_columns_des when column_embeddings was requested so that a single-task
run (column-only or table-only) maps correctly to the right variable (refer to
main_tasks, _find_tables, _find_tables_by_columns, results, tables_des,
tables_by_columns_des).
api/loaders/mysql_loader.py (1)

469-482: ⚠️ Potential issue | 🔴 Critical

Fix the async generator consumption in refresh_graph_schema.

MySQLLoader.load() is an async generator (contains yield statements), so await MySQLLoader.load(...) at line 482 raises TypeError. Additionally, deleting the graph at line 470 before reloading creates a risky window where the schema could be lost if reload fails. Use async for to properly consume the async generator:

Proposed fix
-            success, message = await MySQLLoader.load(prefix, db_url, db=db)
+            success = False
+            message = "Failed to reload schema"
+            async for success, message in MySQLLoader.load(prefix, db_url, db=db):
+                pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/loaders/mysql_loader.py` around lines 469 - 482, The code calls await
MySQLLoader.load(...) but MySQLLoader.load is an async generator, so replace the
await with an async for loop to consume yielded items (e.g., "async for result
in MySQLLoader.load(prefix, db_url, db=db):" and capture success/message from
the yielded values); also avoid deleting the existing graph before a successful
reload—first run the reload into a temporary/staging graph or buffer and only
call graph.delete() and replace the old graph after the async for confirms
success (adjust the logic in refresh_graph_schema to use the async-for
consumption of MySQLLoader.load and perform the delete/replace only after a
successful load).
api/loaders/postgres_loader.py (2)

510-523: ⚠️ Potential issue | 🔴 Critical

Consume the async generator instead of awaiting it.

PostgresLoader.load() yields progress tuples, so await PostgresLoader.load(...) raises TypeError. Since line 511 deletes the graph first, schema refresh can drop the graph and then fail to reload it.

Proposed fix
-            success, message = await PostgresLoader.load(prefix, db_url, db=db)
+            success = False
+            message = "Failed to reload schema"
+            async for success, message in PostgresLoader.load(prefix, db_url, db=db):
+                pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/loaders/postgres_loader.py` around lines 510 - 523, The code awaits
PostgresLoader.load(...) even though load is an async generator and also deletes
the graph before reloading which can cause reload failures; fix by first running
the reload by iterating the async generator (use "async for progress in
PostgresLoader.load(prefix, db_url, db=db)" to consume progress tuples and
capture the final (success, message)), then only call await graph.delete() or
delete the old graph after confirming success; reference
resolve_db.select_graph(graph_id), graph.delete(), graph_id, prefix, and
PostgresLoader.load to locate the code to change.

177-180: ⚠️ Potential issue | 🟠 Major

Use urlparse for PostgreSQL database-name extraction.

The current split-based extraction fails for valid PostgreSQL URLs with Unix-socket parameters, e.g., postgresql:///mydb?host=/var/run/postgresql, producing the scheme name instead of the database name. This causes incorrect graph name generation.

Proposed fix
-            # Extract database name from connection URL
-            db_name = connection_url.split('/')[-1]
-            if '?' in db_name:
-                db_name = db_name.split('?')[0]
+            # Extract database name from connection URL
+            parsed_url = urlparse(connection_url)
+            db_name = unquote(parsed_url.path.lstrip("/"))
+            if not db_name:
+                raise ValueError("PostgreSQL connection URL must include a database name")

Note: urlparse and unquote are already imported.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/loaders/postgres_loader.py` around lines 177 - 180, Replace the brittle
split-based extraction of db_name with urlparse/unquote: parse connection_url
via urlparse(connection_url), take parsed.path, strip any leading '/'
(parsed.path.lstrip('/')), unquote the result to handle percent-encoding, and
assign that to db_name; keep a fallback (e.g., original split logic) only if the
parsed path is empty. Update the code that sets db_name (the variable assigned
from connection_url) to use this parsing logic so Unix-socket and
query-parameter URLs yield the correct database name.
🧹 Nitpick comments (7)
.github/workflows/tests.yml (1)

121-124: Pin uv to a consistent version across jobs.

Other steps in this workflow (and the wider repo) pin version: "0.7.12" (see line 42 in this file, and .github/workflows/playwright.yml). This new sdk-tests job uses "latest", which can drift and make CI non-reproducible.

🔧 Suggested fix
     - name: Install uv
       uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
       with:
-        version: "latest"
+        version: "0.7.12"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml around lines 121 - 124, The "Install uv" step
uses astral-sh/setup-uv with version: "latest" which can cause CI drift; change
the with.version value to the pinned release used elsewhere (e.g., "0.7.12") so
the sdk-tests job matches other workflows; locate the step that references
astral-sh/setup-uv (the "Install uv" step) and replace version: "latest" with
version: "0.7.12".
Makefile (2)

83-86: Prefer --wait over a fixed sleep.

docker-compose.test.yml already declares healthchecks for falkordb/postgres/mysql, so docker compose up -d --wait will block until each service is healthy. The current sleep 10 is flaky on slow runners and unnecessarily slow on fast ones.

🔧 Suggested fix
 docker-test-services: ## Start all test services (FalkorDB + PostgreSQL + MySQL)
-	docker compose -f docker-compose.test.yml up -d
-	`@echo` "Waiting for services to be ready..."
-	`@sleep` 10
+	docker compose -f docker-compose.test.yml up -d --wait --wait-timeout 120
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 83 - 86, The docker-test-services Makefile target uses
a fixed sleep which is flaky; update the docker compose invocation in the
docker-test-services recipe (the `docker compose -f docker-compose.test.yml up
-d` line) to use `--wait` (i.e., `docker compose -f docker-compose.test.yml up
-d --wait`) so it blocks until the healthchecks in docker-compose.test.yml pass,
and remove the subsequent `sleep 10` (and optional "Waiting for services to be
ready..." echo) to rely on the built-in wait behavior.

1-1: Add new targets to .PHONY.

test-sdk-quick (line 94) and test-all (line 97) are new phony targets not listed in .PHONY, so make will treat them as file targets if a file of the same name appears.

🔧 Suggested fix
-.PHONY: help install test test-unit test-e2e test-e2e-headed lint format clean setup-dev build lint-frontend test-sdk docker-test-services docker-test-stop build-package
+.PHONY: help install test test-unit test-e2e test-e2e-headed lint format clean setup-dev build lint-frontend test-sdk test-sdk-quick test-all docker-test-services docker-test-stop build-package
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` at line 1, The Makefile's .PHONY declaration is missing the newly
added phony targets test-sdk-quick and test-all; update the .PHONY line (the
.PHONY entry at the top of the Makefile) to include test-sdk-quick and test-all
so make treats them as phony targets rather than files, ensuring the targets
defined later in the file are always executed.
queryweaver_sdk/client.py (1)

207-215: Redundant per-line import-outside-toplevel disables.

The module-level # pylint: disable=import-outside-toplevel at line 24 already covers the whole file, so the inline disables on lines 213 and 214 are noise. Drop them for consistency with the other lazy imports in this file (e.g. lines 91, 120, 156‑157, 200, 232, 251, 273‑274 which rely on the module-level directive).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queryweaver_sdk/client.py` around lines 207 - 215, Remove the redundant
inline pylint disables on the lazy imports inside the async method
list_databases: delete the trailing "# pylint: disable=import-outside-toplevel"
comments from the two import statements that import list_databases from
api.core.schema_loader and GENERAL_PREFIX from api.core.text2sql_common so the
method uses the module-level disable already present; ensure the imports remain
inside list_databases (preserving lazy import behavior) and run linters to
confirm no new warnings.
tests/test_sdk/conftest.py (1)

147-154: queryweaver fixture swallows close errors silently.

If await qw.close() raises (e.g. Redis pool already disconnected by a test), the exception becomes a teardown error that masks the actual test failure. Consider wrapping teardown in try/except-and-log, or use async with QueryWeaver(...) inside the fixture for symmetric lifecycle handling:

-@pytest.fixture
-async def queryweaver(falkordb_url):
-    """Provide initialized QueryWeaver instance with proper teardown."""
-    from queryweaver_sdk import QueryWeaver
-
-    qw = QueryWeaver(falkordb_url=falkordb_url, user_id="test_user")
-    yield qw
-    await qw.close()
+@pytest.fixture
+async def queryweaver(falkordb_url):
+    """Provide initialized QueryWeaver instance with proper teardown."""
+    from queryweaver_sdk import QueryWeaver
+
+    async with QueryWeaver(falkordb_url=falkordb_url, user_id="test_user") as qw:
+        yield qw
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sdk/conftest.py` around lines 147 - 154, The queryweaver fixture
currently yields a QueryWeaver instance and calls await qw.close() in teardown
which can raise and mask test failures; update the fixture to either use
symmetric lifecycle by creating the instance inside an async context (async with
QueryWeaver(falkordb_url=..., user_id="test_user") as qw: yield qw) or wrap the
teardown call in a try/except that catches exceptions from qw.close() and logs
them (referencing the fixture name queryweaver, the QueryWeaver constructor, and
the qw.close() method) so close errors don't obscure test failures.
tests/test_sdk/test_queryweaver.py (1)

24-30: Sync init tests leak a FalkorDBConnection object.

test_init_defaults and test_init_with_custom_user_id construct QueryWeaver but never close() it. Because _db is lazy this won't open a real Redis pool today, but the pattern is fragile — if __init__ ever starts eagerly probing the connection (e.g. for a version check), these tests will start leaking live connections. Use async with (with @pytest.mark.asyncio) or explicitly await qw.close() to be future‑proof.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sdk/test_queryweaver.py` around lines 24 - 30, The tests
test_init_defaults and test_init_with_custom_user_id create QueryWeaver
instances that may allocate a FalkorDBConnection; update each test to ensure the
connection is closed by either turning the test into an async test (add
`@pytest.mark.asyncio`) and using "async with QueryWeaver(...)" or by explicitly
awaiting qw.close() at the end of the test; reference QueryWeaver and its
close() method (or the context-manager protocol for QueryWeaver) so the
FalkorDBConnection held in _db is properly cleaned up to prevent leaking live
connections.
queryweaver_sdk/connection.py (1)

83-86: Avoid direct access to FalkorDB client's internal connection attribute — use the public aclose() API instead.

When constructed via FalkorDB(host=host, port=port) (lines 84), self._pool remains None and close() falls into the elif branch that calls self._db.connection.aclose() (line 118). This directly accesses an undocumented internal attribute of the FalkorDB client. The public API provides aclose() method on the FalkorDB client itself, which properly handles the underlying connection cleanup.

Consider either:

  1. Simpler fix: Replace await self._db.connection.aclose() with await self._db.aclose() (the public API), or
  2. Unified approach: Build a connection URL from host/port and use BlockingConnectionPool.from_url() for both code paths, ensuring consistent teardown via pool.disconnect().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queryweaver_sdk/connection.py` around lines 83 - 86, The close path currently
calls the FalkorDB client's undocumented internal attribute
(self._db.connection.aclose()); update the teardown to use the public API by
replacing that call with await self._db.aclose() in the close() method of the
connection class (or alternatively refactor construction to use
BlockingConnectionPool.from_url() for both creation branches and call
pool.disconnect() for a unified cleanup); locate references to FalkorDB,
close(), self._db.connection.aclose(), and adjust them to use the public
aclose() or the BlockingConnectionPool.from_url()/pool.disconnect() approach so
the client is torn down via its supported API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/tests.yml:
- Around line 136-143: The "Run SDK tests" job will run without OPENAI_API_KEY
for forked PRs causing LLM tests to fail; update the workflow so LLM-dependent
tests only run when the secret is present by making one of these changes: add a
job-level condition like if: ${{ secrets.OPENAI_API_KEY != '' }} to the "Run SDK
tests" job (or split the job into a non-LLM "test-sdk-quick" job and a separate
"Run SDK LLM tests" job gated by the secret or by a label/pull_request_target
with safeguards), or add an early pre-flight step in "Run SDK tests" that checks
OPENAI_API_KEY and fails with a clear message if empty; ensure the change
references the OPENAI_API_KEY env var used in the job so LLM tests are skipped
when the secret is unavailable.

In `@api/core/db_resolver.py`:
- Around line 12-18: Add explicit type hints to the resolve_db boundary: import
typing (e.g., "from typing import Optional, Any") and change the signature of
resolve_db to accept db: Optional[Any] = None and return Any, and annotate the
lazily imported api.extensions.db (imported as _default_db) as the same type so
the function returns a typed handle; update any local comments or # type: ignore
as needed when importing _default_db to satisfy the type checker.

In `@api/core/text2sql_common.py`:
- Around line 140-143: The function detect_destructive_operation currently uses
sql_query.strip().split()[0] which misclassifies queries with leading comments
(e.g., "-- comment\nDROP TABLE ..."); update detect_destructive_operation to
first remove leading SQL comments and whitespace before extracting the first
token: skip or strip initial single-line comments beginning with "--" and
initial block comments "/* ... */" (repeated until the first non-comment token),
then extract the verb, uppercase it, and check against DESTRUCTIVE_OPS while
safely handling empty/whitespace-only input.
- Around line 67-71: The code currently raises GraphNotFoundError for an
invalid/blank graph_id; change this to raise InvalidArgumentError instead so
callers treat it as a 400 Bad Request. Locate the block where graph_id is
normalized (graph_id = graph_id.strip()[:200]) and replace the
GraphNotFoundError instantiation with an InvalidArgumentError containing the
same or similar message ("Invalid graph_id, must be less than 200 characters.").
Ensure the exception import or reference for InvalidArgumentError is
present/used in text2sql_common.py so the new exception type is available.

In `@api/core/text2sql_sync.py`:
- Around line 412-416: The SDK sync path currently allows destructive queries
against general/demo graphs; update the destructive-op guard in the sync
handlers so destructive operations on general graphs are intercepted: in
query_database_sync() (around the block at 412-416) and in
execute_destructive_operation_sync() (the block around 452-480) change/add the
check to return _confirmation_required_result(ctx, analysis) when
analysis.is_destructive and is_general_graph(ctx.db.graph_id) so that
destructive operations against demo/general graphs require confirmation instead
of being executed.
- Around line 152-158: Add the same per-request override validation used in the
streaming path before constructing _ChatContext: validate getattr(chat_data,
'custom_model', None) follows the "vendor/model" pattern with vendor one of
(openai, anthropic, gemini, azure, ollama, cohere) and validate
getattr(chat_data, 'custom_api_key', None) is either None or has length >= 10;
if either check fails, raise a clear ValueError (or similar) so invalid
overrides are rejected before creating chat_ctx in text2sql_sync.py where
_ChatContext is instantiated.

In `@api/graph.py`:
- Around line 39-41: Annotate all parameters that default to None with explicit
Optional types and add the required typing imports; e.g., update
get_db_description(graph_id: str, db=None) -> tuple[str, str] to something like
get_db_description(graph_id: str, db: Optional[Any] = None) -> tuple[str, str]
(import Optional, Any from typing) and similarly change any parameter like
db_description: str = None to db_description: Optional[str] = None; apply the
same pattern to the other affected functions (the ones around lines 57-59,
73-76, 87, and 279-283) and ensure typing imports are added at the top of the
module.

In `@api/memory/graphiti_tool.py`:
- Around line 60-64: The constructor currently accepts an untyped db injection;
add explicit type hints to the public API by typing the db parameter and the
instance attribute: annotate GraphitiTool.__init__(self, user_id: str, graph_id:
str, db: Optional[DatabaseType] = None) (choose the concrete alias used in the
project, e.g., Optional[Mapping[str, Any]] or the project's DB protocol), and
annotate self._db and self.memory_db_name where appropriate; update resolve_db
return type or cast to the chosen DatabaseType and also apply the same typing
fix to the other constructor/initializer block referenced around lines 82-90 so
all injected db parameters and stored attributes are consistently typed
(reference symbols: __init__, self._db, self.memory_db_name, resolve_db,
FalkorDriver).

In `@docker-compose.test.yml`:
- Around line 5-13: The docker-compose service "falkordb" currently uses an
unpinned image ("falkordb/falkordb:latest"), which breaks test reproducibility;
change the "image" field in docker-compose.test.yml for the falkordb service to
a specific, tested tag (e.g., "falkordb/falkordb:<version>") instead of :latest,
and ensure that this pinned version matches the one used in CI or test docs
(update CI/test config if necessary).

In `@pyproject.toml`:
- Around line 120-123: The pyproject.toml pylint section ([tool.pylint.main])
currently sets ignore-patterns to ["test_.*\\.py", "conftest\\.py"], which
excludes test files from linting; remove or clear the ignore-patterns setting so
pylint runs over all Python files (update or delete the ignore-patterns key
under [tool.pylint.main], leaving max-line-length = 120 intact) to ensure tests
are included in the project-wide lint job.
- Around line 67-68: The console-script entry "queryweaver = \"api.index:main\""
points to a non-existent function; either add a main() function in api/index.py
that initializes and runs the CLI entrypoint, or change the entry to
"api.index:app" if the intended symbol is the existing ASGI/FastAPI app; update
the pyproject.toml entry accordingly and ensure the referenced symbol (main or
app) is exported at module level in api.index.

In `@queryweaver_sdk/client.py`:
- Around line 287-295: The current close() snapshots _pending_tasks once and
awaits them, missing any tasks scheduled during that await; change close() to
repeatedly drain until no tasks remain by looping: while self._pending_tasks:
take a snapshot (e.g., tasks = list(self._pending_tasks)), await
asyncio.gather(*tasks, return_exceptions=True) (optionally await
asyncio.sleep(0) to allow scheduling), and repeat until the set is empty, then
call await self._connection.close(); ensure you reference the close method, the
_pending_tasks collection, and _connection when making this change.
- Around line 254-285: The execute_confirmed method currently ignores
per-request LLM overrides and must accept and forward them: update
execute_confirmed's signature to accept custom_api_key and custom_model (or a
QueryRequest-style wrapper) and populate the ConfirmRequest with those fields
when constructing confirm_data; then pass the updated confirm_data through to
execute_destructive_operation_sync (and/or pass through any wrapper) so the same
per-request LLM credentials used by query()/ChatRequest are honored during
confirmation. Ensure to reference execute_confirmed, ConfirmRequest, and
execute_destructive_operation_sync when making the changes.
- Around line 103-121: connect_database currently calls load_database_sync
directly and therefore does not register any background work with the client's
task sink; to make behavior consistent with query() and execute_confirmed(),
wrap the load_database_sync call with the client's _bind_task_sink so any
background tasks get added to _pending_tasks and will be awaited in close();
locate the connect_database method and replace the direct await
load_database_sync(...) call with an awaited call into _bind_task_sink(...)
passing the same coroutine (referencing connect_database, _bind_task_sink,
_pending_tasks, close, and load_database_sync), or if the omission is
intentional add a clear inline comment in connect_database explaining why schema
loading should not be tracked.

In `@queryweaver_sdk/connection.py`:
- Around line 111-119: The close() method currently clears _db/_pool but allows
the db property to lazily recreate connections; add a boolean attribute _closed
on the Connection class, set self._closed = True at the end of close() (and
ensure close() is idempotent), and update the db property getter to check
self._closed and raise RuntimeError("Connection is closed") if true so
post-__aexit__ calls cannot recreate resources; reference the existing close()
method and the db property in your changes.

In `@README.md`:
- Around line 299-304: The README example shows using QueryWeaver instances
(QueryWeaver(...)) and then calling a.query(...) / b.query(...) without
connecting databases first; update the snippet to call the instance method
connect_database (e.g., await a.connect_database("sales") and await
b.connect_database("ops")) before invoking query, or alternatively add a clear
"illustrative only" note above the block; reference QueryWeaver,
connect_database, and query so the change is applied to the correct example.

In `@tests/test_sdk/conftest.py`:
- Around line 8-18: The pytest test markers registered in pytest_configure
currently add only dependency gates (requires_llm, requires_postgres,
requires_mysql); add guideline-required markers by registering "integration" and
"slow" (and optionally "e2e", "auth", "unit") via additional
config.addinivalue_line calls inside pytest_configure, then annotate the real
integration tests (e.g., module tests/test_sdk/test_queryweaver.py) with
module-level pytest marks such as pytestmark = [pytest.mark.integration,
pytest.mark.slow] so the suite is correctly categorized; update the
pytest_configure function and add the pytestmark declaration in
test_queryweaver.py.
- Around line 49-91: The try block creating and using psycopg2's conn and cursor
can leak resources if an exception is raised; update the block that opens conn
and cursor to use context managers (with conn, with conn.cursor()) or ensure
conn.close() and cursor.close() are called in a finally block so the DB
connection/session is always closed before calling pytest.skip; apply the same
fix pattern to the MySQL fixture that uses conn and cursor to prevent leaking
connections on exceptions.

In `@tests/test_sdk/test_queryweaver.py`:
- Around line 269-357: The TestModels class in
tests/test_sdk/test_queryweaver.py lacks a pytest marker; add a unit marker by
importing pytest (if not present) and either decorate the TestModels class with
`@pytest.mark.unit` or add pytestmark = [pytest.mark.unit] at the top of that
class to mark these pure dataclass tests as unit tests (refer to the TestModels
class name to locate the change).
- Around line 87-90: The test test_connect_invalid_url currently uses a broad
pytest.raises(Exception); update it to assert the documented exception by using
pytest.raises(ValueError) when calling
QueryWeaver.connect_database("invalid://url") so the test specifically expects
ValueError (the exception noted in QueryWeaver.connect_database docstring) and
will fail if a different exception type is raised.

---

Outside diff comments:
In `@api/graph.py`:
- Around line 337-350: The current unpacking logic assumes fixed positions in
results and drops column-only searches; instead iterate results in insertion
order and assign them based on which tasks were appended to main_tasks: after
building main_tasks with _find_tables and/or _find_tables_by_columns, consume
results sequentially (use an index or pop from a list) and set tables_des when
table_embeddings was requested and tables_by_columns_des when column_embeddings
was requested so that a single-task run (column-only or table-only) maps
correctly to the right variable (refer to main_tasks, _find_tables,
_find_tables_by_columns, results, tables_des, tables_by_columns_des).

In `@api/loaders/graph_loader.py`:
- Around line 19-32: The new optional DB handle parameter on load_to_graph needs
a type annotation; update the function signature for load_to_graph to type the
db parameter as Optional[...] (e.g., Optional[FalkorDB] or the concrete DB
interface used by resolve_db) and import Optional from typing (or use
typing.Optional), keeping the existing -> None return type; ensure any
references to resolve_db(db) still type-check with the chosen DB type and update
any stub/type imports (FalkorDB or GraphDB interface) so the annotation is
valid.

In `@api/loaders/mysql_loader.py`:
- Around line 469-482: The code calls await MySQLLoader.load(...) but
MySQLLoader.load is an async generator, so replace the await with an async for
loop to consume yielded items (e.g., "async for result in
MySQLLoader.load(prefix, db_url, db=db):" and capture success/message from the
yielded values); also avoid deleting the existing graph before a successful
reload—first run the reload into a temporary/staging graph or buffer and only
call graph.delete() and replace the old graph after the async for confirms
success (adjust the logic in refresh_graph_schema to use the async-for
consumption of MySQLLoader.load and perform the delete/replace only after a
successful load).

In `@api/loaders/postgres_loader.py`:
- Around line 510-523: The code awaits PostgresLoader.load(...) even though load
is an async generator and also deletes the graph before reloading which can
cause reload failures; fix by first running the reload by iterating the async
generator (use "async for progress in PostgresLoader.load(prefix, db_url,
db=db)" to consume progress tuples and capture the final (success, message)),
then only call await graph.delete() or delete the old graph after confirming
success; reference resolve_db.select_graph(graph_id), graph.delete(), graph_id,
prefix, and PostgresLoader.load to locate the code to change.
- Around line 177-180: Replace the brittle split-based extraction of db_name
with urlparse/unquote: parse connection_url via urlparse(connection_url), take
parsed.path, strip any leading '/' (parsed.path.lstrip('/')), unquote the result
to handle percent-encoding, and assign that to db_name; keep a fallback (e.g.,
original split logic) only if the parsed path is empty. Update the code that
sets db_name (the variable assigned from connection_url) to use this parsing
logic so Unix-socket and query-parameter URLs yield the correct database name.

---

Nitpick comments:
In @.github/workflows/tests.yml:
- Around line 121-124: The "Install uv" step uses astral-sh/setup-uv with
version: "latest" which can cause CI drift; change the with.version value to the
pinned release used elsewhere (e.g., "0.7.12") so the sdk-tests job matches
other workflows; locate the step that references astral-sh/setup-uv (the
"Install uv" step) and replace version: "latest" with version: "0.7.12".

In `@Makefile`:
- Around line 83-86: The docker-test-services Makefile target uses a fixed sleep
which is flaky; update the docker compose invocation in the docker-test-services
recipe (the `docker compose -f docker-compose.test.yml up -d` line) to use
`--wait` (i.e., `docker compose -f docker-compose.test.yml up -d --wait`) so it
blocks until the healthchecks in docker-compose.test.yml pass, and remove the
subsequent `sleep 10` (and optional "Waiting for services to be ready..." echo)
to rely on the built-in wait behavior.
- Line 1: The Makefile's .PHONY declaration is missing the newly added phony
targets test-sdk-quick and test-all; update the .PHONY line (the .PHONY entry at
the top of the Makefile) to include test-sdk-quick and test-all so make treats
them as phony targets rather than files, ensuring the targets defined later in
the file are always executed.

In `@queryweaver_sdk/client.py`:
- Around line 207-215: Remove the redundant inline pylint disables on the lazy
imports inside the async method list_databases: delete the trailing "# pylint:
disable=import-outside-toplevel" comments from the two import statements that
import list_databases from api.core.schema_loader and GENERAL_PREFIX from
api.core.text2sql_common so the method uses the module-level disable already
present; ensure the imports remain inside list_databases (preserving lazy import
behavior) and run linters to confirm no new warnings.

In `@queryweaver_sdk/connection.py`:
- Around line 83-86: The close path currently calls the FalkorDB client's
undocumented internal attribute (self._db.connection.aclose()); update the
teardown to use the public API by replacing that call with await
self._db.aclose() in the close() method of the connection class (or
alternatively refactor construction to use BlockingConnectionPool.from_url() for
both creation branches and call pool.disconnect() for a unified cleanup); locate
references to FalkorDB, close(), self._db.connection.aclose(), and adjust them
to use the public aclose() or the
BlockingConnectionPool.from_url()/pool.disconnect() approach so the client is
torn down via its supported API.

In `@tests/test_sdk/conftest.py`:
- Around line 147-154: The queryweaver fixture currently yields a QueryWeaver
instance and calls await qw.close() in teardown which can raise and mask test
failures; update the fixture to either use symmetric lifecycle by creating the
instance inside an async context (async with QueryWeaver(falkordb_url=...,
user_id="test_user") as qw: yield qw) or wrap the teardown call in a try/except
that catches exceptions from qw.close() and logs them (referencing the fixture
name queryweaver, the QueryWeaver constructor, and the qw.close() method) so
close errors don't obscure test failures.

In `@tests/test_sdk/test_queryweaver.py`:
- Around line 24-30: The tests test_init_defaults and
test_init_with_custom_user_id create QueryWeaver instances that may allocate a
FalkorDBConnection; update each test to ensure the connection is closed by
either turning the test into an async test (add `@pytest.mark.asyncio`) and using
"async with QueryWeaver(...)" or by explicitly awaiting qw.close() at the end of
the test; reference QueryWeaver and its close() method (or the context-manager
protocol for QueryWeaver) so the FalkorDBConnection held in _db is properly
cleaned up to prevent leaking live connections.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4ea8b59-e4f1-420a-b1e0-1cab480fd0e5

📥 Commits

Reviewing files that changed from the base of the PR and between d598388 and 04385b6.

⛔ Files ignored due to path filters (2)
  • app/package-lock.json is excluded by !**/package-lock.json
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • .github/wordlist.txt
  • .github/workflows/playwright.yml
  • .github/workflows/pylint.yml
  • .github/workflows/tests.yml
  • Makefile
  • README.md
  • api/core/__init__.py
  • api/core/db_resolver.py
  • api/core/request_models.py
  • api/core/result_models.py
  • api/core/schema_loader.py
  • api/core/text2sql.py
  • api/core/text2sql_common.py
  • api/core/text2sql_sync.py
  • api/graph.py
  • api/loaders/graph_loader.py
  • api/loaders/mysql_loader.py
  • api/loaders/postgres_loader.py
  • api/memory/graphiti_tool.py
  • api/routes/database.py
  • api/routes/graphs.py
  • docker-compose.test.yml
  • pyproject.toml
  • queryweaver_sdk/__init__.py
  • queryweaver_sdk/client.py
  • queryweaver_sdk/connection.py
  • queryweaver_sdk/models.py
  • tests/test_sdk/__init__.py
  • tests/test_sdk/conftest.py
  • tests/test_sdk/test_queryweaver.py
💤 Files with no reviewable changes (1)
  • api/routes/database.py

Comment on lines +136 to +143
- name: Run SDK tests
env:
FALKORDB_URL: redis://localhost:6379
TEST_POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/testdb
TEST_MYSQL_URL: mysql://root:root@localhost:3306/testdb
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
uv run python -m pytest tests/test_sdk/ -v
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.

⚠️ Potential issue | 🟡 Minor

SDK tests will fail on PRs from forks due to missing OPENAI_API_KEY.

secrets.OPENAI_API_KEY is not available to workflows triggered by pull_request from forked repositories, so this job will run with an empty key and the tests that hit the LLM will fail. Also, if an internal developer forgets to set the secret, the only signal is a late test failure.

Consider either:

  • Skipping this job when the secret is empty (e.g., if: ${{ secrets.OPENAI_API_KEY != '' }} at job level, or a pre-flight step that fails with a clearer message).
  • Splitting into a non-LLM subset that can run on all PRs (similar to test-sdk-quick in the Makefile) and gating the LLM subset behind a label or pull_request_target with appropriate safeguards.
🧰 Tools
🪛 Checkov (3.2.519)

[medium] 139-140: Basic Auth Credentials

(CKV_SECRET_4)


[medium] 140-141: Basic Auth Credentials

(CKV_SECRET_4)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml around lines 136 - 143, The "Run SDK tests" job
will run without OPENAI_API_KEY for forked PRs causing LLM tests to fail; update
the workflow so LLM-dependent tests only run when the secret is present by
making one of these changes: add a job-level condition like if: ${{
secrets.OPENAI_API_KEY != '' }} to the "Run SDK tests" job (or split the job
into a non-LLM "test-sdk-quick" job and a separate "Run SDK LLM tests" job gated
by the secret or by a label/pull_request_target with safeguards), or add an
early pre-flight step in "Run SDK tests" that checks OPENAI_API_KEY and fails
with a clear message if empty; ensure the change references the OPENAI_API_KEY
env var used in the job so LLM tests are skipped when the secret is unavailable.

Comment thread api/core/db_resolver.py
Comment on lines +12 to +18
def resolve_db(db=None):
"""Return the given ``db`` handle, or lazily import the server default."""
if db is not None:
return db
# pylint: disable=import-outside-toplevel
from api.extensions import db as _default_db
return _default_db
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.

⚠️ Potential issue | 🟡 Minor

Add type hints to the resolver boundary.

resolve_db is now shared core API; typing the injected handle keeps the new dependency-injection boundary explicit. As per coding guidelines, api/**/*.py: Include type hints throughout Python code.

Proposed fix
+from typing import Any
+
+
-def resolve_db(db=None):
+def resolve_db(db: Any | None = None) -> Any:
     """Return the given ``db`` handle, or lazily import the server default."""
     if db is not None:
         return db
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/core/db_resolver.py` around lines 12 - 18, Add explicit type hints to the
resolve_db boundary: import typing (e.g., "from typing import Optional, Any")
and change the signature of resolve_db to accept db: Optional[Any] = None and
return Any, and annotate the lazily imported api.extensions.db (imported as
_default_db) as the same type so the function returns a typed handle; update any
local comments or # type: ignore as needed when importing _default_db to satisfy
the type checker.

Comment on lines +67 to +71
graph_id = graph_id.strip()[:200]
if not graph_id:
raise GraphNotFoundError(
"Invalid graph_id, must be less than 200 characters."
)
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.

⚠️ Potential issue | 🟠 Major

Raise InvalidArgumentError for invalid graph IDs.

An empty/blank graph_id is a bad request, not a missing graph. Several callers catch InvalidArgumentError for 400 handling, so raising GraphNotFoundError can produce wrong SDK/HTTP behavior.

Proposed fix
     graph_id = graph_id.strip()[:200]
     if not graph_id:
-        raise GraphNotFoundError(
-            "Invalid graph_id, must be less than 200 characters."
-        )
+        raise InvalidArgumentError("graph_id cannot be empty")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/core/text2sql_common.py` around lines 67 - 71, The code currently raises
GraphNotFoundError for an invalid/blank graph_id; change this to raise
InvalidArgumentError instead so callers treat it as a 400 Bad Request. Locate
the block where graph_id is normalized (graph_id = graph_id.strip()[:200]) and
replace the GraphNotFoundError instantiation with an InvalidArgumentError
containing the same or similar message ("Invalid graph_id, must be less than 200
characters."). Ensure the exception import or reference for InvalidArgumentError
is present/used in text2sql_common.py so the new exception type is available.

Comment on lines +140 to +143
def detect_destructive_operation(sql_query: str) -> tuple[str, bool]:
"""Return ``(sql_type, is_destructive)`` for a SQL statement."""
sql_type = sql_query.strip().split()[0].upper() if sql_query else ""
return sql_type, sql_type in DESTRUCTIVE_OPS
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.

⚠️ Potential issue | 🔴 Critical

Harden destructive-SQL detection against leading comments.

strip().split()[0] classifies -- comment\nDROP TABLE ... as non-destructive, bypassing confirmation/demo-graph protections before execution. Strip leading SQL comments before extracting the statement verb.

Proposed direction
+import re
+
+_LEADING_SQL_COMMENTS = re.compile(
+    r"^\s*(?:(?:--[^\n]*(?:\n|$))|(?:/\*.*?\*/))*",
+    re.DOTALL,
+)
+
 def detect_destructive_operation(sql_query: str) -> tuple[str, bool]:
     """Return ``(sql_type, is_destructive)`` for a SQL statement."""
-    sql_type = sql_query.strip().split()[0].upper() if sql_query else ""
+    normalized = _LEADING_SQL_COMMENTS.sub("", sql_query or "").strip()
+    sql_type = normalized.split()[0].upper() if normalized else ""
     return sql_type, sql_type in DESTRUCTIVE_OPS
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/core/text2sql_common.py` around lines 140 - 143, The function
detect_destructive_operation currently uses sql_query.strip().split()[0] which
misclassifies queries with leading comments (e.g., "-- comment\nDROP TABLE
..."); update detect_destructive_operation to first remove leading SQL comments
and whitespace before extracting the first token: skip or strip initial
single-line comments beginning with "--" and initial block comments "/* ... */"
(repeated until the first non-comment token), then extract the verb, uppercase
it, and check against DESTRUCTIVE_OPS while safely handling
empty/whitespace-only input.

Comment thread api/core/text2sql_sync.py
Comment on lines +152 to +158
chat_ctx = _ChatContext(
queries_history=queries_history,
result_history=result_history,
instructions=instructions,
custom_api_key=getattr(chat_data, 'custom_api_key', None),
custom_model=getattr(chat_data, 'custom_model', None),
)
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether SDK/request models already validate custom_model and custom_api_key.
rg -n -C3 "custom_model|custom_api_key|vendor/model|API key is too short|Unsupported vendor"

Repository: FalkorDB/QueryWeaver

Length of output: 20746


🏁 Script executed:

cat -n api/core/request_models.py | head -100

Repository: FalkorDB/QueryWeaver

Length of output: 1505


🏁 Script executed:

sed -n '140,170p' api/core/text2sql_sync.py

Repository: FalkorDB/QueryWeaver

Length of output: 1104


🏁 Script executed:

sed -n '110,165p' api/core/text2sql_sync.py

Repository: FalkorDB/QueryWeaver

Length of output: 2030


🏁 Script executed:

rg -n "_initialize_query_context|validate_and_truncate_chat" api/core/text2sql_sync.py

Repository: FalkorDB/QueryWeaver

Length of output: 279


🏁 Script executed:

sed -n '335,365p' api/core/text2sql_sync.py

Repository: FalkorDB/QueryWeaver

Length of output: 1202


🏁 Script executed:

rg -n "query_database_sync" api/

Repository: FalkorDB/QueryWeaver

Length of output: 170


🏁 Script executed:

sed -n '170,190p' queryweaver_sdk/client.py

Repository: FalkorDB/QueryWeaver

Length of output: 787


🏁 Script executed:

rg -n "ChatRequest|query_database_sync" api/routes/ api/api.py 2>/dev/null | head -30

Repository: FalkorDB/QueryWeaver

Length of output: 302


🏁 Script executed:

sed -n '130,200p' api/routes/graphs.py

Repository: FalkorDB/QueryWeaver

Length of output: 2943


🏁 Script executed:

rg -n "async def query_database" api/core/

Repository: FalkorDB/QueryWeaver

Length of output: 322


🏁 Script executed:

sed -n '169,220p' api/core/text2sql.py

Repository: FalkorDB/QueryWeaver

Length of output: 2518


Mirror per-request override validation in the SDK path.

The streaming path (api/core/text2sql.py, lines 202–216) validates custom_model format and custom_api_key length, but the sync path (api/core/text2sql_sync.py, lines 152–158) passes them directly to agents without validation. Add the same validation in the sync path initializer before constructing _ChatContext.

custom_model: must be "vendor/model" with vendor in (openai, anthropic, gemini, azure, ollama, cohere)
custom_api_key: must be ≥10 characters if provided
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/core/text2sql_sync.py` around lines 152 - 158, Add the same per-request
override validation used in the streaming path before constructing _ChatContext:
validate getattr(chat_data, 'custom_model', None) follows the "vendor/model"
pattern with vendor one of (openai, anthropic, gemini, azure, ollama, cohere)
and validate getattr(chat_data, 'custom_api_key', None) is either None or has
length >= 10; if either check fails, raise a clear ValueError (or similar) so
invalid overrides are rejected before creating chat_ctx in text2sql_sync.py
where _ChatContext is instantiated.

Comment thread README.md
Comment on lines +299 to +304
```python
async with QueryWeaver(falkordb_url="redis://host-a:6379", user_id="tenant_a") as a, \
QueryWeaver(falkordb_url="redis://host-b:6379", user_id="tenant_b") as b:
await a.query("sales", "Show top customers")
await b.query("ops", "Count open tickets")
```
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.

⚠️ Potential issue | 🟡 Minor

Illustrative snippet passes un-connected database IDs.

The example calls a.query("sales", ...) / b.query("ops", ...) without a prior connect_database, which will fail against the real SDK. Consider either adding a connect step or explicitly labeling the block as illustrative so users don't copy it as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 299 - 304, The README example shows using QueryWeaver
instances (QueryWeaver(...)) and then calling a.query(...) / b.query(...)
without connecting databases first; update the snippet to call the instance
method connect_database (e.g., await a.connect_database("sales") and await
b.connect_database("ops")) before invoking query, or alternatively add a clear
"illustrative only" note above the block; reference QueryWeaver,
connect_database, and query so the change is applied to the correct example.

Comment on lines +8 to +18
def pytest_configure(config):
"""Configure pytest with custom markers."""
config.addinivalue_line(
"markers", "requires_llm: mark test as requiring LLM API key"
)
config.addinivalue_line(
"markers", "requires_postgres: mark test as requiring PostgreSQL"
)
config.addinivalue_line(
"markers", "requires_mysql: mark test as requiring MySQL"
)
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.

🛠️ Refactor suggestion | 🟠 Major

Add guideline-required test markers.

The registered markers (requires_llm, requires_postgres, requires_mysql) are dependency gates and are fine, but per coding guidelines tests should additionally be marked with one of e2e, slow, auth, integration, unit. Since this whole suite talks to real FalkorDB/Postgres/MySQL + an LLM, these tests are integration (arguably e2e + slow). Consider registering those markers here (if not already in root conftest.py/pytest.ini) and applying them in tests/test_sdk/test_queryweaver.py — e.g. via pytestmark = [pytest.mark.integration, pytest.mark.slow] at the module level.

As per coding guidelines: "Mark test functions with pytest markers: e2e, slow, auth, integration, unit".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sdk/conftest.py` around lines 8 - 18, The pytest test markers
registered in pytest_configure currently add only dependency gates
(requires_llm, requires_postgres, requires_mysql); add guideline-required
markers by registering "integration" and "slow" (and optionally "e2e", "auth",
"unit") via additional config.addinivalue_line calls inside pytest_configure,
then annotate the real integration tests (e.g., module
tests/test_sdk/test_queryweaver.py) with module-level pytest marks such as
pytestmark = [pytest.mark.integration, pytest.mark.slow] so the suite is
correctly categorized; update the pytest_configure function and add the
pytestmark declaration in test_queryweaver.py.

Comment on lines +49 to +91
try:
import psycopg2
conn = psycopg2.connect(url)
cursor = conn.cursor()

# Create test tables (DROP + CREATE ensures a clean slate)
cursor.execute("""
DROP TABLE IF EXISTS orders CASCADE;
DROP TABLE IF EXISTS customers CASCADE;

CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
city VARCHAR(100)
);

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
product VARCHAR(100),
amount DECIMAL(10,2),
order_date DATE
);

-- Insert test data (UNIQUE on email allows ON CONFLICT)
INSERT INTO customers (name, email, city) VALUES
('Alice Smith', '[email protected]', 'New York'),
('Bob Jones', '[email protected]', 'Los Angeles'),
('Carol White', '[email protected]', 'New York')
ON CONFLICT (email) DO NOTHING;

INSERT INTO orders (customer_id, product, amount, order_date) VALUES
(1, 'Widget', 29.99, '2024-01-15'),
(1, 'Gadget', 49.99, '2024-01-20'),
(2, 'Widget', 29.99, '2024-02-01');
""")
conn.commit()
conn.close()
except Exception as e:
pytest.skip(f"PostgreSQL not available: {e}")

return url
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.

⚠️ Potential issue | 🟡 Minor

Leaked psycopg2 connection/cursor on exception.

If any of the DDL/DML statements raise, control jumps to the except block and conn.close() is never called — the connection (and its server-side session) leaks until garbage-collected. Same pattern in the MySQL fixture (lines 111‑144).

🛠 Proposed fix using a `with` block
-    try:
-        import psycopg2
-        conn = psycopg2.connect(url)
-        cursor = conn.cursor()
-
-        # Create test tables (DROP + CREATE ensures a clean slate)
-        cursor.execute("""
+    try:
+        import psycopg2
+        with psycopg2.connect(url) as conn:
+            with conn.cursor() as cursor:
+                cursor.execute("""
             DROP TABLE IF EXISTS orders CASCADE;
             ...
-        """)
-        conn.commit()
-        conn.close()
+                """)
+            conn.commit()
     except Exception as e:
         pytest.skip(f"PostgreSQL not available: {e}")
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 88-88: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sdk/conftest.py` around lines 49 - 91, The try block creating and
using psycopg2's conn and cursor can leak resources if an exception is raised;
update the block that opens conn and cursor to use context managers (with conn,
with conn.cursor()) or ensure conn.close() and cursor.close() are called in a
finally block so the DB connection/session is always closed before calling
pytest.skip; apply the same fix pattern to the MySQL fixture that uses conn and
cursor to prevent leaking connections on exceptions.

Comment on lines +87 to +90
@pytest.mark.asyncio
async def test_connect_invalid_url(self, queryweaver):
with pytest.raises(Exception): # noqa: B017 - expect InvalidArgumentError or subclass
await queryweaver.connect_database("invalid://url")
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.

⚠️ Potential issue | 🟡 Minor

Narrow pytest.raises(Exception) to the expected error type.

The # noqa: B017 suppression and the comment both acknowledge this is suboptimal. If the SDK is documented to raise ValueError (see QueryWeaver.connect_database docstring: "Raises: ValueError"), tighten this to pytest.raises(ValueError) so the test fails if the SDK starts raising e.g. a bare ConnectionError for an unparsable URL — which would otherwise slip past this test.

-    async def test_connect_invalid_url(self, queryweaver):
-        with pytest.raises(Exception):  # noqa: B017 - expect InvalidArgumentError or subclass
-            await queryweaver.connect_database("invalid://url")
+    async def test_connect_invalid_url(self, queryweaver):
+        with pytest.raises(ValueError):
+            await queryweaver.connect_database("invalid://url")
📝 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
@pytest.mark.asyncio
async def test_connect_invalid_url(self, queryweaver):
with pytest.raises(Exception): # noqa: B017 - expect InvalidArgumentError or subclass
await queryweaver.connect_database("invalid://url")
`@pytest.mark.asyncio`
async def test_connect_invalid_url(self, queryweaver):
with pytest.raises(ValueError):
await queryweaver.connect_database("invalid://url")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sdk/test_queryweaver.py` around lines 87 - 90, The test
test_connect_invalid_url currently uses a broad pytest.raises(Exception); update
it to assert the documented exception by using pytest.raises(ValueError) when
calling QueryWeaver.connect_database("invalid://url") so the test specifically
expects ValueError (the exception noted in QueryWeaver.connect_database
docstring) and will fail if a different exception type is raised.

Comment on lines +269 to +357
class TestModels:
"""Dataclass serialization / defaults."""

def test_query_result_to_dict(self):
result = QueryResult(
sql_query="SELECT * FROM customers",
results=[{"id": 1, "name": "Alice"}],
ai_response="Found 1 customer",
metadata=QueryMetadata(
confidence=0.95,
is_destructive=False,
requires_confirmation=False,
execution_time=0.5,
),
)

d = result.to_dict()
assert d["sql_query"] == "SELECT * FROM customers"
assert d["confidence"] == 0.95
assert d["results"] == [{"id": 1, "name": "Alice"}]
assert d["ai_response"] == "Found 1 customer"
assert d["is_destructive"] is False
assert d["requires_confirmation"] is False
assert d["execution_time"] == 0.5

def test_schema_result_to_dict(self):
result = SchemaResult(
nodes=[{"id": "customers", "name": "customers"}],
links=[{"source": "orders", "target": "customers"}],
)

d = result.to_dict()
assert d["nodes"][0]["name"] == "customers"
assert d["links"][0]["source"] == "orders"
assert d["links"][0]["target"] == "customers"

def test_database_connection_to_dict(self):
result = DatabaseConnection(
database_id="testdb",
success=True,
tables_loaded=5,
message="Connected successfully",
)

d = result.to_dict()
assert d["database_id"] == "testdb"
assert d["success"] is True
assert d["tables_loaded"] == 5
assert d["message"] == "Connected successfully"

def test_query_result_default_values(self):
result = QueryResult(
sql_query="SELECT 1",
results=[],
ai_response="Test",
metadata=QueryMetadata(confidence=0.8),
)

assert result.is_destructive is False
assert result.requires_confirmation is False
assert result.execution_time == 0.0
assert result.is_valid is True
assert result.missing_information == ""
assert result.ambiguities == ""
assert result.explanation == ""

def test_database_connection_failure(self):
result = DatabaseConnection(
database_id="",
success=False,
tables_loaded=0,
message="Connection refused",
)

d = result.to_dict()
assert d["database_id"] == ""
assert d["success"] is False
assert d["tables_loaded"] == 0
assert "refused" in d["message"].lower()

def test_query_request_custom_model_fields(self):
"""custom_api_key/custom_model are threaded through to agents."""
req = QueryRequest(
question="test",
custom_api_key="sk-test-123",
custom_model="openai/gpt-4.1",
)
assert req.custom_api_key == "sk-test-123"
assert req.custom_model == "openai/gpt-4.1"
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.

🛠️ Refactor suggestion | 🟠 Major

Pure‑unit model tests should be explicitly marked unit.

TestModels (and only TestModels) doesn't require FalkorDB/Postgres/MySQL/LLM — it's a pure dataclass test. Per coding guidelines every test function should carry one of the standard markers (e2e, slow, auth, integration, unit). Mark this class with @pytest.mark.unit and the rest of the classes (which talk to real services) with @pytest.mark.integration (+ @pytest.mark.slow where LLM is involved) — ideally via a module/class pytestmark = [...] to avoid per-test noise.

As per coding guidelines: "Mark test functions with pytest markers: e2e, slow, auth, integration, unit".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sdk/test_queryweaver.py` around lines 269 - 357, The TestModels
class in tests/test_sdk/test_queryweaver.py lacks a pytest marker; add a unit
marker by importing pytest (if not present) and either decorate the TestModels
class with `@pytest.mark.unit` or add pytestmark = [pytest.mark.unit] at the top
of that class to mark these pure dataclass tests as unit tests (refer to the
TestModels class name to locate the change).

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