Commits

Kirill Simonov committed 9158c2c

Documented the `htsql.tr.encode` module.

Renamed `JoinedTableBinding` to `AttachedTableBinding`.

Comments (0)

Files changed (4)

doc/api/htsql.tr.rst

    :members:
 .. automodule:: htsql.tr.coerce
    :members:
+.. automodule:: htsql.tr.encode
+   :members:
 .. automodule:: htsql.tr.error
    :members:
 .. automodule:: htsql.tr.lookup

src/htsql/tr/binding.py

     Represents a table link binding.
 
     This is an abstract class; see :class:`FreeTableBinding` and
-    :class:`JoinedTableBinding` for concrete subclasses.
+    :class:`AttachedTableBinding` for concrete subclasses.
 
     A table binding represents a table context; `Lookup` over a table
     binding looks for columns and links; `Relate` over a table binding
     """
 
 
-class JoinedTableBinding(TableBinding):
+class AttachedTableBinding(TableBinding):
     """
-    Represents a joined table binding.
+    Represents an attached table binding.
 
-    A joined table is attached to its base using a sequence of joins.
+    An attached table is attached to its base using a sequence of joins.
 
     `joins` (a list of :class:`htsql.entity.Join`)
         A sequence of joins that attach the table to its base.
 
     def __init__(self, base, table, joins, syntax):
         assert isinstance(joins, listof(Join)) and len(joins) > 0
-        super(JoinedTableBinding, self).__init__(base, table, syntax)
+        super(AttachedTableBinding, self).__init__(base, table, syntax)
         self.joins = joins
 
 
     `column` (:class:`htsql.entity.ColumnEntity`)
         The column entity.
 
-    `link` (:class:`JoinedTableBinding` or ``None``)
+    `link` (:class:`AttachedTableBinding` or ``None``)
         If set, indicates that the binding could also represent a link
         to another table.  Any `Lookup` or `Relate` requests applied
         to the column binding are delegated to `link`.
 
     def __init__(self, base, column, link, syntax):
         assert isinstance(column, ColumnEntity)
-        assert isinstance(link, maybe(JoinedTableBinding))
+        assert isinstance(link, maybe(AttachedTableBinding))
         super(ColumnBinding, self).__init__(base, column.domain, syntax)
         self.column = column
         # FIXME: this is a hack to permit reparenting of a column binding.

