Skip to content

Commit 0cf2e6e

Browse files
committed
Inline TVFs: CREATE/DROP FUNCTION schema.name(@p [=default]) RETURNS TABLE [WITH SCHEMABINDING | ENCRYPTION] AS RETURN [(] <SELECT> [)] called from FROM clauses, split UserDefinedFunction into abstract base + ScalarFunction /
InlineTableValuedFunction, output schema derived at CREATE-time (Msg 4514 unnamed / Msg 4506 duplicate), routing collisions (scalar-in-FROM → Msg 208, TVF-in-expression → Msg 4121), APPLY requires derived-table-or-TVF, sys.objects 'IF' rows + sys.columns output projection + sys.parameters skips return-row + OBJECT_ID 'IF' filter, density pass on both UDF sections of CLAUDE.md.
1 parent c3bb055 commit 0cf2e6e

14 files changed

Lines changed: 1059 additions & 137 deletions

CLAUDE.md

Lines changed: 39 additions & 23 deletions
Large diffs are not rendered by default.
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Behavioral tests for inline table-valued functions: CREATE / DROP, FROM-
8+
/// clause invocation, parameter binding (including DEFAULT keyword), the
9+
/// always-lateral correlation pattern under CROSS APPLY / OUTER APPLY,
10+
/// catalog-view surface (sys.objects with type 'IF', sys.columns for
11+
/// output projection, sys.parameters with no return-row), and the error
12+
/// paths probe-confirmed against SQL Server 2025 (Msg 487 / 4514 / 4506
13+
/// / 208 / 4121 / 313 / 8144).
14+
/// </summary>
15+
[TestClass]
16+
public sealed class InlineTvfTests
17+
{
18+
private static DbConnection Open() => new Simulation().CreateOpenConnection();
19+
20+
private static DbException AssertSqlError(DbConnection connection, string sql, int errorNumber)
21+
{
22+
var ex = Throws<DbException>(() => connection.CreateCommand(sql).ExecuteScalar());
23+
AreEqual(errorNumber.ToString(), ex.Data["HelpLink.EvtID"], $"expected Msg {errorNumber}");
24+
return ex;
25+
}
26+
27+
[TestMethod]
28+
public void Create_And_Call_BasicTvf_ReturnsRowFromParameter()
29+
{
30+
using var connection = Open();
31+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as a, @x * 2 as b)").ExecuteNonQuery();
32+
using var reader = connection.CreateCommand("select a, b from dbo.tvf(5)").ExecuteReader();
33+
IsTrue(reader.Read());
34+
AreEqual(5, reader.GetInt32(0));
35+
AreEqual(10, reader.GetInt32(1));
36+
IsFalse(reader.Read());
37+
}
38+
39+
[TestMethod]
40+
public void Body_WithoutParens_IsAccepted()
41+
{
42+
using var connection = Open();
43+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return select @x as v").ExecuteNonQuery();
44+
AreEqual(7, connection.CreateCommand("select v from dbo.tvf(7)").ExecuteScalar());
45+
}
46+
47+
[TestMethod]
48+
public void ZeroArg_Tvf_Works()
49+
{
50+
using var connection = Open();
51+
_ = connection.CreateCommand("create function dbo.tvf() returns table as return (select 1 as a, 'hi' as b)").ExecuteNonQuery();
52+
using var reader = connection.CreateCommand("select * from dbo.tvf()").ExecuteReader();
53+
IsTrue(reader.Read());
54+
AreEqual(1, reader.GetInt32(0));
55+
AreEqual("hi", reader.GetString(1));
56+
}
57+
58+
[TestMethod]
59+
public void WithSchemabinding_ParsesAndIgnores()
60+
{
61+
using var connection = Open();
62+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table with schemabinding as return (select @x as v)").ExecuteNonQuery();
63+
AreEqual(11, connection.CreateCommand("select v from dbo.tvf(11)").ExecuteScalar());
64+
}
65+
66+
[TestMethod]
67+
public void WithEncryption_ParsesAndIgnores()
68+
{
69+
using var connection = Open();
70+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table with encryption as return (select @x as v)").ExecuteNonQuery();
71+
AreEqual(13, connection.CreateCommand("select v from dbo.tvf(13)").ExecuteScalar());
72+
}
73+
74+
[TestMethod]
75+
public void WithSchemabindingAndEncryption_Combined_ParsesAndIgnores()
76+
{
77+
using var connection = Open();
78+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table with encryption, schemabinding as return (select @x as v)").ExecuteNonQuery();
79+
AreEqual(17, connection.CreateCommand("select v from dbo.tvf(17)").ExecuteScalar());
80+
}
81+
82+
[TestMethod]
83+
public void With_ReturnsNullOnNullInput_OnTvf_Raises_Msg487()
84+
{
85+
using var connection = Open();
86+
var ex = AssertSqlError(connection, "create function dbo.tvf(@x int) returns table with returns null on null input as return (select @x as v)", 487);
87+
Contains("CREATE/ALTER FUNCTION", ex.Message);
88+
}
89+
90+
[TestMethod]
91+
public void DefaultParam_WithDefaultKeyword_Materializes()
92+
{
93+
using var connection = Open();
94+
_ = connection.CreateCommand("create function dbo.tvf(@x int = 99) returns table as return (select @x as v)").ExecuteNonQuery();
95+
AreEqual(99, connection.CreateCommand("select v from dbo.tvf(default)").ExecuteScalar());
96+
AreEqual(5, connection.CreateCommand("select v from dbo.tvf(5)").ExecuteScalar());
97+
}
98+
99+
[TestMethod]
100+
public void BareOmission_OfDefaultParam_Raises_Msg313()
101+
{
102+
using var connection = Open();
103+
_ = connection.CreateCommand("create function dbo.tvf(@x int = 99) returns table as return (select @x as v)").ExecuteNonQuery();
104+
_ = AssertSqlError(connection, "select v from dbo.tvf()", 313);
105+
}
106+
107+
[TestMethod]
108+
public void Insufficient_Args_Raises_Msg313()
109+
{
110+
using var connection = Open();
111+
_ = connection.CreateCommand("create function dbo.tvf(@x int, @y int) returns table as return (select @x + @y as v)").ExecuteNonQuery();
112+
_ = AssertSqlError(connection, "select v from dbo.tvf(1)", 313);
113+
}
114+
115+
[TestMethod]
116+
public void Too_Many_Args_Raises_Msg8144()
117+
{
118+
using var connection = Open();
119+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as v)").ExecuteNonQuery();
120+
_ = AssertSqlError(connection, "select v from dbo.tvf(1, 2)", 8144);
121+
}
122+
123+
[TestMethod]
124+
public void Unnamed_Projection_Column_Raises_Msg4514()
125+
{
126+
using var connection = Open();
127+
var ex = AssertSqlError(connection, "create function dbo.tvf(@x int) returns table as return (select @x + 1)", 4514);
128+
Contains("column 1", ex.Message);
129+
}
130+
131+
[TestMethod]
132+
public void Duplicate_Column_Names_Raises_Msg4506()
133+
{
134+
using var connection = Open();
135+
var ex = AssertSqlError(connection, "create function dbo.tvf(@x int) returns table as return (select @x as a, @x as a)", 4506);
136+
Contains("'a'", ex.Message);
137+
Contains("'tvf'", ex.Message);
138+
}
139+
140+
[TestMethod]
141+
public void Scalar_UDF_Called_In_FROM_Raises_Msg208()
142+
{
143+
using var connection = Open();
144+
_ = connection.CreateCommand("create function dbo.scalar_f(@x int) returns int as begin return @x * 10 end").ExecuteNonQuery();
145+
_ = AssertSqlError(connection, "select * from dbo.scalar_f(5)", 208);
146+
}
147+
148+
[TestMethod]
149+
public void Inline_TVF_Used_As_Scalar_Raises_Msg4121()
150+
{
151+
using var connection = Open();
152+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as v)").ExecuteNonQuery();
153+
_ = AssertSqlError(connection, "select dbo.tvf(5)", 4121);
154+
}
155+
156+
[TestMethod]
157+
public void Missing_TVF_In_FROM_Raises_Msg208()
158+
{
159+
using var connection = Open();
160+
_ = AssertSqlError(connection, "select * from dbo.does_not_exist(1)", 208);
161+
}
162+
163+
[TestMethod]
164+
public void CrossApply_WithCorrelatedArg_BindsPerRow()
165+
{
166+
using var connection = Open();
167+
_ = connection.CreateCommand("create table #nums(n int)").ExecuteNonQuery();
168+
_ = connection.CreateCommand("insert #nums values (1), (2), (3)").ExecuteNonQuery();
169+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as a, @x * 10 as b)").ExecuteNonQuery();
170+
using var reader = connection.CreateCommand("select n.n, t.a, t.b from #nums n cross apply dbo.tvf(n.n) t order by n.n").ExecuteReader();
171+
var pairs = new List<(int n, int a, int b)>();
172+
while (reader.Read())
173+
pairs.Add((reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2)));
174+
HasCount(3, pairs);
175+
AreEqual((1, 1, 10), pairs[0]);
176+
AreEqual((2, 2, 20), pairs[1]);
177+
AreEqual((3, 3, 30), pairs[2]);
178+
}
179+
180+
[TestMethod]
181+
public void OuterApply_Empty_Tvf_NullFills()
182+
{
183+
using var connection = Open();
184+
_ = connection.CreateCommand("create table #nums(n int)").ExecuteNonQuery();
185+
_ = connection.CreateCommand("insert #nums values (1), (2)").ExecuteNonQuery();
186+
// The body filters its rows on a parameter-dependent predicate so the
187+
// TVF returns zero rows for the test input — exercising OUTER APPLY's
188+
// null-fill path without depending on no-FROM WHERE support.
189+
_ = connection.CreateCommand("create table #src(v int)").ExecuteNonQuery();
190+
_ = connection.CreateCommand("insert #src values (10), (20)").ExecuteNonQuery();
191+
_ = connection.CreateCommand("create function dbo.tvf_empty(@x int) returns table as return (select v from #src where v = @x * 1000)").ExecuteNonQuery();
192+
using var reader = connection.CreateCommand("select n.n, t.v from #nums n outer apply dbo.tvf_empty(n.n) t order by n.n").ExecuteReader();
193+
var rows = new List<(int n, int? v)>();
194+
while (reader.Read())
195+
rows.Add((reader.GetInt32(0), reader.IsDBNull(1) ? null : reader.GetInt32(1)));
196+
HasCount(2, rows);
197+
AreEqual((1, null), rows[0]);
198+
AreEqual((2, null), rows[1]);
199+
}
200+
201+
[TestMethod]
202+
public void DropFunction_Removes_Tvf()
203+
{
204+
using var connection = Open();
205+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as v)").ExecuteNonQuery();
206+
AreEqual(5, connection.CreateCommand("select v from dbo.tvf(5)").ExecuteScalar());
207+
_ = connection.CreateCommand("drop function dbo.tvf").ExecuteNonQuery();
208+
_ = AssertSqlError(connection, "select v from dbo.tvf(5)", 208);
209+
}
210+
211+
[TestMethod]
212+
public void DropFunction_IfExists_NoOps()
213+
{
214+
using var connection = Open();
215+
_ = connection.CreateCommand("drop function if exists dbo.does_not_exist").ExecuteNonQuery();
216+
}
217+
218+
[TestMethod]
219+
public void Tvf_Body_References_Real_Table()
220+
{
221+
using var connection = Open();
222+
_ = connection.CreateCommand("create table dbo.t(id int, label varchar(20))").ExecuteNonQuery();
223+
_ = connection.CreateCommand("insert dbo.t values (1, 'a'), (2, 'b'), (3, 'c')").ExecuteNonQuery();
224+
_ = connection.CreateCommand("create function dbo.tvf(@min int) returns table as return (select id, label from dbo.t where id >= @min)").ExecuteNonQuery();
225+
using var reader = connection.CreateCommand("select id, label from dbo.tvf(2) order by id").ExecuteReader();
226+
var rows = new List<(int id, string label)>();
227+
while (reader.Read())
228+
rows.Add((reader.GetInt32(0), reader.GetString(1)));
229+
HasCount(2, rows);
230+
AreEqual((2, "b"), rows[0]);
231+
AreEqual((3, "c"), rows[1]);
232+
}
233+
234+
[TestMethod]
235+
public void SysObjects_HasInlineTvfRow_WithTypeIF()
236+
{
237+
using var connection = Open();
238+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as v)").ExecuteNonQuery();
239+
using var reader = connection.CreateCommand("select name, type, type_desc from sys.objects where name = 'tvf'").ExecuteReader();
240+
IsTrue(reader.Read());
241+
AreEqual("tvf", reader.GetString(0));
242+
AreEqual("IF", reader.GetString(1));
243+
AreEqual("SQL_INLINE_TABLE_VALUED_FUNCTION", reader.GetString(2));
244+
}
245+
246+
[TestMethod]
247+
public void SysColumns_Emits_TvfOutputProjection()
248+
{
249+
using var connection = Open();
250+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as a, @x * 2 as b)").ExecuteNonQuery();
251+
using var reader = connection.CreateCommand("select name, column_id from sys.columns where object_id = object_id('dbo.tvf', 'IF') order by column_id").ExecuteReader();
252+
var cols = new List<(string name, int id)>();
253+
while (reader.Read())
254+
cols.Add((reader.GetString(0), reader.GetInt32(1)));
255+
HasCount(2, cols);
256+
AreEqual(("a", 1), cols[0]);
257+
AreEqual(("b", 2), cols[1]);
258+
}
259+
260+
[TestMethod]
261+
public void SysParameters_TvfHasNoReturnRow()
262+
{
263+
using var connection = Open();
264+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as v)").ExecuteNonQuery();
265+
using var reader = connection.CreateCommand("select name, parameter_id, is_output from sys.parameters where object_id = object_id('dbo.tvf', 'IF') order by parameter_id").ExecuteReader();
266+
var rows = new List<(string name, int id, bool isOut)>();
267+
while (reader.Read())
268+
rows.Add((reader.GetString(0), reader.GetInt32(1), reader.GetBoolean(2)));
269+
// Only the declared parameter — no synthetic return row.
270+
HasCount(1, rows);
271+
AreEqual(("@x", 1, false), rows[0]);
272+
}
273+
274+
[TestMethod]
275+
public void ObjectId_WithIFFilter_ResolvesInlineTvfOnly()
276+
{
277+
using var connection = Open();
278+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as v)").ExecuteNonQuery();
279+
_ = connection.CreateCommand("create function dbo.scalar_f(@x int) returns int as begin return @x end").ExecuteNonQuery();
280+
281+
IsNotNull(connection.CreateCommand("select object_id('dbo.tvf', 'IF')").ExecuteScalar());
282+
AreEqual(DBNull.Value, connection.CreateCommand("select object_id('dbo.tvf', 'FN')").ExecuteScalar());
283+
AreEqual(DBNull.Value, connection.CreateCommand("select object_id('dbo.scalar_f', 'IF')").ExecuteScalar());
284+
IsNotNull(connection.CreateCommand("select object_id('dbo.scalar_f', 'FN')").ExecuteScalar());
285+
}
286+
287+
[TestMethod]
288+
public void Tvf_WithAlias_RebindsColumnQualifier()
289+
{
290+
using var connection = Open();
291+
_ = connection.CreateCommand("create function dbo.tvf(@x int) returns table as return (select @x as a, @x * 2 as b)").ExecuteNonQuery();
292+
using var reader = connection.CreateCommand("select c.a, c.b from dbo.tvf(3) as c").ExecuteReader();
293+
IsTrue(reader.Read());
294+
AreEqual(3, reader.GetInt32(0));
295+
AreEqual(6, reader.GetInt32(1));
296+
}
297+
}

0 commit comments

Comments
 (0)