Commits

Kirill Simonov committed ad03491

Added some tests for literals and boolean operators.

Also, normalized numeric values, implemented cast expressions
and other minor fixes.

Comments (0)

Files changed (15)

src/htsql/adapter.py

 
     # Override in subclasses.
     signature = None
-    weights = 1
+    weight = 1
     dominated_adapters = []
     dominating_adapters = []
 
                 # For implementations with identical signatures,
                 # dominance is determined by `weight`.
                 if master.signature == slave.signature:
-                    if master.weights > slave.weights:
+                    if master.weight > slave.weight:
                         is_dominated = True
                 # Otherwise, an adapter with a more specific signature
                 # dominates an adapter with a less specific signature.

src/htsql/fmt/text.py

 
 
 from ..adapter import adapts, find_adapters
+from ..util import maybe, oneof
 from .format import Format, Formatter, Renderer
-from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
-                      StringDomain, EnumDomain, DateDomain)
+from ..domain import (Domain, BooleanDomain, NumberDomain, IntegerDomain,
+                      DecimalDomain, FloatDomain, StringDomain, EnumDomain,
+                      DateDomain)
 import re
+import decimal
+import datetime
 
 
 class Layout(object):
         char = match.group()
         if char in self.escape_table:
             return self.escape_table[char]
-        return u"%%%02x" % ord(char)
+        return u"\\u%04x" % ord(char)
 
     def escape_string(self, value):
         if self.unescaped_regexp.match(value):
             return 5
 
     def __call__(self, value, width):
+        assert isinstance(value, maybe(bool))
         if value is None:
             return self.format_null(width)
         if value is True:
         return ["%*s" % (width, value)]
 
 
+class FormatInteger(Format):
+
+    adapts(TextRenderer, IntegerDomain)
+
+    def __call__(self, value, width):
+        assert isinstance(value, maybe(oneof(int, long)))
+        return super(FormatInteger, self).__call__(value, width)
+
+
+class FormatDecimal(Format):
+
+    adapts(TextRenderer, DecimalDomain)
+
+    def __call__(self, value, width):
+        assert isinstance(value, maybe(decimal.Decimal))
+        return super(FormatDecimal, self).__call__(value, width)
+
+
+class FormatFloat(Format):
+
+    adapts(TextRenderer, FloatDomain)
+
+    def __call__(self, value, width):
+        assert isinstance(value, maybe(float))
+        return super(FormatFloat, self).__call__(value, width)
+
+
 class FormatString(Format):
 
     adapts(TextRenderer, StringDomain)
         return max_length
 
     def __call__(self, value, width):
+        assert isinstance(value, maybe(str))
         if value is None:
             return self.format_null(width)
         value = value.decode('utf-8')
         return len(value)
 
     def __call__(self, value, width):
+        assert isinstance(value, maybe(str))
         if value is None:
             return self.format_null(width)
         value = value.decode('utf-8')
         return 10
 
     def __call__(self, value, width):
+        assert isinstance(value, maybe(datetime.date))
         if value is None:
             return self.format_null(width)
         return ["%*s" % (-width, value)]

src/htsql/tr/binder.py

         if 'e' in value or 'E' in value:
             domain = FloatDomain()
             value = float(value)
+            if str(value) in ['inf', '-inf', 'nan']:
+                raise InvalidArgumentError("invalid float value",
+                                           self.syntax.mark)
         elif '.' in value:
             domain = DecimalDomain()
             value = decimal.Decimal(value)
         else:
             domain = IntegerDomain()
             value = int(value)
+            if not (-2**63 <= value < 2**63):
+                raise InvalidArgumentError("invalid integer value",
+                                           self.syntax.mark)
         binding = LiteralBinding(parent, value, domain, self.syntax)
         yield binding
 

src/htsql/tr/code.py

 
     def __init__(self, code, domain, mark):
         super(CastExpression, self).__init__(domain, mark,
-                                             hash=(self.__class__, domain))
+                                             hash=(self.__class__,
+                                                   code.hash, domain))
         self.code = code
 
     def get_units(self):

src/htsql/tr/compiler.py

 from .code import (Expression, LiteralExpression, EqualityExpression,
                    InequalityExpression, ConjunctionExpression,
                    DisjunctionExpression, NegationExpression,
-                   TupleExpression, Unit)
+                   CastExpression, TupleExpression, Unit)
 from .sketch import (Sketch, LeafSketch, ScalarSketch, BranchSketch,
                      SegmentSketch, QuerySketch, Demand,
                      LeafAppointment, BranchAppointment, FrameAppointment)
 from .frame import (LeafFrame, ScalarFrame, BranchFrame, CorrelatedFrame,
                     SegmentFrame, QueryFrame, Link, Phrase, EqualityPhrase,
                     InequalityPhrase, ConjunctionPhrase, DisjunctionPhrase,
-                    NegationPhrase, LiteralPhrase, TuplePhrase,
+                    NegationPhrase, LiteralPhrase, CastPhrase, TuplePhrase,
                     LeafReferencePhrase, BranchReferencePhrase)
 
 
         return NegationPhrase(term, self.expression.mark)
 
 
+class EvaluateCast(Evaluate):
+
+    adapts(CastExpression, Compiler)
+
+    def evaluate(self, references):
+        phrase = self.compiler.evaluate(self.expression.code, references)
+        return CastPhrase(phrase, self.expression.domain, phrase.is_nullable,
+                          self.expression.mark)
+
+
 class EvaluateTuple(Evaluate):
 
     adapts(TupleExpression, Compiler)

src/htsql/tr/encoder.py

         return NegationExpression(term, self.binding.mark)
 
 
+class EncodeCast(Encode):
+
+    adapts(CastBinding, Encoder)
+
+    def encode(self):
+        code = self.encoder.encode(self.binding.binding)
+        return CastExpression(code, self.binding.domain, self.binding.mark)
+
+
 encode_adapters = find_adapters()
 
 

