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

  1. Pass a string column name, sa.text('expression'), or a proper sa.Column expression to the index operation.
  2. Wrap raw SQL fragments in sa.text(): use sa.text('lower(name)') instead of a bare object.
  3. If using a custom expression, ensure it subclasses sqlalchemy.sql.ColumnElement.
  4. 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

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


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)