tortoise / tortoise-orm

Familiar asyncio ORM for python, built with relations in mind
https://tortoise.github.io
Apache License 2.0
4.37k stars 355 forks source link

[BUG] get_or_create function raises "Object does not exist" #1634

Open rathmerdominik opened 4 weeks ago

rathmerdominik commented 4 weeks ago

Describe the bug While writing a discord bot for a project of a friend of mine i noticed that the get_or_create function does not seem to work as expected.

I can also fully confirm that every value has something in it. I did a print before the model creation call which confirms that every parameter should receive a proper value.

The following error occurs: (The last one is obviously a discord error)

Traceback (most recent call last):
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/models.py", line 1075, in get_or_create
    return await cls.filter(**kwargs).using_db(db).get(), False
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/queryset.py", line 1073, in _execute
    raise DoesNotExist("Object does not exist")
tortoise.exceptions.DoesNotExist: Object does not exist

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/fields/base.py", line 284, in validate
    v(value)
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/validators.py", line 44, in __call__
    raise ValidationError("Value must not be None")
tortoise.exceptions.ValidationError: Value must not be None

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/discord/app_commands/commands.py", line 827, in _do_call
    return await self._callback(self.binding, interaction, **params)  # type: ignore
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dominik/Projects/pirate-era-bot/pirate_era_bot/plugins/rule_book/rule_book.py", line 39, in designate_rule_channel
    rule_message_tuple = await RuleMessage(
                         ^^^^^^^^^^^^^^^^^^
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/models.py", line 1079, in get_or_create
    return await cls.create(using_db=connection, **defaults, **kwargs), True
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/models.py", line 1154, in create
    await instance.save(using_db=db, force_create=True)
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/models.py", line 962, in save
    await executor.execute_insert(self)
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/backends/base/executor.py", line 224, in execute_insert
    self.column_map[field_name](getattr(instance, field_name), instance)
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/fields/base.py", line 256, in to_db_value
    self.validate(value)
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/tortoise/fields/base.py", line 286, in validate
    raise ValidationError(f"{self.model_field_name}: {exc}")
tortoise.exceptions.ValidationError: title: Value must not be None

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/discord/app_commands/tree.py", line 1248, in _call
    await command._invoke_with_namespace(interaction, namespace)
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/discord/app_commands/commands.py", line 853, in _invoke_with_namespace
    return await self._do_call(interaction, transformed_values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dominik/.cache/pypoetry/virtualenvs/pirate-era-bot-doMltmxG-py3.12/lib/python3.12/site-packages/discord/app_commands/commands.py", line 846, in _do_call
    raise CommandInvokeError(self, e) from e
discord.app_commands.errors.CommandInvokeError: Command 'create-or-edit-rule-message' raised an exception: ValidationError: title: Value must not be None

To Reproduce Using this code it will crash with the mentioned error.

    @app_commands.command(
        name="create-or-edit-rule-message",
        description="Create or edit rule explanation message",
    )
    @app_commands.describe(
        channel="Channel to designate as rule channel",
        title="Title of the rule message",
        description="Description of the rule message",
        colour="Colour of the embed. Must be in hexadecimal format. Example: #FF0000",
        footer="Footer of the rule message",
    )
    async def designate_rule_channel(
        self,
        interaction: discord.Interaction,
        channel: discord.TextChannel,
        title: str,
        description: str,
        colour: Optional[str],
        footer: Optional[str],
    ):
        print(f"Title: {title}")
        rule_message_tuple = await RuleMessage(
            id=channel.id,
            title=title,
            description=description,
            colour=colour,
            footer=footer,
        ).get_or_create()

        rule_message: RuleMessage = rule_message_tuple[0]

        embed = discord.Embed(
            title=rule_message.title,
            description=rule_message.description,
            colour=discord.Color.from_str(rule_message.colour),
        ).set_footer(text=rule_message.footer)

        assert not isinstance(interaction.channel, discord.ForumChannel)
        assert not isinstance(interaction.channel, discord.CategoryChannel)
        assert not isinstance(interaction.channel, discord.GroupChannel)

        if rule_message.message_id and isinstance(rule_message.message_id, int):
            created_message = channel.get_partial_message(rule_message.message_id)
            await created_message.edit(embed=embed)
            await interaction.response.send_message(
                f"Rule message {rule_message.title} edited with ID {rule_message.id}",
                ephemeral=True,
            )
        else:
            rule_message = await channel.send(embed=embed)
            rule_message = await RuleMessage.filter(id=interaction.channel_id).update(
                message_id=rule_message.id
            )
            await interaction.response.send_message(
                f"Rule message {rule_message.title} created with ID {rule_message.id}",
                ephemeral=True,
            )

RuleMessage looks like this:

from tortoise import fields
from tortoise.models import Model

class RuleMessage(Model):
    id = fields.BigIntField(primary_key=True)
    title = fields.CharField(256)  # Discord limitations
    description = fields.CharField(4096)  # Discord limitations
    colour = fields.CharField(7, null=True)  # Hexadecimal colour
    footer = fields.CharField(2048, null=True)  # Discord limitations
    message_id = fields.BigIntField(null=True)

    def __str__(self) -> str:
        return f"title={self.title} description={self.description} colour={self.colour} footer={self.footer}"

Expected behavior The model is either written to the database or retrieved from the database.

Additional context Using .save() on it saves it to the database and works as expected. But this is rather cumbersome.

abondar commented 4 weeks ago

What does print before calling prints?

Does it reproduce in 100% of cases or only sometimes? Are you able to provide reproducible code? As I can't really use given code to reproduce error