Commits

Kirill Simonov committed a7df387

Added arithmetic operators.

Comments (0)

Files changed (4)

src/htsql/tr/fn/function.py

 
     def bind_function_operator(self, syntax, parent):
         arguments = [syntax.left, syntax.right]
-        keywords = self.bind_arguments(arguments, parent)
+        keywords = self.bind_arguments(arguments, parent, syntax.mark)
         return self.correlate(syntax=syntax, parent=parent, **keywords)
 
     def bind_function_call(self, syntax, parent):
 
     def correlate(self, left, right, syntax, parent):
         Implementation = Add.realize(left.domain, right.domain)
-        addition = Implementation(left, right, self.binder, syntax, parent)
-        yield addition()
+        add = Implementation(left, right, self.binder, syntax, parent)
+        yield add()
+
+
+class SubtractionOperator(ProperFunction):
+
+    adapts(named['_-_'])
+
+    parameters = [
+            Parameter('left'),
+            Parameter('right'),
+    ]
+
+    def correlate(self, left, right, syntax, parent):
+        Implementation = Subtract.realize(left.domain, right.domain)
+        subtract = Implementation(left, right, self.binder, syntax, parent)
+        yield subtract()
+
+
+class MultiplicationOperator(ProperFunction):
+
+    adapts(named['_*_'])
+
+    parameters = [
+            Parameter('left'),
+            Parameter('right'),
+    ]
+
+    def correlate(self, left, right, syntax, parent):
+        Implementation = Multiply.realize(left.domain, right.domain)
+        multiply = Implementation(left, right, self.binder, syntax, parent)
+        yield multiply()
+
+
+class DivisionOperator(ProperFunction):
+
+    #adapts(named['_/_'])
+    adapts(named['div'])
+
+    parameters = [
+            Parameter('left'),
+            Parameter('right'),
+    ]
+
+    def correlate(self, left, right, syntax, parent):
+        Implementation = Divide.realize(left.domain, right.domain)
+        divide = Implementation(left, right, self.binder, syntax, parent)
+        yield divide()
 
 
 class Add(Adapter):
                                    self.syntax.mark)
 
 
+class Subtract(Adapter):
+
+    adapts(Domain, Domain)
+
+    def __init__(self, left, right, binder, syntax, parent):
+        self.left = left
+        self.right = right
+        self.binder = binder
+        self.syntax = syntax
+        self.parent = parent
+
+    def __call__(self):
+        raise InvalidArgumentError("unexpected argument types",
+                                   self.syntax.mark)
+
+
+class Multiply(Adapter):
+
+    adapts(Domain, Domain)
+
+    def __init__(self, left, right, binder, syntax, parent):
+        self.left = left
+        self.right = right
+        self.binder = binder
+        self.syntax = syntax
+        self.parent = parent
+
+    def __call__(self):
+        raise InvalidArgumentError("unexpected argument types",
+                                   self.syntax.mark)
+
+
+class Divide(Adapter):
+
+    adapts(Domain, Domain)
+
+    def __init__(self, left, right, binder, syntax, parent):
+        self.left = left
+        self.right = right
+        self.binder = binder
+        self.syntax = syntax
+        self.parent = parent
+
+    def __call__(self):
+        raise InvalidArgumentError("unexpected argument types",
+                                   self.syntax.mark)
+
+
 class GenericBinding(FunctionBinding):
 
     function = None
     adapts(UntypedDomain, UntypedDomain)
 
 
+AdditionBinding = GenericBinding.factory(AdditionOperator)
+AdditionExpression = GenericExpression.factory(AdditionOperator)
+AdditionPhrase = GenericPhrase.factory(AdditionOperator)
+
+
+EncodeAddition = GenericEncode.factory(AdditionOperator,
+        AdditionBinding, AdditionExpression)
+EvaluateAddition = GenericEvaluate.factory(AdditionOperator,
+        AdditionExpression, AdditionPhrase)
+SerializeAddition = GenericSerialize.factory(AdditionOperator,
+        AdditionPhrase, "(%(left)s + %(right)s)")
+
+
+class AddNumbers(Add):
+
+    adapts_none()
+    domain = None
+
+    def __call__(self):
+        left = self.binder.cast(self.left, self.domain,
+                                parent=self.parent)
+        right = self.binder.cast(self.right, self.domain,
+                                 parent=self.parent)
+        return AdditionBinding(self.parent, self.domain, self.syntax,
+                               left=left, right=right)
+
+
+class AddIntegerToInteger(AddNumbers):
+
+    adapts(IntegerDomain, IntegerDomain)
+    domain = IntegerDomain()
+
+
+class AddIntegerToDecimal(AddNumbers):
+
+    adapts(IntegerDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class AddDecimalToInteger(AddNumbers):
+
+    adapts(DecimalDomain, IntegerDomain)
+    domain = DecimalDomain()
+
+
+class AddDecimalToDecimal(AddNumbers):
+
+    adapts(DecimalDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class AddIntegerToFloat(AddNumbers):
+
+    adapts(IntegerDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class AddDecimalToFloat(AddNumbers):
+
+    adapts(DecimalDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class AddFloatToInteger(AddNumbers):
+
+    adapts(FloatDomain, IntegerDomain)
+    domain = FloatDomain()
+
+
+class AddFloatToDecimal(AddNumbers):
+
+    adapts(FloatDomain, DecimalDomain)
+    domain = FloatDomain()
+
+
+class AddFloatToFloat(AddNumbers):
+
+    adapts(FloatDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+SubtractionBinding = GenericBinding.factory(SubtractionOperator)
+SubtractionExpression = GenericExpression.factory(SubtractionOperator)
+SubtractionPhrase = GenericPhrase.factory(SubtractionOperator)
+
+
+EncodeSubtraction = GenericEncode.factory(SubtractionOperator,
+        SubtractionBinding, SubtractionExpression)
+EvaluateSubtraction = GenericEvaluate.factory(SubtractionOperator,
+        SubtractionExpression, SubtractionPhrase)
+SerializeSubtraction = GenericSerialize.factory(SubtractionOperator,
+        SubtractionPhrase, "(%(left)s - %(right)s)")
+
+
+class SubtractNumbers(Subtract):
+
+    adapts_none()
+    domain = None
+
+    def __call__(self):
+        left = self.binder.cast(self.left, self.domain,
+                                parent=self.parent)
+        right = self.binder.cast(self.right, self.domain,
+                                 parent=self.parent)
+        return SubtractionBinding(self.parent, self.domain, self.syntax,
+                                  left=left, right=right)
+
+
+class SubtractIntegerFromInteger(SubtractNumbers):
+
+    adapts(IntegerDomain, IntegerDomain)
+    domain = IntegerDomain()
+
+
+class SubtractIntegerFromDecimal(SubtractNumbers):
+
+    adapts(DecimalDomain, IntegerDomain)
+    domain = DecimalDomain()
+
+
+class SubtractDecimalFromInteger(SubtractNumbers):
+
+    adapts(IntegerDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class SubtractDecimalToDecimal(SubtractNumbers):
+
+    adapts(DecimalDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class SubtractIntegerFromFloat(SubtractNumbers):
+
+    adapts(FloatDomain, IntegerDomain)
+    domain = FloatDomain()
+
+
+class SubtractDecimalFromFloat(SubtractNumbers):
+
+    adapts(FloatDomain, DecimalDomain)
+    domain = FloatDomain()
+
+
+class SubtractFloatFromInteger(SubtractNumbers):
+
+    adapts(IntegerDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class SubtractFloatFromDecimal(SubtractNumbers):
+
+    adapts(DecimalDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class SubtractFloatFromFloat(SubtractNumbers):
+
+    adapts(FloatDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+MultiplicationBinding = GenericBinding.factory(MultiplicationOperator)
+MultiplicationExpression = GenericExpression.factory(MultiplicationOperator)
+MultiplicationPhrase = GenericPhrase.factory(MultiplicationOperator)
+
+
+EncodeMultiplication = GenericEncode.factory(MultiplicationOperator,
+        MultiplicationBinding, MultiplicationExpression)
+EvaluateMultiplication = GenericEvaluate.factory(MultiplicationOperator,
+        MultiplicationExpression, MultiplicationPhrase)
+SerializeMultiplication = GenericSerialize.factory(MultiplicationOperator,
+        MultiplicationPhrase, "(%(left)s * %(right)s)")
+
+
+class MultiplyNumbers(Multiply):
+
+    adapts_none()
+    domain = None
+
+    def __call__(self):
+        left = self.binder.cast(self.left, self.domain,
+                                parent=self.parent)
+        right = self.binder.cast(self.right, self.domain,
+                                 parent=self.parent)
+        return MultiplicationBinding(self.parent, self.domain, self.syntax,
+                                     left=left, right=right)
+
+
+class MultiplyIntegerByInteger(MultiplyNumbers):
+
+    adapts(IntegerDomain, IntegerDomain)
+    domain = IntegerDomain()
+
+
+class MultiplyIntegerByDecimal(MultiplyNumbers):
+
+    adapts(IntegerDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class MultiplyDecimalByInteger(MultiplyNumbers):
+
+    adapts(DecimalDomain, IntegerDomain)
+    domain = DecimalDomain()
+
+
+class MultiplyDecimalByDecimal(MultiplyNumbers):
+
+    adapts(DecimalDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class MultiplyIntegerByFloat(MultiplyNumbers):
+
+    adapts(IntegerDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class MultiplyDecimalByFloat(MultiplyNumbers):
+
+    adapts(DecimalDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class MultiplyFloatByInteger(MultiplyNumbers):
+
+    adapts(FloatDomain, IntegerDomain)
+    domain = FloatDomain()
+
+
+class MultiplyFloatByDecimal(MultiplyNumbers):
+
+    adapts(FloatDomain, DecimalDomain)
+    domain = FloatDomain()
+
+
+class MultiplyFloatByFloat(MultiplyNumbers):
+
+    adapts(FloatDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+DivisionBinding = GenericBinding.factory(DivisionOperator)
+DivisionExpression = GenericExpression.factory(DivisionOperator)
+DivisionPhrase = GenericPhrase.factory(DivisionOperator)
+
+
+EncodeDivision = GenericEncode.factory(DivisionOperator,
+        DivisionBinding, DivisionExpression)
+EvaluateDivision = GenericEvaluate.factory(DivisionOperator,
+        DivisionExpression, DivisionPhrase)
+SerializeDivision = GenericSerialize.factory(DivisionOperator,
+        DivisionPhrase, "(%(left)s / %(right)s)")
+
+
+class DivideNumbers(Divide):
+
+    adapts_none()
+    domain = None
+
+    def __call__(self):
+        left = self.binder.cast(self.left, self.domain,
+                                parent=self.parent)
+        right = self.binder.cast(self.right, self.domain,
+                                 parent=self.parent)
+        return DivisionBinding(self.parent, self.domain, self.syntax,
+                               left=left, right=right)
+
+
+class DivideIntegerByInteger(DivideNumbers):
+
+    adapts(IntegerDomain, IntegerDomain)
+    domain = DecimalDomain()
+
+
+class DivideIntegerByDecimal(DivideNumbers):
+
+    adapts(IntegerDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class DivideDecimalByInteger(DivideNumbers):
+
+    adapts(DecimalDomain, IntegerDomain)
+    domain = DecimalDomain()
+
+
+class DivideDecimalByDecimal(DivideNumbers):
+
+    adapts(DecimalDomain, DecimalDomain)
+    domain = DecimalDomain()
+
+
+class DivideIntegerByFloat(DivideNumbers):
+
+    adapts(IntegerDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class DivideDecimalByFloat(DivideNumbers):
+
+    adapts(DecimalDomain, FloatDomain)
+    domain = FloatDomain()
+
+
+class DivideFloatByInteger(DivideNumbers):
+
+    adapts(FloatDomain, IntegerDomain)
+    domain = FloatDomain()
+
+
+class DivideFloatByDecimal(DivideNumbers):
+
+    adapts(FloatDomain, DecimalDomain)
+    domain = FloatDomain()
+
+
+class DivideFloatByFloat(DivideNumbers):
+
+    adapts(FloatDomain, FloatDomain)
+    domain = FloatDomain()
+
+
 class IsNullFunction(ProperFunction):
 
     adapts(named['is_null'])

src/htsql/tr/parser.py

         while (tokens.peek(SymbolToken, ['*'])
                or tokens.peek(NameToken)):
             if tokens.peek(SymbolToken, ['*']):
-                symbol_token = token.pop(SymbolToken, ['*'])
-                symbol = token.value
+                symbol_token = tokens.pop(SymbolToken, ['*'])
+                symbol = symbol_token.value
                 left = expression
                 right = FactorParser << tokens
                 mark = Mark.union(left, right)

test/input/pgsql.yaml

                  switch(null(),null(),1,0),
                  switch('Y','X',1,'Y',2,'Z',3),
                  switch('Y','A',1,'B',2,'C',3,0)}
-        # Equality/Inequality
+        # Equality/Inequality.
         - uri: /{1=1,1=0,1=null(),null()=null(),
                  1!=1,1!=0,1!=null(),null()!=null(),
                  1==1,1==0,1==null(),null()==null(),
         - uri: /{'X'='X',1=1.0,1=1e0,1.0=1e0,1='1'}
         - uri: /{integer('1')=string('1')}
           expect: 400
-        # Less Than/Greater Than
+        # Less Than/Greater Than.
         - uri: /{'X'<'Y','X'<='Y','X'>'Y','X'>='Y',
                  'X'<null(),'X'<=null(),'X'>null(),'X'>=null()}
         - uri: /{1<2,1<=2,1>2,1>=2,
         - uri: /{integer('1')<string('1')}
           expect: 400
 
+      - title: Numeric functions and operators
+        tests:
+        # Addition.
+        - uri: /{2+2,2+2.0,2+2e0,2.0+2.0,2.0+2e0,2e0+2e0}
+        # Subtraction.
+        - uri: /{2-1,2-1.0,2-1e0,2.0-1.0,2.0-1e0,2e0-1e0}
+        # Multiplication.
+        - uri: /{5*5,5*5.0,5*5e0,5.0*5.0,5.0*5e0,5e0*5e0}
+        # Division.
+        - uri: /{1 div 2,1 div 2.0,1 div 2e0,1.0 div 2.0,1.0 div 2e0, 1e0 div 2e0}
+        # Division by zero.
+        - uri: /{1 div 0}
+          expect: 409
+        # Addition: invalid types.
+        - uri: /{1+'1'}
+          expect: 400
+        # Multiplication: overflow.
+        - uri: /{65536*65536}
+          expect: 409
+
   # Simple (non-aggregate) filters.
   - title: Simple filters
     tests:

test/output/pgsql.yaml

             invalid argument: incompatible types:
                 /{integer('1')<string('1')}
                   ^^^^^^^^^^^^^^^^^^^^^^^^
+      - id: numeric-functions-and-operators
+        tests:
+        - uri: /{2+2,2+2.0,2+2e0,2.0+2.0,2.0+2e0,2e0+2e0}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{2+2,2+2.0,2+2e0,2.0+2.0,2.0+2e0,2e0+2e0}        |
+            -+---------------------------------------------------+-
+             | 2+2 | 2+2.0 | 2+2e0 | 2.0+2.0 | 2.0+2e0 | 2e0+2e0 |
+            -+-----+-------+-------+---------+---------+---------+-
+             |   4 |   4.0 |   4.0 |     4.0 |     4.0 |     4.0 |
+                                                           (1 row)
+
+             ----
+             /{2+2,2+2.0,2+2e0,2.0+2.0,2.0+2e0,2e0+2e0}
+             SELECT (2 + 2), (CAST(2 AS NUMERIC) + 2.0), (CAST(2 AS FLOAT) + 2.0::float8), (2.0 + 2.0), (CAST(2.0 AS FLOAT) + 2.0::float8), (2.0::float8 + 2.0::float8)
+        - uri: /{2-1,2-1.0,2-1e0,2.0-1.0,2.0-1e0,2e0-1e0}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{2-1,2-1.0,2-1e0,2.0-1.0,2.0-1e0,2e0-1e0}        |
+            -+---------------------------------------------------+-
+             | 2-1 | 2-1.0 | 2-1e0 | 2.0-1.0 | 2.0-1e0 | 2e0-1e0 |
+            -+-----+-------+-------+---------+---------+---------+-
+             |   1 |   1.0 |   1.0 |     1.0 |     1.0 |     1.0 |
+                                                           (1 row)
+
+             ----
+             /{2-1,2-1.0,2-1e0,2.0-1.0,2.0-1e0,2e0-1e0}
+             SELECT (2 - 1), (CAST(2 AS NUMERIC) - 1.0), (CAST(2 AS FLOAT) - 1.0::float8), (2.0 - 1.0), (CAST(2.0 AS FLOAT) - 1.0::float8), (2.0::float8 - 1.0::float8)
+        - uri: /{5*5,5*5.0,5*5e0,5.0*5.0,5.0*5e0,5e0*5e0}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{5*5,5*5.0,5*5e0,5.0*5.0,5.0*5e0,5e0*5e0}        |
+            -+---------------------------------------------------+-
+             | 5*5 | 5*5.0 | 5*5e0 | 5.0*5.0 | 5.0*5e0 | 5e0*5e0 |
+            -+-----+-------+-------+---------+---------+---------+-
+             |  25 |  25.0 |  25.0 |   25.00 |    25.0 |    25.0 |
+                                                           (1 row)
+
+             ----
+             /{5*5,5*5.0,5*5e0,5.0*5.0,5.0*5e0,5e0*5e0}
+             SELECT (5 * 5), (CAST(5 AS NUMERIC) * 5.0), (CAST(5 AS FLOAT) * 5.0::float8), (5.0 * 5.0), (CAST(5.0 AS FLOAT) * 5.0::float8), (5.0::float8 * 5.0::float8)
+        - uri: /{1 div 2,1 div 2.0,1 div 2e0,1.0 div 2.0,1.0 div 2e0, 1e0 div 2e0}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{1 div 2,1 div 2.0,1 div 2e0,1.0 div 2.0,1.0 div 2e0,1e0 div 2e0}                                               |
+            -+------------------------------------------------------------------------------------------------------------------+-
+             | 1 div 2                | 1 div 2.0              | 1 div 2e0 | 1.0 div 2.0            | 1.0 div 2e0 | 1e0 div 2e0 |
+            -+------------------------+------------------------+-----------+------------------------+-------------+-------------+-
+             | 0.50000000000000000000 | 0.50000000000000000000 |       0.5 | 0.50000000000000000000 |         0.5 |         0.5 |
+                                                                                                                          (1 row)
+
+             ----
+             /{1 div 2,1 div 2.0,1 div 2e0,1.0 div 2.0,1.0 div 2e0,1e0 div 2e0}
+             SELECT (CAST(1 AS NUMERIC) / CAST(2 AS NUMERIC)), (CAST(1 AS NUMERIC) / 2.0), (CAST(1 AS FLOAT) / 2.0::float8), (1.0 / 2.0), (CAST(1.0 AS FLOAT) / 2.0::float8), (1.0::float8 / 2.0::float8)
+        - uri: /{1 div 0}
+          status: 409 Conflict
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            engine failure: error while executing 'SELECT (CAST(1 AS NUMERIC) / CAST(0 AS NUMERIC))': division by zero
+            :
+                /{1 div 0}
+                ^^^^^^^^^^
+        - uri: /{1+'1'}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            invalid argument: unexpected argument types:
+                /{1+'1'}
+                  ^^^^^
+        - uri: /{65536*65536}
+          status: 409 Conflict
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            engine failure: error while executing 'SELECT (65536 * 65536)': integer out of range
+            :
+                /{65536*65536}
+                ^^^^^^^^^^^^^^
   - id: simple-filters
     tests:
     - uri: /school?code='ns'