text-to-sql-agent-langgraph

Troubleshooting

Common failure modes, what they mean, and what to actually do about them — including several observed directly while building/auditing this project, not just theoretical.

Database connection failures

python scripts/test_db_connection.py (or the UI’s “Test Connection” button, or GET /health) classifies every connection failure via db.connection.ConnectionErrorCategory — the category and guidance below are exactly what you’ll see, made browsable here:

Category What it means What to do
configuration .env itself is malformed or missing a required field before a connection was even attempted. Check .env for missing/invalid DB_* settings — see docs/CONFIGURATION.md.
driver_missing The Python driver package for your DB_TYPE isn’t installed. pip install -r requirements.txt — all four drivers are listed unconditionally so switching DB_TYPE never needs a fresh install.
auth_failure The server was reachable but rejected the credentials. Check DB_USER/DB_PASSWORD. If you rotated a password, restart the process — Settings is a cached singleton (see docs/CONFIGURATION.md).
host_unreachable Couldn’t establish a TCP connection at all. Check DB_HOST, DB_PORT, VPN, and firewall rules. If running in Docker, see docs/DEPLOYMENT.md’s networking notes.
database_not_found The server was reachable and auth succeeded, but the named database/catalog doesn’t exist. Check DB_NAME for a typo.
timeout The connection attempt itself timed out (distinct from QUERY_TIMEOUT_SECONDS, which is about query execution). Check network path/latency to the host.
unknown Doesn’t match any of the above known patterns. Read the driver error text included alongside the category — it’s never hidden, only classified.

Classification is best-effort keyword matching on the underlying driver’s own error text (db/connection.py::_classify_error) — it’s not authoritative, and the full original error message is always included in the result, not replaced by the category.

“The connected database role appears to have write privileges”

A warning, not an error — surfaced by db.connection.check_write_privileges in the UI sidebar and scripts/test_db_connection.py’s output. It means your DB_USER has INSERT/UPDATE/DELETE grants, which this app never uses but which is your real safety boundary if the SQL validator were ever bypassed (see SECURITY.md’s “What is explicitly not guaranteed”). Fix it by pointing DB_USER at a genuinely read-only database role, not by suppressing the warning — this check itself can’t restrict privileges, it can only tell you they’re wider than they should be. (This exact warning fired during this project’s own reference audit run against its dev database — see docs/RISK_REGISTER.md’s R-002 area and docs/PRODUCTION_READINESS_REPORT.md — it is a real, easy-to-hit misconfiguration, not a hypothetical.)

SAWarning: Unrecognized server version info

Harmless. SQLAlchemy’s dialect has a known list of server versions it recognizes for feature-detection purposes; a newer database engine version than the driver ships metadata for triggers this warning but doesn’t prevent connecting or querying. Safe to ignore; update the driver package if it bothers you.

Ollama

Chroma / schema index

Windows / ODBC (DB_TYPE=mssql)

Requires the Microsoft ODBC Driver for SQL Server installed as a system package (Control Panel → ODBC Data Sources on Windows, odbcinst -j to check on Linux/macOS) — not pip-installable, and the one DB_TYPE with this extra manual step. DB_ODBC_DRIVER in .env must exactly match an installed driver name ("ODBC Driver 17 for SQL Server", "ODBC Driver 18 for SQL Server", …). In Docker, see docs/DEPLOYMENT.md’s mssql section for the extra image layer this needs.

Quality gate (lint/type-check/test) failures

Docker

Streamlit crashes / “Connection error” popup after a query with a date column

A real, observed segfault: pandas==2.2.3 predates real Python 3.14 wheels and crashes (Windows fatal exception: access violation deep in pandas.core.arrays.datetimes._construct_from_dt64_naive) building a DataFrame from any row data containing a raw datetime.datetime value — i.e. any query result with a date/datetime column, via ui/app.py’s pd.DataFrame(rows, columns=columns). This kills the whole Streamlit process (not a catchable Python exception), which is what the browser shows as a generic “Connection error / is Streamlit still running?” toast. Fixed by pinning pandas==2.3.3 in requirements.txt (see its own comment) — if you see this exact crash shape after touching dependency pins, check that pin hasn’t regressed. Reproduces in two lines with no app code involved:

import datetime, pandas as pd
pd.DataFrame([(1, datetime.datetime(1990, 1, 1))], columns=["id", "d"])

“Invalid object name ‘X’” for a table that genuinely exists

Happens on a database whose real tables live under a non-default schema (DB_<NAME>_SCHEMA set to something other than dbo/public) — an engine resolves an unqualified table name against the connecting user’s own default schema, not the schema you configured for introspection. The model correctly sees and generates FROM Employee (schema-qualification was never part of the DDL it was shown — see CLAUDE.md’s multi-database note on why table_descriptions.yaml/schema DDL stay bare-named), but bare Employee then fails to resolve if the connection’s actual default schema is dbo and the table is really employee.Employee. Fixed by agent.sql_validator.qualify_table_schema, applied only at the execution step, transparently, using DB_<NAME>_SCHEMA — you shouldn’t see this anymore for a correctly configured DB_<NAME>_SCHEMA. If you do, confirm the configured schema name actually matches where the tables live (SELECT SCHEMA_NAME(schema_id), name FROM sys.tables on SQL Server) rather than assuming the connection’s default.

Multi-source router picked the wrong source, or a source you configured isn’t offered

See docs/MULTI_SOURCE_GUIDE.md — almost always either a missed app restart after a .env change (Settings is a cached singleton, same as every other setting — see above), or a genuinely ambiguous/compound question. Check the [router] available=[...] sources=[...] reasoning=... log line before assuming something’s broken.

“It rejected my question and I don’t know why”

By design, a rejection message is deliberately generic ("I couldn't process that question. Try rephrasing it...") rather than naming exactly which pattern or rule matched — see SECURITY.md’s reasoning (“a rejection must not confirm to an attacker exactly what was detected”). If you believe a legitimate question was rejected incorrectly, check the terminal/application logs (agent.input_guard’s own logger, security.audit’s structured event) for the actual reason code, or open an issue with the exact question text per SECURITY.md’s “Reporting a vulnerability” section if it looks like a false positive worth fixing.