{"id":"e6edcc85481c6a6d","repo":"psycopg/psycopg2","slug":"the-query-doesn-t-contain-any-s-placeholder","errorCode":null,"errorMessage":"the query doesn't contain any '%s' placeholder","messagePattern":"the query doesn't contain any '(.+?)' placeholder","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/extras.py","lineNumber":1333,"sourceCode":"    for token in tokens:\n        if len(token) != 2 or token[:1] != b'%':\n            curr.append(token)\n            continue\n\n        if token[1:] == b's':\n            if curr is pre:\n                curr = post\n            else:\n                raise ValueError(\n                    \"the query contains more than one '%s' placeholder\")\n        elif token[1:] == b'%':\n            curr.append(b'%')\n        else:\n            raise ValueError(\"unsupported format character: '%s'\"\n                % token[1:].decode('ascii', 'replace'))\n\n    if curr is pre:\n        raise ValueError(\"the query doesn't contain any '%s' placeholder\")\n\n    return pre, post\n\n\n# ascii except alnum and underscore\n_re_clean = _re.compile(\n    '[' + _re.escape(' !\"#$%&\\'()*+,-./:;<=>?@[\\\\]^`{|}~') + ']')\n","sourceCodeStart":1315,"sourceCodeEnd":1341,"githubUrl":"https://github.com/psycopg/psycopg2/blob/3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db/lib/extras.py#L1315-L1341","documentation":"Raised by _split_sql() (lib/extras.py:1332-1333) when the query passed to execute_values contains zero '%s' placeholders. execute_values needs exactly one positional '%s' to mark where the VALUES list is substituted; without it there is nothing to expand and the call is meaningless.","triggerScenarios":"Calling execute_values(cur, \"INSERT INTO t (a,b) VALUES (%s, %s)\", data) — here both '%s' are inside parentheses but there is no top-level single placeholder; or passing a query that uses named placeholders only (e.g. \"VALUES (%(a)s, %(b)s)\") without a '%s' anchor.","commonSituations":"Developers pass a fully-formed query intended for cursor.executemany, forgetting that execute_values expects a single '%s' to be expanded. Also happens when the VALUES clause is omitted entirely or the query was written for named parameters.","solutions":["Rewrite the query to contain exactly one '%s' where the VALUES list should go: 'INSERT INTO t (a,b) VALUES %s'.","If you need per-row placeholders, pass them via the 'template' argument (e.g. template='(%s, %s)') and keep one top-level '%s'.","For named parameters, use template='(%(a)s, %(b)s)' and keep a single '%s' in the base query."],"exampleFix":"// before\nexecute_values(cur, \"INSERT INTO t (a,b) VALUES (%s, %s)\", data)\n// after\nexecute_values(cur, \"INSERT INTO t (a,b) VALUES %s\", data)  # template auto-built","handlingStrategy":"validation","validationCode":"assert sql.count('%s') >= 1, 'execute_values requires exactly one %s placeholder'","typeGuard":"def has_values_placeholder(sql: str) -> bool:\n    import re\n    return any(t == '%s' for t in re.findall(r'%.', sql))","tryCatchPattern":"try:\n    execute_values(cur, sql, data)\nexcept ValueError as e:\n    if \"doesn't contain any\" in str(e):\n        sql = sql.replace('VALUES', 'VALUES %s', 1)\n    else: raise","preventionTips":["Write execute_values queries as '... VALUES %s ...' with exactly one placeholder.","Use the template argument for the per-tuple placeholder pattern."],"tags":["execute-values","placeholder","sql","value-error","extras"],"analyzedSha":"3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db","analyzedAt":"2026-08-04T19:56:51.958Z","schemaVersion":2}