src/htsql/tr/encode.py

 
 
 """
-:mod:`htsql.tr.encoder`
-=======================
+:mod:`htsql.tr.encode`
+======================
 
-This module implements the encoding adapter.
+This module implements the encoding process.
 """
 
 
 from ..domain import Domain, UntypedDomain, TupleDomain, BooleanDomain
 from .error import EncodeError
 from .binding import (Binding, RootBinding, QueryBinding, SegmentBinding,
-                      TableBinding, FreeTableBinding, JoinedTableBinding,
+                      TableBinding, FreeTableBinding, AttachedTableBinding,
                       ColumnBinding, LiteralBinding, SieveBinding,
                       SortBinding, EqualityBinding, TotalEqualityBinding,
                       ConjunctionBinding, DisjunctionBinding,
 
 
 class EncodingState(object):
+    """
+    Encapsulates the (mutable) state of the encoding process.
 
+    Currently encoding is a stateless process, but we will likely add
+    extra state in the future.  The state is also used to store the
+    cache of binding to space and binding to code translations.
+    """
+
+    # Indicates whether results of `encode` or `relate` are cached.
+    # Caching means that two calls of `encode` (or `relate`) on the
+    # same `Binding` instance produce the same object.
+    #
+    # By default, caching is turned on; however the translator must
+    # never rely on that.  That is, the result generated by the
+    # translator must not depend on whether caching is enabled or
+    # disabled.  This parameter gives us an easy way to check this
+    # assumption.  Different results usually mean a bug in comparison
+    # by value for code objects.
     with_cache = True
 
     def __init__(self):
+        # A mapping of cached results of `encode()`.
         self.binding_to_code = {}
+        # A mapping of cached results of `relate()`.
         self.binding_to_space = {}
 
     def encode(self, binding):
+        """
+        Encodes the given binding node to a code expression node.
+
+        Returns a :class:`htsql.tr.code.Code` node (in some cases,
+        a :class:`htsql.tr.code.Expression` node).
+
+        `binding` (:class:`htsql.tr.binding.Binding`)
+            The binding node to encode.
+        """
+        # When caching is enabled, we check if `binding` was
+        # already encoded.  If not, we encode it and save the
+        # result.
         if self.with_cache:
             if binding not in self.binding_to_code:
                 code = encode(binding, self)
                 self.binding_to_code[binding] = code
             return self.binding_to_code[binding]
+        # Caching is disabled; return a new instance every time.
         return encode(binding, self)
 
     def relate(self, binding):
+        """
+        Encodes the given binding node to a space expression node.
+
+        Returns a :class:`htsql.tr.code.Space` node.
+
+        `binding` (:class:`htsql.tr.binding.Binding`)
+            The binding node to encode.
+        """
+        # When caching is enabled, we check if `binding` was
+        # already encoded.  If not, we encode it and save the
+        # result.
         if self.with_cache:
             if binding not in self.binding_to_space:
                 space = relate(binding, self)
                 self.binding_to_space[binding] = space
             return self.binding_to_space[binding]
+        # Caching is disabled; return a new instance every time.
         return relate(binding, self)
 
     def direct(self, binding):
+        """
+        Extracts a direction modifier from the given binding.
+
+        A direction modifier is set by post-fix ``+`` and ``-`` operators.
+        The function returns ``+1`` for the ``+`` modifier (ascending order),
+        ``-1`` for the ``-`` modifier (descending order), ``None`` if there
+        are no modifiers.
+
+        `binding` (:class:`htsql.tr.binding.Binding`)
+            The binding node.
+        """
+        # FIXME: `Direct` does not really depend on the state, should
+        # it still accept it?
         return direct(binding, self)
 
 
 class EncodeBase(Adapter):
+    """
+    Applies an encoding adapter to a binding node.
+
+    This is a base class for three encoding adapters: :class:`Encode`,
+    :class:`Relate`, :class:`Direct`; it encapsulates methods and
+    attributes shared between these adapters.
+
+    The encoding process translates binding nodes to space and code
+    nodes.  Space nodes represent ordered sets of rows; code nodes
+    represent functions on spaces.  See :class:`htsql.tr.binding.Binding`,
+    :class:`htsql.tr.code.Expression`, :class:`htsql.tr.code.Space`,
+    :class:`htsql.tr.code.Code` for more details on the respective
+    node types.
+
+    `binding` (:class:`htsql.tr.binding.Binding`)
+        The binding node to encode.
+
+    `state` (:class:`EncodingState`)
+        The current state of the encoding process.
+    """
 
     adapts(Binding)
 
 
 
 class Encode(EncodeBase):
+    """
+    Translates a binding node to a code expression node.
+
+    This is an interface adapter; see subclasses for implementations.
+
+    The :class:`Encode` adapter has the following signature::
+
+        Encode: (Binding, State) -> Code or Expression
+
+    The adapter is polymorphic on the `Binding` argument.
+
+    See :class:`htsql.tr.binding.Binding`, :class:`htsql.tr.code.Expression`,
+    :class:`htsql.tr.code.Code` for detail on the respective nodes.
+
+    This adapter provides non-trivial implementation for binding
+    nodes representing HTSQL functions and operators.
+    """
 
     def __call__(self):
+        # The default implementation generates an error.
+        # FIXME: a better error message?
         raise EncodeError("expected a valid code expression",
                           self.binding.mark)
 
 
 class Relate(EncodeBase):
+    """
+    Translates a binding node to a space expression node.
+
+    This is an interface adapter; see subclasses for implementations.
+
+    The :class:`Relate` adapter has the following signature::
+
+        Relate: (Binding, State) -> Space
+
+    The adapter is polymorphic on the `Binding` argument.
+
+    See :class:`htsql.tr.binding.Binding` and :class:`htsql.tr.code.Space`
+    for detail on the respective nodes.
+
+    The adapter provides non-trivial implementations for subclasses
+    of :class:`htsql.tr.binding.ChainBinding`; the `base` attributes
+    are used to restore the structure of the space.
+    """
 
     def __call__(self):
+        # The default implementation generates an error.
+        # FIXME: a better error message?
         raise EncodeError("expected a valid space expression",
                           self.binding.mark)
 
 
 class Direct(EncodeBase):
+    """
+    Extracts a direction modifier from the given binding.
+
+    A direction modifier is set by post-fix ``+`` and ``-`` operators.
+    The function returns ``+1`` for the ``+`` modifier (ascending order),
+    ``-1`` for the ``-`` modifier (descending order), ``None`` if there
+    are no modifiers.
+
+    This is an interface adapter; see subclasses for implementations.
+
+    The :class:`Direct` adapter has the following signature::
+
+        Direct: (Binding, State) -> +1 or -1 or None
+
+    The adapter is polymorphic on the `Binding` argument.
+
+    The adapter unwraps binding nodes looking for instances of
+    :class:`htsql.tr.binding.DirectionBinding`, which represent
+    direction modifiers.
+    """
 
     def __call__(self):
+        # The default implementation produces no modifier.
         return None
 
 
 class EncodeQuery(Encode):
+    """
+    Encodes the top-level binding node :class:`htsql.tr.binding.QueryBinding`.
+
+    Produces an instance of :class:`htsql.tr.code.QueryExpression`.
+    """
 
     adapts(QueryBinding)
 
     def __call__(self):
+        # Encode the segment node if it is provided.
         segment = None
         if self.binding.segment is not None:
             segment = self.state.encode(self.binding.segment)
+        # Construct the expression node.
         return QueryExpression(segment, self.binding)
 
 
 class EncodeSegment(Encode):
+    """
+    Encodes a segment binding node :class:`htsql.tr.binding.SegmentBinding`.
+
+    Produces an instance of :class:`htsql.tr.code.SegmentExpression`.
+    """
 
     adapts(SegmentBinding)
 
     def __call__(self):
+        # The base is translated to the segment space.  Note that we still
+        # need to extract and apply direction modifiers.
         space = self.state.relate(self.binding.base)
+        # A list of pairs `(code, dir)` that contains extracted direction
+        # modifiers and respective code nodes.
         order = []
+        # The list of segment elements.
         elements = []
+        # Encode each of the element binding at the same time extracting
+        # direction modifiers.
         for binding in self.binding.elements:
+            # Encode the node.
             element = self.state.encode(binding)
+            elements.append(element)
+            # Extract the direction modifier.
             direction = self.state.direct(binding)
             if direction is not None:
                 order.append((element, direction))
-            elements.append(element)
+        # Augment the segment space by adding explicit ordering node.
+        # FIXME: we add `OrderedSpace` on top even when `order` is empty
+        # because otherwise `ORDER BY` terms will not be assembled.
+        # Fix the assembler?
         space = OrderedSpace(space, order, None, None, self.binding)
+        # Construct the expression node.
         return SegmentExpression(space, elements, self.binding)
 
 
 class RelateRoot(Relate):
+    """
+    Translates the root binding node to a space node.
+
+    Returns a scalar space node :class:`htsql.tr.code.ScalarSpace`.
+    """
 
     adapts(RootBinding)
 
     def __call__(self):
+        # The root binding always originates the scalar space `I`.
         return ScalarSpace(None, self.binding)
 
 
 class RelateFreeTable(Relate):
+    """
+    Translates a free table binding to a space node.
+
+    Returns a cross product node :class:`htsql.tr.code.CrossProductSpace`.
+    """
 
     adapts(FreeTableBinding)
 
     def __call__(self):
+        # Generate a space node corresponding to the binding base.
         base = self.state.relate(self.binding.base)
+        # Produce a cross product space between the base space and
+        # the binding table: `base * table`.
         return CrossProductSpace(base, self.binding.table, self.binding)
 
 
-class RelateJoinedTable(Relate):
+class RelateAttachedTable(Relate):
+    """
+    Translates an attached table binding to a space node.
 
-    adapts(JoinedTableBinding)
+    Returns a join product node :class:`htsql.tr.code.JoinProductSpace`.
+    """
+
+    adapts(AttachedTableBinding)
 
     def __call__(self):
+        # Generate a space node corresponding to the binding base.
         space = self.state.relate(self.binding.base)
+        # The binding is attached to its base by a series of joins.
+        # For each join, we produce a join product space:
+        #   `base . target1 . target2 . ...`.
         for join in self.binding.joins:
             space = JoinProductSpace(space, join, self.binding)
         return space
 
 
 class RelateSieve(Relate):
+    """
+    Translates a sieve binding to a space node.
+
+    Returns a filtered space node :class:`htsql.tr.code.FilteredSpace`.
+    """
 
     adapts(SieveBinding)
 
     def __call__(self):
+        # Generate a space node corresponding to the binding base.
         space = self.state.relate(self.binding.base)
+        # Encode the predicate expression.
         filter = self.state.encode(self.binding.filter)
+        # Augment the base space with a filter: `base ? filter`.
         return FilteredSpace(space, filter, self.binding)
 
 
 class DirectSieve(Direct):
+    """
+    Extracts the direction modifier from a sieve binding.
+    """
 
     adapts(SieveBinding)
 
     def __call__(self):
+        # We delegate the adapter to the binding base, ignoring the filter.
+        # FIXME: It is controversal, but matches the `entitle` adapter.
+        # Perhaps, we need to add a class attribute `is_passthrough`
+        # (or a mixin class `PassthroughBinding`) to indicate bindings
+        # that could delegate modifier extraction adapters?
         return self.state.direct(self.binding.base)
 
 
 class RelateSort(Relate):
+    """
+    Translates a sort binding to a space node.
+
+    Returns an ordered space node :class:`htsql.tr.code.OrderedSpace`.
+    """
 
     adapts(SortBinding)
 
     def __call__(self):
+        # Generate a space node corresponding to the binding base.
         space = self.state.relate(self.binding.base)
+        # A list of pairs `(code, direction)` containing the expressions
+        # by which the space is sorted and respective direction indicators.
         order = []
+        # Iterate over ordering binding nodes.
         for binding in self.binding.order:
+            # Encode the binding.
             code = self.state.encode(binding)
+            # Extract the direction modifier; assume `+` if none.
             direction = self.state.direct(binding)
             if direction is None:
                 direction = +1
             order.append((code, direction))
+        # The slice indicators.
         limit = self.binding.limit
         offset = self.binding.offset
+        # Produce an ordered space node over the base space:
+        #   `base [e,...;offset:offset+limit]`.
         return OrderedSpace(space, order, limit, offset, self.binding)
 
 
 class EncodeColumn(Encode):
+    """
+    Translates a column binding to a code node.
+
+    Returns a column unit node :class:`htsql.tr.code.ColumnUnit`.
+    """
 
     adapts(ColumnBinding)
 
     def __call__(self):
+        # The binding base is translated to the space of the unit node.
         space = self.state.relate(self.binding.base)
+        # Generate a column unit node.
         return ColumnUnit(self.binding.column, space, self.binding)
 
 
 class RelateColumn(Relate):
+    """
+    Translates a column binding to a space node.
+
+    Returns a join product node :class:`htsql.tr.code.JoinProductSpace` or
+    raises an error.
+    """
 
     adapts(ColumnBinding)
 
     def __call__(self):
+        # If the column binding has an associated table binding node,
+        # delegate the adapter to it.
         if self.binding.link is not None:
             return self.state.relate(self.binding.link)
+        # Otherwise, let the parent produce an error message.
         return super(RelateColumn, self).__call__()
 
 
 class EncodeLiteral(Encode):
+    """
+    Encodes a literal binding.
+
+    Returns a literal code node :class:`htsql.tr.code.LiteralCode`.
+    """
 
     adapts(LiteralBinding)
 
     def __call__(self):
+        # Switch the class from `Binding` to `Code` keeping all attributes.
         return LiteralCode(self.binding.value, self.binding.domain,
                            self.binding)
 
 
 class EncodeEquality(Encode):
+    """
+    Encodes an equality (``=``) binding.
+
+    Returns a :class:`htsql.tr.code.EqualityCode` node.
+    """
+
+    # FIXME: this and the next few encoders share the same structure:
+    # encode the operands and generate a new code node that have the
+    # same shape as the original binding node.  There must be a generic
+    # way to do it without adding an adapter for each binding.  This is
+    # also a problem with encoding functions.
 
     adapts(EqualityBinding)
 
     def __call__(self):
+        # Translate the operands and generate a code node.
         lop = self.state.encode(self.binding.lop)
         rop = self.state.encode(self.binding.rop)
         return EqualityCode(lop, rop, self.binding)
 
 
 class EncodeTotalEquality(Encode):
+    """
+    Encodes a total equality (``==``) binding.
+
+    Returns a :class:`htsql.tr.code.TotalEqualityCode` node.
+    """
 
     adapts(TotalEqualityBinding)
 
     def __call__(self):
+        # Translate the operands and generate a code node.
         lop = self.state.encode(self.binding.lop)
         rop = self.state.encode(self.binding.rop)
         return TotalEqualityCode(lop, rop, self.binding)
 
 
 class EncodeConjunction(Encode):
+    """
+    Encodes a logical "AND" (``&``) binding.
+
+    Returns a :class:`htsql.tr.code.ConjunctionCode` node.
+    """
 
     adapts(ConjunctionBinding)
 
     def __call__(self):
+        # Translate the operands and generate a code node.
         ops = [self.state.encode(op) for op in self.binding.ops]
         return ConjunctionCode(ops, self.binding)
 
 
 class EncodeDisjunction(Encode):
+    """
+    Encodes a logical "OR" (``|``) binding.
+
+    Returns a :class:`htsql.tr.code.DisjunctionCode` node.
+    """
 
     adapts(DisjunctionBinding)
 
     def __call__(self):
+        # Translate the operands and generate a code node.
         ops = [self.state.encode(op) for op in self.binding.ops]
         return DisjunctionCode(ops, self.binding)
 
 
 class EncodeNegation(Encode):
+    """
+    Encodes a logical "NOT" (``!``) binding.
+
+    Returns a :class:`htsql.tr.code.NegationCode` node.
+    """
 
     adapts(NegationBinding)
 
     def __call__(self):
+        # Translate the operand and generate a code node.
         op = self.state.encode(self.binding.op)
         return NegationCode(op, self.binding)
 
 
 class Convert(Adapter):
+    """
+    Encodes a cast binding to a code node.
+
+    This is an auxiliary adapter used to encode
+    :class:`htsql.tr.binding.CastBinding` nodes.  The adapter is polymorphic
+    by the origin and the target domains.
+
+    The purpose of the adapter is to handle conversions from special types:
+    :class:`htsql.domain.UntypedDomain` and :class:`htsql.domain.TupleDomain`.
+    Conversions from regular types are passed as is without any extra checks.
+
+    `binding` (:class:`htsql.tr.binding.CastBinding`)
+        The binding node to encode.
+
+    `state` (:class:`EncodingState`)
+        The current state of the encoding process.
+
+    The adapter is dispatched on the pair:
+    `(binding.base.domain, binding.domain)`.
+
+    Aliases:
+
+    `base` (:class:`htsql.tr.binding.Binding`)
+        An alias for `binding.base`; the operand of the cast expression.
+
+    `domain` (:class:`htsql.domain.Domain`)
+        An alias for `binding.domain`; the target domain.
+    """
 
     adapts(Domain, Domain)
 
     @classmethod
     def dispatch(interface, binding, *args, **kwds):
+        # We override the standard extract of the dispatch key, which
+        # returns the type of the first argument(s).  For `Convert`,
+        # the dispatch key is the pair of the origin and the target domains.
         assert isinstance(binding, CastBinding)
         return (type(binding.base.domain), type(binding.domain))
 
         self.state = state
 
     def __call__(self):
+        # The default implementation encodes an operand and
+        # returns a cast code node.
+        # Note: this also handles the case when the origin domain is
+        # `TupleDomain` and the target domain is *not* `BooleanDomain`.
+        # In this case, encoding of the base binding will raise an error.
         base = self.state.encode(self.base)
+        # A minor optimization: when the origin and the target domains
+        # coincide, the cast is no-op.  More elaborate optimizations
+        # are performed further on the stack.
         if base.domain == self.domain:
             return base
         return CastCode(base, self.domain, self.binding)
 
 
 class ConvertUntyped(Convert):
+    """
+    Validates and converts untyped literals.
+    """
 
     adapts(UntypedDomain, Domain)
 
     def __call__(self):
+        # The base binding is of untyped domain, however it does not have
+        # to be an instance of `LiteralBinding` since the actual literal node
+        # could be wrapped by decorators.  However after we encode the node,
+        # the decorators are gone and the result must be a `LiteralCode`.
+        # The domain should remain the same too.
         base = self.state.encode(self.base)
         assert isinstance(base, LiteralCode)
         assert isinstance(base.domain, UntypedDomain)
+        # Convert the serialized literal value to a Python object; raises
+        # a `ValueError` if the literal is not in a valid format.
         try:
             value = self.domain.parse(base.value)
         except ValueError, exc:
             raise EncodeError(str(exc), self.binding.mark)
+        # Generate a new literal node with the converted value and
+        # the target domain.
         return LiteralCode(value, self.domain, self.binding)
 
 
 class ConvertTupleToBoolean(Convert):
+    """
+    Converts a tuple expression to a conditional expression.
+    """
 
     adapts(TupleDomain, BooleanDomain)
 
     def __call__(self):
-        space = self.state.relate(self.binding.base)
+        # When the binding domain is tuple, we assume that the binding
+        # represents some space.  In this case, Boolean cast produces
+        # an expression which is `FALSE` when the space is empty and
+        # `TRUE` otherwise.  The actual expression is:
+        #   `!(column==null())`,
+        # where `column` is some non-nullable column of the prominent
+        # table of the space.
+
+        # Translate the operand to a space node.
+        space = self.state.relate(self.base)
+        # Now the space may lack the prominent table; however this case
+        # is hardly useful and difficult to handle, so we generate an
+        # error here.
         if space.table is None:
             raise EncodeError("expected a space with a prominent table",
                               self.binding.mark)
+        # Find a non-nullable column; if all columns are nullable,
+        # our only option is to raise an error.
         for column in space.table.columns:
             if not column.is_nullable:
                 break
         else:
             raise EncodeError("expected a table with at least one"
                               " non-nullable column", self.binding.mark)
+        # Generate and return the expression: `!(column==null())`.
         unit = ColumnUnit(column, space, self.binding)
         literal = LiteralCode(None, column.domain, self.binding)
         return NegationCode(TotalEqualityCode(unit, literal, self.binding),
 
 
 class EncodeCast(Encode):
+    """
+    Encodes a cast binding.
+
+    The actual encoding is performed by the :class:`Convert` adapter.
+    """
 
     adapts(CastBinding)
 
     def __call__(self):
+        # Delegate it to the `Convert` adapter.
         convert = Convert(self.binding, self.state)
         return convert()
 
 
 class DirectCast(Direct):
+    """
+    Extracts a direction modifier from a cast binding.
+    """
 
     adapts(CastBinding)
 
     def __call__(self):
+        # The adapter is delegated to the binding base; we have to do it
+        # because many expressions (including segment elements) are wrapped
+        # with implicit cast nodes, which otherwise would mask any decorators.
         return self.state.direct(self.binding.base)
 
 
 class EncodeWrapper(Encode):
+    """
+    Translates a wrapper binding to a code node.
+    """
 
     adapts(WrapperBinding)
 
     def __call__(self):
+        # Delegate the adapter to the wrapped binding.
         return self.state.encode(self.binding.base)
 
 
 class RelateWrapper(Relate):
+    """
+    Translates a wrapper binding to a space node.
+    """
 
     adapts(WrapperBinding)
 
     def __call__(self):
+        # Delegate the adapter to the wrapped binding.
         return self.state.relate(self.binding.base)
 
 
 class DirectWrapper(Direct):
+    """
+    Extracts a direction modifier from a wrapper binding.
+    """
 
     adapts(WrapperBinding)
 
     def __call__(self):
+        # Delegate the adapter to the wrapped binding.
         return self.state.direct(self.binding.base)
 
 
 class DirectDirection(Direct):
+    """
+    Extracts a direction modifier from a direction decorator.
+    """
 
     adapts(DirectionBinding)
 
     def __call__(self):
+        # The direction modifier specified by the decorator.
         direction = self.binding.direction
+        # Here we handle nested decorators: `++` and `--` are
+        # translated to `+`; `+-` and `-+` are translated to `-`.
         base_direction = self.state.direct(self.binding.base)
         if base_direction is not None:
             direction *= base_direction
+        # Return the combined direction.
         return direction
 
 
 def encode(binding, state=None):
+    """
+    Encodes the given binding to a code expression node.
+
+    Returns a :class:`htsql.tr.code.Code` instance (in some cases,
+    a :class:`htsql.tr.code.Expression` instance).
+
+    `binding` (:class:`htsql.tr.binding.Binding`)
+        The binding node to encode.
+
+    `state` (:class:`EncodingState` or ``None``)
+        The encoding state to use.  If not set, a new encoding state
+        is instantiated.
+    """
+    # Create a new encoding state if necessary.
     if state is None:
         state = EncodingState()
+    # Realize and apply the `Encode` adapter.
     encode = Encode(binding, state)
     return encode()
 
 
 def relate(binding, state=None):
+    """
+    Encodes the given binding to a space expression node.
+
+    Returns a :class:`htsql.tr.code.Space` instance.
+
+    `binding` (:class:`htsql.tr.binding.Binding`)
+        The binding node to encode.
+
+    `state` (:class:`EncodingState` or ``None``)
+        The encoding state to use.  If not set, a new encoding state
+        is instantiated.
+    """
+    # Create a new encoding state if necessary.
     if state is None:
         state = EncodingState()
+    # Realize and apply the `Relate` adapter.
     relate = Relate(binding, state)
     return relate()
 
 
 def direct(binding, state=None):
+    """
+    Extracts a direction modifier from the given binding.
+
+    Returns
+
+    - ``+1`` for the ``+`` modifier (ascending order);
+    - ``-1`` for the ``-`` modifier (descending order);
+    - ``None`` if there are no modifiers.
+
+    `binding` (:class:`htsql.tr.binding.Binding`)
+        The binding node.
+
+    `state` (:class:`EncodingState` or ``None``)
+        The encoding state to use.  If not set, a new encoding state
+        is instantiated.
+    """
+    # Create a new encoding state if necessary.
     if state is None:
         state = EncodingState()
+    # Realize and apply the `Relate` adapter.
     direct = Direct(binding, state)
     return direct()
 

src/htsql/tr/lookup.py

 from ..entity import DirectJoin, ReverseJoin
 from .syntax import Syntax, IdentifierSyntax
 from .binding import (Binding, RootBinding, ChainBinding,
-                      TableBinding, FreeTableBinding, JoinedTableBinding,
+                      TableBinding, FreeTableBinding, AttachedTableBinding,
                       ColumnBinding, SieveBinding, WrapperBinding, SortBinding)
 import re
 import unicodedata
                 join = DirectJoin(origin, target, fk)
                 joins.append(join)
             # Build and return the link binding.
-            return JoinedTableBinding(self.binding, target, joins, syntax)
+            return AttachedTableBinding(self.binding, target, joins, syntax)
 
 
 class LookupTable(Lookup, LookupItemizeTableMixin):
       context table.
 
     Column members give rise to :class:`ColumnBinding` instances
-    while table members give rise to :class:`JoinedTableBinding` instances.
+    while table members give rise to :class:`AttachedTableBinding` instances.
     """
 
     adapts(TableBinding)
             target_schema = self.catalog.schemas[foreign_key.target_schema_name]
             target = target_schema.tables[foreign_key.target_name]
             join = DirectJoin(origin, target, foreign_key)
-            return JoinedTableBinding(self.binding, target, [join],
-                                      self.identifier)
+            return AttachedTableBinding(self.binding, target, [join],
+                                        self.identifier)
 
     def lookup_reverse_join(self):
         # Finds a table with the given name that possesses a foreign key
             target_schema = self.catalog.schemas[foreign_key.origin_schema_name]
             target = target_schema.tables[foreign_key.origin_name]
             join = ReverseJoin(origin, target, foreign_key)
-            return JoinedTableBinding(self.binding, target, [join],
-                                      self.identifier)
+            return AttachedTableBinding(self.binding, target, [join],
+                                        self.identifier)
 
 
 class ItemizeTable(Itemize, LookupItemizeTableMixin):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.