Commits

Kirill Simonov committed b16f700

Added draft support for correlated sub-queries.
Reimplemented `exists()` and `every()` as correlated expressions.

Comments (0)

Files changed (11)

src/htsql/tr/assembler.py

 from ..adapter import Adapter, adapts, find_adapters
 from .code import (Code, Space, ScalarSpace, FreeTableSpace, JoinedTableSpace,
                    ScreenSpace, OrderedSpace, RelativeSpace, Expression, Unit,
-                   ColumnUnit, AggregateUnit, SegmentCode, QueryCode)
+                   ColumnUnit, AggregateUnit, CorrelatedUnit,
+                   SegmentCode, QueryCode)
 from .term import (TableTerm, ScalarTerm, FilterTerm, JoinTerm,
-                   CorrelationTerm, ProjectionTerm, OrderingTerm, WrapperTerm,
+                   CorrelationTerm, ProjectionTerm, OrderingTerm, HangingTerm,
                    SegmentTerm, QueryTerm, ParallelTie, SeriesTie,
                    LEFT, RIGHT, FORWARD)
 
                 left = self.assembler.inject(axes, left)
                 tie = ParallelTie(axes)
                 ties.append(tie)
+                axes = axes.parent
         else:
             space = self.space
             while not space.is_axis:
     adapts(AggregateUnit, Assembler)
 
     def inject(self, term):
+        if self.code in term.routes:
+            return term
         assert term.space.spans(self.code.space)
         assert not term.space.spans(self.code.plural_space)
         assert not self.code.space.spans(self.code.plural_space)
                         left.space, left.baseline, routes, left.mark)
 
 
+class AssembleCorrelated(AssembleUnit):
+
+    adapts(CorrelatedUnit, Assembler)
+
+    def inject(self, term):
+        if self.code in term.routes:
+            return term
+        assert term.space.spans(self.code.space)
+        assert not term.space.spans(self.code.plural_space)
+        assert not self.code.space.spans(self.code.plural_space)
+        assert self.code.plural_space.spans(self.code.space)
+        with_base_term = (not self.code.space.dominates(term.space))
+        if with_base_term:
+            base_space = self.code.space
+        else:
+            base_space = term.space
+            left = term
+        base_backbone = base_space.axes()
+        plural_space = self.code.plural_space.inflate(base_space)
+        baseline = plural_space
+        while not base_space.concludes(baseline.parent):
+            baseline = baseline.parent
+        baseline = baseline.axes()
+        plural_term = self.assembler.assemble(plural_space, baseline)
+        plural_term = self.assembler.inject(self.code.expression, plural_term)
+        routes = {}
+        for key in plural_term.routes:
+            routes[key] = [FORWARD] + plural_term.routes[key]
+        plural_term = HangingTerm(plural_term,
+                                  plural_term.space, plural_term.baseline,
+                                  routes, plural_term.mark)
+        ties = []
+        axes = []
+        if (base_backbone.concludes(baseline)
+                or baseline.parent in plural_term.routes):
+            axis = baseline
+            while axis in plural_term.routes:
+                axes.append(axis)
+                tie = ParallelTie(axis)
+                ties.append(tie)
+                axis = axis.parent
+        else:
+            axes.append(baseline)
+            tie = SeriesTie(baseline)
+            ties.append(tie)
+        if with_base_term:
+            assert False
+        if (base_backbone.concludes(baseline)
+                or baseline.parent in plural_term.routes):
+            for axis in axes:
+                term = self.assembler.inject(axis, term)
+        else:
+            for axis in axes:
+                term = self.assembler.inject(axis.parent, term)
+        left = term
+        right = plural_term
+        routes = {}
+        for key in left.routes:
+            routes[key] = [LEFT] + left.routes[key]
+        routes[self.code] = [RIGHT]
+        return CorrelationTerm(left, right, ties,
+                               left.space, left.baseline, routes, left.mark)
+
+
 class AssembleSegment(Assemble):
 
     adapts(SegmentCode, Assembler)

src/htsql/tr/code.py

     def __init__(self, value, domain, mark):
         super(LiteralExpression, self).__init__(domain, mark,
                                                 hash=(self.__class__,
-                                                      value, domain))
+                                                      value,
+                                                      domain.__class__))
         self.value = value
 
 
     def __init__(self, code, domain, mark):
         super(CastExpression, self).__init__(domain, mark,
                                              hash=(self.__class__,
-                                                   code.hash, domain))
+                                                   code.hash,
+                                                   domain.__class__))
         self.code = code
 
     def get_units(self):
         self.space = space
 
 
