Skip to content

fix: resolve parser cache collision with dual typeCast connections #3644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 26, 2025

Conversation

MOHIT51196
Copy link
Contributor

Problem

There was a critical bug in the parser cache system (lib/parsers/parser_cache.js) that caused incorrect type casting when multiple connections used different typeCast configurations simultaneously.

The parser cache generates cache keys based on connection options to reuse parsers for performance. However, the cache key generation for typeCast had a flaw that created ambiguous cache keys.

Issue: When multiple connections had different typeCast boolean values (true vs false), both would generate the same cache key, causing:

  1. Parser Cache Collision: Connection A with typeCast: true would cache a parser
  2. Wrong Parser Reused: Connection B with typeCast: false would find the same cache key and reuse the wrong parser
  3. Incorrect Results: Connection B would get typed results instead of raw Buffers

Real-World Impact

This bug manifested when:

  • Multiple connections existed with different typeCast boolean settings (true vs false)
  • Same queries were executed concurrently or in sequence
  • The same query structure was used across connections

Result: Connections with typeCast: false would incorrectly get typed data instead of raw Buffers, breaking applications that expected raw Buffer data.

Root Cause Analysis

The issue was in the keyFromFields function in lib/parsers/parser_cache.js. The original cache key generation approach created ambiguous keys:

  • Function-based typeCast: Generated unique identifier ✅
  • typeCast: true: Generated generic type identifier ❌
  • typeCast: false: Generated same generic type identifier ❌

This caused cache collisions between typeCast: true and typeCast: false connections because both boolean values were treated identically in the cache key generation process.

Solution

Improved Cache Key Generation: Enhanced the cache key generation to create unique identifiers (if typeCast is boolean) for each distinct typeCast value, eliminating ambiguous cache hits.

Key Improvements

  1. Value-Specific Keys: Each distinct typeCast value (boolean) now generates its own unique cache key and no longer collide in the cache
  2. Backward Compatibility: Function-based typeCast continues to work exactly as before
  3. Deterministic Behavior: Same typeCast value always generates the same cache key

The enhanced approach ensures that:

  • typeCast: true gets its own unique cache key
  • typeCast: false gets a different unique cache key
  • Function typeCast maintains its existing behavior
  • No ambiguity between different boolean values

Test Coverage Added

Created test-typecast-dual-connections.test.cjs to validate the fix and prevent regressions:

The test creates two connections with different typeCast configurations and validates that each connection gets the correct data type format, ensuring no cross-connection parser contamination.

Breaking Changes

None. This fix maintains backward compatibility while resolving the cache collision bug.

Checklist

  • ✅ Fixes critical parser cache collision bug
  • ✅ Maintains backward compatibility
  • ✅ Adds comprehensive test coverage
  • ✅ No performance impact
  • ✅ Handles all typeCast value types properly

@MOHIT51196
Copy link
Contributor Author

How to reproduce the error

You can use the following code snippet to reproduce the error

import mysql from 'mysql2/promise';

// Pool 1 - typeCast: true
const pool1 = mysql.createPool({
    host: '<database 1 host>',
    port: 3306,
    user: '<database 1 user>',
    password: '<database 1 password>',
    database: '<database 1 name>',
    connectionLimit: 3,
    typeCast: true
});

// Pool 2 - typeCast: false
const pool2 = mysql.createPool({
    host: '<database 2 host>',
    port: 3306,
    user: '<database 2 user>',
    password: '<database 2 password>',
    database: '<database 2 name>',
    connectionLimit: 5,
    typeCast: false
});

// Query for pool 1
function runQueryUser1() {
    console.log('Creating connection User1');
    return pool1.getConnection()
        .then(connection => {
            const queryString = 'SELECT 1;';
            return connection.query(queryString)
                .then(([rows]) => {
                    console.log(`User1 (typeCast: ${connection.config.typeCast} ) Result:`, JSON.stringify(rows));
                })
                .catch(error => {
                    console.error('User1 Query Error:', error);
                })
                .finally(() => {
                    console.log('Releasing connection');
                    connection.release();
                });
        })
        .catch(error => {
            console.error('User1 Connection Error:', error);
        });
}

// Query for pool 2
function runQueryUser2() {
    console.log('Creating connection User2');
    return pool2.getConnection()
        .then(connection => {
            const queryString = 'SELECT 1;';
            return connection.query(queryString)
                .then(([rows]) => {
                    console.log(`User2 (typeCast: ${connection.config.typeCast} ) Result:`, JSON.stringify(rows));
                })
                .catch(error => {
                    console.error('User2 Query Error:', error);
                })
                .finally(() => {
                    console.log('Releasing connection');
                    connection.release();
                });
        })
        .catch(error => {
            console.error('User2 Connection Error:', error);
        });
}

// Run queries with proper error handling
async function runQueries() {
    await runQueryUser1();
    await runQueryUser2();
    process.exit(0);
}

// Run the queries
runQueries();

@wellwelwel
Copy link
Collaborator

wellwelwel commented Jun 16, 2025

Hi, @MOHIT51196! Thanks for the issue + PR 🙋🏻‍♂️

We have a test to check the parser cache key serialization in test/esm/unit/parsers/cache-key-serialization.test.mjs, where it try the typeCast against true, undefined and function:



With your implementation, it would be useful to pick one of the 6 tests that repeats the value as true and change it to false to test how it works for each variation, then update the assertion expected values.

Your reproduction example is a great case to validate the behavior, but in practice I think it's more practical to use the existing test. What do you think?

@MOHIT51196
Copy link
Contributor Author

MOHIT51196 commented Jun 27, 2025

Hi, @MOHIT51196! Thanks for the issue + PR 🙋🏻‍♂️

We have a test to check the parser cache key serialization in test/esm/unit/parsers/cache-key-serialization.test.mjs, where it try the typeCast against true, undefined and function:

With your implementation, it would be useful to pick one of the 6 tests that repeats the value as true and change it to false to test how it works for each variation, then update the assertion expected values.

Your reproduction example is a great case to validate the behavior, but in practice I think it's more practical to use the existing test. What do you think?

Updating the existing test cases will keep them updated with the change, hence its necessary but in my opinion new test file is also necessary to verify the flow and also better for hardening tests.
I have updated existing tests in lib/parsers/parser_cache.js. Please verify @wellwelwel

@wellwelwel
Copy link
Collaborator

wellwelwel commented Jul 1, 2025

Please verify @wellwelwel

@MOHIT51196, I'll also reply to your private message here, because I believe it applies to the entire community of contributors and users of the project.

My doubt at the moment is whether this problem only applies to true or false values. Otherwise, it would be interesting to have a dedicated Issue for these cases, for example when having multiple typeCast with different functions that will result in the same cache.

  • This PR combines an Issue + PR, but Issues can be closed and reopened if necessary, the same Issue can have multiple PRs assigned to it, as well as being more practical to discuss and control duplicate reports.

Note

Regarding giving priority to issues or reviews, I'd like to point out that our work is entirely voluntary 🤝

About tests

The point of inserting a test that relies on process.on('exit', ...) and process.on('SIGINT', ...), then forcing a process.exit(0) in the end of each process state, leads to concerns about the test effectiveness.

On the other hand, the current tests seem to cover the scenarios adequately: the significant change is that instead of boolean, the cache will be generated with the boolean value itself (true or false), when used.

  • This reinforces the idea of keeping and adapting the current tests, changing one of the repeated boolean cases as true to false, ensuring that the expected value of the cache key is different for each Boolean state.

About the changes

A particular concern would be the flexibility that the current change brings by allowing any value for typeCast in the cache when it's not a function, but this can be easily addressed, for example:

typeof options.typeCast === 'boolean' ? options.typeCast : typeof options.typeCast
  • This would also help prevent undefined from being assigned as null due to JSON.stringify.

Please, feel free to share your thoughts and ideas 🙋🏻‍♂️

@MOHIT51196
Copy link
Contributor Author

MOHIT51196 commented Jul 23, 2025

Please verify @wellwelwel

@MOHIT51196, I'll also reply to your private message here, because I believe it applies to the entire community of contributors and users of the project.

My doubt at the moment is whether this problem only applies to true or false values. Otherwise, it would be interesting to have a dedicated Issue for these cases, for example when having multiple typeCast with different functions that will result in the same cache.

  • This PR combines an Issue + PR, but Issues can be closed and reopened if necessary, the same Issue can have multiple PRs assigned to it, as well as being more practical to discuss and control duplicate reports.

Note

Regarding giving priority to issues or reviews, I'd like to point out that our work is entirely voluntary 🤝

About tests

The point of inserting a test that relies on process.on('exit', ...) and process.on('SIGINT', ...), then forcing a process.exit(0) in the end of each process state, leads to concerns about the test effectiveness.

On the other hand, the current tests seem to cover the scenarios adequately: the significant change is that instead of boolean, the cache will be generated with the boolean value itself (true or false), when used.

  • This reinforces the idea of keeping and adapting the current tests, changing one of the repeated boolean cases as true to false, ensuring that the expected value of the cache key is different for each Boolean state.

About the changes

A particular concern would be the flexibility that the current change brings by allowing any value for typeCast in the cache when it's not a function, but this can be easily addressed, for example:

typeof options.typeCast === 'boolean' ? options.typeCast : typeof options.typeCast
  • This would also help prevent undefined from being assigned as null due to JSON.stringify.

Please, feel free to share your thoughts and ideas 🙋🏻‍♂️

I understand your concern @wellwelwel and hence made some updates to the PR to tackle them. Also I have removed the testcases as existing once seems enough

@wellwelwel
Copy link
Collaborator

Thanks, @MOHIT51196!

as existing once seems enough

What I meant was that we can pick one of the tests with true and change it to false to ensure both the cases 🙋🏻‍♂️

About the assertion errors, it's due to some "undefined" as null and to fix the lint error, just run npm run lint:fix 🤝

The parser cache system had a bug where connections with different
typeCast boolean values (true vs false) would share cached parsers,
causing incorrect data type conversion.

Update `parser_cache.js` with following changes :
- Enhanced cache key generation to create unique keys for each typeCast value
- Now typeCast: true and typeCast: false generate distinct cache keys
- Maintains function-based typeCast compatibility
- Ensures proper cache isolation and eliminates ambiguous cache hits

Add test to validate changes in `test-typecast-dual-connections.test.cjs`
with following changes :
- Added test-typecast-dual-connections.test.cjs to validate the fix
- Tests concurrent connections with different typeCast boolean settings
- Prevents future regressions in parser cache logic

Signed-off-by: Mohit Malhotra <[email protected]>
… match parser cache behavior

Update test expectations for `typeCast` field to reflect actual implementation
where `typeCast` returns the value directly instead of the type.
This aligns tests with the `lib/parsers/parser_cache.js` key generation logic.

Add more robust check for generating key for parser caching for the field `tyepCast`
to ensure value is used for only boolean input. Also removed `test-typecast-dual-connections.test.cjs`
as existing testcases are sufficient.

Signed-off-by: Mohit Malhotra <[email protected]>
@wellwelwel wellwelwel merged commit ce2ad75 into sidorares:master Jul 26, 2025
98 checks passed
@wellwelwel
Copy link
Collaborator

Thanks again, @MOHIT51196! These changes are already available in mysql2@canary and, next week, also in the main version 🙋🏻‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants