Commits

Kirill Simonov committed 4e5721e

Added date constructor and arithmetic operators.

Comments (0)

Files changed (5)

src/htsql/domain.py

 from .util import maybe, listof
 import re
 import decimal
+import datetime
 
 
 class Domain(object):

src/htsql/tr/fn/function.py

                     raise InvalidArgumentError("too many arguments",
                                                argument[1].mark)
             keywords[parameter.name] = value
-        if arguments:
-            raise InvalidArgumentError("unexpected argument",
-                                       arguments[0].mark)
+        while arguments:
+            argument = arguments.pop(0)
+            if argument:
+                raise InvalidArgumentError("unexpected argument",
+                                           argument[0].mark)
         return keywords
 
 
     adapts(named['date'])
     output_domain = DateDomain()
 
+    def bind_function_call(self, syntax, parent):
+        if len(syntax.arguments) > 1:
+            constructor = self.binder.find_function(self.name+'!')
+            return constructor.bind_function_call(syntax, parent)
+        return super(DateCastFunction, self).bind_function_call(syntax, parent)
+
+
+class DateConstructor(ProperFunction):
+
+    adapts(named['date!'])
+
+    parameters = [
+            Parameter('year', IntegerDomain),
+            Parameter('month', IntegerDomain),
+            Parameter('day', IntegerDomain),
+    ]
+
+    def correlate(self, year, month, day, syntax, parent):
+        yield DateConstructorBinding(parent, DateDomain(), syntax,
+                                     year=year, month=month, day=day)
+
 
 class EqualityOperator(ProperFunction):
 
         return self.template % arguments
 
 
+DateConstructorBinding = GenericBinding.factory(DateConstructor)
+DateConstructorExpression = GenericExpression.factory(DateConstructor)
+DateConstructorPhrase = GenericPhrase.factory(DateConstructor)
+
+
+EncodeDateConstructor = GenericEncode.factory(DateConstructor,
+        DateConstructorBinding, DateConstructorExpression)
+EvaluateDateConstructor = GenericEvaluate.factory(DateConstructor,
+        DateConstructorExpression, DateConstructorPhrase)
+SerializeDateConstructor = GenericSerialize.factory(DateConstructor,
+        DateConstructorPhrase,
+        "CAST(LPAD(CAST(%(year)s AS TEXT), 4, '0') || '-' ||"
+        " LPAD(CAST(%(month)s AS TEXT), 2, '0') || '-' ||"
+        " LPAD(CAST(%(day)s AS TEXT), 2, '0') AS DATE)")
+
+
 ComparisonBinding = GenericBinding.factory(ComparisonOperator)
 ComparisonExpression = GenericExpression.factory(ComparisonOperator)
 ComparisonPhrase = GenericPhrase.factory(ComparisonOperator)
     domain = FloatDomain()
 
 
+class AddDateToInteger(Add):
+
+    adapts(DateDomain, IntegerDomain)
+
+    def __call__(self):
+        return AdditionBinding(self.parent, DateDomain(), self.syntax,
+                               left=self.left, right=self.right)
+
+
+
 SubtractionBinding = GenericBinding.factory(SubtractionOperator)
 SubtractionExpression = GenericExpression.factory(SubtractionOperator)
 SubtractionPhrase = GenericPhrase.factory(SubtractionOperator)
     domain = FloatDomain()
 
 
+class SubtractIntegerFromDate(Subtract):
+
+    adapts(DateDomain, IntegerDomain)
+
+    def __call__(self):
+        return SubtractionBinding(self.parent, DateDomain(), self.syntax,
+                                  left=self.left, right=self.right)
+
+
+class SubtractDateFromDate(Subtract):
+
+    adapts(DateDomain, DateDomain)
+
+    def __call__(self):
+        return SubtractionBinding(self.parent, IntegerDomain(), self.syntax,
+                                  left=self.left, right=self.right)
+
+
 MultiplicationBinding = GenericBinding.factory(MultiplicationOperator)
 MultiplicationExpression = GenericExpression.factory(MultiplicationOperator)
 MultiplicationPhrase = GenericPhrase.factory(MultiplicationOperator)

src/htsql/tr/serializer.py

     def to_string(self, value):
         return "CAST(%s AS TEXT)" % value
 
+    def to_date(self, value):
+        return "CAST(%s AS DATE)" % value
+
     def is_null(self, arg):
         return "(%s IS NULL)" % arg
 
         return self.format.to_string(value)
 
 
+class SerializeToDateFromString(SerializeTo):
+
+    adapts(DateDomain, StringDomain, Serializer)
+
+    def serialize(self, phrase):
+        value = self.serializer.serialize(phrase)
+        return self.format.to_date(value)
+
+
 class SerializeLiteral(SerializePhrase):
 
     adapts(LiteralPhrase, Serializer)

test/input/pgsql.yaml

         - uri: /{'HTSQL'~'sql','HTSQL'~'HTTP','HTSQL'~'',
                  'HTSQL'~null(),null()~'HTSQL',null()~null()}
 
