Commits

Kirill Simonov  committed dcb7da8

Added `root()`, `this()` and `cross()` macros.

Also added explicit root context to the binding state, implemented
the `!~` operator.

  • Participants
  • Parent commits e71f825

Comments (0)

Files changed (7)

File src/htsql/tr/assemble.py

 :mod:`htsql.tr.assemble`
 ========================
 
-This module implements the assemble adapter.
+This module implements the assembling process.
 """
 
 
                 if axis in rkid.routes:
                     tie = ParallelTie(axis)
                     ties.append(tie)
+                axis = axis.base
             ties.reverse()
         else:
             lkid = self.state.inject(lkid, [rkid.baseline.base])
                 if axis in rkid.routes:
                     tie = ParallelTie(axis)
                     ties.append(tie)
+                axis = axis.base
             ties.reverse()
         else:
             lkid = self.state.inject(lkid, [rkid.baseline.base])
                 if axis in rkid.routes:
                     tie = ParallelTie(axis)
                     ties.append(tie)
+                axis = axis.base
             ties.reverse()
         else:
             lkid = self.state.inject(lkid, [rkid.baseline.base])
                 if axis in rkid.routes:
                     tie = ParallelTie(axis)
                     ties.append(tie)
+                axis = axis.base
             ties.reverse()
         else:
             lkid = self.state.inject(lkid, [rkid.baseline.base])

File src/htsql/tr/bind.py

 
     State attributes:
 
+    `root` (:class:`htsql.tr.binding.RootBinding`)
+
     `base` (:class:`htsql.tr.binding.Binding`)
         The current lookup context.
     """
 
     def __init__(self):
+        # The root lookup context.
+        self.root = None
         # The current lookup context.
         self.base = None
         # The stack of previous lookup contexts.
         self.base_stack = []
 
+    def set_root(self, root):
+        """
+        Sets the root lookup context.
+
+        This function initializes the lookup context stack and must be
+        called before any calls of :meth:`push_base` and :meth:`pop_base`.
+
+        `root` (:class:`htsql.tr.binding.RootBinding`)
+            The root lookup context.
+        """
+        # Check that the lookup stack is not initialized.
+        assert self.root is None
+        assert self.base is None
+        assert isinstance(root, RootBinding)
+        self.root = root
+        self.base = root
+
+    def unset_root(self):
+        """
+        Removes the root lookup context.
+        """
+        # We expect the lookup context stack being empty and the current
+        # context to coincide with the root context.
+        assert self.root is not None
+        assert not self.base_stack
+        assert self.root is self.base
+        self.root = None
+        self.base = None
+
     def push_base(self, base):
         """
         Sets the new lookup context.
         """
         # Sanity check on the argument.
         assert isinstance(base, Binding)
+        # Ensure that the root context was set.
+        assert self.root is not None
         # Save the current lookup context.
         self.base_stack.append(self.base)
         # Assign the new lookup context.
     adapts(QuerySyntax)
 
     def __call__(self):
-        # Set the root lookup context: `RootBinding` represents a scalar
-        # context with `lookup` implemented as table lookup.
+        # Initialize the lookup context stack with a root context, which
+        # represents a scalar context with `lookup` implemented as table
+        # lookup.
         root = RootBinding(self.syntax)
-        self.state.push_base(root)
+        self.state.set_root(root)
         # Bind the segment node if it is available.
         segment = None
         if self.syntax.segment is not None:
             segment = self.state.bind(self.syntax.segment)
-        # Restore the original lookup context.
-        self.state.pop_base()
+        # Shut down the lookup context stack.
+        self.state.unset_root()
         # Construct and return the top-level binding node.
         yield QueryBinding(root, segment, self.syntax)
 

File src/htsql/tr/encode.py

             if direction is not None:
                 order.append((element, direction))
         # If any direction modifiers are found, augment the segment space
-        # by adding explicit ordering node.
+        # by adding an explicit ordering node.
         if order:
             space = OrderedSpace(space, order, None, None, self.binding)
         # Construct the expression node.

File src/htsql/tr/fn/function.py

 from ..binding import (LiteralBinding, SortBinding, FunctionBinding,
                        EqualityBinding, TotalEqualityBinding,
                        ConjunctionBinding, DisjunctionBinding, NegationBinding,
-                       CastBinding, TitleBinding, DirectionBinding)
+                       CastBinding, WrapperBinding, TitleBinding,
+                       DirectionBinding)
 from ..encode import Encode
 from ..code import (FunctionCode, NegationCode, ScalarUnit, AggregateUnit,
                     CorrelatedUnit, LiteralCode, FilteredSpace)
 from ..frame import FunctionPhrase
 from ..serializer import Serializer, Format, Serialize
 from ..coerce import coerce
+from ..lookup import lookup
 
 
 class Function(Protocol):
         return self.check_arguments(arguments)
 
 
+class RootFunction(Function):
+
+    named('root')
+
+    def __call__(self):
+        if len(self.syntax.arguments) != 0:
+            raise InvalidArgumentError("unexpected arguments",
+                                       self.syntax.mark)
+        yield WrapperBinding(self.state.root, self.syntax)
+
+
+class ThisFunction(Function):
+
+    named('this')
+
+    def __call__(self):
+        if len(self.syntax.arguments) != 0:
+            raise InvalidArgumentError("unexpected arguments",
+                                       self.syntax.mark)
+        yield WrapperBinding(self.state.base, self.syntax)
+
+
+class CrossFunction(Function):
+
+    named('cross')
+
+    def __call__(self):
+        if len(self.syntax.arguments) < 1:
+            raise InvalidArgumentError("an argument expected",
+                                       self.syntax.mark)
+        elif len(self.syntax.arguments) > 1:
+            raise InvalidArgumentError("unexpected arguments",
+                                       self.syntax.mark)
+        argument = self.syntax.arguments[0]
+        if not isinstance(argument, IdentifierSyntax):
+            raise InvalidArgumentError("an identifier expected",
+                                       argument.mark)
+        binding = lookup(self.state.root, argument)
+        if binding is None:
+            raise InvalidArgumentError("unknown identifier",
+                                       argument.mark)
+        binding = binding.clone(base=self.state.base)
+        yield WrapperBinding(binding, self.syntax)
+
+
 class AsFunction(ProperFunction):
 
     named('as')
     def correlate(self, left, right):
         signature = (type(left.domain), type(right.domain))
         Implementation = Contains.realize(signature)
-        length = Implementation(left, right, self.state, self.syntax)
-        yield length()
+        contains = Implementation(left, right, self.state, self.syntax)
+        yield contains()
+
+
+class NotContainsOperator(ProperFunction):
+
+    named('!~')
+
+    parameters = [
+            Parameter('left'),
+            Parameter('right'),
+    ]
+
+    def correlate(self, left, right):
+        signature = (type(left.domain), type(right.domain))
+        Implementation = Contains.realize(signature)
+        contains = Implementation(left, right, self.state, self.syntax)
+        yield NegationBinding(contains(), self.syntax)
 
 
 class Contains(Adapter):

File src/htsql/tr/term.py

     the rows of the space.  Note that taken alone, the term does not
     necessarily generates the rows of the space: some of the operations
     that comprise the space may be missing from the term.  Thus the term
-    space represents a promice: once the term is tied with some other
+    space represents a promise: once the term is tied with some other
     appropriate term, it will generate the rows of the space.
 
     Each term maintains a table of units it is capable to produce.
     but is serialized to SQL as a correlated sub-SELECT clause.
 
     `lkid` (:class:`RoutingTerm`)
-        The left operand of the correlated join.
+        The main term.
 
     `rkid` (:class:`RoutingTerm`)
-        The right operand of the correlated join.
+        The correlated term.
 
     `ties` (a list of :class:`Tie`)
         The ties that establish the join condition.
     is_parallel = True
 
     def __init__(self, space):
-        assert isinstace(space, Space) and space.is_axis
+        assert isinstance(space, Space) and space.is_axis
         # Technically, non-inflated axis spaces could be permitted, but
         # since the assembler only generates ties for inflated spaces,
         # we add a respective check here.

File test/input/pgsql.yaml

     - uri: /school{*,count(department)}
     - uri: /school{*,count(department?exists(course))}
     - uri: /school{*,count(department.exists(course))}
-    # Aggregates with non-native spaces (still broken!).
+    # Aggregates with non-native spaces.
+    # Triggers a bug in the Postgresql optimizer
+    # (see http://archives.postgresql.org/pgsql-bugs/2010-09/msg00265.php).
     - uri: /department{code,school.code,
                        count(school.program),school.count(program)}
       skip: true
                        exists(school.program),school.exists(program)}
       skip: true
 
+  - title: Root, This and Join functions
+    tests:
+    # Cross joins.
+    - uri: /{count(school)*count(department),count(school.cross(department))}
+    # Custom joins.
+    - uri: /{count(school.department),
+             count(school.cross(department)?school=root().school.code)}
+    # Lifting a unit to a dominating space.
+    - uri: /school{code,name,this(){code}?name!~'art',
+                   root().school{code}?name!~'art'}
+    - uri: /school{name,count(department),this(){count(department)}?name!~'art'}
+    - uri: /school{name,exists(department),this(){exists(department)}?name!~'art'}
+
   # Demonstrate selection of a formatter based on the `Accept` header.
   - title: Formatters
     tests:

File test/output/pgsql.yaml

          ----
          /school{*,count(department.exists(course))}
          SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT "department"."school", COUNT(NULLIF(EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((NOT (TRUE IS NOT DISTINCT FROM NULL)) AND ("department"."code" = "course"."department")))), FALSE)) AS "!" FROM "ad"."department" AS "department" GROUP BY 1) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
+  - id: root,-this-and-join-functions
+    tests:
+    - uri: /{count(school)*count(department),count(school.cross(department))}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         |                                                                   |
+        -+-------------------------------------------------------------------+-
+         | count(school)*count(department) | count(school.cross(department)) |
+        -+---------------------------------+---------------------------------+-
+         |                             243 |                             243 |
+                                                                       (1 row)
+
+         ----
+         /{count(school)*count(department),count(school.cross(department))}
+         SELECT (COALESCE("school"."!", 0) * COALESCE("department_1"."!", 0)), COALESCE("department_2"."!", 0) FROM (SELECT 1) AS "!" LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."school" AS "school") AS "school" ON (TRUE) LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department") AS "department_1" ON (TRUE) LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."school" AS "school" CROSS JOIN "ad"."department" AS "department") AS "department_2" ON (TRUE)
+    - uri: /{count(school.department), count(school.cross(department)?school=root().school.code)}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         |                                                                                      |
+        -+--------------------------------------------------------------------------------------+-
+         | count(school.department) | count(school.cross(department)?school=root().school.code) |
+        -+--------------------------+-----------------------------------------------------------+-
+         |                       24 |                                                        24 |
+                                                                                          (1 row)
+
+         ----
+         /{count(school.department),count(school.cross(department)?school=root().school.code)}
+         SELECT COALESCE("department_1"."!", 0), COALESCE("department_2"."!", 0) FROM (SELECT 1) AS "!" LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."school" AS "school" INNER JOIN "ad"."department" AS "department" ON (("school"."code" = "department"."school"))) AS "department_1" ON (TRUE) LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."school" AS "school" CROSS JOIN "ad"."department" AS "department" WHERE ("department"."school" = "school"."code")) AS "department_2" ON (TRUE)
+    - uri: /school{code,name,this(){code}?name!~'art', root().school{code}?name!~'art'}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | school                                                                                               |
+        -+------------------------------------------------------------------------------------------------------+-
+         | code | name                             | this(){code}?name!~'art' | root().school{code}?name!~'art' |
+        -+------+----------------------------------+--------------------------+---------------------------------+-
+         | art  | School of Art and Design         |                          |                                 |
+         | bus  | School of Business               | bus                      | bus                             |
+         | edu  | College of Education             | edu                      | edu                             |
+         | egn  | School of Engineering            | egn                      | egn                             |
+         | la   | School of Arts, Letters, and the |                          |                                 |
+         :      : Humanities                       :                          :                                 :
+         | mart | School of Modern Art             |                          |                                 |
+         | mus  | Musical School                   | mus                      | mus                             |
+         | ns   | School of Natural Sciences       | ns                       | ns                              |
+         | sc   | School of Continuing Studies     | sc                       | sc                              |
+                                                                                                         (9 rows)
+
+         ----
+         /school{code,name,this(){code}?name!~'art',root().school{code}?name!~'art'}
+         SELECT "school_1"."code", "school_1"."name", "school_2"."code", "school_2"."code" FROM "ad"."school" AS "school_1" LEFT OUTER JOIN (SELECT "school"."code" FROM "ad"."school" AS "school" WHERE (NOT (POSITION(LOWER('art') IN LOWER("school"."name")) > 0))) AS "school_2" ON (("school_1"."code" = "school_2"."code")) ORDER BY 1 ASC
+    - uri: /school{name,count(department),this(){count(department)}?name!~'art'}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | school                                                                                       |
+        -+----------------------------------------------------------------------------------------------+-
+         | name                             | count(department) | this(){count(department)}?name!~'art' |
+        -+----------------------------------+-------------------+---------------------------------------+-
+         | School of Art and Design         |                 2 |                                       |
+         | School of Business               |                 3 |                                     3 |
+         | College of Education             |                 2 |                                     2 |
+         | School of Engineering            |                 4 |                                     4 |
+         | School of Arts, Letters, and the |                 5 |                                       |
+         : Humanities                       :                   :                                       :
+         | School of Modern Art             |                 0 |                                       |
+         | Musical School                   |                 4 |                                     4 |
+         | School of Natural Sciences       |                 4 |                                     4 |
+         | School of Continuing Studies     |                 0 |                                     0 |
+                                                                                                 (9 rows)
+
+         ----
+         /school{name,count(department),this(){count(department)}?name!~'art'}
+         SELECT "school_1"."name", COALESCE("department"."!", 0), "school_2"."!" FROM "ad"."school" AS "school_1" LEFT OUTER JOIN (SELECT "department"."school", COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department" GROUP BY 1) AS "department" ON (("school_1"."code" = "department"."school")) LEFT OUTER JOIN (SELECT COALESCE("department"."!", 0) AS "!", "school"."code" FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT (TRUE IS NOT DISTINCT FROM NULL)), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) WHERE (NOT (POSITION(LOWER('art') IN LOWER("school"."name")) > 0))) AS "school_2" ON (("school_1"."code" = "school_2"."code")) ORDER BY "school_1"."code" ASC
+    - uri: /school{name,exists(department),this(){exists(department)}?name!~'art'}
+      status: 200 OK
+      headers:
+      - [Content-Type, text/plain; charset=UTF-8]
+      body: |2
+         | school                                                                                         |
+        -+------------------------------------------------------------------------------------------------+-
+         | name                             | exists(department) | this(){exists(department)}?name!~'art' |
+        -+----------------------------------+--------------------+----------------------------------------+-
+         | School of Art and Design         | true               |                                        |
+         | School of Business               | true               | true                                   |
+         | College of Education             | true               | true                                   |
+         | School of Engineering            | true               | true                                   |
+         | School of Arts, Letters, and the | true               |                                        |
+         : Humanities                       :                    :                                        :
+         | School of Modern Art             | false              |                                        |
+         | Musical School                   | true               | true                                   |
+         | School of Natural Sciences       | true               | true                                   |
+         | School of Continuing Studies     | false              | false                                  |
+                                                                                                   (9 rows)
+
+         ----
+         /school{name,exists(department),this(){exists(department)}?name!~'art'}
+         SELECT "school_1"."name", EXISTS((SELECT TRUE FROM "ad"."department" AS "department" WHERE ((NOT (TRUE IS NOT DISTINCT FROM NULL)) AND ("school_1"."code" = "department"."school")))), "school_2"."!" FROM "ad"."school" AS "school_1" LEFT OUTER JOIN (SELECT EXISTS((SELECT TRUE FROM "ad"."department" AS "department" WHERE ((NOT (TRUE IS NOT DISTINCT FROM NULL)) AND ("school"."code" = "department"."school")))) AS "!", "school"."code" FROM "ad"."school" AS "school" WHERE (NOT (POSITION(LOWER('art') IN LOWER("school"."name")) > 0))) AS "school_2" ON (("school_1"."code" = "school_2"."code")) ORDER BY "school_1"."code" ASC
   - id: formatters
     tests:
     - uri: /school