Repository: kayak/pypika
Pull request #27 (2016-12-28 19:13:51)

Added an is_aggregate flag to Term
Created an is_aggregate flag so that any term or expression can be evaluated.  This flag is always False unless the term is composed of all aggregate functions.  This is helpful when determining whether an expression is valid or not when it is required to be aggregated (not in the group by clause).


Patch:
diff --git a/pypika/functions.py b/pypika/functions.py
index a31cab8..d327dca 100644
--- a/pypika/functions.py
+++ b/pypika/functions.py
@@ -3,7 +3,7 @@
 Package for SQL functions wrappers
 """
 from pypika.enums import SqlTypes
-from pypika.terms import Function, Star
+from pypika.terms import Function, Star, AggregateFunction
 from pypika.utils import builder
 
 __author__ = "Timothy Heys"
@@ -29,32 +29,32 @@ class Count(Function):
 
 
 # Arithmetic Functions
-class Sum(Function):
+class Sum(AggregateFunction):
     def __init__(self, term, alias=None):
         super(Sum, self).__init__('SUM', term, alias=alias)
 
 
-class Avg(Function):
+class Avg(AggregateFunction):
     def __init__(self, term, alias=None):
         super(Avg, self).__init__('AVG', term, alias=alias)
 
 
-class Min(Function):
+class Min(AggregateFunction):
     def __init__(self, term, alias=None):
         super(Min, self).__init__('MIN', term, alias=alias)
 
 
-class Max(Function):
+class Max(AggregateFunction):
     def __init__(self, term, alias=None):
         super(Max, self).__init__('MAX', term, alias=alias)
 
 
-class Std(Function):
+class Std(AggregateFunction):
     def __init__(self, term, alias=None):
         super(Std, self).__init__('STD', term, alias=alias)
 
 
-class StdDev(Function):
+class StdDev(AggregateFunction):
     def __init__(self, term, alias=None):
         super(StdDev, self).__init__('STDDEV', term, alias=alias)
 
diff --git a/pypika/terms.py b/pypika/terms.py
index 9d3cfd1..d3d7fb7 100644
--- a/pypika/terms.py
+++ b/pypika/terms.py
@@ -12,6 +12,8 @@ __email__ = "theys@kayak.com"
 
 
 class Term(object):
+    is_aggregate = False
+
     def __init__(self, alias=None):
         self.alias = alias
 
@@ -473,6 +475,10 @@ class ArithmeticExpression(Term):
         self.left = left
         self.right = right
 
+    @property
+    def is_aggregate(self):
+        return all([self.left.is_aggregate, self.right.is_aggregate])
+
     @property
     def tables_(self):
         return self.left.tables_ | self.right.tables_
@@ -512,6 +518,16 @@ class Case(Term):
         self._else = None
         self.alias = alias
 
+    @property
+    def is_aggregate(self):
+        aggregate_cases = all([term.is_aggregate
+                               for _, term in self._cases])
+
+        if self._else:
+            return aggregate_cases and self._else.is_aggregate
+
+        return aggregate_cases
+
     @builder
     def when(self, criterion, term):
         self._cases.append((criterion, self._wrap(term)))
@@ -558,10 +574,10 @@ class Case(Term):
 
 class Function(Term):
     def __init__(self, name, *params, **kwargs):
+        super(Function, self).__init__(kwargs.get('alias'))
         self.name = name
         self.params = [self._wrap(param)
                        for param in params]
-        self.alias = kwargs.get('alias')
 
     @property
     def tables_(self):
@@ -607,6 +623,10 @@ class Function(Term):
         return self
 
 
+class AggregateFunction(Function):
+    is_aggregate = True
+
+
 class Interval(object):
     units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds']
     labels = ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'MICROSECOND']


Test patch:
diff --git a/pypika/tests/test_aggregate.py b/pypika/tests/test_aggregate.py
new file mode 100644
index 0000000..ef2d48c
--- /dev/null
+++ b/pypika/tests/test_aggregate.py
@@ -0,0 +1,50 @@
+# coding: utf-8
+import unittest
+
+from pypika import Case
+from pypika import Field, functions as fn
+
+
+class IsAggregateTests(unittest.TestCase):
+    def test__field_is_not_aggregate(self):
+        v = Field('foo')
+        self.assertFalse(v.is_aggregate)
+
+    def test__field_arithmetic_is_not_aggregate(self):
+        v = Field('foo') + Field('bar')
+        self.assertFalse(v.is_aggregate)
+
+    def test__agg_func_is_aggregate(self):
+        v = fn.Sum(Field('foo'))
+        self.assertTrue(v.is_aggregate)
+
+    def test__agg_func_arithmetic_is_aggregate(self):
+        v = fn.Sum(Field('foo')) / fn.Sum(Field('foo'))
+        self.assertTrue(v.is_aggregate)
+
+    def test__mixed_func_arithmetic_is_not_aggregate(self):
+        v = Field('foo') / fn.Sum(Field('foo'))
+        self.assertFalse(v.is_aggregate)
+
+    def test__agg_case_is_aggregate(self):
+        v = Case() \
+            .when(Field('foo') == 1, fn.Sum(Field('bar'))) \
+            .when(Field('foo') == 2, fn.Sum(Field('fiz'))) \
+            .else_(fn.Sum(Field('fiz')))
+
+        self.assertTrue(v.is_aggregate)
+
+    def test__mixed_case_is_not_aggregate(self):
+        v = Case() \
+            .when(Field('foo') == 1, fn.Sum(Field('bar'))) \
+            .when(Field('foo') == 2, Field('fiz'))
+
+        self.assertFalse(v.is_aggregate)
+
+    def test__case_mixed_else_is_not_aggregate(self):
+        v = Case() \
+            .when(Field('foo') == 1, fn.Sum(Field('bar'))) \
+            .when(Field('foo') == 2, fn.Sum(Field('fiz'))) \
+            .else_(Field('fiz'))
+
+        self.assertFalse(v.is_aggregate)