{"record":{"id":"cb3b633ae2299c94","repo":"apache/superset","slug":"only-select-statements-are-allowed","errorCode":null,"errorMessage":"Only `SELECT` statements are allowed","messagePattern":"Only `SELECT` statements are allowed","errorType":"http","errorClass":"SupersetSecurityException","httpStatus":403,"severity":"error","filePath":"superset/connectors/sqla/utils.py","lineNumber":154,"sourceCode":"        parsed_script = SQLScript(sql, engine=db_engine_spec.engine)\n    except SupersetParseError as ex:\n        # ``SQLScript`` fails on any invalid SQL, including static SQL\n        # with no template dependency. Only soften when the input\n        # contained Jinja markers — in that case an \"Invalid SQL\"\n        # outcome is very likely a rendering artifact (e.g. an empty\n        # ``filter_values('x')`` producing ``WHERE col IN ()``) rather\n        # than a genuine defect in the user's SQL, and the row is\n        # already persisted by ``UpdateDatasetCommand``. Genuinely\n        # invalid static SQL must still hard-error. See #38012.\n        if _has_jinja_markers(original_sql):\n            raise SupersetVirtualTableParseException(\n                message=_(\"Invalid SQL: %(error)s\", error=ex.error.message),\n            ) from ex\n        raise SupersetGenericDBErrorException(\n            message=_(\"Invalid SQL: %(error)s\", error=ex.error.message),\n        ) from ex\n    if parsed_script.has_mutation():\n        raise SupersetSecurityException(\n            SupersetError(\n                error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,\n                message=_(\"Only `SELECT` statements are allowed\"),\n                level=ErrorLevel.ERROR,\n            )\n        )\n    if len(parsed_script.statements) > 1:\n        raise SupersetSecurityException(\n            SupersetError(\n                error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,\n                message=_(\"Only single queries supported\"),\n                level=ErrorLevel.ERROR,\n            )\n        )\n    return get_columns_description(\n        dataset.database,\n        dataset.catalog,\n        dataset.schema,","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/connectors/sqla/utils.py#L136-L172","documentation":"Raised by get_virtual_table_metadata() when SQLScript.has_mutation() detects a mutating statement (INSERT/UPDATE/DELETE/CREATE/ALTER/DROP etc.) in a virtual dataset's SQL. Virtual datasets are metadata-only views; mutating SQL is rejected with a SupersetSecurityException of type DATASOURCE_SECURITY_ACCESS_ERROR before anything is executed.","triggerScenarios":"Saving a dataset whose SQL is `INSERT INTO ...`, `UPDATE ...`, `DELETE FROM ...`, or DDL like `CREATE TABLE ...` / `DROP VIEW ...`; also `SELECT ...; DELETE ...` where the mutation check trips on any statement. The check runs on the rendered SQL during save/refresh of the dataset.","commonSituations":"Trying to smuggle a mutation through the dataset layer because direct DML via SQL Lab is restricted; pasting a whole script (including cleanup DDL) into a dataset definition; a CTE or function name that sqlglot's engine dialect misclassifies as a mutation.","solutions":["Make the dataset SQL a single pure SELECT; move mutations to a proper pipeline/job.","If flagged falsely, rename non-mutating constructs that the parser mistakes for DML, or restructure the query (e.g. avoid a function named like an update call).","Run the mutation outside Superset, then build the virtual dataset on top of the resulting table."],"exampleFix":"-- before\nDELETE FROM audit_log WHERE ts < '2020-01-01';\nSELECT * FROM audit_log;\n\n-- after\nSELECT * FROM audit_log WHERE ts >= '2020-01-01'","handlingStrategy":"validation","validationCode":"def is_single_select(sql: str, engine: str) -> bool:\n    try:\n        script = SQLScript(sql, engine=engine)\n        return not script.has_mutation()\n    except SupersetParseError:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    get_virtual_table_metadata(dataset)\nexcept SupersetSecurityException:\n    # mutation rejected before execution: rewrite SQL as pure SELECT\n    raise","preventionTips":["Policy: dataset SQL is always a single SELECT — enforce in review and linters.","Never rely on datasets to run DML; use scheduled jobs.","Watch for sqlglot false positives after upgrading and pin known-good behavior with tests."],"tags":["security","dataset","sql","mutation","virtual-table"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}