Skip to content

Commit 09f21c1

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 09f21c1

File tree

9 files changed

+80
-21
lines changed

9 files changed

+80
-21
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/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/tools/custom-tool-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,10 @@ export function createCustomToolHandler(toolConfig: ToolConfig) {
197197
);
198198

199199
// 7. Build response data
200+
// Use rowCount if available (for INSERT/UPDATE/DELETE), otherwise use rows.length
200201
const responseData = {
201202
rows: result.rows,
202-
count: result.rows.length,
203+
count: result.rowCount ?? result.rows.length,
203204
source_id: toolConfig.source,
204205
};
205206

src/tools/execute-sql.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ export function createExecuteSqlToolHandler(sourceId?: string) {
7676
result = await connector.executeSQL(sql, executeOptions);
7777

7878
// Build response data
79+
// Use rowCount if available (for INSERT/UPDATE/DELETE), otherwise use rows.length
7980
const responseData = {
8081
rows: result.rows,
81-
count: result.rows.length,
82+
count: result.rowCount ?? result.rows.length,
8283
source_id: effectiveSourceId,
8384
};
8485

src/utils/multi-statement-result-parser.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,43 @@ export function extractRowsFromMultiStatement(results: any): any[] {
6868
return allRows;
6969
}
7070

71+
/**
72+
* Extracts total affected rows from query results.
73+
*
74+
* For INSERT/UPDATE/DELETE operations, returns the sum of affectedRows.
75+
* For SELECT operations, returns the number of rows.
76+
*
77+
* @param results - Raw results from the database driver
78+
* @returns Total number of affected/returned rows
79+
*/
80+
export function extractAffectedRows(results: any): number {
81+
// Handle metadata object (single INSERT/UPDATE/DELETE)
82+
if (isMetadataObject(results)) {
83+
return results.affectedRows || 0;
84+
}
85+
86+
// Handle non-array results
87+
if (!Array.isArray(results)) {
88+
return 0;
89+
}
90+
91+
// Check if this is a multi-statement result
92+
if (isMultiStatementResult(results)) {
93+
let totalAffected = 0;
94+
for (const result of results) {
95+
if (isMetadataObject(result)) {
96+
totalAffected += result.affectedRows || 0;
97+
} else if (Array.isArray(result)) {
98+
totalAffected += result.length;
99+
}
100+
}
101+
return totalAffected;
102+
}
103+
104+
// Single statement result - results is the rows array directly
105+
return results.length;
106+
}
107+
71108
/**
72109
* Parses database query results, handling both single and multi-statement queries.
73110
*

0 commit comments

Comments
 (0)