crewAIInc/crewAI · error · ValueError
NL2SQLTool blocked a multi-statement query in read-only mode
Error message
NL2SQLTool blocked a multi-statement query in read-only mode. Semicolons are not permitted when allow_dml=False.
What it means
In the default read-only mode (allow_dml=False), NL2SQLTool rejects any sql_query that splits into more than one statement, because a second statement could be a smuggled write (e.g. 'SELECT 1; DROP TABLE users'). The ValueError names this exact rule: no semicolons allowed when allow_dml=False.
Source
Thrown at lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py:308
# Query validation
def _validate_query(self, sql_query: str) -> None:
"""Raise ValueError if *sql_query* is not permitted under the current config.
Splits the query on semicolons and validates each statement
independently. When ``allow_dml=False`` (the default), multi-statement
queries are rejected outright to prevent ``SELECT 1; DROP TABLE users``
style bypasses. When ``allow_dml=True`` every statement is checked and
a warning is emitted for write operations.
"""
statements = [s.strip() for s in sql_query.split(";") if s.strip()]
if not statements:
raise ValueError("NL2SQLTool received an empty SQL query.")
if not self.allow_dml and len(statements) > 1:
raise ValueError(
"NL2SQLTool blocked a multi-statement query in read-only mode. "
"Semicolons are not permitted when allow_dml=False."
)
for stmt in statements:
self._validate_statement(stmt)
def _validate_statement(self, stmt: str) -> None:
"""Validate a single SQL statement (no semicolons)."""
command = self._extract_command(stmt)
# EXPLAIN ANALYZE / EXPLAIN ANALYSE actually *executes* the underlying
# query. Resolve the real command so write operations are caught.
# parenthesized ("EXPLAIN (ANALYZE) DELETE …", "EXPLAIN (ANALYZE, VERBOSE) DELETE …").
# EXPLAIN ANALYZE actually executes the underlying query — resolve the
# real command so write operations are caught.
if command == "EXPLAIN":
resolved = _resolve_explain_command(stmt)View on GitHub (pinned to 754d7323be)
Solutions
- Send one statement per tool call (drop the extra semicolon-separated statements).
- If multi-statement reads are truly needed, set allow_dml=True or env CREWAI_NL2SQL_ALLOW_DML=true — understanding this also permits writes.
- Pre-split batches in your orchestrator and loop over single statements.
- Better: keep read-only mode and run each statement through a separate NL2SQLTool invocation.
Example fix
# before
tool._run("SELECT 1; SELECT COUNT(*) FROM users")
# after
for stmt in ["SELECT 1", "SELECT COUNT(*) FROM users"]:
tool._run(stmt) Defensive patterns
Strategy: validation
Validate before calling
def is_single_statement(sql: str) -> bool:
return len([s for s in sql.split(";") if s.strip()]) <= 1
if read_only_mode and not is_single_statement(sql):
sql = sql.split(";")[0] # or reject explicitly Try / catch
try:
tool._run(sql)
except ValueError as e:
if "multi-statement" in str(e):
for stmt in (s for s in sql.split(";") if s.strip()):
tool._run(stmt) Prevention
- Default to one statement per tool call in agent prompts.
- Strip trailing semicolons before submitting.
- Reserve allow_dml=True for tools wired to a write-enabled, low-privilege DB user.
When it happens
Trigger: Passing any multi-statement query such as 'SELECT * FROM a; SELECT * FROM b' while allow_dml=False (default) and CREWAI_NL2SQL_ALLOW_DML is unset. Trailing semicolon plus another statement, or an LLM that appends multiple queries in one tool call.
Common situations: LLM agents batching several queries into one tool call; copied SQL scripts with trailing semicolons; teams that later want read-only defaults but wrote code assuming multi-statement execution.
Related errors
- NL2SQLTool is configured in read-only mode and blocked a wri
- NL2SQLTool is configured in read-only mode and blocked a '{c
- NL2SQLTool is configured in read-only mode and blocked a '{m
- NL2SQLTool blocked an unrecognised SQL command '{main_cmd}'
- NL2SQLTool blocked an unrecognised SQL command '{command}'.
AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15).
Data as JSON: /api/errors/b9ea8b29fdfaddeb.
Report an issue: GitHub.