mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
parent
a49f612289
commit
974ead202d
@ -57,6 +57,11 @@ export class MysqlDriver implements Driver {
|
||||
*/
|
||||
poolCluster: any
|
||||
|
||||
/**
|
||||
* The actual connector package that was loaded ("mysql" or "mysql2").
|
||||
*/
|
||||
private loadedConnectorPackage: "mysql" | "mysql2" | undefined
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Implemented Properties
|
||||
// -------------------------------------------------------------------------
|
||||
@ -584,6 +589,14 @@ export class MysqlDriver implements Driver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the driver is using mysql2 package.
|
||||
*/
|
||||
protected isUsingMysql2(): boolean {
|
||||
// Check which package was actually loaded during initialization
|
||||
return this.loadedConnectorPackage === "mysql2"
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
*/
|
||||
@ -655,7 +668,28 @@ export class MysqlDriver implements Driver {
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
} else if (columnMetadata.type === "json") {
|
||||
value = typeof value === "string" ? JSON.parse(value) : value
|
||||
// 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") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (columnMetadata.type === "simple-array") {
|
||||
@ -1144,6 +1178,15 @@ export class MysqlDriver implements Driver {
|
||||
* Loads all driver dependencies.
|
||||
*/
|
||||
protected loadDependencies(): void {
|
||||
// Warn if driver is provided directly but connectorPackage is not specified
|
||||
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.",
|
||||
)
|
||||
}
|
||||
|
||||
const connectorPackage = this.options.connectorPackage ?? "mysql"
|
||||
const fallbackConnectorPackage =
|
||||
connectorPackage === "mysql"
|
||||
@ -1166,9 +1209,27 @@ export class MysqlDriver implements Driver {
|
||||
`'${connectorPackage}' was found but it is empty. Falling back to '${fallbackConnectorPackage}'.`,
|
||||
)
|
||||
}
|
||||
// Successfully loaded the requested package
|
||||
// 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
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
this.mysql = PlatformTools.load(fallbackConnectorPackage) // try to load second supported package
|
||||
// Successfully loaded the fallback package
|
||||
this.loadedConnectorPackage = fallbackConnectorPackage
|
||||
} catch (e) {
|
||||
throw new DriverPackageNotInstalledError(
|
||||
"Mysql",
|
||||
|
||||
28
test/functional/json/mysql-json-parsing/entity/JsonEntity.ts
Normal file
28
test/functional/json/mysql-json-parsing/entity/JsonEntity.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from "../../../../../src"
|
||||
|
||||
@Entity()
|
||||
export class JsonEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
jsonObject: any
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
jsonArray: any[]
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
jsonString: string
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
jsonNumber: number
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
jsonBoolean: boolean
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
jsonNull: null
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
complexJson: any
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
import { expect } from "chai"
|
||||
import { DataSource } from "../../../../src"
|
||||
import "../../../utils/test-setup"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { JsonEntity } from "./entity/JsonEntity"
|
||||
|
||||
describe("mysql json parsing", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [JsonEntity],
|
||||
enabledDrivers: ["mysql", "mariadb"],
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("should correctly parse JSON objects", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonObject = { foo: "bar", nested: { value: 123 } }
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonObject).to.deep.equal({
|
||||
foo: "bar",
|
||||
nested: { value: 123 },
|
||||
})
|
||||
}),
|
||||
))
|
||||
|
||||
it("should correctly parse JSON arrays", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonArray = [1, "two", { three: 3 }, null, true]
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonArray).to.deep.equal([
|
||||
1,
|
||||
"two",
|
||||
{ three: 3 },
|
||||
null,
|
||||
true,
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should correctly handle JSON string primitives", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonString = "hello world"
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonString).to.be.a("string")
|
||||
expect(loaded!.jsonString).to.equal("hello world")
|
||||
}),
|
||||
))
|
||||
|
||||
it("should correctly handle JSON number primitives", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonNumber = 42.5
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonNumber).to.be.a("number")
|
||||
expect(loaded!.jsonNumber).to.equal(42.5)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should correctly handle JSON boolean primitives", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonBoolean = true
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonBoolean).to.be.a("boolean")
|
||||
expect(loaded!.jsonBoolean).to.equal(true)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should correctly handle JSON null", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonNull = null
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonNull).to.be.null
|
||||
}),
|
||||
))
|
||||
|
||||
it("should handle complex nested JSON structures", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.complexJson = {
|
||||
users: [
|
||||
{ id: 1, name: "Alice", active: true },
|
||||
{ id: 2, name: "Bob", active: false },
|
||||
],
|
||||
settings: {
|
||||
theme: "dark",
|
||||
notifications: {
|
||||
email: true,
|
||||
push: false,
|
||||
},
|
||||
},
|
||||
metadata: null,
|
||||
count: 42,
|
||||
}
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.complexJson).to.deep.equal(entity.complexJson)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should handle edge case of JSON strings containing quotes", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonString = 'string with "quotes" inside'
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonString).to.be.a("string")
|
||||
expect(loaded!.jsonString).to.equal(
|
||||
'string with "quotes" inside',
|
||||
)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should handle edge case of empty strings", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(JsonEntity)
|
||||
const entity = new JsonEntity()
|
||||
entity.jsonString = ""
|
||||
|
||||
const saved = await repo.save(entity)
|
||||
const loaded = await repo.findOneBy({ id: saved.id })
|
||||
|
||||
expect(loaded).to.be.not.undefined
|
||||
expect(loaded!.jsonString).to.be.a("string")
|
||||
expect(loaded!.jsonString).to.equal("")
|
||||
}),
|
||||
))
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user