Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add distinct method to FakeQuerySet #188

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions modelcluster/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import datetime
import re

from django.db import NotSupportedError, connection
from django.db.models import Model, prefetch_related_objects

from modelcluster.utils import extract_field_value, get_model_field, sort_by_fields
Expand Down Expand Up @@ -516,6 +517,21 @@ def order_by(self, *fields):
clone = self.get_clone(results=self.results[:])
sort_by_fields(clone.results, fields)
return clone

def distinct(self, *fields):
if fields and connection.vendor != 'postgresql':
raise NotSupportedError("DISTINCT ON fields is not supported by this database backend")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed, given that we're bypassing the database here? I guess you're trying to keep FakeQuerySet as close as possible to the real database behaviour, but I think it's fine for FakeQuerySet to have extra functionality that the database doesn't support.


unique_results = []
if not fields:
fields = [field.name for field in self.model._meta.fields if not field.primary_key]
seen_keys = set()
for result in self.results:
key = '$$$'.join([str(extract_field_value(result, field)) for field in fields])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using a tuple here - unlike lists, they're valid to use as keys within a set:

Suggested change
key = '$$$'.join([str(extract_field_value(result, field)) for field in fields])
key = tuple(str(extract_field_value(result, field)) for field in fields)

if key not in seen_keys:
seen_keys.add(key)
unique_results.append(result)
return self.get_clone(results=unique_results)

# a standard QuerySet will store the results in _result_cache on running the query;
# this is effectively the same as self.results on a FakeQuerySet, and so we'll make
Expand Down
33 changes: 32 additions & 1 deletion tests/tests/test_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import datetime
import itertools
from unittest.mock import patch

from django.test import TestCase
from django.db import IntegrityError
from django.db import IntegrityError, NotSupportedError, connection
from django.db.models import Prefetch

from modelcluster.models import get_all_child_relations
Expand Down Expand Up @@ -796,6 +797,36 @@ def test_meta_ordering(self):
albums = [album.name for album in beatles.albums.all()]
self.assertEqual(['With The Beatles', 'Please Please Me', 'Abbey Road'], albums)

def test_distinct_with_no_fields(self):
beatles = Band(name='The Beatles', albums=[
Album(name='Please Please Me', sort_order=1),
Album(name='With The Beatles', sort_order=2),
Album(name='Abbey Road', sort_order=2),
])

albums = [album.name for album in beatles.albums.order_by('sort_order').distinct()]
self.assertEqual(['Please Please Me', 'With The Beatles', 'Abbey Road'], albums)

def test_distinct_with_fields(self):
beatles = Band(name='The Beatles', albums=[
Album(name='Please Please Me', sort_order=1),
Album(name='With The Beatles', sort_order=2),
Album(name='Abbey Road', sort_order=2),
])

for vendor in ['sqlite', 'mysql', 'oracle']:
with patch.object(connection, 'vendor', vendor):
with self.assertRaises(NotSupportedError):
beatles.albums.order_by('sort_order').distinct('sort_order')

# patch db.connection.vendor to pass the vendor check
with patch.object(connection, 'vendor', 'postgresql'):
albums = [album.name for album in beatles.albums.order_by('sort_order').distinct('sort_order')]
self.assertEqual(['Please Please Me', 'With The Beatles'], albums)

albums = [album.name for album in beatles.albums.order_by('sort_order').distinct('name')]
self.assertEqual(['Please Please Me', 'With The Beatles', 'Abbey Road'], albums)

def test_parental_key_checks_clusterable_model(self):
from django.core import checks
from django.db import models
Expand Down