diff --git a/CHANGES.rst b/CHANGES.rst index 01d0d7d9..d0d6d412 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,7 @@ 0.8.5 (unreleased) ------------------ - +- Support TIMETZ datatype + (`Pull #229 `_) - Fix RelationKey unquoted issue (`Pull #228 `_) diff --git a/sqlalchemy_redshift/dialect.py b/sqlalchemy_redshift/dialect.py index 761d2d44..227ea65a 100644 --- a/sqlalchemy_redshift/dialect.py +++ b/sqlalchemy_redshift/dialect.py @@ -62,6 +62,7 @@ class RedshiftImpl(postgresql.PostgresqlImpl): 'VARCHAR', 'DOUBLE_PRECISION', 'TIMESTAMPTZ', + 'TIMETZ', 'CopyCommand', 'UnloadFromSelect', 'RedshiftDialect', 'Compression', 'Encoding', 'Format', 'CreateLibraryCommand', 'AlterTableAppendCommand', @@ -186,6 +187,24 @@ def __init__(self): super(TIMESTAMPTZ, self).__init__(timezone=True) +class TIMETZ(sa.dialects.postgresql.TIME): + """ + Redshift defines a TIMTETZ column type as an alias + of TIME WITH TIME ZONE. + https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html + + Adding an explicit type to the RedshiftDialect allows us follow the + SqlAlchemy conventions for "vendor-specific types." + + https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types + """ + + __visit_name__ = 'TIMETZ' + + def __init__(self): + super(TIMETZ, self).__init__(timezone=True) + + class RelationKey(namedtuple('RelationKey', ('name', 'schema'))): """ Structured tuple of table/view name and schema name. @@ -309,7 +328,7 @@ class RedshiftDDLCompiler(PGDDLCompiler): - The TIMESTAMPTZ column type is also supported in the DDL. + The TIMESTAMPTZ and TIMETZ column types are also supported in the DDL. For SQLAlchemy versions < 1.3.0, passing Redshift dialect options as keyword arguments is not supported on the column level. @@ -404,6 +423,9 @@ class RedshiftTypeCompiler(PGTypeCompiler): def visit_TIMESTAMPTZ(self, type_, **kw): return "TIMESTAMPTZ" + def visit_TIMETZ(self, type_, **kw): + return "TIMETZ" + class RedshiftIdentifierPreparer(PGIdentifierPreparer): reserved_words = RESERVED_WORDS diff --git a/tests/test_dialect_types.py b/tests/test_dialect_types.py index 9961057f..2c2bffa0 100644 --- a/tests/test_dialect_types.py +++ b/tests/test_dialect_types.py @@ -1,3 +1,4 @@ +import pytest import sqlalchemy_redshift.dialect import sqlalchemy @@ -32,11 +33,53 @@ def test_defined_types(): assert sqlalchemy_redshift.dialect.TIMESTAMPTZ \ is not sqlalchemy.sql.sqltypes.TIMESTAMP + assert sqlalchemy_redshift.dialect.TIMETZ \ + is not sqlalchemy.sql.sqltypes.TIME -def test_custom_type(): - timestamptz = sqlalchemy_redshift.dialect.TIMESTAMPTZ() - assert isinstance(timestamptz, sqlalchemy.sql.sqltypes.TIMESTAMP) +custom_type_inheritance = [ + ( + sqlalchemy_redshift.dialect.TIMESTAMP, + sqlalchemy.sql.sqltypes.TIMESTAMP + ), + ( + sqlalchemy_redshift.dialect.TIMETZ, + sqlalchemy.sql.sqltypes.TIME + ), +] + +@pytest.mark.parametrize("custom_type, super_type", custom_type_inheritance) +def test_custom_types_extend_super_type(custom_type, super_type): + custom_type_inst = custom_type() + assert isinstance(custom_type_inst, super_type) + + +column_and_ddl = [ + ( + sqlalchemy_redshift.dialect.TIMESTAMPTZ, + ( + u"\nCREATE TABLE t1 (" + u"\n\tid INTEGER NOT NULL, " + u"\n\tname VARCHAR, " + u"\n\ttest_col TIMESTAMPTZ, " + u"\n\tPRIMARY KEY (id)\n)\n\n" + ) + ), + ( + sqlalchemy_redshift.dialect.TIMETZ, + ( + u"\nCREATE TABLE t1 (" + u"\n\tid INTEGER NOT NULL, " + u"\n\tname VARCHAR, " + u"\n\ttest_col TIMETZ, " + u"\n\tPRIMARY KEY (id)\n)\n\n" + ) + ), +] + + +@pytest.mark.parametrize("custom_datatype, expected", column_and_ddl) +def test_custom_types_ddl_generation(custom_datatype, expected): compiler = sqlalchemy_redshift.dialect.RedshiftDDLCompiler( sqlalchemy_redshift.dialect.RedshiftDialect(), None ) @@ -45,18 +88,9 @@ def test_custom_type(): sqlalchemy.MetaData(), sqlalchemy.Column('id', sqlalchemy.INTEGER, primary_key=True), sqlalchemy.Column('name', sqlalchemy.String), - sqlalchemy.Column( - 'created_at', sqlalchemy_redshift.dialect.TIMESTAMPTZ - ) + sqlalchemy.Column('test_col', custom_datatype) ) create_table = sqlalchemy.schema.CreateTable(table) actual = compiler.process(create_table) - expected = ( - u"\nCREATE TABLE t1 (" - u"\n\tid INTEGER NOT NULL, " - u"\n\tname VARCHAR, " - u"\n\tcreated_at TIMESTAMPTZ, " - u"\n\tPRIMARY KEY (id)\n)\n\n" - ) assert expected == actual