Skip to content

Commit bf8cee7

Browse files
committed
Map DATEADD's out-of-int-range count to its Msg 517 date-overflow error instead of leaking a raw OverflowException, and document the same int-narrowing gap in the sibling string/char-code scalars as a deliberate known limitation.
1 parent c10702f commit bf8cee7

4 files changed

Lines changed: 36 additions & 1 deletion

File tree

SqlServerSimulator.Tests/BuiltInFunctionTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,17 @@ public void DecimalLiteralBeyondMaxPrecision_RaisesMsg1007()
170170
public void ReplaceWithEmptySearch_ReturnsInputUnchanged()
171171
=> AreEqual("abc", ExecuteScalar("select replace('abc', '', 'X')"));
172172

173+
[TestMethod]
174+
[DataRow("dateadd(second, 9223372036854775807, getdate())")]
175+
[DataRow("dateadd(year, 2147483648, getdate())")]
176+
[DataRow("dateadd(day, 9999999999, getdate())")]
177+
public void DateAddCountOutOfIntRange_RaisesMsg517(string expression)
178+
{
179+
var ex = Throws<SimulatedSqlException>(() => ExecuteScalar($"select {expression}"));
180+
AreEqual(517, ex.Number);
181+
Assert.Contains("caused an overflow", ex.Message);
182+
}
183+
173184
[TestMethod]
174185
public void FunctionOfColumn_FromTable()
175186
{

SqlServerSimulator/Parser/Expressions/DateAdd.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public override SqlValue Run(RuntimeContext runtime)
3737
if (value.IsNull || n.IsNull)
3838
return SqlValue.Null(value.Type);
3939
DatePartKinds.RequireCompatible(this.kind, this.keywordText, value.Type, "dateadd");
40-
var nInt = n.CoerceTo(SqlType.Int32).AsInt32;
40+
var nInt = DatePartKinds.CoerceCount(n, value.Type);
4141
return DatePartKinds.Add(this.kind, value, nInt);
4242
}
4343

SqlServerSimulator/Parser/Expressions/DatePartKind.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,26 @@ public static int Extract(DatePartKind kind, SqlValue value)
350350
};
351351
}
352352

353+
/// <summary>
354+
/// Coerces a DATEADD count to <c>int</c>. A value outside int range can
355+
/// only push the result past the date type's representable range, which
356+
/// SQL Server reports as the same Msg 517 date overflow <see cref="Add"/>
357+
/// raises — not an int-conversion error (probe-confirmed across second /
358+
/// day / year). Without this, the bigint-to-int narrowing would leak a
359+
/// raw <see cref="OverflowException"/>.
360+
/// </summary>
361+
public static int CoerceCount(SqlValue number, SqlType targetType)
362+
{
363+
try
364+
{
365+
return number.CoerceTo(SqlType.Int32).AsInt32;
366+
}
367+
catch (OverflowException)
368+
{
369+
throw SimulatedSqlException.DateAddOverflow(FamilyRootName(targetType));
370+
}
371+
}
372+
353373
/// <summary>
354374
/// Returns the result of <paramref name="value"/> + <paramref name="n"/>
355375
/// units of <paramref name="kind"/>, preserving the input's SQL type.

docs/claude/scalars.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ Bundle that fills out the raw-SQL string surface that EF's `FromSqlInterpolated`
9393
- **`SPACE(count)`** always returns `varchar` (never nvarchar), truncated to 8000 chars. NULL / negative count → NULL.
9494
- **`FORMAT(value, format [, culture])`** returns `nvarchar`. Implementation routes through .NET's `IFormattable.ToString(format, culture)` on the underlying CLR value, matching SQL Server's CLR-passthrough shape. Accepted value types: numeric (integer / decimal / float / real / money / smallmoney) and date-time family (date / datetime / smalldatetime / datetime2 / datetimeoffset / time). Strings, bit, binary, uniqueidentifier, rowversion → Msg 8116 at runtime. NULL value → NULL; NULL format → Msg 8116 (probed: ordering doesn't matter — the format-NULL check fires first). Culture defaults to en-US; invalid culture name silently falls back to en-US. .NET `FormatException` (e.g. `decimal.ToString("D5")`) → NULL; unrecognized custom-format tokens that .NET passes through (e.g. `int.ToString("qq qq")`) are echoed verbatim.
9595

96+
## Known gap: out-of-`int`-range integer arguments
97+
98+
A length / position / count / code-point argument that exceeds `int` range surfaces a raw .NET `OverflowException` from the `int` narrowing instead of the function's normal result. Affected: `SUBSTRING` (length), `CHARINDEX` (start), `STUFF` (start / length — preempts the documented out-of-range-returns-NULL handling above), `REPLICATE` (count), `SPACE` (count), `CHOOSE` (index), `CHAR` / `NCHAR` (code point). `LEFT` / `RIGHT` (Msg 8115) and `DATEADD` (Msg 517) harden this argument; the rest don't. Left point-local deliberately: real SQL Server's response is per-function (clamp, compute as bigint, or a value-class error), so a single shared error wouldn't be faithful, and the trigger is pathological — no realistic query or EF emission passes a count / position outside `int` range. A value held in a variable could reach it; the failure is a clean abort, just not the SQL-Server-shaped one.
99+
96100
## EF.Functions-driven type-check / random scalars: `ISNUMERIC` / `ISDATE` / `RAND`
97101
- **`ISNUMERIC(expression)`** returns `int` (1 / 0); NULL → 0 (not NULL). Famously lossy on real SQL Server: a bare sign / decimal point / comma / currency symbol returns 1, hex prefixes return 0, internal whitespace breaks the match. The simulator's hand-rolled scanner consumes (in order: optional sign and currency in either order; digit / decimal / comma run; optional `e`/`E`/`d`/`D` exponent requiring a leading digit AND a trailing digit after optional sign). At least one of {digit, decimal/comma, sign, currency} must have been consumed for the result to be true. Bit-typed input returns 0 even though bit lives in the Integer category (probe-confirmed). Anything that doesn't fully consume after trimming whitespace returns 0.
98102
- **`ISDATE(expression)`** returns `int` (1 / 0) and validates against the legacy `datetime` range (1753-9999). Empty string short-circuits to 0 (the shared `TryParseLegacyDateTime` treats `""` as datetime base-date for CAST support, but ISDATE specifically rejects). Modern `date` / `time` / `datetimeoffset` raise Msg 8116 — ISDATE intentionally lives in the legacy datetime domain. Integer input is implicitly stringified and re-parsed (so `ISDATE(20260512)` = 1 via `'20260512'` matching `yyyyMMdd`; `ISDATE(1)` = 0 because `'1'` parses to year 1 < 1753). Float / decimal / non-integer-non-string types always return 0.

0 commit comments

Comments
 (0)