[
  {
    "type": "create_table",
    "description": "Spec 051 T1B.2: create userInvites (kiosk_users) — invite identity with invite-scoped hashed tokens (ADR-051-11). uk_current is a NULL-tolerant unique on (userId, typeNum, isCurrent): InnoDB/MariaDB treat each NULL as distinct in a unique index, so at most ONE row per (userId, typeNum) can have isCurrent=1 while any number of superseded rows sit at isCurrent=NULL.",
    "database": "kiosk_users",
    "check_query": "SELECT 1 AS exact_match WHERE EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='userInvites' AND TABLE_TYPE='BASE TABLE' AND ENGINE='InnoDB' AND TABLE_COLLATION='utf8mb4_unicode_ci' AND TABLE_COMMENT='Invite identity - invite-scoped hashed tokens (ADR-051-11) - Spec 051') AND EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='userInvites' HAVING COUNT(*)=14 AND SUM(COLUMN_NAME='inviteId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='int unsigned' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND LOWER(EXTRA)='auto_increment' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='userId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='int unsigned' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='FK to users.id - invitee')=1 AND SUM(COLUMN_NAME='typeNum' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='varchar(10)' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='Store identifier')=1 AND SUM(COLUMN_NAME='invitedByUserId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='int unsigned' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='FK to users.id - actor who sent the invite')=1 AND SUM(COLUMN_NAME='kind' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='enum(''credential'',''storenotify'')' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='status' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='enum(''pending'',''activated'',''expired'',''revoked'',''done'')' AND IS_NULLABLE='NO' AND TRIM(BOTH '\\'' FROM COLUMN_DEFAULT)='pending' AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='tokenHash' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='char(64)' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='SHA-256 hex hash of the invite-scoped token; raw token never stored')=1 AND SUM(COLUMN_NAME='tokenExpiresAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='consumedAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='revokedAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='isCurrent' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='tinyint' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='1 = active invite; NULL on superseded rows (NULL-tolerant uk ignores these)')=1 AND SUM(COLUMN_NAME='batchId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='char(26)' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='ULID grouping a bulk invite-all run')=1 AND SUM(COLUMN_NAME='createdAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='NO' AND LOWER(COLUMN_DEFAULT) IN ('current_timestamp','current_timestamp()') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='activatedAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1) AND (SELECT COALESCE(GROUP_CONCAT(CONCAT(INDEX_NAME,':',NON_UNIQUE,':',cols) ORDER BY INDEX_NAME SEPARATOR ';'),'') FROM (SELECT INDEX_NAME,NON_UNIQUE,GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') cols FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='userInvites' GROUP BY INDEX_NAME,NON_UNIQUE) i)='fk_userInvites_invitedBy:1:invitedByUserId;idx_batchid:1:batchId;idx_typenum_status:1:typeNum,status;PRIMARY:0:inviteId;uk_current:0:userId,typeNum,isCurrent' AND EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON r.CONSTRAINT_SCHEMA=k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME=k.CONSTRAINT_NAME WHERE k.TABLE_SCHEMA='kiosk_users' AND k.TABLE_NAME='userInvites' AND k.REFERENCED_TABLE_NAME IS NOT NULL HAVING COUNT(*)=2 AND SUM(k.CONSTRAINT_NAME='fk_userInvites_user' AND k.COLUMN_NAME='userId' AND k.REFERENCED_TABLE_NAME='users' AND k.REFERENCED_COLUMN_NAME='id' AND r.UPDATE_RULE='RESTRICT' AND r.DELETE_RULE='RESTRICT')=1 AND SUM(k.CONSTRAINT_NAME='fk_userInvites_invitedBy' AND k.COLUMN_NAME='invitedByUserId' AND k.REFERENCED_TABLE_NAME='users' AND k.REFERENCED_COLUMN_NAME='id' AND r.UPDATE_RULE='RESTRICT' AND r.DELETE_RULE='RESTRICT')=1)",
    "sql": "CREATE TABLE `userInvites` (`inviteId` int unsigned NOT NULL AUTO_INCREMENT, `userId` int unsigned NOT NULL COMMENT 'FK to users.id - invitee', `typeNum` varchar(10) NOT NULL COMMENT 'Store identifier', `invitedByUserId` int unsigned NOT NULL COMMENT 'FK to users.id - actor who sent the invite', `kind` enum('credential','storeNotify') NOT NULL, `status` enum('pending','activated','expired','revoked','done') NOT NULL DEFAULT 'pending', `tokenHash` char(64) DEFAULT NULL COMMENT 'SHA-256 hex hash of the invite-scoped token; raw token never stored', `tokenExpiresAt` datetime DEFAULT NULL, `consumedAt` datetime DEFAULT NULL, `revokedAt` datetime DEFAULT NULL, `isCurrent` tinyint(1) DEFAULT NULL COMMENT '1 = active invite; NULL on superseded rows (NULL-tolerant uk ignores these)', `batchId` char(26) DEFAULT NULL COMMENT 'ULID grouping a bulk invite-all run', `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `activatedAt` datetime DEFAULT NULL, PRIMARY KEY (`inviteId`), UNIQUE KEY `uk_current` (`userId`, `typeNum`, `isCurrent`), KEY `idx_typenum_status` (`typeNum`, `status`), KEY `idx_batchid` (`batchId`), CONSTRAINT `fk_userInvites_user` FOREIGN KEY (`userId`) REFERENCES `users` (`id`), CONSTRAINT `fk_userInvites_invitedBy` FOREIGN KEY (`invitedByUserId`) REFERENCES `users` (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Invite identity - invite-scoped hashed tokens (ADR-051-11) - Spec 051'"
  },
  {
    "type": "create_table",
    "description": "Spec 051 T1B.2: create inviteMessages (kiosk_users) — per-channel delivery outbox (transactional-outbox state machine + retries, ADR-051-10). messageRef is an opaque per-message link tag used for winning-channel attribution instead of a tamperable query-string flag. Depends on userInvites existing first (FK).",
    "database": "kiosk_users",
    "check_query": "SELECT 1 AS exact_match WHERE EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='inviteMessages' AND TABLE_TYPE='BASE TABLE' AND ENGINE='InnoDB' AND TABLE_COLLATION='utf8mb4_unicode_ci' AND TABLE_COMMENT='Per-channel invite delivery outbox (state machine + retries) - Spec 051') AND EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='inviteMessages' HAVING COUNT(*)=15 AND SUM(COLUMN_NAME='messageId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='int unsigned' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND LOWER(EXTRA)='auto_increment' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='inviteId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='int unsigned' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='FK to userInvites.inviteId')=1 AND SUM(COLUMN_NAME='channel' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='enum(''email'',''sms'')' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='state' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='enum(''queued'',''sending'',''sent'',''delivered'',''failed'',''bounced'',''dead'')' AND IS_NULLABLE='NO' AND TRIM(BOTH '\\'' FROM COLUMN_DEFAULT)='queued' AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='attemptCount' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='tinyint unsigned' AND IS_NULLABLE='NO' AND TRIM(BOTH '\\'' FROM COLUMN_DEFAULT)='0' AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='nextAttemptAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='lockedAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='errorCode' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='varchar(40)' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='stateReason' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='varchar(255)' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='provider' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='varchar(20)' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='providerMessageId' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='varchar(64)' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='messageRef' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='char(12)' AND IS_NULLABLE='NO' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='Opaque per-message link tag - winning-channel attribution')=1 AND SUM(COLUMN_NAME='sentAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='YES' AND (COLUMN_DEFAULT IS NULL OR UPPER(COLUMN_DEFAULT)='NULL') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='createdAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='NO' AND LOWER(COLUMN_DEFAULT) IN ('current_timestamp','current_timestamp()') AND EXTRA='' AND COLUMN_COMMENT='')=1 AND SUM(COLUMN_NAME='updatedAt' AND CASE WHEN LOWER(COLUMN_TYPE) REGEXP '^(tinyint|int|bigint)[(][0-9]+[)]' THEN CONCAT(SUBSTRING_INDEX(LOWER(COLUMN_TYPE), '(', 1), IF(LOWER(COLUMN_TYPE) LIKE '% unsigned', ' unsigned', '')) ELSE LOWER(COLUMN_TYPE) END='datetime' AND IS_NULLABLE='NO' AND LOWER(COLUMN_DEFAULT) IN ('current_timestamp','current_timestamp()') AND LOWER(EXTRA)='on update current_timestamp()' AND COLUMN_COMMENT='')=1) AND (SELECT COALESCE(GROUP_CONCAT(CONCAT(INDEX_NAME,':',NON_UNIQUE,':',cols) ORDER BY INDEX_NAME SEPARATOR ';'),'') FROM (SELECT INDEX_NAME,NON_UNIQUE,GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') cols FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='inviteMessages' GROUP BY INDEX_NAME,NON_UNIQUE) i)='fk_inviteMessages_invite:1:inviteId;idx_state_next_attempt:1:state,nextAttemptAt;PRIMARY:0:messageId;uk_message_ref:0:messageRef;uk_provider_msg:0:provider,providerMessageId' AND EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON r.CONSTRAINT_SCHEMA=k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME=k.CONSTRAINT_NAME WHERE k.TABLE_SCHEMA='kiosk_users' AND k.TABLE_NAME='inviteMessages' AND k.REFERENCED_TABLE_NAME IS NOT NULL HAVING COUNT(*)=1 AND SUM(k.CONSTRAINT_NAME='fk_inviteMessages_invite' AND k.COLUMN_NAME='inviteId' AND k.REFERENCED_TABLE_NAME='userInvites' AND k.REFERENCED_COLUMN_NAME='inviteId' AND r.UPDATE_RULE='CASCADE' AND r.DELETE_RULE='CASCADE')=1)",
    "sql": "CREATE TABLE `inviteMessages` (`messageId` int unsigned NOT NULL AUTO_INCREMENT, `inviteId` int unsigned NOT NULL COMMENT 'FK to userInvites.inviteId', `channel` enum('email','sms') NOT NULL, `state` enum('queued','sending','sent','delivered','failed','bounced','dead') NOT NULL DEFAULT 'queued', `attemptCount` tinyint unsigned NOT NULL DEFAULT 0, `nextAttemptAt` datetime DEFAULT NULL, `lockedAt` datetime DEFAULT NULL, `errorCode` varchar(40) DEFAULT NULL, `stateReason` varchar(255) DEFAULT NULL, `provider` varchar(20) DEFAULT NULL, `providerMessageId` varchar(64) DEFAULT NULL, `messageRef` char(12) NOT NULL COMMENT 'Opaque per-message link tag - winning-channel attribution', `sentAt` datetime DEFAULT NULL, `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`messageId`), UNIQUE KEY `uk_provider_msg` (`provider`, `providerMessageId`), UNIQUE KEY `uk_message_ref` (`messageRef`), KEY `idx_state_next_attempt` (`state`, `nextAttemptAt`), CONSTRAINT `fk_inviteMessages_invite` FOREIGN KEY (`inviteId`) REFERENCES `userInvites` (`inviteId`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Per-channel invite delivery outbox (state machine + retries) - Spec 051'"
  }
]
