{"record":{"id":"69a3e3a8ed74187a","repo":"pola-rs/polars","slug":"query-type-statements-are-not-valid-read-queri","errorCode":null,"errorMessage":"{query_type} statements are not valid 'read' queries","messagePattern":"(.+?) statements are not valid 'read' queries","errorType":"exception","errorClass":"UnsuitableSQLError","httpStatus":null,"severity":"error","filePath":"py-polars/src/polars/io/database/_executor.py","lineNumber":545,"sourceCode":"        if cursor_execute is None:\n            cursor_execute = (\n                self._sqlalchemy_async_execute if is_async else self.cursor.execute\n            )\n        return cursor_execute, options, query\n\n    def execute(\n        self,\n        query: str | TextClause | Selectable,\n        *,\n        options: dict[str, Any] | None = None,\n        select_queries_only: bool = True,\n    ) -> Self:\n        \"\"\"Execute a query and reference the result set.\"\"\"\n        if select_queries_only and isinstance(query, str):\n            q = re.search(r\"\\w{3,}\", re.sub(r\"/\\*(.|[\\r\\n])*?\\*/\", \"\", query))\n            if (query_type := \"\" if not q else q.group(0)) in _INVALID_QUERY_TYPES:\n                msg = f\"{query_type} statements are not valid 'read' queries\"\n                raise UnsuitableSQLError(msg)\n\n        options = options or {}\n\n        if self._is_alchemy_object(self.cursor):\n            cursor_execute, options, query = self._sqlalchemy_setup(query, options)\n        else:\n            cursor_execute = self.cursor.execute\n\n        # note: some cursor execute methods (eg: sqlite3) only take positional\n        # params, hence the slightly convoluted resolution of the 'options' dict\n        try:\n            params = signature(cursor_execute).parameters\n        except ValueError:\n            params = {}  # type: ignore[assignment]\n\n        if not options or any(\n            p.kind in (Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD)\n            for p in params.values()","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/pola-rs/polars/blob/df599052daf96e7a9cc30a3b0c6bd25d6947e3c0/py-polars/src/polars/io/database/_executor.py#L527-L563","documentation":"Because read_database is a read API, ConnectionExecutor.execute guards it when select_queries_only=True (the default): it strips /* */ comments, finds the first word of 3+ letters via regex, and if it is one of _INVALID_QUERY_TYPES (ALTER, ANALYZE, CREATE, DELETE, DROP, GRANT, INSERT, REPLACE, REVOKE, UPDATE, UPSERT, USE, VACUUM) it raises UnsuitableSQLError - a polars exception. This is a footgun guard against accidental writes, not a security boundary; it is purely lexical (comment stripping plus first-token match), so it can both miss obfuscated writes and false-positive on statements merely starting with such a word.","triggerScenarios":"pl.read_database('INSERT INTO t ...', connection=conn); 'USE schema_x' as a preamble; 'UPDATE ...' passed for side-effectful fetches; CTE wrappers do NOT trigger it (WITH ... UPDATE does not start with an invalid word), and a leading /* comment */ is stripped before the first-word check.","commonSituations":"Reusing a query-building helper that defaults to writing; executing temp-table setup (CREATE TEMP ...) alongside reads through one code path; migrating scripts from a generic execute() utility to read_database; users surprised that DDL/DML is blocked in a read API.","solutions":["Move write/DDL statements to the driver's own execute method (conn.execute / cursor.execute), keeping read_database for SELECTs","If you deliberately need a non-SELECT through this API, pass select_queries_only=False - accepting that you are bypassing the read-only guard","Run setup statements (CREATE TEMP TABLE, USE) on a separate cursor before calling read_database with the SELECT"],"exampleFix":"# before\npl.read_database('INSERT INTO audit VALUES (1)', connection=conn)\n\n# after - write via the driver, read via polars\nconn.cursor().execute('INSERT INTO audit VALUES (1)')\ndf = pl.read_database('SELECT * FROM audit', connection=conn)\n\n# after - or explicitly opt out of the read-only guard\npl.read_database('INSERT INTO audit VALUES (1)', connection=conn,\n                 select_queries_only=False)","handlingStrategy":"try-catch","validationCode":"import re\n\nINVALID = {'ALTER','ANALYZE','CREATE','DELETE','DROP','GRANT','INSERT',\n           'REPLACE','REVOKE','UPDATE','UPSERT','USE','VACUUM'}\n\ndef is_read_query(sql: str) -> bool:\n    stripped = re.sub(r'/\\*(.|[\\r\\n])*?\\*/', '', sql)\n    first = re.search(r'\\w{3,}', stripped)\n    return not first or first.group(0).upper() not in INVALID\n\nif not is_read_query(sql):\n    raise ValueError(f'read_database is read-only; got: {sql[:40]!r}')\ndf = pl.read_database(sql, connection=conn)","typeGuard":null,"tryCatchPattern":"from polars.exceptions import UnsuitableSQLError\n\ntry:\n    df = pl.read_database(sql, connection=conn)\nexcept UnsuitableSQLError:\n    # route the statement to the driver's own writer path\n    conn.cursor().execute(sql)\n    df = None","preventionTips":["Keep a strict read/write split in data-access layers: SELECTs through polars, writes through the driver","Run setup DDL (CREATE TEMP, USE) on a separate cursor before the read","Mirror the guard client-side (strip /* */ comments, uppercase first token) if queries come from users"],"tags":["polars","database","sql","read-only","unsuitablesqlerror"],"backgroundTag":null,"analyzedSha":"df599052daf96e7a9cc30a3b0c6bd25d6947e3c0","analyzedAt":"2026-08-16T12:10:03.978Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}