+class CorrelatedUnit(Unit):
+
+    def __init__(self, expression, plural_space, space, mark):
+        assert isinstance(expression, Expression)
+        assert isinstance(plural_space, Space)
+        super(CorrelatedUnit, self).__init__(expression.domain, space, mark,
+                                             hash=(self.__class__,
+                                                   expression.hash,
+                                                   plural_space.hash,
+                                                   space.hash))
+        self.expression = expression
+        self.plural_space = plural_space
+        self.space = space
+
+
 class QueryCode(Code):
 
     def __init__(self, binding, segment, mark):

src/htsql/tr/compiler.py

                     TotalInequalityPhrase, ConjunctionPhrase,
                     DisjunctionPhrase, NegationPhrase, LiteralPhrase,
                     CastPhrase, TuplePhrase,
-                    LeafReferencePhrase, BranchReferencePhrase)
+                    LeafReferencePhrase, BranchReferencePhrase,
+                    CorrelatedFramePhrase)
 
 
 class Compiler(object):
 
-    def compile(self, sketch, *args, **kwds):
-        compile = Compile(sketch, self)
+    def compile(self, sketch, attachment=None, *args, **kwds):
+        compile = Compile(sketch, self, attachment)
         return compile.compile(*args, **kwds)
 
     def evaluate(self, expression, references):
 
     adapts(Sketch, Compiler)
 
-    def __init__(self, sketch, compiler):
+    def __init__(self, sketch, compiler, attachment=None):
         self.sketch = sketch
         self.compiler = compiler
+        self.attachment = attachment
 
     def compile(self, *args, **kwds):
         raise NotImplementedError()
 
     adapts(BranchSketch, Compiler)
 
