Commits

Kirill Simonov  committed 483551e

Permit an identifier to produce multiple bindings.

  • Participants
  • Parent commits edb537c

Comments (0)

Files changed (8)

File src/htsql/tr/bind.py

                 for syntax, recipe in bare_group:
                     syntax = syntax.clone(mark=base.mark)
                     bind = BindByRecipe(recipe, syntax, self.state)
-                    bare_elements.append(bind())
+                    bare_elements.extend(bind())
                 self.state.pop_base()
         # Validate and specialize the domains of the elements.
         elements = []
                               len(self.syntax.arguments))
         if recipe is not None:
             bind = BindByRecipe(recipe, self.syntax, self.state)
-            bindings = [bind()]
+            bindings = list(bind())
         else:
             bindings = self.state.call(self.syntax)
         return bindings
                               len(self.syntax.arguments))
         if recipe is not None:
             bind = BindByRecipe(recipe, self.syntax, self.state)
-            bindings = [bind()]
+            bindings = list(bind())
         else:
             bindings = self.state.call(self.syntax, base)
         self.state.pop_base()
             raise BindError("unable to resolve an identifier",
                             self.syntax.mark)
         bind = BindByRecipe(recipe, self.syntax, self.state)
-        yield bind()
+        return bind()
 
 
 class BindWildcard(Bind):
         for syntax, recipe in group:
             syntax = syntax.clone(mark=self.syntax.mark)
             bind = BindByRecipe(recipe, syntax, self.state)
-            yield bind()
+            for binding in bind():
+                yield binding
 
 
 class BindComplement(Bind):
         if recipe is None:
             raise BindError("expected a quotient context", self.syntax.mark)
         bind = BindByRecipe(recipe, self.syntax, self.state)
-        yield bind()
+        return bind()
 
 
 class BindString(Bind):
     adapts(FreeTableRecipe)
 
     def __call__(self):
-        return FreeTableBinding(self.state.base, self.recipe.table,
-                                self.syntax)
+        yield FreeTableBinding(self.state.base, self.recipe.table,
+                               self.syntax)
 
 
 class BindByAttachedTable(BindByRecipe):
         binding = self.state.base
         for join in self.recipe.joins:
             binding = AttachedTableBinding(binding, join, self.syntax)
-        return binding
+        yield binding
 
 
 class BindByColumn(BindByRecipe):
         link = None
         if self.recipe.link is not None:
             bind = BindByRecipe(self.recipe.link, self.syntax, self.state)
-            link = bind()
-        return ColumnBinding(self.state.base, self.recipe.column,
-                             link, self.syntax)
+            links = list(bind())
+            if len(links) != 1:
+                raise BindError("unexpected selector or wildcard expression",
+                                syntax.mark)
+            link = links[0]
+        yield ColumnBinding(self.state.base, self.recipe.column,
+                            link, self.syntax)
 
 
 class BindByComplement(BindByRecipe):
 
     def __call__(self):
         syntax = self.recipe.seed.syntax.clone(mark=self.syntax.mark)
-        return ComplementBinding(self.state.base, self.recipe.seed, syntax)
+        yield ComplementBinding(self.state.base, self.recipe.seed, syntax)
 
 
 class BindByKernel(BindByRecipe):
     def __call__(self):
         binding = self.recipe.kernel[self.recipe.index]
         syntax = binding.syntax.clone(mark=self.syntax.mark)
-        return KernelBinding(self.state.base, self.recipe.index,
-                             binding.domain, syntax)
+        yield KernelBinding(self.state.base, self.recipe.index,
+                            binding.domain, syntax)
 
 
 class BindBySubstitution(BindByRecipe):
                 raise BindError("unable to resolve an identifier",
                                 self.syntax.mark)
             bind = BindByRecipe(recipe, self.syntax, self.state)
-            binding = bind()
+            bindings = list(bind())
+            if len(bindings) != 1:
+                raise BindError("unexpected selector or wildcard expression",
+                                syntax.mark)
+            binding = bindings[0]
             binding = DefinitionBinding(binding, self.recipe.subnames[0],
                                         self.recipe.subnames[1:],
                                         self.recipe.arguments,
                                         self.recipe.body,
                                         binding.syntax)
-            return binding
+            yield binding
+            return
         base = RedirectBinding(self.state.base, self.recipe.base,
                                self.state.base.syntax)
         if self.recipe.arguments is not None:
                                     self.syntax.arguments):
                 binding = self.state.bind(syntax)
                 base = AliasBinding(base, name, binding, base.syntax)
-        binding = self.state.bind(self.recipe.body, base=base)
-        binding = WrapperBinding(binding, self.syntax)
-        return binding
+        for binding in self.state.bind_all(self.recipe.body, base=base):
+            binding = WrapperBinding(binding, self.syntax)
+            yield binding
 
 
 class BindByBinding(BindByRecipe):
     adapts(BindingRecipe)
 
     def __call__(self):
-        return self.recipe.binding
+        yield self.recipe.binding
 
 
 class BindByAmbiguous(BindByRecipe):

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

         if recipe is None:
             raise BindError("unknown identifier", table.mark)
         bind = BindByRecipe(recipe, table, self.state)
-        binding = bind()
+        bindings = list(bind())
+        if len(bindings) != 1:
+            raise BindError("unexpected selector or wildcard expression",
+                            syntax.mark)
+        binding = bindings[0]
         if image is None and counterimage is None:
             yield WrapperBinding(binding, self.syntax)
             return
             syntax, recipe = group[index]
             syntax = syntax.clone(mark=self.syntax.mark)
             bind = BindByRecipe(recipe, syntax, self.state)
-            binding = bind()
-            yield binding
+            for binding in bind():
+                yield binding
         else:
             for syntax, recipe in group:
                 syntax = syntax.clone(mark=self.syntax.mark)
                 bind = BindByRecipe(recipe, syntax, self.state)
-                binding = bind()
-                yield binding
+                for binding in bind():
+                    yield binding
 
 
 class BindComplement(BindMacro):
         if recipe is None:
             raise BindError("expected a quotient context", self.syntax.mark)
         bind = BindByRecipe(recipe, self.syntax, self.state)
-        binding = bind()
-        yield binding
+        return bind()
 
 
 class BindAs(BindMacro):

File test/regress/input/translation.yaml

   - title: Table Expressions
     tests:
     - uri: /(school?code='art').department
+    # An empty segment
+    - uri: /{}
+      expect: 400
+    - uri: /school{}
+      expect: 400
 
   - title: Assignments
     tests:
                          count(student?gender='m'),
                          count(student?gender='f')}
                         :where(student:=student?is_active)}
+    - uri: /department.define(stats(c):={min(c.credits) :as min,max(c.credits) :as max})
+                {stats(course), stats(course?no>=100&no<200)}?code='acc'
 
   - title: Projections
     tests:

File test/regress/output/mssql.yaml

                                ON ([school].[code] = [department].[school])
                WHERE ([school].[code] = 'art')
                ORDER BY [school].[code] ASC, 1 ASC
+          - uri: /{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /{}
+                   ^^
+          - uri: /school{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /school{}
+                   ^^^^^^^^
         - id: assignments
           tests:
           - uri: /school.define(c:=department.course.credits) {code,min(c),max(c),sum(c),avg(c)}?exists(c)
                                      GROUP BY [student].[school]) AS [student_3]
                                     ON ([school].[code] = [student_3].[school])
                ORDER BY 1 ASC
+          - uri: /department.define(stats(c):={min(c.credits) :as min,max(c.credits)
+              :as max}) {stats(course), stats(course?no>=100&no<200)}?code='acc'
+            status: 200 OK
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |2
+               | (department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)})?code='acc') |
+              -+-------------------------------------------------------------------------------------------+-
+               | min                  | max                  | min                  | max                  |
+              -+----------------------+----------------------+----------------------+----------------------+-
+               |                    2 |                    5 |                    2 |                    2 |
+                                                                                                     (1 row)
+
+               ----
+               /department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)}){stats(course),stats(course?no>=100&no<200)}?code='acc'
+               SELECT [course_1].[min],
+                      [course_1].[max],
+                      [course_2].[min],
+                      [course_2].[max]
+               FROM [ad].[department] AS [department]
+                    LEFT OUTER JOIN (SELECT MIN([course].[credits]) AS [min],
+                                            MAX([course].[credits]) AS [max],
+                                            [course].[department]
+                                     FROM [ad].[course] AS [course]
+                                     GROUP BY [course].[department]) AS [course_1]
+                                    ON ([department].[code] = [course_1].[department])
+                    LEFT OUTER JOIN (SELECT MIN([course].[credits]) AS [min],
+                                            MAX([course].[credits]) AS [max],
+                                            [course].[department]
+                                     FROM [ad].[course] AS [course]
+                                     WHERE ([course].[no] >= 100)
+                                           AND ([course].[no] < 200)
+                                     GROUP BY [course].[department]) AS [course_2]
+                                    ON ([department].[code] = [course_2].[department])
+               WHERE ([department].[code] = 'acc')
+               ORDER BY [department].[code] ASC
         - id: projections
           tests:
           - uri: /quotient(program, degree)

File test/regress/output/mysql.yaml

                                ON (`school`.`code` = `department`.`school`)
                WHERE (`school`.`code` = 'art')
                ORDER BY `school`.`code` ASC, 1 ASC
+          - uri: /{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /{}
+                   ^^
+          - uri: /school{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /school{}
+                   ^^^^^^^^
         - id: assignments
           tests:
           - uri: /school.define(c:=department.course.credits) {code,min(c),max(c),sum(c),avg(c)}?exists(c)
                                      GROUP BY 2) AS `student_3`
                                     ON (`school`.`code` = `student_3`.`school`)
                ORDER BY 1 ASC
+          - uri: /department.define(stats(c):={min(c.credits) :as min,max(c.credits)
+              :as max}) {stats(course), stats(course?no>=100&no<200)}?code='acc'
+            status: 200 OK
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |2
+               | (department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)})?code='acc') |
+              -+-------------------------------------------------------------------------------------------+-
+               | min                  | max                  | min                  | max                  |
+              -+----------------------+----------------------+----------------------+----------------------+-
+               |                    2 |                    5 |                    2 |                    2 |
+                                                                                                     (1 row)
+
+               ----
+               /department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)}){stats(course),stats(course?no>=100&no<200)}?code='acc'
+               SELECT `course_1`.`min`,
+                      `course_1`.`max`,
+                      `course_2`.`min`,
+                      `course_2`.`max`
+               FROM `htsql_regress`.`department` AS `department`
+                    LEFT OUTER JOIN (SELECT MIN(`course`.`credits`) AS `min`,
+                                            MAX(`course`.`credits`) AS `max`,
+                                            `course`.`department`
+                                     FROM `htsql_regress`.`course` AS `course`
+                                     GROUP BY 3) AS `course_1`
+                                    ON (`department`.`code` = `course_1`.`department`)
+                    LEFT OUTER JOIN (SELECT MIN(`course`.`credits`) AS `min`,
+                                            MAX(`course`.`credits`) AS `max`,
+                                            `course`.`department`
+                                     FROM `htsql_regress`.`course` AS `course`
+                                     WHERE (`course`.`no` >= 100)
+                                           AND (`course`.`no` < 200)
+                                     GROUP BY 3) AS `course_2`
+                                    ON (`department`.`code` = `course_2`.`department`)
+               WHERE (`department`.`code` = 'acc')
+               ORDER BY `department`.`code` ASC
         - id: projections
           tests:
           - uri: /quotient(program, degree)