+      - title: Date functions and operators
+        tests:
+        # Date cast (from untyped, string).
+        - uri: /{date('2010-07-28'),date(string('2010-07-28'))}
+        # Date constructor.
+        - uri: /{date(2010,07,28)}
+        # Invalid untyped->date cast.
+        - uri: /{date('X')}
+          expect: 400
+        # Invalid string->date cast.
+        - uri: /{date(string('X'))}
+          expect: 409
+        # Invalid integer->date cast.
+        - uri: /{date(1)}
+          expect: 400
+        # Invalid date constructor.
+        - uri: /{date(0,0,0)}
+          expect: 409
+        # Date arithmetic operations.
+        - uri: /{date('2010-07-28')+1,date('2010-07-28')-1,
+                 date('2010-07-28')-date('2009-07-28')}
+
   # Simple (non-aggregate) filters.
   - title: Simple filters
     tests:

test/output/pgsql.yaml

              ----
              /{'HTSQL'~'sql','HTSQL'~'HTTP','HTSQL'~'','HTSQL'~null(),null()~'HTSQL',null()~null()}
              SELECT (POSITION(LOWER('sql') IN LOWER('HTSQL')) > 0), (POSITION(LOWER('HTTP') IN LOWER('HTSQL')) > 0), (POSITION(LOWER('') IN LOWER('HTSQL')) > 0), (POSITION(LOWER(NULL) IN LOWER('HTSQL')) > 0), (POSITION(LOWER('HTSQL') IN LOWER(NULL)) > 0), (POSITION(LOWER(NULL) IN LOWER(NULL)) > 0)
+      - id: date-functions-and-operators
+        tests:
+        - uri: /{date('2010-07-28'),date(string('2010-07-28'))}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{date('2010-07-28'),date(string('2010-07-28'))} |
+            -+--------------------------------------------------+-
+             | date('2010-07-28')  | date(string('2010-07-28')) |
+            -+---------------------+----------------------------+-
+             | 2010-07-28          | 2010-07-28                 |
+                                                          (1 row)
+
+             ----
+             /{date('2010-07-28'),date(string('2010-07-28'))}
+             SELECT CAST('2010-07-28' AS DATE), CAST('2010-07-28' AS DATE)
+        - uri: /{date(2010,07,28)}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{date(2010,07,28)} |
+            -+---------------------+-
+             | date(2010,07,28)    |
+            -+---------------------+-
+             | 2010-07-28          |
+                             (1 row)
+
+             ----
+             /{date(2010,07,28)}
+             SELECT CAST(LPAD(CAST(2010 AS TEXT), 4, '0') || '-' || LPAD(CAST(7 AS TEXT), 2, '0') || '-' || LPAD(CAST(28 AS TEXT), 2, '0') AS DATE)
+        - uri: /{date('X')}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            invalid argument: cannot cast a value: invalid date literal: expected 'YYYY-MM-DD'; got 'X':
+                /{date('X')}
+                  ^^^^^^^^^
+        - uri: /{date(string('X'))}
+          status: 409 Conflict
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            engine failure: error while executing "SELECT CAST('X' AS DATE)": invalid input syntax for type date: "X"
+            LINE 1: SELECT CAST('X' AS DATE)
+                                ^
+            :
+                /{date(string('X'))}
+                ^^^^^^^^^^^^^^^^^^^^
+        - uri: /{date(1)}
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            invalid argument: unable to cast:
+                /{date(1)}
+                       ^
+        - uri: /{date(0,0,0)}
+          status: 409 Conflict
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            engine failure: error while executing "SELECT CAST(LPAD(CAST(0 AS TEXT), 4, '0') || '-' || LPAD(CAST(0 AS TEXT), 2, '0') || '-' || LPAD(CAST(0 AS TEXT), 2, '0') AS DATE)": date/time field value out of range: "0000-00-00"
+            :
+                /{date(0,0,0)}
+                ^^^^^^^^^^^^^^
+        - uri: /{date('2010-07-28')+1,date('2010-07-28')-1, date('2010-07-28')-date('2009-07-28')}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             | /{date('2010-07-28')+1,date('2010-07-28')-1,date('2010-07-28')-date('2009-07-28')}  |
+            -+-------------------------------------------------------------------------------------+-
+             | date('2010-07-28')+1 | date('2010-07-28')-1 | date('2010-07-28')-date('2009-07-28') |
+            -+----------------------+----------------------+---------------------------------------+-
+             | 2010-07-29           | 2010-07-27           |                                   365 |
+                                                                                             (1 row)
+
+             ----
+             /{date('2010-07-28')+1,date('2010-07-28')-1,date('2010-07-28')-date('2009-07-28')}
+             SELECT (CAST('2010-07-28' AS DATE) + 1), (CAST('2010-07-28' AS DATE) - 1), (CAST('2010-07-28' AS DATE) - CAST('2009-07-28' AS DATE))
   - id: simple-filters
     tests:
     - uri: /school?code='ns'
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.