Bug #120702 Multi-table UPDATE with trigger can leak `Field::m_is_tmp_null` and cause false `ER_BAD_NULL_ERROR` on later statement
Submitted: 16 Jun 12:34 Modified: 16 Jun 14:04
Reporter: Huaxiong Song (OCA) Email Updates:
Status: Duplicate Impact on me:
None 
Category:MySQL Server Severity:S3 (Non-critical)
Version:9.7/8.4/8.0 OS:Any
Assigned to: CPU Architecture:Any

[16 Jun 12:34] Huaxiong Song
Description:
A multi-table `UPDATE` that writes a `NULL` expression into a `NOT NULL` column can leave `Field::m_is_tmp_null` set on the target table's reusable `Field` object when the target table has a trigger.

The first `UPDATE` correctly fails with:
ERROR 1048 (23000): Column 'val' cannot be null

However, after that failure, later UPDATE or INSERT statements that explicitly provide a non-NULL value for the same column can still fail with the same error:
ERROR 1048 (23000): Column 'val' cannot be null

The issue appears to be caused by temporary nullability state leaking from a failed multi-table UPDATE delayed update path into the table cache.

This appears to affect the multi-table UPDATE delayed update path when all of the following are true:

1. The UPDATE uses the delayed update path for the target table.
2. The target table has a trigger. The trigger does not have to be an UPDATE trigger; a DELETE trigger is enough.
3. The UPDATE expression evaluates to NULL for a NOT NULL column.

How to repeat:
# Run the MTR with the case

CREATE TABLE main (
  id int PRIMARY KEY,
  val decimal(18,2) NOT NULL,
  status int NOT NULL
);

CREATE TABLE item (
  id int PRIMARY KEY,
  main_id int NOT NULL,
  qty int NOT NULL
);

INSERT INTO main VALUES (1, 10.00, 0);
INSERT INTO item VALUES (1, 1, 2);
DELIMITER |;
CREATE TRIGGER bd_main BEFORE DELETE ON main
FOR EACH ROW
BEGIN
  SET @dummy = OLD.id;
END|
DELIMITER ;|

#First error is expected: NULL * i.qty is NULL, and val is NOT NULL.
--error ER_BAD_NULL_ERROR
UPDATE item i STRAIGHT_JOIN main m ON m.id = i.main_id
SET m.val = m.val + NULL * i.qty
WHERE m.id = 1;

# Updating another column succeeds because val is not checked here.
UPDATE main SET status = 1 WHERE id = 1;

# BUG!!!
# These should succeed because val is explicitly non-NULL. However, they currently fail if the stale tmp-null flag leaked.
--error ER_BAD_NULL_ERROR
UPDATE main SET val = 20.00 WHERE id = 1;
--error ER_BAD_NULL_ERROR
INSERT INTO main VALUES (2, 10.00, 0);
--error ER_BAD_NULL_ERROR
INSERT INTO main VALUES (3, 10.00, 0);
SELECT * FROM main ORDER BY id;

DROP TABLE main, item;

Suggested fix:
A cause analysis:
=================

In the multi-table UPDATE delayed update path, MySQL stores updated values in a temporary table and later copies them back to the real target table.

When the target table has a trigger, the temporary fields are made temporarily nullable. If the updated expression evaluates to NULL, the temporary field gets marked with m_is_tmp_null=true.

During delayed update, Copy_field::invoke_do_copy() copies this tmp-null state from the temporary field to the real target table field. After that, disable_fields_temporary_nullability() only clears temporary nullability, but does not clear m_is_tmp_null.
So before constraint checking, the real target field is in this state:
m_is_tmp_nullable = false
m_is_tmp_null = true
The first ER_BAD_NULL_ERROR is correct.

The problem is that after this error, m_is_tmp_null remains on the reusable Field object. Later statements may reuse the same polluted Field; since set_notnull() does not clear m_is_tmp_null when m_is_tmp_nullable=false, later non-NULL UPDATE/INSERT statements can still fail with a false ER_BAD_NULL_ERROR.

Suggested fix:
==============
The first failing UPDATE should still report ER_BAD_NULL_ERROR. The fix should ensure that temporary NULL state is cleared after constraint checking or during statement cleanup, before the TABLE / Field object can be reused from the table cache.
[16 Jun 14:04] Roy Lyseng
Thank you for the bug reort.
Verified as a duplicate of bug#110962.
A fix for the problem will be delivered in the next release.