veilletech.fr
13 sept. Feed du jour
#04 POSTGRES Article

NocoBase met à jour par clé, pas par filtre

Une condition qui sert au SELECT et meurt avant l'UPDATE ne protège rien.

Sur NocoBase 2.2.7, une mise à jour conditionnelle exécute un SELECT avec le filtre, collecte les clés primaires, puis émet les UPDATE sur ces clés — la condition n'atteint jamais l'instruction de mise à jour. Une ligne qui cesse de remplir la condition pendant l'opération est modifiée quand même : cinq fois sur cinq sur un test de 10 001 lignes. Le signalement dort sur le forum officiel depuis le 4 septembre.

4 min de lectureintermédiairevidéo 1:08
Partager
Sommaire5 sections
  1. Ce qui se passe
  2. Le mécanisme
  3. La mesure
  4. Ce que ça change
  5. À retenir

Ce qui se passe

Avec Django, on choisit entre QuerySet.update() — une instruction SQL, pas de signaux — et une boucle de save() — un ordre par ligne, signaux émis. NocoBase n'offre pas ce choix : toute mise à jour filtrée passe par la seconde forme. Un développeur a lu le code au tag v2.2.7, capturé le SQL avec log_statement=all sur PostgreSQL 16, et mesuré ce que ça coûte.

Le mécanisme

La méthode update de packages/core/database/src/repository.ts cherche les lignes avec le filtre, n'en retient que les clés primaires, puis écrit sur ces clés. La condition ne franchit jamais la frontière :

packages/core/database/src/repository.ts (v2.2.7)TypeScript
const queryOptions = this.buildQueryOptions({ ...options, fields: [primaryKeyField] });
const rows = await this.find({ ...queryOptions, transaction });
const [result] = await Model.update(values, {
  where: { [primaryKeyField]: rows.map((row) => row.get(primaryKeyField)) },
});

Le journal PostgreSQL le confirme : WHERE status = 'pending' apparaît une seule fois, dans le SELECT.

SQL
SELECT "id","status","note" FROM "public"."orders" AS "orders"
  WHERE "orders"."status" = 'pending' ORDER BY "orders"."id" ASC NULLS LAST;
UPDATE "public"."orders" SET "note"=$1 WHERE "id" = $2;   -- ligne 1
UPDATE "public"."orders" SET "note"=$1 WHERE "id" = $2;   -- ligne 2
COMMIT;

Entre le SELECT et chaque UPDATE, rien ne revérifie rien — et la transaction n'y change rien, puisqu'il n'y a aucune condition à revalider.

Autre écrivainPostgreSQLrepository.update-()ClientAutre écrivainPostgreSQLrepository.update-()Clientla ligne n'est plus « pending »,elle est écrite quand mêmePOSTorders:update?filter={"status":-"pending"}SELECT id WHERE status ='pending'10 001 clés primairesUPDATE orders SETstatus='cancelled' WHEREid=10001UPDATE orders SET note=$1WHERE id = 10001

La mesure

10 001 lignes en status = 'pending', réécriture de note sur toutes, et à la première seconde on bascule une seule ligne — la dernière à être traitée — en cancelled.

Résultat Valeur
Durée de la mise à jour en masse 6,2 à 6,4 s
Ligne sortie de la condition, écrite quand même 5 fois sur 5
Avec un seul UPDATE ... WHERE 10 000 lignes sur 10 001, la bonne exclue
Coût des 10 002 instructions 9 fois plus lent qu'une seule

La fenêtre s'élargit avec le volume : sur 2 002 lignes, l'opération se termine en 1,5 s et l'interruption programmée à 2 s arrive trop tard. Même mécanisme, intervalle plus court — voilà pourquoi une petite table ne montre jamais rien.

Ce que ça change

Ce n'est pas un arbitrage de performance : la lenteur achète les événements de collection, que le nœud SQL d'un workflow ne déclenche pas. La branche individualHooks: false émet bien un seul UPDATE, avec ce commentaire dans la source — « would be more efficient up to 100000 ~ 1000000 rows » — mais elle reste indexée sur les clés primaires, et l'API REST ne peut pas l'atteindre.

En attendant, deux habitudes :

  1. Ne pas traiter « seules les lignes qui remplissent le filtre » comme une garantie. Un filtre désigne une cible ; il ne promet pas qu'elle y soit encore au moment de l'écriture. Validations, allocations, clôtures de période : partout où toucher une ligne devenue hors critère est un vrai problème, il faut autre chose.
  2. Quand l'atomicité prime sur les événements, passer par un nœud SQL dans un workflow.

À retenir

Le signalement est arrivé sur le forum officiel le 4 septembre (t/14097), avec une réponse de l'équipe — « nous enquêtons » — et rien depuis. Le fichier repository.ts a été comparé entre v2.2.7, v2.2.8, v2.2.12, v2.4.0-alpha.4 et v2.4.0-alpha.5 : les cinq sont identiques à l'octet, 1 009 lignes, même empreinte MD5. Au 12 septembre, 2.2.12 est la version courante et 2.4.0-alpha.5 l'alpha.

La mesure ne porte que sur PostgreSQL, mais la condition étant abandonnée dans le code applicatif, la base ne devrait rien y changer. Et la question se pose telle quelle à n'importe quel ORM : est-ce que ma condition arrive jusqu'au WHERE de l'UPDATE ?