Skip to content

Commit 7ba8630

Browse files
committed
Support the "datetime2" type.
1 parent 63893fc commit 7ba8630

12 files changed

Lines changed: 499 additions & 159 deletions

File tree

SqlServerSimulator.Tests.Internal/Storage/RowRoundTripTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,57 @@ public void EncodedDateRow_StoresDayCountLittleEndian()
221221
AreEqual(7, BitConverter.ToUInt16(bytes, 2)); // fixed-length end offset (4 + 3)
222222
CollectionAssert.AreEqual(new byte[] { 0x9B, 0x49, 0x0B }, bytes[4..7]);
223223
}
224+
225+
[TestMethod]
226+
[DataRow(0, 6)] // precision 0-2: 6 bytes (3 time + 3 date)
227+
[DataRow(1, 6)]
228+
[DataRow(2, 6)]
229+
[DataRow(3, 7)] // precision 3-4: 7 bytes (4 time + 3 date)
230+
[DataRow(4, 7)]
231+
[DataRow(5, 8)] // precision 5-7: 8 bytes (5 time + 3 date)
232+
[DataRow(6, 8)]
233+
[DataRow(7, 8)]
234+
public void DateTime2_FixedLength_FollowsPrecisionTier(int precision, int expectedBytes) =>
235+
AreEqual(expectedBytes, SqlType.GetDateTime2(precision).FixedLength);
236+
237+
[TestMethod]
238+
[DataRow(0)]
239+
[DataRow(3)]
240+
[DataRow(7)]
241+
public void SingleColumn_DateTime2_RoundTrips(int precision)
242+
{
243+
var type = SqlType.GetDateTime2(precision);
244+
var v = SqlValue.FromDateTime2(type, new DateTime(2026, 5, 4, 13, 45, 30));
245+
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [v]));
246+
AreEqual(1, decoded.Length);
247+
AreEqual(v, decoded[0]);
248+
}
249+
250+
[TestMethod]
251+
public void SingleColumn_DateTime2_PreservesAllFractionalTicks()
252+
{
253+
var type = SqlType.GetDateTime2(7);
254+
var v = SqlValue.FromDateTime2(type, new DateTime(2026, 5, 4, 13, 45, 30).AddTicks(1234567));
255+
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [v]));
256+
AreEqual(v, decoded[0]);
257+
}
258+
259+
[TestMethod]
260+
public void SingleColumn_DateTime2Null_RoundTrips()
261+
{
262+
var type = SqlType.GetDateTime2(7);
263+
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [SqlValue.Null(type)]));
264+
IsTrue(decoded[0].IsNull);
265+
AreSame(type, decoded[0].Type);
266+
}
267+
268+
[TestMethod]
269+
public void SingleColumn_DateTime2_AtRangeBoundaries()
270+
{
271+
var type = SqlType.GetDateTime2(7);
272+
var min = SqlValue.FromDateTime2(type, DateTime.MinValue);
273+
var max = SqlValue.FromDateTime2(type, new DateTime(9999, 12, 31, 23, 59, 59).AddTicks(9_999_999));
274+
AreEqual(min, RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [min]))[0]);
275+
AreEqual(max, RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [max]))[0]);
276+
}
224277
}

SqlServerSimulator.Tests.Internal/Storage/SqlValueTests.cs

Lines changed: 9 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Data.Common;
21
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
32

43
namespace SqlServerSimulator.Storage;
@@ -170,59 +169,6 @@ public void CoerceTo_BigIntToInt32_OutOfRangeOverflows() =>
170169
public void CoerceTo_BetweenStringAndInt_NotSupported() =>
171170
Throws<NotSupportedException>(() => SqlValue.FromVarchar("42").CoerceTo(SqlType.Int32));
172171

173-
[TestMethod]
174-
public void FromDate_RoundTripsViaAsDate()
175-
{
176-
var v = SqlValue.FromDate(new DateOnly(2026, 5, 4));
177-
IsFalse(v.IsNull);
178-
AreSame(SqlType.Date, v.Type);
179-
AreEqual(new DateOnly(2026, 5, 4), v.AsDate);
180-
}
181-
182-
[TestMethod]
183-
[DataRow(1, 1, 1)] // 0001-01-01: minimum
184-
[DataRow(9999, 12, 31)] // 9999-12-31: maximum
185-
[DataRow(2026, 5, 4)]
186-
[DataRow(1900, 1, 1)]
187-
public void FromDate_PreservesAcrossSupportedRange(int year, int month, int day)
188-
{
189-
var d = new DateOnly(year, month, day);
190-
AreEqual(d, SqlValue.FromDate(d).AsDate);
191-
}
192-
193-
[TestMethod]
194-
public void Date_Equals_ComparesDayValue()
195-
{
196-
AreEqual(SqlValue.FromDate(new DateOnly(2026, 5, 4)), SqlValue.FromDate(new DateOnly(2026, 5, 4)));
197-
AreNotEqual(SqlValue.FromDate(new DateOnly(2026, 5, 4)), SqlValue.FromDate(new DateOnly(2026, 5, 5)));
198-
}
199-
200-
[TestMethod]
201-
public void Date_CompareTo_OrdersChronologically()
202-
{
203-
var earlier = SqlValue.FromDate(new DateOnly(2024, 1, 1));
204-
var later = SqlValue.FromDate(new DateOnly(2026, 12, 31));
205-
IsTrue(earlier.CompareTo(later) < 0);
206-
IsTrue(later.CompareTo(earlier) > 0);
207-
AreEqual(0, earlier.CompareTo(earlier));
208-
}
209-
210-
[TestMethod]
211-
public void Date_ToObject_ReturnsDateTimeAtMidnight()
212-
{
213-
var v = SqlValue.FromDate(new DateOnly(2026, 5, 4));
214-
var obj = v.ToObject();
215-
var dt = IsInstanceOfType<DateTime>(obj);
216-
AreEqual(new DateTime(2026, 5, 4, 0, 0, 0), dt);
217-
}
218-
219-
[TestMethod]
220-
public void Date_NullToObject_ReturnsNull()
221-
{
222-
var v = SqlValue.Null(SqlType.Date);
223-
IsNull(v.ToObject());
224-
}
225-
226172
[TestMethod]
227173
public void Date_AsDate_OnNull_Throws() =>
228174
Throws<InvalidOperationException>(() => SqlValue.Null(SqlType.Date).AsDate);
@@ -232,50 +178,17 @@ public void Date_AsDate_OnWrongType_Throws() =>
232178
Throws<InvalidOperationException>(() => SqlValue.FromInt32(1).AsDate);
233179

234180
[TestMethod]
235-
[DataRow("2026-05-04", 2026, 5, 4)]
236-
[DataRow("20260504", 2026, 5, 4)]
237-
[DataRow("2026-05-04T13:45:30", 2026, 5, 4)]
238-
[DataRow("2026-05-04T13:45:30.1234567", 2026, 5, 4)]
239-
[DataRow("0001-01-01", 1, 1, 1)]
240-
[DataRow("9999-12-31", 9999, 12, 31)]
241-
public void CoerceTo_VarcharToDate_ParsesIsoForms(string text, int year, int month, int day)
242-
{
243-
var coerced = SqlValue.FromVarchar(text).CoerceTo(SqlType.Date);
244-
AreSame(SqlType.Date, coerced.Type);
245-
AreEqual(new DateOnly(year, month, day), coerced.AsDate);
246-
}
247-
248-
[TestMethod]
249-
public void CoerceTo_NVarcharToDate_ParsesIsoForms()
250-
{
251-
var coerced = SqlValue.FromNVarchar("2026-05-04").CoerceTo(SqlType.Date);
252-
AreEqual(new DateOnly(2026, 5, 4), coerced.AsDate);
253-
}
254-
255-
[TestMethod]
256-
[DataRow("not a date")]
257-
[DataRow("")]
258-
[DataRow("2026-13-01")] // invalid month
259-
[DataRow("Jan 15, 2026")] // locale-sensitive form not handled
260-
public void CoerceTo_BadStringToDate_RaisesConversionFailed(string text)
261-
{
262-
var ex = Throws<DbException>(() => SqlValue.FromVarchar(text).CoerceTo(SqlType.Date));
263-
AreEqual("Conversion failed when converting date and/or time from character string.", ex.Message);
264-
}
265-
266-
[TestMethod]
267-
public void CoerceTo_DateToVarchar_FormatsIso()
268-
{
269-
var coerced = SqlValue.FromDate(new DateOnly(2026, 5, 4)).CoerceTo(SqlType.Varchar);
270-
AreSame(SqlType.Varchar, coerced.Type);
271-
AreEqual("2026-05-04", coerced.AsString);
272-
}
181+
public void FromDateTime2_RejectsNonDateTime2Type() =>
182+
Throws<ArgumentException>(() => SqlValue.FromDateTime2(SqlType.Int32, DateTime.UtcNow));
273183

274184
[TestMethod]
275-
public void CoerceTo_DateToNVarchar_FormatsIso()
185+
public void DateTime2_DifferentPrecisions_AreDistinctTypes()
276186
{
277-
var coerced = SqlValue.FromDate(new DateOnly(2026, 5, 4)).CoerceTo(SqlType.NVarchar);
278-
AreSame(SqlType.NVarchar, coerced.Type);
279-
AreEqual("2026-05-04", coerced.AsString);
187+
// The reference-equality of Type tags isn't observable from SQL — this
188+
// pins the per-precision singleton invariant Promote/CoerceTo rely on.
189+
var v3 = SqlValue.FromDateTime2(SqlType.GetDateTime2(3), new DateTime(2026, 5, 4));
190+
var v7 = SqlValue.FromDateTime2(SqlType.GetDateTime2(7), new DateTime(2026, 5, 4));
191+
AreNotSame(v3.Type, v7.Type);
192+
AreNotEqual(v3, v7);
280193
}
281194
}

SqlServerSimulator.Tests/BuiltInFunctionTests.cs

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -136,63 +136,4 @@ public void StringFunctionOfColumn_FromTable()
136136
AreEqual("ALICE", connection.CreateCommand("select upper(name) from t").ExecuteScalar());
137137
}
138138