-    def compile(self, demands, BranchFrame=BranchFrame):
+    def compile(self, demands, BranchFrame=BranchFrame,
+                external_supplies=None):
         assert isinstance(demands, listof(Demand))
         assert all((demand.sketch in self.sketch.absorbed or
                     demand.sketch in self.sketch.descended)
         for appointment, dir in self.sketch.order:
             inner_demands.extend(appointment.get_demands())
 
+        if not self.sketch.is_proper:
+            for connection in self.attachment.connections:
+                for demand in connection.right.get_demands():
+                    inner_demands.append(demand)
+
         idx = 0
         while idx < len(inner_demands):
             demand = inner_demands[idx]
         for attachment in self.sketch.linkage:
             child = attachment.sketch
             child_demands = demands_by_child[child]
-            child_supplies = self.compiler.compile(child, child_demands)
+            if attachment.is_proper:
+                child_supplies = self.compiler.compile(child, attachment,
+                                                       child_demands)
+            else:
+                child_supplies = self.compiler.compile(child, attachment,
+                            child_demands, external_supplies=inner_supplies)
             inner_supplies.update(child_supplies)
 
         branch_demands = reversed(demands_by_child[None])
             phrase = self.meet(demand.appointment, inner_supplies)
             inner_supplies[demand] = phrase
 
-        select = []
-        phrase_by_expression = {}
-        for appointment in self.sketch.select:
-            if appointment.expression not in phrase_by_expression:
+        if self.sketch.is_proper:
+
+            select = []
+            phrase_by_expression = {}
+            for appointment in self.sketch.select:
+                if appointment.expression not in phrase_by_expression:
+                    phrase = self.meet(appointment, inner_supplies)
+                    phrase_by_expression[appointment.expression] = phrase
+                phrase = phrase_by_expression[appointment.expression]
+                select.append(phrase)
+            position_by_demand = {}
+            position_by_phrase = {}
+            for demand in demands:
+                if demand.appointment.is_frame:
+                    continue
+                if demand.sketch in self.sketch.absorbed:
+                    appointment = demand.appointment
+                    if appointment.expression in phrase_by_expression:
+                        phrase = phrase_by_expression[appointment.expression]
+                    else:
+                        phrase = self.meet(appointment, inner_supplies)
+                        phrase_by_expression[appointment.expression] = phrase
+                else:
+                    phrase = inner_supplies[demand]
+                if phrase not in position_by_phrase:
+                    position_by_phrase[phrase] = len(select)
+                    select.append(phrase)
+                position_by_demand[demand] = position_by_phrase[phrase]
+
+            linkage = []
+            for attachment in self.sketch.linkage:
+                if not attachment.sketch.is_proper:
+                    continue
+                link = self.link(attachment, inner_supplies)
+                linkage.append(link)
+
+            filter = None
+            conditions = []
+            for appointment in self.sketch.filter:
                 phrase = self.meet(appointment, inner_supplies)
-                phrase_by_expression[appointment.expression] = phrase
-            phrase = phrase_by_expression[appointment.expression]
-            select.append(phrase)
-        position_by_demand = {}
-        position_by_phrase = {}
-        for demand in demands:
-            if demand.appointment.is_frame:
-                continue
-            if demand.sketch in self.sketch.absorbed:
-                appointment = demand.appointment
+                conditions.append(phrase)
+            if len(conditions) == 1:
+                filter = conditions[0]
+            elif len(conditions) > 1:
+                filter = ConjunctionPhrase(conditions, self.sketch.mark)
+
+            group = []
+            for appointment in self.sketch.group:
                 if appointment.expression in phrase_by_expression:
                     phrase = phrase_by_expression[appointment.expression]
                 else:
                     phrase = self.meet(appointment, inner_supplies)
-                    phrase_by_expression[appointment.expression] = phrase
-            else:
-                phrase = inner_supplies[demand]
-            if phrase not in position_by_phrase:
-                position_by_phrase[phrase] = len(select)
-                select.append(phrase)
-            position_by_demand[demand] = position_by_phrase[phrase]
+                group.append(phrase)
 
-        linkage = []
-        for attachment in self.sketch.linkage:
-            if not attachment.sketch.is_proper:
-                continue
-            link = self.link(attachment, inner_supplies)
-            linkage.append(link)
+            group_filter = None
+            conditions = []
+            for appointment in self.sketch.group_filter:
+                phrase = self.meet(appointment, inner_supplies)
+                conditions.append(phrase)
+            if len(conditions) == 1:
+                group_filter = conditions[0]
+            elif len(conditions) > 1:
+                group_filter = ConjunctionPhrase(conditions, self.sketch.mark)
 
-        filter = None
-        conditions = []
-        for appointment in self.sketch.filter:
-            phrase = self.meet(appointment, inner_supplies)
-            conditions.append(phrase)
-        if len(conditions) == 1:
-            filter = conditions[0]
-        elif len(conditions) > 1:
-            filter = ConjunctionPhrase(conditions, self.sketch.mark)
+            order = []
+            for appointment, dir in self.sketch.order:
+                if appointment.expression in phrase_by_expression:
+                    phrase = phrase_by_expression[appointment.expression]
+                else:
+                    phrase = self.meet(appointment, inner_supplies)
+                order.append((phrase, dir))
 
-        group = []
-        for appointment in self.sketch.group:
-            if appointment.expression in phrase_by_expression:
-                phrase = phrase_by_expression[appointment.expression]
-            else:
-                phrase = self.meet(appointment, inner_supplies)
-            group.append(phrase)
+            limit = self.sketch.limit
+            offset = self.sketch.offset
 
-        group_filter = None
-        conditions = []
-        for appointment in self.sketch.group_filter:
-            phrase = self.meet(appointment, inner_supplies)
-            conditions.append(phrase)
-        if len(conditions) == 1:
-            group_filter = conditions[0]
-        elif len(conditions) > 1:
-            group_filter = ConjunctionPhrase(conditions, self.sketch.mark)
+            frame = BranchFrame(select=select,
+                                linkage=linkage,
+                                filter=filter,
+                                group=group,
+                                group_filter=group_filter,
+                                order=order,
+                                limit=limit,
+                                offset=offset,
+                                mark=self.sketch.mark)
 
-        order = []
-        for appointment, dir in self.sketch.order:
-            if appointment.expression in phrase_by_expression:
-                phrase = phrase_by_expression[appointment.expression]
-            else:
-                phrase = self.meet(appointment, inner_supplies)
-            order.append((phrase, dir))
+            supplies = {}
+            reference_by_position = {}
+            for demand in demands:
+                if demand.appointment.is_frame:
+                    supplies[demand] = frame
+                else:
+                    position = position_by_demand[demand]
+                    if position in reference_by_position:
+                        phrase = reference_by_position[position]
+                    else:
+                        mark = select[position].mark
+                        phrase = BranchReferencePhrase(frame,
+                                self.sketch.is_inner, position, mark)
+                        reference_by_position[position] = phrase
+                    supplies[demand] = phrase
 
-        limit = self.sketch.limit
-        offset = self.sketch.offset
+            return supplies
 
-        frame = BranchFrame(select=select,
-                            linkage=linkage,
-                            filter=filter,
-                            group=group,
-                            group_filter=group_filter,
-                            order=order,
-                            limit=limit,
-                            offset=offset,
-                            mark=self.sketch.mark)
+        else:
 
-        supplies = {}
-        reference_by_position = {}
-        for demand in demands:
-            if demand.appointment.is_frame:
-                supplies[demand] = frame
-            else:
-                position = position_by_demand[demand]
-                if position in reference_by_position:
-                    phrase = reference_by_position[position]
+            supplies = {}
+            for demand in demands:
+                assert demand.appointment.is_branch
+                if demand.sketch in self.sketch.absorbed:
+                    appointment = demand.appointment
+                    phrase = self.meet(appointment, inner_supplies)
                 else:
-                    mark = select[position].mark
-                    phrase = BranchReferencePhrase(frame,
-                            self.sketch.is_inner, position, mark)
-                    reference_by_position[position] = phrase
+                    phrase = inner_supplies[demand]
+                select = [phrase]
+
+                linkage = []
+                for attachment in self.sketch.linkage:
+                    if not attachment.sketch.is_proper:
+                        continue
+                    link = self.link(attachment, inner_supplies)
+                    linkage.append(link)
+
+                filter = None
+                conditions = []
+                for appointment in self.sketch.filter:
+                    phrase = self.meet(appointment, inner_supplies)
+                    conditions.append(phrase)
+                for connection in self.attachment.connections:
+                    left = self.meet(connection.left, external_supplies)
+                    right = self.meet(connection.right, inner_supplies)
+                    condition = EqualityPhrase(left, right,
+                                               self.sketch.mark)
+                    conditions.append(condition)
+                if len(conditions) == 1:
+                    filter = conditions[0]
+                elif len(conditions) > 1:
+                    filter = ConjunctionPhrase(conditions, self.sketch.mark)
+
+                assert not self.sketch.group
+                assert not self.sketch.group_filter
+
+                order = []
+                for appointment, dir in self.sketch.order:
+                    phrase = self.meet(appointment, inner_supplies)
+                    order.append((phrase, dir))
+
+                limit = self.sketch.limit
+                offset = self.sketch.offset
+
+                frame = CorrelatedFrame(select=select,
+                                        linkage=linkage,
+                                        filter=filter,
+                                        order=order,
+                                        limit=limit,
+                                        offset=offset,
+                                        mark=self.sketch.mark)
+                phrase = CorrelatedFramePhrase(frame, self.sketch.mark)
                 supplies[demand] = phrase
 
-        return supplies
+            return supplies
 
     def meet(self, appointment, inner_supplies):
         references = {}

src/htsql/tr/fn/function.py

                        TotalEqualityBinding, TotalInequalityBinding,
                        ConjunctionBinding, DisjunctionBinding, NegationBinding)
 from ..encoder import Encoder, Encode
