Skip to content
Merged
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
19 changes: 7 additions & 12 deletions src/connectors/__tests__/mariadb.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,20 +222,15 @@ describe('MariaDB Connector Integration Tests', () => {
});

it('should handle MariaDB auto-increment properly', async () => {
const insertResult = await mariadbTest.connector.executeSQL(
"INSERT INTO users (name, email, age) VALUES ('Auto Inc Test', 'autoinc@example.com', 40)",
{}
);

expect(insertResult).toBeDefined();

const selectResult = await mariadbTest.connector.executeSQL(
'SELECT LAST_INSERT_ID() as last_id',
// Execute INSERT and SELECT LAST_INSERT_ID() in a single call to ensure same connection
const result = await mariadbTest.connector.executeSQL(
"INSERT INTO users (name, email, age) VALUES ('Auto Inc Test', 'autoinc@example.com', 40); SELECT LAST_INSERT_ID() as last_id",
{}
);

expect(selectResult.rows).toHaveLength(1);
expect(Number(selectResult.rows[0].last_id)).toBeGreaterThan(0);

expect(result).toBeDefined();
expect(result.rows).toHaveLength(1);
expect(Number(result.rows[0].last_id)).toBeGreaterThan(0);
});

it('should work with MariaDB-specific functions', async () => {
Expand Down
19 changes: 7 additions & 12 deletions src/connectors/__tests__/mysql.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,20 +213,15 @@ describe('MySQL Connector Integration Tests', () => {
});

it('should handle MySQL auto-increment properly', async () => {
const insertResult = await mysqlTest.connector.executeSQL(
"INSERT INTO users (name, email, age) VALUES ('Auto Inc Test', 'autoinc@example.com', 40)",
{}
);

expect(insertResult).toBeDefined();

const selectResult = await mysqlTest.connector.executeSQL(
'SELECT LAST_INSERT_ID() as last_id',
// Execute INSERT and SELECT LAST_INSERT_ID() in a single call to ensure same connection
const result = await mysqlTest.connector.executeSQL(
"INSERT INTO users (name, email, age) VALUES ('Auto Inc Test', 'autoinc@example.com', 40); SELECT LAST_INSERT_ID() as last_id",
{}
);

expect(selectResult.rows).toHaveLength(1);
expect(Number(selectResult.rows[0].last_id)).toBeGreaterThan(0);

expect(result).toBeDefined();
expect(result.rows).toHaveLength(1);
expect(Number(result.rows[0].last_id)).toBeGreaterThan(0);
});

it('should work with MySQL-specific functions', async () => {
Expand Down
48 changes: 16 additions & 32 deletions src/connectors/mariadb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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";

/**
* MariaDB DSN Parser
Expand Down Expand Up @@ -474,6 +475,9 @@ export class MariaDBConnector implements Connector {
throw new Error("Not connected to database");
}

// Get a dedicated connection from the pool to ensure session consistency
// This is critical for session-specific features like LAST_INSERT_ID()
const conn = await this.pool.getConnection();
try {
// Apply maxRows limit to SELECT queries if specified
let processedSQL = sql;
Expand All @@ -482,49 +486,29 @@ export class MariaDBConnector implements Connector {
const statements = sql.split(';')
.map(statement => statement.trim())
.filter(statement => statement.length > 0);
const processedStatements = statements.map(statement =>

const processedStatements = statements.map(statement =>
SQLRowLimiter.applyMaxRows(statement, options.maxRows)
);

processedSQL = processedStatements.join('; ');
if (sql.trim().endsWith(';')) {
processedSQL += ';';
}
}

// Use pool.query - MariaDB driver returns rows directly for single statements
const results = await this.pool.query(processedSQL) as any;

// MariaDB driver returns different formats:
// - Single statement: returns rows array directly
// - Multiple statements: returns array of results (when multipleStatements is true)

if (Array.isArray(results)) {
// Check if this looks like multiple statement results
// Multiple statements return an array where each element might be an array of results
if (results.length > 0 && Array.isArray(results[0]) && results[0].length > 0) {
// This might be multiple statement results - flatten them
let allRows: any[] = [];

for (const result of results) {
if (Array.isArray(result)) {
allRows.push(...result);
}
}

return { rows: allRows };
} else {
// Single statement result - results is the rows array
return { rows: results };
}
} else {
// Non-array result (like for INSERT/UPDATE/DELETE without RETURNING)
return { rows: [] };
}
// Use dedicated connection - MariaDB driver returns rows directly for single statements
const results = await conn.query(processedSQL) as any;

// Parse results using shared utility that handles both single and multi-statement queries
const rows = parseQueryResults(results);
return { rows };
} catch (error) {
console.error("Error executing query:", error);
throw error;
} finally {
// Always release the connection back to the pool
conn.release();
}
}
}
Expand Down
51 changes: 19 additions & 32 deletions src/connectors/mysql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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";

/**
* MySQL DSN Parser
Expand Down Expand Up @@ -471,6 +472,9 @@ export class MySQLConnector implements Connector {
throw new Error("Not connected to database");
}

// Get a dedicated connection from the pool to ensure session consistency
// This is critical for session-specific features like LAST_INSERT_ID()
const conn = await this.pool.getConnection();
try {
// Apply maxRows limit to SELECT queries if specified
let processedSQL = sql;
Expand All @@ -479,50 +483,33 @@ export class MySQLConnector implements Connector {
const statements = sql.split(';')
.map(statement => statement.trim())
.filter(statement => statement.length > 0);
const processedStatements = statements.map(statement =>

const processedStatements = statements.map(statement =>
SQLRowLimiter.applyMaxRows(statement, options.maxRows)
);

processedSQL = processedStatements.join('; ');
if (sql.trim().endsWith(';')) {
processedSQL += ';';
}
}

// Use pool.query with multipleStatements: true support
const results = await this.pool.query(processedSQL) as any;

// MySQL2 with multipleStatements returns:
// - Single statement: [rows, fields]
// - Multiple statements: [array_of_results, fields] where array_of_results contains [rows, fields] for each statement

// Use dedicated connection with multipleStatements: true support
const results = await conn.query(processedSQL) as any;

// MySQL2 returns results in format [rows, fields]
// Extract the first element which contains the actual row data
const [firstResult] = results;

// Check if this is a multi-statement result
if (Array.isArray(firstResult) && firstResult.length > 0 &&
Array.isArray(firstResult[0])) {
// Multiple statements - firstResult is an array of results
let allRows: any[] = [];

for (const result of firstResult) {
// Each result is either a ResultSetHeader object (for INSERT/UPDATE/DELETE)
// or an array of rows (for SELECT)
if (Array.isArray(result)) {
// This is a rows array from a SELECT query
allRows.push(...result);
}
// Skip non-array results (ResultSetHeader objects)
}

return { rows: allRows };
} else {
// Single statement - firstResult is the rows array directly
return { rows: Array.isArray(firstResult) ? firstResult : [] };
}

// Parse results using shared utility that handles both single and multi-statement queries
const rows = parseQueryResults(firstResult);
return { rows };
} catch (error) {
console.error("Error executing query:", error);
throw error;
} finally {
// Always release the connection back to the pool
conn.release();
}
}
}
Expand Down
93 changes: 93 additions & 0 deletions src/utils/multi-statement-result-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Multi-Statement Result Parser Utility
*
* Provides shared logic for parsing multi-statement SQL execution results
* from different database drivers (MariaDB, MySQL2) that have similar but
* slightly different result formats.
*/

/**
* Checks if an element is a metadata object from INSERT/UPDATE/DELETE operations
* rather than a row array from SELECT queries.
*
* Different drivers use different property names for metadata:
* - MariaDB: affectedRows, warningStatus, insertId
* - MySQL2: affectedRows, insertId, fieldCount, ResultSetHeader type
*/
function isMetadataObject(element: any): boolean {
if (!element || typeof element !== 'object' || Array.isArray(element)) {
return false;
}

// Check for common metadata properties that indicate this is not a row array
return 'affectedRows' in element ||
'insertId' in element ||
'fieldCount' in element ||
'warningStatus' in element;
}

/**
* Checks if results appear to be from a multi-statement query.
*
* Multi-statement results are arrays containing mixed types:
* - Metadata objects (from INSERT/UPDATE/DELETE)
* - Arrays of rows (from SELECT queries)
*/
function isMultiStatementResult(results: any): boolean {
if (!Array.isArray(results) || results.length === 0) {
return false;
}

const firstElement = results[0];

// If first element is metadata or an array, it's a multi-statement result
return isMetadataObject(firstElement) || Array.isArray(firstElement);
}

/**
* Extracts row arrays from multi-statement results, filtering out metadata objects.
*
* @param results - The raw results from a multi-statement query
* @returns Array containing only the rows from SELECT queries
*/
export function extractRowsFromMultiStatement(results: any): any[] {
if (!Array.isArray(results)) {
return [];
}

const allRows: any[] = [];

for (const result of results) {
if (Array.isArray(result)) {
// This is a row array from a SELECT query - add all rows
allRows.push(...result);
}
// Skip metadata objects from INSERT/UPDATE/DELETE
}

return allRows;
}

/**
* Parses database query results, handling both single and multi-statement queries.
*
* This function unifies the result parsing logic for MariaDB and MySQL2 drivers,
* which have similar but slightly different result formats.
*
* @param results - Raw results from the database driver
* @returns Array of row objects from SELECT queries
*/
export function parseQueryResults(results: any): any[] {
// Handle non-array results (e.g., from INSERT/UPDATE/DELETE without RETURNING)
if (!Array.isArray(results)) {
return [];
}

// Check if this is a multi-statement result
if (isMultiStatementResult(results)) {
return extractRowsFromMultiStatement(results);
}

// Single statement result - results is the rows array directly
return results;
}