{"id":"cbc01096bea50264","repo":"sqlalchemy/alembic","slug":"string-or-text-construct-expected","errorCode":null,"errorMessage":"String or text() construct expected","messagePattern":"String or text\\(\\) construct expected","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"alembic/util/sqla_compat.py","lineNumber":389,"sourceCode":"        collection.remove(to_remove)\n\n\ndef _textual_index_column(\n    table: Table, text_: str | TextClause | ColumnElement[Any]\n) -> ColumnElement[Any] | Column[Any]:\n    \"\"\"a workaround for the Index construct's severe lack of flexibility\"\"\"\n    if isinstance(text_, str):\n        c = Column(text_, sqltypes.NULLTYPE)\n        table.append_column(c)\n        return c\n    elif isinstance(text_, TextClause):\n        return _textual_index_element(table, text_)\n    elif isinstance(text_, _textual_index_element):\n        return _textual_index_column(table, text_.text)\n    elif isinstance(text_, sql.ColumnElement):\n        return _copy_expression(text_, table)\n    else:\n        raise ValueError(\"String or text() construct expected\")\n\n\ndef _copy_expression(expression: _CE, target_table: Table) -> _CE:\n    def replace(col):\n        if (\n            isinstance(col, Column)\n            and col.table is not None\n            and col.table is not target_table\n        ):\n            if col.name in target_table.c:\n                return target_table.c[col.name]\n            else:\n                c = _copy(col)\n                target_table.append_column(c)\n                return c\n        else:\n            return None\n","sourceCodeStart":371,"sourceCodeEnd":407,"githubUrl":"https://github.com/sqlalchemy/alembic/blob/44fb3450330204b222ff05135e1fbbbdb28c44db/alembic/util/sqla_compat.py#L371-L407","documentation":"Raised as ValueError in _textual_index_column (sqla_compat.py:389) when the text_ argument is none of: a plain str, a sqlalchemy.text() TextClause, a _textual_index_element, or a ColumnElement. The function exists to coerce the textual index-expression argument of op.create_index / index definitions into a proper column; anything else is rejected because it cannot be mapped to a column expression.","triggerScenarios":"Passing an int, float, dict, list, or a raw SQL fragment object that isn't a TextClause to the textual index column path; calling _textual_index_column directly with an unsupported type; an autogenerate render path that emits a non-expression value into an index column spec.","commonSituations":"Writing op.create_index with a columns list that accidentally includes a non-string/non-expression value; mixing old-style string SQL fragments with expression objects; a custom type whose __repr__ is mistakenly passed instead of the object.","solutions":["Wrap raw SQL in sqlalchemy.text('...') before passing it as an index column.","Pass actual Column / ColumnElement objects (e.g. table.c.my_col) instead of bare values.","Ensure every entry in the create_index columns list is a str, TextClause, or ColumnElement.","If using a computed/functional index, use text('lower(col)') or func.lower(table.c.col)."],"exampleFix":"# before\nfrom alembic import op\nop.create_index('ix', 't', [123])  # int triggers ValueError\n\n# after\nimport sqlalchemy as sa\nop.create_index('ix', 't', [sa.text('lower(name)')])","handlingStrategy":"type-guard","validationCode":"import sqlalchemy as sa\nfrom sqlalchemy.sql.elements import ColumnElement\nfrom sqlalchemy.sql.expression import TextClause\n\ndef coerce_index_column(table, col):\n    if isinstance(col, (str, TextClause, ColumnElement)):\n        return col\n    if isinstance(col, int):\n        return sa.text(str(col))\n    raise TypeError(f'index column must be str/text()/ColumnElement, got {type(col).__name__}')","typeGuard":"import sqlalchemy as sa\nfrom sqlalchemy.sql.elements import ColumnElement\nfrom sqlalchemy.sql.expression import TextClause\ndef is_valid_index_column(c) -> bool:\n    return isinstance(c, (str, TextClause, ColumnElement))","tryCatchPattern":"from alembic.util.sqla_compat import _textual_index_column\ntry:\n    col = _textual_index_column(table, value)\nexcept ValueError:\n    col = sa.text(str(value))  # coerce to a text expression","preventionTips":["Always pass Column objects (table.c.x) or sa.text('...') in index column lists.","Never put bare numbers/dicts/lists into create_index columns.","Type-annotate helper functions that build index specs."],"tags":["alembic","sqlalchemy","index","type-mismatch","ddl"],"analyzedSha":"44fb3450330204b222ff05135e1fbbbdb28c44db","analyzedAt":"2026-08-04T19:57:10.248Z","schemaVersion":2}