-from ..code import FunctionExpression, AggregateUnit
+from ..code import (FunctionExpression, NegationExpression, AggregateUnit,
+                    CorrelatedUnit, LiteralExpression, ScreenSpace)
 from ..compiler import Compiler, Evaluate
 from ..frame import FunctionPhrase
 from ..serializer import Serializer, Format, Serialize
         if self.is_null_regular:
             is_nullable = False
         arguments = {}
+        children = []
         for parameter in self.function.parameters:
             value = self.expression.arguments[parameter.name]
             if not parameter.is_mandatory and value is None:
             elif parameter.is_list:
                 argument = [self.compiler.evaluate(item, references)
                             for item in value]
+                children.extend(argument)
                 is_nullable = is_nullable or any(item.is_nullable
                                                  for item in argument)
             else:
                 argument = self.compiler.evaluate(value, references)
+                children.append(argument)
                 is_nullable = is_nullable or argument.is_nullable
             arguments[parameter.name] = argument
         for name in sorted(self.expression.arguments):
             if name not in arguments:
                 arguments[name] = self.expression.arguments[name]
         return self.phrase_class(self.expression.domain, is_nullable,
-                                 self.expression.mark, **arguments)
+                                 children, self.expression.mark, **arguments)
 
 
 class GenericSerialize(Serialize):
 
 
 ExistsBinding = GenericBinding.factory(ExistsFunction)
