Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mathesar-424 Handle TIMESTAMP data type in the backend #865

Merged
merged 17 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
670aeca
mathesar-424 Handle `TIMESTAMP` data type in the backend
silentninja Dec 3, 2021
3bd0b7c
Exclude comparing null values when casting to datetime types as it re…
silentninja Dec 6, 2021
ec34603
Remove timestamp data type from types_map as it is replaced by timest…
silentninja Dec 6, 2021
1a2cdf5
Merge branch 'master' into timestamp-type
silentninja Dec 13, 2021
8ed57e2
Fix lint errors
silentninja Dec 13, 2021
4bac3f5
Merge remote-tracking branch 'origin/master' into timestamp-type
silentninja Dec 20, 2021
569a4e5
Sort Timestamp datatype variable alphabetically
silentninja Dec 17, 2021
a49bc7f
Merge branch 'master' into timestamp-type
silentninja Dec 21, 2021
a00c6a3
Allow timestamp with timezone and without timezone to accept string w…
silentninja Dec 21, 2021
421bd92
Remove date comparison when casting to timestamp without timezone, so…
silentninja Dec 22, 2021
33c0843
Convert function used to cast to different data type as a strict func…
silentninja Dec 23, 2021
a32238c
Fix datetime casting function to set timezone locally instead of glob…
silentninja Dec 23, 2021
349981b
Merge branch 'master' into timestamp-type
silentninja Dec 23, 2021
93df286
Merge branch 'master' into timestamp-type
mathemancer Jan 6, 2022
75c88d3
Change casting function parameter from `Strict` to `RETURNS NULL ON N…
silentninja Jan 6, 2022
149a86b
Add comments to warn about the timezone leak when calling the functio…
silentninja Jan 6, 2022
03bc152
Merge branch 'master' into timestamp-type
silentninja Jan 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def engine(test_db):
test_db,
),
future=True,
# Setting a fixed timezone makes the timezone aware test cases predictable.
connect_args={"options": "-c timezone=utc"}
)


Expand Down
4 changes: 3 additions & 1 deletion db/columns/operations/infer_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
],
base.STRING: [
base.PostgresType.BOOLEAN.value,
base.PostgresType.NUMERIC.value,
base.PostgresType.DATE.value,
base.PostgresType.NUMERIC.value,
base.PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE.value,
base.PostgresType.TIMESTAMP_WITH_TIME_ZONE.value,
# We only infer to TIME_WITHOUT_TIME_ZONE as time zones don't make much sense
# without additional date information. See postgres documentation for further
# details: https://www.postgresql.org/docs/13/datatype-datetime.html
Expand Down
2 changes: 2 additions & 0 deletions db/tests/columns/operations/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def _check_duplicate_unique_constraint(
'TEXT',
'TIME WITH TIME ZONE',
'TIME WITHOUT TIME ZONE',
'TIMESTAMP WITH TIME ZONE',
'TIMESTAMP WITHOUT TIME ZONE',
'VARCHAR',
}

Expand Down
12 changes: 11 additions & 1 deletion db/tests/tables/operations/test_infer_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,18 @@
(String, ["t", "false", "2", "0"], VARCHAR),
(String, ["a", "cat", "mat", "bat"], VARCHAR),
(String, ["2", "1", "0", "0"], NUMERIC),
(String, ["2000-01-12", "6/23/2004", "May-2007-29", "20200909"], DATE),
(String, ["2000-01-12", "6/23/2004", "May-2007-29", "May-2007-29 00:00:00+0", "20200909"], DATE),
(String, ["9:24+01", "23:12", "03:04:05", "3:4:5"], datetime.TIME_WITHOUT_TIME_ZONE),
(
String,
["2000-01-12 9:24", "6/23/2004 23:12", "May-2007-29 03:04:05", "May-2007-29 5:00:00+0", "May-2007-29", "20200909 3:4:5"],
datetime.TIMESTAMP_WITHOUT_TIME_ZONE
),
(
String,
["2000-01-12 9:24-3", "6/23/2004 23:12+01", "May-2007-29 03:04:05", "May-2007-29", "20200909 3:4:5+01:30"],
datetime.TIMESTAMP_WITH_TIME_ZONE
),
(
String,
["alice@example.com", "bob@example.com", "jon.doe@example.ca"],
Expand Down
123 changes: 123 additions & 0 deletions db/tests/types/operations/test_cast.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import timedelta, date, time
from datetime import datetime as py_datetime
from decimal import Decimal

import pytest
Expand Down Expand Up @@ -39,6 +40,8 @@
SMALLINT = PostgresType.SMALLINT.value.upper()
TIME_WITHOUT_TIME_ZONE = PostgresType.TIME_WITHOUT_TIME_ZONE.value.upper()
TIME_WITH_TIME_ZONE = PostgresType.TIME_WITH_TIME_ZONE.value.upper()
TIMESTAMP_WITH_TIME_ZONE = PostgresType.TIMESTAMP_WITH_TIME_ZONE.value.upper()
TIMESTAMP_WITHOUT_TIME_ZONE = PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE.value.upper()
TEXT = PostgresType.TEXT.value.upper()

CHAR = "CHAR"
Expand Down Expand Up @@ -148,6 +151,8 @@
REAL: {VALID: [("1", 1.0)], INVALID: ["b"]},
SMALLINT: {VALID: [("4", 4)], INVALID: ["j"]},
DATE: {VALID: [], INVALID: ["n"]},
TIMESTAMP_WITH_TIME_ZONE: {VALID: [], INVALID: ["n"]},
TIMESTAMP_WITHOUT_TIME_ZONE: {VALID: [], INVALID: ["n"]},
TEXT: {VALID: [("a", "a")]},
URI: {VALID: [], INVALID: ["a"]},
VARCHAR: {VALID: [("a", "a")]},
Expand All @@ -161,6 +166,20 @@
DATE: {VALID: [(date(1999, 1, 18), date(1999, 1, 18))]},
TEXT: {VALID: [(date(1999, 1, 18), "1999-01-18")]},
VARCHAR: {VALID: [(date(1999, 1, 18), "1999-01-18")]},
TIMESTAMP_WITH_TIME_ZONE: {
VALID: [(
date(1999, 1, 18),
py_datetime(1999, 1, 18, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0))
),
]
},
TIMESTAMP_WITHOUT_TIME_ZONE: {
VALID: [(
date(1999, 1, 18),
py_datetime(1999, 1, 18, 0, 0, 0)
),
]
},
},
},
DECIMAL: {
Expand Down Expand Up @@ -446,6 +465,62 @@
},
},
},
TIMESTAMP_WITH_TIME_ZONE: {
ISCHEMA_NAME: PostgresType.TIMESTAMP_WITH_TIME_ZONE.value,
REFLECTED_NAME: TIMESTAMP_WITH_TIME_ZONE,
TARGET_DICT: {
CHAR: {VALID: []},
DATE: {VALID: [(py_datetime(1999, 1, 18, 0, 0, 0), date(1999, 1, 18)),
(
py_datetime(1999, 1, 18, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0)), date(1999, 1, 18))],
INVALID: [py_datetime(1999, 1, 18, 12, 30, 45),
py_datetime(1999, 1, 18, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=60))
]
},
TIMESTAMP_WITH_TIME_ZONE: {
VALID: [
(py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60)),
py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60))),
]
},
TIMESTAMP_WITHOUT_TIME_ZONE: {
VALID: [(py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0)),
py_datetime(1999, 1, 18, 12, 30, 45)
)],
},
TEXT: {
VALID: [
(py_datetime(1999, 1, 18, 12, 30, 45),
"1999-01-18 12:30:45+00")
]
},
VARCHAR: {
VALID: [
(py_datetime(1999, 1, 18, 12, 30, 45),
"1999-01-18 12:30:45+00")
]
},
},
},
TIMESTAMP_WITHOUT_TIME_ZONE: {
ISCHEMA_NAME: PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE.value,
REFLECTED_NAME: TIMESTAMP_WITHOUT_TIME_ZONE,
TARGET_DICT: {
CHAR: {VALID: []},
DATE: {VALID: [(py_datetime(1999, 1, 18, 0, 0, 0), date(1999, 1, 18))],
INVALID: [(py_datetime(1999, 1, 18, 12, 30, 45), date(1999, 1, 18))]},
TIMESTAMP_WITHOUT_TIME_ZONE: {
VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), py_datetime(1999, 1, 18, 12, 30, 45))]
},
TIMESTAMP_WITH_TIME_ZONE: {
VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), py_datetime(1999, 1, 18, 12, 30, 45,
tzinfo=FixedOffsetTimezone(offset=0)))
]
},
TEXT: {VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), "1999-01-18 12:30:45")]},
VARCHAR: {VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), "1999-01-18 12:30:45")]},
},
},
TEXT: {
ISCHEMA_NAME: PostgresType.TEXT.value,
REFLECTED_NAME: TEXT,
Expand Down Expand Up @@ -551,6 +626,25 @@
"not a time",
]
},
TIMESTAMP_WITH_TIME_ZONE: {
VALID: [
("1999-01-18 12:30:45+00",
py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0)),
)
],
INVALID: [
"not a timestamp",
]
},
TIMESTAMP_WITHOUT_TIME_ZONE: {
VALID: [
("1999-01-18 12:30:45", py_datetime(1999, 1, 18, 12, 30, 45),
)
],
INVALID: [
"not a timestamp",
]
},
VARCHAR: {VALID: [("a string", "a string")]},
}
},
Expand Down Expand Up @@ -661,6 +755,25 @@
"not a time",
]
},
TIMESTAMP_WITH_TIME_ZONE: {
VALID: [
("1999-01-18 12:30:45+00",
py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0)),
)
],
INVALID: [
"not a timestamp",
]
},
TIMESTAMP_WITHOUT_TIME_ZONE: {
VALID: [
("1999-01-18 12:30:45", py_datetime(1999, 1, 18, 12, 30, 45),
)
],
INVALID: [
"not a timestamp",
]
},
URI: {
VALID: [("https://centerofci.org", "https://centerofci.org")],
INVALID: ["/sdf/"]
Expand Down Expand Up @@ -722,6 +835,12 @@ def test_get_alter_column_types_with_unfriendly_names(engine_with_types):
] + [
(val[ISCHEMA_NAME], "time with time zone", {"precision": 5}, "TIME(5) WITH TIME ZONE")
for val in MASTER_DB_TYPE_MAP_SPEC.values() if TIME_WITH_TIME_ZONE in val[TARGET_DICT]
] + [
(val[ISCHEMA_NAME], "timestamp with time zone", {"precision": 5}, "TIMESTAMP(5) WITH TIME ZONE")
for val in MASTER_DB_TYPE_MAP_SPEC.values() if TIMESTAMP_WITH_TIME_ZONE in val[TARGET_DICT]
] + [
(val[ISCHEMA_NAME], "timestamp without time zone", {"precision": 5}, "TIMESTAMP(5) WITHOUT TIME ZONE")
for val in MASTER_DB_TYPE_MAP_SPEC.values() if TIMESTAMP_WITHOUT_TIME_ZONE in val[TARGET_DICT]
] + [
(val[ISCHEMA_NAME], "char", {"length": 5}, "CHAR(5)")
for val in MASTER_DB_TYPE_MAP_SPEC.values() if CHAR in val[TARGET_DICT]
Expand Down Expand Up @@ -786,6 +905,10 @@ def test_alter_column_type_alters_column_type(
(datetime.TIME_WITH_TIME_ZONE, "time with time zone", {"precision": 0},
time(0, 0, 0, 9, tzinfo=FixedOffsetTimezone(offset=0)),
time(0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0))),
(datetime.TIMESTAMP_WITH_TIME_ZONE, "timestamp with time zone", {"precision": 0},
py_datetime(1999, 1, 1, 0, 0, 0), py_datetime(1999, 1, 1, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0))),
(datetime.TIMESTAMP_WITHOUT_TIME_ZONE, "timestamp without time zone", {"precision": 0},
py_datetime(1999, 1, 1, 0, 0, 0), py_datetime(1999, 1, 1, 0, 0, 0)),
(String, "char", {"length": 5}, "abcde", "abcde"),
]

Expand Down
2 changes: 2 additions & 0 deletions db/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
money.DB_TYPE: money.Money,
datetime.TIME_ZONE_DB_TYPE: datetime.TIME_WITH_TIME_ZONE,
datetime.WITHOUT_TIME_ZONE_DB_TYPE: datetime.TIME_WITHOUT_TIME_ZONE,
datetime.TIMESTAMP_TIME_ZONE_DB_TYPE: datetime.TIMESTAMP_WITH_TIME_ZONE,
datetime.TIMESTAMP_WITHOUT_TIME_ZONE_DB_TYPE: datetime.TIMESTAMP_WITHOUT_TIME_ZONE,
uri.DB_TYPE: uri.URI
}
4 changes: 2 additions & 2 deletions db/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class PostgresType(Enum):
TIME_WITH_TIME_ZONE = 'time with time zone'
TIME_WITHOUT_TIME_ZONE = 'time without time zone'
TIMESTAMP = 'timestamp'
TIMESTAMP_WITH_TIMESTAMP_ZONE = 'timestamp with time zone'
TIMESTAMP_WITHOUT_TIMESTAMP_ZONE = 'timestamp without time zone'
TIMESTAMP_WITH_TIME_ZONE = 'timestamp with time zone'
TIMESTAMP_WITHOUT_TIME_ZONE = 'timestamp without time zone'
TSRANGE = 'tsrange'
TSTZRANGE = 'tstzrange'
TSVECTOR = 'tsvector'
Expand Down
29 changes: 29 additions & 0 deletions db/types/datetime.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from sqlalchemy.dialects.postgresql.base import TIME as SA_TIME
from sqlalchemy.dialects.postgresql.base import TIMESTAMP as SA_TIMESTAMP

from db.types import base

TIME_ZONE_DB_TYPE = base.PostgresType.TIME_WITH_TIME_ZONE.value
WITHOUT_TIME_ZONE_DB_TYPE = base.PostgresType.TIME_WITHOUT_TIME_ZONE.value
TIMESTAMP_TIME_ZONE_DB_TYPE = base.PostgresType.TIMESTAMP_WITH_TIME_ZONE.value
TIMESTAMP_WITHOUT_TIME_ZONE_DB_TYPE = base.PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE.value


class TIME_WITHOUT_TIME_ZONE(SA_TIME):
Expand All @@ -30,3 +33,29 @@ def __init__(self, *args, precision=None, **kwargs):
@classmethod
def __str__(cls):
return cls.__name__


class TIMESTAMP_WITHOUT_TIME_ZONE(SA_TIMESTAMP):
def __init__(self, *args, precision=None, **kwargs):
# On reflection timezone is passed as a kwarg, so we need to make sure it isn't
# included in the TIMESTAMP init call twice
if "timezone" in kwargs:
kwargs.pop("timezone")
super().__init__(*args, timezone=False, precision=precision, **kwargs)

@classmethod
def __str__(cls):
return cls.__name__


class TIMESTAMP_WITH_TIME_ZONE(SA_TIMESTAMP):
def __init__(self, *args, precision=None, **kwargs):
# On reflection timezone is passed as a kwarg, so we need to make sure it isn't
# included in the TIMESTAMP init call twice
if "timezone" in kwargs:
kwargs.pop("timezone")
super().__init__(*args, timezone=True, precision=precision, **kwargs)

@classmethod
def __str__(cls):
return cls.__name__
Loading