Skip to content

Commit 5e1a836

Browse files
committed
Implemented GENERATE_SERIES.
1 parent 0795a16 commit 5e1a836

7 files changed

Lines changed: 670 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
140140

141141
Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger: read the linked file on demand when working in the matching area.
142142

143-
- **Built-in scalars** (math, date incl. DATETRUNC / DATE_BUCKET / SWITCHOFFSET / TODATETIMEOFFSET, current-time, `*FROMPARTS`, AT TIME ZONE, CONCAT, char-code, SOUNDEX / STR / TRANSLATE / STRING_ESCAPE / DIFFERENCE, CHOOSE / IIF, bit manipulation (BIT_COUNT / GET_BIT / SET_BIT / LEFT_SHIFT / RIGHT_SHIFT), CHECKSUM / BINARY_CHECKSUM, FORMAT, RAND, STRING_SPLIT, COMPRESS/DECOMPRESS, session/server `@@`-constants + HOST_NAME / APP_NAME / GETANSINULL / ORIGINAL_DB_NAME) → [`scalars.md`](docs/claude/scalars.md).
143+
- **Built-in scalars** (math, date incl. DATETRUNC / DATE_BUCKET / SWITCHOFFSET / TODATETIMEOFFSET, current-time, `*FROMPARTS`, AT TIME ZONE, CONCAT, char-code, SOUNDEX / STR / TRANSLATE / STRING_ESCAPE / DIFFERENCE, CHOOSE / IIF, bit manipulation (BIT_COUNT / GET_BIT / SET_BIT / LEFT_SHIFT / RIGHT_SHIFT), CHECKSUM / BINARY_CHECKSUM, FORMAT, RAND, STRING_SPLIT, GENERATE_SERIES, COMPRESS/DECOMPRESS, session/server `@@`-constants + HOST_NAME / APP_NAME / GETANSINULL / ORIGINAL_DB_NAME) → [`scalars.md`](docs/claude/scalars.md).
144144
- **`SqlType.Promote` / `PromoteForArithmetic` / decimal precision-scale / int↔string promotion**[`arithmetic.md`](docs/claude/arithmetic.md).
145145
- **`Cast` / coercion error paths** (CAST/CONVERT narrow targets, TRY_CAST/TRY_CONVERT swallow set, PARSE/TRY_PARSE culture-aware parsing) → [`casting.md`](docs/claude/casting.md).
146146
- **`Selection`, aggregates, window functions, set ops, CASE, OFFSET/FETCH**[`query.md`](docs/claude/query.md).
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Behavioral tests for the built-in <c>GENERATE_SERIES(start, stop [, step])</c>
8+
/// rowset function. Validates the output schema, type-inference rules
9+
/// (integer-subtype distinctness, decimal-family collapse), default-step
10+
/// direction (probe-confirmed against SQL Server 2025), NULL handling
11+
/// (empty rowset), and error paths (Msg 313 / 4199 / 5373 / 8116 / 8144).
12+
/// </summary>
13+
[TestClass]
14+
public sealed class GenerateSeriesTests
15+
{
16+
private static List<long> ReadAllLong(DbDataReader reader)
17+
{
18+
var rows = new List<long>();
19+
while (reader.Read())
20+
rows.Add(reader.GetInt64(0));
21+
return rows;
22+
}
23+
24+
[TestMethod]
25+
public void Basic_TwoArg_AscendingSequence()
26+
{
27+
using var conn = new Simulation().CreateOpenConnection();
28+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(1, 5)").ExecuteReader();
29+
var rows = new List<int>();
30+
while (reader.Read()) rows.Add(reader.GetInt32(0));
31+
HasCount(5, rows);
32+
AreEqual(1, rows[0]);
33+
AreEqual(5, rows[4]);
34+
}
35+
36+
[TestMethod]
37+
public void OutputColumn_NamedValue()
38+
{
39+
using var conn = new Simulation().CreateOpenConnection();
40+
using var reader = conn.CreateCommand("select * from GENERATE_SERIES(1, 1)").ExecuteReader();
41+
AreEqual(1, reader.FieldCount);
42+
AreEqual("value", reader.GetName(0));
43+
}
44+
45+
[TestMethod]
46+
public void ThreeArg_PositiveStep()
47+
{
48+
using var conn = new Simulation().CreateOpenConnection();
49+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(1, 10, 2)").ExecuteReader();
50+
var rows = new List<int>();
51+
while (reader.Read()) rows.Add(reader.GetInt32(0));
52+
CollectionAssert.AreEqual(new[] { 1, 3, 5, 7, 9 }, rows);
53+
}
54+
55+
[TestMethod]
56+
public void DefaultStep_Descending_WhenStartGreaterThanStop()
57+
{
58+
// Default step direction follows start vs stop: -1 when start > stop,
59+
// 1 otherwise (matches Microsoft's docs and live SQL Server 2025).
60+
using var conn = new Simulation().CreateOpenConnection();
61+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(5, 1)").ExecuteReader();
62+
var rows = new List<int>();
63+
while (reader.Read()) rows.Add(reader.GetInt32(0));
64+
CollectionAssert.AreEqual(new[] { 5, 4, 3, 2, 1 }, rows);
65+
}
66+
67+
[TestMethod]
68+
public void ExplicitNegativeStep_Descending()
69+
{
70+
using var conn = new Simulation().CreateOpenConnection();
71+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(5, 1, -1)").ExecuteReader();
72+
var rows = new List<int>();
73+
while (reader.Read()) rows.Add(reader.GetInt32(0));
74+
CollectionAssert.AreEqual(new[] { 5, 4, 3, 2, 1 }, rows);
75+
}
76+
77+
[TestMethod]
78+
public void WrongDirectionStep_PositiveStartGreaterThanStop_Empty()
79+
{
80+
// Probe: explicit positive step with start>stop → empty rowset, no error.
81+
using var conn = new Simulation().CreateOpenConnection();
82+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(5, 1, 1)").ExecuteReader();
83+
IsFalse(reader.Read());
84+
}
85+
86+
[TestMethod]
87+
public void WrongDirectionStep_NegativeStartLessThanStop_Empty()
88+
{
89+
using var conn = new Simulation().CreateOpenConnection();
90+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(1, 5, -1)").ExecuteReader();
91+
IsFalse(reader.Read());
92+
}
93+
94+
[TestMethod]
95+
public void StartEqualsStop_SingleRow()
96+
{
97+
using var conn = new Simulation().CreateOpenConnection();
98+
AreEqual(1, conn.CreateCommand("select count(*) from GENERATE_SERIES(5, 5)").ExecuteScalar());
99+
AreEqual(5, conn.CreateCommand("select value from GENERATE_SERIES(5, 5)").ExecuteScalar());
100+
}
101+
102+
[TestMethod]
103+
public void StepLandsExactlyOnStop_IncludesStop()
104+
{
105+
using var conn = new Simulation().CreateOpenConnection();
106+
var rows = new List<int>();
107+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(0, 10, 5)").ExecuteReader();
108+
while (reader.Read()) rows.Add(reader.GetInt32(0));
109+
CollectionAssert.AreEqual(new[] { 0, 5, 10 }, rows);
110+
}
111+
112+
[TestMethod]
113+
public void StepUndershootsStop_StopsBefore()
114+
{
115+
using var conn = new Simulation().CreateOpenConnection();
116+
var rows = new List<int>();
117+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(0, 9, 5)").ExecuteReader();
118+
while (reader.Read()) rows.Add(reader.GetInt32(0));
119+
CollectionAssert.AreEqual(new[] { 0, 5 }, rows);
120+
}
121+
122+
[TestMethod]
123+
public void NegativeRange()
124+
{
125+
using var conn = new Simulation().CreateOpenConnection();
126+
var rows = new List<int>();
127+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(-3, 3)").ExecuteReader();
128+
while (reader.Read()) rows.Add(reader.GetInt32(0));
129+
CollectionAssert.AreEqual(new[] { -3, -2, -1, 0, 1, 2, 3 }, rows);
130+
}
131+
132+
// === Type preservation ===
133+
134+
[TestMethod]
135+
public void TinyInt_PreservesType()
136+
{
137+
using var conn = new Simulation().CreateOpenConnection();
138+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(cast(1 as tinyint), cast(3 as tinyint))").ExecuteReader();
139+
IsTrue(reader.Read());
140+
AreEqual(typeof(byte), reader.GetFieldType(0));
141+
AreEqual((byte)1, reader.GetByte(0));
142+
}
143+
144+
[TestMethod]
145+
public void SmallInt_PreservesType()
146+
{
147+
using var conn = new Simulation().CreateOpenConnection();
148+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(cast(1 as smallint), cast(3 as smallint))").ExecuteReader();
149+
IsTrue(reader.Read());
150+
AreEqual(typeof(short), reader.GetFieldType(0));
151+
}
152+
153+
[TestMethod]
154+
public void BigInt_PreservesType()
155+
{
156+
using var conn = new Simulation().CreateOpenConnection();
157+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(cast(1 as bigint), cast(3 as bigint))").ExecuteReader();
158+
IsTrue(reader.Read());
159+
AreEqual(typeof(long), reader.GetFieldType(0));
160+
}
161+
162+
[TestMethod]
163+
public void Decimal_PreservesType()
164+
{
165+
using var conn = new Simulation().CreateOpenConnection();
166+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(cast(1.0 as decimal(10,1)), cast(2.0 as decimal(10,1)), cast(0.5 as decimal(10,1)))").ExecuteReader();
167+
var rows = new List<decimal>();
168+
while (reader.Read()) rows.Add(reader.GetDecimal(0));
169+
CollectionAssert.AreEqual(new[] { 1.0m, 1.5m, 2.0m }, rows);
170+
}
171+
172+
[TestMethod]
173+
public void Decimal_MixedPrecisionAndScale_Promotes()
174+
{
175+
// Probe: DECIMAL(10,1) + DECIMAL(10,2) is accepted; the projected
176+
// scale unifies to the wider one.
177+
using var conn = new Simulation().CreateOpenConnection();
178+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(cast(1.0 as decimal(10,1)), cast(3.00 as decimal(10,2)))").ExecuteReader();
179+
IsTrue(reader.Read());
180+
AreEqual(typeof(decimal), reader.GetFieldType(0));
181+
}
182+
183+
// === NULL handling ===
184+
185+
[TestMethod]
186+
public void NullStart_Empty()
187+
{
188+
using var conn = new Simulation().CreateOpenConnection();
189+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(cast(NULL as int), 5)").ExecuteReader();
190+
IsFalse(reader.Read());
191+
}
192+
193+
[TestMethod]
194+
public void NullStop_Empty()
195+
{
196+
using var conn = new Simulation().CreateOpenConnection();
197+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(1, cast(NULL as int))").ExecuteReader();
198+
IsFalse(reader.Read());
199+
}
200+
201+
[TestMethod]
202+
public void NullStep_Empty()
203+
{
204+
using var conn = new Simulation().CreateOpenConnection();
205+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(1, 5, cast(NULL as int))").ExecuteReader();
206+
IsFalse(reader.Read());
207+
}
208+
209+
[TestMethod]
210+
public void UntypedNullStart_Empty()
211+
{
212+
// Probe: bare NULL is accepted and produces an empty rowset; SQL
213+
// Server infers the column type from the non-NULL arg.
214+
using var conn = new Simulation().CreateOpenConnection();
215+
using var reader = conn.CreateCommand("select value from GENERATE_SERIES(NULL, 5)").ExecuteReader();
216+
IsFalse(reader.Read());
217+
}
218+
219+
// === Errors ===
220+
221+
[TestMethod]
222+
public void StepZero_Msg4199()
223+
{
224+
var ex = new Simulation().AssertSqlError("select value from GENERATE_SERIES(1, 5, 0)", 4199);
225+
Assert.Contains("Argument value 0 is invalid for argument 3 of generate_series function", ex.Message);
226+
}
227+
228+
[TestMethod]
229+
public void MismatchedIntegerSubtypes_Msg5373()
230+
{
231+
var ex = new Simulation().AssertSqlError("select value from GENERATE_SERIES(1, cast(5 as bigint))", 5373);
232+
Assert.Contains("same type", ex.Message);
233+
}
234+
235+
[TestMethod]
236+
public void IntAndDecimalMismatch_Msg5373()
237+
{
238+
_ = new Simulation().AssertSqlError("select value from GENERATE_SERIES(cast(1.5 as decimal(10,1)), 5)", 5373);
239+
}
240+
241+
[TestMethod]
242+
public void StepTypeMismatch_Msg5373()
243+
{
244+
_ = new Simulation().AssertSqlError("select value from GENERATE_SERIES(1, 5, cast(1 as bigint))", 5373);
245+
}
246+
247+
[TestMethod]
248+
public void FloatArg_Msg8116()
249+
{
250+
var ex = new Simulation().AssertSqlError("select value from GENERATE_SERIES(1.0e0, 5.0e0)", 8116);
251+
Assert.Contains("Argument data type float is invalid for argument 1 of generate_series function", ex.Message);
252+
}
253+
254+
[TestMethod]
255+
public void MoneyArg_Msg8116()
256+
{
257+
_ = new Simulation().AssertSqlError("select value from GENERATE_SERIES(cast(1 as money), cast(5 as money))", 8116);
258+
}
259+
260+
[TestMethod]
261+
public void VarcharArg_Msg8116()
262+
{
263+
_ = new Simulation().AssertSqlError("select value from GENERATE_SERIES('1', '5')", 8116);
264+
}
265+
266+
[TestMethod]
267+
public void DateArg_Msg8116()
268+
{
269+
_ = new Simulation().AssertSqlError("select value from GENERATE_SERIES(cast('2025-01-01' as date), cast('2025-01-10' as date))", 8116);
270+
}
271+
272+
[TestMethod]
273+
public void ZeroArgs_Msg313()
274+
{
275+
var ex = new Simulation().AssertSqlError("select value from GENERATE_SERIES()", 313);
276+
Assert.Contains("insufficient number of arguments", ex.Message);
277+
Assert.Contains("GENERATE_SERIES", ex.Message);
278+
}
279+
280+
[TestMethod]
281+
public void OneArg_Msg313()
282+
{
283+
_ = new Simulation().AssertSqlError("select value from GENERATE_SERIES(5)", 313);
284+
}
285+
286+
[TestMethod]
287+
public void FourArgs_Msg8144()
288+
{
289+
var ex = new Simulation().AssertSqlError("select value from GENERATE_SERIES(1, 5, 1, 1)", 8144);
290+
Assert.Contains("too many arguments", ex.Message);
291+
}
292+
293+
// === Composition ===
294+
295+
[TestMethod]
296+
public void CrossJoin_SelfMultiplies()
297+
{
298+
using var conn = new Simulation().CreateOpenConnection();
299+
AreEqual(6, conn.CreateCommand("select count(*) from GENERATE_SERIES(1,3) a cross join GENERATE_SERIES(1,2) b").ExecuteScalar());
300+
}
301+
302+
[TestMethod]
303+
public void CrossApply_LateralPerOuterRow()
304+
{
305+
// Per outer row, GENERATE_SERIES(1, outer.n) yields outer.n rows.
306+
// For n IN {3, 5}: 3 + 5 = 8 rows total.
307+
using var conn = new Simulation().CreateOpenConnection();
308+
_ = conn.CreateCommand("""
309+
create table src (n int);
310+
insert src values (3), (5)
311+
""").ExecuteNonQuery();
312+
AreEqual(8, conn.CreateCommand("""
313+
select count(*)
314+
from src s
315+
cross apply GENERATE_SERIES(1, s.n) as v
316+
""").ExecuteScalar());
317+
}
318+
319+
[TestMethod]
320+
public void AliasedColumn_QualifiedAccess()
321+
{
322+
using var conn = new Simulation().CreateOpenConnection();
323+
AreEqual(6, conn.CreateCommand("select sum(v.value) from GENERATE_SERIES(1,3) v").ExecuteScalar());
324+
}
325+
326+
[TestMethod]
327+
public void Variable_StartAndStop()
328+
{
329+
AreEqual(10, new Simulation().ExecuteScalar("declare @s int = 1, @e int = 4; select sum(value) from GENERATE_SERIES(@s, @e)"));
330+
}
331+
332+
[TestMethod]
333+
public void Expression_StartAndStop()
334+
{
335+
AreEqual(21, new Simulation().ExecuteScalar("select sum(value) from GENERATE_SERIES(1+0, 2*3)"));
336+
}
337+
338+
[TestMethod]
339+
public void BigIntNearMax_NoOverflow()
340+
{
341+
// Probe: start near MAX_BIGINT, stop = MAX_BIGINT, step = 3 yields
342+
// three rows (the last is 9223372036854775806; the next iteration's
343+
// overflow is the termination signal, not an exception).
344+
using var conn = new Simulation().CreateOpenConnection();
345+
using var reader = conn.CreateCommand(
346+
"select value from GENERATE_SERIES(cast(9223372036854775800 as bigint), cast(9223372036854775807 as bigint), cast(3 as bigint))").ExecuteReader();
347+
var rows = ReadAllLong(reader);
348+
CollectionAssert.AreEqual(new[] { 9223372036854775800L, 9223372036854775803L, 9223372036854775806L }, rows);
349+
}
350+
351+
[TestMethod]
352+
public void DecimalStep_FractionalIncrements()
353+
{
354+
using var conn = new Simulation().CreateOpenConnection();
355+
using var reader = conn.CreateCommand(
356+
"select value from GENERATE_SERIES(cast(0 as decimal(5,1)), cast(1 as decimal(5,1)), cast(0.3 as decimal(5,1)))").ExecuteReader();
357+
var rows = new List<decimal>();
358+
while (reader.Read()) rows.Add(reader.GetDecimal(0));
359+
CollectionAssert.AreEqual(new[] { 0.0m, 0.3m, 0.6m, 0.9m }, rows);
360+
}
361+
}

