--- /django/trunk/django/db/models/query.py	2007-07-27 19:38:58.000000000 -0700
+++ /django/trunk/django/db/models/query.py	2007-07-27 19:38:43.000000000 -0700
@@ -88,6 +88,7 @@
         self.model = model
         self._filters = Q()
         self._order_by = None        # Ordering, e.g. ('date', '-name'). If None, use model's ordering.
+        self._group_by = []        # Grouping, e.g. ('foreignkey').
         self._select_related = False # Whether to fill cache for related objects.
         self._max_related_depth = 0  # Maximum "depth" for select_related
         self._distinct = False       # Whether the query should use SELECT DISTINCT.
@@ -215,6 +216,8 @@
         If the queryset is already cached (i.e. self._result_cache is set) this
         simply returns the length of the cached results set to avoid multiple
         SELECT COUNT(*) calls.
+
+        If they were using GROUP_BY we have to change this to a COUNT(DISTINCT).
         """
         if self._result_cache is not None:
             return len(self._result_cache)
@@ -228,6 +231,14 @@
         counter._offset = None
         counter._limit = None
 
+        if counter._group_by:
+            id_col = ", ".join(counter.groupby2columns(self.model._meta))
+            counter._group_by = ()
+            counter._distinct = True
+        elif self._distinct:
+            id_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table),
+                backend.quote_name(self.model._meta.pk.column))
+
         try:
             select, sql, params = counter._get_sql_clause()
         except EmptyResultSet:
@@ -418,6 +429,10 @@
                 "Cannot reorder a query once a slice has been taken."
         return self._clone(_order_by=field_names)
 
+    def group_by(self, *field_names):
+        "Returns a new QuerySet instance with '_group_by' modified."
+        return self._clone(_group_by=field_names)
+
     def distinct(self, true_or_false=True):
         "Returns a new QuerySet instance with '_distinct' modified."
         return self._clone(_distinct=true_or_false)
@@ -443,6 +458,7 @@
         c.model = self.model
         c._filters = self._filters
         c._order_by = self._order_by
+        c._group_by = self._group_by
         c._select_related = self._select_related
         c._max_related_depth = self._max_related_depth
         c._distinct = self._distinct
@@ -473,6 +489,9 @@
         if (self._order_by is not None and len(self._order_by) > 0) and \
            (combined._order_by is None or len(combined._order_by) == 0):
             combined._order_by = self._order_by
+        if (self._group_by is not None and len(self._group_by) > 0) and \
+           (combined._group_by is None or len(combined._group_by) == 0):
+            combined._group_by = self._group_by
         return combined
 
     def _get_data(self):
@@ -523,6 +542,11 @@
         if where:
             sql.append(where and "WHERE " + " AND ".join(where))
 
+        # GROUP BY clause
+        group_by = self.groupby2columns(opts)
+        if group_by:
+            sql.append("GROUP BY " + ", ".join(group_by))
+
         # ORDER BY clause
         order_by = []
         if self._order_by is not None:
@@ -561,6 +585,22 @@
 
         return select, " ".join(sql), params
 
+    def groupby2columns(self, opts):
+        group_by = []
+        for col_name in self._group_by:
+            if "." in col_name:
+                table_prefix, col_name = col_name.split('.', 1)
+                table_prefix = backend.quote_name(table_prefix) + '.'
+            else:
+                # Use the database table as a column prefix if it wasn't given,
+                # and if the requested column isn't a custom SELECT.
+                if "." not in col_name and col_name not in (self._select or ()):
+                    table_prefix = backend.quote_name(opts.db_table) + '.'
+                else:
+                    table_prefix = ''
+            group_by.append('%s%s' % (table_prefix, backend.quote_name(orderfield2column(col_name, opts))))
+        return group_by
+
 # Use the backend's QuerySet class if it defines one, otherwise use _QuerySet.
 if hasattr(backend, 'get_query_set_class'):
     QuerySet = backend.get_query_set_class(_QuerySet)