-ExistsExpression = GenericExpression.factory(ExistsFunction)
 ExistsWrapperExpression = GenericExpression.factory(ExistsFunction)
-ExistsPhrase = GenericPhrase.factory(ExistsFunction)
 ExistsWrapperPhrase = GenericPhrase.factory(ExistsFunction)
 
 
-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(%(expression)s, FALSE)")
-
-
 class EveryFunction(ProperFunction):
 
     adapts(named['every'])
 
 
 EveryBinding = GenericBinding.factory(EveryFunction)
-EveryExpression = GenericExpression.factory(EveryFunction)
 EveryWrapperExpression = GenericExpression.factory(EveryFunction)
-EveryPhrase = GenericPhrase.factory(EveryFunction)
 EveryWrapperPhrase = GenericPhrase.factory(EveryFunction)
 
 
-EncodeEvery = GenericAggregateEncode.factory(EveryFunction,
-        EveryBinding, EveryExpression, EveryWrapperExpression)
-EvaluateEvery = GenericEvaluate.factory(EveryFunction,
-        EveryExpression, EveryPhrase)
+class EncodeExistsEvery(Encode):
+
+    adapts_none()
+    is_exists = False
+    is_every = False
+
+    def encode(self):
+        expression = self.encoder.encode(self.binding.expression)
+        if self.is_every:
+            expression = NegationExpression(expression, expression.mark)
+        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)
+        plural_space = ScreenSpace(plural_space, expression, self.binding.mark)
+        expression = LiteralExpression(True, BooleanDomain(),
+                                       self.binding.mark)
+        aggregate = CorrelatedUnit(expression, plural_space, space,
+                                   self.binding.mark)
+        if self.is_exists:
+            wrapper = ExistsWrapperExpression(self.binding.domain,
+                                              self.binding.mark,
+                                              expression=aggregate)
+        if self.is_every:
+            wrapper = EveryWrapperExpression(self.binding.domain,
+                                             self.binding.mark,
+                                             expression=aggregate)
+        return wrapper
+
+
+class EncodeExists(EncodeExistsEvery):
+
+    adapts(ExistsBinding, Encoder)
+    is_exists = True
+
+
+EvaluateExistsWrapper = GenericEvaluate.factory(ExistsFunction,
+        ExistsWrapperExpression, ExistsWrapperPhrase)
+SerializeExistsWrapper = GenericSerialize.factory(ExistsFunction,
+        ExistsWrapperPhrase, "EXISTS(%(expression)s)")
+
+
+class EncodeEvery(EncodeExistsEvery):
+
+    adapts(EveryBinding, Encoder)
+    is_every = True
+
+
 EvaluateEveryWrapper = GenericEvaluate.factory(EveryFunction,
         EveryWrapperExpression, EveryWrapperPhrase)
-SerializeEvery = GenericSerialize.factory(EveryFunction,
-        EveryPhrase, "BOOL_AND(%(expression)s IS TRUE)")
 SerializeEveryWrapper = GenericSerialize.factory(EveryFunction,
-        EveryWrapperPhrase, "COALESCE(%(expression)s, TRUE)")
+        EveryWrapperPhrase, "NOT EXISTS(%(expression)s)")
 
 
 class MinFunction(ProperFunction):

src/htsql/tr/frame.py

 
 class Phrase(Clause):
 
-    def __init__(self, domain, is_nullable, mark):
+    def __init__(self, domain, is_nullable, children, mark):
         assert isinstance(domain, Domain)
         assert isinstance(is_nullable, bool)
+        assert isinstance(children, listof(Phrase))
         assert isinstance(mark, Mark)
         self.domain = domain
         self.is_nullable = is_nullable
+        self.children = children
         self.mark = mark
 
     def optimize(self):
         assert isinstance(right, Phrase)
         domain = BooleanDomain()
         is_nullable = (left.is_nullable or right.is_nullable)
