sqlalchemy/alembic · error · ValueError
String or text() construct expected
Error message
String or text() construct expected
What it means
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.
Source
Thrown at alembic/util/sqla_compat.py:389
collection.remove(to_remove)
def _textual_index_column(
table: Table, text_: str | TextClause | ColumnElement[Any]
) -> ColumnElement[Any] | Column[Any]:
"""a workaround for the Index construct's severe lack of flexibility"""
if isinstance(text_, str):
c = Column(text_, sqltypes.NULLTYPE)
table.append_column(c)
return c
elif isinstance(text_, TextClause):
return _textual_index_element(table, text_)
elif isinstance(text_, _textual_index_element):
return _textual_index_column(table, text_.text)
elif isinstance(text_, sql.ColumnElement):
return _copy_expression(text_, table)
else:
raise ValueError("String or text() construct expected")
def _copy_expression(expression: _CE, target_table: Table) -> _CE:
def replace(col):
if (
isinstance(col, Column)
and col.table is not None
and col.table is not target_table
):
if col.name in target_table.c:
return target_table.c[col.name]
else:
c = _copy(col)
target_table.append_column(c)
return c
else:
return None
View on GitHub (pinned to 44fb345033)
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).
Example fix
# before
from alembic import op
op.create_index('ix', 't', [123]) # int triggers ValueError
# after
import sqlalchemy as sa
op.create_index('ix', 't', [sa.text('lower(name)')]) Defensive patterns
Strategy: type-guard
Validate before calling
import sqlalchemy as sa
from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy.sql.expression import TextClause
def coerce_index_column(table, col):
if isinstance(col, (str, TextClause, ColumnElement)):
return col
if isinstance(col, int):
return sa.text(str(col))
raise TypeError(f'index column must be str/text()/ColumnElement, got {type(col).__name__}') Type guard
import sqlalchemy as sa
from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy.sql.expression import TextClause
def is_valid_index_column(c) -> bool:
return isinstance(c, (str, TextClause, ColumnElement)) Try / catch
from alembic.util.sqla_compat import _textual_index_column
try:
col = _textual_index_column(table, value)
except ValueError:
col = sa.text(str(value)) # coerce to a text expression Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Can't drop table in batch mode
- revision identifier %r is not a string; ensure database driv
- Don't know how to comma-format %r
- Constraint must have a name
- No such constraint: '%s'
AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04).
Data as JSON: /data/errors/cbc01096bea50264.json.
Report an issue: GitHub.