{"id":"7819d34832e4e32f","repo":"psycopg/psycopg2","slug":"composed-join-argument-must-be-a-string-or-an-sq","errorCode":null,"errorMessage":"Composed.join() argument must be a string or an SQL","messagePattern":"Composed\\.join\\(\\) argument must be a string or an SQL","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"lib/sql.py","lineNumber":153,"sourceCode":"\n    def join(self, joiner):\n        \"\"\"\n        Return a new `!Composed` interposing the *joiner* with the `!Composed` items.\n\n        The *joiner* must be a `SQL` or a string which will be interpreted as\n        an `SQL`.\n\n        Example::\n\n            >>> fields = sql.Identifier('foo') + sql.Identifier('bar')  # a Composed\n            >>> print(fields.join(', ').as_string(conn))\n            \"foo\", \"bar\"\n\n        \"\"\"\n        if isinstance(joiner, str):\n            joiner = SQL(joiner)\n        elif not isinstance(joiner, SQL):\n            raise TypeError(\n                \"Composed.join() argument must be a string or an SQL\")\n\n        return joiner.join(self)\n\n\nclass SQL(Composable):\n    \"\"\"\n    A `Composable` representing a snippet of SQL statement.\n\n    `!SQL` exposes `join()` and `format()` methods useful to create a template\n    where to merge variable parts of a query (for instance field or table\n    names).\n\n    The *string* doesn't undergo any form of escaping, so it is not suitable to\n    represent variable identifiers or values: you should only use it to pass\n    constant strings representing templates or snippets of SQL statements; use\n    other objects such as `Identifier` or `Literal` to represent variable\n    parts.","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/psycopg/psycopg2/blob/3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db/lib/sql.py#L135-L171","documentation":"Raised by Composed.join() (lib/sql.py:152-154) when the 'joiner' argument is neither a str nor a SQL instance. join() interposes the joiner between the Composed's items; the joiner must be a literal SQL snippet (constant, unescaped) so that the result remains a safe composition. Other Composable types (Identifier, Literal, Placeholder) are rejected.","triggerScenarios":"Calling composed.join(sql.Identifier('x')), composed.join(sql.Literal(1)), composed.join(42), or composed.join(None). The method accepts a plain str only because it implicitly wraps it in SQL (line 150-151); any other type fails.","commonSituations":"Developers pass an Identifier as a separator (e.g. wanting 'schema.table' joins) instead of a literal. Passing a Placeholder or Literal as a joiner. Forgetting that join() is on Composed, not SQL (SQL.join accepts a sequence of any Composables).","solutions":["Pass a plain string or a sql.SQL instance as the joiner, e.g. composed.join(', ') or composed.join(sql.SQL(', ')).","If you need identifiers separated by a dot, use sql.Identifier('schema', 'table') which handles dotted names natively.","Double-check you are calling join() on the right class: Composed.join(separator) vs SQL.join(sequence)."],"exampleFix":"// before\nfields.join(sql.Identifier(','))\n// after\nfields.join(', ')","handlingStrategy":"type-guard","validationCode":"if not isinstance(joiner, (str, sql.SQL)):\n    raise TypeError('joiner must be str or sql.SQL')\nresult = composed.join(joiner)","typeGuard":"from psycopg2.sql import SQL\ndef is_valid_joiner(j) -> bool:\n    return isinstance(j, (str, SQL))","tryCatchPattern":"try:\n    result = composed.join(joiner)\nexcept TypeError as e:\n    if 'must be a string or an SQL' in str(e):\n        result = composed.join(str(joiner))\n    else: raise","preventionTips":["Pass a literal separator string (e.g. ', ') to Composed.join.","Remember Composed.join takes a separator; SQL.join takes a sequence."],"tags":["sql","composable","join","type-error","api-misuse"],"analyzedSha":"3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db","analyzedAt":"2026-08-04T19:56:51.958Z","schemaVersion":2}