-        super(EqualityPhrase, self).__init__(domain, is_nullable, mark)
+        super(EqualityPhrase, self).__init__(domain, is_nullable,
+                                             [left, right], mark)
         self.left = left
         self.right = right
 
         assert isinstance(right, Phrase)
         domain = BooleanDomain()
         is_nullable = (left.is_nullable or right.is_nullable)
-        super(InequalityPhrase, self).__init__(domain, is_nullable, mark)
+        super(InequalityPhrase, self).__init__(domain, is_nullable,
+                                               [left, right], mark)
         self.left = left
         self.right = right
 
         assert isinstance(left, Phrase)
         assert isinstance(right, Phrase)
         domain = BooleanDomain()
-        super(TotalEqualityPhrase, self).__init__(domain, False, mark)
+        super(TotalEqualityPhrase, self).__init__(domain, False,
+                                                  [left, right], mark)
         self.left = left
         self.right = right
 
         assert isinstance(left, Phrase)
         assert isinstance(right, Phrase)
         domain = BooleanDomain()
-        super(TotalInequalityPhrase, self).__init__(domain, False, mark)
+        super(TotalInequalityPhrase, self).__init__(domain, False,
+                                                    [left, right], mark)
         self.left = left
         self.right = right
 
         assert isinstance(terms, listof(Phrase))
         domain = BooleanDomain()
         is_nullable = any(term.is_nullable for term in terms)
-        super(ConjunctionPhrase, self).__init__(domain, is_nullable, mark)
+        super(ConjunctionPhrase, self).__init__(domain, is_nullable,
+                                                terms, mark)
         self.terms = terms
 
     def optimize(self):
         assert isinstance(terms, listof(Phrase))
         domain = BooleanDomain()
         is_nullable = any(term.is_nullable for term in terms)
-        super(DisjunctionPhrase, self).__init__(domain, is_nullable, mark)
+        super(DisjunctionPhrase, self).__init__(domain, is_nullable,
+                                                terms, mark)
         self.terms = terms
 
     def optimize(self):
         assert isinstance(term, Phrase)
         domain = BooleanDomain()
         is_nullable = term.is_nullable
-        super(NegationPhrase, self).__init__(domain, is_nullable, mark)
+        super(NegationPhrase, self).__init__(domain, is_nullable,
+                                             [term], mark)
         self.term = term
 
     def optimize(self):
         assert isinstance(units, listof(Phrase))
         domain = BooleanDomain()
         is_nullable = False
-        super(TuplePhrase, self).__init__(domain, is_nullable, mark)
+        super(TuplePhrase, self).__init__(domain, is_nullable, units, mark)
         self.units = units
 
 
 
     def __init__(self, phrase, domain, is_nullable, mark):
         assert isinstance(phrase, Phrase)
-        super(CastPhrase, self).__init__(domain, is_nullable, mark)
+        super(CastPhrase, self).__init__(domain, is_nullable, [phrase], mark)
         self.phrase = phrase
 
 
 
     def __init__(self, value, domain, mark):
         is_nullable = (value is None)
-        super(LiteralPhrase, self).__init__(domain, is_nullable, mark)
+        super(LiteralPhrase, self).__init__(domain, is_nullable, [], mark)
         self.value = value
 
 
 class FunctionPhrase(Phrase):
 
-    def __init__(self, domain, is_nullable, mark, **arguments):
-        super(FunctionPhrase, self).__init__(domain, is_nullable, mark)
+    def __init__(self, domain, is_nullable, children, mark, **arguments):
+        super(FunctionPhrase, self).__init__(domain, is_nullable,
+                                             children, mark)
         self.arguments = arguments
         for key in arguments:
             setattr(self, key, arguments[key])
         assert isinstance(column, ColumnEntity)
         is_nullable = (column.is_nullable or not is_inner)
         super(LeafReferencePhrase, self).__init__(column.domain,
-                                                  is_nullable, mark)
+                                                  is_nullable, [], mark)
         self.frame = frame
         self.is_inner = is_inner
         self.column = column
         phrase = frame.select[position]
         domain = phrase.domain
         is_nullable = (phrase.is_nullable or not is_inner)
-        super(BranchReferencePhrase, self).__init__(domain, is_nullable, mark)
+        super(BranchReferencePhrase, self).__init__(domain, is_nullable,
+                                                    [], mark)
         self.frame = frame
         self.is_inner = is_inner
         self.position = position
         assert isinstance(frame, CorrelatedFrame)
         assert len(frame.select) == 1
         domain = frame.select[0].domain
