@@ -4,13 +4,16 @@ namespace SqlServerSimulator;
44
55/// <summary>
66/// Deferred name resolution in skipped control-flow branches. Real SQL Server
7- /// binds object / column names lazily, so an un-taken IF / WHILE branch (or a
8- /// block skipped after BREAK / CONTINUE / RETURN) that references a
9- /// nonexistent table / column compiles fine and is discarded. The simulator
10- /// resolves names inline with parsing, so it swallows the Msg 208 / Msg 207
11- /// that would otherwise surface — but only while dispatching in skip mode.
12- /// A taken branch still raises, and syntax / structural errors in skipped
13- /// branches still raise (only name resolution defers).
7+ /// binds base object names lazily, so an un-taken IF / WHILE branch (or a block
8+ /// skipped after BREAK / CONTINUE / RETURN) that references a nonexistent table
9+ /// / view / function compiles fine and is discarded. The simulator resolves
10+ /// names inline with parsing, so a skip-mode FROM / function-call miss
11+ /// substitutes placeholder metadata and the statement parses to completion (no
12+ /// Msg 208 / 4121). Deferral is scoped to the missing base object, though: a
13+ /// missing column on a <em>resolvable</em> table binds eagerly and raises
14+ /// Msg 207 even in a dead branch (probe-confirmed against SQL Server 2025), and
15+ /// a taken branch still raises. Syntax / structural errors in skipped branches
16+ /// still raise (only name resolution of a missing object defers).
1417/// </summary>
1518[ TestClass ]
1619public sealed class SkipModeNameResolutionTests
@@ -29,12 +32,19 @@ public void IfFalse_UnknownTable_NoBlock()
2932 public void IfTrue_UnknownTable_StillRaises208 ( )
3033 => new Simulation ( ) . AssertSqlError ( "if 1 = 1 select * from nosuchtable" , 208 ) ;
3134
35+ /// <summary>
36+ /// A missing column on a <em>resolvable</em> table is not deferred — real
37+ /// SQL Server binds an existing table's columns at compile time and raises
38+ /// Msg 207 even from an un-taken branch (probe-confirmed SQL Server 2025,
39+ /// 2026-07-17). Contrast <see cref="IfFalse_UnknownTable_NoBlock"/>, where
40+ /// the missing base object defers.
41+ /// </summary>
3242 [ TestMethod ]
33- public void IfFalse_UnknownColumnInKnownTable_Tolerated ( )
43+ public void IfFalse_UnknownColumnInKnownTable_StillRaises207 ( )
3444 {
3545 var sim = new Simulation ( ) ;
3646 _ = sim . ExecuteNonQuery ( "create table t (id int)" ) ;
37- AreEqual ( "ok" , sim . ExecuteScalar ( "if 1 = 0 begin select bad_col from t end select 'ok'" ) ) ;
47+ _ = sim . AssertSqlError ( "if 1 = 0 begin select bad_col from t end select 'ok'" , 207 ) ;
3848 }
3949
4050 [ TestMethod ]
@@ -136,4 +146,136 @@ public void TableVariableDeclaredInSkippedBranch_UsableAfter()
136146 [ TestMethod ]
137147 public void SetUndeclaredVariableInSkippedBranch_StillRaises137 ( )
138148 => new Simulation ( ) . AssertSqlError ( "if 1 = 0 begin set @never = 1 end select 'ok'" , 137 ) ;
149+
150+ // ---- Placeholder parse-continuation (probe matrix, 2026-07-17) ----
151+
152+ /// <summary>
153+ /// A missing schema-qualified scalar function in an un-taken branch defers
154+ /// (probe-confirmed: real SQL Server binds user functions lazily too, unlike
155+ /// a bare 1-part call which is a compile-time Msg 195). The call parses to
156+ /// completion and is discarded.
157+ /// </summary>
158+ [ TestMethod ]
159+ public void IfFalse_UnknownQualifiedFunction_Tolerated ( )
160+ => AreEqual ( "ok" , new Simulation ( ) . ExecuteScalar (
161+ "if 1 = 0 select dbo.no_such_fn(1, 2) select 'ok'" ) ) ;
162+
163+ /// <summary>
164+ /// A bare (1-part) unresolved function is NOT deferred — real SQL Server
165+ /// treats it as a missing built-in and raises Msg 195 at compile time, even
166+ /// in a dead branch.
167+ /// </summary>
168+ [ TestMethod ]
169+ public void IfFalse_UnknownBareFunction_StillRaises195 ( )
170+ => new Simulation ( ) . AssertSqlError ( "if 1 = 0 select no_such_fn(1) select 'ok'" , 195 ) ;
171+
172+ /// <summary>
173+ /// The un-taken THEN branch defers, so its trailing ELSE runs — the missing
174+ /// function must not orphan the ELSE into a spurious Msg 102.
175+ /// </summary>
176+ [ TestMethod ]
177+ public void IfFalse_UnknownFunctionThenElse_ElseRuns ( )
178+ => AreEqual ( "else" , new Simulation ( ) . ExecuteScalar (
179+ "if 1 = 0 select dbo.no_such_fn(1) as r else select 'else' as r" ) ) ;
180+
181+ /// <summary>
182+ /// The SSMS Query Store / server-properties shape: a missing table behind an
183+ /// EXISTS in an un-taken outer branch. Regression for the orphaned-fragment
184+ /// cascade — the recovery scan used to abandon this mid-parse and re-dispatch
185+ /// the inner branch as a bare statement (spurious Msg 102 / 156).
186+ /// </summary>
187+ [ TestMethod ]
188+ public void SkippedOuter_ExistsMissingTableWithInnerElse_Tolerated ( )
189+ => AreEqual ( "after" , new Simulation ( ) . ExecuteScalar ( """
190+ if 1 = 0 begin if exists(select * from missing) select 1 as r else select 2 as r end
191+ select 'after' as r
192+ """ ) ) ;
193+
194+ /// <summary>
195+ /// The same EXISTS-behind-a-missing-table shape at top level: the un-taken
196+ /// THEN's inner IF/ELSE parses to completion and the following statement
197+ /// runs.
198+ /// </summary>
199+ [ TestMethod ]
200+ public void SkippedIf_ExistsMissingTableWithElse_Tolerated ( )
201+ => AreEqual ( "after" , new Simulation ( ) . ExecuteScalar (
202+ "if 1 = 0 if exists(select * from missing) select 1 as r else select 2 as r select 'after' as r" ) ) ;
203+
204+ /// <summary>
205+ /// A missing table plus a second missing table inside a scalar subquery,
206+ /// both in the un-taken THEN — the whole statement defers, the ELSE runs.
207+ /// </summary>
208+ [ TestMethod ]
209+ public void SkippedIf_MissingTableWithMissingSubqueryTable_ElseRuns ( )
210+ => AreEqual ( "else" , new Simulation ( ) . ExecuteScalar (
211+ "if 1 = 0 select * from missing where a = (select b from other) else select 'else' as r" ) ) ;
212+
213+ /// <summary>
214+ /// A missing table behind a CASE WHEN EXISTS in a skipped block, with the
215+ /// block's own ELSE — parses to completion and the ELSE runs.
216+ /// </summary>
217+ [ TestMethod ]
218+ public void SkippedBlock_CaseWhenExistsMissing_ElseRuns ( )
219+ => AreEqual ( "else" , new Simulation ( ) . ExecuteScalar ( """
220+ if 1 = 0 begin select case when exists(select * from missing) then 1 else 2 end as r end
221+ else select 'else' as r
222+ """ ) ) ;
223+
224+ /// <summary>
225+ /// A missing TVF invoked in the FROM clause of an un-taken branch defers
226+ /// (the argument list is parsed and discarded); the ELSE runs.
227+ /// </summary>
228+ [ TestMethod ]
229+ public void SkippedIf_MissingTvfInFrom_ElseRuns ( )
230+ => AreEqual ( "else" , new Simulation ( ) . ExecuteScalar (
231+ "if 1 = 0 select * from dbo.no_such_tvf(1, 2) else select 'else' as r" ) ) ;
232+
233+ /// <summary>
234+ /// ORDER BY referencing a column of a missing table defers along with the
235+ /// table — no compile error in the dead branch.
236+ /// </summary>
237+ [ TestMethod ]
238+ public void SkippedIf_MissingTableOrderByMissingColumn_Tolerated ( )
239+ => AreEqual ( "ok" , new Simulation ( ) . ExecuteScalar (
240+ "if 1 = 0 select * from missing order by also_missing select 'ok'" ) ) ;
241+
242+ /// <summary>
243+ /// ORDER BY referencing a missing column of a <em>resolvable</em> table is
244+ /// not deferred — Msg 207 fires even in the dead branch.
245+ /// </summary>
246+ [ TestMethod ]
247+ public void SkippedIf_KnownTableOrderByMissingColumn_StillRaises207 ( )
248+ {
249+ var sim = new Simulation ( ) ;
250+ _ = sim . ExecuteNonQuery ( "create table t (id int)" ) ;
251+ _ = sim . AssertSqlError ( "if 1 = 0 select id from t order by nope select 'ok'" , 207 ) ;
252+ }
253+
254+ /// <summary>
255+ /// A missing table in one statement of a skipped block plus a missing column
256+ /// on a resolvable table in the next: real SQL Server binds the resolvable
257+ /// table's columns eagerly, so Msg 207 wins and aborts the batch — the
258+ /// block's ELSE never runs.
259+ /// </summary>
260+ [ TestMethod ]
261+ public void SkippedBlock_MissingTableThenBadColumnOnRealTable_Raises207 ( )
262+ {
263+ var sim = new Simulation ( ) ;
264+ _ = sim . ExecuteNonQuery ( "create table t (id int)" ) ;
265+ _ = sim . AssertSqlError ( """
266+ if 1 = 0 begin select * from missing; select bad_col from t end
267+ else select 'else' as r
268+ select 'after' as r
269+ """ , 207 ) ;
270+ }
271+
272+ /// <summary>
273+ /// A qualified column reference against a placeholder (missing) table
274+ /// resolves leniently, so the whole SELECT parses to completion and the ELSE
275+ /// runs.
276+ /// </summary>
277+ [ TestMethod ]
278+ public void SkippedIf_QualifiedColumnsOnMissingTable_ElseRuns ( )
279+ => AreEqual ( "else" , new Simulation ( ) . ExecuteScalar (
280+ "if 1 = 0 select m.foo, m.bar from missing m else select 'else' as r" ) ) ;
139281}
0 commit comments