odmantic icon indicating copy to clipboard operation
odmantic copied to clipboard

Odmantic Does Not Add Pydantic Computed Fields to MongoDB

Open ltieman opened this issue 1 year ago • 2 comments

Bug

A clear and concise description of what the bug is.

Pydantic has a nice feature around computed fields that allow you to create an attribute based on logic in other fields, however these fields are not added to Model.__odm_fields__ which means they don't get used when running engine.save(Model)

If you add a ODMBaseField to Model.__odm_fields__ it will include it, but this is very hacky and shouldn't be the right way to do this.

Current Behavior

class ExampleModel(Model):
    data_from_user: str
    
    @computed_field
    def computed_data(self)->str:
        return somefunction(self.data_from_user)
        
item = ExampleModel.model_validate({"data_from_user":"foo"})

item.model_dump_doc() == {"data_from_user":"foo", "computed_data":"bar"}

engine.save(item)

then the data in the collection is {"data_from_user":"foo"}

Expected behavior

class ExampleModel(Model):
    data_from_user: str
    
    @computed_field
    def computed_data(self)->str:
        return somefunction(self.data_from_user)
        
item = ExampleModel.model_validate({"data_from_user":"foo"})

item.model_dump_doc() == {"data_from_user":"foo", "computed_data":"bar"}

engine.save(item)

then the data in the collection would be {"data_from_user":"foo", "computed_data":"bar"}

Environment

  • ODMantic version: 1.0.0
  • MongoDB version: 6.0.13
  • Pydantic infos (output of python -c "import pydantic.utils; print(pydantic.utils.version_info())): pydantic version: 2.5.3 pydantic-core version: 2.14.6 pydantic-core build: profile=release pgo=true install path: /home/lucastieman/PycharmProjects/monorepo/venv/lib/python3.11/site-packages/pydantic python version: 3.11.6 (main, Oct 3 2023, 00:00:00) [GCC 13.2.1 20230728 (Red Hat 13.2.1-1)] platform: Linux-6.6.3-100.fc38.x86_64-x86_64-with-glibc2.37 related packages: typing_extensions-4.8.0 fastapi-0.104.1 email-validator-2.1.0.post1

ltieman avatar Jan 26 '24 22:01 ltieman

I'd propose the following change to the engine._save method

async def _save(
        self, instance: ModelType, session: "AsyncIOMotorClientSession"
    ) -> ModelType:
        """Perform an atomic save operation in the specified session"""
        for ref_field_name in instance.__references__:
            sub_instance = cast(Model, getattr(instance, ref_field_name))
            await self._save(sub_instance, session)

        fields_to_update = instance.__fields_modified__ | instance.__mutable_fields__ | {ODMBaseField(key,instance.model_config) for key, value in instance.model_computed_fields.items() if value.repr}
        if len(fields_to_update) > 0:
            doc = instance.model_dump_doc(include=fields_to_update)
            collection = self.get_collection(type(instance))
            try:
                await collection.update_one(
                    instance.model_dump_doc(include={instance.__primary_field__}),
                    {"$set": doc},
                    upsert=True,
                    session=session,
                )
            except pymongo.errors.DuplicateKeyError as e:
                raise DuplicateKeyError(instance, e)
            object.__setattr__(instance, "__fields_modified__", set())
        return instance

ltieman avatar Jan 26 '24 23:01 ltieman

I support this change, I am facing the same issue.

Oliversinn avatar May 31 '24 01:05 Oliversinn