Repository: maurerle/eralchemy2 (language: python)

Issue:
Fix one-to-one relationships



Discussion / hints:
Got it. Done!
Can you run black please - it probably wants to have double quotes for consistency.
Oh, sorry. Fixed it now.

Fix patch:
diff --git a/eralchemy2/sqla.py b/eralchemy2/sqla.py
index 95efcff..b08ff6f 100644
--- a/eralchemy2/sqla.py
+++ b/eralchemy2/sqla.py
@@ -20,12 +20,37 @@ class DeclarativeBase(Protocol):
         metadata: sa.MetaData
 
 
+def check_all_compound_same_parent(fk: sa.ForeignKey):
+    """Checks if all other ForeignKey Constraints of our table are on the same parent table as the current one"""
+    table = fk.column.table.fullname
+    for col in fk.constraint.table.columns:
+        if not col.foreign_keys:
+            return False
+        for foreign_column in col.foreign_keys:
+            if table != foreign_column.column.table.fullname:
+                return False
+    return True
+
+
 def relation_to_intermediary(fk: sa.ForeignKey) -> Relation:
     """Transform an SQLAlchemy ForeignKey object to its intermediary representation."""
+
+    primkey_count = sum([1 for x in fk.constraint.table.columns if x.primary_key])
+    # when there is only a single primary key column of the current key
+    if (primkey_count == 1 and fk.parent.primary_key) or fk.parent.unique:
+        right_cardinality = "1"
+    else:
+        # check if the other primkeys have a foreign key onto the same table
+        # if this is the case, we are not optional and must be unique
+        if check_all_compound_same_parent(fk):
+            right_cardinality = "1"
+        else:
+            right_cardinality = "*"
+
     return Relation(
         right_col=format_name(fk.parent.table.fullname),
         left_col=format_name(fk.column.table.fullname),
-        right_cardinality="1" if fk.parent.primary_key or fk.parent.unique else "*",
+        right_cardinality=right_cardinality,
         left_cardinality="?" if fk.parent.nullable else "1",
     )
 


Test patch:
diff --git a/tests/test_sqla_multi_key.py b/tests/test_sqla_multi_key.py
new file mode 100644
index 0000000..7406b5b
--- /dev/null
+++ b/tests/test_sqla_multi_key.py
@@ -0,0 +1,106 @@
+from sqlalchemy import Column, ForeignKey, String
+from sqlalchemy.orm import DeclarativeBase
+
+from eralchemy2.models import Relation, Table
+from eralchemy2.sqla import (
+    column_to_intermediary,
+    database_to_intermediary,
+    declarative_to_intermediary,
+    table_to_intermediary,
+)
+
+
+def test_columns_parent():
+    class Base(DeclarativeBase):
+        pass
+
+    class Parent(Base):
+        __tablename__ = "parent"
+        id = Column(String(), primary_key=True)
+
+    class Child(Base):
+        __tablename__ = "child"
+        id = Column(String(), primary_key=True)
+        parent = Column(String(), ForeignKey(Parent.id), primary_key=True)
+
+    tables, relationships = declarative_to_intermediary(Base)
+
+    assert len(tables) == 2
+    assert len(relationships) == 1
+    assert all(isinstance(t, Table) for t in tables)
+    assert all(isinstance(r, Relation) for r in relationships)
+    relation = relationships[0]
+    assert relation.right_cardinality == "*"
+    assert relation.left_cardinality == "1"
+
+
+def test_columns_one_to_many_parent():
+    class Base(DeclarativeBase):
+        pass
+
+    class Parent(Base):
+        __tablename__ = "parent"
+        id = Column(String(), primary_key=True)
+
+    class Child(Base):
+        __tablename__ = "child"
+        id = Column(String(), primary_key=True)
+        parent = Column(String(), ForeignKey(Parent.id))
+
+    tables, relationships = declarative_to_intermediary(Base)
+
+    assert len(tables) == 2
+    assert len(relationships) == 1
+    assert all(isinstance(t, Table) for t in tables)
+    assert all(isinstance(r, Relation) for r in relationships)
+    relation = relationships[0]
+    assert relation.right_cardinality == "*"
+    assert relation.left_cardinality == "?"
+
+
+def test_columns_one_to_one_parent():
+    class Base(DeclarativeBase):
+        pass
+
+    class Parent(Base):
+        __tablename__ = "parent"
+        id = Column(String(), primary_key=True)
+
+    class Child(Base):
+        __tablename__ = "child"
+        id = Column(String(), ForeignKey(Parent.id), primary_key=True)
+
+    tables, relationships = declarative_to_intermediary(Base)
+
+    assert len(tables) == 2
+    assert len(relationships) == 1
+    assert all(isinstance(t, Table) for t in tables)
+    assert all(isinstance(r, Relation) for r in relationships)
+    relation = relationships[0]
+    assert relation.right_cardinality == "1"
+    assert relation.left_cardinality == "1"
+
+
+def test_compound_one_to_one_parent():
+    class Base(DeclarativeBase):
+        pass
+
+    class Parent(Base):
+        __tablename__ = "parent"
+        id = Column(String(), primary_key=True)
+        id2 = Column(String(), primary_key=True)
+
+    class Child(Base):
+        __tablename__ = "child"
+        id = Column(String(), ForeignKey(Parent.id), primary_key=True)
+        id2 = Column(String(), ForeignKey(Parent.id2), primary_key=True)
+
+    tables, relationships = declarative_to_intermediary(Base)
+
+    assert len(tables) == 2
+    assert len(relationships) == 2
+    assert all(isinstance(t, Table) for t in tables)
+    assert all(isinstance(r, Relation) for r in relationships)
+    for relation in relationships:
+        assert relation.right_cardinality == "1"
+        assert relation.left_cardinality == "1"