diff --git a/CHANGES.rst b/CHANGES.rst index 4a99bf0b..213cd5d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Version history =============== +**UNRELEASED** + +- Fixed ``remote_side`` in a self-referential relationship pointing at the primary key + when the foreign key targets other columns (such as a ``UNIQUE`` constraint), which + made mapper configuration fail with ``ArgumentError`` + (`#484 `_; PR by @NixBiks) + **4.0.4** - Added autoincrement to primary key columns to prevent missing field errors. diff --git a/src/sqlacodegen/generators.py b/src/sqlacodegen/generators.py index a10a21e6..788c91e4 100644 --- a/src/sqlacodegen/generators.py +++ b/src/sqlacodegen/generators.py @@ -1271,8 +1271,8 @@ def generate_relationships( # For self referential relationships, remote_side needs to be set if source is target: relationship.remote_side = [ - source.get_column_attribute(col.name) - for col in constraint.referred_table.primary_key + source.get_column_attribute(element.column.name) + for element in constraint.elements ] # If the two tables share more than one foreign key constraint, diff --git a/tests/test_generator_declarative.py b/tests/test_generator_declarative.py index c42e9545..1c68eca3 100644 --- a/tests/test_generator_declarative.py +++ b/tests/test_generator_declarative.py @@ -483,6 +483,56 @@ class SimpleItems(Base): ) +def test_onetomany_selfref_composite_unique_target(generator: CodeGenerator) -> None: + Table( + "simple_items", + generator.metadata, + Column("id", INTEGER, primary_key=True), + Column("scheme", VARCHAR, nullable=False), + Column("code", VARCHAR, nullable=False), + Column("parent_scheme", VARCHAR), + Column("parent_code", VARCHAR), + UniqueConstraint("scheme", "code"), + ForeignKeyConstraint( + ["parent_scheme", "parent_code"], + ["simple_items.scheme", "simple_items.code"], + ), + ) + + validate_code( + generator.generate(), + """\ +from typing import Optional + +from sqlalchemy import ForeignKeyConstraint, Integer, String, UniqueConstraint +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + +class Base(DeclarativeBase): + pass + + +class SimpleItems(Base): + __tablename__ = 'simple_items' + __table_args__ = ( + ForeignKeyConstraint(['parent_scheme', 'parent_code'], \ +['simple_items.scheme', 'simple_items.code']), + UniqueConstraint('scheme', 'code') + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + scheme: Mapped[str] = mapped_column(String, nullable=False) + code: Mapped[str] = mapped_column(String, nullable=False) + parent_scheme: Mapped[Optional[str]] = mapped_column(String) + parent_code: Mapped[Optional[str]] = mapped_column(String) + + simple_items: Mapped[Optional['SimpleItems']] = relationship('SimpleItems', \ +remote_side=[scheme, code], back_populates='simple_items_reverse') + simple_items_reverse: Mapped[list['SimpleItems']] = relationship('SimpleItems', \ +remote_side=[parent_scheme, parent_code], back_populates='simple_items') +""", + ) + + def test_onetomany_composite(generator: CodeGenerator) -> None: Table( "simple_items",