Kirill Simonov avatar Kirill Simonov committed 4dc22bb

Added `Time` and `DateTime` domains, implemented function `now()`.

Comments (0)

Files changed (18)

src/htsql/domain.py

 """
 
 
-from .util import maybe, oneof, listof
+from .util import maybe, oneof, listof, UTC, FixedTZ
 import re
 import decimal
 import datetime
     """
 
     # Regular expression to match YYYY-MM-DD.
-    pattern = r'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})$'
+    pattern = r'''(?x)
+        ^ \s*
+        (?P<year> \d{4} )
+        - (?P<month> \d{2} )
+        - (?P<day> \d{2} )
+        \s* $
+    '''
     regexp = re.compile(pattern)
 
     def parse(self, data):
         return str(value)
 
 
+class TimeDomain(Domain):
+    """
+    Represents a time data type.
+
+    Valid literal values: valid time values in the form `HH:MM[:SS[.SSSSSS]]`.
+
+    Valid native values: `datetime.time` objects.
+    """
+
+    # Regular expression to match HH:MM:SS.SSSSSS.
+    pattern = r'''(?x)
+        ^ \s*
+        (?P<hour> \d{1,2} )
+        : (?P<minute> \d{2} )
+        (?: : (?P<second> \d{2} )
+            (?: \. (?P<microsecond> \d+ ) )? )?
+        \s* $
+    '''
+    regexp = re.compile(pattern)
+
+    def parse(self, data):
+        # Sanity check on the argument.
+        assert isinstance(data, maybe(str))
+        # `None` represents `NULL` both in literal and native format.
+        if data is None:
+            return None
+        # Parse `data` as HH:MM:SS.SSS.
+        match = self.regexp.match(data)
+        if match is None:
+            raise ValueError("invalid time literal: expected a valid time"
+                             " in a 'HH:SS:MM.SSSSSS' format; got %r" % data)
+        hour = int(match.group('hour'))
+        minute = int(match.group('minute'))
+        second = match.group('second')
+        if second is not None:
+            second = int(second)
+        else:
+            second = 0
+        microsecond = match.group('microsecond')
+        if microsecond is not None:
+            if len(microsecond) < 6:
+                microsecond += '0'*(6-len(microsecond))
+            microsecond = microsecond[:6]
+            microsecond = int(microsecond)
+        else:
+            microsecond = 0
+        # Generate a `datetime.time` value; may fail if the time is not valid.
+        try:
+            value = datetime.time(hour, minute, second, microsecond)
+        except ValueError, exc:
+            raise ValueError("invalid time literal: %s" % exc)
+        return value
+
+    def dump(self, value):
+        # Sanity check on the argument.
+        assert isinstance(value, maybe(datetime.time))
+        # `None` represents `NULL` both in literal and native format.
+        if value is None:
+            return None
+        # `str` on `datetime.date` gives us the date in HH:MM:SS.SSSSSS format.
+        return str(value)
+
+
+class DateTimeDomain(Domain):
+    """
+    Represents a date and time data type.
+
+    Valid literal values: valid date and time values in the form
+    `YYYY-MM-DD HH:MM[:SS[.SSSSSS]]`.
+
+    Valid native values: `datetime.datetime` objects.
+    """
+
+    # Regular expression to match YYYY-MM-DD HH:MM:SS.SSSSSS.
+    pattern = r'''(?x)
+        ^ \s*
+        (?P<year> \d{4} )
+        - (?P<month> \d{2} )
+        - (?P<day> \d{2} )
+        (?:
+            (?: \s+ | [tT] )
+            (?P<hour> \d{1,2} )
+            : (?P<minute> \d{2} )
+            (?: : (?P<second> \d{2} )
+                (?: \. (?P<microsecond> \d+ ) )? )?
+        )?
+        (?:
+          \s*
+          (?: (?P<tz_utc> Z ) |
+              (?P<tz_sign> [+-] )
+              (?P<tz_hour> \d{1,2} )
+              (?: :
+                  (?P<tz_minute> \d{2} )
+              )? )
+        )?
+        \s* $
+    '''
+    regexp = re.compile(pattern)
+
+    def parse(self, data):
+        # Sanity check on the argument.
+        assert isinstance(data, maybe(str))
+        # `None` represents `NULL` both in literal and native format.
+        if data is None:
+            return None
+        # Parse `data` as YYYY-DD-MM HH:MM:SS.SSSSSS.
+        match = self.regexp.match(data)
+        if match is None:
+            raise ValueError("invalid datetime literal: expected a valid time"
+                             " in a 'YYYY-MM-DD HH:SS:MM.SSSSSS' format;"
+                             " got %r" % data)
+        year = int(match.group('year'))
+        month = int(match.group('month'))
+        day = int(match.group('day'))
+        hour = match.group('hour')
+        hour = int(hour) if hour is not None else 0
+        minute = match.group('minute')
+        minute = int(minute) if minute is not None else 0
+        second = match.group('second')
+        second = int(second) if second is not None else 0
+        microsecond = match.group('microsecond')
+        if microsecond is not None:
+            if len(microsecond) < 6:
+                microsecond += '0'*(6-len(microsecond))
+            microsecond = microsecond[:6]
+            microsecond = int(microsecond)
+        else:
+            microsecond = 0
+        tz_utc = match.group('tz_utc')
+        tz_sign = match.group('tz_sign')
+        tz_hour = match.group('tz_hour')
+        tz_minute = match.group('tz_minute')
+        if tz_utc is not None:
+            tz = UTC()
+        elif tz_sign is not None:
+            tz_hour = int(tz_hour)
+            tz_minute = int(tz_minute) if tz_minute is not None else 0
+            offset = tz_hour+60+tz_minute
+            if tz_sign == '-':
+                offset = -offset
+            tz = FixedTZ(offset)
+        else:
+            tz = None
+        # Generate a `datetime.datetime` value; may fail if the value is
+        # invalid.
+        try:
+            value = datetime.datetime(year, month, day, hour, minute, second,
+                                      microsecond, tz)
+        except ValueError, exc:
+            raise ValueError("invalid datetime literal: %s" % exc)
+        return value
+
+    def dump(self, value):
+        # Sanity check on the argument.
+        assert isinstance(value, maybe(datetime.datetime))
+        # `None` represents `NULL` both in literal and native format.
+        if value is None:
+            return None
+        # `str` on `datetime.datetime` gives us the date in ISO format.
+        return str(value)
+
+
 class OpaqueDomain(Domain):
     """
     Represents an unsupported SQL data type.

src/htsql/tr/fn/bind.py

 from ...adapter import Adapter, Component, adapts, adapts_many, named
 from ...domain import (Domain, UntypedDomain, BooleanDomain, StringDomain,
                        IntegerDomain, DecimalDomain, FloatDomain,
-                       DateDomain, EnumDomain)
+                       DateDomain, TimeDomain, DateTimeDomain, EnumDomain)
 from ..syntax import (NumberSyntax, StringSyntax, IdentifierSyntax,
                       SpecifierSyntax, FunctionCallSyntax)
 from ..binding import (LiteralBinding, SortBinding, SieveBinding,
                         HeadSig, TailSig, SliceSig, AtSig, ReplaceSig,
                         UpperSig, LowerSig, TrimSig,
                         DateIncrementSig, SubtractSig, DateDecrementSig,
-                        DateDifferenceSig, TodaySig,
+                        DateDifferenceSig, TodaySig, NowSig,
                         MultiplySig, DivideSig, IfSig, SwitchSig,
                         KeepPolaritySig, ReversePolaritySig,
                         RoundSig, RoundToSig, LengthSig,
     hint = """today() -> the current date"""
 
 
+class BindNow(BindMonoFunction):
+
+    named('now')
+    signature = NowSig
+    codomain = DateTimeDomain()
+
+
 class BindExtractYear(BindPolyFunction):
 
     named('year')

src/htsql/tr/fn/dump.py

                         MultiplySig, DivideSig, IfSig, SwitchSig,
                         ReversePolaritySig, RoundSig, RoundToSig,
                         LengthSig, LikeSig, ReplaceSig, SubstringSig,
-                        UpperSig, LowerSig, TrimSig, TodaySig, MakeDateSig,
-                        ExtractYearSig, ExtractMonthSig, ExtractDaySig,
-                        ExistsSig, CountSig, MinMaxSig, SumSig, AvgSig)
+                        UpperSig, LowerSig, TrimSig, TodaySig, NowSig,
+                        MakeDateSig, ExtractYearSig, ExtractMonthSig,
+                        ExtractDaySig, ExistsSig, CountSig, MinMaxSig,
+                        SumSig, AvgSig)
 
 
 class DumpFunction(DumpBySignature):
     template = "CURRENT_DATE"
 
 
+class DumpNow(DumpFunction):
+
+    adapts(NowSig)
+    template = "LOCALTIMESTAMP"
+
+
 class DumpMakeDate(DumpFunction):
 
     adapts(MakeDateSig)

src/htsql/tr/fn/signature.py

     pass
 
 
+class NowSig(NullarySig):
+    pass
+
+
 class AggregateSig(Signature):
 
     slots = [

src/htsql/util.py

 import sys
 import urllib
 import pkgutil
+import datetime, time
 
 
 #
         __import__(module_name)
 
 
+#
+# Timezone implementations.
+#
+
+
+class UTC(datetime.tzinfo):
+
+    def utcoffset(self, dt):
+        return datetime.timedelta(0)
+
+    def dst(self, dt):
+        return datetime.timedelta(0)
+
+    def tzname(self, dt):
+        return "Z"
+
+
+class FixedTZ(datetime.tzinfo):
+
+    def __init__(self, offset):
+        self.offset = offset
+
+    def utcoffset(self, dt):
+        return datetime.timedelta(minutes=self.offset)
+
+    def dst(self, dt):
+        return datetime.timedelta(0)
+
+    def tzname(self, dt):
+        hour = abs(self.offset) / 60
+        minute = abs(self.offset) % 60
+        sign = '+'
+        if self.offset < 0:
+            sign = '-'
+        if minute:
+            return "%s%02d:%02d" % (sign, hour, minute)
+        else:
+            return "%s%d" % (sign, hour)
+
+
+class LocalTZ(datetime.tzinfo):
+
+    def utcoffset(self, dt):
+        if self.isdst(dt):
+            return datetime.timedelta(seconds=-time.altzone)
+        else:
+            return datetime.timedelta(seconds=-time.timezone)
+
+    def dst(self, dt):
+        if self.isdst(dt):
+            return datetime.timedelta(seconds=(time.timezone-time.altzone))
+        else:
+            return datetime.timedelta(0)
+
+    def tzname(self, dt):
+        if self.isdst(dt):
+            offset = -time.altzone/60
+        else:
+            offset = -time.timezone/60
+        hour = abs(offset) / 60
+        minute = abs(offset) % 60
+        sign = '+'
+        if offset < 0:
+            sign = '-'
+        if minute:
+            return "%s%02d:%02d" % (sign, hour, minute)
+        else:
+            return "%s%d" % (sign, hour)
+
+    def isdst(self, dt):
+        tt = (dt.year, dt.month, dt.day,
+              dt.hour, dt.minute, dt.second,
+              dt.weekday(), 0, 0)
+        stamp = time.mktime(tt)
+        tt = time.localtime(stamp)
+        return tt.tm_isdst > 0
+
+

src/htsql_mssql/domain.py

 
 
 from htsql.domain import (Domain, BooleanDomain, IntegerDomain, DecimalDomain,
-                          FloatDomain, StringDomain, DateDomain, OpaqueDomain)
+                          FloatDomain, StringDomain, DateDomain, TimeDomain,
+                          DateTimeDomain, OpaqueDomain)
 
 
 class MSSQLDomain(Domain):
     """
 
 
+class MSSQLTimeDomain(MSSQLDomain, TimeDomain):
+    pass
+
+
+class MSSQLDateTimeDomain(MSSQLDomain, DateTimeDomain):
+    pass
+
+
 class MSSQLOpaqueDomain(MSSQLDomain, OpaqueDomain):
     """
     Represents an unsupported MS SQL Server data type.

src/htsql_mssql/tr/dump.py

                            DumpToDate)
 from htsql.tr.fn.dump import (DumpRound, DumpRoundTo, DumpLength,
                               DumpConcatenate, DumpSubstring, DumpTrim,
-                              DumpToday, DumpExtractYear, DumpExtractMonth,
-                              DumpExtractDay, DumpMakeDate,
+                              DumpToday, DumpNow, DumpExtractYear,
+                              DumpExtractMonth, DumpExtractDay, DumpMakeDate,
                               DumpDateIncrement, DumpDateDecrement,
                               DumpDateDifference)
 from htsql.tr.signature import FromPredicateSig, ToPredicateSig
     template = "CAST(FLOOR(CAST(GETDATE() AS FLOAT)) AS DATETIME)"
 
 
+class MSSQLDumpNow(DumpNow):
+
+    template = "GETDATE()"
+
+
 class MSSQLDumpExtractYear(DumpExtractYear):
 
     template = "DATEPART(YEAR, {op})"

src/htsql_mysql/domain.py

 
 from htsql.domain import (Domain, BooleanDomain, IntegerDomain, DecimalDomain,
                           FloatDomain, StringDomain, EnumDomain, DateDomain,
-                          OpaqueDomain)
+                          TimeDomain, DateTimeDomain, OpaqueDomain)
 
 
 class MySQLDomain(Domain):
     """
 
 