139-
[TestMethod]
140-
[DataRow("cast(1 as int)", 1)]
141-
[DataRow("cast(1 as bigint)", 1L)]
142-
[DataRow("cast(1 as smallint)", (short)1)]
143-
[DataRow("cast(255 as tinyint)", (byte)255)]
144-
[DataRow("cast(1 as bit)", true)]
145-
[DataRow("cast(0 as bit)", false)]
146-
public void Cast(string expression, object expected) =>
147-
AreEqual(expected, ExecuteScalar($"select {expression}"));
148-
149-
[TestMethod]
150-
public void Cast_NullPassesThroughWithRetargetedType() =>
151-
IsInstanceOfType<DBNull>(ExecuteScalar("select cast(null as int)"));
152-
153-
[TestMethod]
154-
public void Cast_NarrowingOverflow_RaisesArithmeticOverflow()
155-
{
156-
var ex = Throws<DbException>(() => ExecuteScalar("select cast(300 as tinyint)"));
157-
StringAssert.Contains(ex.Message, "Arithmetic overflow");
158-
}
159-
160-
[TestMethod]
161-
public void Cast_CrossCategory_NotSupported()
162-
{
163-
// String ↔ numeric coercion isn't modeled yet; CAST surfaces the same
164-
// NotSupportedException SqlValue.CoerceTo throws.
165-
_ = Throws<NotSupportedException>(() => ExecuteScalar("select cast('42' as int)"));
166-
}
167-
168-
[TestMethod]
169-
[DataRow("cast('2026-05-04' as date)", "2026-05-04")]
170-
[DataRow("cast('20260504' as date)", "2026-05-04")]
171-
[DataRow("cast('2026-05-04T13:45:30' as date)", "2026-05-04")]
172-
[DataRow("cast(N'2026-05-04' as date)", "2026-05-04")]
173-
public void Cast_StringToDate(string expression, string expectedIso)
174-
{
175-
var value = ExecuteScalar($"select {expression}");
176-
var dt = IsInstanceOfType<DateTime>(value);
177-
AreEqual(DateTime.Parse(expectedIso, System.Globalization.CultureInfo.InvariantCulture), dt);
178-
}
179-
180-
[TestMethod]
181-
[DataRow("not a date")]
182-
[DataRow("2026-13-01")]
183-
public void Cast_BadStringToDate_RaisesConversionFailed(string text)
184-
{
185-
var ex = Throws<DbException>(() => ExecuteScalar($"select cast('{text}' as date)"));
186-
AreEqual("Conversion failed when converting date and/or time from character string.", ex.Message);
187-
}
188-
189-
[TestMethod]
190-
[DataRow("cast(cast('2026-05-04' as date) as varchar(10))", "2026-05-04")]
191-
[DataRow("cast(cast('2026-05-04' as date) as nvarchar(10))", "2026-05-04")]
192-
public void Cast_DateToString(string expression, string expected) =>
193-
AreEqual(expected, ExecuteScalar($"select {expression}"));
194-
195-
[TestMethod]
196-
public void Cast_DateNullPassesThrough() =>
197-
IsInstanceOfType<DBNull>(ExecuteScalar("select cast(cast(null as date) as varchar(10))"));
198139
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
using static SqlServerSimulator.TestHelpers;
4+
5+
namespace SqlServerSimulator;
6+
7+
[TestClass]
8+
public sealed class CastTests
9+
{
10+
[TestMethod]
11+
[DataRow("cast(1 as int)", 1)]
12+
[DataRow("cast(1 as bigint)", 1L)]
13+
[DataRow("cast(1 as smallint)", (short)1)]
14+
[DataRow("cast(255 as tinyint)", (byte)255)]
15+
[DataRow("cast(1 as bit)", true)]
16+
[DataRow("cast(0 as bit)", false)]
17+
public void Cast(string expression, object expected) =>
18+
AreEqual(expected, ExecuteScalar($"select {expression}"));
19+
20+
[TestMethod]
21+
public void Cast_NullPassesThroughWithRetargetedType() =>
22+
IsInstanceOfType<DBNull>(ExecuteScalar("select cast(null as int)"));
23+
24+
[TestMethod]
25+
public void Cast_NarrowingOverflow_RaisesArithmeticOverflow()
26+
{
27+
var ex = Throws<DbException>(() => ExecuteScalar("select cast(300 as tinyint)"));
28+
StringAssert.Contains(ex.Message, "Arithmetic overflow");
29+
}
30+
31+
[TestMethod]
32+
public void Cast_CrossCategory_NotSupported()
33+
{
34+
// String ↔ numeric coercion isn't modeled yet; CAST surfaces the same
35+
// NotSupportedException SqlValue.CoerceTo throws.
36+
_ = Throws<NotSupportedException>(() => ExecuteScalar("select cast('42' as int)"));
37+
}
38+
39+
[TestMethod]
40+
[DataRow("cast('2026-05-04' as date)", "2026-05-04")]
41+
[DataRow("cast('20260504' as date)", "2026-05-04")]
42+
[DataRow("cast('2026-05-04T13:45:30' as date)", "2026-05-04")]
43+
[DataRow("cast(N'2026-05-04' as date)", "2026-05-04")]
44+
public void Cast_StringToDate(string expression, string expectedIso)
45+
{
46+
var value = ExecuteScalar($"select {expression}");
47+
var dt = IsInstanceOfType<DateTime>(value);
48+
AreEqual(DateTime.Parse(expectedIso, System.Globalization.CultureInfo.InvariantCulture), dt);
49+
}
50+
51+
[TestMethod]
52+
[DataRow("not a date")]
53+
[DataRow("2026-13-01")]
54+
public void Cast_BadStringToDate_RaisesConversionFailed(string text)
55+
{
56+
var ex = Throws<DbException>(() => ExecuteScalar($"select cast('{text}' as date)"));
57+
AreEqual("Conversion failed when converting date and/or time from character string.", ex.Message);
58+
}
59+
60+
[TestMethod]
61+
[DataRow("cast(cast('2026-05-04' as date) as varchar(10))", "2026-05-04")]
62+
[DataRow("cast(cast('2026-05-04' as date) as nvarchar(10))", "2026-05-04")]
63+
public void Cast_DateToString(string expression, string expected) =>
64+
AreEqual(expected, ExecuteScalar($"select {expression}"));
65+
66+
[TestMethod]
67+
public void Cast_DateNullPassesThrough() =>
68+
IsInstanceOfType<DBNull>(ExecuteScalar("select cast(cast(null as date) as varchar(10))"));
69+
70+
[TestMethod]
71+
[DataRow("cast('2026-05-04 13:45:30' as datetime2(0))", "2026-05-04T13:45:30")]
72+
[DataRow("cast('2026-05-04 13:45:30.123' as datetime2(3))", "2026-05-04T13:45:30.123")]
73+
[DataRow("cast('2026-05-04T13:45:30.1234567' as datetime2(7))", "2026-05-04T13:45:30.1234567")]
74+
public void Cast_StringToDateTime2(string expression, string expectedIso)
75+
{
76+
var value = ExecuteScalar($"select {expression}");
77+
var dt = IsInstanceOfType<DateTime>(value);
78+
AreEqual(DateTime.Parse(expectedIso, System.Globalization.CultureInfo.InvariantCulture), dt);
79+
}
80+
81+
[TestMethod]
82+
[DataRow("cast(cast('2026-05-04 13:45:30' as datetime2(0)) as varchar(20))", "2026-05-04 13:45:30")]
83+
[DataRow("cast(cast('2026-05-04 13:45:30.123' as datetime2(3)) as varchar(25))", "2026-05-04 13:45:30.123")]
84+
[DataRow("cast(cast('2026-05-04 13:45:30.1234567' as datetime2(7)) as varchar(30))", "2026-05-04 13:45:30.1234567")]
85+
public void Cast_DateTime2ToString(string expression, string expected) =>
86+
AreEqual(expected, ExecuteScalar($"select {expression}"));
87+
88+
[TestMethod]
89+
public void Cast_DateTime2_NarrowingPrecisionRoundsHalfUp()
90+
{
91+
// 0.5 second exactly at precision 0 => round to next second.
92+
var result = ExecuteScalar("select cast(cast('2026-05-04 13:45:30.5' as datetime2(7)) as datetime2(0))");
93+
var dt = IsInstanceOfType<DateTime>(result);
94+
AreEqual(new DateTime(2026, 5, 4, 13, 45, 31), dt);
95+
}
96+
97+
[TestMethod]
98+
public void Cast_DateToDateTime2_AppliesMidnight()
99+
{
100+
var result = ExecuteScalar("select cast(cast('2026-05-04' as date) as datetime2(7))");
101+
AreEqual(new DateTime(2026, 5, 4), IsInstanceOfType<DateTime>(result));
102+
}
103+
104+
[TestMethod]
105+
public void Cast_DateTime2ToDate_DropsTimePortion()
106+
{
107+
var result = ExecuteScalar("select cast(cast('2026-05-04 13:45:30' as datetime2(7)) as date)");
108+
AreEqual(new DateTime(2026, 5, 4), IsInstanceOfType<DateTime>(result));
109+
}
110+
111+
[TestMethod]
112+
public void Cast_DateTime2NullPassesThrough() =>
113+
IsInstanceOfType<DBNull>(ExecuteScalar("select cast(cast(null as datetime2(7)) as varchar(30))"));
114+
}