src/htsql/tr/fn/function.py

 """
 
 
-from ...adapter import Adapter, Utility, adapts, adapts_none, find_adapters
+from ...adapter import (Adapter, Utility, adapts, adapts_none,
+                        find_adapters, weights)
 from ...error import InvalidArgumentError
 from ...domain import (Domain, UntypedDomain, BooleanDomain, StringDomain,
                        IntegerDomain, DecimalDomain, FloatDomain, DateDomain)
 
 class FormatFunctions(Format):
 
+    weights(0)
+
     def concat_op(self, left, right):
         return "(%s || %s)" % (left, right)
 

src/htsql/tr/frame.py

 
 class CastPhrase(Phrase):
 
-    def __init__(self, term, domain, is_nullable, mark):
-        assert isinstance(term, Phrase)
+    def __init__(self, phrase, domain, is_nullable, mark):
+        assert isinstance(phrase, Phrase)
         super(CastPhrase, self).__init__(domain, is_nullable, mark)
-        self.term = term
+        self.phrase = phrase
 
 
 class LiteralPhrase(Phrase):

src/htsql/tr/serializer.py

 
 
 from ..adapter import Adapter, Utility, adapts, find_adapters
-from ..domain import (Domain, BooleanDomain, NumberDomain, StringDomain,
-                      DateDomain)
+from ..domain import (Domain, BooleanDomain, NumberDomain, IntegerDomain,
+                      DecimalDomain, FloatDomain, StringDomain, DateDomain)
 from .frame import (Clause, Frame, LeafFrame, ScalarFrame,
                     BranchFrame, CorrelatedFrame, SegmentFrame,
                     QueryFrame, Phrase, EqualityPhrase, InequalityPhrase,
                     ConjunctionPhrase, DisjunctionPhrase, NegationPhrase,
-                    LiteralPhrase, LeafReferencePhrase, BranchReferencePhrase,
-                    CorrelatedFramePhrase, TuplePhrase)
+                    CastPhrase, LiteralPhrase, LeafReferencePhrase,
+                    BranchReferencePhrase, CorrelatedFramePhrase, TuplePhrase)
 from .plan import Plan
 import decimal
 
         if isinstance(value, (long, decimal.Decimal)):
             return str(value)
 
+    def integer(self, value):
+        return str(value)
+
+    def decimal(self, value):
+        return str(value)
+
+    def float(self, value):
+        return repr(value)
+
     def string(self, value):
         return "'%s'" % value.replace("'", "''")
 
             op = "!="
         return self.binary_op(left, op, right)
 
+    def to_boolean(self, value):
+        return "(%s IS NOT NULL)" % value
+
+    def to_boolean_from_string(self, value):
+        return "(NULLIF(%s, '') IS NOT NULL)" % value
+
     def is_null(self, arg):
         return "(%s IS NULL)" % arg
 
         return self.format.and_op(conditions)
 
 
+class SerializeCast(SerializePhrase):
+
+    adapts(CastPhrase, Serializer)
+
+    def serialize(self):
+        serialize_to = SerializeTo(self.phrase.domain,
+                                   self.phrase.phrase.domain,
+                                   self.serializer)
+        return serialize_to.serialize(self.phrase.phrase)
+
+
+class SerializeTo(Adapter):
+
+    adapts(Domain, Domain, Serializer)
+
+    def __init__(self, to_domain, from_domain, serializer):
+        self.to_domain = to_domain
+        self.from_domain = from_domain
+        self.serializer = serializer
+        self.format = serializer.format
+
+
+class SerializeToBooleanFromString(SerializeTo):
+
+    adapts(BooleanDomain, StringDomain, Serializer)
+
+    def serialize(self, phrase):
+        value = self.serializer.serialize(phrase)
+        return self.format.to_boolean_from_string(value)
+
+
+class SerializeToBooleanFromNumber(SerializeTo):
+
+    adapts(BooleanDomain, NumberDomain, Serializer)
+
+    def serialize(self, phrase):
+        value = self.serializer.serialize(phrase)
+        return self.format.to_boolean(value)
+
+
 class SerializeLiteral(SerializePhrase):
 
     adapts(LiteralPhrase, Serializer)
 
     def serialize(self, value):
         if value is None:
-            return self.format.none()
+            return self.format.null()
         return self.format.number(value)
 
 
+class SerializeIntegerConstant(SerializeConstant):
+
+    adapts(IntegerDomain, Serializer)
+
+    def serialize(self, value):
+        if value is None:
+            return self.format.null()
+        return self.format.integer(value)
+
+
+class SerializeDecimalConstant(SerializeConstant):
+
+    adapts(DecimalDomain, Serializer)
+
+    def serialize(self, value):
+        if value is None:
+            return self.format.null()
+        return self.format.decimal(value)
+
+
+class SerializeFloatConstant(SerializeConstant):
+
+    adapts(FloatDomain, Serializer)
+
+    def serialize(self, value):
+        if value is None:
+            return self.format.null()
+        return self.format.float(value)
+
+
 class SerializeStringConstant(SerializeConstant):
 
     adapts(StringDomain, Serializer)
 
     def serialize(self, value):
         if value is None:
-            return self.format.none()
+            return self.format.null()
         return self.format.string(value)
 
 
 
     def serialize(self, value):
         if value is None:
-            return self.format.none()
+            return self.format.null()
         return self.format.date(value)
 
 

src/htsql/tr/syntax.py

     # The pattern to %-escape unsafe characters.
     escape_pattern = r"[\x00-\x1F%\x7F]"
     escape_regexp = re.compile(escape_pattern)
-    escape_replace = (lambda m: "%%%02X" % ord(m.group()))
+    escape_replace = (lambda s, m: "%%%02X" % ord(m.group()))
 
     def __init__(self, mark):
         assert isinstance(mark, Mark)

src/htsql_pgsql/export.py

 from .connect import connect_adapters
 from .split_sql import split_sql_adapters
 from .introspect import introspect_adapters
+from .tr.serializer import serializer_adapters
 
 
 class ENGINE_PGSQL(Addon):
     # List of adapters exported by the addon.
     adapters = (connect_adapters +
                 split_sql_adapters +
-                introspect_adapters)
+                introspect_adapters +
+                serializer_adapters)
 
 

src/htsql_pgsql/tr/__init__.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_pgsql.tr`
+=====================
+
+This package adapts the HTSQL-to-SQL translator for PostgreSQL.
+"""
+
+

