Commits

Kirill Simonov committed ed8b514

Updated parse error messages.

  • Participants
  • Parent commits c5a61cd

Comments (0)

Files changed (8)

File src/htsql/tr/parse.py

                      AssignmentSyntax, SpecifierSyntax, GroupSyntax,
                      IdentifierSyntax, WildcardSyntax, ComplementSyntax,
                      ReferenceSyntax, StringSyntax, NumberSyntax)
+from .error import ParseError
 
 
 class Parser(object):
         # Parse the input query.
         syntax = self.process(tokens)
         # Ensure that we reached the end of the token stream.
-        tokens.pop(EndToken)
+        if not tokens.pop(EndToken, do_force=False):
+            token = tokens.pop()
+            raise ParseError("expected the end of query; got '%s'" % token,
+                             token.mark)
         return syntax
 
     @classmethod
         # Expect:
         #   segment     ::= '/' ( top command* )?
         #   command     ::= '/' ':' identifier ( '/' top? | call | flow )?
-        head_token = tokens.pop(SymbolToken, [u'/'])
+        head_token = tokens.pop(SymbolToken, [u'/'], do_force=False)
+        if not head_token:
+            token = tokens.pop()
+            raise ParseError("query must start with symbol '/'", token.mark)
         branch = None
         if not (tokens.peek(EndToken) or
                 tokens.peek(SymbolToken, [u',', u')', u'}'])):
                 tail_token = tokens.pop(SymbolToken, [u'/'])
                 if not (tokens.peek(EndToken) or
                         tokens.peek(SymbolToken, [u',', u')', u'}'])):
-                    tokens.pop(SymbolToken, [u':'])
+                    if not (tokens.pop(SymbolToken, [u':'], do_force=False) and
+                            tokens.peek(NameToken)):
+                        raise ParseError("symbol '/' must be followed by ':'"
+                                         " and an identifier", tail_token.mark)
                     mark = Mark.union(head_token, branch)
                     lbranch = SegmentSyntax(branch, mark)
                     identifier = IdentifierParser << tokens
                                 rbranch = SegmentSyntax(rbranch, mark)
                                 rbranches.append(rbranch)
                         elif tokens.peek(SymbolToken, [u'(']):
-                            tokens.pop(SymbolToken, [u'('])
+                            open_token = tokens.pop(SymbolToken, [u'('])
                             while not tokens.peek(SymbolToken, [u')']):
                                 if tokens.peek(SymbolToken, [u'/']):
                                     rbranch = SegmentParser << tokens
                                 else:
                                     rbranch = TopParser << tokens
                                 rbranches.append(rbranch)
-                                if not tokens.peek(SymbolToken, [u')']):
-                                    tokens.pop(SymbolToken, [u',', u')'])
+                                if tokens.peek(SymbolToken, [u',']):
+                                    tokens.pop(SymbolToken, [u','])
+                                elif not tokens.peek(SymbolToken, [u')']):
+                                    mark = Mark.union(open_token, *rbranches)
+                                    raise ParseError("cannot find a matching"
+                                                     " ')'", mark)
                             tail_token = tokens.pop(SymbolToken, [u')'])
                         else:
                             rbranch = FlowParser << tokens
             # Parse `mapping` application.
             else:
                 symbol_token = tokens.pop(SymbolToken, [u':'])
+                if not tokens.peek(NameToken):
+                    raise ParseError("symbol ':' must be followed by"
+                                     " an identifier", symbol_token.mark)
                 identifier = IdentifierParser << tokens
                 lbranch = top
                 rbranches = []
                 # Mapping parameters in parentheses.
                 if tokens.peek(SymbolToken, [u'(']):
-                    tokens.pop(SymbolToken, [u'('])
+                    open_token = tokens.pop(SymbolToken, [u'('])
                     while not tokens.peek(SymbolToken, [u')']):
                         if tokens.peek(SymbolToken, [u'/']):
                             rbranch = SegmentParser << tokens
                         else:
                             rbranch = TopParser << tokens
                         rbranches.append(rbranch)
-                        if not tokens.peek(SymbolToken, [u')']):
-                            tokens.pop(SymbolToken, [u',', u')'])
+                        if tokens.peek(SymbolToken, [u',']):
+                            tokens.pop(SymbolToken, [u','])
+                        elif not tokens.peek(SymbolToken, [u')']):
+                            mark = Mark.union(open_token, *rbranches)
+                            raise ParseError("cannot find a matching ')'",
+                                             mark)
                     tail_token = tokens.pop(SymbolToken, [u')'])
                     mark = Mark.union(top, tail_token)
                 # No parenthesis: either no parameters or a single parameter.
                 rbranch = DisjunctionParser << tokens
                 mark = Mark.union(lbranch, rbranch)
                 flow = QuotientSyntax(lbranch, rbranch, mark)
-            elif tokens.peek(SymbolToken, [u'{'], do_pop=True):
+            elif tokens.peek(SymbolToken, [u'{']):
+                open_token = tokens.pop(SymbolToken, [u'{'])
                 lbranch = flow
                 rbranches = []
                 while not tokens.peek(SymbolToken, [u'}']):
                     else:
                         rbranch = TopParser << tokens
                     rbranches.append(rbranch)
-                    if not tokens.peek(SymbolToken, [u'}']):
-                        # We know it's not going to be '}', but we put it into
-                        # the list of accepted values to generate a better
-                        # error message.
-                        tokens.pop(SymbolToken, [u',', u'}'])
+                    if tokens.peek(SymbolToken, [u',']):
+                        tokens.pop(SymbolToken, [u','])
+                    elif not tokens.peek(SymbolToken, [u'}']):
+                        mark = Mark.union(open_token, *rbranches)
+                        raise ParseError("cannot find a matching '}'",
+                                         mark)
                 tail_token = tokens.pop(SymbolToken, [u'}'])
                 mark = Mark.union(flow, tail_token)
                 flow = SelectorSyntax(lbranch, rbranches, mark)
         elif tokens.peek(NameToken):
             identifier = IdentifierParser << tokens
             if tokens.peek(SymbolToken, [u'(']):
-                tokens.pop(SymbolToken, [u'('])
+                open_token = tokens.pop(SymbolToken, [u'('])
                 branches = []
                 while not tokens.peek(SymbolToken, [u')']):
                     if tokens.peek(SymbolToken, [u'/']):
                     else:
                         rbranch = TopParser << tokens
                     branches.append(rbranch)
-                    if not tokens.peek(SymbolToken, [u')']):
-                        tokens.pop(SymbolToken, [u',', u')'])
+                    if tokens.peek(SymbolToken, [u',']):
+                        tokens.pop(SymbolToken, [u','])
+                    elif not tokens.peek(SymbolToken, [u')']):
+                        mark = Mark.union(open_token, *branches)
+                        raise ParseError("cannot find a matching ')'", mark)
                 tail_token = tokens.pop(SymbolToken, [u')'])
                 mark = Mark.union(identifier, tail_token)
                 function = FunctionSyntax(identifier, branches, mark)
         # A reference.
         elif tokens.peek(SymbolToken, [u'$']):
             head_token = tokens.pop(SymbolToken, [u'$'])
+            if not tokens.peek(NameToken):
+                raise ParseError("symbol '$' must be followed by an identifier",
+                                 head_token.mark)
             identifier = IdentifierParser << tokens
             mark = Mark.union(head_token, identifier)
             reference = ReferenceSyntax(identifier, mark)
         elif tokens.peek(NumberToken):
             token = tokens.pop(NumberToken)
             return NumberSyntax(token.value, token.mark)
-        # We expect it to always produce an error message.
-        tokens.pop(NameToken)
+        # Can't find anything suitable; produce an error message.
+        token = tokens.pop()
+        if token.is_end:
+            raise ParseError("unexpected end of query", token.mark)
+        else:
+            raise ParseError("unexpected symbol '%s'" % token, token.mark)
         # Not reachable.
         assert False
 
         #   group       ::= '(' top ')'
         head_token = tokens.pop(SymbolToken, [u'('])
         branch = TopParser << tokens
-        tail_token = tokens.pop(SymbolToken, [u')'])
+        tail_token = tokens.pop(SymbolToken, [u')'], do_force=False)
+        if tail_token is None:
+            mark = Mark.union(head_token, branch)
+            raise ParseError("cannot find a matching ')'", mark)
         mark = Mark.union(head_token, tail_token)
         group = GroupSyntax(branch, mark)
         return group
             else:
                 rbranch = TopParser << tokens
             branches.append(rbranch)
