|
| 1 | +using System.Data.Common; |
| 2 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 3 | +using static SqlServerSimulator.TestHelpers; |
| 4 | + |
| 5 | +namespace SqlServerSimulator; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// Direct simulator coverage for the <c>DATEDIFF</c> / <c>DATEDIFF_BIG</c> |
| 9 | +/// scalar functions. Probe-confirmed against SQL Server 2025 (2026-05-08): |
| 10 | +/// boundary-crossing semantics (year boundary <c>2023-12-31 → 2024-01-01 = 1</c>, |
| 11 | +/// not elapsed time); accept-everything type compatibility (only |
| 12 | +/// <c>tzoffset</c> and <c>iso_week</c> rejected, function-level Msg 9806); |
| 13 | +/// <c>datetimeoffset</c> compared via UTC instant; result widths (int vs |
| 14 | +/// bigint) and Msg 535 overflow on result-width violation. The two |
| 15 | +/// functions share an implementation; only the result type / overflow |
| 16 | +/// threshold differ. |
| 17 | +/// </summary> |
| 18 | +[TestClass] |
| 19 | +public sealed class DateDiffTests |
| 20 | +{ |
| 21 | + [TestMethod] |
| 22 | + [DataRow("year", "2020-01-01", "2024-01-01", 4)] |
| 23 | + [DataRow("year", "2023-12-31", "2024-01-01", 1)] |
| 24 | + [DataRow("quarter", "2024-03-31", "2024-04-01", 1)] |
| 25 | + [DataRow("quarter", "2023-10-15", "2024-02-15", 1)] |
| 26 | + [DataRow("month", "2024-01-31", "2024-02-01", 1)] |
| 27 | + [DataRow("month", "2024-06-01", "2024-06-30", 0)] |
| 28 | + [DataRow("day", "2024-01-01 23:59:59", "2024-01-02 00:00:01", 1)] |
| 29 | + [DataRow("hour", "2024-01-01 12:59:59", "2024-01-01 13:00:01", 1)] |
| 30 | + public void DateDiff_BoundaryCrossing(string part, string startStr, string endStr, int expected) => |
| 31 | + AreEqual(expected, ExecuteScalar($"select datediff({part}, '{startStr}', '{endStr}')")); |
| 32 | + |
| 33 | + [TestMethod] |
| 34 | + public void DateDiff_NegativeWhenStartAfterEnd() => |
| 35 | + AreEqual(-4, ExecuteScalar("select datediff(year, '2024-01-01', '2020-01-01')")); |
| 36 | + |
| 37 | + [TestMethod] |
| 38 | + public void DateDiff_WeekIsSundayAnchored() |
| 39 | + { |
| 40 | + // 2024-06-15 is Saturday; 2024-06-16 is Sunday — boundary crossed. |
| 41 | + AreEqual(1, ExecuteScalar("select datediff(week, '2024-06-15', '2024-06-16')")); |
| 42 | + // 2024-06-16 (Sun) → 2024-06-22 (Sat): same week, no boundary. |
| 43 | + AreEqual(0, ExecuteScalar("select datediff(week, '2024-06-16', '2024-06-22')")); |
| 44 | + // Weekday is treated as plain day count (probe: 7 for 2024-06-15→2024-06-22). |
| 45 | + AreEqual(7, ExecuteScalar("select datediff(weekday, '2024-06-15', '2024-06-22')")); |
| 46 | + } |
| 47 | + |
| 48 | + [TestMethod] |
| 49 | + [DataRow("yy", 4)] |
| 50 | + [DataRow("yyyy", 4)] |
| 51 | + [DataRow("ww", 1)] |
| 52 | + [DataRow("wk", 1)] |
| 53 | + [DataRow("dw", 7)] |
| 54 | + [DataRow("dy", 7)] |
| 55 | + [DataRow("hh", 0)] |
| 56 | + public void DateDiff_AcceptsKeywordAliases(string alias, int expected) |
| 57 | + { |
| 58 | + var sql = alias is "ww" or "wk" |
| 59 | + ? $"select datediff({alias}, '2024-06-15', '2024-06-22')" |
| 60 | + : alias is "dw" or "dy" |
| 61 | + ? $"select datediff({alias}, '2024-06-15', '2024-06-22')" |
| 62 | + : alias is "hh" |
| 63 | + ? $"select datediff({alias}, '2024-01-01 13:00', '2024-01-01 13:30')" |
| 64 | + : $"select datediff({alias}, '2020-01-01', '2024-01-01')"; |
| 65 | + AreEqual(expected, ExecuteScalar(sql)); |
| 66 | + } |
| 67 | + |
| 68 | + [TestMethod] |
| 69 | + public void DateDiff_NullStartOrEnd_ReturnsTypedNullInt() |
| 70 | + { |
| 71 | + _ = IsInstanceOfType<DBNull>(ExecuteScalar("select datediff(day, cast(null as date), '2024-01-01')")); |
| 72 | + _ = IsInstanceOfType<DBNull>(ExecuteScalar("select datediff(day, '2024-01-01', cast(null as date))")); |
| 73 | + } |
| 74 | + |
| 75 | + [TestMethod] |
| 76 | + public void DateDiffBig_ReturnsBigint() |
| 77 | + { |
| 78 | + AreEqual(86400000000000L, ExecuteScalar("select datediff_big(nanosecond, '2024-01-01', '2024-01-02')")); |
| 79 | + AreEqual(4L, ExecuteScalar("select datediff_big(year, '2020-01-01', '2024-01-01')")); |
| 80 | + } |
| 81 | + |
| 82 | + [TestMethod] |
| 83 | + public void DateDiffBig_NullStartOrEnd_ReturnsTypedNullBigint() => |
| 84 | + IsInstanceOfType<DBNull>(ExecuteScalar("select datediff_big(day, cast(null as date), '2024-01-01')")); |
| 85 | + |
| 86 | + [TestMethod] |
| 87 | + public void DateDiff_AcceptsAllTypeCombinations() |
| 88 | + { |
| 89 | + // date - time mix: time anchored to 1900-01-01 (probe-confirmed). |
| 90 | + AreEqual(5, ExecuteScalar("select datediff(hour, cast('1900-01-01' as date), cast('05:00' as time))")); |
| 91 | + // year/day on time-only inputs returns 0 (no error like DATEPART would raise Msg 9810). |
| 92 | + AreEqual(0, ExecuteScalar("select datediff(year, cast('10:00' as time), cast('11:00' as time))")); |
| 93 | + AreEqual(0, ExecuteScalar("select datediff(day, cast('10:00' as time), cast('11:00' as time))")); |
| 94 | + // hour on date-only: always 24 × day diff. |
| 95 | + AreEqual(3984, ExecuteScalar("select datediff(hour, cast('2024-01-01' as date), cast('2024-06-15' as date))")); |
| 96 | + } |
| 97 | + |
| 98 | + [TestMethod] |
| 99 | + public void DateDiff_DateTimeOffset_UsesUtcInstant() |
| 100 | + { |
| 101 | + // Same wall-clock, different offsets → identical UTC instants → 0. |
| 102 | + AreEqual(0, ExecuteScalar("select datediff(hour, cast('2024-01-01 12:00:00 +00:00' as datetimeoffset), cast('2024-01-01 17:00:00 +05:00' as datetimeoffset))")); |
| 103 | + // Same wall-clock literal but +5 offset shifts UTC backward by 5h. |
| 104 | + AreEqual(-5, ExecuteScalar("select datediff(hour, cast('2024-01-01 12:00:00 +00:00' as datetimeoffset), cast('2024-01-01 12:00:00 +05:00' as datetimeoffset))")); |
| 105 | + } |
| 106 | + |
| 107 | + [TestMethod] |
| 108 | + public void DateDiff_TzOffsetPart_RaisesMsg9806() |
| 109 | + { |
| 110 | + // tzoffset is rejected at the function level, regardless of operand type. |
| 111 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff(tzoffset, cast('2024-01-01 00:00:00 +00:00' as datetimeoffset), cast('2024-06-15 00:00:00 +00:00' as datetimeoffset))")); |
| 112 | + AreEqual("The datepart tzoffset is not supported by date function datediff.", ex.Message); |
| 113 | + AreEqual("9806", ex.Data["HelpLink.EvtID"]); |
| 114 | + } |
| 115 | + |
| 116 | + [TestMethod] |
| 117 | + public void DateDiff_IsoWeekPart_RaisesMsg9806() |
| 118 | + { |
| 119 | + // iso_week works in DATEPART/DATEADD but is unconditionally rejected by DATEDIFF. |
| 120 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff(iso_week, '2024-06-15', '2024-06-22')")); |
| 121 | + AreEqual("The datepart iso_week is not supported by date function datediff.", ex.Message); |
| 122 | + AreEqual("9806", ex.Data["HelpLink.EvtID"]); |
| 123 | + } |
| 124 | + |
| 125 | + [TestMethod] |
| 126 | + public void DateDiffBig_TzOffset_MessageEmbedsFunctionName() |
| 127 | + { |
| 128 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff_big(iso_week, '2024-06-15', '2024-06-22')")); |
| 129 | + AreEqual("The datepart iso_week is not supported by date function datediff_big.", ex.Message); |
| 130 | + } |
| 131 | + |
| 132 | + [TestMethod] |
| 133 | + public void DateDiff_UnknownDatepart_RaisesMsg155() |
| 134 | + { |
| 135 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff(badpart, '2024-01-01', '2024-06-15')")); |
| 136 | + AreEqual("'badpart' is not a recognized datediff option.", ex.Message); |
| 137 | + AreEqual("155", ex.Data["HelpLink.EvtID"]); |
| 138 | + } |
| 139 | + |
| 140 | + [TestMethod] |
| 141 | + public void DateDiffBig_UnknownDatepart_MessageSaysDatediffBig() |
| 142 | + { |
| 143 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff_big(badpart, '2024-01-01', '2024-06-15')")); |
| 144 | + AreEqual("'badpart' is not a recognized datediff_big option.", ex.Message); |
| 145 | + } |
| 146 | + |
| 147 | + [TestMethod] |
| 148 | + public void DateAdd_UnknownDatepart_MessageSaysDateadd() |
| 149 | + { |
| 150 | + // Pre-DATEDIFF, this said "datepart option" — Msg 155 wording is per-caller. |
| 151 | + var ex = Throws<DbException>(() => ExecuteScalar("select dateadd(badpart, 1, '2024-01-01')")); |
| 152 | + AreEqual("'badpart' is not a recognized dateadd option.", ex.Message); |
| 153 | + } |
| 154 | + |
| 155 | + [TestMethod] |
| 156 | + public void DateDiff_MillisecondOverflow_RaisesMsg535() |
| 157 | + { |
| 158 | + // ~25 days of ms is > int.MaxValue (probe boundary). |
| 159 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff(millisecond, '2024-01-01', '2024-01-26')")); |
| 160 | + AreEqual("The datediff function resulted in an overflow. The number of dateparts separating two date/time instances is too large. Try to use datediff with a less precise datepart.", ex.Message); |
| 161 | + AreEqual("535", ex.Data["HelpLink.EvtID"]); |
| 162 | + } |
| 163 | + |
| 164 | + [TestMethod] |
| 165 | + public void DateDiff_MillisecondJustUnderInt_DoesNotOverflow() |
| 166 | + { |
| 167 | + // 24 days × 86400000 ms = 2,073,600,000 — fits in int.MaxValue (2,147,483,647). |
| 168 | + AreEqual(2073600000, ExecuteScalar("select datediff(millisecond, '2024-01-01', '2024-01-25')")); |
| 169 | + } |
| 170 | + |
| 171 | + [TestMethod] |
| 172 | + public void DateDiffBig_MillisecondLargeRange_StaysInBigint() => |
| 173 | + AreEqual(3913056000000L, ExecuteScalar("select datediff_big(millisecond, '1900-01-01', '2024-01-01')")); |
| 174 | + |
| 175 | + [TestMethod] |
| 176 | + public void DateDiffBig_NanosecondCenturies_OverflowsToMsg535() |
| 177 | + { |
| 178 | + // 0001 → 9999 in nanoseconds overflows even bigint; probe-confirmed Msg 535. |
| 179 | + var ex = Throws<DbException>(() => ExecuteScalar("select datediff_big(nanosecond, cast('0001-01-01' as datetime2), cast('9999-12-31' as datetime2))")); |
| 180 | + AreEqual("535", ex.Data["HelpLink.EvtID"]); |
| 181 | + AreEqual("The datediff_big function resulted in an overflow. The number of dateparts separating two date/time instances is too large. Try to use datediff_big with a less precise datepart.", ex.Message); |
| 182 | + } |
| 183 | + |
| 184 | + [TestMethod] |
| 185 | + public void DateDiff_SubsecondParts() |
| 186 | + { |
| 187 | + // 100ms → 200ms = 100ms diff. |
| 188 | + AreEqual(100, ExecuteScalar("select datediff(millisecond, '2024-01-01 00:00:00.100', '2024-01-01 00:00:00.200')")); |
| 189 | + // datetime2(7) ticks: 0.0000001s = 1 tick; 0.0000005s = 5 ticks. nanosecond diff = (5-1)*100 = 400. |
| 190 | + AreEqual(400, ExecuteScalar("select datediff(nanosecond, cast('2024-01-01 00:00:00.0000001' as datetime2(7)), cast('2024-01-01 00:00:00.0000005' as datetime2(7)))")); |
| 191 | + // Same tick range in microseconds: both quantize to 0 μs → diff 0. |
| 192 | + AreEqual(0, ExecuteScalar("select datediff(microsecond, cast('2024-01-01 00:00:00.0000001' as datetime2(7)), cast('2024-01-01 00:00:00.0000005' as datetime2(7)))")); |
| 193 | + } |
| 194 | + |
| 195 | + [TestMethod] |
| 196 | + public void DateDiff_OnColumn_PreservesIntType() |
| 197 | + { |
| 198 | + var sim = new Simulation(); |
| 199 | + _ = sim.ExecuteNonQuery("create table t (a datetime2, b datetime2)"); |
| 200 | + _ = sim.ExecuteNonQuery("insert t values ('2024-01-01', '2024-06-15')"); |
| 201 | + AreEqual(166, sim.ExecuteScalar("select datediff(day, a, b) from t")); |
| 202 | + } |
| 203 | +} |
0 commit comments