src/htsql_pgsql/tr/serializer.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_pgsql.tr.serializer`
+================================
+
+This module adapts the SQL serializer for PostgreSQL.
+"""
+
+
+from htsql.adapter import adapts, find_adapters
+from htsql.tr.serializer import Format, SerializeBranch
+
+
+class PGSQLFormat(Format):
+
+    def float(self, value):
+        return "%s::float8" % value
+
+
+class PGSQLSerializeBranch(SerializeBranch):
+
+    def serialize_from_clause(self):
+        if (len(self.frame.linkage) == 1 and
+            self.frame.linkage[0].frame.is_scalar):
+            return None
+        return super(PGSQLSerializeBranch, self).serialize_from_clause()
+
+
+serializer_adapters = find_adapters()
+
+

test/input/pgsql.yaml

     - uri: /
     - uri: /{'HT'+'SQL'}
 
+    - title: Literals
+      tests:
+      # String literals.
+      - uri: /{'','HTSQL','O''Reilly','λόγος',
+               '%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F',
+               '%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'}
+      # Integer literals.
+      - uri: /{0,1,100,65536}
+      # Invalid integer literal (must be in range from -2**63 to 2**63-1).
+      - uri: /{115792089237316195423570985008687907853269984665640564039457584007913129639936}
+        expect: 400
+      # Decimal literals.
+      - uri: /{0.0,1.0,3.14,0.00000000001,1234567890.0987654321}
+      # Decimal values are of arbitrary size.
+      - uri: /{10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1}
+      # Float literals.
+      - uri: /{0e0,1e1,0.31415926535897931e1,2718281828459045e-16}
+      # Invalid float literal (inf).
+      - uri: /{1e1024}
+        expect: 400
+
+    - title: Scalar functions
+      tests:
+      - title: Boolean functions and operators
+        tests:
+        # Boolean constants.
+        - uri: /{true(),false()}
+        # Boolean->Boolean cast (identity).
+        - uri: /{boolean(null()),boolean(true()),boolean(false())}
+        # Untyped boolean values.
+        - uri: /{boolean('true'),boolean('TRUE'),boolean('True'),
+                 boolean('false'),boolean('FALSE'),boolean('False')}
+        # Invalid untyped boolean values.
+        - uri: /{boolean('')}
+          expect: 400
+        - uri: /{boolean('X')}
+          expect: 400
+        # Number->Boolean cast (null => false, otherwise => true).
+        - uri: /{boolean(integer(null())),boolean(0),boolean(1),
+                 boolean(0.0),boolean(1.0),boolean(0e0),boolean(1e0)}
+        # String->Boolean cast (null, '' => false, otherwise => true).
+        - uri: /{boolean(string(null())),boolean(string('')),boolean(string('X')),
+                 boolean(string('true')),boolean(string('false'))}
+               # Note: boolean(string('false')) => true
+        # The AND operator.
+        - uri: /{false()&false(),false()&true(),true()&false(),true()&true(),
+                 null()&false(),null()&true(),null()&null()}
+        # Auto-cast of arguments (false,true).
+        - uri: /{string('')&string('X'),0&1}
+        # Auto-cast of NULL values (null,false,false)
+        - uri: /{null()&null(),integer(null())&null(),integer(null())&integer(null())}
+        # The OR operator.
+        - uri: /{false()|false(),false()|true(),true()|false(),true()|true(),
+                 null()|false(),null()|true(),null()|null()}
+        # Auto-cast of arguments (true,true,false).
+        - uri: /{string('')|string('X'),0|1,integer(null())|string(null())}
+        # The NOT operator.
+        - uri: /{!true(),!false(),!null()}
+        # Auto-cast of arguments (true,false,false,true).
+        - uri: /{!string(''),!string('X'),!1,!integer(null())}
+
   # Simple (non-aggregate) filters.
   - title: Simple filters
     tests:

test/output/pgsql.yaml

 
          ----
          /{'HT'+'SQL'}
-         SELECT (COALESCE('HT', '') || COALESCE('SQL', '')) FROM (SELECT 1) AS "!_2"
+         SELECT (COALESCE('HT', '') || COALESCE('SQL', ''))
+    - id: literals
+      tests:
+      - uri: "/{'','HTSQL','O''Reilly','\u03BB\u03CC\u03B3\u03BF\u03C2', '%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F',
+          '%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'}"
+        status: 200 OK
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: " | /{'','HTSQL','O''Reilly','\u03BB\u03CC\u03B3\u03BF\u03C2','%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F','%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'}
+          \                                                                                |\n-+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-\n
+          | '' | 'HTSQL' | 'O''Reilly' | '\u03BB\u03CC\u03B3\u03BF\u03C2' | '%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F'
+          \                         | '%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'
+          \                                                |\n-+----+---------+-------------+---------+--------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------+-\n
+          | \"\" | HTSQL   | O'Reilly    | \u03BB\u03CC\u03B3\u03BF\u03C2   | \"\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\"
+          | \"\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\"
+          |\n                                                                                                                                                                                                                  (1
+          row)\n\n ----\n /{'','HTSQL','O''Reilly','\u03BB\u03CC\u03B3\u03BF\u03C2','%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F','%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'}\n
+          SELECT '', 'HTSQL', 'O''Reilly', '\u03BB\u03CC\u03B3\u03BF\u03C2', '\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0E\x0F',
+          '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\e\x1C\x1D\x1E\x1F'\n"
+      - uri: /{0,1,100,65536}
+        status: 200 OK
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: |2
+           | /{0,1,100,65536}    |
+          -+---------------------+-
+           | 0 | 1 | 100 | 65536 |
+          -+---+---+-----+-------+-
+           | 0 | 1 | 100 | 65536 |
+                           (1 row)
+
+           ----
+           /{0,1,100,65536}
+           SELECT 0, 1, 100, 65536
+      - uri: /{115792089237316195423570985008687907853269984665640564039457584007913129639936}
+        status: 400 Bad Request
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: |
+          invalid argument: invalid integer value:
+              /{115792089237316195423570985008687907853269984665640564039457584007913129639936}
+                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+      - uri: /{0.0,1.0,3.14,0.00000000001,1234567890.0987654321}
+        status: 200 OK
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: |2
+           | /{0.0,1.0,3.14,0.00000000001,1234567890.0987654321}      |
+          -+----------------------------------------------------------+-
+           | 0.0 | 1.0 | 3.14 | 0.00000000001 | 1234567890.0987654321 |
+          -+-----+-----+------+---------------+-----------------------+-
+           | 0.0 | 1.0 | 3.14 |         1E-11 | 1234567890.0987654321 |
+                                                                (1 row)
+
+           ----
+           /{0.0,1.0,3.14,0.00000000001,1234567890.0987654321}
+           SELECT 0.0, 1.0, 3.14, 1E-11, 1234567890.0987654321
+      - uri: /{10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1}
+        status: 200 OK
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: |2
+           | /{10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1} |
+          -+------------------------------------------------------------------------------------------------------------+-
+           | 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1    |
+          -+------------------------------------------------------------------------------------------------------------+-
+           |    10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1 |
+                                                                                                                  (1 row)
+
+           ----
+           /{10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1}
+           SELECT 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1
+      - uri: /{0e0,1e1,0.31415926535897931e1,2718281828459045e-16}
+        status: 200 OK
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: |2
+           | /{0e0,1e1,0.31415926535897931e1,2718281828459045e-16}     |
+          -+-----------------------------------------------------------+-
+           | 0e0 | 1e1  | 0.31415926535897931e1 | 2718281828459045e-16 |
+          -+-----+------+-----------------------+----------------------+-
+           | 0.0 | 10.0 |         3.14159265359 |       0.271828182846 |
+                                                                 (1 row)
+
+           ----
+           /{0e0,1e1,0.31415926535897931e1,2718281828459045e-16}
+           SELECT 0.0::float8, 10.0::float8, 3.14159265359::float8, 0.271828182846::float8
+      - uri: /{1e1024}
+        status: 400 Bad Request
+        headers:
+        - [Content-Type, text/plain; charset=UTF-8]
+        body: |
+          invalid argument: invalid float value:
+              /{1e1024}
+                ^^^^^^
+    - id: scalar-functions
+      tests:
+      - id: boolean-functions-and-operators
+        tests:
+        - uri: /{true(),false()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{true(),false()} |
+            -+-------------------+-
+             | true()  | false() |
+            -+---------+---------+-
+             | true    | false   |
+                           (1 row)
+
+             ----
+             /{true(),false()}
+             SELECT TRUE, FALSE
+        - uri: /{boolean(null()),boolean(true()),boolean(false())}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{boolean(null()),boolean(true()),boolean(false())} |
+            -+-----------------------------------------------------+-
+             | boolean(null())       | true()       | false()      |
+            -+-----------------------+--------------+--------------+-
+             |                       | true         | false        |
+                                                             (1 row)
+
+             ----
+             /{boolean(null()),boolean(true()),boolean(false())}
+             SELECT NULL, TRUE, FALSE
+        - uri: /{boolean('true'),boolean('TRUE'),boolean('True'), boolean('false'),boolean('FALSE'),boolean('False')}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{boolean('true'),boolean('TRUE'),boolean('True'),boolean('false'),boolean('FALSE'),boolean('False')}        |
+            -+--------------------------------------------------------------------------------------------------------------+-
+             | boolean('true') | boolean('TRUE') | boolean('True') | boolean('false') | boolean('FALSE') | boolean('False') |
+            -+-----------------+-----------------+-----------------+------------------+------------------+------------------+-
+             | true            | true            | true            | false            | false            | false            |
+                                                                                                                      (1 row)
+
+             ----
+             /{boolean('true'),boolean('TRUE'),boolean('True'),boolean('false'),boolean('FALSE'),boolean('False')}
+             SELECT TRUE, TRUE, TRUE, FALSE, FALSE, FALSE
+        - uri: /{boolean('')}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            invalid argument: cannot cast a value: invalid Boolean literal: expected 'true' or 'false'; got '':
+                /{boolean('')}
+                  ^^^^^^^^^^^
+        - uri: /{boolean('X')}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            invalid argument: cannot cast a value: invalid Boolean literal: expected 'true' or 'false'; got 'X':
+                /{boolean('X')}
+                  ^^^^^^^^^^^^
+        - uri: /{boolean(integer(null())),boolean(0),boolean(1), boolean(0.0),boolean(1.0),boolean(0e0),boolean(1e0)}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{boolean(integer(null())),boolean(0),boolean(1),boolean(0.0),boolean(1.0),boolean(0e0),boolean(1e0)}          |
+            -+----------------------------------------------------------------------------------------------------------------+-
+             | boolean(integer(null())) | boolean(0) | boolean(1) | boolean(0.0) | boolean(1.0) | boolean(0e0) | boolean(1e0) |
+            -+--------------------------+------------+------------+--------------+--------------+--------------+--------------+-
+             | false                    | true       | true       | true         | true         | true         | true         |
+                                                                                                                        (1 row)
+
+             ----
+             /{boolean(integer(null())),boolean(0),boolean(1),boolean(0.0),boolean(1.0),boolean(0e0),boolean(1e0)}
+             SELECT (NULL IS NOT NULL), (0 IS NOT NULL), (1 IS NOT NULL), (0.0 IS NOT NULL), (1.0 IS NOT NULL), (0.0::float8 IS NOT NULL), (1.0::float8 IS NOT NULL)
+        - uri: /{boolean(string(null())),boolean(string('')),boolean(string('X')),
+            boolean(string('true')),boolean(string('false'))}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{boolean(string(null())),boolean(string('')),boolean(string('X')),boolean(string('true')),boolean(string('false'))}      |
+            -+---------------------------------------------------------------------------------------------------------------------------+-
+             | boolean(string(null())) | boolean(string('')) | boolean(string('X')) | boolean(string('true')) | boolean(string('false')) |
+            -+-------------------------+---------------------+----------------------+-------------------------+--------------------------+-
+             | false                   | false               | true                 | true                    | true                     |
+                                                                                                                                   (1 row)
+
+             ----
+             /{boolean(string(null())),boolean(string('')),boolean(string('X')),boolean(string('true')),boolean(string('false'))}
+             SELECT (NULLIF(NULL, '') IS NOT NULL), (NULLIF('', '') IS NOT NULL), (NULLIF('X', '') IS NOT NULL), (NULLIF('true', '') IS NOT NULL), (NULLIF('false', '') IS NOT NULL)
+        - uri: /{false()&false(),false()&true(),true()&false(),true()&true(), null()&false(),null()&true(),null()&null()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{false()&false(),false()&true(),true()&false(),true()&true(),null()&false(),null()&true(),null()&null()}          |
+            -+--------------------------------------------------------------------------------------------------------------------+-
+             | false()&false() | false()&true() | true()&false() | true()&true() | null()&false() | null()&true() | null()&null() |
+            -+-----------------+----------------+----------------+---------------+----------------+---------------+---------------+-
+             | false           | false          | false          | true          | false          |               |               |
+                                                                                                                            (1 row)
+
+             ----
+             /{false()&false(),false()&true(),true()&false(),true()&true(),null()&false(),null()&true(),null()&null()}
+             SELECT (FALSE AND FALSE), (FALSE AND TRUE), (TRUE AND FALSE), (TRUE AND TRUE), (NULL AND FALSE), (NULL AND TRUE), (NULL AND NULL)
+        - uri: /{string('')&string('X'),0&1}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{string('')&string('X'),0&1} |
+            -+-------------------------------+-
+             | string('')&string('X') | 0&1  |
+            -+------------------------+------+-
+             | false                  | true |
+                                       (1 row)
+
+             ----
+             /{string('')&string('X'),0&1}
+             SELECT ((NULLIF('', '') IS NOT NULL) AND (NULLIF('X', '') IS NOT NULL)), ((0 IS NOT NULL) AND (1 IS NOT NULL))
+        - uri: /{null()&null(),integer(null())&null(),integer(null())&integer(null())}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{null()&null(),integer(null())&null(),integer(null())&integer(null())}  |
+            -+--------------------------------------------------------------------------+-
+             | null()&null() | integer(null())&null() | integer(null())&integer(null()) |
+            -+---------------+------------------------+---------------------------------+-
+             |               | false                  | false                           |
+                                                                                  (1 row)
+
+             ----
+             /{null()&null(),integer(null())&null(),integer(null())&integer(null())}
+             SELECT (NULL AND NULL), ((NULL IS NOT NULL) AND NULL), ((NULL IS NOT NULL) AND (NULL IS NOT NULL))
+        - uri: /{false()|false(),false()|true(),true()|false(),true()|true(), null()|false(),null()|true(),null()|null()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{false()|false(),false()|true(),true()|false(),true()|true(),null()|false(),null()|true(),null()|null()}          |
+            -+--------------------------------------------------------------------------------------------------------------------+-
+             | false()|false() | false()|true() | true()|false() | true()|true() | null()|false() | null()|true() | null()|null() |
+            -+-----------------+----------------+----------------+---------------+----------------+---------------+---------------+-
+             | false           | true           | true           | true          |                | true          |               |
+                                                                                                                            (1 row)
+
+             ----
+             /{false()|false(),false()|true(),true()|false(),true()|true(),null()|false(),null()|true(),null()|null()}
+             SELECT (FALSE OR FALSE), (FALSE OR TRUE), (TRUE OR FALSE), (TRUE OR TRUE), (NULL OR FALSE), (NULL OR TRUE), (NULL OR NULL)
+        - uri: /{string('')|string('X'),0|1,integer(null())|string(null())}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{string('')|string('X'),0|1,integer(null())|string(null())}   |
+            -+----------------------------------------------------------------+-
+             | string('')|string('X') | 0|1  | integer(null())|string(null()) |
+            -+------------------------+------+--------------------------------+-
+             | true                   | true | false                          |
+                                                                        (1 row)
+
+             ----
+             /{string('')|string('X'),0|1,integer(null())|string(null())}
+             SELECT ((NULLIF('', '') IS NOT NULL) OR (NULLIF('X', '') IS NOT NULL)), ((0 IS NOT NULL) OR (1 IS NOT NULL)), ((NULL IS NOT NULL) OR (NULLIF(NULL, '') IS NOT NULL))
+        - uri: /{!true(),!false(),!null()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{!true(),!false(),!null()}  |
+            -+------------------------------+-
+             | !true() | !false() | !null() |
+            -+---------+----------+---------+-
+             | false   | true     |         |
+                                      (1 row)
+
+             ----
+             /{!true(),!false(),!null()}
+             SELECT (NOT TRUE), (NOT FALSE), (NOT NULL)
+        - uri: /{!string(''),!string('X'),!1,!integer(null())}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{!string(''),!string('X'),!1,!integer(null())}       |
+            -+-------------------------------------------------------+-
+             | !string('') | !string('X') | !1    | !integer(null()) |
+            -+-------------+--------------+-------+------------------+-
+             | true        | false        | false | true             |
+                                                               (1 row)
+
+             ----
+             /{!string(''),!string('X'),!1,!integer(null())}
+             SELECT (NOT (NULLIF('', '') IS NOT NULL)), (NOT (NULLIF('X', '') IS NOT NULL)), (NOT (1 IS NOT NULL)), (NOT (NULL IS NOT NULL))
   - id: simple-filters
     tests:
     - uri: /school?code='ns'