-            if not tokens.peek(SymbolToken, [u'}']):
-                # We know it's not going to be '}', but we put it into the list
-                # of accepted values to generate a better error message.
-                tokens.pop(SymbolToken, [u',', u'}'])
+            if tokens.peek(SymbolToken, [u',']):
+                tokens.pop(SymbolToken, [u','])
+            elif not tokens.peek(SymbolToken, [u'}']):
+                mark = Mark.union(head_token, *branches)
+                raise ParseError("cannot find a matching '}'", mark)
         tail_token = tokens.pop(SymbolToken, [u'}'])
         mark = Mark.union(head_token, tail_token)
         selector = SelectorSyntax(None, branches, mark)

File src/htsql/tr/scan.py

             expected = "%s" % token_class.name.upper()
             if values:
                 if len(values) == 1:
-                    expected = "%s %r" % (expected, values[0])
+                    expected = "%s %r" % (expected, values[0].encode('utf-8'))
                 else:
                     expected = "%s (%s)" % (expected,
-                                            ", ".join(repr(value)
-                                                      for value in values))
-            got = "%s %r" % (token.name.upper(), token.value)
+                                        ", ".join(repr(value.encode('utf-8'))
+                                                  for value in values))
+            got = "%s %r" % (token.name.upper(), token.value.encode('utf-8'))
             raise ParseError("expected %s; got %s" % (expected, got),
                              token.mark)
         # Advance the pointer.
 
         return token
 
-    def pop(self, token_class=None, values=None):
+    def pop(self, token_class=None, values=None, do_force=True):
         """
         Returns the active token and advances the pointer to the next token.
 
         `values` (a list of strings or ``None``)
             If not ``None``, the method checks that the value of the active
             token belongs to the list.
+
+        `do_force` (Boolean)
+            This flag affects the method behavior when any of the token
+            checks fail.  If set, the method will raise
+            :exc:`htsql.tr.error.ParseError`; otherwise it will return
+            ``None``.
         """
         return self.peek(token_class, values,
-                         do_pop=True, do_force=True)
+                         do_pop=True, do_force=do_force)
 
 
 class Scanner(object):
                 end = len(match.string[:end].decode('utf-8', 'ignore'))
                 mark = Mark(input, start, end)
                 raise ScanError("symbol '%' must be followed by two hexdecimal"
-                                " digits", mark,
-                                hint="use '%25' to represent '%' literally")
+                                " digits", mark)
             # Return the character corresponding to the escape sequence.
             return chr(int(code, 16))
 

File test/input/error.yaml

 
 - title: Scan Errors
   tests:
+  # % requires two hexdecimal digits
   - uri: /'?%@$'
     expect: 400
+  # invalid UTF-8 sequence
   - uri: /'%FF'
     expect: 400
+  # no matching quote
   - uri: /'Hello
     expect: 400