SqlServerSimulator/Errors/SimulatedSqlException.QueryErrors.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,25 @@ internal static SimulatedSqlException UnrecognizedTableHint(ReadOnlySpan<char> h
475475
internal static SimulatedSqlException StringSplitEnableOrdinalMustBeConstant() =>
476476
new($"The enable_ordinal argument for string_split only supports constant values (not variables or columns).", 8748, 16, 1);
477477

478+
/// <summary>
479+
/// Mimics SQL Server's Msg 5373 — fired by <c>GENERATE_SERIES</c> when the
480+
/// three argument types don't all match. Wording probe-confirmed against
481+
/// SQL Server 2025: the suffix lists the supported types verbatim
482+
/// (<c>tinyint, smallint, int, bigint, decimal and numeric</c>) and the
483+
/// State / Class are 1 / 16.
484+
/// </summary>
485+
internal static SimulatedSqlException GenerateSeriesArgsMustShareType() =>
486+
new("All the input parameters should be of the same type. Supported types are tinyint, smallint, int, bigint, decimal and numeric.", 5373, 16, 1);
487+
488+
/// <summary>
489+
/// Mimics SQL Server's Msg 4199 — fired by <c>GENERATE_SERIES</c> when the
490+
/// optional third argument (<c>step</c>) is zero. Same Msg-number as
491+
/// <see cref="StringSplitInvalidEnableOrdinal"/>; only the function name in
492+
/// the message differs (<c>generate_series</c> here).
493+
/// </summary>
494+
internal static SimulatedSqlException GenerateSeriesStepZero() =>
495+
new("Argument value 0 is invalid for argument 3 of generate_series function.", 4199, 16, 1);
496+
478497
/// <summary>
479498
/// Mimics SQL Server error 13618: an <c>AS JSON</c> modifier in an
480499
/// <c>OPENJSON ... WITH</c> column declaration appears on a column whose

0 commit comments

Comments
 (0)