[
  {
    "type": "create_table",
    "description": "Punch audit F-01 (rev 4): archive table for rows removed by the dedupe below — hard-deleting payroll punches without a recovery path is not acceptable. Column-clone of scheduleTimePunches (CTAS, no indexes — the same punchId may be archived across reruns) plus archivedAt. Rebuild a row if ever needed: INSERT INTO scheduleTimePunches (cols…) SELECT cols… FROM scheduleTimePunchesDedupeArchive WHERE punchId = :id.",
    "database": "{{store}}",
    "check_query": "SHOW TABLES LIKE 'scheduleTimePunchesDedupeArchive'",
    "sql": "CREATE TABLE IF NOT EXISTS `scheduleTimePunchesDedupeArchive` AS SELECT t.*, UTC_TIMESTAMP() AS archivedAt FROM scheduleTimePunches t WHERE 1 = 0"
  },
  {
    "type": "multi_sql",
    "description": "Punch audit F-01 (rev 7): ATOMICALLY archive-and-delete exact-duplicate punch rows in one transaction, scoped to a TRANSACTION-LOCAL doomed set. Statement 3 captures AND X-locks the doomed punchIds in one atomic statement (INSERT IGNORE ... SELECT ... FOR UPDATE into a temporary table — verified on MariaDB 12; temp-table DDL does not break the transaction). Statements 4-5 archive then delete ONLY the ids in that captured set — the DELETE never consults the historical archive, so a previously-archived punchId that becomes doomed again mid-transaction (concurrent twin insert under READ COMMITTED) cannot be deleted against a stale archive entry; it waits for the next convergent pass to be re-captured, re-locked, and freshly archived. Doomed = same employeeId + punchType + punchTime; an ACTIVE row (deleted_at IS NULL) outranks a soft-deleted one; within the same active-status the lowest punchId (original insert) survives. CONVERGENT via always_check: re-runs every conductor run until the unique index exists, so a failed ALTER can never wedge the install behind the operation log.",
    "database": "{{store}}",
    "always_check": true,
    "check_query": "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'scheduleTimePunches' AND INDEX_NAME = 'uq_employee_type_time'",
    "check_expects": "empty",
    "sqls": [
      "DROP TEMPORARY TABLE IF EXISTS tmpPunchDedupeDoomed",
      "CREATE TEMPORARY TABLE tmpPunchDedupeDoomed (punchId INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=InnoDB",
      "INSERT IGNORE INTO tmpPunchDedupeDoomed SELECT t1.punchId FROM scheduleTimePunches t1 INNER JOIN scheduleTimePunches t2 ON t2.employeeId = t1.employeeId AND t2.punchType = t1.punchType AND t2.punchTime = t1.punchTime AND t2.punchId <> t1.punchId AND ( (t2.deleted_at IS NULL AND t1.deleted_at IS NOT NULL) OR ((t2.deleted_at IS NULL) = (t1.deleted_at IS NULL) AND t2.punchId < t1.punchId) ) FOR UPDATE",
      "INSERT INTO scheduleTimePunchesDedupeArchive SELECT t1.*, UTC_TIMESTAMP() FROM scheduleTimePunches t1 INNER JOIN tmpPunchDedupeDoomed d ON d.punchId = t1.punchId",
      "DELETE t1 FROM scheduleTimePunches t1 INNER JOIN tmpPunchDedupeDoomed d ON d.punchId = t1.punchId",
      "DROP TEMPORARY TABLE IF EXISTS tmpPunchDedupeDoomed"
    ]
  },
  {
    "type": "alter_table",
    "description": "Punch audit F-01: add dedupe unique key so an exact double-submit (same employee, same punch type, same second) becomes a duplicate-key error instead of a silent duplicate punch. deleted_at is intentionally NOT part of the key — a NULL column inside a composite unique key disables the constraint for every active row in MySQL. Soft-deleted rows therefore still occupy their slot; an exact-same-second recreate of a deleted punch is rejected, which is the safe behavior.",
    "database": "{{store}}",
    "check_query": "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'scheduleTimePunches' AND INDEX_NAME = 'uq_employee_type_time'",
    "sql": "ALTER TABLE `scheduleTimePunches` ADD UNIQUE KEY `uq_employee_type_time` (`employeeId`, `punchType`, `punchTime`)"
  }
]
