crewAIInc/crewAI · warning · ValueError

NL2SQLTool received an empty SQL query.

Error message

NL2SQLTool received an empty SQL query.

What it means

NL2SQLTool._validate_query splits the submitted sql_query on semicolons and discards empty fragments; if nothing remains, the query was empty or only whitespace/semicolons, and a ValueError is raised. This guards the executor against empty statements before touching the database.

Source

Thrown at lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py:305

        self.tables = tables
        self.columns = data

    # 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

View on GitHub (pinned to 754d7323be)

Solutions

  1. Log the raw sql_query the agent actually sent and fix the prompt/template so a real SQL statement is passed.
  2. Reject empty queries in your own code before invoking the tool.
  3. If the agent may legitimately have nothing to ask, add an early branch that skips the tool call.

Example fix

# before
tool._run("")

# after
if not sql_query or not sql_query.strip("; \t\n"):
    raise ValueError("refusing to run empty SQL")
tool._run(sql_query)
Defensive patterns

Strategy: validation

Validate before calling

def has_statement(sql: str) -> bool:
    return any(s.strip() for s in sql.split(";"))

if not has_statement(sql_query):
    raise ValueError("no SQL statement to run")

Type guard

def is_non_empty_sql(sql: str | None) -> bool:
    return isinstance(sql, str) and bool(sql.strip("; \t\r\n"))

Try / catch

try:
    tool._run(sql_query)
except ValueError as e:
    if "empty SQL query" in str(e):
        # ask the agent to regenerate the query
        ...

Prevention

When it happens

Trigger: Calling the tool (or _run) with sql_query="", sql_query=" ", or sql_query=";;;". Typically an LLM agent emits an empty string because it put the SQL in the wrong field or failed to generate a query.

Common situations: Agent produces an empty tool call argument; upstream prompt/variable interpolation yields an empty string; a templating bug passes the placeholder name instead of its value.

Related errors


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