tortoise / tortoise-orm

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

OperationalError after retrieving row from db by id, manually assigning the value from IntEnum to the relevant attribute of the model instance and calling `save()` on it. #113

Closed croshchupkin closed 5 years ago

croshchupkin commented 5 years ago

Basically, subj. To repro, please use the following code:

# models.py
from enum import IntEnum

from tortoise import Model, fields

class ContactTypeEnum(IntEnum):
    home = 1
    work = 2
    other = 3

class Contact(Model):
    id = fields.IntField(pk=True)
    phone_no = fields.CharField(30, null=False, default='')
    type = fields.IntField(required=True, default=ContactTypeEnum.other)

Then run the script:

from tortoise import run_async, Tortoise

from models import Contact, ContactTypeEnum

async def run():
    await Tortoise.init(
        db_url='sqlite://db.sqlite3',
        modules={'models': ['models']}
    )
    await Tortoise.generate_schemas()

    await Contact.create(phone_no='222', type=ContactTypeEnum.work)

    breakpoint()
    contact = await Contact.get(id=1)
    for name, value in {'phone_no': '1111', 'type': ContactTypeEnum.other}.items():
        setattr(contact, name, value)
    await contact.save()

if __name__ == '__main__':
    run_async(run())

I get the following exception: tortoise.exceptions.OperationalError: no such column: ContactTypeEnum.other. But before the update, the new model instance was saved successfully.

croshchupkin commented 5 years ago

After having stepped through the code for a bit, it seems that pypika constructs the following SQL query: UPDATE "contact" SET "phone_no"='1111',"type"=ContactTypeEnum.other

croshchupkin commented 5 years ago

I assume that the query will work fine if I do int(ContactTypeEnum.other) - but I just find it strange that the entity is created without issue while the update has problems.

croshchupkin commented 5 years ago

After digging around some more, I think this is most likely not a bug, but a "feature" of Enum's default __str__. This can be tweaked by creating the enum like so:

class ContactTypeEnum(IntEnum):
    home = 1
    work = 2
    other = 3

    def __str__(self):
        return str(self._value_)

For REPL readability purposes, in the end, it's probably better to just explicitly cast to int, I suppose...

grigi commented 5 years ago

Creates are done using parametrized queries, updates/selects are not yet. related to #81

grigi commented 5 years ago

Could you add this as some failing tests that are marked with @test.expectedFailure that does insert/update and filter on an enumfield? The same feature also blocks dealing with binary blobs.

croshchupkin commented 5 years ago

Ok, I'll try to do this later today.

croshchupkin commented 5 years ago

Some Travis builds are failing for the pull request, but I'm not exactly sure why...

grigi commented 5 years ago

I had a look, and it is because the linter complained about import order:

flake8 tortoise/ examples/ setup.py conftest.py
tortoise/tests/test_update.py:2:1: I001 isort found an import in the wrong position
Makefile:27: recipe for target 'check' failed
make: *** [check] Error 1

I tried to document some basic guidelines here: https://tortoise-orm.readthedocs.io/en/latest/CONTRIBUTING.html#style

But basically all it means is that an import at line 2 of test_update.py isn't according to the isort (https://github.com/timothycrosley/isort) spec.

Basically run isort tortoise/tests/test_update.py or make style.

It will update imports to be grouped and sorted:

from tortoise.tests.testmodels import Event, Tournament, Contact, ContactTypeEnum

to

from tortoise.tests.testmodels import Contact, ContactTypeEnum, Event, Tournament
revimi commented 5 years ago

@croshchupkin You are trying to save something other than INT to IntField

In [2]: type(ContactTypeEnum.work)                                                                                                                                                                                             
Out[2]: <enum 'ContactTypeEnum'>

In [3]: type(ContactTypeEnum.work.value)                                                                                                                                                                                       
Out[3]: int

It's normal behavior. The only change that is required is to provide a readable exception.

P.S. You can use something like an EnumField used in the test suite:

class Contact(Model):
    id = fields.IntField(pk=True)
    type = EnumField(enum_type=ContactTypeEnum, default=ContactTypeEnum.other)