Skip to content

Commit aa9f151

Browse files
committed
feat(mssql): implement inference for DATETIME2 and DATETIMEOFFSET
1 parent 099d1ec commit aa9f151

File tree

4 files changed

+50
-17
lines changed

4 files changed

+50
-17
lines changed

ibis/backends/base/sql/alchemy/datatypes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,20 @@ def sa_datetime(_, satype, nullable=True, default_timezone='UTC'):
426426
return dt.Timestamp(timezone=timezone, nullable=nullable)
427427

428428

429+
@dt.dtype.register(MSDialect, mssql.DATETIMEOFFSET)
430+
def _datetimeoffset(_, sa_type, nullable=True):
431+
if (prec := sa_type.precision) is None:
432+
prec = 7
433+
return dt.Timestamp(scale=prec, timezone="UTC", nullable=nullable)
434+
435+
436+
@dt.dtype.register(MSDialect, mssql.DATETIME2)
437+
def _datetime2(_, sa_type, nullable=True):
438+
if (prec := sa_type.precision) is None:
439+
prec = 7
440+
return dt.Timestamp(scale=prec, nullable=nullable)
441+
442+
429443
@dt.dtype.register(PGDialect, sa.ARRAY)
430444
def sa_pg_array(dialect, satype, nullable=True):
431445
dimensions = satype.dimensions

ibis/backends/mssql/datatypes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def _type_from_result_set_info(col: _FieldDescription) -> dt.DataType:
2929
typ = partial(typ, precision=col["precision"], scale=col['scale'])
3030
elif typename in ("GEOMETRY", "GEOGRAPHY"):
3131
typ = partial(typ, geotype=typename.lower())
32+
elif typename == 'DATETIME2':
33+
typ = partial(typ, scale=col["scale"])
34+
elif typename == 'DATETIMEOFFSET':
35+
typ = partial(typ, scale=col["scale"], timezone="UTC")
3236
elif typename == 'FLOAT':
3337
if col['precision'] <= 24:
3438
typ = dt.Float32
@@ -98,3 +102,13 @@ def _type_from_result_set_info(col: _FieldDescription) -> dt.DataType:
98102
@to_sqla_type.register(mssql.dialect, tuple(_MSSQL_TYPE_MAP.keys()))
99103
def _simple_types(_, itype):
100104
return _MSSQL_TYPE_MAP[type(itype)]
105+
106+
107+
@to_sqla_type.register(mssql.dialect, dt.Timestamp)
108+
def _datetime(_, itype):
109+
if (precision := itype.scale) is None:
110+
precision = 7
111+
if itype.timezone is not None:
112+
return mssql.DATETIMEOFFSET(precision=precision)
113+
else:
114+
return mssql.DATETIME2(precision=precision)

ibis/backends/mssql/tests/test_client.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
# Date and time
2929
('DATE', dt.date),
3030
('TIME', dt.time),
31-
('DATETIME2', dt.timestamp),
32-
('DATETIMEOFFSET', dt.timestamp),
31+
('DATETIME2', dt.timestamp(scale=7)),
32+
('DATETIMEOFFSET', dt.timestamp(scale=7, timezone="UTC")),
3333
('SMALLDATETIME', dt.timestamp),
3434
('DATETIME', dt.timestamp),
3535
# Characters strings
@@ -54,13 +54,27 @@
5454
not geospatial_supported, reason="geospatial dependencies not installed"
5555
)
5656

57+
broken_sqlalchemy_autoload = pytest.mark.xfail(
58+
reason="scale not inferred by sqlalchemy autoload"
59+
)
60+
5761

5862
@pytest.mark.parametrize(
5963
("server_type", "expected_type"),
6064
DB_TYPES
6165
+ [
6266
param("GEOMETRY", dt.geometry, marks=[skipif_no_geospatial_deps]),
6367
param("GEOGRAPHY", dt.geography, marks=[skipif_no_geospatial_deps]),
68+
]
69+
+ [
70+
param(
71+
'DATETIME2(4)', dt.timestamp(scale=4), marks=[broken_sqlalchemy_autoload]
72+
),
73+
param(
74+
'DATETIMEOFFSET(5)',
75+
dt.timestamp(scale=5, timezone="UTC"),
76+
marks=[broken_sqlalchemy_autoload],
77+
),
6478
],
6579
ids=str,
6680
)
@@ -73,9 +87,9 @@ def test_get_schema_from_query(con, server_type, expected_type):
7387
c.execute(sa.text(f"CREATE TABLE {name} (x {server_type})"))
7488
expected_schema = ibis.schema(dict(x=expected_type))
7589
result_schema = con._get_schema_using_query(f"SELECT * FROM {name}")
90+
assert result_schema == expected_schema
7691
t = con.table(raw_name)
7792
assert t.schema() == expected_schema
78-
assert result_schema == expected_schema
7993
finally:
8094
with con.begin() as c:
8195
c.execute(sa.text(f"DROP TABLE IF EXISTS {name}"))

ibis/backends/tests/test_temporal.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def test_temporal_binop_pandas_timedelta(
463463

464464

465465
@pytest.mark.parametrize("func_name", ["gt", "ge", "lt", "le", "eq", "ne"])
466-
@pytest.mark.notimpl(["bigquery", "mssql"])
466+
@pytest.mark.notimpl(["bigquery"])
467467
def test_timestamp_comparison_filter(backend, con, alltypes, df, func_name):
468468
ts = pd.Timestamp('20100302', tz="UTC").to_pydatetime()
469469

@@ -490,7 +490,7 @@ def test_timestamp_comparison_filter(backend, con, alltypes, df, func_name):
490490
"ne",
491491
],
492492
)
493-
@pytest.mark.notimpl(["bigquery", "mssql"])
493+
@pytest.mark.notimpl(["bigquery"])
494494
def test_timestamp_comparison_filter_numpy(backend, con, alltypes, df, func_name):
495495
ts = np.datetime64('2010-03-02 00:00:00.000123')
496496

@@ -993,21 +993,12 @@ def test_large_timestamp(con):
993993
id="ns",
994994
marks=[
995995
pytest.mark.broken(
996-
[
997-
"clickhouse",
998-
"duckdb",
999-
"impala",
1000-
"mssql",
1001-
"postgres",
1002-
"pyspark",
1003-
"sqlite",
1004-
"trino",
1005-
],
996+
["clickhouse", "duckdb", "impala", "pyspark", "trino"],
1006997
reason="drivers appear to truncate nanos",
1007998
),
1008999
pytest.mark.notyet(
1009-
["bigquery"],
1010-
reason="bigquery doesn't support nanosecond timestamps",
1000+
["bigquery", "mssql", "postgres", "sqlite"],
1001+
reason="doesn't support nanoseconds",
10111002
),
10121003
],
10131004
),

0 commit comments

Comments
 (0)