|
| 1 | +using Microsoft.Data.SqlClient; |
| 2 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 3 | + |
| 4 | +namespace SqlServerSimulator; |
| 5 | + |
| 6 | +/// <summary> |
| 7 | +/// Result-set width metadata (COLMETADATA) as a real SqlClient reader observes |
| 8 | +/// it via <c>GetColumnSchema().ColumnSize</c>. String / binary literals type at |
| 9 | +/// their exact value width (probe-confirmed against SQL Server 2025 — a bare |
| 10 | +/// <c>'abc'</c> advertises <c>varchar(3)</c>, not the <c>varchar(8000)</c> |
| 11 | +/// container it once did), and the width algebra that combines them (concat |
| 12 | +/// sum-capped, CASE / COALESCE / set-op max, ISNULL first-arg, per-function |
| 13 | +/// derivations) flows through to the wire. SqlClient reports <c>ColumnSize</c> |
| 14 | +/// in characters for the string families and <c>int.MaxValue</c> for a MAX |
| 15 | +/// column. |
| 16 | +/// </summary> |
| 17 | +[TestClass] |
| 18 | +public sealed class StringLiteralWidthWireTests |
| 19 | +{ |
| 20 | + public TestContext TestContext { get; set; } = null!; |
| 21 | + |
| 22 | + private async Task<int> ColumnSizeAsync(string sql) |
| 23 | + { |
| 24 | + var simulation = new Simulation(); |
| 25 | + await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken); |
| 26 | + await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken); |
| 27 | + await using var command = new SqlCommand(sql, connection); |
| 28 | + await using var reader = await command.ExecuteReaderAsync(TestContext.CancellationToken); |
| 29 | + return reader.GetColumnSchema()[0].ColumnSize ?? -1; |
| 30 | + } |
| 31 | + |
| 32 | + // Bare literals: exact value width, empty floors to 1, over-cap widens to MAX. |
| 33 | + [TestMethod] |
| 34 | + [DataRow("select 'abc' as x", 3)] |
| 35 | + [DataRow("select '' as x", 1)] |
| 36 | + [DataRow("select 'ab ' as x", 4)] // trailing spaces counted |
| 37 | + [DataRow("select N'abc' as x", 3)] // chars, not bytes |
| 38 | + [DataRow("select N'' as x", 1)] |
| 39 | + [DataRow("select 0xAABB as x", 2)] |
| 40 | + [DataRow("select 0x as x", 1)] // empty binary floors to 1 |
| 41 | + public async Task BareLiterals_TypeAtExactWidth(string sql, int expected) |
| 42 | + => AreEqual(expected, await ColumnSizeAsync(sql)); |
| 43 | + |
| 44 | + [TestMethod] |
| 45 | + [DataRow("select cast('x' as varchar(max)) + 'abc' as x")] // MAX operand propagates |
| 46 | + public async Task MaxOperand_WidensToMax(string sql) |
| 47 | + => AreEqual(int.MaxValue, await ColumnSizeAsync(sql)); |
| 48 | + |
| 49 | + // A string literal longer than the family bound widens to the MAX form. |
| 50 | + [TestMethod] |
| 51 | + public async Task OverBoundLiteral_WidensToMax() |
| 52 | + { |
| 53 | + AreEqual(int.MaxValue, await ColumnSizeAsync($"select '{new string('a', 8001)}' as x")); |
| 54 | + AreEqual(int.MaxValue, await ColumnSizeAsync($"select N'{new string('a', 4001)}' as x")); |
| 55 | + } |
| 56 | + |
| 57 | + // Concatenation: sum of widths, capped at the family maximum (not MAX). |
| 58 | + [TestMethod] |
| 59 | + [DataRow("select 'ab' + 'cde' as x", 5)] |
| 60 | + [DataRow("select N'ab' + N'cde' as x", 5)] |
| 61 | + [DataRow("select 'ab' + N'cde' as x", 5)] // mixed family → nvarchar, char-count sum |
| 62 | + [DataRow("select cast('x' as varchar(10)) + 'abc' as x", 13)] |
| 63 | + [DataRow("select replicate(cast('a' as varchar(5000)), 1) + replicate(cast('b' as varchar(5000)), 1) as x", 8000)] |
| 64 | + public async Task Concatenation_SumsCappedAtFamilyMax(string sql, int expected) |
| 65 | + => AreEqual(expected, await ColumnSizeAsync(sql)); |
| 66 | + |
| 67 | + // CASE / COALESCE / IIF / NULLIF / set ops: maximum of arm widths. |
| 68 | + [TestMethod] |
| 69 | + [DataRow("select case when 1=1 then 'ab' else 'wxyz' end as x", 4)] |
| 70 | + [DataRow("select case when 1=1 then 'ab' else null end as x", 2)] |
| 71 | + [DataRow("select case when 1=1 then 'ab' else N'wxyz' end as x", 4)] // national family, char-count max |
| 72 | + [DataRow("select coalesce('ab', 'wxyz') as x", 4)] |
| 73 | + [DataRow("select iif(1=1, 'ab', 'wxyz') as x", 4)] |
| 74 | + [DataRow("select nullif('abcd', 'x') as x", 4)] |
| 75 | + [DataRow("select 'ab' as x union all select 'wxyz'", 4)] |
| 76 | + [DataRow("select 'ab' as x union select 'wxyz'", 4)] |
| 77 | + public async Task Unification_TakesMaxWidth(string sql, int expected) |
| 78 | + => AreEqual(expected, await ColumnSizeAsync(sql)); |
| 79 | + |
| 80 | + // ISNULL fixes the result to the FIRST argument's width (unlike COALESCE). |
| 81 | + [TestMethod] |
| 82 | + [DataRow("select isnull('ab', 'wxyz') as x", 2)] |
| 83 | + [DataRow("select isnull('wxyz', 'ab') as x", 4)] |
| 84 | + public async Task IsNull_TakesFirstArgumentWidth(string sql, int expected) |
| 85 | + => AreEqual(expected, await ColumnSizeAsync(sql)); |
| 86 | + |
| 87 | + // Width-deriving / preserving functions consume the literal width. |
| 88 | + [TestMethod] |
| 89 | + [DataRow("select upper('abc') as x", 3)] // preserve input width |
| 90 | + [DataRow("select ltrim(' abc') as x", 5)] |
| 91 | + [DataRow("select left('abcdef', 2) as x", 2)] // min(inputWidth, n) |
| 92 | + [DataRow("select left('abcdef', 20) as x", 6)] |
| 93 | + [DataRow("select right('abcdef', 2) as x", 2)] |
| 94 | + [DataRow("select substring('abcdef', 2, 3) as x", 3)] |
| 95 | + [DataRow("select substring('abcdef', 5, 20) as x", 6)] |
| 96 | + [DataRow("select replicate('ab', 3) as x", 6)] // inputWidth × count |
| 97 | + [DataRow("select space(5) as x", 5)] |
| 98 | + [DataRow("select space(0) as x", 1)] // floors to 1 |
| 99 | + [DataRow("select stuff('abcdef', 2, 1, 'XY') as x", 7)] // inputWidth - delete + replacement |
| 100 | + public async Task LengthDerivingFunctions_ComputeWidth(string sql, int expected) |
| 101 | + => AreEqual(expected, await ColumnSizeAsync(sql)); |
| 102 | + |
| 103 | + // REPLACE can grow the input, so it stays the family container (8000). |
| 104 | + [TestMethod] |
| 105 | + public async Task Replace_StaysContainerWidth() |
| 106 | + => AreEqual(8000, await ColumnSizeAsync("select replace('aaa', 'a', 'XY') as x")); |
| 107 | +} |
0 commit comments