{"record":{"id":"b9ea8b29fdfaddeb","repo":"crewAIInc/crewAI","slug":"nl2sqltool-blocked-a-multi-statement-query-in-read","errorCode":null,"errorMessage":"NL2SQLTool blocked a multi-statement query in read-only mode. Semicolons are not permitted when allow_dml=False.","messagePattern":"NL2SQLTool blocked a multi-statement query in read-only mode\\. Semicolons are not permitted when allow_dml=False\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py","lineNumber":308,"sourceCode":"\n    # Query validation\n\n    def _validate_query(self, sql_query: str) -> None:\n        \"\"\"Raise ValueError if *sql_query* is not permitted under the current config.\n\n        Splits the query on semicolons and validates each statement\n        independently.  When ``allow_dml=False`` (the default), multi-statement\n        queries are rejected outright to prevent ``SELECT 1; DROP TABLE users``\n        style bypasses.  When ``allow_dml=True`` every statement is checked and\n        a warning is emitted for write operations.\n        \"\"\"\n        statements = [s.strip() for s in sql_query.split(\";\") if s.strip()]\n\n        if not statements:\n            raise ValueError(\"NL2SQLTool received an empty SQL query.\")\n\n        if not self.allow_dml and len(statements) > 1:\n            raise ValueError(\n                \"NL2SQLTool blocked a multi-statement query in read-only mode. \"\n                \"Semicolons are not permitted when allow_dml=False.\"\n            )\n\n        for stmt in statements:\n            self._validate_statement(stmt)\n\n    def _validate_statement(self, stmt: str) -> None:\n        \"\"\"Validate a single SQL statement (no semicolons).\"\"\"\n        command = self._extract_command(stmt)\n\n        # EXPLAIN ANALYZE / EXPLAIN ANALYSE actually *executes* the underlying\n        # query.  Resolve the real command so write operations are caught.\n        # parenthesized (\"EXPLAIN (ANALYZE) DELETE …\", \"EXPLAIN (ANALYZE, VERBOSE) DELETE …\").\n        # EXPLAIN ANALYZE actually executes the underlying query — resolve the\n        # real command so write operations are caught.\n        if command == \"EXPLAIN\":\n            resolved = _resolve_explain_command(stmt)","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/crewAIInc/crewAI/blob/754d7323beb2fd042e33444a115ea2d5a47193f0/lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py#L290-L326","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\ntool._run(\"SELECT 1; SELECT COUNT(*) FROM users\")\n\n# after\nfor stmt in [\"SELECT 1\", \"SELECT COUNT(*) FROM users\"]:\n    tool._run(stmt)","handlingStrategy":"validation","validationCode":"def is_single_statement(sql: str) -> bool:\n    return len([s for s in sql.split(\";\") if s.strip()]) <= 1\n\nif read_only_mode and not is_single_statement(sql):\n    sql = sql.split(\";\")[0]  # or reject explicitly","typeGuard":null,"tryCatchPattern":"try:\n    tool._run(sql)\nexcept ValueError as e:\n    if \"multi-statement\" in str(e):\n        for stmt in (s for s in sql.split(\";\") if s.strip()):\n            tool._run(stmt)","preventionTips":["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."],"tags":["nl2sql","sql-injection-guard","read-only","validation","security"],"backgroundTag":null,"analyzedSha":"754d7323beb2fd042e33444a115ea2d5a47193f0","analyzedAt":"2026-08-15T04:06:56.746Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}