{"id":"2e614f151a16c210","repo":"psycopg/psycopg2","slug":"the-query-contains-more-than-one-s-placeholder","errorCode":null,"errorMessage":"the query contains more than one '%s' placeholder","messagePattern":"the query contains more than one '(.+?)' placeholder","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/extras.py","lineNumber":1324,"sourceCode":"def _split_sql(sql):\n    \"\"\"Split *sql* on a single ``%s`` placeholder.\n\n    Split on the %s, perform %% replacement and return pre, post lists of\n    snippets.\n    \"\"\"\n    curr = pre = []\n    post = []\n    tokens = _re.split(br'(%.)', sql)\n    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":1306,"sourceCodeEnd":1341,"githubUrl":"https://github.com/psycopg/psycopg2/blob/3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db/lib/extras.py#L1306-L1341","documentation":"Raised by _split_sql() (lib/extras.py:1324-1325), invoked from execute_values(), when the query passed to execute_values contains more than one '%s' positional placeholder. execute_values replaces exactly one '%s' with a VALUES list; additional '%s' are ambiguous and rejected. Note that '%%' is correctly handled as a literal percent and does not count.","triggerScenarios":"Calling execute_values(cur, \"INSERT INTO t (a,b) VALUES %s ON CONFLICT DO NOTHING RETURNING %s\", data) or any execute_values query with two or more '%s'. The tokenizer at lib/extras.py:1314 splits on '(%.)' and the second b's' token flips curr from post back into the error branch.","commonSituations":"Developers add an extra '%s' expecting to bind another scalar parameter alongside the values list, or they reuse a query template written for cursor.execute (which supports many %s) without removing the extras.","solutions":["Ensure the execute_values query contains exactly one '%s' (the values placeholder).","Move any other parameters into the template via cur.mogrify, or run a separate execute.","Use named placeholders (%(name)s) inside the 'template' argument rather than top-level '%s'.","If you need a dynamic column or table name, use psycopg2.sql composition instead of a second '%s'."],"exampleFix":"// before\nexecute_values(cur, \"INSERT INTO t VALUES %s RETURNING %s\", rows, ['id'])\n// after\nexecute_values(cur, \"INSERT INTO t VALUES %s RETURNING id\", rows, fetch=True)","handlingStrategy":"validation","validationCode":"assert sql.count('%s') == 1, f'execute_values requires exactly one %s, got {sql.count(\"%s\")}'\n# Note: also account for %% escaping if present.","typeGuard":"def has_single_values_placeholder(sql: str) -> bool:\n    import re\n    toks = re.split(r'(%.)', sql)\n    return sum(1 for t in toks if t == '%s') == 1","tryCatchPattern":"try:\n    execute_values(cur, sql, data)\nexcept ValueError as e:\n    if 'more than one' in str(e):\n        sql = sql.replace(extra_placeholder, ...)  # rewrite\n    else: raise","preventionTips":["Keep execute_values templates minimal: one '%s' for the values list.","Use the template argument for per-row placeholders instead of extra top-level '%s'."],"tags":["execute-values","placeholder","sql","value-error","extras"],"analyzedSha":"3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db","analyzedAt":"2026-08-04T19:56:51.958Z","schemaVersion":2}