-        super(CorrelatedFramePhrase, self).__init__(domain, True, mark)
+        super(CorrelatedFramePhrase, self).__init__(domain, True, [], mark)
         self.frame = frame
 
 

src/htsql/tr/outliner.py

 
 from ..adapter import Adapter, adapts, find_adapters
 from .term import (Term, TableTerm, ScalarTerm, FilterTerm, JoinTerm,
-                   CorrelationTerm, ProjectionTerm, OrderingTerm, WrapperTerm,
+                   CorrelationTerm, ProjectionTerm, OrderingTerm, HangingTerm,
                    SegmentTerm, QueryTerm, Tie, ParallelTie, SeriesTie)
-from .code import (Unit, ColumnUnit, AggregateUnit, Space, ScalarSpace,
-                   FreeTableSpace, JoinedTableSpace)
+from .code import (Unit, ColumnUnit, AggregateUnit, CorrelatedUnit,
+                   Space, ScalarSpace, FreeTableSpace, JoinedTableSpace)
 from .sketch import (Sketch, LeafSketch, ScalarSketch, BranchSketch,
                      SegmentSketch, QuerySketch, Demand, LeafAppointment,
                      BranchAppointment, Connection, Attachment)
                             mark=self.term.mark)
 
 
-class OutlineWrapper(Outline):
+class OutlineHanging(Outline):
 
-    adapts(WrapperTerm, Outliner)
+    adapts(HangingTerm, Outliner)
 
     def outline(self, is_inner=True, is_proper=True):
         child = self.outliner.outline(self.term.child)
         return Demand(sketch, appointment)
 
 
+class DelegateCorrelated(Delegate):
+
+    adapts(CorrelatedUnit, Outliner)
+
+    def delegate(self, sketch, term):
+        route = term.routes[self.unit]
+        for idx in route:
+            sketch = sketch.linkage[idx].sketch
+            term = term.children[idx]
+        appointment = self.outliner.appoint(self.unit.expression,
+                                            sketch, term)
+        return Demand(sketch, appointment)
+
+
 class Flatten(Adapter):
 
     adapts(Sketch, Outliner)
     adapts(FreeTableSpace, Outliner)
 
     def connect_parallel(self):
-        table = self.term.space.table
+        table = self.tie.space.table
         if table.primary_key is None:
             raise InvalidArgumentError()
         for name in table.primary_key.origin_column_names:
             column = table.columns[name]
-            code = ColumnUnit(column, self.term.space, self.term.mark)
+            code = ColumnUnit(column, self.tie.space, self.tie.mark)
             yield (code, code)
 
     def connect_series(self):

src/htsql/tr/serializer.py

             self.serializer.serialize_aliases(link.frame)
             collection.append(link.frame)
         self.serialize_alias_collection(collection, taken_aliases)
+        children = []
+        children.extend(self.frame.select)
+        for link in self.frame.linkage:
+            if link.condition is not None:
+                children.append(link.condition)
+        if self.frame.filter is not None:
+            children.append(self.frame.filter)
+        children.extend(self.frame.group)
+        if self.frame.group_filter is not None:
+            children.extend(self.frame.group_filter)
+        children.extend(phrase for phrase, dir in self.frame.order)
+        idx = 0
+        while idx < len(children):
+            children.extend(children[idx].children)
+            idx += 1
+        for child in children:
+            if isinstance(child, CorrelatedFramePhrase):
+                self.serializer.serialize_aliases(child.frame,
+                                                  parents+[self.frame])
 
     def serialize_alias_collection(self, collection, taken_aliases):
         clauses_by_name = {}
 
     adapts(CorrelatedFramePhrase, Serializer)
 
+    def serialize(self):
+        return self.serializer.serialize(self.phrase.frame)
+
+    def call(self):
+        return self.serializer.call(self.phrase.frame)
+
 
 serialize_adapters = find_adapters()
 

src/htsql/tr/sketch.py

         assert isinstance(sketch, Sketch)
         assert isinstance(connections, listof(Connection))
         self.sketch = sketch
+        self.is_inner = sketch.is_inner
+        self.is_proper = sketch.is_proper
         self.connections = connections
         self.demand = Demand(sketch, FrameAppointment(self.sketch.mark))
 
     def get_demands(self):
