fix: change how array columns are compared on column changed detection (#11269)

* fix: change how array columns are compared on column changed detection

Closes: #5967

* add tests with date array colum

* Normalize date arrays before comparing
This commit is contained in:
Mohamed Nader Baccari 2025-04-29 19:33:12 -04:00 committed by GitHub
parent 61a6f971af
commit a61654e079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 294 additions and 13 deletions

View File

@ -85,16 +85,32 @@ export class SubjectChangedColumnsComputer {
if (entityValue !== null) {
switch (column.type) {
case "date":
normalizedValue =
DateUtils.mixedDateToDateString(entityValue)
normalizedValue = column.isArray
? entityValue.map((date: Date) =>
DateUtils.mixedDateToDateString(date),
)
: DateUtils.mixedDateToDateString(entityValue)
databaseValue = column.isArray
? databaseValue.map((date: Date) =>
DateUtils.mixedDateToDateString(date),
)
: DateUtils.mixedDateToDateString(databaseValue)
break
case "time":
case "time with time zone":
case "time without time zone":
case "timetz":
normalizedValue =
DateUtils.mixedDateToTimeString(entityValue)
normalizedValue = column.isArray
? entityValue.map((date: Date) =>
DateUtils.mixedDateToTimeString(date),
)
: DateUtils.mixedDateToTimeString(entityValue)
databaseValue = column.isArray
? databaseValue.map((date: Date) =>
DateUtils.mixedDateToTimeString(date),
)
: DateUtils.mixedDateToTimeString(databaseValue)
break
case "datetime":
@ -105,14 +121,26 @@ export class SubjectChangedColumnsComputer {
case "timestamp with time zone":
case "timestamp with local time zone":
case "timestamptz":
normalizedValue =
DateUtils.mixedDateToUtcDatetimeString(
entityValue,
)
databaseValue =
DateUtils.mixedDateToUtcDatetimeString(
databaseValue,
)
normalizedValue = column.isArray
? entityValue.map((date: Date) =>
DateUtils.mixedDateToUtcDatetimeString(
date,
),
)
: DateUtils.mixedDateToUtcDatetimeString(
entityValue,
)
databaseValue = column.isArray
? databaseValue.map((date: Date) =>
DateUtils.mixedDateToUtcDatetimeString(
date,
),
)
: DateUtils.mixedDateToUtcDatetimeString(
databaseValue,
)
break
case "json":
@ -155,7 +183,10 @@ export class SubjectChangedColumnsComputer {
}
// if value is not changed - then do nothing
if (
if (column.isArray) {
if (OrmUtils.deepCompare(normalizedValue, databaseValue))
return
} else if (
Buffer.isBuffer(normalizedValue) &&
Buffer.isBuffer(databaseValue)
) {

View File

@ -0,0 +1,20 @@
import { Column, PrimaryGeneratedColumn } from "../../../../src"
import { Entity } from "../../../../src/decorator/entity/Entity"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column("int", {
array: true,
default: "{}",
})
roles: number[]
@Column("date", {
array: true,
default: "{}",
})
dates: Date[]
}

View File

@ -0,0 +1,200 @@
import { expect } from "chai"
import { DataSource } from "../../../src"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { User } from "./entity/User"
import { MemoryLogger } from "./memory-logger"
describe("github issues > #5967 @afterUpdate always says array/json field updated", () => {
let dataSources: DataSource[]
before(
async () =>
(dataSources = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchema: true,
enabledDrivers: ["postgres"], // Array column are only supported by postgres src/decorator/options/ColumnCommonOptions.ts
createLogger: () => new MemoryLogger(true),
})),
)
beforeEach(() => reloadTestingDatabases(dataSources))
after(() => closeTestingConnections(dataSources))
it("should not update an array column if there was no change", () =>
Promise.all(
dataSources.map(async (dataSource) => {
const valueBefore = [1, 2, 3]
const valueAfter = [1, 2, 3]
const repository = dataSource.getRepository(User)
const logger = dataSource.logger as MemoryLogger
logger.clear()
const user = await repository.save({
roles: valueBefore,
})
const insertQueries = logger.queries.filter((q) =>
q.startsWith("INSERT"),
)
expect(insertQueries).to.have.length(1)
logger.clear()
await repository.save({
id: user.id,
roles: valueAfter,
})
const updateQueries = logger.queries.filter((q) =>
q.startsWith("UPDATE"),
)
expect(updateQueries).to.have.length(0)
}),
))
it("should not update a date array column if there was no change", () =>
Promise.all(
dataSources.map(async (dataSource) => {
const date = new Date("2023-01-01")
const valueBefore = [date]
const valueAfter = [date]
const repository = dataSource.getRepository(User)
const logger = dataSource.logger as MemoryLogger
logger.clear()
const user = await repository.save({
dates: valueBefore,
})
const insertQueries = logger.queries.filter((q) =>
q.startsWith("INSERT"),
)
expect(insertQueries).to.have.length(1)
logger.clear()
await repository.save({
id: user.id,
dates: valueAfter,
})
const updateQueries = logger.queries.filter((q) =>
q.startsWith("UPDATE"),
)
expect(updateQueries).to.have.length(0)
}),
))
it("should not update a date array column if the only change was a normalization one", () =>
Promise.all(
dataSources.map(async (dataSource) => {
const valueBefore = [new Date("2023-01-01:00:00:00")]
const valueAfter = [new Date("2023-01-01:01:00:00")]
const repository = dataSource.getRepository(User)
const logger = dataSource.logger as MemoryLogger
logger.clear()
const user = await repository.save({
dates: valueBefore,
})
const insertQueries = logger.queries.filter((q) =>
q.startsWith("INSERT"),
)
expect(insertQueries).to.have.length(1)
logger.clear()
await repository.save({
id: user.id,
dates: valueAfter,
})
const updateQueries = logger.queries.filter((q) =>
q.startsWith("UPDATE"),
)
expect(updateQueries).to.have.length(0)
}),
))
it("should update and array column if there was a change", () =>
Promise.all(
dataSources.map(async (dataSource) => {
const valueBefore = [1, 2, 3]
const valueAfter = [4, 5, 6]
const repository = dataSource.getRepository(User)
const logger = dataSource.logger as MemoryLogger
logger.clear()
const user = await repository.save({
roles: valueBefore,
})
const insertQueries = logger.queries.filter((q) =>
q.startsWith("INSERT"),
)
expect(insertQueries).to.have.length(1)
logger.clear()
await repository.save({
id: user.id,
roles: valueAfter,
})
const updateQueries = logger.queries.filter((q) =>
q.startsWith("UPDATE"),
)
expect(updateQueries).to.have.length(1)
}),
))
it("should update a date array column if there was a change", () =>
Promise.all(
dataSources.map(async (dataSource) => {
const valueBefore = [
new Date("2023-01-01"),
new Date("2023-01-02"),
new Date("2023-01-03"),
]
const valueAfter = [
new Date("2023-01-04"),
new Date("2023-01-05"),
new Date("2023-01-06"),
]
const repository = dataSource.getRepository(User)
const logger = dataSource.logger as MemoryLogger
logger.clear()
const user = await repository.save({
dates: valueBefore,
})
const insertQueries = logger.queries.filter((q) =>
q.startsWith("INSERT"),
)
expect(insertQueries).to.have.length(1)
logger.clear()
await repository.save({
id: user.id,
dates: valueAfter,
})
const updateQueries = logger.queries.filter((q) =>
q.startsWith("UPDATE"),
)
expect(updateQueries).to.have.length(1)
}),
))
})

View File

@ -0,0 +1,30 @@
import { Logger } from "../../../src/logger/Logger"
export class MemoryLogger implements Logger {
constructor(public enabled = true) {}
private _queries: string[] = []
get queries() {
return this._queries
}
logQuery(query: string) {
if (this.enabled) {
this._queries.push(query)
}
}
logQueryError(error: string, query: string) {}
logQuerySlow(time: number, query: string) {}
logSchemaBuild(message: string) {}
logMigration(message: string) {}
log(level: "log" | "info" | "warn", message: any) {}
clear() {
this._queries = []
}
}