Skip to content

Creating a self-referential foreign key with composite primary key throws a SA DuplicateColumnError #1215

@Kalmis

Description

@Kalmis

Describe the bug
With SA 2, creating a self-referential foreign key with composite primary key throws a SQLAlchemy DuplicateColumnError. In more general terms, an error is thrown if self-referential foreign key has at least one same column as "source" and "target" column.

Expected behavior
The foreign key is created without an error. Works with SA 1.4, although, a deprecation warning is raised.

SADeprecationWarning: A column with name 'group_id' is already present in table 'product_mcve'. Please use method :meth:`_schema.Table.append_column` with the parameter ``replace_existing=True`` to replace an existing column.

To Reproduce

Create a migration with the code below and run it.

def upgrade():
    op.create_table(
        "product_mcve",
        sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
        sa.Column("group_id", postgresql.UUID(as_uuid=True), nullable=False),
        sa.Column("parent_id", postgresql.UUID(as_uuid=True), nullable=False),
        sa.PrimaryKeyConstraint("id", "group_id"),
    )
    op.create_foreign_key(
        None,
        "product_mcve",
        "product_mcve",
        ["parent_id", "group_id"],
        ["id", "group_id"],
        ondelete="CASCADE",
    )

Error

Stacktrace when running migration with custom Pytest setup that resets DB & runs migrations.

self = Column('group_id', NullType(), table=None)
parent = Table('product_mcve', MetaData(), Column('parent_id', NullType(), table=<product_mcve>), Column('group_id', NullType(), table=<product_mcve>), Column('id', NullType(), table=<product_mcve>), schema=None)
all_names = {'group_id': Column('group_id', NullType(), table=<product_mcve>), 'id': Column('id', NullType(), table=<product_mcve>), 'parent_id': Column('parent_id', NullType(), table=<product_mcve>)}
allow_replacements = None, kw = {}, existing = None, extra_remove = None
existing_col = Column('group_id', NullType(), table=<product_mcve>)
conflicts_on = 'name'

    def _set_parent(  # type: ignore[override]
        self,
        parent: SchemaEventTarget,
        *,
        all_names: Dict[str, Column[Any]],
        allow_replacements: bool,
        **kw: Any,
    ) -> None:
        table = parent
        assert isinstance(table, Table)
        if not self.name:
            raise exc.ArgumentError(
                "Column must be constructed with a non-blank name or "
                "assign a non-blank .name before adding to a Table."
            )
    
        self._reset_memoizations()
    
        if self.key is None:
            self.key = self.name
    
        existing = getattr(self, "table", None)
        if existing is not None and existing is not table:
            raise exc.ArgumentError(
                "Column object '%s' already assigned to Table '%s'"
                % (self.key, existing.description)
            )
    
        extra_remove = None
        existing_col = None
        conflicts_on = ""
    
        if self.key in table._columns:
            existing_col = table._columns[self.key]
            if self.key == self.name:
                conflicts_on = "name"
            else:
                conflicts_on = "key"
        elif self.name in all_names:
            existing_col = all_names[self.name]
            extra_remove = {existing_col}
            conflicts_on = "name"
    
        if existing_col is not None:
            if existing_col is not self:
                if not allow_replacements:
>                   raise exc.DuplicateColumnError(
                        f"A column with {conflicts_on} "
                        f"""'{
                            self.key if conflicts_on == 'key' else self.name
                        }' """
                        f"is already present in table '{table.name}'."
                    )
E                   sqlalchemy.exc.DuplicateColumnError: A column with name 'group_id' is already present in table 'product_mcve'.

../venv/lib/python3.11/site-packages/sqlalchemy/sql/schema.py:2181: DuplicateColumnError

Stacktrace when running cli alembic upgrade head

Traceback (most recent call last):
  File "/usr/local/bin/alembic", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/usr/local/lib/python3.11/site-packages/alembic/config.py", line 591, in main
    CommandLine(prog=prog).main(argv=argv)
  File "/usr/local/lib/python3.11/site-packages/alembic/config.py", line 585, in main
    self.run_cmd(cfg, options)
  File "/usr/local/lib/python3.11/site-packages/alembic/config.py", line 562, in run_cmd
    fn(
  File "/usr/local/lib/python3.11/site-packages/alembic/command.py", line 378, in upgrade
    script.run_env()
  File "/usr/local/lib/python3.11/site-packages/alembic/script/base.py", line 576, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/usr/local/lib/python3.11/site-packages/alembic/util/pyfiles.py", line 94, in load_python_file
    module = load_module_py(module_id, path)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/alembic/util/pyfiles.py", line 110, in load_module_py
    spec.loader.exec_module(module)  # type: ignore
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/backend/fondion_alembic/env.py", line 80, in <module>
    run_migrations_online()
  File "/backend/fondion_alembic/env.py", line 74, in run_migrations_online
    context.run_migrations()
  File "<string>", line 8, in run_migrations
  File "/usr/local/lib/python3.11/site-packages/alembic/runtime/environment.py", line 868, in run_migrations
    self.get_context().run_migrations(**kw)
  File "/usr/local/lib/python3.11/site-packages/alembic/runtime/migration.py", line 622, in run_migrations
    step.migration_fn(**kw)
  File "/backend/fondion_alembic/versions/57b2cbd83d3a_mcve.py", line 27, in upgrade
    op.create_foreign_key(
  File "<string>", line 8, in create_foreign_key
  File "<string>", line 3, in create_foreign_key
  File "/usr/local/lib/python3.11/site-packages/alembic/operations/ops.py", line 665, in create_foreign_key
    return operations.invoke(op)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/alembic/operations/base.py", line 401, in invoke
    return fn(self, operation)
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/alembic/operations/toimpl.py", line 176, in create_constraint
    operation.to_constraint(operations.migration_context)
  File "/usr/local/lib/python3.11/site-packages/alembic/operations/ops.py", line 578, in to_constraint
    return schema_obj.foreign_key_constraint(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/alembic/operations/schemaobj.py", line 89, in foreign_key_constraint
    t1 = sa_schema.Table(
         ^^^^^^^^^^^^^^^^
  File "<string>", line 2, in __new__
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/util/deprecations.py", line 277, in warned
    return fn(*args, **kwargs)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/schema.py", line 428, in __new__
    return cls._new(*args, **kw)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/schema.py", line 482, in _new
    with util.safe_reraise():
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/util/langhelpers.py", line 147, in __exit__
    raise exc_value.with_traceback(exc_tb)
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/schema.py", line 478, in _new
    table.__init__(name, metadata, *args, _no_init=False, **kw)
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/schema.py", line 866, in __init__
    self._init_items(
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/schema.py", line 225, in _init_items
    spwd(self, **kw)
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1300, in _set_parent_with_dispatch
    self._set_parent(parent, **kw)
  File "/usr/local/lib/python3.11/site-packages/sqlalchemy/sql/schema.py", line 2181, in _set_parent
    raise exc.DuplicateColumnError(
sqlalchemy.exc.DuplicateColumnError: A column with name 'group_id' is already present in table 'product_mcve'.

Versions.

  • OS: Mac OS 13.2.1
  • Python: 3.11
  • Alembic: 1.10.3
  • SQLAlchemy: 2.0.7
  • Database: Postgres 12.11
  • DBAPI: psycopg2==2.9.5

Additional context

One fix is to remove duplicates from the list of columns in here https://github.com/sqlalchemy/alembic/blob/main/alembic/operations/schemaobj.py#L79 t1_cols = set(local_cols + remote_cols), but I'm lacking Alembic/SA knowledge to know if this breaks something else.

Have a nice day!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions