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

  1. Send one statement per tool call (drop the extra semicolon-separated statements).
  2. If multi-statement reads are truly needed, set allow_dml=True or env CREWAI_NL2SQL_ALLOW_DML=true — understanding this also permits writes.
  3. Pre-split batches in your orchestrator and loop over single statements.
  4. 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

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


AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15). Data as JSON: /api/errors/b9ea8b29fdfaddeb. Report an issue: GitHub.