Commits

Kirill Simonov committed 374ab1e

Functions: in progress.

Adding HTSQL functions and operators. Added infrastructure for
type casting and coercion. Lots of other minor fixes.

Comments (0)

Files changed (19)

src/htsql/adapter.py

 
         # Find implementations for the given interface among active adapters.
         implementations = [adapter for adapter in self.adapters
-                                   if issubclass(adapter, interface)]
+                                   if issubclass(adapter, interface)
+                                   and adapter.signature is not None]
 
         # Sanity check on the implementations.
         for adapter in implementations:
     frame.f_locals['signature'] = types
 
 
+def adapts_none():
+    """
+    Indicates that the adapter has no signature.
+
+    Use it in the namespace of the adapter, for example::
+
+        class DoSmth(Adapter):
+
+            adapts_none()
+    """
+    frame = sys._getframe(1)
+    frame.f_locals['signature'] = None
+
+
 def dominates(*adapters):
     """
     Specifies the implementations dominated by the adapter.

src/htsql/domain.py

     name = 'untyped'
 
 
-class UntypedNumberDomain(UntypedDomain):
-    pass
-
-
-class UntypedStringDomain(UntypedDomain):
-    pass
-
-

src/htsql/entity.py

         assert isinstance(foreign_key, ForeignKeyEntity)
         super(ReverseJoin, self).__init__(origin, target)
         self.foreign_key = foreign_key
-        self.origin_columns = [target.columns[name]
+        self.origin_columns = [origin.columns[name]
                                for name in foreign_key.target_column_names]
-        self.target_columns = [origin.columns[name]
+        self.target_columns = [target.columns[name]
                                for name in foreign_key.origin_column_names]
         self.is_expanding = False
         self.is_contracting = False

src/htsql/export.py

 from tr.outliner import outline_adapters
 from tr.compiler import compile_adapters
 from tr.serializer import serialize_adapters
+from tr.fn.function import function_adapters
 from fmt import fmt_adapters
 from fmt.format import format_adapters
 from fmt.json import json_adapters
                 outline_adapters +
                 compile_adapters +
                 serialize_adapters +
+                function_adapters +
                 request_adapters +
                 fmt_adapters +
                 format_adapters +

src/htsql/split_sql.py

             # None of the tokens matched.
             else:
                 # Determine the current position and complain.
-                line = sql[:start].count('\n')
+                line = input[:start].count('\n')
                 if line:
-                    column = start-sql[:start].rindex('\n')-1
+                    column = start-input[:start].rindex('\n')-1
                 else:
                     column = start
                 raise ValueError("unable to parse an SQL statement"

src/htsql/tr/assembler.py

             return term.clone(routes=routes)
         backbone = self.space.axes()
         term_backbone = term.space.axes()
-        if term_backbone.conclude(self.space):
+        if term_backbone.concludes(self.space):
             axis = term_backbone
             while axis.parent != self.space:
                 axis = axis.parent
     def assemble(self, baseline):
         child = self.assembler.assemble(self.space.parent, baseline)
         child = self.assembler.inject(self.space.filter, child)
-        assert self.space not in term.routes
+        assert self.space not in child.routes
         routes = {}
         for key in child.routes:
             routes[key] = [FORWARD] + child.routes[key]

src/htsql/tr/binder.py

                      NumberSyntax)
 from .binding import (Binding, RootBinding, QueryBinding, SegmentBinding,
                       TableBinding, FreeTableBinding, JoinedTableBinding,
-                      ColumnBinding, LiteralBinding, SieveBinding)
+                      ColumnBinding, LiteralBinding, SieveBinding,
+                      CastBinding)
 from .lookup import Lookup
+from .fn.function import FindFunction
 from ..introspect import Introspect
-from ..domain import UntypedStringDomain, UntypedNumberDomain
+from ..domain import (Domain, BooleanDomain, IntegerDomain, DecimalDomain,
+                      FloatDomain, StringDomain, DateDomain,
+                      UntypedDomain, VoidDomain)
 from ..error import InvalidArgumentError
+import decimal
 
 
 class Binder(object):
         bind = Bind(syntax, self)
         return bind.bind_one(parent)
 
+    def find_function(self, name):
+        find_function = FindFunction()
+        return find_function(name, self)
+
+    def cast(self, binding, domain, syntax=None, parent=None):
+        if syntax is None:
+            syntax = binding.syntax
+        if parent is None:
+            parent = binding.parent
+        cast = Cast(binding, binding.domain, domain, self)
+        return cast.cast(syntax, parent)
+
+    def coerce(self, left_domain, right_domain=None):
+        if right_domain is not None:
+            coerce = BinaryCoerce(left_domain, right_domain)
+            domain = coerce()
+            if domain is None:
+                coerce = BinaryCoerce(right_domain, left_domain)
+                domain = coerce()
+            return domain
+        else:
+            coerce = UnaryCoerce(left_domain)
+            return coerce()
+
+
+class UnaryCoerce(Adapter):
+
+    adapts(Domain)
+
+    def __init__(self, domain):
+        self.domain = domain
+
+    def __call__(self):
+        return self.domain
+
+
+class CoerceVoid(UnaryCoerce):
+
+    adapts(VoidDomain)
+
+    def __call__(self):
+        return None
+
+
+class CoerceUntyped(UnaryCoerce):
+
+    adapts(UntypedDomain)
+
+    def __call__(self):
+        return StringDomain()
+
+
+class BinaryCoerce(Adapter):
+
+    adapts(Domain, Domain)
+
+    def __init__(self, left_domain, right_domain):
+        self.left_domain = left_domain
+        self.right_domain = right_domain
+
+    def __call__(self):
+        return None
+
+
+class BinaryCoerceUntyped(BinaryCoerce):
+
+    adapts(Domain, UntypedDomain)
+
+    def __call__(self):
+        return self.left_domain
+
+
+class BinaryCoerceBoolean(BinaryCoerce):
+
+    adapts(BooleanDomain, BooleanDomain)
+
+    def __call__(self):
+        return BooleanDomain()
+
+
+class BinaryCoerceInteger(BinaryCoerce):
+
+    adapts(IntegerDomain, IntegerDomain)
+
+    def __call__(self):
+        return IntegerDomain()
+
+
+class BinaryCoerceFloat(BinaryCoerce):
+
+    adapts(FloatDomain, FloatDomain)
+
+    def __call__(self):
+        return FloatDomain()
+
+
+class BinaryCoerceDecimal(BinaryCoerce):
+
+    adapts(DecimalDomain, DecimalDomain)
+
+    def __call__(self):
+        return DecimalDomain()
+
+
+class BinaryCoerceString(BinaryCoerce):
+
+    adapts(StringDomain, StringDomain)
+
+    def __call__(self):
+        return StringDomain()
+
+
+class BinaryCoerceDate(BinaryCoerce):
+
+    adapts(DateDomain, DateDomain)
+
+    def __call__(self):
+        return DateDomain()
+
+
+class CoerceIntegerToDecimal(BinaryCoerce):
+
+    adapts(IntegerDomain, DecimalDomain)
+
+    def __call__(self):
+        return DecimalDomain()
+
+
+class CoerceIntegerToFloat(BinaryCoerce):
+
+    adapts(IntegerDomain, FloatDomain)
+
+    def __call__(self):
+        return FloatDomain()
+
+
+class CoerceDecimalToFloat(BinaryCoerce):
+
+    adapts(DecimalDomain, FloatDomain)
+
+    def __call__(self):
+        return FloatDomain()
+
+
+class Cast(Adapter):
+
+    adapts(Binding, Domain, Domain, Binder)
+
+    def __init__(self, binding, from_domain, to_domain, binder):
+        self.binding = binding
+        self.from_domain = from_domain
+        self.to_domain = to_domain
+        self.binder = binder
+
+    def cast(self, syntax, parent):
+        return CastBinding(parent, self.binding, self.to_domain, syntax)
+
+
+class CastBooleanToBoolean(Cast):
+
+    adapts(Binding, BooleanDomain, BooleanDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return self.binding
+
+
+class CastStringToString(Cast):
+
+    adapts(Binding, StringDomain, StringDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return self.binding
+
+
+class CastIntegerToInteger(Cast):
+
+    adapts(Binding, IntegerDomain, IntegerDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return self.binding
+
+
+class CastDecimalToDecimal(Cast):
+
+    adapts(Binding, DecimalDomain, DecimalDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return self.binding
+
+
+class CastFloatToFloat(Cast):
+
+    adapts(Binding, FloatDomain, FloatDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return self.binding
+
+
+class CastDateToDate(Cast):
+
+    adapts(Binding, DateDomain, DateDomain, Binder)
+
+    def cast(self, syntax, parent):
+        return self.binding
+
+
+class CastLiteral(Cast):
+
+    adapts(LiteralBinding, UntypedDomain, Domain, Binder)
+
+    def cast(self, syntax, parent):
+        try:
+            value = self.to_domain.parse(self.binding.value)
+        except ValueError, exc:
+            raise InvalidArgumentError("cannot cast a value: %s" % exc,
+                                       syntax.mark)
+        return LiteralBinding(parent, value, self.to_domain, syntax)
+
 
 class Bind(Adapter):
 
             base_syntax = GroupSyntax(base_syntax, base.mark)
             base = SieveBinding(base, filter, base_syntax)
         if self.syntax.selector is not None:
-            elements = list(self.binder.bind(self.syntax.selector, base))
+            bare_elements = list(self.binder.bind(self.syntax.selector, base))
         else:
             lookup = Lookup(base)
-            elements = list(lookup.enumerate(base.syntax))
+            bare_elements = list(lookup.enumerate(base.syntax))
+        elements = []
+        for element in bare_elements:
+            domain = self.binder.coerce(element.domain)
+            if domain is None:
+                raise InvalidArgumentError("invalid type", element.mark)
+            if domain is not element.domain:
+                element = self.binder.cast(element, domain)
+            elements.append(element)
         yield SegmentBinding(parent, base, elements, self.syntax)
 
 
             yield base
 
 
+class BindOperator(Bind):
+
+    adapts(OperatorSyntax, Binder)
+
+    def bind(self, parent):
+        name = self.syntax.symbol
+        if self.syntax.left is not None:
+            name = '_'+name
+        if self.syntax.right is not None:
+            name = name+'_'
+        function = self.binder.find_function(name)
+        return function.bind_operator(self.syntax, parent)
+
+
+class BindFunctionOperator(Bind):
+
+    adapts(FunctionOperatorSyntax, Binder)
+
+    def bind(self, parent):
+        name = self.syntax.identifier.value
+        function = self.binder.find_function(name)
+        return function.bind_function_operator(self.syntax, parent)
+
+
+class BindFunctionCall(Bind):
+
+    adapts(FunctionCallSyntax, Binder)
+
+    def bind(self, parent):
+        if self.syntax.base is not None:
+            parent = self.binder.bind_one(self.syntax.base)
+        name = self.syntax.identifier.value
+        function = self.binder.find_function(name)
+        return function.bind_function_call(self.syntax, parent)
+
+
 class BindGroup(Bind):
 
     adapts(GroupSyntax, Binder)
 
     def bind(self, parent):
         binding = LiteralBinding(parent, self.syntax.value,
-                                 UntypedStringDomain(),
+                                 UntypedDomain(),
                                  self.syntax)
         yield binding
 
     adapts(NumberSyntax, Binder)
 
     def bind(self, parent):
-        binding = LiteralBinding(parent, self.syntax.value,
-                                 UntypedNumberDomain(),
-                                 self.syntax)
+        value = self.syntax.value
+        if 'e' in value or 'E' in value:
+            domain = FloatDomain()
+            value = float(value)
+        elif '.' in value:
+            domain = DecimalDomain()
+            value = decimal.Decimal(value)
+        else:
+            domain = IntegerDomain()
+            value = int(value)
+        binding = LiteralBinding(parent, value, domain, self.syntax)
         yield binding
 
 

src/htsql/tr/binding.py

 
 
 from ..entity import CatalogEntity, TableEntity, ColumnEntity, Join
-from ..domain import Domain, VoidDomain
+from ..domain import Domain, VoidDomain, BooleanDomain
 from .syntax import Syntax
 from ..util import maybe, listof, Node
 
         self.filter = filter
 
 
+class EqualityBinding(Binding):
+
+    def __init__(self, parent, left, right, syntax):
+        assert isinstance(left, Binding)
+        assert isinstance(right, Binding)
+        domain = BooleanDomain()
+        super(EqualityBinding, self).__init__(parent, domain, syntax)
+        self.left = left
+        self.right = right
+
+
+class InequalityBinding(Binding):
+
+    def __init__(self, parent, left, right, syntax):
+        assert isinstance(left, Binding)
+        assert isinstance(right, Binding)
+        domain = BooleanDomain()
+        super(InequalityBinding, self).__init__(parent, domain, syntax)
+        self.left = left
+        self.right = right
+
+
+class ConjunctionBinding(Binding):
+
+    def __init__(self, parent, terms, syntax):
+        assert isinstance(terms, listof(Binding))
+        domain = BooleanDomain()
+        super(ConjunctionBinding, self).__init__(parent, domain, syntax)
+        self.terms = terms
+
+
+class DisjunctionBinding(Binding):
+
+    def __init__(self, parent, terms, syntax):
+        assert isinstance(terms, listof(Binding))
+        domain = BooleanDomain()
+        super(DisjunctionBinding, self).__init__(parent, domain, syntax)
+        self.terms = terms
+
+
+class NegationBinding(Binding):
+
+    def __init__(self, parent, term, syntax):
+        assert isinstance(term, Binding)
+        domain = BooleanDomain()
+        super(NegationBinding, self).__init__(parent, domain, syntax)
+        self.term = term
+
+
+class CastBinding(Binding):
+
+    def __init__(self, parent, binding, domain, syntax):
+        super(CastBinding, self).__init__(parent, domain, syntax)
+        self.binding = binding
+
+
+class FunctionBinding(Binding):
+
+    def __init__(self, parent, domain, syntax, **arguments):
+        super(FunctionBinding, self).__init__(parent, domain, syntax)
+        self.arguments = arguments
+        for key in arguments:
+            setattr(self, key, arguments[key])
+
+

src/htsql/tr/code.py

                 space = component.clone(parent=space)
         return space
 
+    def inflate(self, other):
+        self_components = self.unfold()
+        other_components = other.unfold()
+        space = None
+        while self_components and other_components:
+            self_component = self_components[-1]
+            other_component = other_components[-1]
+            if self_component.resembles(other_component):
+                if self_component.is_axis:
+                    space = self_component.clone(parent=space)
+                self_components.pop()
+                other_components.pop()
+            elif not other_component.is_axis:
+                other_components.pop()
+            elif not self_component.is_axis:
+                space = self_component.clone(parent=space)
+                self_components.pop()
+            else:
+                break
+        while self_components:
+            component = self_components.pop()
+            space = component.clone(parent=space)
+        return space
+
+    def spans(self, other):
+        if self is other or self == other:
+            return True
+        self_axes = self.axes().unfold()
+        other_axes = other.axes().unfold()
+        while self_axes and other_axes:
+            if self_axes[-1].resembles(other_axes[-1]):
+                self_axes.pop()
+                other_axes.pop()
+            else:
+                break
+        for other_axis in other_axes:
+            if not other_axis.is_contracting:
+                return False
+        return True
+
+    def conforms(self, other):
+        if self is other or self == other:
+            return True
+        self_components = self.unfold()
+        other_components = other.unfold()
+        while self_components and other_components:
+            self_component = self_components[-1]
+            other_component = other_components[-1]
+            if self_component.resembles(other_component):
+                self_components.pop()
+                other_components.pop()
+            elif (self_component.is_contracting and
+                  self_component.is_expanding and
+                  not self_component.is_axis):
+                self_components.pop()
+            elif (other_component.is_contrating and
+                  other_component.is_expanding and
+                  not other_component.is_axis):
+                other_components.pop()
+            else:
+                break
+        for component in self_components + other_components:
+            if not (component.is_contracting and component.is_expanding):
+                return False
+        return True
+
+    def dominates(self, other):
+        if self is other or self == other:
+            return True
+        self_components = self.unfold()
+        other_components = other.unfold()
+        while self_components and other_components:
+            self_component = self_components[-1]
+            other_component = other_components[-1]
+            if self_component.resembles(other_component):
+                self_components.pop()
+                other_components.pop()
+            elif (other_component.is_contrating and
+                  not other_component.is_axis):
+                other_components.pop()
+            else:
+                break
+        for component in self_components:
+            if not component.is_expanding:
+                return False
+        for component in other_components:
+            if not component.is_contracting:
+                return False
+        return True
+
+    def concludes(self, other):
+        if self is other or self == other:
+            return True
+        space = self
+        while space is not None:
+            if space == other:
+                return True
+            space = space.parent
+        return False
+
+    def resembles(self, other):
+        return False
+
 
 class ScalarSpace(Space):
 
         super(ScalarSpace, self).__init__(None, None, False, False, mark,
                                           hash=(self.__class__))
 
+    def resembles(self, other):
+        return isinstance(other, ScalarSpace)
+
 
 class FreeTableSpace(Space):
 
                                              hash=(self.__class__,
                                                    parent.hash, table))
 
+    def resembles(self, other):
+        return isinstance(other, FreeTableSpace)
+
 
 class JoinedTableSpace(Space):
 
                                                      join.foreign_key))
         self.join = join
 
+    def resembles(self, other):
+        return (isinstance(other, JoinedTableSpace) and
+                self.join is other.join)
+
 
 class ScreenSpace(Space):
 
                                                 filter.hash))
         self.filter = filter
 
+    def resembles(self, other):
+        return (isinstance(other, ScreenSpace) and
+                self.filter == other.filter)
+
 
 class OrderedSpace(Space):
 
         self.limit = limit
         self.offset = offset
 
+    def resembles(self, other):
+        return (isinstance(other, OrderedSpace) and
+                self.other == other.order and
+                self.limit == other.limit and
+                self.offset == other.offset)
+
 
 class Expression(Code):
 
         self.value = value
 
 
-class ElementExpression(Expression):
+class EqualityExpression(Expression):
 
-    def __init__(self, code, order):
-        assert isinstance(code, Code)
-        assert order is None or order in [+1, -1]
-        super(ElementExpression, self).__init__(code.domain, code.mark)
+    def __init__(self, left, right, mark):
+        assert isinstance(left, Expression)
+        assert isinstance(right, Expression)
+        domain = BooleanDomain()
+        super(EqualityExpression, self).__init__(domain, mark,
+                                                 hash=(self.__class__,
+                                                       left.hash,
+                                                       right.hash))
+        self.left = left
+        self.right = right
+
+    def get_units(self):
+        return self.left.get_units()+self.right.get_units()
+
+
+class InequalityExpression(Expression):
+
+    def __init__(self, left, right, mark):
+        assert isinstance(left, Expression)
+        assert isinstance(right, Expression)
+        domain = BooleanDomain()
+        super(InequalityExpression, self).__init__(domain, mark,
+                                                   hash=(self.__class__,
+                                                         left.hash,
+                                                         right.hash))
+        self.left = left
+        self.right = right
+
+    def get_units(self):
+        return self.left.get_units()+self.right.get_units()
+
+
+class ConjunctionExpression(Expression):
+
+    def __init__(self, terms, mark):
+        assert isinstance(terms, listof(Expression))
+        domain = BooleanDomain()
+        hash = (self.__class__, tuple(term.hash for term in terms))
+        super(ConjunctionExpression, self).__init__(domain, mark, hash=hash)
+        self.terms = terms
+
+    def get_units(self):
+        units = []
+        for term in self.terms:
+            units.extend(term.get_units())
+        return units
+
+
+class DisjunctionExpression(Expression):
+
+    def __init__(self, terms, mark):
+        assert isinstance(terms, listof(Expression))
+        domain = BooleanDomain()
+        hash = (self.__class__, tuple(term.hash for term in terms))
+        super(DisjunctionExpression, self).__init__(domain, mark, hash=hash)
+        self.terms = terms
+
+    def get_units(self):
+        units = []
+        for term in self.terms:
+            units.extend(term.get_units())
+        return units
+
+
+class NegationExpression(Expression):
+
+    def __init__(self, term, mark):
+        assert isinstance(term, Expression)
+        domain = BooleanDomain()
+        hash = (self.__class__, term.hash)
+        super(NegationExpression, self).__init__(domain, mark, hash=hash)
+        self.term = term
+
+    def get_units(self):
+        return self.term.get_units()
+
+
+class CastExpression(Expression):
+
+    def __init__(self, code, domain, mark):
+        super(CastExpression, self).__init__(domain, mark,
+                                             hash=(self.__class__, domain))
         self.code = code
-        self.order = order
 
     def get_units(self):
         return self.code.get_units()
 
 
+class ElementExpression(Expression):
+
+    def __init__(self, code):
+        assert isinstance(code, Code)
+        super(ElementExpression, self).__init__(code.domain, code.mark)
+        self.code = code
+
+    def get_units(self):
+        return self.code.get_units()
+
+
+class FunctionExpression(Expression):
+
+    def __init__(self, domain, mark, **arguments):
+        arguments_hash = []
+        for key in sorted(arguments):
+            value = arguments[key]
+            if isinstance(value, list):
+                value = tuple(item.hash for item in value)
+            elif value is not None:
+                value = value.hash
+            arguments_hash.append((key, value))
+        hash = (self.__class__, tuple(arguments_hash))
+        super(FunctionExpression, self).__init__(domain, mark, hash=hash)
+
+
 class Unit(Expression):
 
     def __init__(self, domain, space, mark, hash=None):

src/htsql/tr/compiler.py

 
 from ..util import listof
 from ..adapter import Adapter, adapts, find_adapters
-from .code import Expression, LiteralExpression, Unit
+from .code import (Expression, LiteralExpression, EqualityExpression,
+                   InequalityExpression, ConjunctionExpression,
+                   DisjunctionExpression, NegationExpression, 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,
-                    ConjunctionPhrase, LiteralPhrase,
+                    InequalityPhrase, ConjunctionPhrase, DisjunctionPhrase,
+                    NegationPhrase, LiteralPhrase,
                     LeafReferencePhrase, BranchReferencePhrase)
 
 
     def compile(self, demands, BranchFrame=BranchFrame):
         assert isinstance(demands, listof(Demand))
         assert all((demand.sketch in self.sketch.absorbed or
-                    demand.sketch in self.sketch.descended) and
-                   isinstance(demand.appointment, (BranchAppointment,
-                                                   FrameAppointment))
+                    demand.sketch in self.sketch.descended)
                    for demand in demands)
 
         child_by_sketch = {}
         for attachment in self.sketch.linkage:
             inner_demands.extend(attachment.get_demands())
         for appointment in self.sketch.filter:
-            inner_demands.exend(appointment.get_demands())
+            inner_demands.extend(appointment.get_demands())
         for appointment in self.sketch.group:
             inner_demands.extend(appointment.get_demands())
         for appointment in self.sketch.group_filter:
             child = attachment.sketch
             child_demands = demands_by_child[child]
             child_supplies = self.compiler.compile(child, child_demands)
+            assert len(child_demands) == len(child_supplies), (child_demands, child_supplies)
             inner_supplies.update(child_supplies)
 
         branch_demands = reversed(demands_by_child[None])
         for demand in demands:
             if demand.appointment.is_frame:
                 supplies[demand] = frame
-            if demand.appointment.is_branch:
+            else:
                 position = position_by_demand[demand]
                 if position in reference_by_position:
-                    phrase = reference_by_position
+                    phrase = reference_by_position[position]
                 else:
                     mark = select[position].mark
                     phrase = BranchReferencePhrase(frame,
                              self.expression.mark)
 
 
+class EvaluateEquality(Evaluate):
+
+    adapts(EqualityExpression, Compiler)
+
+    def evaluate(self, references):
+        left = self.compiler.evaluate(self.expression.left, references)
+        right = self.compiler.evaluate(self.expression.right, references)
+        return EqualityPhrase(left, right, self.expression.mark)
+
+
+class EvaluateInequality(Evaluate):
+
+    adapts(InequalityExpression, Compiler)
+
+    def evaluate(self, references):
+        left = self.compiler.evaluate(self.expression.left, references)
+        right = self.compiler.evaluate(self.expression.right, references)
+        return InequalityPhrase(left, right, self.expression.mark)
+
+
+class EvaluateConjunction(Evaluate):
+
+    adapts(ConjunctionExpression, Compiler)
+
+    def evaluate(self, references):
+        terms = [self.compiler.evaluate(term, references)
+                 for term in self.expression.terms]
+        return ConjunctionPhrase(terms, self.expression.mark)
+
+
+class EvaluateDisjunction(Evaluate):
+
+    adapts(DisjunctionExpression, Compiler)
+
+    def evaluate(self, references):
+        terms = [self.compiler.evaluate(term, references)
+                 for term in self.expression.terms]
+        return DisjunctionPhrase(terms, self.expression.mark)
+
+
 class EvaluateUnit(Evaluate):
 
     adapts(Unit, Compiler)

src/htsql/tr/encoder.py

 from ..adapter import Adapter, adapts, find_adapters
 from .binding import (Binding, RootBinding, QueryBinding, SegmentBinding,
                       TableBinding, FreeTableBinding, JoinedTableBinding,
-                      ColumnBinding, LiteralBinding, SieveBinding)
+                      ColumnBinding, LiteralBinding, SieveBinding,
+                      EqualityBinding, InequalityBinding,
+                      ConjunctionBinding, DisjunctionBinding,
+                      NegationBinding, CastBinding)
 from .code import (ScalarSpace, FreeTableSpace, JoinedTableSpace,
                    ScreenSpace, OrderedSpace, LiteralExpression, ColumnUnit,
-                   QueryCode, SegmentCode, ElementExpression)
+                   QueryCode, SegmentCode, ElementExpression,
+                   EqualityExpression, InequalityExpression,
+                   ConjunctionExpression, DisjunctionExpression,
+                   NegationExpression, CastExpression)
+from .lookup import Lookup
 from ..error import InvalidArgumentError
 
 
 
     def encode_element(self):
         code = self.encode()
-        return ElementExpression(code, None)
+        return ElementExpression(code)
 
     def relate(self):
         raise InvalidArgumentError("unable to relate a node",
         for binding in self.binding.elements:
             element = self.encoder.encode_element(binding)
             elements.append(element.code)
-            if element.order is not None and element.code not in order_set:
-                order.append((element.code, element.order))
-                order_set.add(element.code)
         if space.table is not None and space.table.primary_key is not None:
             for column_name in space.table.primary_key.origin_column_names:
                 column = space.table.columns[column_name]
 
     def relate(self):
         space = self.encoder.relate(self.binding.parent)
-        binding = self.binding.as_table()
+        lookup = Lookup(self.binding)
+        binding = lookup.as_table()
         for join in binding.joins:
             space = JoinedTableSpace(space, join, binding.mark)
         return space
         return ScreenSpace(space, filter, self.binding.mark)
 
 
+class EncodeLiteral(Encode):
+
+    adapts(LiteralBinding, Encoder)
+
+    def encode(self):
+        return LiteralExpression(self.binding.value, self.binding.domain,
+                                 self.binding.mark)
+
+
+class EncodeEquality(Encode):
+
+    adapts(EqualityBinding, Encoder)
+
+    def encode(self):
+        left = self.encoder.encode(self.binding.left)
+        right = self.encoder.encode(self.binding.right)
+        return EqualityExpression(left, right, self.binding.mark)
+
+
+class EncodeInequality(Encode):
+
+    adapts(InequalityBinding, Encoder)
+
+    def encode(self):
+        left = self.encoder.encode(self.binding.left)
+        right = self.encoder.encode(self.binding.right)
+        return InequalityExpression(left, right, self.binding.mark)
+
+
+class EncodeConjunction(Encode):
+
+    adapts(ConjunctionBinding, Encoder)
+
+    def encode(self):
+        terms = [self.encoder.encode(term) for term in self.binding.terms]
+        return ConjunctionExpression(terms, self.binding.mark)
+
+
+class EncodeDisjunction(Encode):
+
+    adapts(DisjunctionBinding, Encoder)
+
+    def encode(self):
+        terms = [self.encoder.encode(term) for term in self.binding.terms]
+        return DisjunctionExpression(terms, self.binding.mark)
+
+
+class EncodeNegation(Encode):
+
+    adapts(NegationBinding, Encoder)
+
+    def encode(self):
+        term = self.encoder.encode(term)
+        return NegationExpression(term, self.binding.mark)
+
+
 encode_adapters = find_adapters()
 
 

src/htsql/tr/fn/__init__.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.tr.fn`
+==================
+
+This package implements HTSQL functions.
+"""
+
+

src/htsql/tr/fn/function.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.tr.fn.function`
+===========================
+
+This module implements HTSQL functions.
+"""
+
+
+from ...adapter import Adapter, Utility, adapts, adapts_none, find_adapters
+from ...error import InvalidArgumentError
+from ...domain import (Domain, UntypedDomain, BooleanDomain, StringDomain,
+                       IntegerDomain, DecimalDomain, FloatDomain, DateDomain)
+from ..binding import (LiteralBinding, FunctionBinding,
+                       EqualityBinding, InequalityBinding,
+                       ConjunctionBinding, DisjunctionBinding, NegationBinding)
+
+
+class named(str):
+
+    name_registry = {}
+
+    class __metaclass__(type):
+
+        def __getitem__(cls, key):
+            if key in cls.name_registry:
+                return cls.name_registry[key]
+            key_type = type(key, (cls,), {})
+            cls.name_registry.setdefault(key, key_type)
+            return cls.name_registry[key]
+
+    def __new__(cls):
+        return super(named, cls).__new__(cls, cls.__name__)
+
+
+class FindFunction(Utility):
+
+    def __call__(self, name, binder):
+        name = named[name]()
+        function = Function(name, binder)
+        return function
+
+
+class Function(Adapter):
+
+    adapts(str)
+
+    def __init__(self, name, binder):
+        self.name = str(name)
+        self.binder = binder
+
+    def bind_operator(self, syntax, parent):
+        raise InvalidArgumentError("unknown operator %s" % self.name,
+                                   syntax.mark)
+
+    def bind_function_operator(self, syntax, parent):
+        raise InvalidArgumentError("unknown function %s" % self.name,
+                                   syntax.identifier.mark)
+
+    def bind_function_call(self, syntax, parent):
+        raise InvalidArgumentError("unknown function %s" % self.name,
+                                   syntax.identifier.mark)
+
+
+class Parameter(object):
+
+    def __init__(self, name, domain_class=Domain,
+                 is_mandatory=True, is_list=False):
+        assert isinstance(name, str)
+        assert issubclass(domain_class, Domain)
+        assert isinstance(is_mandatory, bool)
+        assert isinstance(is_list, bool)
+        self.name = name
+        self.domain_class = domain_class
+        self.is_mandatory = is_mandatory
+        self.is_list = is_list
+
+
+class ProperFunction(Function):
+
+    adapts_none()
+
+    parameters = []
+
+    def bind_operator(self, syntax, parent):
+        arguments = []
+        if syntax.left is not None:
+            arguments.append(syntax.left)
+        if syntax.right is not None:
+            arguments.append(syntax.right)
+        keywords = self.bind_arguments(arguments, parent, syntax.mark)
+        return self.correlate(syntax=syntax, parent=parent, **keywords)
+
+    def bind_function_operator(self, syntax, parent):
+        arguments = [syntax.left, syntax.right]
+        keywords = self.bind_arguments(arguments, parent)
+        return self.correlate(syntax=syntax, parent=parent, **keywords)
+
+    def bind_function_call(self, syntax, parent):
+        arguments = syntax.arguments
+        keywords = self.bind_arguments(arguments, parent, syntax.mark)
+        return self.correlate(syntax=syntax, parent=parent, **keywords)
+
+    def bind_arguments(self, arguments, parent, mark):
+        arguments = [list(self.binder.bind(argument, parent))
+                     for argument in arguments]
+        return self.check_arguments(arguments, mark)
+
+    def check_arguments(self, arguments, mark):
+        arguments = arguments[:]
+        keywords = {}
+        for idx, parameter in enumerate(self.parameters):
+            value = None
+            if not arguments:
+                if parameter.is_mandatory:
+                    raise InvalidArgumentError("missing argument %s"
+                                               % parameter.name, mark)
+            elif parameter.is_list:
+                value = []
+                if len(arguments) > 1 and idx == len(self.parameters)-1:
+                    while arguments:
+                        argument = arguments.pop(0)
+                        if len(argument) != 1:
+                            raise InvalidArgumentError("invalid argument %s"
+                                                       % parameter.name, mark)
+                        value.append(argument[0])
+                else:
+                    argument = arguments.pop(0)
+                    if parameter.is_mandatory and not argument:
+                        raise InvalidArgumentError("missing argument %s"
+                                                   % parameter.name, mark)
+                    value = argument[:]
+                for argument in value:
+                    if not isinstance(argument.domain, parameter.domain_class):
+                        raise InvalidArgumentError("unexpected argument type",
+                                                   argument.mark)
+            else:
+                argument = arguments.pop(0)
+                if len(argument) == 0:
+                    if parameter.is_mandatory:
+                        raise InvalidArgumentError("missing argument %s"
+                                                   % parameter.name, mark)
+                    value = None
+                elif len(argument) == 1:
+                    value = argument[0]
+                    if not isinstance(value.domain, parameter.domain_class):
+                        raise InvalidArgumentError("unexpected argument type",
+                                                   value.mark)
+                else:
+                    raise InvalidArgumentError("too many arguments",
+                                               argument[1].mark)
+            keywords[parameter.name] = value
+        if arguments:
+            raise InvalidArgumentError("unexpected argument",
+                                       arguments[0].mark)
+        return keywords
+
+
+class ProperMethod(ProperFunction):
+
+    adapts_none()
+
+    def bind_arguments(self, arguments, parent, mark):
+        arguments = [parent] + [list(self.binder.bind(argument, parent))
+                                for argument in arguments]
+        return self.check_arguments(arguments, mark)
+
+
+class NullFunction(ProperFunction):
+
+    adapts(named['null'])
+
+    def correlate(self, syntax, parent):
+        yield LiteralBinding(parent, None, UntypedDomain(), syntax)
+
+
+class TrueFunction(ProperFunction):
+
+    adapts(named['true'])
+
+    def correlate(self, syntax, parent):
+        yield LiteralBinding(parent, True, BooleanDomain(), syntax)
+
+
+class FalseFunction(ProperFunction):
+
+    adapts(named['false'])
+
+    def correlate(self, syntax, parent):
+        yield LiteralBinding(parent, False, BooleanDomain(), syntax)
+
+
+class CastFunction(ProperFunction):
+
+    parameters = [
+            Parameter('argument'),
+    ]
+    output_domain = None
+
+    def correlate(self, argument, syntax, parent):
+        yield self.binder.cast(argument, self.output_domain, syntax, parent)
+
+
+class BooleanCastFunction(CastFunction):
+
+    adapts(named['boolean'])
+    output_domain = BooleanDomain()
+
+
+class StringCastFunction(CastFunction):
+
+    adapts(named['string'])
+    output_domain = StringDomain()
+
+
+class IntegerCastFunction(CastFunction):
+
+    adapts(named['integer'])
+    output_domain = IntegerDomain()
+
+
+class DecimalCastFunction(CastFunction):
+
+    adapts(named['decimal'])
+    output_domain = DecimalDomain()
+
+
+class FloatCastFunction(CastFunction):
+
+    adapts(named['float'])
+    output_domain = FloatDomain()
+
+
+class DateCastFunction(CastFunction):
+
+    adapts(named['date'])
+    output_domain = DateDomain()
+
+
+class EqualityOperator(ProperFunction):
+
+    adapts(named['_=_'])
+
+    parameters = [
+            Parameter('left_argument'),
+            Parameter('right_argument'),
+    ]
+
+    def correlate(self, left_argument, right_argument, syntax, parent):
+        domain = self.binder.coerce(left_argument.domain,
+                                    right_argument.domain)
+        if domain is None:
+            raise InvalidArgumentError("invalid arguments", syntax.mark)
+        domain = self.binder.coerce(domain)
+        if domain is None:
+            raise InvalidArgumentError("invalid arguments", syntax.mark)
+        left_argument = self.binder.cast(left_argument, domain)
+        right_argument = self.binder.cast(right_argument, domain)
+        yield EqualityBinding(parent, left_argument, right_argument, syntax)
+
+
+class InequalityOperator(ProperFunction):
+
+    adapts(named['_!=_'])
+
+    parameters = [
+            Parameter('left_argument'),
+            Parameter('right_argument'),
+    ]
+
+    def correlate(self, left_argument, right_argument, syntax, parent):
+        domain = self.binder.coerce(left_argument.domain,
+                                    right_argument.domain)
+        if domain is None:
+            raise InvalidArgumentError("invalid arguments", syntax.mark)
+        domain = self.binder.coerce(domain)
+        if domain is None:
+            raise InvalidArgumentError("invalid arguments", syntax.mark)
+        left_argument = self.binder.cast(left_argument, domain)
+        right_argument = self.binder.cast(right_argument, domain)
+        yield InequalityBinding(parent, left_argument, right_argument, syntax)
+
+
+class ConjunctionOperator(ProperFunction):
+
+    adapts(named['_&_'])
+
+    parameters = [
+            Parameter('left_argument'),
+            Parameter('right_argument'),
+    ]
+
+    def correlate(self, left_argument, right_argument, syntax, parent):
+        left_argument = self.binder.cast(left_argument, BooleanDomain())
+        right_argument = self.binder.cast(right_argument, BooleanDomain())
+        yield ConjunctionBinding(parent,
+                                 [left_argument, right_argument], syntax)
+
+
+class DisjunctionOperator(ProperFunction):
+
+    adapts(named['_|_'])
+
+    parameters = [
+            Parameter('left_argument'),
+            Parameter('right_argument'),
+    ]
+
+    def correlate(self, left_argument, right_argument, syntax, parent):
+        left_argument = self.binder.cast(left_argument, BooleanDomain())
+        right_argument = self.binder.cast(right_argument, BooleanDomain())
+        yield DisjunctionBinding(parent,
+                                 [left_argument, right_argument], syntax)
+
+
+class AdditionOperator(ProperFunction):
+
+    adapts(named['_+_'])
+
+    parameters = [
+            Parameter('left_argument'),
+            Parameter('right_argument'),
+    ]
+
+    def correlate(self, left_argument, right_argument, syntax, parent):
+        Implementation = Add.realize(left_argument.domain,
+                                     right_argument.domain)
+        addition = Implementation(left_argument, right_argument,
+                                  self.binder, syntax, parent)
+        yield addition()
+
+
+class Add(Adapter):
+
+    adapts(Domain, Domain)
+
+    def __init__(self, left_argument, right_argument, binder, syntax, parent):
+        self.left_argument = left_argument
+        self.right_argument = right_argument
+        self.binder = binder
+        self.syntax = syntax
+        self.parent = parent
+
+    def __call__(self):
+        raise InvalidArgumentError("unexpected argument types",
+                                   self.syntax.mark)
+
+
+class ConcatenateBinding(FunctionBinding):
+
+    def __init__(self, parent, left_argument, right_argument, syntax):
+        super(ConcatenateBinding, self).__init__(parent, StringDomain(), syntax,
+                                                 left_argument=left_argument,
+                                                 right_argument=right_argument)
+
+
+class Concatenate(Add):
+
+    adapts_none()
+
+    def __call__(self):
+        left_argument = self.binder.cast(self.left_argument, StringDomain(),
+                                         parent=self.parent)
+        right_argument = self.binder.cast(self.right_argument, StringDomain(),
+                                          parent=self.parent)
+        return ConcatenateBinding(self.parent, left_argument, right_argument,
+                                  self.syntax)
+
+
+class ConcatenateStringToString(Concatenate):
+
+    adapts(StringDomain, StringDomain)
+
+
+class ConcatenateStringToUntyped(Concatenate):
+
+    adapts(StringDomain, UntypedDomain)
+
+
+class ConcatenateUntypedToString(Concatenate):
+
+    adapts(UntypedDomain, StringDomain)
+
+
+class ConcatenateUntypedToUntyped(Concatenate):
+
+    adapts(UntypedDomain, UntypedDomain)
+
+
+function_adapters = find_adapters()
+
+

src/htsql/tr/frame.py

         self.right = right
 
 
+class InequalityPhrase(Phrase):
+
+    def __init__(self, left, right, mark):
+        assert isinstance(left, Phrase)
+        assert isinstance(right, Phrase)
+        domain = BooleanDomain()
+        is_nullable = (left.is_nullable or right.is_nullable)
+        super(InequalityPhrase, self).__init__(domain, is_nullable, mark)
+        self.left = left
+        self.right = right
+
+
 class ConjunctionPhrase(Phrase):
 
     def __init__(self, terms, mark):
         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(DisjunctionPhrase, self).__init__(domain, is_nullable, mark)
         self.terms = terms
 
     def optimize(self):
         return self
 
 
+class CastPhrase(Phrase):
+
+    def __init__(self, term, domain, is_nullable, mark):
+        assert isinstance(term, Phrase)
+        super(CastPhrase, self).__init__(domain, is_nullable, mark)
+        self.term = term
+
+
 class LiteralPhrase(Phrase):
 
     def __init__(self, value, domain, mark):

src/htsql/tr/lookup.py

             joins = []
             for fk in foreign_keys:
                 origin_schema = catalog.schemas[fk.origin_schema_name]
-                origin = catalog.schemas[fk.origin_name]
+                origin = origin_schema.tables[fk.origin_name]
                 target_schema = catalog.schemas[fk.target_schema_name]
                 target = target_schema.tables[fk.target_name]
                 join = DirectJoin(origin, target, fk)

src/htsql/tr/parser.py

             symbol_token = tokens.pop(SymbolToken, ['&'])
             symbol = symbol_token.value
             left = test
-            right = ImpliesParser << tokens
+            right = ImpliesTestParser << tokens
             mark = Mark.union(left, right)
             test = OperatorSyntax(symbol, left, right, mark)
         return test

src/htsql/tr/serializer.py

                       DateDomain)
 from .frame import (Clause, Frame, LeafFrame, ScalarFrame,
                     BranchFrame, CorrelatedFrame, SegmentFrame,
-                    QueryFrame, Phrase, EqualityPhrase, ConjunctionPhrase,
-                    DisjunctionPhrase, NegationPhrase, LiteralPhrase,
-                    LeafReferencePhrase, BranchReferencePhrase,
+                    QueryFrame, Phrase, EqualityPhrase, InequalityPhrase,
+                    ConjunctionPhrase, DisjunctionPhrase, NegationPhrase,
+                    LiteralPhrase, LeafReferencePhrase, BranchReferencePhrase,
                     CorrelatedFramePhrase)
 from .plan import Plan
 import decimal
         return self.format.equal_op(left, right)
 
 
+class SerializeInequality(SerializePhrase):
+
+    adapts(InequalityPhrase, Serializer)
+
+    def serialize(self):
+        left = self.serializer.serialize(self.phrase.left)
+        right = self.serializer.serialize(self.phrase.right)
+        return self.format.equal_op(left, right, is_negative=True)
+
+
 class SerializeConjunction(SerializePhrase):
 
     adapts(ConjunctionPhrase, Serializer)
 
     def serialize(self, value):
         if value is None:
-            return self.format.none()
+            return self.format.null()
         if value is True:
             return self.format.true()
-        if value is false:
+        if value is False:
             return self.format.false()
 
 

src/htsql/tr/sketch.py

     def get_demands(self):
         for appointment in [self.left, self.right]:
             for demand in appointment.get_demands():
-                yield demands
+                yield demand
 
 
 class Attachment(Node):

src/htsql/tr/term.py

 from ..util import Node, listof, dictof, oneof, tupleof, maybe
 from ..mark import Mark
 from ..entity import TableEntity
-from .code import Space, Code, Unit, QueryCode
+from ..domain import BooleanDomain
+from .code import Space, Code, Unit, QueryCode, Expression
 
 
 LEFT = 0
     def __init__(self, child, filter, space, baseline, routes, mark):
         assert isinstance(filter, Expression)
         assert isinstance(filter.domain, BooleanDomain)
-        super(FilterTerm, self).__init__(term, space, baseline,
+        super(FilterTerm, self).__init__(child, space, baseline,
                                          routes, mark)
         self.filter = filter
 
     def __init__(self, space):
         assert isinstance(space, Space) and space.is_axis
         self.space = space
+        self.mark = space.mark
 
 
 class SeriesTie(Tie):
         assert isinstance(is_reverse, bool)
         self.space = space
         self.is_reverse = is_reverse
+        self.mark = space.mark