SqlServerSimulator.Tests/CreateTableTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,36 @@ public void CreateTableDateRejectsLengthSpecifier()
256256
var x = Assert.Throws<DbException>(() => command.ExecuteNonQuery());
257257
Assert.AreEqual("Column, parameter, or variable #1: Cannot specify a column width on data type date.", x.Message);
258258
}
259+
260+
[TestMethod]
261+
[DataRow("datetime2")] // default precision = 7
262+
[DataRow("DATETIME2")]
263+
[DataRow("datetime2(0)")]
264+
[DataRow("datetime2(3)")]
265+
[DataRow("datetime2(7)")]
266+
public void CreateTableDateTime2(string typeSpec)
267+
{
268+
var simulation = new Simulation();
269+
270+
using var connection = simulation.CreateDbConnection();
271+
using var command = connection.CreateCommand($"create table t ( v {typeSpec} )");
272+
273+
connection.Open();
274+
Assert.AreEqual(-1, command.ExecuteNonQuery());
275+
}
276+
277+
[TestMethod]
278+
[DataRow(8)]
279+
[DataRow(99)]
280+
public void CreateTableDateTime2_PrecisionOutOfRange(int precision)
281+
{
282+
var simulation = new Simulation();
283+
284+
using var connection = simulation.CreateDbConnection();
285+
using var command = connection.CreateCommand($"create table t ( v datetime2({precision}) )");
286+
287+
connection.Open();
288+
var x = Assert.Throws<DbException>(() => command.ExecuteNonQuery());
289+
Assert.AreEqual($"Specified scale {precision} is invalid.", x.Message);
290+
}
259291
}

0 commit comments

Comments
 (0)