Commits

Kirill Simonov committed 4897c91

Introduced a scalar unit: a scalar function defined on a given space.

Use scalar units to wrap aggregate functions; this should fix
queries like `/department{code,school.count(program)}`. Unfortunately,
a bug in Postgresql optimizer makes it produce incorrect output
(see http://archives.postgresql.org/pgsql-bugs/2010-09/msg00265.php).

Comments (0)

Files changed (5)

src/htsql/tr/assemble.py

 from ..adapter import Adapter, adapts
 from .code import (Expression, Code, Space, ScalarSpace, ProductSpace,
                    FilteredSpace, OrderedSpace, MaskedSpace,
-                   Unit, ColumnUnit, AggregateUnit, CorrelatedUnit,
+                   Unit, ScalarUnit, ColumnUnit, AggregateUnit, CorrelatedUnit,
                    QueryExpression, SegmentExpression,
                    GroupExpression, AggregateGroupExpression)
 from .term import (RoutingTerm, ScalarTerm, TableTerm, FilterTerm, JoinTerm,
         return self.state.inject(self.term, [self.unit.space])
 
 
+class InjectScalar(Inject):
+
+    adapts(ScalarUnit)
+
+    def __call__(self):
+        if not self.unit.singular(self.term.space):
+            raise AssembleError("expected a singular expression",
+                                self.unit.mark)
+        if self.unit in self.term.routes:
+            return self.term
+        if self.space.dominates(self.term.space):
+            term = self.state.inject(self.term, [self.unit.expression])
+            if term.is_nullary:
+                term = WrapperTerm(self.state.make_id(), term,
+                                   term.space, term.routes.copy())
+            routes = term.routes.copy()
+            routes[self.unit] = term.id
+            return term.clone(routes=routes)
+        lkid = self.term
+        baseline = self.space.prune(self.term.space)
+        while not baseline.is_inflated:
+            baseline = baseline.base
+        rkid = self.state.assemble(self.space,
+                                   baseline=baseline,
+                                   mask=self.term.space)
+        rkid = self.state.inject(rkid, [self.unit.expression])
+        if rkid.is_nullary:
+            rkid = WrapperTerm(self.state.make_id(), rkid,
+                               rkid.space, rkid.routes.copy())
+        id = self.state.make_id()
+        ties = []
+        if lkid.backbone.concludes(rkid.baseline):
+            lkid = self.state.inject(lkid, [rkid.baseline])
+            axis = lkid.backbone
+            while rkid.baseline.base != axis:
+                if axis in rkid.routes:
+                    tie = ParallelTie(axis)
+                    ties.append(tie)
+            ties.reverse()
+        else:
+            lkid = self.state.inject(lkid, [rkid.baseline.base])
+            tie = SeriesTie(rkid.baseline)
+            ties.append(tie)
+        routes = lkid.routes.copy()
+        routes[self.unit] = rkid.id
+        return JoinTerm(id, lkid, rkid, ties, False, lkid.space, routes)
+
+
 class InjectAggregate(Inject):
 
     adapts(AggregateUnit)
                                           baseline=baseline,
                                           mask=ground_term.space)
         plural_term = self.state.inject(plural_term, [self.unit.composite])
-        plural_term = WrapperTerm(self.state.make_id(), plural_term,
-                                  plural_term.space, plural_term.routes.copy())
+        if plural_term.is_nullary:
+            plural_term = WrapperTerm(self.state.make_id(), plural_term,
+                                      plural_term.space,
+                                      plural_term.routes.copy())
         ties = []
         axes = []
         if ground_term.backbone.concludes(plural_term.baseline):

src/htsql/tr/code.py

     see its subclasses for concrete types of expressions.
 
     Among all code expressions, we distinguish *unit expressions*:
-    elementary functions on spaces.  There are two kinds of units:
-    columns and aggregate functions (see :class:`Unit` for more detail).
-    Every non-unit code could be expressed as a composition of a scalar
-    function and one or several units:
+    elementary functions on spaces.  There are several kinds of units:
+    among them are columns and aggregate functions (see :class:`Unit`
+    for more detail).  Every non-unit code could be expressed as
+    a composition of a scalar function and one or several units:
 
         `f = F(u(a),v(b),...)`,
 
     """
     Represents a unit expression.
 
-    A unit is an elementary function on a space.  There are two kinds
-    of units: columns and aggregates; see subclasses :class:`ColumnUnit`,
+    A unit is an elementary function on a space.  There are several kinds
+    of units; see subclasses :class:`ColumnUnit`, :class:`ScalarUnit`,
     :class:`AggregateUnit`, and :class:`CorrelatedUnit` for more detail.
 
     Note that it is easy to *trasfer* a unit code from one space to another.
         self.column = column
 
 
+class ScalarUnit(Unit):
+    """
+    Represents a scalar unit.
+
+    A scalar unit is a scalar function evaluated in the specified space.
+
+    `expression` (:class:`Code`)
+        The expression to evaluate.
+
+    `space` (:class:`Space`)
+        The space on which the unit is defined.
+    """
+
+    def __init__(self, expression, space, binding):
+        assert isinstance(expression, Code)
+        super(ScalarUnit, self).__init__(
+                    space=space,
+                    domain=expression.domain,
+                    binding=binding,
+                    equality_vector=(expression, space))
+        self.expression = expression
+
+
 class AggregateUnitBase(Unit):
     """
     Represents an aggregate unit.

src/htsql/tr/fn/function.py

                        ConjunctionBinding, DisjunctionBinding, NegationBinding,
                        CastBinding, TitleBinding, DirectionBinding)
 from ..encode import Encode
-from ..code import (FunctionCode, NegationCode, AggregateUnit,
+from ..code import (FunctionCode, NegationCode, ScalarUnit, AggregateUnit,
                     CorrelatedUnit, LiteralCode, FilteredSpace)
 from ..compiler import Compiler, Evaluate
 from ..frame import FunctionPhrase
                                   self.binding)
         wrapper = self.wrapper_class(self.binding.domain, self.binding,
                                      expression=aggregate)
+        wrapper = ScalarUnit(wrapper, space, self.binding)
         return wrapper
 
 
             wrapper = EveryWrapperExpression(self.binding.domain,
                                              self.binding,
                                              expression=aggregate)
+        wrapper = ScalarUnit(wrapper, space, self.binding)
         return wrapper
 
 

src/htsql/tr/outliner.py

                    JoinTerm, CorrelationTerm, ProjectionTerm, OrderTerm,
                    WrapperTerm, SegmentTerm, QueryTerm,
                    Tie, ParallelTie, SeriesTie)
-from .code import (Unit, ColumnUnit, AggregateUnit, CorrelatedUnit,
+from .code import (Unit, ColumnUnit, ScalarUnit, AggregateUnit, CorrelatedUnit,
                    Space, ScalarSpace, CrossProductSpace, JoinProductSpace)
 from .sketch import (Sketch, LeafSketch, ScalarSketch, BranchSketch,
                      SegmentSketch, QuerySketch, Demand, LeafAppointment,
         return Demand(sketch, appointment)
 
 
+class DelegateScalar(Delegate):
+
+    adapts(ScalarUnit, Outliner)
+
+    def delegate(self, term):
+        term_id = term.routes[self.unit]
+        sketch = self.outliner.sketch_by_term_id[term_id]
+        term = self.outliner.term_by_term_id[term_id]
+        appointment = self.outliner.appoint(self.unit.expression, term)
+        return Demand(sketch, appointment)
+
+
 class DelegateAggregate(Delegate):
 
     adapts(AggregateUnit, Outliner)

test/output/pgsql.yaml

 
          ----
          /department{code,count(course{credits=3})}
-         SELECT "department"."code", COALESCE("course"."!", 0) FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT COUNT(NULLIF(("course"."credits" = 3), FALSE)) AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course" ON (("department"."code" = "course"."department")) ORDER BY 1 ASC
+         SELECT "department"."code", COALESCE("course"."!", 0) FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", COUNT(NULLIF(("course"."credits" = 3), FALSE)) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course" ON (("department"."code" = "course"."department")) ORDER BY 1 ASC
     - uri: /school{code,count(department.course{credits=3})}
       status: 200 OK
       headers:
 
          ----
          /school{code,count(department.course{credits=3})}
-         SELECT "school"."code", COALESCE("course"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF(("course"."credits" = 3), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" INNER JOIN "ad"."course" AS "course" ON (("department"."code" = "course"."department")) GROUP BY 2) AS "course" ON (("school"."code" = "course"."school")) ORDER BY 1 ASC
+         SELECT "school"."code", COALESCE("course"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT "department"."school", COUNT(NULLIF(("course"."credits" = 3), FALSE)) AS "!" FROM "ad"."department" AS "department" INNER JOIN "ad"."course" AS "course" ON (("department"."code" = "course"."department")) GROUP BY 1) AS "course" ON (("school"."code" = "course"."school")) ORDER BY 1 ASC
     - uri: /school{code}?count(department.course{credits=3})=20
       status: 200 OK
       headers:
 
          ----
          /department{code,min(course.credits),max(course.credits)}
-         SELECT "department"."code", "course_1"."!", "course_2"."!" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT MIN("course"."credits") AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_1" ON (("department"."code" = "course_1"."department")) LEFT OUTER JOIN (SELECT MAX("course"."credits") AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_2" ON (("department"."code" = "course_2"."department")) ORDER BY 1 ASC
+         SELECT "department"."code", "course_1"."!", "course_2"."!" FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", MIN("course"."credits") AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course_1" ON (("department"."code" = "course_1"."department")) LEFT OUTER JOIN (SELECT "course"."department", MAX("course"."credits") AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course_2" ON (("department"."code" = "course_2"."department")) ORDER BY 1 ASC
     - uri: /department{code,avg(course.credits), sum(course.credits)/count(course.credits)}
       status: 200 OK
       headers:
 
          ----
          /department{code,avg(course.credits),sum(course.credits)/count(course.credits)}
-         SELECT "department"."code", "course_1"."!", (CAST("course_2"."!" AS NUMERIC) / CAST(COALESCE("course_3"."!", 0) AS NUMERIC)) FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT AVG(CAST("course"."credits" AS NUMERIC)) AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_1" ON (("department"."code" = "course_1"."department")) LEFT OUTER JOIN (SELECT SUM("course"."credits") AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_2" ON (("department"."code" = "course_2"."department")) LEFT OUTER JOIN (SELECT COUNT(NULLIF(("course"."credits" IS NOT NULL), FALSE)) AS "!", "course"."department" FROM "ad"."course" AS "course" GROUP BY 2) AS "course_3" ON (("department"."code" = "course_3"."department")) ORDER BY 1 ASC
+         SELECT "department"."code", "course_1"."!", (CAST("course_2"."!" AS NUMERIC) / CAST(COALESCE("course_3"."!", 0) AS NUMERIC)) FROM "ad"."department" AS "department" LEFT OUTER JOIN (SELECT "course"."department", AVG(CAST("course"."credits" AS NUMERIC)) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course_1" ON (("department"."code" = "course_1"."department")) LEFT OUTER JOIN (SELECT "course"."department", SUM("course"."credits") AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course_2" ON (("department"."code" = "course_2"."department")) LEFT OUTER JOIN (SELECT "course"."department", COUNT(NULLIF(("course"."credits" IS NOT NULL), FALSE)) AS "!" FROM "ad"."course" AS "course" GROUP BY 1) AS "course_3" ON (("department"."code" = "course_3"."department")) ORDER BY 1 ASC
     - uri: /department?exists(course)
       status: 200 OK
       headers:
 
          ----
          /school{*,count(department)}
-         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT ("department"."code" IS NOT DISTINCT FROM NULL)), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
+         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT "department"."school", COUNT(NULLIF((NOT ("department"."code" IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department" GROUP BY 1) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
     - uri: /school{*,count(department?exists(course))}
       status: 200 OK
       headers:
 
          ----
          /school{*,count(department?exists(course))}
-         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF((NOT ("department"."code" IS NOT DISTINCT FROM NULL)), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" WHERE EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((NOT ("course"."department" IS NOT DISTINCT FROM NULL)) AND ("department"."code" = "course"."department")))) GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
+         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT "department"."school", COUNT(NULLIF((NOT ("department"."code" IS NOT DISTINCT FROM NULL)), FALSE)) AS "!" FROM "ad"."department" AS "department" WHERE EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((NOT ("course"."department" IS NOT DISTINCT FROM NULL)) AND ("department"."code" = "course"."department")))) GROUP BY 1) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
     - uri: /school{*,count(department.exists(course))}
       status: 200 OK
       headers:
 
          ----
          /school{*,count(department.exists(course))}
-         SELECT "school"."code", "school"."name", COALESCE("department"."!", 0) FROM "ad"."school" AS "school" LEFT OUTER JOIN (SELECT COUNT(NULLIF(EXISTS((SELECT TRUE FROM "ad"."course" AS "course" WHERE ((NOT ("course"."department" IS NOT DISTINCT FROM NULL)) AND ("department"."code" = "course"."department")))), FALSE)) AS "!", "department"."school" FROM "ad"."department" AS "department" GROUP BY 2) AS "department" ON (("school"."code" = "department"."school")) ORDER BY 1 ASC
+         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 ("course"."department" 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: formatters
     tests:
     - uri: /school
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.