Kirill Simonov avatar Kirill Simonov committed 9f90569

Added aggregates `exists()`, `every()`, `min()`, `max()`.

Note that `exists()` and `every()` aggregates are implemented
via `BOOL_AND`/`BOOL_OR` -- will be reimplemented as correlated
subqueries.

Also added catalog caching, tuple->boolean cast, negation operator.

Comments (0)

Files changed (13)

src/htsql/application.py

         # TODO: these should be defined by the `htsql.core` addon.
         # Initialize the adapter registry.
         self.adapter_registry = AdapterRegistry(adapters)
+        self.cached_catalog = None
 
     def __enter__(self):
         """

src/htsql/domain.py

     name = 'untyped'
 
 
+class TupleDomain(Domain):
+
+    name = 'tuple'
+
+

src/htsql/tr/binder.py

 from .binding import (Binding, RootBinding, QueryBinding, SegmentBinding,
                       TableBinding, FreeTableBinding, JoinedTableBinding,
                       ColumnBinding, LiteralBinding, SieveBinding,
-                      CastBinding)
+                      CastBinding, TupleBinding)
 from .lookup import Lookup
 from .fn.function import FindFunction
 from ..introspect import Introspect
 from ..domain import (Domain, BooleanDomain, IntegerDomain, DecimalDomain,
                       FloatDomain, StringDomain, DateDomain,
-                      UntypedDomain, VoidDomain)
+                      TupleDomain, UntypedDomain, VoidDomain)
 from ..error import InvalidArgumentError
+from ..context import context
 import decimal
 
 
 
     def bind(self, syntax, parent=None):
         if parent is None:
-            introspect = Introspect()
-            catalog = introspect()
+            app = context.app
+            if app.cached_catalog is None:
+                introspect = Introspect()
+                catalog = introspect()
+                app.cached_catalog = catalog
+            catalog = app.cached_catalog
             parent = RootBinding(catalog, syntax)
         bind = Bind(syntax, self)
         return bind.bind(parent)
 
     def bind_one(self, syntax, parent=None):
         if parent is None:
-            introspect = Introspect()
-            catalog = introspect()
+            app = context.app
+            if app.cached_catalog is None:
+                introspect = Introspect()
+                catalog = introspect()
+                app.cached_catalog = catalog
+            catalog = app.cached_catalog
             parent = RootBinding(catalog, syntax)
         bind = Bind(syntax, self)
         return bind.bind_one(parent)
         return None
 
 
+class CoerceTuple(UnaryCoerce):
+
+    adapts(TupleDomain)
+
+    def __call__(self):
+        return None
+
+
 class CoerceUntyped(UnaryCoerce):
 
     adapts(UntypedDomain)
         return self.binding
 
 
+class CastTupleToBoolean(Cast):
+
+    adapts(Binding, TupleDomain, BooleanDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return TupleBinding(self.binding)
+
+
 class CastStringToString(Cast):
 
     adapts(Binding, StringDomain, StringDomain, Binder)

src/htsql/tr/binding.py

 
 
 from ..entity import CatalogEntity, TableEntity, ColumnEntity, Join
-from ..domain import Domain, VoidDomain, BooleanDomain
+from ..domain import Domain, VoidDomain, BooleanDomain, TupleDomain
 from .syntax import Syntax
 from ..util import maybe, listof, Node
 
 
     def __init__(self, parent, table, syntax):
         assert isinstance(table, TableEntity)
-        super(TableBinding, self).__init__(parent, VoidDomain(), syntax)
+        super(TableBinding, self).__init__(parent, TupleDomain(), syntax)
         self.table = table
 
 
         self.binding = binding
 
 
+class TupleBinding(Binding):
+
+    def __init__(self, binding):
+        super(TupleBinding, self).__init__(binding.parent, BooleanDomain(),
+                                           binding.syntax)
+        self.binding = binding
+
+
 class FunctionBinding(Binding):
 
     def __init__(self, parent, domain, syntax, **arguments):

src/htsql/tr/code.py

         return self.term.get_units()
 
 
+class TupleExpression(Expression):
+
+    def __init__(self, space, mark):
+        assert space.table is not None
+        assert space.table.primary_key is not None
+        super(TupleExpression, self).__init__(BooleanDomain(), mark,
+                                              hash=(self.__class__,
+                                                    space.hash))
+        self.space = space
+        columns = [space.table.columns[name]
+                   for name in space.table.primary_key.origin_column_names]
+        self.units = [ColumnUnit(column, space, mark) for column in columns]
+
+    def get_units(self):
+        return self.units
+
+
 class CastExpression(Expression):
 
     def __init__(self, code, domain, mark):

src/htsql/tr/compiler.py

 from ..adapter import Adapter, adapts, find_adapters
 from .code import (Expression, LiteralExpression, EqualityExpression,
                    InequalityExpression, ConjunctionExpression,
-                   DisjunctionExpression, NegationExpression, Unit)
+                   DisjunctionExpression, NegationExpression,
+                   TupleExpression, Unit)
 from .sketch import (Sketch, LeafSketch, ScalarSketch, BranchSketch,
                      SegmentSketch, QuerySketch, Demand,
                      LeafAppointment, BranchAppointment, FrameAppointment)
 from .frame import (LeafFrame, ScalarFrame, BranchFrame, CorrelatedFrame,
                     SegmentFrame, QueryFrame, Link, Phrase, EqualityPhrase,
                     InequalityPhrase, ConjunctionPhrase, DisjunctionPhrase,
-                    NegationPhrase, LiteralPhrase,
+                    NegationPhrase, LiteralPhrase, TuplePhrase,
                     LeafReferencePhrase, BranchReferencePhrase)
 
 
         return DisjunctionPhrase(terms, self.expression.mark)
 
 
+class EvaluateNegation(Evaluate):
+
+    adapts(NegationExpression, Compiler)
+
+    def evaluate(self, references):
+        term = self.compiler.evaluate(self.expression.term, references)
+        return NegationPhrase(term, self.expression.mark)
+
+
+class EvaluateTuple(Evaluate):
+
+    adapts(TupleExpression, Compiler)
+
+    def evaluate(self, references):
+        units = [self.compiler.evaluate(unit, references)
+                 for unit in self.expression.units]
+        return TuplePhrase(units, self.expression.mark)
+
+
 class EvaluateUnit(Evaluate):
 
     adapts(Unit, Compiler)

src/htsql/tr/encoder.py

                       ColumnBinding, LiteralBinding, SieveBinding,
                       EqualityBinding, InequalityBinding,
                       ConjunctionBinding, DisjunctionBinding,
-                      NegationBinding, CastBinding)
+                      NegationBinding, CastBinding, TupleBinding)
 from .code import (ScalarSpace, FreeTableSpace, JoinedTableSpace,
                    ScreenSpace, OrderedSpace, LiteralExpression, ColumnUnit,
-                   QueryCode, SegmentCode, ElementExpression,
+                   TupleExpression, QueryCode, SegmentCode, ElementExpression,
                    EqualityExpression, InequalityExpression,
                    ConjunctionExpression, DisjunctionExpression,
                    NegationExpression, CastExpression)
         return ScreenSpace(space, filter, self.binding.mark)
 
 
+class EncodeTuple(Encode):
+
+    adapts(TupleBinding, Encoder)
+
+    def encode(self):
+        space = self.encoder.relate(self.binding.binding)
+        return TupleExpression(space, self.binding.mark)
+
+
 class EncodeLiteral(Encode):
 
     adapts(LiteralBinding, Encoder)
     adapts(NegationBinding, Encoder)
 
     def encode(self):
-        term = self.encoder.encode(term)
+        term = self.encoder.encode(self.binding.term)
         return NegationExpression(term, self.binding.mark)
 
 

src/htsql/tr/fn/function.py

 class CastFunction(ProperFunction):
 
     parameters = [
-            Parameter('argument'),
+            Parameter('expression'),
     ]
     output_domain = None
 
-    def correlate(self, argument, syntax, parent):
-        yield self.binder.cast(argument, self.output_domain, syntax, parent)
+    def correlate(self, expression, syntax, parent):
+        yield self.binder.cast(expression, self.output_domain, syntax, parent)
 
 
 class BooleanCastFunction(CastFunction):
         yield DisjunctionBinding(parent, [left, right], syntax)
 
 
+class NegationOperator(ProperFunction):
+
+    adapts(named['!_'])
+
+    parameters = [
+            Parameter('term'),
+    ]
+
+    def correlate(self, term, syntax, parent):
+        term = self.binder.cast(term, BooleanDomain())
+        yield NegationBinding(parent, term, syntax)
+
+
 class AdditionOperator(ProperFunction):
 
     adapts(named['_+_'])
                                    self.syntax.mark)
 
 
-class ConcatenationBinding(FunctionBinding):
+class GenericBinding(FunctionBinding):
 
-    def __init__(self, parent, left, right, syntax):
-        super(ConcatenationBinding, self).__init__(parent, StringDomain(),
-                                                   syntax,
-                                                   left=left, right=right)
+    function = None
 
+    @classmethod
+    def factory(cls, function):
+        name = function.__name__ + 'Binding'
+        binding_class = type(name, (cls,), {'function': function})
+        return binding_class
 
-class ConcatenationExpression(FunctionExpression):
 
-    def __init__(self, left, right, mark):
-        super(ConcatenationExpression, self).__init__(StringDomain(), mark,
-                                                      left=left, right=right)
+class GenericExpression(FunctionExpression):
 
+    function = None
 
-class ConcatenationPhrase(FunctionPhrase):
+    @classmethod
+    def factory(cls, function):
+        name = function.__name__ + 'Expression'
+        expression_class = type(name, (cls,), {'function': function})
+        return expression_class
 
-    def __init__(self, left, right, mark):
-        super(ConcatenationPhrase, self).__init__(StringDomain(), False, mark,
-                                                  left=left, right=right)
+
+class GenericPhrase(FunctionPhrase):
+
+    function = None
+
+    @classmethod
+    def factory(cls, function):
+        name = function.__name__ + 'Phrase'
+        phrase_class = type(name, (cls,), {'function': function})
+        return phrase_class
+
+
+class GenericEncode(Encode):
+
+    adapts_none()
+
+    function = None
+    binding_class = None
+    expression_class = None
+
+    @classmethod
+    def factory(cls, function, binding_class, expression_class):
+        name = 'Encode' + function.__name__
+        signature = (binding_class, Encoder)
+        encode_class = type(name, (cls,),
+                            {'function': function,
+                             'signature': signature,
+                             'binding_class': binding_class,
+                             'expression_class': expression_class})
+        return encode_class
+
+    def encode(self):
+        arguments = {}
+        for name in sorted(self.binding.arguments):
+            value = self.binding.arguments[name]
+            if isinstance(value, list):
+                value = [self.encoder.encode(item) for item in value]
+            elif value is not None:
+                value = self.encoder.encode(value)
+            arguments[name] = value
+        return self.expression_class(self.binding.domain, self.binding.mark,
+                                     **arguments)
+
+
+class GenericAggregateEncode(Encode):
+
+    adapts_none()
+
+    function = None
+    binding_class = None
+    expression_class = None
+    wrapper_class = None
+
+    @classmethod
+    def factory(cls, function, binding_class, expression_class, wrapper_class):
+        name = 'Encode' + function.__name__
+        signature = (binding_class, Encoder)
+        encode_class = type(name, (cls,),
+                            {'function': function,
+                             'signature': signature,
+                             'binding_class': binding_class,
+                             'expression_class': expression_class,
+                             'wrapper_class': wrapper_class})
+        return encode_class
+
+    def encode(self):
+        expression = self.encoder.encode(self.binding.expression)
+        expression = self.expression_class(self.binding.domain,
+                                           self.binding.mark,
+                                           expression=expression)
+        space = self.encoder.relate(self.binding.parent)
+        plural_units = [unit for unit in expression.get_units()
+                             if not space.spans(unit.space)]
+        if not plural_units:
+            raise InvalidArgumentError("a plural expression is required",
+                                       expression.mark)
+        plural_spaces = []
+        for unit in plural_units:
+            if any(plural_space.dominates(unit.space)
+                   for plural_space in plural_spaces):
+                continue
+            plural_spaces = [plural_space
+                             for plural_space in plural_spaces
+                             if not unit.space.dominates(plural_space)]
+            plural_spaces.append(unit.space)
+        if len(plural_spaces) > 1:
+            raise InvalidArgumentError("invalid plural expression",
+                                       expression.mark)
+        plural_space = plural_spaces[0]
+        if not plural_space.spans(space):
+            raise InvalidArgumentError("invalid plural expression",
+                                       expression.mark)
+        aggregate = AggregateUnit(expression, plural_space, space,
+                                  expression.mark)
+        wrapper = self.wrapper_class(self.binding.domain, self.binding.mark,
+                                     aggregate=aggregate)
+        return wrapper
+
+
+class GenericEvaluate(Evaluate):
+
+    adapts_none()
+
+    function = None
+    expression_class = None
+    phrase_class = None
+    is_null_regular = True
+    is_nullable = True
+
+    @classmethod
+    def factory(cls, function, expression_class, phrase_class,
+                is_null_regular=True, is_nullable=True):
+        name = 'Evaluate' + function.__name__
+        signature = (expression_class, Compiler)
+        evaluate_class = type(name, (cls,),
+                              {'function': function,
+                               'signature': signature,
+                               'expression_class': expression_class,
+                               'phrase_class': phrase_class,
+                               'is_null_regular': is_null_regular,
+                               'is_nullable': is_nullable})
+        return evaluate_class
+
+    def evaluate(self, references):
+        is_nullable = self.is_nullable
+        if self.is_null_regular:
+            is_nullable = False
+        arguments = {}
+        for name in sorted(self.expression.arguments):
+            value = self.expression.arguments[name]
+            if isinstance(value, list):
+                value = [self.compiler.evaluate(item, references)
+                         for item in value]
+                if self.is_null_regular:
+                    for item in value:
+                        is_nullable = is_nullable or item.is_nullable
+            elif value is not None:
+                value = self.compiler.evaluate(value, references)
+                if self.is_null_regular:
+                    is_nullable = is_nullable or value.is_nullable
+            arguments[name] = value
+        return self.phrase_class(self.expression.domain, is_nullable,
+                                 self.expression.mark, **arguments)
+
+
+class GenericSerialize(Serialize):
+
+    adapts_none()
+
+    function = None
+    phrase_class = None
+    template = None
+
+    @classmethod
+    def factory(cls, function, phrase_class, template):
+        name = 'Serialize' + function.__name__
+        signature = (phrase_class, Serializer)
+        serialize_class = type(name, (cls,),
+                               {'function': function,
+                                'signature': signature,
+                                'phrase_class': phrase_class,
+                                'template': template})
+        return serialize_class
+
+    def serialize(self):
+        arguments = {}
+        for name in sorted(self.phrase.arguments):
+            value = self.phrase.arguments[name]
+            if isinstance(value, list):
+                value = [self.serializer.serialize(item) for item in value]
+            elif value is not None:
+                value = self.serializer.serialize(value)
+            arguments[name] = value
+        return self.template % arguments
+
+
+ConcatenationBinding = GenericBinding.factory(AdditionOperator)
+ConcatenationExpression = GenericExpression.factory(AdditionOperator)
+ConcatenationPhrase = GenericPhrase.factory(AdditionOperator)
+
+
+EncodeConcatenation = GenericEncode.factory(AdditionOperator,
+        ConcatenationBinding, ConcatenationExpression)
+EvaluateConcatenation = GenericEvaluate.factory(AdditionOperator,
+        ConcatenationExpression, ConcatenationPhrase,
+        is_null_regular=False, is_nullable=False)
+SerializeConcatenation = GenericSerialize.factory(AdditionOperator,
+        ConcatenationPhrase,
+        "(COALESCE(%(left)s, '') || COALESCE(%(right)s, ''))")
 
 
 class Concatenate(Add):
                                 parent=self.parent)
         right = self.binder.cast(self.right, StringDomain(),
                                  parent=self.parent)
-        return ConcatenationBinding(self.parent, left, right, self.syntax)
+        return ConcatenationBinding(self.parent, StringDomain(), self.syntax,
+                                    left=left, right=right)
 
 
 class ConcatenateStringToString(Concatenate):
     adapts(UntypedDomain, UntypedDomain)
 
 
-class EncodeConcatenation(Encode):
-
-    adapts(ConcatenationBinding, Encoder)
-
-    def encode(self):
-        left = self.encoder.encode(self.binding.left)
-        right = self.encoder.encode(self.binding.right)
-        return ConcatenationExpression(left, right, self.binding.mark)
-
-
-class EvaluateConcatenation(Evaluate):
-
-    adapts(ConcatenationExpression, Compiler)
-
-    def evaluate(self, references):
-        left = self.compiler.evaluate(self.expression.left, references)
-        right = self.compiler.evaluate(self.expression.right, references)
-        return ConcatenationPhrase(left, right, self.expression.mark)
-
-
 class FormatFunctions(Format):
 
     def concat_op(self, left, right):
         return "COALESCE(%s, 0)" % aggregate
 
 
-class SerializeConcatenation(Serialize):
-
-    adapts(ConcatenationPhrase, Serializer)
-
-    def serialize(self):
-        left = self.serializer.serialize(self.phrase.left)
-        right = self.serializer.serialize(self.phrase.right)
-        return self.format.concat_op(left, right)
-
-
 class CountFunction(ProperFunction):
 
     adapts(named['count'])
     ]
 
     def correlate(self, condition, syntax, parent):
-        condition = self.binder.cast(condition, BooleanDomain())
-        yield CountBinding(parent, condition, syntax)
+        expression = self.binder.cast(condition, BooleanDomain())
+        yield CountBinding(parent, IntegerDomain(), syntax,
+                           expression=expression)
 
 
-class CountBinding(FunctionBinding):
+CountBinding = GenericBinding.factory(CountFunction)
+CountExpression = GenericExpression.factory(CountFunction)
+CountWrapperExpression = GenericExpression.factory(CountFunction)
+CountPhrase = GenericPhrase.factory(CountFunction)
+CountWrapperPhrase = GenericPhrase.factory(CountFunction)
 
-    def __init__(self, parent, condition, syntax):
-        super(CountBinding, self).__init__(parent, IntegerDomain(), syntax,
-                                           condition=condition)
 
+EncodeCount = GenericAggregateEncode.factory(CountFunction,
+        CountBinding, CountExpression, CountWrapperExpression)
+EvaluateCount = GenericEvaluate.factory(CountFunction,
+        CountExpression, CountPhrase)
+EvaluateCountWrapper = GenericEvaluate.factory(CountFunction,
+        CountWrapperExpression, CountWrapperPhrase)
+SerializeCount = GenericSerialize.factory(CountFunction,
+        CountPhrase, "COUNT(NULLIF(%(expression)s, FALSE))")
+SerializeCountWrapper = GenericSerialize.factory(CountFunction,
+        CountWrapperPhrase, "COALESCE(%(aggregate)s, 0)")
 
-class CountExpression(FunctionExpression):
 
-    def __init__(self, condition, mark):
-        super(CountExpression, self).__init__(IntegerDomain(), mark,
-                                              condition=condition)
+class ExistsFunction(ProperFunction):
 
-class CountWrapperExpression(FunctionExpression):
+    adapts(named['exists'])
 
-    def __init__(self, aggregate, mark):
-        super(CountWrapperExpression, self).__init__(IntegerDomain(), mark,
-                                                     aggregate=aggregate)
+    parameters = [
+            Parameter('condition'),
+    ]
 
+    def correlate(self, condition, syntax, parent):
+        expression = self.binder.cast(condition, BooleanDomain())
+        yield ExistsBinding(parent, BooleanDomain(), syntax,
+                            expression=expression)
 
-class CountPhrase(FunctionPhrase):
 
-    def __init__(self, condition, mark):
-        super(CountPhrase, self).__init__(IntegerDomain(), True, mark,
-                                          condition=condition)
+ExistsBinding = GenericBinding.factory(ExistsFunction)
+ExistsExpression = GenericExpression.factory(ExistsFunction)
+ExistsWrapperExpression = GenericExpression.factory(ExistsFunction)
+ExistsPhrase = GenericPhrase.factory(ExistsFunction)
+ExistsWrapperPhrase = GenericPhrase.factory(ExistsFunction)
 
 
-class CountWrapperPhrase(FunctionPhrase):
+EncodeExists = GenericAggregateEncode.factory(ExistsFunction,
+        ExistsBinding, ExistsExpression, ExistsWrapperExpression)
+EvaluateExists = GenericEvaluate.factory(ExistsFunction,
+        ExistsExpression, ExistsPhrase)
+EvaluateExistsWrapper = GenericEvaluate.factory(ExistsFunction,
+        ExistsWrapperExpression, ExistsWrapperPhrase)
+SerializeExists = GenericSerialize.factory(ExistsFunction,
+        ExistsPhrase, "BOOL_OR(%(expression)s IS TRUE)")
+SerializeExistsWrapper = GenericSerialize.factory(ExistsFunction,
+        ExistsWrapperPhrase, "COALESCE(%(aggregate)s, FALSE)")
 
-    def __init__(self, aggregate, mark):
-        super(CountWrapperPhrase, self).__init__(IntegerDomain(), False, mark,
-                                                 aggregate=aggregate)
 
+class EveryFunction(ProperFunction):
 
-class EncodeCount(Encode):
+    adapts(named['every'])
 
-    adapts(CountBinding, Encoder)
+    parameters = [
+            Parameter('condition'),
+    ]
 
-    def encode(self):
-        condition = self.encoder.encode(self.binding.condition)
-        function = CountExpression(condition, self.binding.mark)
-        space = self.encoder.relate(self.binding.parent)
-        plural_units = [unit for unit in condition.get_units()
-                             if not space.spans(unit.space)]
-        if not plural_units:
-            raise InvalidArgumentError("a plural expression is required",
-                                       condition.mark)
-        plural_spaces = []
-        for unit in plural_units:
-            if any(plural_space.dominates(unit.space)
-                   for plural_space in plural_spaces):
-                continue
-            plural_spaces = [plural_space
-                             for plural_space in plural_spaces
-                             if not unit.space.dominates(plural_space)]
-            plural_spaces.append(unit.space)
-        if len(plural_spaces) > 1:
-            raise InvalidArgumentError("invalid plural expression",
-                                       condition.mark)
-        plural_space = plural_spaces[0]
-        if not plural_space.spans(space):
-            raise InvalidArgumentError("invalid plural expression",
-                                       condition.mark)
-        aggregate = AggregateUnit(function, plural_space, space, function.mark)
-        wrapper = CountWrapperExpression(aggregate, aggregate.mark)
-        return wrapper
+    def correlate(self, condition, syntax, parent):
+        expression = self.binder.cast(condition, BooleanDomain())
+        yield EveryBinding(parent, BooleanDomain(), syntax,
+                           expression=expression)
 
 
-class EvaluateCount(Evaluate):
+EveryBinding = GenericBinding.factory(EveryFunction)
+EveryExpression = GenericExpression.factory(EveryFunction)
+EveryWrapperExpression = GenericExpression.factory(EveryFunction)
+EveryPhrase = GenericPhrase.factory(EveryFunction)
+EveryWrapperPhrase = GenericPhrase.factory(EveryFunction)
 
-    adapts(CountExpression, Compiler)
 
-    def evaluate(self, references):
-        condition = self.compiler.evaluate(self.expression.condition,
-                                           references)
-        return CountPhrase(condition, self.expression.mark)
+EncodeEvery = GenericAggregateEncode.factory(EveryFunction,
+        EveryBinding, EveryExpression, EveryWrapperExpression)
+EvaluateEvery = GenericEvaluate.factory(EveryFunction,
+        EveryExpression, EveryPhrase)
+EvaluateEveryWrapper = GenericEvaluate.factory(EveryFunction,
+        EveryWrapperExpression, EveryWrapperPhrase)
+SerializeEvery = GenericSerialize.factory(EveryFunction,
+        EveryPhrase, "BOOL_AND(%(expression)s IS TRUE)")
+SerializeEveryWrapper = GenericSerialize.factory(EveryFunction,
+        EveryWrapperPhrase, "COALESCE(%(aggregate)s, TRUE)")
 
 
-class EvaluateCountWrapper(Evaluate):
+class MinFunction(ProperFunction):
 
-    adapts(CountWrapperExpression, Compiler)
+    adapts(named['min'])
 
-    def evaluate(self, references):
-        aggregate = self.compiler.evaluate(self.expression.aggregate,
-                                           references)
-        return CountWrapperPhrase(aggregate, self.expression.mark)
+    parameters = [
+            Parameter('expression'),
+    ]
 
+    def correlate(self, expression, syntax, parent):
+        Implementation = Min.realize(expression.domain)
+        function = Implementation(expression, self.binder, syntax, parent)
+        yield function()
 
-class SerializeCount(Serialize):
 
-    adapts(CountPhrase, Serializer)
+class Min(Adapter):
 
-    def serialize(self):
-        condition = self.serializer.serialize(self.phrase.condition)
-        return self.format.count_fn(condition)
+    adapts(Domain)
 
+    def __init__(self, expression, binder, syntax, parent):
+        self.expression = expression
+        self.binder = binder
+        self.syntax = syntax
+        self.parent = parent
 
-class SerializeCountWrapper(Serialize):
+    def __call__(self):
+        expression = self.expression
+        return MinBinding(self.parent, expression.domain, self.syntax,
+                          expression=expression)
 
-    adapts(CountWrapperPhrase, Serializer)
 
-    def serialize(self):
-        aggregate = self.serializer.serialize(self.phrase.aggregate)
-        return self.format.count_wrapper(aggregate)
+class MinString(Min):
+
+    adapts(StringDomain)
+
+
+class MinInteger(Min):
+
+    adapts(IntegerDomain)
+
+
+class MinDecimal(Min):
+
+    adapts(DecimalDomain)
+
+
+class MinFloat(Min):
+
+    adapts(FloatDomain)
+
+
+class MinDate(Min):
+
+    adapts(DateDomain)
+
+
+MinBinding = GenericBinding.factory(MinFunction)
+MinExpression = GenericExpression.factory(MinFunction)
+MinWrapperExpression = GenericExpression.factory(MinFunction)
+MinPhrase = GenericPhrase.factory(MinFunction)
+MinWrapperPhrase = GenericPhrase.factory(MinFunction)
+
+
+EncodeMin = GenericAggregateEncode.factory(MinFunction,
+        MinBinding, MinExpression, MinWrapperExpression)
+EvaluateMin = GenericEvaluate.factory(MinFunction,
+        MinExpression, MinPhrase)
+EvaluateMinWrapper = GenericEvaluate.factory(MinFunction,
+        MinWrapperExpression, MinWrapperPhrase)
+SerializeMin = GenericSerialize.factory(MinFunction,
+        MinPhrase, "MIN(%(expression)s)")
+SerializeMinWrapper = GenericSerialize.factory(MinFunction,
+        MinWrapperPhrase, "%(aggregate)s")
+
+
+class MaxFunction(ProperFunction):
+
+    adapts(named['max'])
+
+    parameters = [
+            Parameter('expression'),
+    ]
+
+    def correlate(self, expression, syntax, parent):
+        Implementation = Max.realize(expression.domain)
+        function = Implementation(expression, self.binder, syntax, parent)
+        yield function()
+
+
+class Max(Adapter):
+
+    adapts(Domain)
+
+    def __init__(self, expression, binder, syntax, parent):
+        self.expression = expression
+        self.binder = binder
+        self.syntax = syntax
+        self.parent = parent
+
+    def __call__(self):
+        expression = self.expression
+        return MaxBinding(self.parent, expression.domain, self.syntax,
+                          expression=expression)
+
+
+class MaxString(Max):
+
+    adapts(StringDomain)
+
+
+class MaxInteger(Max):
+
+    adapts(IntegerDomain)
+
+
+class MaxDecimal(Max):
+
+    adapts(DecimalDomain)
+
+
+class MaxFloat(Max):
+
+    adapts(FloatDomain)
+
+
+class MaxDate(Max):
+
+    adapts(DateDomain)
+
+
+MaxBinding = GenericBinding.factory(MaxFunction)
+MaxExpression = GenericExpression.factory(MaxFunction)
+MaxWrapperExpression = GenericExpression.factory(MaxFunction)
+MaxPhrase = GenericPhrase.factory(MaxFunction)
+MaxWrapperPhrase = GenericPhrase.factory(MaxFunction)
+
+
+EncodeMax = GenericAggregateEncode.factory(MaxFunction,
+        MaxBinding, MaxExpression, MaxWrapperExpression)
+EvaluateMax = GenericEvaluate.factory(MaxFunction,
+        MaxExpression, MaxPhrase)
+EvaluateMaxWrapper = GenericEvaluate.factory(MaxFunction,
+        MaxWrapperExpression, MaxWrapperPhrase)
+SerializeMax = GenericSerialize.factory(MaxFunction,
+        MaxPhrase, "MAX(%(expression)s)")
+SerializeMaxWrapper = GenericSerialize.factory(MaxFunction,
+        MaxWrapperPhrase, "%(aggregate)s")
 
 
 function_adapters = find_adapters()

src/htsql/tr/frame.py

         return self
 
 
+class TuplePhrase(Phrase):
+
+    def __init__(self, units, mark):
+        assert isinstance(units, listof(Phrase))
+        domain = BooleanDomain()
+        is_nullable = False
+        super(TuplePhrase, self).__init__(domain, is_nullable, mark)
+        self.units = units
+
+
 class CastPhrase(Phrase):
 
     def __init__(self, term, domain, is_nullable, mark):

src/htsql/tr/outliner.py

         self.unit = unit
         self.outliner = outliner
 
-    def delegate(self):
+    def delegate(self, sketch, term):
         raise NotImplementedError()
 
 

src/htsql/tr/serializer.py

                     QueryFrame, Phrase, EqualityPhrase, InequalityPhrase,
                     ConjunctionPhrase, DisjunctionPhrase, NegationPhrase,
                     LiteralPhrase, LeafReferencePhrase, BranchReferencePhrase,
-                    CorrelatedFramePhrase)
+                    CorrelatedFramePhrase, TuplePhrase)
 from .plan import Plan
 import decimal
 
             op = "!="
         return self.binary_op(left, op, right)
 
+    def is_null(self, arg):
+        return "(%s IS NULL)" % arg
+
+    def is_not_null(self, arg):
+        return "(%s IS NOT NULL)" % arg
+
     def order(self, value, dir):
         if dir == +1:
             op = "ASC"
         return self.format.not_op(value)
 
 
+class SerializeTuple(SerializePhrase):
+
+    adapts(TuplePhrase, Serializer)
+
+    def serialize(self):
+        units = [self.serializer.serialize(unit)
+                 for unit in self.phrase.units]
+        conditions = [self.format.is_not_null(unit) for unit in units]
+        return self.format.and_op(conditions)
+
+
 class SerializeLiteral(SerializePhrase):
 
     adapts(LiteralPhrase, Serializer)

test/input/pgsql.yaml

     - uri: /department{code,count(course{credits=3})}
     - uri: /school{code,count(department.course{credits=3})}
     - uri: /school{code}?count(department.course{credits=3})=20
+    - uri: /department?exists(course.credits=5)
+    - uri: /department?every(course.credits=5)
+    - uri: /department{code,min(course.credits),max(course.credits)}
+    - uri: /department?exists(course)
+    - uri: /school?!exists(department)
+    - uri: /school{*,count(department)}
+    - uri: /school{*,count(department?exists(course))}
 
   # Demonstrate selection of a formatter based on the `Accept` header.
   - title: Formatters

test/output/pgsql.yaml

 
          ----
          /{'HT'+'SQL'}
-         SELECT ('HT' || 'SQL') FROM (SELECT 1) AS "!_2"
+         SELECT (COALESCE('HT', '') || COALESCE('SQL', '')) FROM (SELECT 1) AS "!_2"
   - id: simple-filters
     tests:
     - uri: /school?code='ns'
 
          ----
          /department{school.name+' - '+name}
-         SELECT (("school"."name" || ' - ') || "department"."name") FROM "ad"."department" AS "department" INNER JOIN "ad"."school" AS "school" ON (("department"."school" = "school"."code")) ORDER BY "department"."code" ASC
+         SELECT (COALESCE((COALESCE("school"."name", '') || COALESCE(' - ', '')), '') || COALESCE("department"."name", '')) FROM "ad"."department" AS "department" INNER JOIN "ad"."school" AS "school" ON (("department"."school" = "school"."code")) ORDER BY "department"."code" ASC
   - id: aggregates
     tests:
     - uri: /department{code,count(course{credits=3})}
          ----
          /school{code}?count(department.course{credits=3})=20
          SELECT "school"."code" FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT "department"."school", COUNT(NULLIF(("course"."credits" = 3), FALSE)) AS "!" FROM "ad"."department" AS "department" INNER JOIN "ad"."course" AS "course" ON (("department"."code" = "course"."department")) GROUP BY 1) AS "course" ON (("school"."code" = "course"."school")) WHERE (COALESCE("course"."!", 0) = 20) ORDER BY 1 ASC
+    - uri: /department?exists(course.credits=5)
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | (department?exists(course.credits=5)) |
+        -+---------------------------------------+-
+         | code      | name           | school   |
+        -+-----------+----------------+----------+-
+         | acc       | Accounting     | bus      |
+         | artstd    | Studio Art     | art      |
+         | mth       | Mathematics    | ns       |
+                                          (3 rows)
+
+         ----
+         /department?exists(course.credits=5)
+         SELECT "department"."code", "department"."name", "department"."school" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", BOOL_OR(("course"."credits" = 5) IS TRUE) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course" ON (("department"."code" = "course"."department")) WHERE COALESCE("course"."!", FALSE) ORDER BY 1 ASC
+    - uri: /department?every(course.credits=5)
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | (department?every(course.credits=5)) |
+        -+--------------------------------------+-
+         | code    | name           | school    |
+        -+---------+----------------+-----------+-
+         | mth     | Mathematics    | ns        |
+         | pia     | Piano          | mus       |
+         | str     | Strings        | mus       |
+         | voc     | Vocals         | mus       |
+         | win     | Wind           | mus       |
+                                         (5 rows)
+
+         ----
+         /department?every(course.credits=5)
+         SELECT "department"."code", "department"."name", "department"."school" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", BOOL_AND(("course"."credits" = 5) IS TRUE) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course" ON (("department"."code" = "course"."department")) WHERE COALESCE("course"."!", TRUE) ORDER BY 1 ASC
+    - uri: /department{code,min(course.credits),max(course.credits)}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | department                                         |
+        -+----------------------------------------------------+-
+         | code   | min(course.credits) | max(course.credits) |
+        -+--------+---------------------+---------------------+-
+         | acc    |                   2 |                   5 |
+         | arthis |                   3 |                   4 |
+         | artstd |                   0 |                   5 |
+         | astro  |                   1 |                   3 |
+         | be     |                   3 |                   8 |
+         | capmrk |                   3 |                   3 |
+         | chem   |                   2 |                   3 |
+         | comp   |                   3 |                   4 |
+         | corpfi |                   3 |                   3 |
+         | edpol  |                   3 |                   3 |
+         | ee     |                   3 |                   4 |
+         | eng    |                   2 |                   3 |
+         | hist   |                   3 |                   3 |
+         | lang   |                   2 |                   4 |
+         | me     |                   3 |                   4 |
+         | mth    |                   5 |                   5 |
+         | phys   |                   2 |                   3 |
+         | pia    |                     |                     |
+         | poli   |                   3 |                   4 |
+         | psych  |                   3 |                   4 |
+         | str    |                     |                     |
+         | tched  |                   3 |                   4 |
+         | voc    |                     |                     |
+         | win    |                     |                     |
+                                                      (24 rows)
+
+         ----
+         /department{code,min(course.credits),max(course.credits)}
+         SELECT "department"."code", "course_1"."!", "course_2"."!" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT MIN("course"."credits") AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_1" ON (("department"."code" = "course_1"."department")) LEFT OUTER JOIN (SELECT MAX("course"."credits") AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_2" ON (("department"."code" = "course_2"."department")) ORDER BY 1 ASC
+    - uri: /department?exists(course)
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | (department?exists(course))              |
+        -+------------------------------------------+-
+         | code   | name                   | school |
+        -+--------+------------------------+--------+-
+         | acc    | Accounting             | bus    |
+         | arthis | Art History            | art    |
+         | artstd | Studio Art             | art    |
+         | astro  | Astronomy              | ns     |
+         | be     | Bioengineering         | egn    |
+         | capmrk | Capital Markets        | bus    |
+         | chem   | Chemistry              | ns     |
+         | comp   | Computer Science       | egn    |
+         | corpfi | Corporate Finance      | bus    |
+         | edpol  | Educational Policy     | edu    |
+         | ee     | Electrical Engineering | egn    |
+         | eng    | English                | la     |
+         | hist   | History                | la     |
+         | lang   | Foreign Languages      | la     |
+         | me     | Mechanical Engineering | egn    |
+         | mth    | Mathematics            | ns     |
+         | phys   | Physics                | ns     |
+         | poli   | Political Science      | la     |
+         | psych  | Psychology             | la     |
+         | tched  | Teacher Education      | edu    |
+                                            (20 rows)
+
+         ----
+         /department?exists(course)
+         SELECT "department"."code", "department"."name", "department"."school" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", BOOL_OR((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) IS TRUE) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course" ON (("department"."code" = "course"."department")) WHERE COALESCE("course"."!", FALSE) ORDER BY 1 ASC
+    - uri: /school?!exists(department)
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | (school?!exists(department))        |
+        -+-------------------------------------+-
+         | code | name                         |
+        -+------+------------------------------+-
+         | mart | School of Modern Art         |
+         | sc   | School of Continuing Studies |
+                                        (2 rows)
+
+         ----
+         /school?!exists(department)
+         SELECT "school"."code", "school"."name" FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT "department"."school", BOOL_OR((("department"."code" IS NOT NULL)) IS TRUE) AS "!" FROM "ad"."department" AS "department" GROUP BY 1) AS "department" ON (("school"."code" = "department"."school")) WHERE (NOT COALESCE("department"."!", FALSE)) ORDER BY 1 ASC
+    - uri: /school{*,count(department)}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | school                                                      |
+        -+-------------------------------------------------------------+-
+         | code | name                             | count(department) |
+        -+------+----------------------------------+-------------------+-
+         | art  | School of Art and Design         |                 2 |
+         | bus  | School of Business               |                 3 |
+         | edu  | College of Education             |                 2 |
+         | egn  | School of Engineering            |                 4 |
+         | la   | School of Arts, Letters, and the |                 5 |
+         :      : Humanities                       :                   :
+         | mart | School of Modern Art             |                 0 |
+         | mus  | Musical School                   |                 4 |
+         | ns   | School of Natural Sciences       |                 4 |
+         | sc   | School of Continuing Studies     |                 0 |
+                                                                (9 rows)
+
+         ----
+         /school{*,count(department)}
+         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF((("department"."code" IS NOT NULL)), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
+    - uri: /school{*,count(department?exists(course))}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | school                                                                     |
+        -+----------------------------------------------------------------------------+-
+         | code | name                             | count(department?exists(course)) |
+        -+------+----------------------------------+----------------------------------+-
+         | art  | School of Art and Design         |                                2 |
+         | bus  | School of Business               |                                3 |
+         | edu  | College of Education             |                                2 |
+         | egn  | School of Engineering            |                                4 |
+         | la   | School of Arts, Letters, and the |                                5 |
+         :      : Humanities                       :                                  :
+         | mart | School of Modern Art             |                                0 |
+         | mus  | Musical School                   |                                0 |
+         | ns   | School of Natural Sciences       |                                4 |
+         | sc   | School of Continuing Studies     |                                0 |
+                                                                               (9 rows)
+
+         ----
+         /school{*,count(department?exists(course))}
+         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF((("department"."code" IS NOT NULL)), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", BOOL_OR((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) IS TRUE) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course" ON (("department"."code" = "course"."department")) WHERE COALESCE("course"."!", FALSE) GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
   - id: formatters
     tests:
     - uri: /school
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.