A TIMESTAMP column has a default you did not ask for

shipped_at TIMESTAMP in a migration and the column that ends up in the database are not the same column. MySQL applies two implicit rules to the first TIMESTAMP in a table — a default of CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP — so a column meant to record one event quietly records the most recent write to the row.

CREATE TABLE orders (
  id         INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  placed_at  TIMESTAMP,
  shipped_at TIMESTAMP
) ENGINE=InnoDB;

SHOW CREATE TABLE ordersG
--  `placed_at`  timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
--                 ON UPDATE CURRENT_TIMESTAMP,
--  `shipped_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',

-- say what you mean, on every TIMESTAMP column in the table
  placed_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  shipped_at TIMESTAMP NULL     DEFAULT NULL,

The second TIMESTAMP gets the other surprise: NOT NULL with a zero default, which is not a valid date and which strict mode will refuse later. Both behaviours are historical, and 5.6 finally allows turning them off with explicit_defaults_for_timestamp=1 — but it is off by default and enabling it changes the meaning of migrations already written, so it is a decision for a new database rather than a fix for a running one. The dependable habit is to write an explicit default, or NULL DEFAULT NULL, on every TIMESTAMP column and never rely on position. DATETIME has none of this, and since 5.6 it accepts DEFAULT CURRENT_TIMESTAMP too — which removes the last reason to use TIMESTAMP for anything except the four bytes and the automatic UTC conversion.