-        yield self.demand
-        for connection in self.connections:
-            for demand in connection.get_demands():
-                yield demand
+        if self.is_proper:
+            yield self.demand
+            for connection in self.connections:
+                for demand in connection.get_demands():
+                    yield demand
+        else:
+            for connection in self.connections:
+                for demand in connection.left.get_demands():
+                    yield demand
 
 

src/htsql/tr/term.py

         self.offset = offset
 
 
-class WrapperTerm(UnaryTerm):
+class HangingTerm(UnaryTerm):
     pass
 
 

src/htsql/util.py

     pattern = r'''(?x)
         ^
         (?P<engine> %(key_chars)s )
-        ://
-        (?: (?P<username> %(key_chars)s )?
-            (?: : (?P<password> %(value_chars)s )? )? @ )?
-        (?: (?P<host> %(key_chars)s )?
-            (?: : (?P<port> %(key_chars)s )? )? )?
-        /
+        :
+        (?: //
+            (?: (?P<username> %(key_chars)s )?
+                (?: : (?P<password> %(value_chars)s )? )? @ )?
+            (?: (?P<host> %(key_chars)s )?
+                (?: : (?P<port> %(key_chars)s )? )? )?
+            /
+        )?
         (?P<database> %(value_chars)s )
         (?: \?
             (?P<options>

test/output/pgsql.yaml

 
          ----
          /{count(department),count(department?exists(course))}
-         SELECT COALESCE("department_1"."!", 0), COALESCE("department_2"."!", 0) FROM (SELECT 1) AS "!" LEFT OUTER JOIN (SELECT COUNT(NULLIF((("department"."code" IS NOT NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department") AS "department_1" ON (TRUE) LEFT OUTER JOIN (SELECT COUNT(NULLIF((("department"."code" IS NOT NULL)), FALSE)) AS "!" 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)) AS "department_2" ON (TRUE)
+         SELECT COALESCE("department_1"."!", 0), COALESCE("department_2"."!", 0) FROM (SELECT 1) AS "!" LEFT OUTER JOIN (SELECT COUNT(NULLIF((("department"."code" IS NOT NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department") AS "department_1" ON (TRUE) LEFT OUTER JOIN (SELECT COUNT(NULLIF((("department"."code" IS NOT NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department" WHERE EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) AND ("department"."code" = "course"."department"))))) AS "department_2" ON (TRUE)
     - uri: /department{code,count(course{credits=3})}
       status: 200 OK
       headers:
 
          ----
          /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
+         SELECT "department"."code", "department"."name", "department"."school" FROM "ad"."department" AS "department" WHERE EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE (("course"."credits" = 5) AND ("department"."code" = "course"."department")))) ORDER BY 1 ASC
     - uri: /department?every(course.credits=5)
       status: 200 OK
       headers:
 
          ----
          /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
+         SELECT "department"."code", "department"."name", "department"."school" FROM "ad"."department" AS "department" WHERE NOT EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((NOT ("course"."credits" = 5)) AND ("department"."code" = "course"."department")))) ORDER BY 1 ASC
     - uri: /department{code,min(course.credits),max(course.credits)}
       status: 200 OK
       headers:
 
          ----
          /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
+         SELECT "department"."code", "department"."name", "department"."school" FROM "ad"."department" AS "department" WHERE EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) AND ("department"."code" = "course"."department")))) ORDER BY 1 ASC
     - uri: /school?!exists(department)
       status: 200 OK
       headers:
 
          ----
          /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
+         SELECT "school"."code", "school"."name" FROM "ad"."school" AS "school" WHERE (NOT EXISTS((SELECT TRUE FROM "ad"."department" AS "department" WHERE ((("department"."code" IS NOT NULL)) AND ("school"."code" = "department"."school"))))) ORDER BY 1 ASC
     - uri: /school{*,count(department)}
       status: 200 OK
       headers:
 
          ----
          /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
+         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" WHERE EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) AND ("department"."code" = "course"."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:
 
          ----
          /school{*,count(department.exists(course))}
-         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF(COALESCE("course"."!", FALSE), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT BOOL_OR((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) IS TRUE) AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course" ON (("department"."code" = "course"."department")) GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
+         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF(EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((("course"."department" IS NOT NULL) AND ("course"."number" IS NOT NULL)) AND ("department"."code" = "course"."department")))), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
   - id: formatters
     tests:
     - uri: /school