Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/connectors/__tests__/sqlserver.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,46 @@ describe('SQL Server Connector Integration Tests', () => {
expect(result.rows[0]).toHaveProperty('age_rank');
});

it('should capture PRINT output in messages', async () => {
const result = await sqlServerTest.connector.executeSQL(
"PRINT 'hello from sql server'; SELECT 1 as value;",
{}
);

expect(result.rows).toHaveLength(1);
expect(result.rows[0].value).toBe(1);
expect(result.messages).toBeDefined();
expect(result.messages!.length).toBeGreaterThan(0);
expect(result.messages).toContain('hello from sql server');
});

it('should capture SET STATISTICS TIME output in messages', async () => {
const result = await sqlServerTest.connector.executeSQL(
'SET STATISTICS TIME ON; SELECT COUNT(*) as cnt FROM users; SET STATISTICS TIME OFF;',
{}
);

expect(result.rows).toHaveLength(1);
expect(result.messages).toBeDefined();
expect(result.messages!.length).toBeGreaterThan(0);
// STATISTICS TIME emits messages containing "CPU time" and "elapsed time"
const hasTimingMessage = result.messages!.some(
msg => msg.includes('CPU time') || msg.includes('elapsed time')
);
expect(hasTimingMessage).toBe(true);
});

it('should not include messages field when no informational messages are emitted', async () => {
const result = await sqlServerTest.connector.executeSQL(
'SELECT 1 as value',
{}
);

expect(result.rows).toHaveLength(1);
// messages should be undefined (not present) when no info messages were emitted
expect(result.messages).toBeUndefined();
});

it('should ignore maxRows when not specified', async () => {
// Test without maxRows - should return all rows
const result = await sqlServerTest.connector.executeSQL(
Expand Down
2 changes: 2 additions & 0 deletions src/connectors/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type ConnectorType = "postgres" | "mysql" | "mariadb" | "sqlite" | "sqlse
export interface SQLResult {
rows: any[];
rowCount: number;
/** Informational messages from the database (e.g. SQL Server STATISTICS TIME/IO, PRINT output) */
messages?: string[];
}

export interface TableColumn {
Expand Down
8 changes: 7 additions & 1 deletion src/connectors/sqlserver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,13 @@ export class SQLServerConnector implements Connector {
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
}

// Create request and add parameters if provided
// Create request and collect informational messages (e.g. SET STATISTICS TIME/IO, PRINT)
const request = this.connection.request();
const messages: string[] = [];
request.on('info', (info: { message: string }) => {
messages.push(info.message);
});
Comment thread
esetnik marked this conversation as resolved.

if (parameters && parameters.length > 0) {
// SQL Server uses @p1, @p2, etc. for parameters
parameters.forEach((param, index) => {
Expand Down Expand Up @@ -625,6 +630,7 @@ export class SQLServerConnector implements Connector {
return {
rows: result.recordset || [],
rowCount: result.rowsAffected[0] || 0,
Comment on lines 630 to 632
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rowCount is derived from result.rowsAffected[0], which is incorrect for multi-statement batches (e.g. SET STATISTICS TIME ON; SELECT ...; SET STATISTICS TIME OFF; or PRINT ...; SELECT ...;). In those cases the first statement often affects 0 rows, so count will be reported as 0 even when recordset contains rows. Consider computing rowCount based on the returned recordset (for SELECT) and/or using the last entry in rowsAffected (or summing it) to reflect the statement(s) that actually produced the returned rows.

Suggested change
return {
rows: result.recordset || [],
rowCount: result.rowsAffected[0] || 0,
const rows = result.recordset || [];
const rowCount = rows.length > 0
? rows.length
: (result.rowsAffected && result.rowsAffected.length > 0
? result.rowsAffected[result.rowsAffected.length - 1] || 0
: 0);
return {
rows,
rowCount,

Copilot uses AI. Check for mistakes.
...(messages.length > 0 ? { messages } : {}),
};
} catch (error) {
throw new Error(`Failed to execute query: ${(error as Error).message}`);
Expand Down
1 change: 1 addition & 0 deletions src/tools/execute-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function createExecuteSqlToolHandler(sourceId?: string) {
rows: result.rows,
count: result.rowCount,
source_id: effectiveSourceId,
...(result.messages && result.messages.length > 0 ? { messages: result.messages } : {}),
};
Comment thread
esetnik marked this conversation as resolved.

return createToolSuccessResponse(responseData);
Expand Down
Loading