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.
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
When running code above, approximately 50% of the script runs, it fails with error similar to this one:
Sometimes error says that object has no attribute "mime" or "size".
Expected behavior
Query selects reverse relations properly.
Additional context
Broken sql
Working sql
Queries seem to be similar, but some of the joins/select fields ordered differently.
Also, in both cases there are duplicated fields in
SELECTclause (e.g. stickersetthumb is selected 4 times), which may be relevant to the issue.Removing
stickersetfromFileand then movingthumbrelation toStickersetsolves the problem.Removing either
stickersetoremojisetfromChannelalso solves this issue.Using
.only()(like in the comment), also seems to solve this.