sqlalchemy/alembic · error · ValueError
String or text() construct expected
Error message
String or text() construct expected
What it means
Raised as ValueError by _textual_index_column when the text_ argument is not a str, TextClause, _textual_index_element, or ColumnElement (sqla_compat.py:383-398). This helper converts textual index column expressions into proper SQLAlchemy column objects for use in migrations. Passing an unsupported type means Alembic cannot interpret the index definition and refuses to proceed.
Solutions
- Pass a string column name, sa.text('expression'), or a proper sa.Column expression to the index operation.
- Wrap raw SQL fragments in sa.text(): use sa.text('lower(name)') instead of a bare object.
- If using a custom expression, ensure it subclasses sqlalchemy.sql.ColumnElement.
- Check the SQLAlchemy version compatibility — some expression types changed across major versions.
Example fix
# before
op.create_index('idx_name', 'users', [123]) # int not valid
# after
op.create_index('idx_name', 'users', ['name']) # string column name
# or
op.create_index('idx_name', 'users', [sa.text('lower(name)')]) # text construct Defensive patterns
Strategy: type-guard
Validate before calling
import sqlalchemy as sa
from sqlalchemy.sql.elements import ColumnElement, TextClause
def is_valid_index_expression(value):
return isinstance(value, (str, TextClause, ColumnElement)) Type guard
import sqlalchemy as sa
from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy.sql.elements import TextClause
def is_valid_textual_index_arg(value) -> bool:
"""Type guard: True if value is acceptable for _textual_index_column."""
return isinstance(value, (str, TextClause, ColumnElement)) Prevention
- Always pass string column names or sa.text() constructs to index operations.
- Avoid passing raw integers or non-SQLAlchemy objects as index column arguments.
- When in doubt, wrap expressions in sa.text() to ensure they're valid TextClause objects.
When it happens
Trigger: Calling an Alembic operation that internally uses _textual_index_column with an invalid expression type — e.g., passing an integer, a raw SQL fragment object, or a custom expression that isn't a SQLAlchemy ColumnElement. This is used in index operations that accept textual column definitions.
Common situations: Using op.create_index with a non-standard expression type for a column argument. Passing a Python int/float where a column name string or text() construct is expected. Custom SQLAlchemy constructs that don't subclass ColumnElement. Version mismatches where an expression type changed between SQLAlchemy versions.
Related errors
- Can not set dispatch function for object
- Don't know how to comma-format %r
- no dispatch function for object
- A plugin named is already registered
- Can't change down_revision on a refresh operation.
AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11).
Data as JSON: /api/errors/cbc01096bea50264.
Report an issue: GitHub.
Appendix: source
Thrown at alembic/util/sqla_compat.py:398
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 5551b5d35f)