Skip to content

Generated query with multiple reverse joins to the same model is sometimes broken #2188

@RuslanUC

Description

@RuslanUC

Describe the bug

When some query has multiple reverse relations to same model, sometimes (approximately 50% of the runs (not query executions, but actual script runs)) generated sql is broken.

To Reproduce

main.py
import asyncio
from os import environ

from tortoise import fields, Model, Tortoise


class File(Model):
    id: int = fields.BigIntField(primary_key=True)
    mime: str = fields.CharField(max_length=64)
    size: int = fields.IntField()
    stickerset: Stickerset | None = fields.ForeignKeyField("models.Stickerset", null=True, default=None)


class Stickerset(Model):
    id: int = fields.BigIntField(primary_key=True)
    name: str = fields.CharField(max_length=64)


class StickersetThumb(Model):
    id: int = fields.BigIntField(primary_key=True)
    set: Stickerset = fields.OneToOneField("models.Stickerset", related_name="thumb")
    file: File = fields.ForeignKeyField("models.File")


class Channel(Model):
    id: int = fields.BigIntField(primary_key=True)
    name: str = fields.CharField(max_length=64)
    stickerset: Stickerset | None = fields.ForeignKeyField("models.Stickerset", related_name="stickerset_channels", null=True, default=None)
    emojiset: Stickerset | None = fields.ForeignKeyField("models.Stickerset", related_name="emojiset_channels", null=True, default=None)
    photo: File | None = fields.ForeignKeyField("models.File", null=True, default=None)


TORTOISE_ORM = {
    "connections": {
        "default": environ.get("DB_CONNECTION_STRING", "sqlite://:memory:"),
    },
    "apps": {
        "models": {
            "models": ["__main__"] if __name__ == "__main__" else ["main"],
            "default_connection": "default",
            "migrations": "migrations",
        },
    },
}


async def _actually_main() -> None:
    photo_file = await File.create(mime="image/jpeg", size=123456)
    channel = await Channel.create(name="test", photo=photo_file)

    access_query = Channel.get_or_none(id=channel.id).select_related(
        "photo", "stickerset", "emojiset", "stickerset__thumb", "stickerset__thumb__file", "emojiset__thumb",
        "emojiset__thumb__file",
    )  # .only("id", "photo__id", "photo__mime", "photo__size")

    print(access_query.sql(True))

    channel = await access_query
    print(
        channel, channel.photo, channel.photo.id, channel.photo.mime, channel.photo.size
    )


async def _main() -> None:
    await Tortoise.init(config=TORTOISE_ORM, _create_db=True)
    await Tortoise.generate_schemas()

    try:
        await _actually_main()
    finally:
        await Tortoise.close_connections()


if __name__ == "__main__":
    asyncio.new_event_loop().run_until_complete(_main())

When running code above, approximately 50% of the script runs, it fails with error similar to this one:

Traceback (most recent call last):
  File "/home/ruslan/dev/python/tmp/tortoise-orm-broken-select_related/main.py", line 75, in <module>
    asyncio.new_event_loop().run_until_complete(_main())
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 719, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/home/ruslan/dev/python/tmp/tortoise-orm-broken-select_related/main.py", line 69, in _main
    await _actually_main()
  File "/home/ruslan/dev/python/tmp/tortoise-orm-broken-select_related/main.py", line 60, in _actually_main
    channel, channel.photo, channel.photo.id, channel.photo.mime, channel.photo.size
                            ^^^^^^^^^^^^^^^^
AttributeError: 'File' object has no attribute 'id'

Sometimes error says that object has no attribute "mime" or "size".

Expected behavior

Query selects reverse relations properly.

Additional context

Broken sql
SELECT 
    "channel"."stickerset_id",
    "channel"."photo_id",
    "channel"."name",
    "channel"."emojiset_id",
    "channel"."id",
    "channel__stickerset"."id" "channel__stickerset.id",
    "channel__stickerset"."name" "channel__stickerset.name",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "stickersetthumb__file"."size" "stickersetthumb__file.size",
    "stickersetthumb__file"."id" "stickersetthumb__file.id",
    "stickersetthumb__file"."stickerset_id" "stickersetthumb__file.stickerset_id",
    "stickersetthumb__file"."mime" "stickersetthumb__file.mime",
    "channel__stickerset"."id" "channel__stickerset.id",
    "channel__stickerset"."name" "channel__stickerset.name",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "channel__emojiset"."id" "channel__emojiset.id",
    "channel__emojiset"."name" "channel__emojiset.name",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "channel__emojiset"."id" "channel__emojiset.id",
    "channel__emojiset"."name" "channel__emojiset.name",
    "channel__stickerset"."id" "channel__stickerset.id",
    "channel__stickerset"."name" "channel__stickerset.name",
    "channel__emojiset"."id" "channel__emojiset.id",
    "channel__emojiset"."name" "channel__emojiset.name",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "stickersetthumb__file"."size" "stickersetthumb__file.size",
    "stickersetthumb__file"."id" "stickersetthumb__file.id",
    "stickersetthumb__file"."stickerset_id" "stickersetthumb__file.stickerset_id",
    "stickersetthumb__file"."mime" "stickersetthumb__file.mime",
    "channel__photo"."size" "channel__photo.size",
    "channel__photo"."id" "channel__photo.id",
    "channel__photo"."stickerset_id" "channel__photo.stickerset_id",
    "channel__photo"."mime" "channel__photo.mime" 
FROM "channel" 
    LEFT OUTER JOIN "stickerset" "channel__stickerset" ON "channel__stickerset"."id"="channel"."stickerset_id" 
    LEFT OUTER JOIN "stickersetthumb" ON "channel__stickerset"."id"="stickersetthumb"."set_id" 
    LEFT OUTER JOIN "file" "stickersetthumb__file" ON "stickersetthumb__file"."id"="stickersetthumb"."file_id" 
    LEFT OUTER JOIN "stickerset" "channel__emojiset" ON "channel__emojiset"."id"="channel"."emojiset_id" 
    LEFT OUTER JOIN "file" "channel__photo" ON "channel__photo"."id"="channel"."photo_id" 
WHERE "channel"."id"=1 LIMIT 2;
Working sql
SELECT 
    "channel"."name",
    "channel"."photo_id",
    "channel"."id",
    "channel"."stickerset_id",
    "channel"."emojiset_id",
    "channel__emojiset"."name" "channel__emojiset.name",
    "channel__emojiset"."id" "channel__emojiset.id",
    "channel__photo"."stickerset_id" "channel__photo.stickerset_id",
    "channel__photo"."id" "channel__photo.id",
    "channel__photo"."size" "channel__photo.size",
    "channel__photo"."mime" "channel__photo.mime",
    "channel__emojiset"."name" "channel__emojiset.name",
    "channel__emojiset"."id" "channel__emojiset.id",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "stickersetthumb__file"."stickerset_id" "stickersetthumb__file.stickerset_id",
    "stickersetthumb__file"."id" "stickersetthumb__file.id",
    "stickersetthumb__file"."size" "stickersetthumb__file.size",
    "stickersetthumb__file"."mime" "stickersetthumb__file.mime",
    "channel__emojiset"."name" "channel__emojiset.name",
    "channel__emojiset"."id" "channel__emojiset.id",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "channel__stickerset"."name" "channel__stickerset.name",
    "channel__stickerset"."id" "channel__stickerset.id",
    "channel__stickerset"."name" "channel__stickerset.name",
    "channel__stickerset"."id" "channel__stickerset.id",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id",
    "stickersetthumb__file"."stickerset_id" "stickersetthumb__file.stickerset_id",
    "stickersetthumb__file"."id" "stickersetthumb__file.id",
    "stickersetthumb__file"."size" "stickersetthumb__file.size",
    "stickersetthumb__file"."mime" "stickersetthumb__file.mime",
    "channel__stickerset"."name" "channel__stickerset.name",
    "channel__stickerset"."id" "channel__stickerset.id",
    "stickersetthumb"."file_id" "stickersetthumb.file_id",
    "stickersetthumb"."id" "stickersetthumb.id",
    "stickersetthumb"."set_id" "stickersetthumb.set_id" 
FROM "channel" 
    LEFT OUTER JOIN "stickerset" "channel__emojiset" ON "channel__emojiset"."id"="channel"."emojiset_id" 
    LEFT OUTER JOIN "file" "channel__photo" ON "channel__photo"."id"="channel"."photo_id" 
    LEFT OUTER JOIN "stickersetthumb" ON "channel__emojiset"."id"="stickersetthumb"."set_id" 
    LEFT OUTER JOIN "file" "stickersetthumb__file" ON "stickersetthumb__file"."id"="stickersetthumb"."file_id" 
    LEFT OUTER JOIN "stickerset" "channel__stickerset" ON "channel__stickerset"."id"="channel"."stickerset_id" 
WHERE "channel"."id"=1 LIMIT 2;

Queries seem to be similar, but some of the joins/select fields ordered differently.
Also, in both cases there are duplicated fields in SELECT clause (e.g. stickersetthumb is selected 4 times), which may be relevant to the issue.

Removing stickerset from File and then moving thumb relation to Stickerset solves the problem.
Removing either stickerset or emojiset from Channel also solves this issue.
Using .only() (like in the comment), also seems to solve this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions