Skip to content

Postgres + populate + array field + transactional/fork = parasitic updates on select-only queries #6432

@KazimirPodolski

Description

@KazimirPodolski

Describe the bug

In following scenario there is a "parasitic" useless UPDATEs following readonly queries.

In short, this:

await em.transactional(async (tx) => {
  await tx.findOneOrFail(User, user.id, { populate: ['contacts.contact'] });
});

leads to this:

console.log
    [query] begin

      at DefaultLogger.log (node_modules/@mikro-orm/core/logging/DefaultLogger.js:38:14)

  console.log
    [query] select "u0".*, "c1"."user_id" as "c1__user_id", "c1"."contact_id" as "c1__contact_id", "c2"."id" as "c2__id", "c2"."emails" as "c2__emails" from "test"."user" as "u0" left join "test"."user_contact" as "c1" on "u0"."id" = "c1"."user_id" left join "test"."contact" as "c2" on "c1"."contact_id" = "c2"."id" where "u0"."id" = 1 [took 1 ms, 1 result]

      at DefaultLogger.log (node_modules/@mikro-orm/core/logging/DefaultLogger.js:38:14)

  console.log
    [query] update "test"."contact" set "emails" = '{foo}' where "id" = 1 [took 0 ms, 1 row affected]

      at DefaultLogger.log (node_modules/@mikro-orm/core/logging/DefaultLogger.js:38:14)

  console.log
    [query] commit

      at DefaultLogger.log (node_modules/@mikro-orm/core/logging/DefaultLogger.js:38:14)

which is extremely unexpected and actually harmful in scenarios with triggers. Because the triggers will be triggered not only with real updates, but also with read-only requests which are usually many many more than write requests.

I think the problem starts somewhere here:

private computePayload<T extends object>(entity: T, ignoreUndefined = false): EntityData<T> {
  const data = this.comparator.prepareEntity(entity);
  ...
  const diff = comparator(originalEntityData, data);

The problem is originalEntityData did not undergo a prepareEntity, but one of the things prepareEntity does with Postgres array fields like string[] is convertToDatabaseValue_*.

I see this in debugger for em.getUnitOfWork().comparator.getSnapshotGenerator('Contact').toString():

if (typeof entity.emails !== 'undefined') {
  ret.emails = clone(convertToDatabaseValue_emails(entity.emails));
}

So, original value ['foo@example.com'] from originalEntityData is being compared to "prepared" value '{foo@example.com}' from data, of course being different. And this provokes a parasite update. Updating nothing, but still an update.

Reproduction

https://github.com/KazimirPodolski/mikro-orm-parasite-update-repro

Reproes with either em.transactional or em.fork. Does not repro without one.

Does not repro on sqlite in-memory driver.

Does not repro without a relation (e.g. if it was just emails in User).

What driver are you using?

@mikro-orm/postgresql

MikroORM version

At least 6.3.11 and up

Node.js version

20

Operating system

No response

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions