Skip to content

Conversation

@dlhck
Copy link
Collaborator

@dlhck dlhck commented Sep 19, 2025

Summary

  • Fixes JSON parsing issue when using mysql2 client library
  • mysql2 returns JSON values already parsed, causing JSON.parse to fail on non-JSON strings
  • Added detection for mysql2 vs mysql client to handle JSON parsing appropriately

Testing

It locally tested it by manually removing node_modules/mysql to force TypeORM to use the mysql2 driver. After that I set the isUsingMysql2 to always return false, basically tricking the code into thinking it uses the mysql library, but of course it was using mysql2. This made the tests fail, which means the tests are exactly covering the problematic parsing case and the detection process works perfectly fine.

image

Details

The mysql2 client library automatically parses JSON values, but can still return strings for JSON string primitives (e.g., "hello"). The previous implementation would blindly attempt to parse any string value, which would fail for already-parsed string values from mysql2.

This fix:

  1. Adds a method to detect whether mysql2 or mysql client is being used
  2. Applies different JSON parsing logic based on the client:
    • For mysql: Always parse string values (existing behavior)
    • For mysql2: Only parse if the string is valid JSON, otherwise keep as-is

Fixes #8319

Summary by CodeRabbit

  • Bug Fixes

    • Improved JSON parsing for MySQL connectors to avoid double-parsing and preserve plain string values when appropriate, ensuring correct handling of JSON types and nested structures across connectors.
  • New Features

    • Detects which MySQL connector is in use and warns when a driver is supplied without an explicit connector package to improve diagnostics.
  • Tests

    • Added functional tests validating JSON persistence/retrieval across MySQL-compatible connectors, including quoted/empty strings and complex nested JSON.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

Walkthrough

Adds connector-aware JSON hydration to the MySQL driver by tracking which connector package was loaded and branching JSON parsing accordingly; updates dependency-detection/warnings for direct driver inputs and adds functional tests (entity + test) validating JSON persistence/hydration across mysql-compatible drivers.

Changes

Cohort / File(s) Summary
MySQL driver JSON handling
src/driver/mysql/MysqlDriver.ts
Adds `loadedConnectorPackage: "mysql"
MySQL JSON parsing tests & entity
test/functional/json/mysql-json-parsing/entity/JsonEntity.ts, test/functional/json/mysql-json-parsing/mysql-json-parsing.test.ts
Adds JsonEntity with multiple nullable json columns (object, array, string, number, boolean, null, complex) and a functional test suite exercising persistence and reload against mysql-compatible drivers (mysql, mariadb/mysql2), covering primitives, arrays/objects, nested structures, quoted-string edge cases, and empty strings.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as Application
  participant ORM as ORM Layer
  participant Driver as MysqlDriver
  participant Conn as Connector (mysql | mysql2)
  participant DB as MySQL

  App->>ORM: find()/query()
  ORM->>Driver: execute query
  Driver->>Conn: send SQL
  Conn->>DB: execute
  DB-->>Conn: rows with JSON columns
  Conn-->>Driver: result set (values)

  rect rgb(245,250,255)
    note over Driver: Decide JSON hydration based on loadedConnectorPackage
    alt isUsingMysql2() = true
      note right of Driver: mysql2 may already parse some JSON values\nFor string values: attempt JSON.parse inside try/catch
      Driver->>Driver: try { JSON.parse(value) } catch { keep string }
    else Using mysql
      note right of Driver: previous behavior: JSON.parse for string values
      Driver->>Driver: JSON.parse(value)
    end
  end

  Driver-->>ORM: hydrated entities
  ORM-->>App: results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • gioboa
  • sgarner
  • alumni
  • OSA413

Poem

In my burrow I nibble code with care,
Two tunnels open — I mark which where.
No double-parse on a quoted bite,
mysql or mysql2 — all parsed right.
Tests hop in, and the night is bright. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "fix: JSON parsing for mysql2 client library (#8319)" is concise, directly describes the primary change (a mysql2-specific JSON parsing fix), and references the linked issue; it clearly matches the modifications in MysqlDriver.ts and the added JSON tests. It is short, specific, and appropriate for a teammate scanning the history.
Linked Issues Check ✅ Passed The PR implements the linked issue's coding objective by adding connector detection, a mysql2-aware code path in prepareHydratedValue that avoids unconditional JSON.parse for mysql2, and updates connector-loading logic so the detection works even when the driver is provided directly; test fixtures and a functional test were also added to exercise JSON persistence. The one gap is that the automated test matrix in the new test file targets "mysql" and "mariadb" but does not explicitly run the "mysql2" client, so while the code implements the intended fix the repository tests do not directly exercise the mysql2 path.
Out of Scope Changes Check ✅ Passed I found no out-of-scope changes: edits are limited to MysqlDriver.ts (connector detection and JSON hydration behavior) and supporting test files (JsonEntity and mysql-json-parsing.test.ts), and the added warning about a directly-provided driver is related to connector detection rather than an unrelated feature. All changes are aligned with the PR objectives to fix mysql2 JSON parsing.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-8319

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbc5f33 and d6d2d55.

📒 Files selected for processing (1)
  • test/functional/json/mysql-json-parsing/mysql-json-parsing.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/functional/json/mysql-json-parsing/mysql-json-parsing.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Sep 19, 2025

Deploying typeorm with  Cloudflare Pages  Cloudflare Pages

Latest commit: d6d2d55
Status: ✅  Deploy successful!
Preview URL: https://08ef3107.typeorm.pages.dev
Branch Preview URL: https://issue-8319.typeorm.pages.dev

View logs

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 19, 2025

typeorm-sql-js-example

npm i https://pkg.pr.new/typeorm/typeorm@11659

commit: d6d2d55

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/driver/mysql/MysqlDriver.ts (2)

587-601: Consider a more robust detection method for mysql2.

While the current detection logic works, it relies on undocumented properties that could change in future versions of mysql2. Consider using a more stable approach.

You could simplify and make the detection more explicit:

protected isUsingMysql2(): boolean {
-    // mysql2 can be detected by checking for specific properties that are unique to it
-    // mysql2 exports a 'version' property directly on the module
-    // Also mysql2 has different connection promise API
-    return !!(
-        this.mysql &&
-        (this.mysql.version ||
-            this.mysql.createConnectionPromise ||
-            (this.mysql.Connection &&
-                this.mysql.Connection.prototype.execute))
-    )
+    // mysql2 has a distinct module name property or we can check the loaded package
+    // Consider checking if this.options.connectorPackage is available
+    const connectorPackage = this.options.connectorPackage ?? "mysql"
+    return connectorPackage === "mysql2" || 
+           !!(this.mysql && this.mysql.version) // mysql2 exports version, mysql doesn't
}

Additionally, consider caching this result as a property since it won't change during runtime:

private _isUsingMysql2?: boolean

protected isUsingMysql2(): boolean {
    if (this._isUsingMysql2 === undefined) {
        this._isUsingMysql2 = // ... detection logic
    }
    return this._isUsingMysql2
}

681-689: Consider extracting the JSON parsing logic for better testability.

The nested JSON parsing logic could be extracted into a separate method for clarity and testing.

+private parseJsonValueForMysql2(value: any): any {
+    if (typeof value === "string") {
+        try {
+            return JSON.parse(value)
+        } catch {
+            // It's a string that's not valid JSON, which means mysql2
+            // already parsed it and it's just a string value
+            return value
+        }
+    }
+    // If it's not a string, mysql2 has already parsed it correctly
+    return value
+}

 prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
     // ... existing code ...
     } else if (columnMetadata.type === "json") {
         if (this.isUsingMysql2()) {
-            // With mysql2, only parse if it's a valid JSON string representation
-            // but not if it's already an object or a JSON primitive
-            if (typeof value === "string") {
-                try {
-                    // Try to parse it - if it fails, it's already a parsed string value
-                    const parsed = JSON.parse(value)
-                    value = parsed
-                } catch {
-                    // It's a string that's not valid JSON, which means mysql2
-                    // already parsed it and it's just a string value
-                    // Keep value as is
-                }
-            }
-            // If it's not a string, mysql2 has already parsed it correctly
+            value = this.parseJsonValueForMysql2(value)
         } else {
             // Classic mysql always returns JSON as strings
             value = typeof value === "string" ? JSON.parse(value) : value
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a49f612 and 79b87e7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • package.json (1 hunks)
  • src/driver/mysql/MysqlDriver.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: tests-linux (20) / postgres (17)
  • GitHub Check: tests-linux (20) / postgres (14)
  • GitHub Check: tests-linux (20) / mssql
  • GitHub Check: tests-linux (20) / mysql_mariadb_latest
  • GitHub Check: tests-linux (20) / cockroachdb
  • GitHub Check: tests-linux (20) / mysql_mariadb
  • GitHub Check: tests-linux (18) / postgres (17)
  • GitHub Check: tests-linux (18) / postgres (14)
  • GitHub Check: tests-linux (18) / sqlite
  • GitHub Check: tests-linux (18) / oracle
  • GitHub Check: tests-linux (18) / sqljs
  • GitHub Check: tests-linux (18) / mysql_mariadb_latest
  • GitHub Check: tests-linux (18) / better-sqlite3
  • GitHub Check: tests-linux (18) / mongodb
  • GitHub Check: tests-linux (18) / mysql_mariadb
  • GitHub Check: tests-linux (18) / mssql
  • GitHub Check: tests-windows / better-sqlite3
  • GitHub Check: tests-windows / sqljs
  • GitHub Check: tests-windows / sqlite
🔇 Additional comments (3)
package.json (1)

129-129: Verify if upgrading better-sqlite3 is necessary for this JSON parsing fix.

The PR objectives focus on fixing JSON parsing for mysql2, but better-sqlite3 (SQLite driver) has been upgraded from ^8.7.0 to ^12.2.0. This seems unrelated to the stated fix.

Can you confirm if this dependency update is intentional or if it accidentally got included from another branch? If intentional, please explain how it relates to the JSON parsing fix for mysql2.

src/driver/mysql/MysqlDriver.ts (2)

673-695: LGTM! The JSON parsing logic correctly handles both mysql and mysql2 behaviors.

The implementation properly addresses the double-parsing issue:

  • For mysql2: Attempts parsing with proper error handling, preserving already-parsed string primitives
  • For mysql (classic): Maintains backward compatibility with existing parsing behavior

The try-catch approach safely handles JSON string primitives that mysql2 may have already parsed.


673-695: No action required — JSON null / empty-string / primitive cases are handled.

preparePersistentValue uses JSON.stringify; prepareHydratedValue parses classic-mysql strings with JSON.parse and, for mysql2, only attempts JSON.parse on string inputs and falls back on the original value on parse failure; SQL NULL is returned early. See src/driver/mysql/MysqlDriver.ts (preparePersistentValue / prepareHydratedValue).

@coveralls
Copy link

coveralls commented Sep 19, 2025

Coverage Status

coverage: 76.401% (-0.02%) from 76.423%
when pulling d6d2d55 on issue-8319
into a49f612 on master.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/driver/mysql/MysqlDriver.ts (2)

592-599: Harden mysql2 detection when a custom options.driver is supplied.

If callers inject a driver via options.driver but don’t set connectorPackage, loadedConnectorPackage will still reflect the default (“mysql”), which can misroute JSON hydration for mysql2 and reintroduce double‑parsing. Either (a) infer from this.options.connectorPackage first, falling back to loadedConnectorPackage, or (b) require connectorPackage whenever options.driver is provided.

Apply this minimal guard to prefer the explicit option when present:

-    protected isUsingMysql2(): boolean {
-        // Check which package was actually loaded during initialization
-        return this.loadedConnectorPackage === "mysql2"
-    }
+    protected isUsingMysql2(): boolean {
+        const pkg = (this.options as any).connectorPackage ?? this.loadedConnectorPackage
+        return pkg === "mysql2"
+    }

Optionally, emit a warning when options.driver is set but connectorPackage is absent so users don’t hit subtle JSON behavior differences.


670-693: Connector‑aware JSON hydration works; consider simplifying to a driver‑agnostic safe parse.

Current branching is correct but depends on connector detection. You can avoid that coupling and keep behavior identical by parsing only when a string lexically looks like JSON and otherwise leaving it as‑is. This covers both mysql and mysql2 without needing isUsingMysql2().

Apply this refactor:

-        } else if (columnMetadata.type === "json") {
-            // mysql2 returns JSON values already parsed, but may still be a string
-            // if the JSON value itself is a string (e.g., "\"hello\"")
-            // mysql (classic) always returns JSON as strings that need parsing
-            if (this.isUsingMysql2()) {
-                // With mysql2, only parse if it's a valid JSON string representation
-                // but not if it's already an object or a JSON primitive
-                if (typeof value === "string") {
-                    try {
-                        // Try to parse it - if it fails, it's already a parsed string value
-                        const parsed = JSON.parse(value)
-                        value = parsed
-                    } catch {
-                        // It's a string that's not valid JSON, which means mysql2
-                        // already parsed it and it's just a string value
-                        // Keep value as is
-                    }
-                }
-                // If it's not a string, mysql2 has already parsed it correctly
-            } else {
-                // Classic mysql always returns JSON as strings
-                value = typeof value === "string" ? JSON.parse(value) : value
-            }
-        } else if (columnMetadata.type === "time") {
+        } else if (columnMetadata.type === "json") {
+            if (typeof value === "string") {
+                const s = value.trim()
+                const first = s[0], last = s[s.length - 1]
+                const looksJson =
+                    (first === "{" && last === "}") ||
+                    (first === "[" && last === "]") ||
+                    (first === '"' && last === '"') ||
+                    s === "true" || s === "false" || s === "null" ||
+                    /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(s)
+                if (looksJson) {
+                    try { value = JSON.parse(s) } catch { /* leave as-is */ }
+                }
+            }
+        } else if (columnMetadata.type === "time") {
test/functional/json/mysql-json-parsing/entity/JsonEntity.ts (1)

8-28: Type nits for stronger signal in tests.

Using any everywhere hides type regressions. Consider tightening where easy:

  • jsonObject: Record<string, unknown>
  • jsonArray: unknown[]
  • complexJson: unknown

This remains flexible but avoids any.

-    jsonObject: any
+    jsonObject: Record<string, unknown>
@@
-    jsonArray: any[]
+    jsonArray: unknown[]
@@
-    complexJson: any
+    complexJson: unknown
test/functional/json/mysql-json-parsing/mysql-json-parsing.test.ts (2)

11-160: Tests are clear and cover core cases; consider DRYing across connectors.

Both “mysql” and “mysql2” suites duplicate the same scenarios. You could parameterize over connectorPackage and run a single table‑driven suite to reduce maintenance.

- describe("with mysql driver", () => { ... })
- describe("with mysql2 driver", () => { ... })
+ ;(["mysql", "mysql2"] as const).forEach((connectorPackage) => {
+   describe(`with ${connectorPackage} driver`, () => {
+     // create connections using connectorPackage
+     // run the same set of tests
+   })
+ })

311-344: Add a regression that mimics the original failure mode (string primitive serialized by connector).

To mirror issue #8319 more closely, insert a JSON string primitive via raw SQL (so storage is '"hello"') and read it back through the repository to ensure no throw and correct value.

+        it("should not throw when JSON string primitive is returned serialized by the connector", () =>
+            Promise.all(
+                connections.map(async (connection) => {
+                    const qr = connection.createQueryRunner()
+                    await qr.query(`INSERT INTO json_entity (jsonString) VALUES (CAST('\"hello\"' AS JSON))`)
+                    const repo = connection.getRepository(JsonEntity)
+                    const rows = await repo.find()
+                    expect(rows[0].jsonString).to.equal("hello")
+                    await qr.release()
+                }),
+            ))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79b87e7 and 9f259de.

📒 Files selected for processing (3)
  • src/driver/mysql/MysqlDriver.ts (4 hunks)
  • test/functional/json/mysql-json-parsing/entity/JsonEntity.ts (1 hunks)
  • test/functional/json/mysql-json-parsing/mysql-json-parsing.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
test/functional/json/mysql-json-parsing/entity/JsonEntity.ts (2)
src/decorator/columns/PrimaryGeneratedColumn.ts (1)
  • PrimaryGeneratedColumn (55-119)
src/decorator/columns/Column.ts (1)
  • Column (134-220)
test/functional/json/mysql-json-parsing/mysql-json-parsing.test.ts (1)
test/utils/test-utils.ts (3)
  • createTestingConnections (388-482)
  • reloadTestingDatabases (501-506)
  • closeTestingConnections (487-496)
src/driver/mysql/MysqlDriver.ts (1)
src/platform/PlatformTools.ts (1)
  • PlatformTools (17-279)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
  • GitHub Check: tests-linux (20) / sqljs
  • GitHub Check: tests-linux (20) / mysql_mariadb_latest
  • GitHub Check: tests-linux (20) / sqlite
  • GitHub Check: tests-linux (20) / sap
  • GitHub Check: tests-linux (18) / postgres (14)
  • GitHub Check: tests-linux (18) / sap
  • GitHub Check: tests-linux (18) / postgres (17)
  • GitHub Check: tests-linux (20) / oracle
  • GitHub Check: tests-linux (18) / oracle
  • GitHub Check: tests-linux (20) / cockroachdb
  • GitHub Check: tests-linux (18) / mysql_mariadb
  • GitHub Check: tests-linux (18) / mssql
  • GitHub Check: tests-windows / sqljs
  • GitHub Check: tests-linux (18) / mysql_mariadb_latest
  • GitHub Check: tests-windows / sqlite
  • GitHub Check: tests-windows / better-sqlite3
🔇 Additional comments (2)
src/driver/mysql/MysqlDriver.ts (2)

60-64: LGTM: tracking the loaded connector package is a good enabler for connector‑aware behavior.


1180-1218: Prefer explicit connectorPackage when a custom driver is injected

MysqlConnectionOptions already defines connectorPackage (src/driver/mysql/MysqlConnectionOptions.ts:110). loadDependencies unconditionally sets loadedConnectorPackage = connectorPackage (src/driver/mysql/MysqlDriver.ts:1180–1218), which can misidentify the active connector when callers inject a driver via options.driver. Repo scan found no call sites setting options.driver for mysql — external callers may still inject a custom driver.

Change suggestion — warn when driver is injected and prefer an explicit connectorPackage; mirror in fallback:

-            // Successfully loaded the requested package
-            this.loadedConnectorPackage = connectorPackage
+            // Mark which package we believe is in use.
+            if (this.options.driver && (this.options as any).connectorPackage == null) {
+                PlatformTools.logWarn(
+                    "MysqlDriver: When providing `options.driver`, also set `connectorPackage` to 'mysql' or 'mysql2' to ensure correct JSON handling.",
+                )
+            }
+            this.loadedConnectorPackage =
+                (this.options as any).connectorPackage ?? connectorPackage
-                // Successfully loaded the fallback package
-                this.loadedConnectorPackage = fallbackConnectorPackage
+                this.loadedConnectorPackage =
+                    (this.options as any).connectorPackage ?? fallbackConnectorPackage

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/driver/mysql/MysqlDriver.ts (2)

592-599: Harden mysql2 detection fallback

Relying only on loadedConnectorPackage can misclassify when users pass a direct driver with a mismatched connectorPackage. Add a runtime feature check fallback.

-    protected isUsingMysql2(): boolean {
-        // Check which package was actually loaded during initialization
-        return this.loadedConnectorPackage === "mysql2"
-    }
+    protected isUsingMysql2(): boolean {
+        // Primary signal from dependency loader
+        if (this.loadedConnectorPackage) {
+            return this.loadedConnectorPackage === "mysql2"
+        }
+        // Fallback: feature-detect mysql2 (Connection.prototype.execute exists only in mysql2)
+        const proto = this.mysql?.Connection?.prototype
+        return !!proto && typeof (proto as any).execute === "function"
+    }

1181-1189: Use PlatformTools logging instead of console.warn

Keeps logging consistent with the rest of the codebase.

-        if (this.options.driver && !this.options.connectorPackage) {
-            console.warn(
-                "Warning: MySQL driver instance provided directly without specifying connectorPackage. " +
-                    "This may lead to unexpected JSON parsing behavior differences between mysql and mysql2. " +
-                    "Consider explicitly setting connectorPackage: 'mysql' or 'mysql2' in your configuration.",
-            )
-        }
+        if (this.options.driver && !this.options.connectorPackage) {
+            PlatformTools.logWarn(
+                "MysqlDriver",
+                "MySQL driver instance provided directly without specifying connectorPackage. " +
+                    "This may lead to JSON parsing differences between 'mysql' and 'mysql2'. " +
+                    "Consider explicitly setting connectorPackage: 'mysql' or 'mysql2'.",
+            )
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f259de and bbc5f33.

📒 Files selected for processing (1)
  • src/driver/mysql/MysqlDriver.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/driver/mysql/MysqlDriver.ts (1)
src/platform/PlatformTools.ts (1)
  • PlatformTools (17-279)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: tests-linux (20) / sqljs
  • GitHub Check: tests-linux (20) / sqlite
  • GitHub Check: tests-linux (20) / better-sqlite3
  • GitHub Check: tests-linux (20) / mysql_mariadb
  • GitHub Check: tests-linux (20) / mssql
  • GitHub Check: tests-linux (18) / sqljs
  • GitHub Check: tests-linux (18) / postgres (17)
  • GitHub Check: tests-linux (18) / mongodb
  • GitHub Check: tests-linux (18) / postgres (14)
  • GitHub Check: tests-linux (18) / sqlite
  • GitHub Check: tests-linux (18) / mysql_mariadb_latest
  • GitHub Check: tests-linux (18) / better-sqlite3
  • GitHub Check: tests-linux (18) / mssql
  • GitHub Check: tests-linux (18) / mysql_mariadb
  • GitHub Check: tests-linux (20) / mongodb
  • GitHub Check: tests-linux (20) / cockroachdb
  • GitHub Check: tests-windows / sqljs
  • GitHub Check: tests-windows / better-sqlite3
  • GitHub Check: tests-windows / sqlite
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
src/driver/mysql/MysqlDriver.ts (3)

60-64: LGTM: explicit connector tracking

The private union type is clear and helps branch behavior safely.


1232-1232: LGTM: record fallback connector

Setting loadedConnectorPackage after fallback ensures isUsingMysql2() works consistently.


1213-1227: Tighten package heuristics for direct driver input

Replace the brittle this.mysql.version check with feature-detection (Connection.prototype.execute), warn if options.connectorPackage conflicts with the detected driver, and prefer the detected value. Verified: PlatformTools.logWarn exists at src/platform/PlatformTools.ts. Sandbox had no mysql/mysql2 installed so runtime detection couldn't be validated — run the node checks below locally.

File: src/driver/mysql/MysqlDriver.ts (around the shown snippet)

-            // If driver was provided directly, try to detect which package it is
-            if (this.options.driver && !this.options.connectorPackage) {
-                // Try to detect if it's mysql2 based on unique properties
-                if (
-                    this.mysql.version ||
-                    (this.mysql.Connection &&
-                        this.mysql.Connection.prototype.execute)
-                ) {
-                    this.loadedConnectorPackage = "mysql2"
-                } else {
-                    this.loadedConnectorPackage = "mysql"
-                }
-            } else {
-                this.loadedConnectorPackage = connectorPackage
-            }
+            // If driver was provided directly, detect which package it is
+            if (this.options.driver) {
+                const isMysql2 =
+                    !!this.mysql?.Connection?.prototype &&
+                    typeof this.mysql.Connection.prototype.execute === "function"
+                if (
+                    this.options.connectorPackage &&
+                    this.options.connectorPackage !== (isMysql2 ? "mysql2" : "mysql")
+                ) {
+                    PlatformTools.logWarn(
+                        "MysqlDriver",
+                        `connectorPackage ('${this.options.connectorPackage}') does not match provided driver; detected '${isMysql2 ? "mysql2" : "mysql"}'. Using detected value.`,
+                    )
+                }
+                this.loadedConnectorPackage = isMysql2 ? "mysql2" : "mysql"
+            } else {
+                this.loadedConnectorPackage = connectorPackage
+            }

Runtime verification to run locally:

node -e "try{const m=require('mysql');console.log('mysql keys:',Object.keys(m).slice(0,10))}catch(e){console.log('mysql not installed')}"
node -e "try{const m=require('mysql2');console.log('mysql2 Connection has execute:', Boolean(m.Connection && m.Connection.prototype && m.Connection.prototype.execute))}catch(e){console.log('mysql2 not installed')}"

Comment on lines +671 to 693
// mysql2 returns JSON values already parsed, but may still be a string
// if the JSON value itself is a string (e.g., "\"hello\"")
// mysql (classic) always returns JSON as strings that need parsing
if (this.isUsingMysql2()) {
// With mysql2, only parse if it's a valid JSON string representation
// but not if it's already an object or a JSON primitive
if (typeof value === "string") {
try {
// Try to parse it - if it fails, it's already a parsed string value
const parsed = JSON.parse(value)
value = parsed
} catch {
// It's a string that's not valid JSON, which means mysql2
// already parsed it and it's just a string value
// Keep value as is
}
}
// If it's not a string, mysql2 has already parsed it correctly
} else {
// Classic mysql always returns JSON as strings
value = typeof value === "string" ? JSON.parse(value) : value
}
} else if (columnMetadata.type === "time") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid accidental type changes for JSON string primitives like "123"

With mysql2, parsing any “valid JSON-looking” string (e.g., "123") will coerce a stored JSON string primitive "123" into number 123. Instead, only parse containers or explicitly quoted JSON strings to avoid breaking semantics.

-            if (this.isUsingMysql2()) {
-                // With mysql2, only parse if it's a valid JSON string representation
-                // but not if it's already an object or a JSON primitive
-                if (typeof value === "string") {
-                    try {
-                        // Try to parse it - if it fails, it's already a parsed string value
-                        const parsed = JSON.parse(value)
-                        value = parsed
-                    } catch {
-                        // It's a string that's not valid JSON, which means mysql2
-                        // already parsed it and it's just a string value
-                        // Keep value as is
-                    }
-                }
-                // If it's not a string, mysql2 has already parsed it correctly
-            } else {
+            if (this.isUsingMysql2()) {
+                // mysql2 usually returns parsed values; only parse obvious JSON containers or quoted strings
+                if (typeof value === "string") {
+                    const s = value.trim()
+                    const looksLikeContainer =
+                        (s.startsWith("{") && s.endsWith("}")) ||
+                        (s.startsWith("[") && s.endsWith("]"))
+                    const looksLikeQuotedString =
+                        s.length >= 2 && s.startsWith('"') && s.endsWith('"')
+                    if (looksLikeContainer || looksLikeQuotedString) {
+                        try {
+                            value = JSON.parse(s)
+                        } catch {
+                            /* keep as-is */
+                        }
+                    }
+                }
+            } else {
                 // Classic mysql always returns JSON as strings
                 value = typeof value === "string" ? JSON.parse(value) : value
             }

Would you like me to add tests covering objects, arrays, numbers, booleans, null, and string primitives (including "123") across both mysql and mysql2?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// mysql2 returns JSON values already parsed, but may still be a string
// if the JSON value itself is a string (e.g., "\"hello\"")
// mysql (classic) always returns JSON as strings that need parsing
if (this.isUsingMysql2()) {
// With mysql2, only parse if it's a valid JSON string representation
// but not if it's already an object or a JSON primitive
if (typeof value === "string") {
try {
// Try to parse it - if it fails, it's already a parsed string value
const parsed = JSON.parse(value)
value = parsed
} catch {
// It's a string that's not valid JSON, which means mysql2
// already parsed it and it's just a string value
// Keep value as is
}
}
// If it's not a string, mysql2 has already parsed it correctly
} else {
// Classic mysql always returns JSON as strings
value = typeof value === "string" ? JSON.parse(value) : value
}
} else if (columnMetadata.type === "time") {
// mysql2 returns JSON values already parsed, but may still be a string
// if the JSON value itself is a string (e.g., "\"hello\"")
// mysql (classic) always returns JSON as strings that need parsing
if (this.isUsingMysql2()) {
// mysql2 usually returns parsed values; only parse obvious JSON containers or quoted strings
if (typeof value === "string") {
const s = value.trim()
const looksLikeContainer =
(s.startsWith("{") && s.endsWith("}")) ||
(s.startsWith("[") && s.endsWith("]"))
const looksLikeQuotedString =
s.length >= 2 && s.startsWith('"') && s.endsWith('"')
if (looksLikeContainer || looksLikeQuotedString) {
try {
value = JSON.parse(s)
} catch {
/* keep as-is */
}
}
}
} else {
// Classic mysql always returns JSON as strings
value = typeof value === "string" ? JSON.parse(value) : value
}
} else if (columnMetadata.type === "time") {

@michaelbromley michaelbromley merged commit 974ead2 into master Sep 19, 2025
98 of 99 checks passed
@michaelbromley michaelbromley deleted the issue-8319 branch September 19, 2025 08:34
ThbltLmr pushed a commit to ThbltLmr/typeorm that referenced this pull request Dec 2, 2025
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.

JSON string value will be parsed twice with mysql2

5 participants