cocoindex-io/cocoindex · error · RuntimeError

Failed to replace column {col_name!r} in SQLite table {table

Error message

Failed to replace column {col_name!r} in SQLite table {table_name!r}: {e}

What it means

For a `replace` column action, SQLite has no `ALTER COLUMN TYPE`, so the connector emulates replacement by adding a new column with the desired type (after copying data). If that `ALTER TABLE ... ADD COLUMN` fails with anything other than 'duplicate column name', the `OperationalError` is wrapped in a `RuntimeError` naming the column and table.

Source

Thrown at python/cocoindex/connectors/sqlite/_target.py:870

        if action == "replace":
            # SQLite doesn't support ALTER COLUMN TYPE directly.
            # For type changes, attempt to drop and re-add column.
            try:
                conn.execute(f'ALTER TABLE {qualified_name} DROP COLUMN "{col_name}"')
            except sqlite3.OperationalError:
                pass
            nullable = "" if desired_col.nullable else " NOT NULL"
            try:
                conn.execute(
                    f"ALTER TABLE {qualified_name} "
                    f'ADD COLUMN "{col_name}" {desired_col.type}{nullable}'
                )
            except sqlite3.OperationalError as e:
                if "duplicate column name" in str(e).lower():
                    pass
                else:
                    raise RuntimeError(
                        f"Failed to replace column {col_name!r} in SQLite table {table_name!r}: {e}"
                    ) from e
            continue


def _apply_table_actions(
    context_provider: ContextProvider,
    actions: Sequence[_TableAction],
) -> list[coco.ChildTargetDef["_RowHandler"] | None]:
    """Apply table actions (DDL) and return child row handlers."""
    actions_list = list(actions)
    outputs: list[coco.ChildTargetDef[_RowHandler] | None] = [None] * len(actions_list)

    # Group actions by table key so we can apply all DDL for the same table
    by_key: dict[_TableKey, list[int]] = {}
    for i, action in enumerate(actions_list):
        by_key.setdefault(action.key, []).append(i)

View on GitHub (pinned to e84aa99b32)

Solutions

  1. Inspect the wrapped SQLite error message for the exact ADD COLUMN failure
  2. Fix the changed field's type mapping so it compiles to a valid SQLite type
  3. Make the replacement column nullable or give it a default value
  4. Close other connections / retry the sync once the database is not locked

Example fix

// before
class Row:
    tags: int  # changed from str, triggers replace
// after — revert the type, or delete/recreate the table to avoid in-place replace
class Row:
    tags: str
Defensive patterns

Strategy: try-catch

Validate before calling

# detect type changes on pk-adjacent/replaceable columns before syncing
assert all(isinstance(f.type, type) for f in dataclasses.fields(MyRow))

Try / catch

try:
    await app.update()
except RuntimeError as e:
    if str(e).startswith("Failed to replace column"):
        inspect(e.__cause__)  # underlying OperationalError has the real reason
    else:
        raise

Prevention

When it happens

Trigger: Replacing a column with a type SQLite cannot add (invalid type string, bad vector spec); a non-nullable add violating SQLite ADD COLUMN rules; the target table having been dropped or locked by another connection mid-migration.

Common situations: Changing a column's type in a record dataclass (e.g. `int` to a vector) triggers column replacement during sync; running the app against a database file opened by another process holding a write lock.

Related errors


AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08). Data as JSON: /api/errors/af34fdf2f9ed660d. Report an issue: GitHub.