Skip to content

Commit 5389658

Browse files
authored
Fix error handling in SimpleExecuteRequest to return actual SQL error messages (#2536)
* Collect SQL error messages for simple query execution * Return success with empty columns for queries with no result sets * Add integration tests for error message collection and no-result-set queries * Revert manual loc changes * only add to sr.strings... * run srgen
1 parent 373d0f9 commit 5389658

File tree

6 files changed

+148
-3
lines changed

6 files changed

+148
-3
lines changed

src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@ public static string QueryServiceCompletedSuccessfully
287287
}
288288
}
289289

290+
public static string QueryServiceQueryExecutionCompletedWithErrors
291+
{
292+
get
293+
{
294+
return Keys.GetString(Keys.QueryServiceQueryExecutionCompletedWithErrors);
295+
}
296+
}
297+
290298
public static string QueryServiceColumnNull
291299
{
292300
get
@@ -11111,6 +11119,9 @@ public class Keys
1111111119
public const string QueryServiceQueryFailed = "QueryServiceQueryFailed";
1111211120

1111311121

11122+
public const string QueryServiceQueryExecutionCompletedWithErrors = "QueryServiceQueryExecutionCompletedWithErrors";
11123+
11124+
1111411125
public const string QueryServiceColumnNull = "QueryServiceColumnNull";
1111511126

1111611127

src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@
305305
<comment>.
306306
Parameters: 0 - message (string) </comment>
307307
</data>
308+
<data name="QueryServiceQueryExecutionCompletedWithErrors" xml:space="preserve">
309+
<value>Query execution completed with errors</value>
310+
<comment></comment>
311+
</data>
308312
<data name="QueryServiceColumnNull" xml:space="preserve">
309313
<value>(No column name)</value>
310314
<comment></comment>

src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ QueryServiceErrorFormat(int msg, int lvl, int state, int line, string newLine, s
126126

127127
QueryServiceQueryFailed(string message) = Query failed: {0}
128128

129+
QueryServiceQueryExecutionCompletedWithErrors = Query execution completed with errors
130+
129131
QueryServiceColumnNull = (No column name)
130132

131133
QueryServiceCellNull = NULL

src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7043,6 +7043,11 @@ The Query Processor estimates that implementing the following index could improv
70437043
<target state="new">If your application is connecting to an AlwaysOn availability group (AG) on different subnets, setting this to true provides faster detection of and connection to the (currently) active server.</target>
70447044
<note></note>
70457045
</trans-unit>
7046+
<trans-unit id="QueryServiceQueryExecutionCompletedWithErrors">
7047+
<source>Query execution completed with errors</source>
7048+
<target state="new">Query execution completed with errors</target>
7049+
<note></note>
7050+
</trans-unit>
70467051
</body>
70477052
</file>
70487053
</xliff>

src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,42 @@ internal async Task HandleSimpleExecuteRequest(SimpleExecuteParams executeParams
323323

324324
ResultOnlyContext<SimpleExecuteResult> newContext = new ResultOnlyContext<SimpleExecuteResult>(requestContext);
325325

326+
// Collect error messages during execution using thread-safe ordered collection
327+
var errorMessages = new System.Collections.Concurrent.ConcurrentQueue<string>();
328+
326329
// handle sending event back when the query completes
327330
Query.QueryAsyncEventHandler queryComplete = async query =>
328331
{
329332
try
330333
{
331-
// check to make sure any results were recieved
334+
// Check if the batch has errors (SQL errors that didn't throw exceptions)
335+
if (query.Batches.Length > 0 && query.Batches[0].HasError)
336+
{
337+
// If we collected error messages, send those
338+
if (errorMessages.Count > 0)
339+
{
340+
await requestContext.SendError(string.Join(Environment.NewLine, errorMessages));
341+
}
342+
else
343+
{
344+
// Fallback message if we somehow didn't collect messages
345+
await requestContext.SendError(SR.QueryServiceQueryExecutionCompletedWithErrors);
346+
}
347+
return;
348+
}
349+
350+
// check to make sure any results were received
332351
if (query.Batches.Length == 0
333352
|| query.Batches[0].ResultSets.Count == 0)
334353
{
335-
await requestContext.SendError(SR.QueryServiceResultSetHasNoResults);
354+
// No result sets - return empty result with no columns
355+
SimpleExecuteResult emptyResult = new SimpleExecuteResult
356+
{
357+
RowCount = 0,
358+
ColumnInfo = new DbColumnWrapper[0],
359+
Rows = new DbCellValue[0][]
360+
};
361+
await requestContext.SendResult(emptyResult);
336362
return;
337363
}
338364

@@ -388,7 +414,26 @@ internal async Task HandleSimpleExecuteRequest(SimpleExecuteParams executeParams
388414
await requestContext.SendError(e);
389415
};
390416

391-
await InterServiceExecuteQuery(executeStringParams, newConn, newContext, null, queryCreateFailureAction, queryComplete, queryFail);
417+
// Collect batch messages (errors) during query execution
418+
Batch.BatchAsyncMessageHandler messageHandler = async (message) =>
419+
{
420+
if (message.IsError)
421+
{
422+
errorMessages.Enqueue(message.Message);
423+
}
424+
};
425+
426+
// Execute query with message collection
427+
Query createdQuery = null;
428+
Func<Query, Task<bool>> queryCreateSuccess = async (q) =>
429+
{
430+
createdQuery = q;
431+
// Subscribe to batch messages to collect errors
432+
q.BatchMessageSent += messageHandler;
433+
return true;
434+
};
435+
436+
await InterServiceExecuteQuery(executeStringParams, newConn, newContext, queryCreateSuccess, queryCreateFailureAction, queryComplete, queryFail);
392437
});
393438

394439
ActiveSimpleExecuteRequests.TryAdd(randomUri, workTask);

test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/QueryExecutionServiceTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,83 @@ private async Task<ResultSetSubset> ExecuteAndVerifyQuery(string query, string o
165165

166166
return subsetResult.ResultSubset;
167167
}
168+
169+
/// <summary>
170+
/// Test that SimpleExecuteRequest collects and returns SQL error messages for queries with errors
171+
/// </summary>
172+
[Test]
173+
public async Task SimpleExecuteRequestReturnsErrorMessages()
174+
{
175+
// Establish a new connection
176+
ConnectionService.Instance.OwnerToConnectionMap.Clear();
177+
ConnectionInfo connectionInfo = LiveConnectionHelper.InitLiveConnectionInfo().ConnectionInfo;
178+
179+
var requestContext = new Mock<RequestContext<SimpleExecuteResult>>();
180+
ManualResetEvent errorEvent = new ManualResetEvent(false);
181+
string? errorMessage = null;
182+
183+
requestContext.Setup(x => x.SendError(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<string>()))
184+
.Callback<string, int, string>((error, code, data) =>
185+
{
186+
errorMessage = error;
187+
errorEvent.Set();
188+
})
189+
.Returns(Task.FromResult(new object()));
190+
191+
var executeParams = new SimpleExecuteParams
192+
{
193+
OwnerUri = connectionInfo.OwnerUri,
194+
QueryString = "SELECT * FROM NonExistentTable123456;"
195+
};
196+
197+
await QueryExecutionService.Instance.HandleSimpleExecuteRequest(executeParams, requestContext.Object);
198+
199+
// Wait for error to be sent
200+
bool gotError = errorEvent.WaitOne(TimeSpan.FromSeconds(10));
201+
Assert.IsTrue(gotError, "Expected error message was not received");
202+
Assert.IsNotNull(errorMessage, "Error message should not be null");
203+
Assert.IsTrue(errorMessage!.Contains("NonExistentTable123456") || errorMessage!.Contains("Invalid object name"),
204+
$"Error message should contain table name or invalid object error. Got: {errorMessage}");
205+
}
206+
207+
/// <summary>
208+
/// Test that SimpleExecuteRequest returns success with empty columns for queries with no result sets (UPDATE, DELETE, etc.)
209+
/// </summary>
210+
[Test]
211+
public async Task SimpleExecuteRequestSucceedsForNoResultSetQueries()
212+
{
213+
// Establish a new connection
214+
ConnectionService.Instance.OwnerToConnectionMap.Clear();
215+
ConnectionInfo connectionInfo = LiveConnectionHelper.InitLiveConnectionInfo().ConnectionInfo;
216+
217+
var requestContext = new Mock<RequestContext<SimpleExecuteResult>>();
218+
ManualResetEvent resultEvent = new ManualResetEvent(false);
219+
SimpleExecuteResult? result = null;
220+
221+
requestContext.Setup(x => x.SendResult(It.IsAny<SimpleExecuteResult>()))
222+
.Callback<SimpleExecuteResult>(r =>
223+
{
224+
result = r;
225+
resultEvent.Set();
226+
})
227+
.Returns(Task.FromResult(new object()));
228+
229+
// Use a query that doesn't return a result set - DECLARE creates a variable
230+
var executeParams = new SimpleExecuteParams
231+
{
232+
OwnerUri = connectionInfo.OwnerUri,
233+
QueryString = "DECLARE @TestVar INT; SET @TestVar = 1;"
234+
};
235+
236+
await QueryExecutionService.Instance.HandleSimpleExecuteRequest(executeParams, requestContext.Object);
237+
238+
// Wait for result to be sent
239+
bool gotResult = resultEvent.WaitOne(TimeSpan.FromSeconds(10));
240+
Assert.IsTrue(gotResult, "Expected result was not received");
241+
Assert.IsNotNull(result, "Result should not be null");
242+
Assert.AreEqual(0, result!.RowCount, "Row count should be 0 for queries without result sets");
243+
Assert.IsNotNull(result!.ColumnInfo, "Column info should not be null");
244+
Assert.AreEqual(0, result!.ColumnInfo.Length, "Column info should be empty for queries without result sets");
245+
}
168246
}
169247
}

0 commit comments

Comments
 (0)