cocoindex-io/cocoindex · error · RuntimeError

Failed to replace column

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.

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.

Appendix: 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)