The Introduction
I recently started using the SearchManager from the Mercury Tide white paper on using MySQL full-text search with Django. It's been helpful, but I ran into a bug recently while trying to add a default filter to a SearchManager subclass.
The Boring Context
Rather than deleting objects from the database, my application sets a boolean flag to indicate that the content is not longer relevant. I wanted my manager to apply a filter to every query set to include only items that are not disabled. Here's what the manager class looks like:
class SearchableItemManager(SearchMangager): def __init__(self): zuper = super(SearchableItemManager, self) zuper.__init__(('name','description',)) def get_query_set(self): query = super(SearchableItemManager, self).get_query_set() return query.filter(is_enabled=True)
The Ugly Crash
When I made the change, I found that calling the search()
method raised a TypeError: "'NoneType' object is not iterable." The error occurred when the SearchQuerySet tried to construct the SQL for the MATCH…AGAINST clause. Somehow, the _search_fields
tuple on the SearchQuerySet was None.
The Mystery Solved
This had me baffled until I had a look at the _QuerySet code in Django. It seems obvious now, but adding an additional filter to a query set returns a clone of the original with the new filter added. The _QuerySet object contains a _clone
method that copies a hard-coded list of fields from the old QS to the new one. Naturally, that hard-coded list doesn't know anything about my _search_fields
, so the property has no value on the clone.
The Fix
Now, depending on how much of a zealot you are about modifying “private” functions, there are two ways to fix this. The easiest method is to simply override the _clone
method and add the _search_fields
tuple to the clone. The alternative is to override every method that depends on the _clone method, and copy over the _search_fields
tuple for each one. I think that would be stupid, and will speak of it no further. Here's the code I added to generate happiness:
class SearchQuerySet(models.query.QuerySet): # ... code from the original Mercury Tide class def _clone(self, klass=None, **kwargs): zuper = super(SearchQuerySet, self) clone = zuper._clone(klass, **kwargs) clone._search_fields = self._search_fields return clone