File test/regress/output/oracle.yaml

                                ON ("SCHOOL"."CODE" = "DEPARTMENT"."SCHOOL")
                WHERE ("SCHOOL"."CODE" = 'art')
                ORDER BY "SCHOOL"."CODE" ASC, 1 ASC
+          - uri: /{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /{}
+                   ^^
+          - uri: /school{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /school{}
+                   ^^^^^^^^
         - id: assignments
           tests:
           - uri: /school.define(c:=department.course.credits) {code,min(c),max(c),sum(c),avg(c)}?exists(c)
                                      GROUP BY "STUDENT"."SCHOOL") "STUDENT_3"
                                     ON ("SCHOOL"."CODE" = "STUDENT_3"."SCHOOL")
                ORDER BY 1 ASC
+          - uri: /department.define(stats(c):={min(c.credits) :as min,max(c.credits)
+              :as max}) {stats(course), stats(course?no>=100&no<200)}?code='acc'
+            status: 200 OK
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |2
+               | (department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)})?code='acc') |
+              -+-------------------------------------------------------------------------------------------+-
+               | min                  | max                  | min                  | max                  |
+              -+----------------------+----------------------+----------------------+----------------------+-
+               |                    2 |                    5 |                    2 |                    2 |
+                                                                                                     (1 row)
+
+               ----
+               /department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)}){stats(course),stats(course?no>=100&no<200)}?code='acc'
+               SELECT "COURSE_1"."min",
+                      "COURSE_1"."max",
+                      "COURSE_2"."min",
+                      "COURSE_2"."max"
+               FROM "HTSQL_REGRESS"."DEPARTMENT" "DEPARTMENT"
+                    LEFT OUTER JOIN (SELECT MIN("COURSE"."CREDITS") AS "min",
+                                            MAX("COURSE"."CREDITS") AS "max",
+                                            "COURSE"."DEPARTMENT"
+                                     FROM "HTSQL_REGRESS"."COURSE" "COURSE"
+                                     GROUP BY "COURSE"."DEPARTMENT") "COURSE_1"
+                                    ON ("DEPARTMENT"."CODE" = "COURSE_1"."DEPARTMENT")
+                    LEFT OUTER JOIN (SELECT MIN("COURSE"."CREDITS") AS "min",
+                                            MAX("COURSE"."CREDITS") AS "max",
+                                            "COURSE"."DEPARTMENT"
+                                     FROM "HTSQL_REGRESS"."COURSE" "COURSE"
+                                     WHERE ("COURSE"."NO" >= 100)
+                                           AND ("COURSE"."NO" < 200)
+                                     GROUP BY "COURSE"."DEPARTMENT") "COURSE_2"
+                                    ON ("DEPARTMENT"."CODE" = "COURSE_2"."DEPARTMENT")
+               WHERE ("DEPARTMENT"."CODE" = 'acc')
+               ORDER BY "DEPARTMENT"."CODE" ASC
         - id: projections
           tests:
           - uri: /quotient(program, degree)

File test/regress/output/pgsql.yaml

                                ON ("school"."code" = "department"."school")
                WHERE ("school"."code" = 'art')
                ORDER BY "school"."code" ASC, 1 ASC
