@@ -29,6 +29,9 @@ class DatabaseTestConfig:
2929 create_connection_args : Callable [..., list [str ]]
3030 # Whether this DB uses LIMIT syntax (False for MSSQL TOP, Oracle FETCH FIRST)
3131 uses_limit : bool = True
32+ # Timezone-aware datetime type name (None if not supported)
33+ # e.g., "DATETIMEOFFSET" for MSSQL, "TIMESTAMPTZ" for PostgreSQL
34+ timezone_datetime_type : str | None = None
3235
3336
3437class BaseDatabaseTests (ABC ):
@@ -822,6 +825,136 @@ def test_get_index_definition(self, request):
822825 assert isinstance (info , dict ), "get_index_definition should return a dict"
823826 assert "name" in info , "Index info should contain 'name'"
824827
828+ def test_timezone_aware_datetime (self , request ):
829+ """Test that timezone-aware datetime columns can be queried.
830+
831+ This tests that databases with timezone-aware datetime types (like
832+ DATETIMEOFFSET, TIMESTAMPTZ, TIMESTAMP WITH TIME ZONE) can be queried
833+ without errors. This is important because some drivers (like pyodbc)
834+ don't natively support these types and need custom converters.
835+
836+ See: https://github.com/mkleehammer/pyodbc/issues/134
837+ """
838+ if self .config .timezone_datetime_type is None :
839+ pytest .skip (f"{ self .config .display_name } does not have a timezone-aware datetime type" )
840+
841+ from sqlit .config import load_connections
842+ from sqlit .db .adapters import get_adapter
843+ from sqlit .services .session import ConnectionSession
844+
845+ connection_name = request .getfixturevalue (self .config .connection_fixture )
846+ connections = load_connections ()
847+ config = next ((c for c in connections if c .name == connection_name ), None )
848+ assert config is not None , f"Connection { connection_name } not found"
849+
850+ tz_type = self .config .timezone_datetime_type
851+
852+ with ConnectionSession .create (config , get_adapter ) as session :
853+ conn = session .connection
854+ adapter = session .adapter
855+
856+ # Create a test table with timezone-aware datetime column
857+ # Use a unique table name to avoid conflicts
858+ table_name = "test_tz_datetime"
859+
860+ # Drop table if exists (database-specific syntax)
861+ try :
862+ if self .config .db_type == "mssql" :
863+ adapter .execute_non_query (conn , f"""
864+ IF OBJECT_ID('{ table_name } ', 'U') IS NOT NULL
865+ DROP TABLE { table_name }
866+ """ )
867+ elif self .config .db_type == "oracle" :
868+ try :
869+ adapter .execute_non_query (conn , f"DROP TABLE { table_name } " )
870+ except Exception :
871+ pass # Table doesn't exist
872+ else :
873+ adapter .execute_non_query (conn , f"DROP TABLE IF EXISTS { table_name } " )
874+ except Exception :
875+ pass # Ignore errors from DROP
876+
877+ # Create table with timezone-aware datetime
878+ if self .config .db_type == "oracle" :
879+ create_sql = f"""
880+ CREATE TABLE { table_name } (
881+ id NUMBER PRIMARY KEY,
882+ event_name VARCHAR2(100),
883+ event_time { tz_type }
884+ )
885+ """
886+ else :
887+ create_sql = f"""
888+ CREATE TABLE { table_name } (
889+ id INT PRIMARY KEY,
890+ event_name VARCHAR(100),
891+ event_time { tz_type }
892+ )
893+ """
894+ adapter .execute_non_query (conn , create_sql )
895+
896+ # Insert test data with timezone info
897+ if self .config .db_type == "mssql" :
898+ insert_sql = f"""
899+ INSERT INTO { table_name } (id, event_name, event_time) VALUES
900+ (1, 'Event UTC', '2024-06-15 12:00:00.000000 +00:00'),
901+ (2, 'Event EST', '2024-06-15 08:00:00.000000 -04:00'),
902+ (3, 'Event IST', '2024-06-15 17:30:00.000000 +05:30')
903+ """
904+ elif self .config .db_type == "oracle" :
905+ # Oracle uses different syntax for TIMESTAMP WITH TIME ZONE
906+ adapter .execute_non_query (conn , f"""
907+ INSERT INTO { table_name } (id, event_name, event_time) VALUES
908+ (1, 'Event UTC', TIMESTAMP '2024-06-15 12:00:00 +00:00')
909+ """ )
910+ adapter .execute_non_query (conn , f"""
911+ INSERT INTO { table_name } (id, event_name, event_time) VALUES
912+ (2, 'Event EST', TIMESTAMP '2024-06-15 08:00:00 -04:00')
913+ """ )
914+ adapter .execute_non_query (conn , f"""
915+ INSERT INTO { table_name } (id, event_name, event_time) VALUES
916+ (3, 'Event IST', TIMESTAMP '2024-06-15 17:30:00 +05:30')
917+ """ )
918+ insert_sql = None
919+ else :
920+ # PostgreSQL, CockroachDB, DuckDB use standard syntax
921+ insert_sql = f"""
922+ INSERT INTO { table_name } (id, event_name, event_time) VALUES
923+ (1, 'Event UTC', '2024-06-15 12:00:00+00'),
924+ (2, 'Event EST', '2024-06-15 08:00:00-04'),
925+ (3, 'Event IST', '2024-06-15 17:30:00+05:30')
926+ """
927+
928+ if insert_sql :
929+ adapter .execute_non_query (conn , insert_sql )
930+
931+ # Query the table - this is where type -155 errors would occur
932+ columns , rows , truncated = adapter .execute_query (
933+ conn , f"SELECT * FROM { table_name } ORDER BY id"
934+ )
935+
936+ # Verify we got results
937+ assert len (columns ) == 3 , f"Expected 3 columns, got { len (columns )} "
938+ assert len (rows ) == 3 , f"Expected 3 rows, got { len (rows )} "
939+
940+ # Verify the data is readable (timezone info should be present in some form)
941+ for row in rows :
942+ event_time = row [2 ]
943+ assert event_time is not None , "event_time should not be None"
944+ # The value should be either a datetime object or a string representation
945+ event_time_str = str (event_time )
946+ assert "2024" in event_time_str , f"Expected year 2024 in { event_time_str } "
947+
948+ # Clean up
949+ try :
950+ if self .config .db_type == "oracle" :
951+ adapter .execute_non_query (conn , f"DROP TABLE { table_name } " )
952+ else :
953+ adapter .execute_non_query (conn , f"DROP TABLE IF EXISTS { table_name } " )
954+ except Exception :
955+ pass
956+
957+
825958class BaseDatabaseTestsWithLimit (BaseDatabaseTests ):
826959 """Base tests for databases that support LIMIT syntax."""
827960
0 commit comments