From 5b4c9e4f24592348abdb739b66cf17ac13f8aac6 Mon Sep 17 00:00:00 2001 From: Daniil Fajnberg Date: Thu, 25 Aug 2022 17:49:02 +0200 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20page=20for=20?= =?UTF-8?q?self-referential=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/self-referential.md | 136 ++++++++++++++++++ .../advanced/self_referential/__init__.py | 0 .../advanced/self_referential/tutorial001.py | 77 ++++++++++ mkdocs.yml | 1 + .../test_self_referential/__init__.py | 0 .../test_self_referential/test_tutorial001.py | 94 ++++++++++++ 6 files changed, 308 insertions(+) create mode 100644 docs/advanced/self-referential.md create mode 100644 docs_src/advanced/self_referential/__init__.py create mode 100644 docs_src/advanced/self_referential/tutorial001.py create mode 100644 tests/test_advanced/test_self_referential/__init__.py create mode 100644 tests/test_advanced/test_self_referential/test_tutorial001.py diff --git a/docs/advanced/self-referential.md b/docs/advanced/self-referential.md new file mode 100644 index 0000000000..f168e5ff92 --- /dev/null +++ b/docs/advanced/self-referential.md @@ -0,0 +1,136 @@ +# Self-referential relationships + +Oftentimes we need to model a relationship between one entity of some class and another entity (or multiple entities) of that **same** class. This is called a **self-referential** or **recursive** relationship. (The pattern is also sometimes referred to as an **adjacency list**.) + +In database terms this means having a table with a foreign key reference to the primary key in the same table. + +Say, for example, we want to introduce a `Villain` class. 😈 Every villain can have a **boss**, who also must be a villain. If a villain is the boss to other villains, we want to call those his **minions**. + +Let's do this with **SQLModel**. 🤓 + +## Using SQLAlchemy arguments + +We already learned a lot about [Relationship attributes](../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy** and we know that the latter allows defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/14/orm/self_referential.html){.external-link target=_blank}). + +To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/14/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This is supposed to be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments. + +Since SQLAlchemy relationships provide the [`remote_side`](https://docs.sqlalchemy.org/en/14/orm/relationship_api.html#sqlalchemy.orm.relationship.params.remote_side){.external-link target=_blank} parameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code. + +```Python hl_lines="12" +# Code above omitted 👆 + +{!./docs_src/advanced/self_referential/tutorial001.py[ln:6-17]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/self_referential/tutorial001.py!} +``` + +
+ +Using the `sa_relationship_kwargs` parameter, we pass the keyword-argument `remote_side='Villain.id'` to the underlying relationship property. + +!!! info + The SQLAlchemy documentation mentions this in passing, but crucially the `remote_side` value _"may be passed as a Python-evaluable string when using Declarative."_ + + This allows us to pass the `id` field of the class we are just now defining as the remote side of that relationship. + +## Back-populating and self-referencing + +Notice that we explicitly defined the relationship attributes we wanted for referring to the `boss` **as well as** the `minions` of a `Villain`. + +For our purposes, it is necessary that we also provide the `back_populates` parameter to both relationships as explained in detail in a [dedicated chapter](../tutorial/relationship-attributes/back-populates.md){.internal-link target=_blank}. + +In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. (See the chapter on [type annotation strings](../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation.) + +Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide, whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss. (In fact, we would otherwise introduce a circular reference chain, which would not make sense in this context.) Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../tutorial/relationship-attributes/define-relationships-attributes.md#optional-relationship-attributes){.internal-link target=_blank}. + +## Creating instances + +Now let us see how we can create villains with a boss: + +```Python hl_lines="6-7" +# Code above omitted 👆 + +{!./docs_src/advanced/self_referential/tutorial001.py[ln:30-49]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/self_referential/tutorial001.py!} +``` + +
+ +Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor with `boss=thinnus`. + +If we only learn that a villain actually had a secret boss after we have already created him, we can just as easily assign him that boss retroactively: + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/advanced/self_referential/tutorial001.py[ln:30-31]!} + + # Previous code here omitted 👈 + +{!./docs_src/advanced/self_referential/tutorial001.py[ln:51-55]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/self_referential/tutorial001.py!} +``` + +
+ +And if we want to add minions to a boss after the fact, this is as easy as adding items to a Python list (because that's all it is 🤓): + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/advanced/self_referential/tutorial001.py[ln:30-31]!} + + # Previous code here omitted 👈 + +{!./docs_src/advanced/self_referential/tutorial001.py[ln:57-68]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/self_referential/tutorial001.py!} +``` + +
+ +Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead we can simply add `ultra_bot` once again and commit the changes. We do need to refresh them all individually though, if we want to get their updated attributes. + +## Traversing the relationship graph + +By setting up our relationships this way, we can easily go back and forth along the graph representing all relationships we have created so far. + +For example, we can verify that our `clone_bot_1` has a boss, who has his own boss, and one of that top-boss' minions is `ebonite_mew`: + +```Python +top_boss_minions = clone_bot_3.boss.boss.minions +assert any(minion is ebonite_mew for minion in top_boss_minions) # passes +``` + +!!! info + Notice that we can in fact check for **identity** using `is` as opposed to `==` here, since we are dealing with those exact same objects, not just objects that hold the same **data**. diff --git a/docs_src/advanced/self_referential/__init__.py b/docs_src/advanced/self_referential/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/self_referential/tutorial001.py b/docs_src/advanced/self_referential/tutorial001.py new file mode 100644 index 0000000000..f8d1418f04 --- /dev/null +++ b/docs_src/advanced/self_referential/tutorial001.py @@ -0,0 +1,77 @@ +from typing import List, Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Villain(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + power_level: int + + boss_id: Optional[int] = Field( + foreign_key="villain.id", default=None, nullable=True + ) + boss: Optional["Villain"] = Relationship( + back_populates="minions", sa_relationship_kwargs=dict(remote_side="Villain.id") + ) + minions: List["Villain"] = Relationship(back_populates="boss") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=False) + + +def create_db_and_tables() -> None: + SQLModel.metadata.create_all(engine) + + +def create_villains() -> None: + with Session(engine) as session: + thinnus = Villain(name="Thinnus", power_level=9001) + ebonite_mew = Villain(name="Ebonite Mew", power_level=400, boss=thinnus) + dark_shorty = Villain(name="Dark Shorty", power_level=200, boss=thinnus) + ultra_bot = Villain(name="Ultra Bot", power_level=2 ** 9) + session.add(ebonite_mew) + session.add(dark_shorty) + session.add(ultra_bot) + session.commit() + + session.refresh(thinnus) + session.refresh(ebonite_mew) + session.refresh(dark_shorty) + session.refresh(ultra_bot) + + print("Created villain:", thinnus) + print("Created villain:", ebonite_mew) + print("Created villain:", dark_shorty) + print("Created villain:", ultra_bot) + + ultra_bot.boss = thinnus + session.add(ultra_bot) + session.commit() + session.refresh(ultra_bot) + print("Updated villain:", ultra_bot) + + clone_bot_1 = Villain(name="Clone Bot 1", power_level=2 ** 6) + clone_bot_2 = Villain(name="Clone Bot 2", power_level=2 ** 6) + clone_bot_3 = Villain(name="Clone Bot 3", power_level=2 ** 6) + ultra_bot.minions.extend([clone_bot_1, clone_bot_2, clone_bot_3]) + session.add(ultra_bot) + session.commit() + session.refresh(clone_bot_1) + session.refresh(clone_bot_2) + session.refresh(clone_bot_3) + print("Added minion:", clone_bot_1) + print("Added minion:", clone_bot_2) + print("Added minion:", clone_bot_3) + + +def main() -> None: + create_db_and_tables() + create_villains() + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index a27bbde8a1..65ffde2f85 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ nav: - Advanced User Guide: - advanced/index.md - advanced/decimal.md + - advanced/self-referential.md - alternatives.md - help.md - contributing.md diff --git a/tests/test_advanced/test_self_referential/__init__.py b/tests/test_advanced/test_self_referential/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_self_referential/test_tutorial001.py b/tests/test_advanced/test_self_referential/test_tutorial001.py new file mode 100644 index 0000000000..c54ce256fe --- /dev/null +++ b/tests/test_advanced/test_self_referential/test_tutorial001.py @@ -0,0 +1,94 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + +expected_calls = [ + [ + "Created villain:", + { + "name": "Thinnus", + "power_level": 9001, + "id": 1, + "boss_id": None, + }, + ], + [ + "Created villain:", + { + "name": "Ebonite Mew", + "power_level": 400, + "id": 3, + "boss_id": 1, + }, + ], + [ + "Created villain:", + { + "name": "Dark Shorty", + "power_level": 200, + "id": 4, + "boss_id": 1, + }, + ], + [ + "Created villain:", + { + "name": "Ultra Bot", + "power_level": 2 ** 9, + "id": 2, + "boss_id": None, + }, + ], + [ + "Updated villain:", + { + "name": "Ultra Bot", + "power_level": 2 ** 9, + "id": 2, + "boss_id": 1, + }, + ], + [ + "Added minion:", + { + "name": "Clone Bot 1", + "power_level": 2 ** 6, + "id": 5, + "boss_id": 2, + }, + ], + [ + "Added minion:", + { + "name": "Clone Bot 2", + "power_level": 2 ** 6, + "id": 6, + "boss_id": 2, + }, + ], + [ + "Added minion:", + { + "name": "Clone Bot 3", + "power_level": 2 ** 6, + "id": 7, + "boss_id": 2, + }, + ], +] + + +def test_tutorial(clear_sqlmodel): + from docs_src.advanced.self_referential import tutorial001 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls From 7065c7bf00ae89b99cf3014bd57bf54ea0d7d893 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 06:16:57 +0000 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/advanced/self_referential/tutorial001.py | 8 ++++---- .../test_self_referential/test_tutorial001.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs_src/advanced/self_referential/tutorial001.py b/docs_src/advanced/self_referential/tutorial001.py index f8d1418f04..8bcd5baba4 100644 --- a/docs_src/advanced/self_referential/tutorial001.py +++ b/docs_src/advanced/self_referential/tutorial001.py @@ -32,7 +32,7 @@ def create_villains() -> None: thinnus = Villain(name="Thinnus", power_level=9001) ebonite_mew = Villain(name="Ebonite Mew", power_level=400, boss=thinnus) dark_shorty = Villain(name="Dark Shorty", power_level=200, boss=thinnus) - ultra_bot = Villain(name="Ultra Bot", power_level=2 ** 9) + ultra_bot = Villain(name="Ultra Bot", power_level=2**9) session.add(ebonite_mew) session.add(dark_shorty) session.add(ultra_bot) @@ -54,9 +54,9 @@ def create_villains() -> None: session.refresh(ultra_bot) print("Updated villain:", ultra_bot) - clone_bot_1 = Villain(name="Clone Bot 1", power_level=2 ** 6) - clone_bot_2 = Villain(name="Clone Bot 2", power_level=2 ** 6) - clone_bot_3 = Villain(name="Clone Bot 3", power_level=2 ** 6) + clone_bot_1 = Villain(name="Clone Bot 1", power_level=2**6) + clone_bot_2 = Villain(name="Clone Bot 2", power_level=2**6) + clone_bot_3 = Villain(name="Clone Bot 3", power_level=2**6) ultra_bot.minions.extend([clone_bot_1, clone_bot_2, clone_bot_3]) session.add(ultra_bot) session.commit() diff --git a/tests/test_advanced/test_self_referential/test_tutorial001.py b/tests/test_advanced/test_self_referential/test_tutorial001.py index c54ce256fe..2864f27703 100644 --- a/tests/test_advanced/test_self_referential/test_tutorial001.py +++ b/tests/test_advanced/test_self_referential/test_tutorial001.py @@ -36,7 +36,7 @@ "Created villain:", { "name": "Ultra Bot", - "power_level": 2 ** 9, + "power_level": 2**9, "id": 2, "boss_id": None, }, @@ -45,7 +45,7 @@ "Updated villain:", { "name": "Ultra Bot", - "power_level": 2 ** 9, + "power_level": 2**9, "id": 2, "boss_id": 1, }, @@ -54,7 +54,7 @@ "Added minion:", { "name": "Clone Bot 1", - "power_level": 2 ** 6, + "power_level": 2**6, "id": 5, "boss_id": 2, }, @@ -63,7 +63,7 @@ "Added minion:", { "name": "Clone Bot 2", - "power_level": 2 ** 6, + "power_level": 2**6, "id": 6, "boss_id": 2, }, @@ -72,7 +72,7 @@ "Added minion:", { "name": "Clone Bot 3", - "power_level": 2 ** 6, + "power_level": 2**6, "id": 7, "boss_id": 2, }, From d0bd01fa5b364fb6bbbde36628fe62b28a2bddda Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 25 Aug 2025 08:24:27 +0200 Subject: [PATCH 03/12] Fix Ruff C408 Unnecessary `dict()` --- docs_src/advanced/self_referential/tutorial001.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs_src/advanced/self_referential/tutorial001.py b/docs_src/advanced/self_referential/tutorial001.py index 8bcd5baba4..d5752d41aa 100644 --- a/docs_src/advanced/self_referential/tutorial001.py +++ b/docs_src/advanced/self_referential/tutorial001.py @@ -12,7 +12,8 @@ class Villain(SQLModel, table=True): foreign_key="villain.id", default=None, nullable=True ) boss: Optional["Villain"] = Relationship( - back_populates="minions", sa_relationship_kwargs=dict(remote_side="Villain.id") + back_populates="minions", + sa_relationship_kwargs={"remote_side": "Villain.id"}, ) minions: List["Villain"] = Relationship(back_populates="boss") From cfa3541fb61523a97cebfbbb6a197be59ac06faa Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 25 Aug 2025 08:57:49 +0200 Subject: [PATCH 04/12] Fix formatting and update links --- docs/advanced/self-referential.md | 96 ++++++------------------------- 1 file changed, 17 insertions(+), 79 deletions(-) diff --git a/docs/advanced/self-referential.md b/docs/advanced/self-referential.md index f168e5ff92..ad44a7a3f4 100644 --- a/docs/advanced/self-referential.md +++ b/docs/advanced/self-referential.md @@ -10,35 +10,23 @@ Let's do this with **SQLModel**. 🤓 ## Using SQLAlchemy arguments -We already learned a lot about [Relationship attributes](../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy** and we know that the latter allows defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/14/orm/self_referential.html){.external-link target=_blank}). +We already learned a lot about [Relationship attributes](../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy** and we know that the latter allows defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/20/orm/self_referential.html){.external-link target=_blank}). -To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/14/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This is supposed to be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments. +To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This is supposed to be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments. -Since SQLAlchemy relationships provide the [`remote_side`](https://docs.sqlalchemy.org/en/14/orm/relationship_api.html#sqlalchemy.orm.relationship.params.remote_side){.external-link target=_blank} parameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code. +Since SQLAlchemy relationships provide the [`remote_side`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship.params.remote_side){.external-link target=_blank} parameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code. -```Python hl_lines="12" -# Code above omitted 👆 +{* ./docs_src/advanced/self_referential/tutorial001.py ln[6:17] hl[16] *} -{!./docs_src/advanced/self_referential/tutorial001.py[ln:6-17]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/advanced/self_referential/tutorial001.py!} -``` +Using the `sa_relationship_kwargs` parameter, we pass the keyword-argument `remote_side='Villain.id'` to the underlying relationship property. -
+/// info -Using the `sa_relationship_kwargs` parameter, we pass the keyword-argument `remote_side='Villain.id'` to the underlying relationship property. +The SQLAlchemy documentation mentions this in passing, but crucially the `remote_side` value _"may be passed as a Python-evaluable string when using Declarative."_ -!!! info - The SQLAlchemy documentation mentions this in passing, but crucially the `remote_side` value _"may be passed as a Python-evaluable string when using Declarative."_ +This allows us to pass the `id` field of the class we are just now defining as the remote side of that relationship. - This allows us to pass the `id` field of the class we are just now defining as the remote side of that relationship. +/// ## Back-populating and self-referencing @@ -54,70 +42,17 @@ Finally, as with regular (i.e. non-self-referential) foreign key relationships, Now let us see how we can create villains with a boss: -```Python hl_lines="6-7" -# Code above omitted 👆 - -{!./docs_src/advanced/self_referential/tutorial001.py[ln:30-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/advanced/self_referential/tutorial001.py!} -``` - -
+{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:49] hl[34-35] *} Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor with `boss=thinnus`. If we only learn that a villain actually had a secret boss after we have already created him, we can just as easily assign him that boss retroactively: -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/advanced/self_referential/tutorial001.py[ln:30-31]!} - - # Previous code here omitted 👈 - -{!./docs_src/advanced/self_referential/tutorial001.py[ln:51-55]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/advanced/self_referential/tutorial001.py!} -``` - -
+{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,51-55] hl[52] *} And if we want to add minions to a boss after the fact, this is as easy as adding items to a Python list (because that's all it is 🤓): -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/advanced/self_referential/tutorial001.py[ln:30-31]!} - - # Previous code here omitted 👈 - -{!./docs_src/advanced/self_referential/tutorial001.py[ln:57-68]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/advanced/self_referential/tutorial001.py!} -``` - -
+{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,57-68] hl[61] *} Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead we can simply add `ultra_bot` once again and commit the changes. We do need to refresh them all individually though, if we want to get their updated attributes. @@ -132,5 +67,8 @@ top_boss_minions = clone_bot_3.boss.boss.minions assert any(minion is ebonite_mew for minion in top_boss_minions) # passes ``` -!!! info - Notice that we can in fact check for **identity** using `is` as opposed to `==` here, since we are dealing with those exact same objects, not just objects that hold the same **data**. +/// info + +Notice that we can in fact check for **identity** using `is` as opposed to `==` here, since we are dealing with those exact same objects, not just objects that hold the same **data**. + +/// From aac3497ec952f9582e3784477dd36fab3c5e81af Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 25 Aug 2025 09:05:34 +0200 Subject: [PATCH 05/12] Fix include and highlight intervals --- docs/advanced/self-referential.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/advanced/self-referential.md b/docs/advanced/self-referential.md index ad44a7a3f4..b01a517714 100644 --- a/docs/advanced/self-referential.md +++ b/docs/advanced/self-referential.md @@ -42,17 +42,17 @@ Finally, as with regular (i.e. non-self-referential) foreign key relationships, Now let us see how we can create villains with a boss: -{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:49] hl[34-35] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:49] hl[34:35] *} Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor with `boss=thinnus`. If we only learn that a villain actually had a secret boss after we have already created him, we can just as easily assign him that boss retroactively: -{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,51-55] hl[52] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,51:55] hl[52] *} And if we want to add minions to a boss after the fact, this is as easy as adding items to a Python list (because that's all it is 🤓): -{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,57-68] hl[61] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,57:68] hl[61] *} Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead we can simply add `ultra_bot` once again and commit the changes. We do need to refresh them all individually though, if we want to get their updated attributes. From b39981356f3613bbce33fb1e3701005a6c23b7ad Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 25 Aug 2025 09:36:51 +0200 Subject: [PATCH 06/12] Fix include and highlight intervals 2 --- docs/advanced/self-referential.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced/self-referential.md b/docs/advanced/self-referential.md index b01a517714..a7370e63a2 100644 --- a/docs/advanced/self-referential.md +++ b/docs/advanced/self-referential.md @@ -16,7 +16,7 @@ To allow more fine-grained control over it, the `Relationship` constructor allow Since SQLAlchemy relationships provide the [`remote_side`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship.params.remote_side){.external-link target=_blank} parameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code. -{* ./docs_src/advanced/self_referential/tutorial001.py ln[6:17] hl[16] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[6:18] hl[16] *} Using the `sa_relationship_kwargs` parameter, we pass the keyword-argument `remote_side='Villain.id'` to the underlying relationship property. @@ -42,17 +42,17 @@ Finally, as with regular (i.e. non-self-referential) foreign key relationships, Now let us see how we can create villains with a boss: -{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:49] hl[34:35] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[31:50] hl[34:35] *} Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor with `boss=thinnus`. If we only learn that a villain actually had a secret boss after we have already created him, we can just as easily assign him that boss retroactively: -{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,51:55] hl[52] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[31:32,52:56] hl[52] *} And if we want to add minions to a boss after the fact, this is as easy as adding items to a Python list (because that's all it is 🤓): -{* ./docs_src/advanced/self_referential/tutorial001.py ln[30:31,57:68] hl[61] *} +{* ./docs_src/advanced/self_referential/tutorial001.py ln[31:32,58:69] hl[61] *} Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead we can simply add `ultra_bot` once again and commit the changes. We do need to refresh them all individually though, if we want to get their updated attributes. From 4016556196ff5d502b637392b4c96bcc428663d2 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:27:10 +0200 Subject: [PATCH 07/12] Update issue-manager.yml --- .github/workflows/issue-manager.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 0737b40a8f..a44397d03b 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -1,8 +1,8 @@ name: Issue Manager on: - schedule: - - cron: "13 18 * * *" + # schedule: + # - cron: "13 18 * * *" issue_comment: types: - created From 7e45ae386e941d871ae9bd92b965d4c27ad42096 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Wed, 17 Sep 2025 17:02:10 +0200 Subject: [PATCH 08/12] Apply Yurii's rephrasing suggestions Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/advanced/self-referential.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/advanced/self-referential.md b/docs/advanced/self-referential.md index a7370e63a2..24330b03da 100644 --- a/docs/advanced/self-referential.md +++ b/docs/advanced/self-referential.md @@ -1,6 +1,6 @@ # Self-referential relationships -Oftentimes we need to model a relationship between one entity of some class and another entity (or multiple entities) of that **same** class. This is called a **self-referential** or **recursive** relationship. (The pattern is also sometimes referred to as an **adjacency list**.) +Oftentimes we need to model a relationship between one entity of some class and another entity (or multiple entities) of that **same** class. This is known as a **self-referential** or **recursive** relationship, sometimes also called an **adjacency list**. In database terms this means having a table with a foreign key reference to the primary key in the same table. @@ -22,9 +22,13 @@ Using the `sa_relationship_kwargs` parameter, we pass the keyword-argument `remo /// info -The SQLAlchemy documentation mentions this in passing, but crucially the `remote_side` value _"may be passed as a Python-evaluable string when using Declarative."_ +The `remote_side` parameter accepts a Python-evaluable string when using Declarative. This allows us to reference `Villain.id` even though the class is still being defined. -This allows us to pass the `id` field of the class we are just now defining as the remote side of that relationship. +Alternatively, you can use a callable: + +```py +sa_relationship_kwargs={"remote_side": lambda : Villain.id} +``` /// @@ -34,9 +38,9 @@ Notice that we explicitly defined the relationship attributes we wanted for refe For our purposes, it is necessary that we also provide the `back_populates` parameter to both relationships as explained in detail in a [dedicated chapter](../tutorial/relationship-attributes/back-populates.md){.internal-link target=_blank}. -In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. (See the chapter on [type annotation strings](../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation.) +In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. See the chapter on [type annotation strings](../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation. -Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide, whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss. (In fact, we would otherwise introduce a circular reference chain, which would not make sense in this context.) Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../tutorial/relationship-attributes/define-relationships-attributes.md#optional-relationship-attributes){.internal-link target=_blank}. +Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide, whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss (in fact, we would otherwise introduce a circular reference chain, which would not make sense in this context). Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../tutorial/relationship-attributes/define-relationships-attributes.md#optional-relationship-attributes){.internal-link target=_blank}. ## Creating instances From 95df345d186447fc962c5d90378b605884209018 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 8 Oct 2025 22:05:46 +0200 Subject: [PATCH 09/12] Move to `advanced-relationships`, fix links --- .../{ => advanced-relationships}/self-referential.md | 8 ++++---- mkdocs.yml | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) rename docs/advanced/{ => advanced-relationships}/self-referential.md (83%) diff --git a/docs/advanced/self-referential.md b/docs/advanced/advanced-relationships/self-referential.md similarity index 83% rename from docs/advanced/self-referential.md rename to docs/advanced/advanced-relationships/self-referential.md index 24330b03da..7aa1d32c0c 100644 --- a/docs/advanced/self-referential.md +++ b/docs/advanced/advanced-relationships/self-referential.md @@ -10,7 +10,7 @@ Let's do this with **SQLModel**. 🤓 ## Using SQLAlchemy arguments -We already learned a lot about [Relationship attributes](../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy** and we know that the latter allows defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/20/orm/self_referential.html){.external-link target=_blank}). +We already learned a lot about [Relationship attributes](../../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy** and we know that the latter allows defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/20/orm/self_referential.html){.external-link target=_blank}). To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This is supposed to be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments. @@ -36,11 +36,11 @@ sa_relationship_kwargs={"remote_side": lambda : Villain.id} Notice that we explicitly defined the relationship attributes we wanted for referring to the `boss` **as well as** the `minions` of a `Villain`. -For our purposes, it is necessary that we also provide the `back_populates` parameter to both relationships as explained in detail in a [dedicated chapter](../tutorial/relationship-attributes/back-populates.md){.internal-link target=_blank}. +For our purposes, it is necessary that we also provide the `back_populates` parameter to both relationships as explained in detail in a [dedicated chapter](../../tutorial/relationship-attributes/back-populates.md){.internal-link target=_blank}. -In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. See the chapter on [type annotation strings](../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation. +In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. See the chapter on [type annotation strings](../../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation. -Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide, whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss (in fact, we would otherwise introduce a circular reference chain, which would not make sense in this context). Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../tutorial/relationship-attributes/define-relationships-attributes.md#optional-relationship-attributes){.internal-link target=_blank}. +Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide, whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss (in fact, we would otherwise introduce a circular reference chain, which would not make sense in this context). Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../../tutorial/relationship-attributes/define-relationships-attributes.md#relationship-attributes-or-none){.internal-link target=_blank}. ## Creating instances diff --git a/mkdocs.yml b/mkdocs.yml index d9f381f6ba..7c9ccad1c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -128,7 +128,8 @@ nav: - advanced/index.md - advanced/decimal.md - advanced/uuid.md - - advanced/self-referential.md + - Advanced Relationships: + - advanced/advanced-relationships/self-referential.md - Resources: - resources/index.md - help.md From daf3975dbf513d453ae2ceea1a41589d49fd222e Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 8 Oct 2025 22:07:06 +0200 Subject: [PATCH 10/12] Some text improvements --- .../self-referential.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/advanced/advanced-relationships/self-referential.md b/docs/advanced/advanced-relationships/self-referential.md index 7aa1d32c0c..51f03828ca 100644 --- a/docs/advanced/advanced-relationships/self-referential.md +++ b/docs/advanced/advanced-relationships/self-referential.md @@ -6,19 +6,19 @@ In database terms this means having a table with a foreign key reference to the Say, for example, we want to introduce a `Villain` class. 😈 Every villain can have a **boss**, who also must be a villain. If a villain is the boss to other villains, we want to call those his **minions**. -Let's do this with **SQLModel**. 🤓 +Let's implement this with **SQLModel**. 🤓 ## Using SQLAlchemy arguments -We already learned a lot about [Relationship attributes](../../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy** and we know that the latter allows defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/20/orm/self_referential.html){.external-link target=_blank}). +We already learned a lot about [Relationship attributes](../../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy**, which supports defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/20/orm/self_referential.html){.external-link target=_blank}). -To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This is supposed to be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments. +To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This should be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments. Since SQLAlchemy relationships provide the [`remote_side`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship.params.remote_side){.external-link target=_blank} parameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code. {* ./docs_src/advanced/self_referential/tutorial001.py ln[6:18] hl[16] *} -Using the `sa_relationship_kwargs` parameter, we pass the keyword-argument `remote_side='Villain.id'` to the underlying relationship property. +Using the `sa_relationship_kwargs` parameter, we pass the keyword argument `remote_side='Villain.id'` to the underlying relationship property. /// info @@ -40,31 +40,31 @@ For our purposes, it is necessary that we also provide the `back_populates` para In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. See the chapter on [type annotation strings](../../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation. -Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide, whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss (in fact, we would otherwise introduce a circular reference chain, which would not make sense in this context). Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../../tutorial/relationship-attributes/define-relationships-attributes.md#relationship-attributes-or-none){.internal-link target=_blank}. +Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss (in fact, we would otherwise introduce a circular reference chain, which would not make sense in this context). Therefore we declare `boss_id: Optional[int]` and `boss: Optional['Villain']`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../../tutorial/relationship-attributes/define-relationships-attributes.md#relationship-attributes-or-none){.internal-link target=_blank}. ## Creating instances -Now let us see how we can create villains with a boss: +Now let's see how we can create villains with a boss: {* ./docs_src/advanced/self_referential/tutorial001.py ln[31:50] hl[34:35] *} -Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor with `boss=thinnus`. +Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor using `boss=thinnus`. -If we only learn that a villain actually had a secret boss after we have already created him, we can just as easily assign him that boss retroactively: +If we later learn that a villain actually had a secret boss after we've already created him, we can just as easily assign that boss retroactively: {* ./docs_src/advanced/self_referential/tutorial001.py ln[31:32,52:56] hl[52] *} -And if we want to add minions to a boss after the fact, this is as easy as adding items to a Python list (because that's all it is 🤓): +And if we want to add minions to a boss afterward, it's as easy as adding items to a Python list (because that's all it is 🤓): {* ./docs_src/advanced/self_referential/tutorial001.py ln[31:32,58:69] hl[61] *} -Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead we can simply add `ultra_bot` once again and commit the changes. We do need to refresh them all individually though, if we want to get their updated attributes. +Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead, we can simply add ultra_bot again and commit the changes. We do need to refresh them individually, though, if we want to access their updated attributes. ## Traversing the relationship graph -By setting up our relationships this way, we can easily go back and forth along the graph representing all relationships we have created so far. +By setting up our relationships this way, we can easily go back and forth along the graph representing all the relationships we've created so far. -For example, we can verify that our `clone_bot_1` has a boss, who has his own boss, and one of that top-boss' minions is `ebonite_mew`: +For example, we can verify that our `clone_bot_1` has a boss, who has his own boss, and that one of that top boss's minions is `ebonite_mew`: ```Python top_boss_minions = clone_bot_3.boss.boss.minions @@ -73,6 +73,6 @@ assert any(minion is ebonite_mew for minion in top_boss_minions) # passes /// info -Notice that we can in fact check for **identity** using `is` as opposed to `==` here, since we are dealing with those exact same objects, not just objects that hold the same **data**. +Notice that we can, in fact, check for **identity** using `is` instead of `==` here, since we are dealing with the exact same objects, not just objects containing the same **data**. /// From a8a4cea9d0e0aa65409902829438e9624b030be2 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 8 Oct 2025 22:10:58 +0200 Subject: [PATCH 11/12] Revert "Update issue-manager.yml" This reverts commit 4016556196ff5d502b637392b4c96bcc428663d2. --- .github/workflows/issue-manager.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index ae1afc7b34..90d3d5fd4f 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -1,8 +1,8 @@ name: Issue Manager on: - # schedule: - # - cron: "13 18 * * *" + schedule: + - cron: "13 18 * * *" issue_comment: types: - created From c5de013bb6a05cfed24984d7966f44ff9fc55ddf Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:45:04 +0200 Subject: [PATCH 12/12] Apply suggestions from code review Co-authored-by: Sofie Van Landeghem --- docs/advanced/advanced-relationships/self-referential.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/advanced-relationships/self-referential.md b/docs/advanced/advanced-relationships/self-referential.md index 51f03828ca..e44be8b8fe 100644 --- a/docs/advanced/advanced-relationships/self-referential.md +++ b/docs/advanced/advanced-relationships/self-referential.md @@ -4,7 +4,7 @@ Oftentimes we need to model a relationship between one entity of some class and In database terms this means having a table with a foreign key reference to the primary key in the same table. -Say, for example, we want to introduce a `Villain` class. 😈 Every villain can have a **boss**, who also must be a villain. If a villain is the boss to other villains, we want to call those his **minions**. +Say, for example, we want to introduce a `Villain` class. 😈 Every villain can have a **boss**, who also must be a villain. If a villain is the boss of other villains, we want to call those his **minions**. Let's implement this with **SQLModel**. 🤓 @@ -58,7 +58,7 @@ And if we want to add minions to a boss afterward, it's as easy as adding items {* ./docs_src/advanced/self_referential/tutorial001.py ln[31:32,58:69] hl[61] *} -Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead, we can simply add ultra_bot again and commit the changes. We do need to refresh them individually, though, if we want to access their updated attributes. +Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead, we can simply add `ultra_bot` again and commit the changes. We do need to refresh them individually, though, if we want to access their updated attributes. ## Traversing the relationship graph