0904
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
-- ============================================================================
|
||||
-- TamaBox 时间列 timestamptz 迁移脚本(线上执行版)
|
||||
-- 前置: 本脚本已在你提供的生产克隆库(nekobox_clone)完整验证通过:
|
||||
-- * 迁移后 epoch 与迁移前逐项一致(差异 0)
|
||||
-- * 回滚路径同样验证无损(可安全回退)
|
||||
-- * 会话时区无关(验证时使用 Asia/Shanghai 会话)
|
||||
-- 实际受影响表(含数据): questions / users / censor_logs / upload_images
|
||||
-- 其余表(conversations/messages/votes/...)时间列本来就是 timestamptz, 脚本会自动跳过
|
||||
-- 覆盖: 只读 + ALTER(DDL), 建议低峰窗口执行; 克隆实测全库 1.7s 完成
|
||||
-- 使用: psql/pgAdmin 整段按 PART 顺序执行, 把输出贴回核对
|
||||
-- 注意: 迁移期间避免应用滚动发布; 执行前确认无长时间运行事务持锁
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 0 前置核对: 列出将被迁移的列(线上应为 4 表 x 3 列 = 12 列)
|
||||
-- ============================================================================
|
||||
SELECT 'PART 0: 将被迁移的无时区时间列' AS step;
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'nekobox'
|
||||
AND data_type = 'timestamp without time zone'
|
||||
AND column_name IN ('created_at','updated_at','deleted_at')
|
||||
ORDER BY table_name, column_name;
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 1 迁移前 epoch 快照(存入 diag_test schema, 供 PART 3 自动比对)
|
||||
-- ============================================================================
|
||||
SELECT 'PART 1: 迁移前快照' AS step;
|
||||
CREATE SCHEMA IF NOT EXISTS diag_test;
|
||||
DROP TABLE IF EXISTS diag_test.snap_before;
|
||||
CREATE TABLE diag_test.snap_before(tbl text, col text, n bigint, minv numeric, maxv numeric, s numeric);
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_name tbl, column_name col
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='nekobox' AND data_type='timestamp without time zone'
|
||||
AND column_name IN ('created_at','updated_at','deleted_at')
|
||||
ORDER BY table_name, column_name
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'INSERT INTO diag_test.snap_before SELECT %L, %L, count(%I), min(extract(epoch FROM %I)*1000000000), max(extract(epoch FROM %I)*1000000000), sum(extract(epoch FROM %I)*1000000000) FROM nekobox.%I',
|
||||
r.tbl, r.col, r.col, r.col, r.col, r.col, r.tbl);
|
||||
END LOOP;
|
||||
END $$;
|
||||
SELECT '快照完成, 共 ' || count(*) || ' 列' AS info FROM diag_test.snap_before;
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 2 执行迁移
|
||||
-- 核心语义: 库存值是 UTC 墙钟, 用 AT TIME ZONE 'UTC' 显式解释为 timestamptz,
|
||||
-- 与会话时区无关。绝不可用裸 ALTER(会按会话时区解释 -> 全部 -8h 偏移)。
|
||||
-- ============================================================================
|
||||
SELECT 'PART 2: 执行迁移' AS step;
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_name tbl, column_name col
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='nekobox' AND data_type='timestamp without time zone'
|
||||
AND column_name IN ('created_at','updated_at','deleted_at')
|
||||
ORDER BY table_name, column_name
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'ALTER TABLE nekobox.%I ALTER COLUMN %I TYPE timestamptz USING %I AT TIME ZONE ''UTC''',
|
||||
r.tbl, r.col, r.col);
|
||||
END LOOP;
|
||||
END $$;
|
||||
SELECT '迁移完成' AS step;
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 3 验收(4 项全绿 = 迁移成功; 任一项不满足立即执行 PART 4 回滚)
|
||||
-- ============================================================================
|
||||
SELECT 'PART 3: 验收' AS step;
|
||||
|
||||
-- 3.1 迁移后快照并自动比对: 差异应为 0 行
|
||||
DROP TABLE IF EXISTS diag_test.snap_after;
|
||||
CREATE TABLE diag_test.snap_after(tbl text, col text, n bigint, minv numeric, maxv numeric, s numeric);
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_name tbl, column_name col
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='nekobox' AND data_type='timestamp with time zone'
|
||||
AND column_name IN ('created_at','updated_at','deleted_at')
|
||||
ORDER BY table_name, column_name
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'INSERT INTO diag_test.snap_after SELECT %L, %L, count(%I), min(extract(epoch FROM %I)*1000000000), max(extract(epoch FROM %I)*1000000000), sum(extract(epoch FROM %I)*1000000000) FROM nekobox.%I',
|
||||
r.tbl, r.col, r.col, r.col, r.col, r.col, r.tbl);
|
||||
END LOOP;
|
||||
END $$;
|
||||
SELECT '3.1 迁移前后 epoch 差异(应为 0 行)' AS check_name;
|
||||
SELECT b.tbl, b.col, b.n AS n_before, a.n AS n_after,
|
||||
b.minv AS min_before, a.minv AS min_after,
|
||||
b.maxv AS max_before, a.maxv AS max_after,
|
||||
b.s AS sum_before, a.s AS sum_after
|
||||
FROM diag_test.snap_before b
|
||||
JOIN diag_test.snap_after a USING (tbl, col)
|
||||
WHERE b.n IS DISTINCT FROM a.n OR b.minv IS DISTINCT FROM a.minv
|
||||
OR b.maxv IS DISTINCT FROM a.maxv OR b.s IS DISTINCT FROM a.s;
|
||||
|
||||
-- 3.2 残余无时区时间列(应为 0)
|
||||
SELECT '3.2 残余 timestamp without time zone(应 0)' AS check_name;
|
||||
SELECT count(*) AS remain_without
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='nekobox' AND data_type='timestamp without time zone'
|
||||
AND column_name IN ('created_at','updated_at','deleted_at');
|
||||
|
||||
-- 3.3 关键行 epoch 指纹(应与迁移前/线上游标一致)
|
||||
SELECT '3.3 关键行 epoch 指纹' AS check_name;
|
||||
SELECT id, (extract(epoch FROM created_at)*1000000000)::numeric(30,0) AS epoch_ns
|
||||
FROM nekobox.questions WHERE id IN (3109, 3089, 2411) ORDER BY id;
|
||||
|
||||
-- 3.4 keyset 翻页抽查: 应精确返回 3088,2883,...,2411 共 18 条(与修复后线上第2页一致)
|
||||
SELECT '3.4 keyset 翻页抽查(第2页)' AS check_name;
|
||||
SELECT q.id
|
||||
FROM nekobox.questions q
|
||||
WHERE q.user_id = (SELECT id FROM nekobox.users WHERE domain='aoi')
|
||||
AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false
|
||||
AND q.is_pinned = false
|
||||
AND (q.created_at < TIMESTAMPTZ '2026-09-05 00:01:16.678885+00'
|
||||
OR (q.created_at = TIMESTAMPTZ '2026-09-05 00:01:16.678885+00' AND q.id < 3089))
|
||||
ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC;
|
||||
|
||||
-- ============================================================================
|
||||
-- PART 4 回滚(仅在 PART 3 验收不通过或线上出现异常时执行)
|
||||
-- 还原为 timestamp, 与迁移前快照一致(已在克隆库验证无损)
|
||||
-- ============================================================================
|
||||
-- SELECT 'PART 4: 回滚(按需执行)' AS step;
|
||||
-- DO $$
|
||||
-- DECLARE r record;
|
||||
-- BEGIN
|
||||
-- FOR r IN
|
||||
-- SELECT table_name tbl, column_name col
|
||||
-- FROM information_schema.columns
|
||||
-- WHERE table_schema='nekobox' AND data_type='timestamp with time zone'
|
||||
-- AND column_name IN ('created_at','updated_at','deleted_at')
|
||||
-- AND table_name IN ('questions','users','censor_logs','upload_images')
|
||||
-- ORDER BY table_name, column_name
|
||||
-- LOOP
|
||||
-- EXECUTE format(
|
||||
-- 'ALTER TABLE nekobox.%I ALTER COLUMN %I TYPE timestamp USING %I AT TIME ZONE ''UTC''',
|
||||
-- r.tbl, r.col, r.col);
|
||||
-- END LOOP;
|
||||
-- END $$;
|
||||
-- SELECT '回滚完成(可再次跑 PART 3 核验)' AS step;
|
||||
-- ============================================================================
|
||||
-- 验收说明:
|
||||
-- 3.1 = 0 行且 3.2 = 0、3.3 指纹与迁移前一致、3.4 翻出 18 条 -> 迁移成功
|
||||
-- 任何一项不符 -> 执行 PART 4 回滚后停止排查, 联系开发
|
||||
-- 迁移后建议:
|
||||
-- 1) 重新部署应用(含 parsePinCursor 的 .UTC() 修复版本)
|
||||
-- 2) 复跑今日翻页验收脚本, 预期 38 条唯一收敛
|
||||
-- 3) 确认无误后可清理 diag_test schema: DROP SCHEMA diag_test;
|
||||
-- ============================================================================
|
||||
Reference in New Issue
Block a user