W1ldPo1nter / django-queryable-properties

Write Django model properties that can be used in database queries.
BSD 3-Clause "New" or "Revised" License
72 stars 1 forks source link

AttributeError: __querying_properties__ when doing Model.objects.all().delete() #17

Open merpius opened 1 month ago

merpius commented 1 month ago

I have a model class with the following setup;

class Tenant(models.Model):
    owner_store = models.ForeignKey(
        SearchStaxUser, related_name="owner_store", on_delete=models.CASCADE, null=True, blank=True)
    active = models.BooleanField(default=True)
    deleted = models.BooleanField(default=False, db_index=True)
    parent = models.ForeignKey(
        'self',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='children',
        db_index=True
    )

    objects = QueryablePropertiesManager()

    @queryable_property
    def owner(self):
        if self.owner_store is None and self.parent is not None and self.parent.deleted == False and self.parent.active == True:
            # Since we can only have one generation, we can just return the owner_store
            return self.parent.owner_store
        return self.owner_store

    @owner.setter
    def owner(self, value):
        self.owner_store = value

    @owner.filter
    @classmethod
    def owner(cls, lookup, value):
        print(lookup, value)
        return models.Q(**{f"owner_store__{lookup}": value}) | models.Q(**{f"parent__owner_store__{lookup}": value})

    @owner.updater
    @classmethod
    def owner(cls, value):
        return {"owner_store": value}

There's never a parent of a parent, thus the comment implying that. (Not sure if that is relevant to this issue). Parent is always either None or another Tenant.

In the teardown for unit tests I'm getting this;

Traceback (most recent call last):
  File "/usr/lib/python3.8/unittest/case.py", line 60, in testPartExecutor
    yield
  File "/usr/lib/python3.8/unittest/case.py", line 679, in run
    self._callTearDown()
  File "/usr/lib/python3.8/unittest/case.py", line 636, in _callTearDown
    self.tearDown()
  File "/var/lib/jenkins/workspace/SearchStax/ss_unit_tests/searchstax/testing/py/django/sstest.py", line 52, in tearDown
    user_models.Tenant.objects.all().delete()
  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/django/db/models/query.py", line 710, in delete
    collector.collect(del_query)
  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/django/db/models/deletion.py", line 223, in collect
    elif sub_objs:
  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/django/db/models/query.py", line 278, in __bool__
    self._fetch_all()
  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/django/db/models/query.py", line 1242, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/queryable_properties/managers.py", line 57, in __iter__
    yield self._postprocess_queryable_properties(obj)
  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/queryable_properties/managers.py", line 153, in _postprocess_queryable_properties
    delattr(obj, QUERYING_PROPERTIES_MARKER)
AttributeError: __querying_properties__

Am I doing something wrong here in the setup? Am I missing something? Or is this a bug? It seems like just checking obj for the QUERYING_PROPERTIES_MARKER attribute before calling delattr would get past this, but is this maybe a sign of another issue?

W1ldPo1nter commented 1 month ago

It seems like just checking obj for the QUERYING_PROPERTIES_MARKER attribute before calling delattr would get past this, but is this maybe a sign of another issue?

It is. The marker should always be present - if it is not, other required parts of the code did not run (meaning something else went wrong), which is why the delattr call is performed like that.

I don't see anything wrong with the model you've posted, so I'm guessing there's a bug here. Although I'm somewhat confused as there are definitely queryset deletions in django-queryable-properties' test setup. What caught my eye is this part of the stack trace:

  File "/var/lib/jenkins/envs/ss-test-p3/lib/python3.8/site-packages/django/db/models/deletion.py", line 223, in collect
    elif sub_objs:

This means that there are other objects being deleted along with the Tenant objects by cascading. Could you check which other objects are being deleted and whether their models also contain queryable properties? This could at least give me a hint about what the actual issue is.

Could you also state your Django version and django-queryable-properties version? With this information, I could try to recreate this scenario on my end.

merpius commented 1 month ago

Django==2.2.28 django-queryable-properties==1.9.3

This model is the only one using queryable properties, though there are many that have a foreign key to the Tenant model (I think most have the cascade on_delete setting). Could the parent field be the culprit here? It is referencing back to the same model (which is, of course, a queryable property model...).

W1ldPo1nter commented 3 weeks ago

So far I've been unable to recreate the issue. Using Django 2.2, I tried to delete (via cascading from a model with queryable properties):

All of them worked without encountering any problems.

I've also noticed that Django uses the base manager of each related model to query instances that should be deleted by cascading. If you set the queryable properties manager as shown above (objects = QueryablePropertiesManager()), this should only affect the default manager (attribute _default_manager), but not the base manager (attribute _base_manager) of that model. The base manager should still be one of Django's basic manager instances - which also means that there shouldn't be any queryable property code involved while deleting objects by cascading at all. This is why I'm starting to suspect that there's something else happening with the managers in your particular project.

Since I can't recreate and investigate the issue, you could: