Skip to content

Commit a200027

Browse files
tianzhouclaude
andcommitted
feat: return affected row count for INSERT/UPDATE/DELETE
- Add rowCount to all database connectors (PostgreSQL, MySQL, MariaDB, SQLite, SQL Server) - Add extractAffectedRows utility for MySQL/MariaDB result parsing - Update execute-sql and custom-tool-handler to use rowCount for count field - Frontend now displays "N rows affected" for write operations instead of "No results returned" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 4491222 commit a200027

File tree

12 files changed

+86
-35
lines changed

12 files changed

+86
-35
lines changed

frontend/src/api/tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export async function executeTool(
7575

7676
const rows = toolResult.data.rows;
7777
if (rows.length === 0) {
78-
return { columns: [], rows: [], rowCount: 0 };
78+
// For INSERT/UPDATE/DELETE, rows is empty but count reflects affected rows
79+
return { columns: [], rows: [], rowCount: toolResult.data.count };
7980
}
8081

8182
const columns = Object.keys(rows[0]);

frontend/src/components/tool/ResultsTable.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,15 @@ export function ResultsTable({ result, error, isLoading, executedSql, executionT
108108
);
109109
}
110110

111-
// No results
111+
// No rows returned - could be empty SELECT or successful INSERT/UPDATE/DELETE
112112
if (result.rows.length === 0) {
113113
return (
114114
<div className="border border-border rounded-lg bg-card p-8 text-center">
115-
<p className="text-muted-foreground text-sm">No results returned</p>
115+
<p className="text-muted-foreground text-sm">
116+
{result.rowCount > 0
117+
? `${result.rowCount} row${result.rowCount !== 1 ? 's' : ''} affected`
118+
: 'No results returned'}
119+
</p>
116120
</div>
117121
);
118122
}

src/connectors/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type ConnectorType = "postgres" | "mysql" | "mariadb" | "sqlite" | "sqlse
99
*/
1010
export interface SQLResult {
1111
rows: any[];
12-
[key: string]: any;
12+
rowCount: number;
1313
}
1414

1515
export interface TableColumn {

src/connectors/mariadb/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { SafeURL } from "../../utils/safe-url.js";
1515
import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js";
1616
import { SQLRowLimiter } from "../../utils/sql-row-limiter.js";
17-
import { parseQueryResults } from "../../utils/multi-statement-result-parser.js";
17+
import { parseQueryResults, extractAffectedRows } from "../../utils/multi-statement-result-parser.js";
1818

1919
/**
2020
* MariaDB DSN Parser
@@ -542,7 +542,8 @@ export class MariaDBConnector implements Connector {
542542

543543
// Parse results using shared utility that handles both single and multi-statement queries
544544
const rows = parseQueryResults(results);
545-
return { rows };
545+
const rowCount = extractAffectedRows(results);
546+
return { rows, rowCount };
546547
} catch (error) {
547548
console.error("Error executing query:", error);
548549
throw error;

src/connectors/mysql/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { SafeURL } from "../../utils/safe-url.js";
1515
import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js";
1616
import { SQLRowLimiter } from "../../utils/sql-row-limiter.js";
17-
import { parseQueryResults } from "../../utils/multi-statement-result-parser.js";
17+
import { parseQueryResults, extractAffectedRows } from "../../utils/multi-statement-result-parser.js";
1818

1919
/**
2020
* MySQL DSN Parser
@@ -553,7 +553,8 @@ export class MySQLConnector implements Connector {
553553

554554
// Parse results using shared utility that handles both single and multi-statement queries
555555
const rows = parseQueryResults(firstResult);
556-
return { rows };
556+
const rowCount = extractAffectedRows(firstResult);
557+
return { rows, rowCount };
557558
} catch (error) {
558559
console.error("Error executing query:", error);
559560
throw error;

src/connectors/postgres/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,17 +444,21 @@ export class PostgresConnector implements Connector {
444444
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
445445

446446
// Use parameters if provided
447+
let result;
447448
if (parameters && parameters.length > 0) {
448449
try {
449-
return await client.query(processedStatement, parameters);
450+
result = await client.query(processedStatement, parameters);
450451
} catch (error) {
451452
console.error(`[PostgreSQL executeSQL] ERROR: ${(error as Error).message}`);
452453
console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
453454
console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
454455
throw error;
455456
}
457+
} else {
458+
result = await client.query(processedStatement);
456459
}
457-
return await client.query(processedStatement);
460+
// Explicitly return rows and rowCount to ensure rowCount is preserved
461+
return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
458462
} else {
459463
// Multiple statements - parameters not supported for multi-statement queries
460464
if (parameters && parameters.length > 0) {
@@ -463,6 +467,7 @@ export class PostgresConnector implements Connector {
463467

464468
// Execute all in same session for transaction consistency
465469
let allRows: any[] = [];
470+
let totalRowCount = 0;
466471

467472
// Execute within a transaction to ensure session consistency
468473
await client.query('BEGIN');
@@ -476,14 +481,18 @@ export class PostgresConnector implements Connector {
476481
if (result.rows && result.rows.length > 0) {
477482
allRows.push(...result.rows);
478483
}
484+
// Accumulate rowCount for INSERT/UPDATE/DELETE statements
485+
if (result.rowCount) {
486+
totalRowCount += result.rowCount;
487+
}
479488
}
480489
await client.query('COMMIT');
481490
} catch (error) {
482491
await client.query('ROLLBACK');
483492
throw error;
484493
}
485494

486-
return { rows: allRows };
495+
return { rows: allRows, rowCount: totalRowCount };
487496
}
488497
} finally {
489498
client.release();

src/connectors/sqlite/index.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ export class SQLiteConnector implements Connector {
413413
if (parameters && parameters.length > 0) {
414414
try {
415415
const rows = this.db.prepare(processedStatement).all(...parameters);
416-
return { rows };
416+
return { rows, rowCount: rows.length };
417417
} catch (error) {
418418
console.error(`[SQLite executeSQL] ERROR: ${(error as Error).message}`);
419419
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
@@ -422,23 +422,24 @@ export class SQLiteConnector implements Connector {
422422
}
423423
} else {
424424
const rows = this.db.prepare(processedStatement).all();
425-
return { rows };
425+
return { rows, rowCount: rows.length };
426426
}
427427
} else {
428428
// Use run() for statements that don't return data
429+
let result;
429430
if (parameters && parameters.length > 0) {
430431
try {
431-
this.db.prepare(processedStatement).run(...parameters);
432+
result = this.db.prepare(processedStatement).run(...parameters);
432433
} catch (error) {
433434
console.error(`[SQLite executeSQL] ERROR: ${(error as Error).message}`);
434435
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
435436
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
436437
throw error;
437438
}
438439
} else {
439-
this.db.prepare(processedStatement).run();
440+
result = this.db.prepare(processedStatement).run();
440441
}
441-
return { rows: [] };
442+
return { rows: [], rowCount: result.changes };
442443
}
443444
} else {
444445
// Multiple statements - parameters not supported for multi-statement queries
@@ -469,9 +470,11 @@ export class SQLiteConnector implements Connector {
469470
}
470471
}
471472

472-
// Execute write statements using native .exec() for optimal performance
473-
if (writeStatements.length > 0) {
474-
this.db.exec(writeStatements.join('; '));
473+
// Execute write statements individually to track changes
474+
let totalChanges = 0;
475+
for (const statement of writeStatements) {
476+
const result = this.db.prepare(statement).run();
477+
totalChanges += result.changes;
475478
}
476479

477480
// Execute read statements individually to collect results
@@ -483,7 +486,8 @@ export class SQLiteConnector implements Connector {
483486
allRows.push(...result);
484487
}
485488

486-
return { rows: allRows };
489+
// rowCount is total changes for writes, plus rows returned for reads
490+
return { rows: allRows, rowCount: totalChanges + allRows.length };
487491
}
488492
} catch (error) {
489493
throw error;

src/connectors/sqlserver/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -556,12 +556,6 @@ export class SQLServerConnector implements Connector {
556556

557557
return {
558558
rows: result.recordset || [],
559-
fields:
560-
result.recordset && result.recordset.length > 0
561-
? Object.keys(result.recordset[0]).map((key) => ({
562-
name: key,
563-
}))
564-
: [],
565559
rowCount: result.rowsAffected[0] || 0,
566560
};
567561
} catch (error) {

src/tools/__tests__/execute-sql.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('execute-sql tool', () => {
5353

5454
describe('basic execution', () => {
5555
it('should execute SELECT and return rows', async () => {
56-
const mockResult: SQLResult = { rows: [{ id: 1, name: 'test' }] };
56+
const mockResult: SQLResult = { rows: [{ id: 1, name: 'test' }], rowCount: 1 };
5757
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
5858

5959
const handler = createExecuteSqlToolHandler('test_source');
@@ -67,7 +67,7 @@ describe('execute-sql tool', () => {
6767
});
6868

6969
it('should pass multi-statement SQL directly to connector', async () => {
70-
const mockResult: SQLResult = { rows: [{ id: 1 }] };
70+
const mockResult: SQLResult = { rows: [{ id: 1 }], rowCount: 1 };
7171
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
7272

7373
const sql = 'SELECT * FROM users; SELECT * FROM roles;';
@@ -102,7 +102,7 @@ describe('execute-sql tool', () => {
102102
});
103103

104104
it('should allow SELECT statements', async () => {
105-
const mockResult: SQLResult = { rows: [{ id: 1 }] };
105+
const mockResult: SQLResult = { rows: [{ id: 1 }], rowCount: 1 };
106106
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
107107

108108
const handler = createExecuteSqlToolHandler('test_source');
@@ -114,7 +114,7 @@ describe('execute-sql tool', () => {
114114
});
115115

116116
it('should allow multiple read-only statements', async () => {
117-
const mockResult: SQLResult = { rows: [] };
117+
const mockResult: SQLResult = { rows: [], rowCount: 0 };
118118
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
119119

120120
const sql = 'SELECT * FROM users; SELECT * FROM roles;';
@@ -173,7 +173,7 @@ describe('execute-sql tool', () => {
173173
mockGetToolRegistry.mockReturnValue({
174174
getBuiltinToolConfig: vi.fn().mockReturnValue(toolConfig),
175175
} as any);
176-
const mockResult: SQLResult = { rows: [] };
176+
const mockResult: SQLResult = { rows: [], rowCount: 0 };
177177
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
178178

179179
const handler = createExecuteSqlToolHandler('writable_source');
@@ -208,7 +208,7 @@ describe('execute-sql tool', () => {
208208
['inline comments', 'SELECT id, -- user id\n name FROM users'],
209209
['only comments', '-- Just a comment\n/* Another */'],
210210
])('should allow SELECT with %s', async (_, sql) => {
211-
const mockResult: SQLResult = { rows: [] };
211+
const mockResult: SQLResult = { rows: [], rowCount: 0 };
212212
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
213213

214214
const handler = createExecuteSqlToolHandler('test_source');
@@ -231,7 +231,7 @@ describe('execute-sql tool', () => {
231231
['empty string', ''],
232232
['only semicolons and whitespace', ' ; ; ; '],
233233
])('should handle %s', async (_, sql) => {
234-
const mockResult: SQLResult = { rows: [] };
234+
const mockResult: SQLResult = { rows: [], rowCount: 0 };
235235
vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
236236

237237
const handler = createExecuteSqlToolHandler('test_source');

src/tools/custom-tool-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export function createCustomToolHandler(toolConfig: ToolConfig) {
199199
// 7. Build response data
200200
const responseData = {
201201
rows: result.rows,
202-
count: result.rows.length,
202+
count: result.rowCount,
203203
source_id: toolConfig.source,
204204
};
205205

0 commit comments

Comments
 (0)