+  # unexpected symbol
   - uri: /`Hello'
     expect: 400
 
+- title: Parse Errors
+  tests:
+  # expected the query end
+  - uri: /'Hello','World'
+    expect: 400
+  # expected `/`
+  - uri: school
+    expect: 400
+  # expected `/:<identifier>`
+  - uri: /school/department
+    expect: 400
+    skip: true  # interpreted as division
+  - uri: /school/:1
+    expect: 400
+  # expected `)`
+  - uri: /school/:html(/program}
+    expect: 400
+  # expected `:<identifier>`
+  - uri: /school:1
+    expect: 400
+  # expected `)`
+  - uri: /school :as ('School'}
+    expect: 400
+  # expected `}`
+  - uri: /school{code,name)
+    expect: 400
+  # expected `)`
+  - uri: /count(school}
+    expect: 400
+  # unexpected end of query
+  - uri: /school{code,name,
+    expect: 400
+  # unexpected symbol
+  - uri: /school{code,,name}
+    expect: 400
+  # expected ')'
+  - uri: /(2+2}
+    expect: 400
+  # expected '}'
+  - uri: /{count(school))
+    expect: 400
 
+

File test/output/mssql.yaml

           headers:
           - [Content-Type, text/plain; charset=UTF-8]
           body: |
-            parse error: expected NAME; got SYMBOL u'/':
+            parse error: unexpected symbol '/':
                 /-/not-found
                   ^
         - uri: /;)/not-found
             scan error: symbol '%' must be followed by two hexdecimal digits:
                 /%27?%@$'
                      ^
-            (use '%25' to represent '%' literally)
         - uri: /'%FF'
           status: 400 Bad Request
           headers:
             scan error: unexpected symbol '`':
                 /`Hello'
                  ^
+      - id: parse-errors
+        tests:
+        - uri: /'Hello','World'
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: expected the end of query; got ',':
+                /'Hello','World'
+                        ^
+        - uri: school
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: query must start with symbol '/':
+                school
+                ^^^^^^
+        - uri: /school/:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol '/' must be followed by ':' and an identifier:
+                /school/:1
+                       ^
+        - uri: /school/:html(/program}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school/:html(/program}
+                             ^^^^^^^^^
+        - uri: /school:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol ':' must be followed by an identifier:
+                /school:1
+                       ^
+        - uri: /school :as ('School'}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school :as ('School'}
+                            ^^^^^^^^^
+        - uri: /school{code,name)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /school{code,name)
+                       ^^^^^^^^^^
+        - uri: /count(school}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /count(school}
+                      ^^^^^^^
+        - uri: /school{code,name,
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected end of query:
+                /school{code,name,
+                                  ^
+        - uri: /school{code,,name}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected symbol ',':
+                /school{code,,name}
+                             ^
+        - uri: /(2+2}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /(2+2}
+                 ^^^^
+        - uri: /{count(school))
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /{count(school))
+                 ^^^^^^^^^^^^^^

File test/output/mysql.yaml

           headers:
           - [Content-Type, text/plain; charset=UTF-8]
           body: |
-            parse error: expected NAME; got SYMBOL u'/':
+            parse error: unexpected symbol '/':
                 /-/not-found
                   ^
         - uri: /;)/not-found
             scan error: symbol '%' must be followed by two hexdecimal digits:
                 /%27?%@$'
                      ^
-            (use '%25' to represent '%' literally)
         - uri: /'%FF'
           status: 400 Bad Request
           headers:
             scan error: unexpected symbol '`':
                 /`Hello'
                  ^
+      - id: parse-errors
+        tests:
+        - uri: /'Hello','World'
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: expected the end of query; got ',':
+                /'Hello','World'
+                        ^
+        - uri: school
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: query must start with symbol '/':
+                school
+                ^^^^^^
+        - uri: /school/:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol '/' must be followed by ':' and an identifier:
+                /school/:1
+                       ^
+        - uri: /school/:html(/program}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school/:html(/program}
+                             ^^^^^^^^^
+        - uri: /school:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol ':' must be followed by an identifier:
+                /school:1
+                       ^
+        - uri: /school :as ('School'}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school :as ('School'}
+                            ^^^^^^^^^
+        - uri: /school{code,name)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /school{code,name)
+                       ^^^^^^^^^^
+        - uri: /count(school}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /count(school}
+                      ^^^^^^^
+        - uri: /school{code,name,
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected end of query:
+                /school{code,name,
+                                  ^
+        - uri: /school{code,,name}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected symbol ',':
+                /school{code,,name}
+                             ^
+        - uri: /(2+2}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /(2+2}
+                 ^^^^
+        - uri: /{count(school))
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /{count(school))
+                 ^^^^^^^^^^^^^^

File test/output/oracle.yaml

           headers:
           - [Content-Type, text/plain; charset=UTF-8]
           body: |
-            parse error: expected NAME; got SYMBOL u'/':
+            parse error: unexpected symbol '/':
                 /-/not-found
                   ^
         - uri: /;)/not-found
             scan error: symbol '%' must be followed by two hexdecimal digits:
                 /%27?%@$'
                      ^
-            (use '%25' to represent '%' literally)
         - uri: /'%FF'
           status: 400 Bad Request
           headers:
             scan error: unexpected symbol '`':
                 /`Hello'
                  ^
+      - id: parse-errors
+        tests:
+        - uri: /'Hello','World'
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: expected the end of query; got ',':
+                /'Hello','World'
+                        ^
+        - uri: school
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: query must start with symbol '/':
+                school
+                ^^^^^^
+        - uri: /school/:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol '/' must be followed by ':' and an identifier:
+                /school/:1
+                       ^
+        - uri: /school/:html(/program}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school/:html(/program}
+                             ^^^^^^^^^
+        - uri: /school:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol ':' must be followed by an identifier:
+                /school:1
+                       ^
+        - uri: /school :as ('School'}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school :as ('School'}
+                            ^^^^^^^^^
+        - uri: /school{code,name)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /school{code,name)
+                       ^^^^^^^^^^
+        - uri: /count(school}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /count(school}
+                      ^^^^^^^
+        - uri: /school{code,name,
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected end of query:
+                /school{code,name,
+                                  ^
+        - uri: /school{code,,name}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected symbol ',':
+                /school{code,,name}
+                             ^
+        - uri: /(2+2}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /(2+2}
+                 ^^^^
+        - uri: /{count(school))
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /{count(school))
+                 ^^^^^^^^^^^^^^

File test/output/pgsql.yaml

           headers:
           - [Content-Type, text/plain; charset=UTF-8]
           body: |
-            parse error: expected NAME; got SYMBOL u'/':
+            parse error: unexpected symbol '/':
                 /-/not-found
                   ^
         - uri: /;)/not-found
             scan error: symbol '%' must be followed by two hexdecimal digits:
                 /%27?%@$'
                      ^
-            (use '%25' to represent '%' literally)
         - uri: /'%FF'
           status: 400 Bad Request
           headers:
             scan error: unexpected symbol '`':
                 /`Hello'
                  ^
+      - id: parse-errors
+        tests:
+        - uri: /'Hello','World'
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: expected the end of query; got ',':
+                /'Hello','World'
+                        ^
+        - uri: school
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: query must start with symbol '/':
+                school
+                ^^^^^^
+        - uri: /school/:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol '/' must be followed by ':' and an identifier:
+                /school/:1
+                       ^
+        - uri: /school/:html(/program}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school/:html(/program}
+                             ^^^^^^^^^
+        - uri: /school:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol ':' must be followed by an identifier:
+                /school:1
+                       ^
+        - uri: /school :as ('School'}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school :as ('School'}
+                            ^^^^^^^^^
+        - uri: /school{code,name)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /school{code,name)
+                       ^^^^^^^^^^
+        - uri: /count(school}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /count(school}
+                      ^^^^^^^
+        - uri: /school{code,name,
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected end of query:
+                /school{code,name,
+                                  ^
+        - uri: /school{code,,name}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected symbol ',':
+                /school{code,,name}
+                             ^
+        - uri: /(2+2}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /(2+2}
+                 ^^^^
+        - uri: /{count(school))
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /{count(school))
+                 ^^^^^^^^^^^^^^

File test/output/sqlite.yaml

           headers:
           - [Content-Type, text/plain; charset=UTF-8]
           body: |
-            parse error: expected NAME; got SYMBOL u'/':
+            parse error: unexpected symbol '/':
                 /-/not-found
                   ^
         - uri: /;)/not-found
             scan error: symbol '%' must be followed by two hexdecimal digits:
                 /%27?%@$'
                      ^
-            (use '%25' to represent '%' literally)
         - uri: /'%FF'
           status: 400 Bad Request
           headers:
             scan error: unexpected symbol '`':
                 /`Hello'
                  ^
+      - id: parse-errors
+        tests:
+        - uri: /'Hello','World'
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: expected the end of query; got ',':
+                /'Hello','World'
+                        ^
+        - uri: school
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: query must start with symbol '/':
+                school
+                ^^^^^^
+        - uri: /school/:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol '/' must be followed by ':' and an identifier:
+                /school/:1
+                       ^
+        - uri: /school/:html(/program}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school/:html(/program}
+                             ^^^^^^^^^
+        - uri: /school:1
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: symbol ':' must be followed by an identifier:
+                /school:1
+                       ^
+        - uri: /school :as ('School'}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /school :as ('School'}
+                            ^^^^^^^^^
+        - uri: /school{code,name)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /school{code,name)
+                       ^^^^^^^^^^
+        - uri: /count(school}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /count(school}
+                      ^^^^^^^
+        - uri: /school{code,name,
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected end of query:
+                /school{code,name,
+                                  ^
+        - uri: /school{code,,name}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: unexpected symbol ',':
+                /school{code,,name}
+                             ^
+        - uri: /(2+2}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching ')':
+                /(2+2}
+                 ^^^^
+        - uri: /{count(school))
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            parse error: cannot find a matching '}':
+                /{count(school))
+                 ^^^^^^^^^^^^^^