+class MySQLTimeDomain(MySQLDomain, TimeDomain):
+    pass
+
+
+class MySQLDateTimeDomain(MySQLDomain, DateTimeDomain):
+    pass
+
+
 class MySQLOpaqueDomain(MySQLDomain, OpaqueDomain):
     """
     Represents an unsupported MySQL data type.

src/htsql_oracle/domain.py

 
 
 from htsql.domain import (Domain, BooleanDomain, IntegerDomain, DecimalDomain,
-                          FloatDomain, StringDomain, DateDomain, OpaqueDomain)
+                          FloatDomain, StringDomain, DateDomain, TimeDomain,
+                          DateTimeDomain, OpaqueDomain)
 
 
 class OracleDomain(Domain):
     """
 
 
+class OracleTimeDomain(OracleDomain, TimeDomain):
+    pass
+
+
+class OracleDateTimeDomain(OracleDomain, DateTimeDomain):
+    pass
+
+
 class OracleOpaqueDomain(OracleDomain, OpaqueDomain):
     """
     Represents an unsupported Oracle data type.

src/htsql_pgsql/domain.py

 
 from htsql.domain import (Domain, BooleanDomain, IntegerDomain, FloatDomain,
                           DecimalDomain, StringDomain, EnumDomain, DateDomain,
-                          OpaqueDomain)
+                          TimeDomain, DateTimeDomain, OpaqueDomain)
 
 
 class PGDomain(Domain):
     """
 
 
