-- sp_generic_shift_dates:按指定时间窗口平移库内日期类字段
|
-- 修复点:
|
-- 1) 唯一索引包含日期列的表(如 hrm_attendance_record.uk_user_date)改为整表统一平移,
|
-- 常量偏移不会破坏唯一键,避免 1062 Duplicate entry
|
-- 2) 单表执行失败不再中断整轮,错误记入预览表后继续处理后续表
|
|
DROP PROCEDURE IF EXISTS `sp_generic_shift_dates`;
|
|
DELIMITER $$
|
|
CREATE DEFINER = `root`@`%` PROCEDURE `sp_generic_shift_dates`(
|
IN p_schema VARCHAR(64),
|
IN p_start DATETIME,
|
IN p_end DATETIME,
|
IN p_execute BOOLEAN,
|
IN p_exclude_regex VARCHAR(1000),
|
IN p_include_regex VARCHAR(1000)
|
)
|
BEGIN
|
DECLARE v_done BOOL DEFAULT FALSE;
|
DECLARE v_table VARCHAR(64);
|
DECLARE v_pk VARCHAR(64);
|
DECLARE v_pk_count INT;
|
DECLARE v_cols LONGTEXT;
|
DECLARE v_anchor VARCHAR(64);
|
DECLARE v_sql LONGTEXT;
|
DECLARE v_seconds BIGINT;
|
DECLARE v_count BIGINT DEFAULT 0;
|
DECLARE v_tables BIGINT DEFAULT 0;
|
DECLARE v_fields INT;
|
DECLARE v_uniq_hit INT;
|
DECLARE v_uniq_cols VARCHAR(1000);
|
DECLARE v_min_expr LONGTEXT;
|
DECLARE v_max_expr LONGTEXT;
|
DECLARE v_regex VARCHAR(1000);
|
DECLARE v_err TEXT;
|
DECLARE cur CURSOR FOR
|
SELECT t.TABLE_NAME
|
FROM information_schema.TABLES t
|
WHERE t.TABLE_SCHEMA = p_schema
|
AND t.TABLE_TYPE = 'BASE TABLE'
|
AND (p_exclude_regex IS NULL OR TRIM(p_exclude_regex) = ''
|
OR NOT REGEXP_LIKE(t.TABLE_NAME, p_exclude_regex, 'i'))
|
ORDER BY t.TABLE_NAME;
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
|
IF p_schema IS NULL OR p_schema = '' OR p_schema IN ('mysql','information_schema','performance_schema','sys') THEN
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid or system schema';
|
END IF;
|
IF p_start IS NULL OR p_end IS NULL OR p_end < p_start THEN
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid date range';
|
END IF;
|
IF NOT EXISTS (SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = p_schema) THEN
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Schema not found';
|
END IF;
|
|
-- 列清单可能很长,避免 GROUP_CONCAT 被默认 1024 字节截断
|
SET SESSION group_concat_max_len = 1048576;
|
|
SET v_seconds = TIMESTAMPDIFF(SECOND, p_start, p_end);
|
-- 空串不是 NULL,必须显式归一化:MySQL 8 的 REGEXP_LIKE 遇到空模式会抛 3685
|
IF p_include_regex IS NULL OR TRIM(p_include_regex) = '' THEN
|
SET v_regex = '(^|_)(create|created|update|updated|modify|modified|order|production|produce|receipt|receive|delivery|ship|stock|inbound|outbound|start|end|finish|complete|plan|actual|inspect|check|approve|audit|pay|payment|due|expire|expiry|effective|business|biz|date|time|timestamp)(_|$)';
|
ELSE
|
SET v_regex = p_include_regex;
|
END IF;
|
|
DROP TEMPORARY TABLE IF EXISTS tmp_date_shift_preview;
|
CREATE TEMPORARY TABLE tmp_date_shift_preview (
|
table_name VARCHAR(64),
|
anchor_column VARCHAR(64),
|
shift_mode VARCHAR(16),
|
field_count INT,
|
affected_rows BIGINT,
|
statement_text LONGTEXT
|
);
|
|
OPEN cur;
|
loop_tables: LOOP
|
FETCH cur INTO v_table;
|
IF v_done THEN LEAVE loop_tables; END IF;
|
|
SELECT COUNT(*), MAX(k.COLUMN_NAME) INTO v_pk_count, v_pk
|
FROM information_schema.KEY_COLUMN_USAGE k
|
WHERE k.TABLE_SCHEMA = p_schema AND k.TABLE_NAME = v_table AND k.CONSTRAINT_NAME = 'PRIMARY';
|
IF v_pk_count <> 1 THEN ITERATE loop_tables; END IF;
|
|
SELECT COUNT(*) INTO v_fields
|
FROM information_schema.COLUMNS c
|
WHERE c.TABLE_SCHEMA = p_schema AND c.TABLE_NAME = v_table
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i');
|
IF v_fields = 0 THEN ITERATE loop_tables; END IF;
|
|
SELECT c.COLUMN_NAME INTO v_anchor
|
FROM information_schema.COLUMNS c
|
WHERE c.TABLE_SCHEMA = p_schema AND c.TABLE_NAME = v_table
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i')
|
ORDER BY CASE WHEN c.COLUMN_NAME IN ('create_time','created_at','creation_time','created_time') THEN 0 ELSE 1 END,
|
c.ORDINAL_POSITION
|
LIMIT 1;
|
|
-- 待平移的日期列中是否有落在唯一索引上的
|
SELECT COUNT(*) INTO v_uniq_hit
|
FROM information_schema.STATISTICS s
|
JOIN information_schema.COLUMNS c
|
ON c.TABLE_SCHEMA = s.TABLE_SCHEMA
|
AND c.TABLE_NAME = s.TABLE_NAME
|
AND c.COLUMN_NAME = s.COLUMN_NAME
|
WHERE s.TABLE_SCHEMA = p_schema AND s.TABLE_NAME = v_table AND s.NON_UNIQUE = 0
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i');
|
|
SET @tbl = CONCAT('`', REPLACE(p_schema,'`','``'), '`.`', REPLACE(v_table,'`','``'), '`');
|
SET @anchor = CONCAT('`', REPLACE(v_anchor,'`','``'), '`');
|
|
inner_block: BEGIN
|
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
BEGIN
|
GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
|
INSERT INTO tmp_date_shift_preview
|
VALUES (v_table, v_anchor, 'failed', v_fields, 0, CONCAT('[error] ', v_err));
|
END;
|
|
IF v_uniq_hit > 0 THEN
|
-- 统一平移:整表使用同一偏移,保留唯一键。
|
-- 偏移量按所有待平移列的整体跨度计算,保证每一列都落在目标窗口内。
|
-- COALESCE 保护:整列为 NULL 时 MIN/MAX 返回 NULL,会让 LEAST/GREATEST 整体变 NULL
|
SELECT GROUP_CONCAT(CONCAT('COALESCE(MIN(`', REPLACE(c.COLUMN_NAME,'`','``'), '`), ''9999-12-31 23:59:59'')')
|
ORDER BY c.ORDINAL_POSITION SEPARATOR ', '),
|
GROUP_CONCAT(CONCAT('COALESCE(MAX(`', REPLACE(c.COLUMN_NAME,'`','``'), '`), ''1000-01-01 00:00:00'')')
|
ORDER BY c.ORDINAL_POSITION SEPARATOR ', ')
|
INTO v_min_expr, v_max_expr
|
FROM information_schema.COLUMNS c
|
WHERE c.TABLE_SCHEMA = p_schema AND c.TABLE_NAME = v_table
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i');
|
|
SET @sql = CONCAT('SELECT LEAST(', v_min_expr, '), GREATEST(', v_max_expr,
|
') INTO @min_all, @max_all FROM ', @tbl);
|
PREPARE stmt FROM @sql;
|
EXECUTE stmt;
|
DEALLOCATE PREPARE stmt;
|
|
SET @span_days = TIMESTAMPDIFF(DAY, DATE(@min_all), DATE(@max_all));
|
SET @range_days = TIMESTAMPDIFF(DAY, DATE(p_start), DATE(p_end));
|
|
IF @min_all IS NULL OR @min_all >= '9999-01-01' THEN
|
INSERT INTO tmp_date_shift_preview
|
VALUES (v_table, v_anchor, 'skipped', v_fields, 0, 'no non-null date value');
|
ELSEIF @span_days > @range_days THEN
|
INSERT INTO tmp_date_shift_preview
|
VALUES (v_table, v_anchor, 'skipped', v_fields, 0,
|
CONCAT('date span ', @span_days, ' days exceeds target range ', @range_days, ' days'));
|
ELSE
|
-- 按整日平移,保留时分秒与相对顺序
|
SET @shift_delta = (TIMESTAMPDIFF(DAY, DATE(@min_all), DATE(p_start))
|
+ FLOOR(RAND() * (@range_days - @span_days + 1))) * 86400;
|
-- 随机到 0 天等于没改,强制至少平移 1 天
|
IF @shift_delta = 0 AND @span_days < @range_days THEN
|
SET @shift_delta = 86400;
|
END IF;
|
|
SELECT GROUP_CONCAT(
|
CONCAT('`', REPLACE(c.COLUMN_NAME,'`','``'), '` = DATE_ADD(`',
|
REPLACE(c.COLUMN_NAME,'`','``'), '`, INTERVAL @shift_delta SECOND)')
|
ORDER BY c.ORDINAL_POSITION SEPARATOR ', ')
|
INTO v_cols
|
FROM information_schema.COLUMNS c
|
WHERE c.TABLE_SCHEMA = p_schema AND c.TABLE_NAME = v_table
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i');
|
|
-- InnoDB 逐行更新唯一键会瞬时冲突,按偏移方向决定扫描顺序:
|
-- 正偏移先动较大的值,负偏移先动较小的值
|
SELECT GROUP_CONCAT(CONCAT('`', REPLACE(d.COLUMN_NAME,'`','``'), '`')
|
ORDER BY d.ORDINAL_POSITION SEPARATOR ', ')
|
INTO v_uniq_cols
|
FROM (
|
SELECT DISTINCT c.COLUMN_NAME, c.ORDINAL_POSITION
|
FROM information_schema.STATISTICS s
|
JOIN information_schema.COLUMNS c
|
ON c.TABLE_SCHEMA = s.TABLE_SCHEMA
|
AND c.TABLE_NAME = s.TABLE_NAME
|
AND c.COLUMN_NAME = s.COLUMN_NAME
|
WHERE s.TABLE_SCHEMA = p_schema AND s.TABLE_NAME = v_table AND s.NON_UNIQUE = 0
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i')
|
) d;
|
|
SET @update_sql = CONCAT('UPDATE ', @tbl, ' SET ', v_cols,
|
' ORDER BY ', v_uniq_cols, ' ',
|
IF(@shift_delta >= 0, 'DESC', 'ASC'));
|
SET @count_sql = CONCAT('SELECT COUNT(*) INTO @affected FROM ', @tbl);
|
PREPARE stmt FROM @count_sql;
|
EXECUTE stmt;
|
DEALLOCATE PREPARE stmt;
|
|
IF p_execute THEN
|
PREPARE stmt FROM @update_sql;
|
EXECUTE stmt;
|
DEALLOCATE PREPARE stmt;
|
END IF;
|
|
SET v_count = v_count + @affected;
|
INSERT INTO tmp_date_shift_preview
|
VALUES (v_table, v_anchor, IF(p_execute, 'uniform-executed', 'uniform-preview'),
|
v_fields, @affected, @update_sql);
|
SET v_tables = v_tables + 1;
|
END IF;
|
ELSE
|
-- 逐行随机:每行一个随机目标时间,以锚点整体平移
|
SELECT GROUP_CONCAT(
|
CONCAT('`', REPLACE(c.COLUMN_NAME,'`','``'), '` = CASE WHEN `',
|
REPLACE(c.COLUMN_NAME,'`','``'), '` IS NULL THEN NULL ELSE ',
|
CASE WHEN c.DATA_TYPE = 'date' THEN 'DATE(' ELSE '' END,
|
'DATE_ADD(`', REPLACE(c.COLUMN_NAME,'`','``'), '`, INTERVAL TIMESTAMPDIFF(SECOND, `',
|
REPLACE(v_anchor,'`','``'), '`, __new_anchor) SECOND)',
|
CASE WHEN c.DATA_TYPE = 'date' THEN ')' ELSE '' END, ' END')
|
ORDER BY c.ORDINAL_POSITION SEPARATOR ', ')
|
INTO v_cols
|
FROM information_schema.COLUMNS c
|
WHERE c.TABLE_SCHEMA = p_schema AND c.TABLE_NAME = v_table
|
AND c.DATA_TYPE IN ('date','datetime','timestamp')
|
AND c.EXTRA NOT LIKE '%VIRTUAL GENERATED%' AND c.EXTRA NOT LIKE '%STORED GENERATED%'
|
AND c.EXTRA NOT LIKE '%on update%'
|
AND REGEXP_LIKE(c.COLUMN_NAME, v_regex, 'i');
|
|
DROP TEMPORARY TABLE IF EXISTS tmp_shift_row_dates;
|
SET @sql = CONCAT('CREATE TEMPORARY TABLE tmp_shift_row_dates AS SELECT `',
|
REPLACE(v_pk,'`','``'), '` AS __pk, DATE_ADD(?, INTERVAL FLOOR(RAND() * ',
|
v_seconds + 1, ') SECOND) AS __new_anchor FROM ', @tbl,
|
' WHERE ', @anchor, ' IS NOT NULL');
|
|
IF p_execute THEN
|
PREPARE stmt FROM @sql;
|
SET @start_date = p_start;
|
EXECUTE stmt USING @start_date;
|
DEALLOCATE PREPARE stmt;
|
|
SET @update_sql = CONCAT('UPDATE ', @tbl, ' t JOIN tmp_shift_row_dates s ON t.`',
|
REPLACE(v_pk,'`','``'), '` = s.__pk SET ',
|
REPLACE(v_cols, '__new_anchor', 's.__new_anchor'));
|
SET @count_sql = 'SELECT COUNT(*) INTO @affected FROM tmp_shift_row_dates';
|
PREPARE stmt FROM @count_sql;
|
EXECUTE stmt;
|
DEALLOCATE PREPARE stmt;
|
|
PREPARE stmt FROM @update_sql;
|
EXECUTE stmt;
|
DEALLOCATE PREPARE stmt;
|
|
SET v_count = v_count + @affected;
|
INSERT INTO tmp_date_shift_preview
|
VALUES (v_table, v_anchor, 'random-executed', v_fields, @affected, @update_sql);
|
DROP TEMPORARY TABLE tmp_shift_row_dates;
|
ELSE
|
SET @count_sql = CONCAT('SELECT COUNT(*) INTO @affected FROM ', @tbl, ' WHERE ', @anchor, ' IS NOT NULL');
|
PREPARE stmt FROM @count_sql;
|
EXECUTE stmt;
|
DEALLOCATE PREPARE stmt;
|
INSERT INTO tmp_date_shift_preview
|
VALUES (v_table, v_anchor, 'random-preview', v_fields, @affected,
|
CONCAT('Will shift ', v_fields, ' date columns per row; anchor=', v_anchor));
|
END IF;
|
SET v_tables = v_tables + 1;
|
END IF;
|
END inner_block;
|
END LOOP;
|
CLOSE cur;
|
|
SELECT * FROM tmp_date_shift_preview ORDER BY shift_mode, table_name;
|
SELECT p_schema AS database_name, p_execute AS executed,
|
v_tables AS tables_processed, v_count AS rows_targeted;
|
END$$
|
|
DELIMITER ;
|