+          - uri: /{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /{}
+                   ^^
+          - uri: /school{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /school{}
+                   ^^^^^^^^
         - id: assignments
           tests:
           - uri: /school.define(c:=department.course.credits) {code,min(c),max(c),sum(c),avg(c)}?exists(c)
                                      GROUP BY 2) AS "student_3"
                                     ON ("school"."code" = "student_3"."school")
                ORDER BY 1 ASC
+          - uri: /department.define(stats(c):={min(c.credits) :as min,max(c.credits)
+              :as max}) {stats(course), stats(course?no>=100&no<200)}?code='acc'
+            status: 200 OK
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |2
+               | (department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)})?code='acc') |
+              -+-------------------------------------------------------------------------------------------+-
+               | min                  | max                  | min                  | max                  |
+              -+----------------------+----------------------+----------------------+----------------------+-
+               |                    2 |                    5 |                    2 |                    2 |
+                                                                                                     (1 row)
+
+               ----
+               /department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)}){stats(course),stats(course?no>=100&no<200)}?code='acc'
+               SELECT "course_1"."min",
+                      "course_1"."max",
+                      "course_2"."min",
+                      "course_2"."max"
+               FROM "ad"."department" AS "department"
+                    LEFT OUTER JOIN (SELECT MIN("course"."credits") AS "min",
+                                            MAX("course"."credits") AS "max",
+                                            "course"."department"
+                                     FROM "ad"."course" AS "course"
+                                     GROUP BY 3) AS "course_1"
+                                    ON ("department"."code" = "course_1"."department")
+                    LEFT OUTER JOIN (SELECT MIN("course"."credits") AS "min",
+                                            MAX("course"."credits") AS "max",
+                                            "course"."department"
+                                     FROM "ad"."course" AS "course"
+                                     WHERE ("course"."no" >= 100)
+                                           AND ("course"."no" < 200)
+                                     GROUP BY 3) AS "course_2"
+                                    ON ("department"."code" = "course_2"."department")
+               WHERE ("department"."code" = 'acc')
+               ORDER BY "department"."code" ASC
         - id: projections
           tests:
           - uri: /quotient(program, degree)

File test/regress/output/sqlite.yaml

                                ON ("school"."code" = "department"."school")
                WHERE ("school"."code" = 'art')
                ORDER BY "school"."code" ASC, 1 ASC
+          - uri: /{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /{}
+                   ^^
+          - uri: /school{}
+            status: 400 Bad Request
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |
+              bind error: empty selector:
+                  /school{}
+                   ^^^^^^^^
         - id: assignments
           tests:
           - uri: /school.define(c:=department.course.credits) {code,min(c),max(c),sum(c),avg(c)}?exists(c)
                                      GROUP BY 2) AS "student_3"
                                     ON ("school"."code" = "student_3"."school")
                ORDER BY 1 ASC
+          - uri: /department.define(stats(c):={min(c.credits) :as min,max(c.credits)
+              :as max}) {stats(course), stats(course?no>=100&no<200)}?code='acc'
+            status: 200 OK
+            headers:
+            - [Content-Type, text/plain; charset=UTF-8]
+            body: |2
+               | (department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)})?code='acc') |
+              -+-------------------------------------------------------------------------------------------+-
+               | min                  | max                  | min                  | max                  |
+              -+----------------------+----------------------+----------------------+----------------------+-
+               |                    2 |                    5 |                    2 |                    2 |
+                                                                                                     (1 row)
+
+               ----
+               /department.define(stats(c):={min(c.credits):as(min),max(c.credits):as(max)}){stats(course),stats(course?no>=100&no<200)}?code='acc'
+               SELECT "course_1"."min",
+                      "course_1"."max",
+                      "course_2"."min",
+                      "course_2"."max"
+               FROM "department" AS "department"
+                    LEFT OUTER JOIN (SELECT MIN("course"."credits") AS "min",
+                                            MAX("course"."credits") AS "max",
+                                            "course"."department"
+                                     FROM "course" AS "course"
+                                     GROUP BY 3) AS "course_1"
+                                    ON ("department"."code" = "course_1"."department")
+                    LEFT OUTER JOIN (SELECT MIN("course"."credits") AS "min",
+                                            MAX("course"."credits") AS "max",
+                                            "course"."department"
+                                     FROM "course" AS "course"
+                                     WHERE ("course"."no" >= 100)
+                                           AND ("course"."no" < 200)
+                                     GROUP BY 3) AS "course_2"
+                                    ON ("department"."code" = "course_2"."department")
+               WHERE ("department"."code" = 'acc')
+               ORDER BY "department"."code" ASC
         - id: projections
           tests:
           - uri: /quotient(program, degree)