+class PGTimeDomain(PGDomain, TimeDomain):
+    pass
+
+
+class PGDateTimeDomain(PGDomain, DateTimeDomain):
+    pass
+
+
 class PGOpaqueDomain(PGDomain, OpaqueDomain):
     """
     Represents an unsupported PostgreSQL data type.

src/htsql_sqlite/domain.py

 
 
 from htsql.domain import (Domain, BooleanDomain, IntegerDomain, FloatDomain,
-                          StringDomain, DateDomain, OpaqueDomain)
+                          StringDomain, DateDomain, TimeDomain, DateTimeDomain,
+                          OpaqueDomain)
 
 
 class SQLiteDomain(Domain):
     """
 
 
+class SQLiteTimeDomain(SQLiteDomain, TimeDomain):
+    pass
+
+
+class SQLiteDateTimeDomain(SQLiteDomain, DateTimeDomain):
+    pass
+
+
 class SQLiteOpaqueDomain(SQLiteDomain, OpaqueDomain):
     """
     Represents an unsupported SQLite data type.

src/htsql_sqlite/tr/dump.py

                               DumpDateIncrement, DumpDateDecrement,
                               DumpDateDifference, DumpMakeDate,
                               DumpExtractYear, DumpExtractMonth,
-                              DumpExtractDay)
+                              DumpExtractDay, DumpToday, DumpNow)
 from htsql.tr.error import SerializeError
 
 
             self.format("TRIM({op})", self.arguments)
 
 
+class SQLiteDumpToday(DumpToday):
+
+    template = "DATE('now', 'localtime')"
+
+
+class SQLiteDumpNow(DumpNow):
+
+    template = "DATETIME('now', 'localtime')"
+
+
 class SQLiteDumpDateIncrement(DumpDateIncrement):
 
     template = "DATE(JULIANDAY({lop}) + {rop})"

test/regress/input/library.yaml

 
 ########################################################################
 
+- title: DateTime Functions and Operators
+  tests:
+
+  # Construction
+  - uri: /{now()}
+    ignore: true
+
+########################################################################
+
 - title: Aggregate functions
   tests:
 

test/regress/output/mssql.yaml

              SELECT DATEADD(DAY, 6813, CAST('1991-08-20' AS DATETIME)),
                     DATEADD(DAY, -6813, CAST('2010-04-15' AS DATETIME)),
                     DATEDIFF(DAY, CAST('1991-08-20' AS DATETIME), CAST('2010-04-15' AS DATETIME))
+      - id: datetime-functions-and-operators
+        tests:
+        - uri: /{now()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             |                            |
+            -+----------------------------+-
+             | now()                      |
+            -+----------------------------+-
+             | 2011-03-02 04:35:17.717000 |
+                                    (1 row)
+
+             ----
+             /{now()}
+             SELECT GETDATE()
       - id: aggregate-functions
         tests:
         - uri: /course?department='lang'

test/regress/output/mysql.yaml

              SELECT ADDDATE(DATE('1991-08-20'), 6813),
                     SUBDATE(DATE('2010-04-15'), 6813),
                     DATEDIFF(DATE('2010-04-15'), DATE('1991-08-20'))
+      - id: datetime-functions-and-operators
+        tests:
+        - uri: /{now()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             |                     |
+            -+---------------------+-
+             | now()               |
+            -+---------------------+-
+             | 2011-03-22 22:34:30 |
+                             (1 row)
+
+             ----
+             /{now()}
+             SELECT LOCALTIMESTAMP
       - id: aggregate-functions
         tests:
         - uri: /course?department='lang'

test/regress/output/oracle.yaml

                     (DATE '2010-04-15' - 6813),
                     (DATE '2010-04-15' - DATE '1991-08-20')
              FROM DUAL "!"
+      - id: datetime-functions-and-operators
+        tests:
+        - uri: /{now()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             |                            |
+            -+----------------------------+-
+             | now()                      |
+            -+----------------------------+-
+             | 2011-03-02 00:12:09.550043 |
+                                    (1 row)
+
+             ----
+             /{now()}
+             SELECT LOCALTIMESTAMP
+             FROM DUAL "!"
       - id: aggregate-functions
         tests:
         - uri: /course?department='lang'

test/regress/output/pgsql.yaml

              SELECT ('1991-08-20'::DATE + 6813),
                     ('2010-04-15'::DATE - 6813),
                     ('2010-04-15'::DATE - '1991-08-20'::DATE)
+      - id: datetime-functions-and-operators
+        tests:
+        - uri: /{now()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             |                            |
+            -+----------------------------+-
+             | now()                      |
+            -+----------------------------+-
+             | 2011-03-22 22:34:15.476233 |
+                                    (1 row)
+
+             ----
+             /{now()}
+             SELECT LOCALTIMESTAMP
       - id: aggregate-functions
         tests:
         - uri: /course?department='lang'

test/regress/output/sqlite.yaml

              SELECT DATE(JULIANDAY('1991-08-20') + 6813),
                     DATE(JULIANDAY('2010-04-15') - 6813),
                     CAST(JULIANDAY('2010-04-15') - JULIANDAY('1991-08-20') AS INTEGER)
+      - id: datetime-functions-and-operators
+        tests:
+        - uri: /{now()}
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |2
+             |                     |
+            -+---------------------+-
+             | now()               |
+            -+---------------------+-
+             | 2011-03-22 22:33:41 |
+                             (1 row)
+
+             ----
+             /{now()}
+             SELECT DATETIME('now', 'localtime')
       - id: aggregate-functions
         tests:
         - uri: /course?department='lang'
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.