diff --git a/frontend/src/api/tools.ts b/frontend/src/api/tools.ts index f62f0ba..1dafeda 100644 --- a/frontend/src/api/tools.ts +++ b/frontend/src/api/tools.ts @@ -75,7 +75,8 @@ export async function executeTool( const rows = toolResult.data.rows; if (rows.length === 0) { - return { columns: [], rows: [], rowCount: 0 }; + // For INSERT/UPDATE/DELETE, rows is empty but count reflects affected rows + return { columns: [], rows: [], rowCount: toolResult.data.count }; } const columns = Object.keys(rows[0]); diff --git a/frontend/src/components/tool/ResultsTable.tsx b/frontend/src/components/tool/ResultsTable.tsx index 40bdcbc..f9cabaa 100644 --- a/frontend/src/components/tool/ResultsTable.tsx +++ b/frontend/src/components/tool/ResultsTable.tsx @@ -108,11 +108,15 @@ export function ResultsTable({ result, error, isLoading, executedSql, executionT ); } - // No results + // No rows returned - could be empty SELECT or successful INSERT/UPDATE/DELETE if (result.rows.length === 0) { return (
-

No results returned

+

+ {result.rowCount > 0 + ? `${result.rowCount} row${result.rowCount !== 1 ? 's' : ''} affected` + : 'No results returned'} +

); } diff --git a/src/connectors/interface.ts b/src/connectors/interface.ts index 6b72c1c..67913b3 100644 --- a/src/connectors/interface.ts +++ b/src/connectors/interface.ts @@ -9,7 +9,7 @@ export type ConnectorType = "postgres" | "mysql" | "mariadb" | "sqlite" | "sqlse */ export interface SQLResult { rows: any[]; - [key: string]: any; + rowCount: number; } export interface TableColumn { diff --git a/src/connectors/mariadb/index.ts b/src/connectors/mariadb/index.ts index fc73f53..27a5b6b 100644 --- a/src/connectors/mariadb/index.ts +++ b/src/connectors/mariadb/index.ts @@ -14,7 +14,7 @@ import { import { SafeURL } from "../../utils/safe-url.js"; import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js"; import { SQLRowLimiter } from "../../utils/sql-row-limiter.js"; -import { parseQueryResults } from "../../utils/multi-statement-result-parser.js"; +import { parseQueryResults, extractAffectedRows } from "../../utils/multi-statement-result-parser.js"; /** * MariaDB DSN Parser @@ -542,7 +542,8 @@ export class MariaDBConnector implements Connector { // Parse results using shared utility that handles both single and multi-statement queries const rows = parseQueryResults(results); - return { rows }; + const rowCount = extractAffectedRows(results); + return { rows, rowCount }; } catch (error) { console.error("Error executing query:", error); throw error; diff --git a/src/connectors/mysql/index.ts b/src/connectors/mysql/index.ts index 58ba7cc..a7ec8b2 100644 --- a/src/connectors/mysql/index.ts +++ b/src/connectors/mysql/index.ts @@ -14,7 +14,7 @@ import { import { SafeURL } from "../../utils/safe-url.js"; import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js"; import { SQLRowLimiter } from "../../utils/sql-row-limiter.js"; -import { parseQueryResults } from "../../utils/multi-statement-result-parser.js"; +import { parseQueryResults, extractAffectedRows } from "../../utils/multi-statement-result-parser.js"; /** * MySQL DSN Parser @@ -553,7 +553,8 @@ export class MySQLConnector implements Connector { // Parse results using shared utility that handles both single and multi-statement queries const rows = parseQueryResults(firstResult); - return { rows }; + const rowCount = extractAffectedRows(firstResult); + return { rows, rowCount }; } catch (error) { console.error("Error executing query:", error); throw error; diff --git a/src/connectors/postgres/index.ts b/src/connectors/postgres/index.ts index 09b0bc2..bfe0242 100644 --- a/src/connectors/postgres/index.ts +++ b/src/connectors/postgres/index.ts @@ -444,17 +444,21 @@ export class PostgresConnector implements Connector { const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows); // Use parameters if provided + let result; if (parameters && parameters.length > 0) { try { - return await client.query(processedStatement, parameters); + result = await client.query(processedStatement, parameters); } catch (error) { console.error(`[PostgreSQL executeSQL] ERROR: ${(error as Error).message}`); console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`); console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`); throw error; } + } else { + result = await client.query(processedStatement); } - return await client.query(processedStatement); + // Explicitly return rows and rowCount to ensure rowCount is preserved + return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length }; } else { // Multiple statements - parameters not supported for multi-statement queries if (parameters && parameters.length > 0) { @@ -463,6 +467,7 @@ export class PostgresConnector implements Connector { // Execute all in same session for transaction consistency let allRows: any[] = []; + let totalRowCount = 0; // Execute within a transaction to ensure session consistency await client.query('BEGIN'); @@ -476,6 +481,10 @@ export class PostgresConnector implements Connector { if (result.rows && result.rows.length > 0) { allRows.push(...result.rows); } + // Accumulate rowCount for INSERT/UPDATE/DELETE statements + if (result.rowCount) { + totalRowCount += result.rowCount; + } } await client.query('COMMIT'); } catch (error) { @@ -483,7 +492,7 @@ export class PostgresConnector implements Connector { throw error; } - return { rows: allRows }; + return { rows: allRows, rowCount: totalRowCount }; } } finally { client.release(); diff --git a/src/connectors/sqlite/index.ts b/src/connectors/sqlite/index.ts index d268bcd..ae2a6ab 100644 --- a/src/connectors/sqlite/index.ts +++ b/src/connectors/sqlite/index.ts @@ -413,7 +413,7 @@ export class SQLiteConnector implements Connector { if (parameters && parameters.length > 0) { try { const rows = this.db.prepare(processedStatement).all(...parameters); - return { rows }; + return { rows, rowCount: rows.length }; } catch (error) { console.error(`[SQLite executeSQL] ERROR: ${(error as Error).message}`); console.error(`[SQLite executeSQL] SQL: ${processedStatement}`); @@ -422,13 +422,14 @@ export class SQLiteConnector implements Connector { } } else { const rows = this.db.prepare(processedStatement).all(); - return { rows }; + return { rows, rowCount: rows.length }; } } else { // Use run() for statements that don't return data + let result; if (parameters && parameters.length > 0) { try { - this.db.prepare(processedStatement).run(...parameters); + result = this.db.prepare(processedStatement).run(...parameters); } catch (error) { console.error(`[SQLite executeSQL] ERROR: ${(error as Error).message}`); console.error(`[SQLite executeSQL] SQL: ${processedStatement}`); @@ -436,9 +437,9 @@ export class SQLiteConnector implements Connector { throw error; } } else { - this.db.prepare(processedStatement).run(); + result = this.db.prepare(processedStatement).run(); } - return { rows: [] }; + return { rows: [], rowCount: result.changes }; } } else { // Multiple statements - parameters not supported for multi-statement queries @@ -469,9 +470,11 @@ export class SQLiteConnector implements Connector { } } - // Execute write statements using native .exec() for optimal performance - if (writeStatements.length > 0) { - this.db.exec(writeStatements.join('; ')); + // Execute write statements individually to track changes + let totalChanges = 0; + for (const statement of writeStatements) { + const result = this.db.prepare(statement).run(); + totalChanges += result.changes; } // Execute read statements individually to collect results @@ -483,7 +486,8 @@ export class SQLiteConnector implements Connector { allRows.push(...result); } - return { rows: allRows }; + // rowCount is total changes for writes, plus rows returned for reads + return { rows: allRows, rowCount: totalChanges + allRows.length }; } } catch (error) { throw error; diff --git a/src/connectors/sqlserver/index.ts b/src/connectors/sqlserver/index.ts index fe569d3..c72793d 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -556,12 +556,6 @@ export class SQLServerConnector implements Connector { return { rows: result.recordset || [], - fields: - result.recordset && result.recordset.length > 0 - ? Object.keys(result.recordset[0]).map((key) => ({ - name: key, - })) - : [], rowCount: result.rowsAffected[0] || 0, }; } catch (error) { diff --git a/src/tools/__tests__/execute-sql.test.ts b/src/tools/__tests__/execute-sql.test.ts index f467a6d..68fb89a 100644 --- a/src/tools/__tests__/execute-sql.test.ts +++ b/src/tools/__tests__/execute-sql.test.ts @@ -53,7 +53,7 @@ describe('execute-sql tool', () => { describe('basic execution', () => { it('should execute SELECT and return rows', async () => { - const mockResult: SQLResult = { rows: [{ id: 1, name: 'test' }] }; + const mockResult: SQLResult = { rows: [{ id: 1, name: 'test' }], rowCount: 1 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const handler = createExecuteSqlToolHandler('test_source'); @@ -67,7 +67,7 @@ describe('execute-sql tool', () => { }); it('should pass multi-statement SQL directly to connector', async () => { - const mockResult: SQLResult = { rows: [{ id: 1 }] }; + const mockResult: SQLResult = { rows: [{ id: 1 }], rowCount: 1 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const sql = 'SELECT * FROM users; SELECT * FROM roles;'; @@ -102,7 +102,7 @@ describe('execute-sql tool', () => { }); it('should allow SELECT statements', async () => { - const mockResult: SQLResult = { rows: [{ id: 1 }] }; + const mockResult: SQLResult = { rows: [{ id: 1 }], rowCount: 1 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const handler = createExecuteSqlToolHandler('test_source'); @@ -114,7 +114,7 @@ describe('execute-sql tool', () => { }); it('should allow multiple read-only statements', async () => { - const mockResult: SQLResult = { rows: [] }; + const mockResult: SQLResult = { rows: [], rowCount: 0 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const sql = 'SELECT * FROM users; SELECT * FROM roles;'; @@ -173,7 +173,7 @@ describe('execute-sql tool', () => { mockGetToolRegistry.mockReturnValue({ getBuiltinToolConfig: vi.fn().mockReturnValue(toolConfig), } as any); - const mockResult: SQLResult = { rows: [] }; + const mockResult: SQLResult = { rows: [], rowCount: 0 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const handler = createExecuteSqlToolHandler('writable_source'); @@ -208,7 +208,7 @@ describe('execute-sql tool', () => { ['inline comments', 'SELECT id, -- user id\n name FROM users'], ['only comments', '-- Just a comment\n/* Another */'], ])('should allow SELECT with %s', async (_, sql) => { - const mockResult: SQLResult = { rows: [] }; + const mockResult: SQLResult = { rows: [], rowCount: 0 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const handler = createExecuteSqlToolHandler('test_source'); @@ -231,7 +231,7 @@ describe('execute-sql tool', () => { ['empty string', ''], ['only semicolons and whitespace', ' ; ; ; '], ])('should handle %s', async (_, sql) => { - const mockResult: SQLResult = { rows: [] }; + const mockResult: SQLResult = { rows: [], rowCount: 0 }; vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult); const handler = createExecuteSqlToolHandler('test_source'); diff --git a/src/tools/custom-tool-handler.ts b/src/tools/custom-tool-handler.ts index e4ae344..ccc5ab4 100644 --- a/src/tools/custom-tool-handler.ts +++ b/src/tools/custom-tool-handler.ts @@ -199,7 +199,7 @@ export function createCustomToolHandler(toolConfig: ToolConfig) { // 7. Build response data const responseData = { rows: result.rows, - count: result.rows.length, + count: result.rowCount, source_id: toolConfig.source, }; diff --git a/src/tools/execute-sql.ts b/src/tools/execute-sql.ts index 6fc3df3..d00d2fc 100644 --- a/src/tools/execute-sql.ts +++ b/src/tools/execute-sql.ts @@ -78,7 +78,7 @@ export function createExecuteSqlToolHandler(sourceId?: string) { // Build response data const responseData = { rows: result.rows, - count: result.rows.length, + count: result.rowCount, source_id: effectiveSourceId, }; diff --git a/src/utils/multi-statement-result-parser.ts b/src/utils/multi-statement-result-parser.ts index 6d24cc7..1734ec4 100644 --- a/src/utils/multi-statement-result-parser.ts +++ b/src/utils/multi-statement-result-parser.ts @@ -68,6 +68,43 @@ export function extractRowsFromMultiStatement(results: any): any[] { return allRows; } +/** + * Extracts total affected rows from query results. + * + * For INSERT/UPDATE/DELETE operations, returns the sum of affectedRows. + * For SELECT operations, returns the number of rows. + * + * @param results - Raw results from the database driver + * @returns Total number of affected/returned rows + */ +export function extractAffectedRows(results: any): number { + // Handle metadata object (single INSERT/UPDATE/DELETE) + if (isMetadataObject(results)) { + return results.affectedRows || 0; + } + + // Handle non-array results + if (!Array.isArray(results)) { + return 0; + } + + // Check if this is a multi-statement result + if (isMultiStatementResult(results)) { + let totalAffected = 0; + for (const result of results) { + if (isMetadataObject(result)) { + totalAffected += result.affectedRows || 0; + } else if (Array.isArray(result)) { + totalAffected += result.length; + } + } + return totalAffected; + } + + // Single statement result - results is the rows array directly + return results.length; +} + /** * Parses database query results, handling both single and multi-statement queries. *