{"record":{"id":"6a17eee993075cda","repo":"crewAIInc/crewAI","slug":"nl2sqltool-blocked-an-unrecognised-sql-command-m","errorCode":null,"errorMessage":"NL2SQLTool blocked an unrecognised SQL command '{main_cmd}' after a CTE. Only {sorted(_READ_ONLY_COMMANDS)} are allowed in read-only mode.","messagePattern":"NL2SQLTool blocked an unrecognised SQL command '(.+?)' after a CTE\\. Only (.+?) are allowed in read-only mode\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py","lineNumber":365,"sourceCode":"\n            main_query = _extract_main_query_after_cte(stmt)\n            if main_query:\n                main_cmd = main_query.split()[0].upper().rstrip(\";\")\n                if main_cmd in _WRITE_COMMANDS:\n                    if not self.allow_dml:\n                        raise ValueError(\n                            f\"NL2SQLTool is configured in read-only mode and blocked a \"\n                            f\"'{main_cmd}' statement after a CTE. To allow write \"\n                            f\"operations set allow_dml=True or \"\n                            f\"CREWAI_NL2SQL_ALLOW_DML=true.\"\n                        )\n                    logger.warning(\n                        \"NL2SQLTool: executing '%s' after CTE because allow_dml=True.\",\n                        main_cmd,\n                    )\n                elif main_cmd not in _READ_ONLY_COMMANDS:\n                    if not self.allow_dml:\n                        raise ValueError(\n                            f\"NL2SQLTool blocked an unrecognised SQL command '{main_cmd}' \"\n                            f\"after a CTE. Only {sorted(_READ_ONLY_COMMANDS)} are allowed \"\n                            f\"in read-only mode.\"\n                        )\n            return\n\n        if command in _WRITE_COMMANDS:\n            if not self.allow_dml:\n                raise ValueError(\n                    f\"NL2SQLTool is configured in read-only mode and blocked a \"\n                    f\"'{command}' statement. To allow write operations set \"\n                    f\"allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true.\"\n                )\n            logger.warning(\n                \"NL2SQLTool: executing write statement '%s' because allow_dml=True.\",\n                command,\n            )\n        elif command not in _READ_ONLY_COMMANDS:","sourceCodeStart":347,"sourceCodeEnd":383,"githubUrl":"https://github.com/crewAIInc/crewAI/blob/754d7323beb2fd042e33444a115ea2d5a47193f0/lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py#L347-L383","documentation":"After read-only CTEs, if the main statement's first keyword is neither a write command nor in _READ_ONLY_COMMANDS (SELECT/SHOW/DESCRIBE/EXPLAIN), the tool blocks it as unrecognised. This is a deny-by-default allowlist: dialect-specific or unusual statements after a CTE are rejected in read-only mode.","triggerScenarios":"Running 'WITH x AS (SELECT 1) VACUUM t', 'WITH x AS (...) SET search_path TO public', or any other statement whose leading keyword is not in the allowlist, while allow_dml=False.","commonSituations":"Portability statements like SET/USE/PRAGMA appended after CTEs; new or dialect-specific SQL keywords the allowlist does not know; LLM hallucinating an exotic command after a CTE.","solutions":["Remove the non-read statement or move it to a separate call against an engine configured for it.","Add the missing read-only statement outside a CTE form if supported, or use plain SELECT equivalents.","Set allow_dml=True only if you accept that ALL writes become possible — there is no per-keyword bypass."],"exampleFix":"# before (blocked)\ntool._run(\"WITH recent AS (SELECT * FROM logs) SET work_mem = '64MB'\")\n\n# after\ntool._run(\"WITH recent AS (SELECT * FROM logs) SELECT * FROM recent LIMIT 10\")","handlingStrategy":"validation","validationCode":"READ_ONLY = {\"SELECT\", \"SHOW\", \"DESCRIBE\", \"EXPLAIN\"}\n\ndef tail_cmd_after_cte(sql: str) -> str:\n    idx = sql.rfind(\")\")\n    tail = sql[idx+1:].strip() if idx != -1 else \"\"\n    return tail.split()[0].upper().rstrip(\";\") if tail.split() else \"\"\n\nif not allow_dml and tail_cmd_after_cte(sql) not in READ_ONLY:\n    raise ValueError(f\"command after CTE not allowed: {tail_cmd_after_cte(sql)}\")","typeGuard":null,"tryCatchPattern":"try:\n    tool._run(sql)\nexcept ValueError as e:\n    if \"after a CTE\" in str(e):\n        # rephrase to plain SELECT or run via direct engine\n        ...","preventionTips":["Keep session/admin commands off NL2SQLTool; use a raw engine for those.","Teach the model the allowlist: SELECT, SHOW, DESCRIBE, EXPLAIN."],"tags":["nl2sql","cte","allowlist","read-only","validation"],"backgroundTag":null,"analyzedSha":"754d7323beb2fd042e33444a115ea2d5a47193f0","analyzedAt":"2026-08-15T04:06:56.746Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}