Files
railwaysystem/RuoYi-Vue/sql/train/v2/000_preflight.sql
T

438 lines
24 KiB
SQL
Raw Normal View History

-- ==============================================================================
-- RailwaySystem 数据库 V2 迁移前置检查: 000_preflight.sql
-- 适用数据库: KingbaseES V009R003C018 (schema: public)
-- 执行目标: 严格只读核验当前数据库完全符合 Phase 0.5 精确结构骨架与数据基线
-- 设计依据: rollback/000_phase05_exact_schema.sql 与 13_live_database_snapshot.md
-- 验收机制:
-- 1. 纯只读探测,零写入,零结构修改
-- 2. 基于预期元数据集合 (VALUES CTE) 进行全集合差异断言,杜绝数量对冲假象
-- 3. 精确校验: 14旧业务表PK + 24旧业务FK + 7组业务UNIQUE + 6个identity主键 + 9个触发器
-- 4. 任何 missing / unexpected / definition mismatch 立即抛出 EXCEPTION 中止后续迁移
-- ==============================================================================
DO $$
DECLARE
v_schema TEXT;
v_cnt INT;
v_diff_text TEXT := '';
r RECORD;
BEGIN
-- ==========================================================================
-- 1. 会话 Schema 校验 (必须显式为 public)
-- ==========================================================================
SELECT current_schema() INTO v_schema;
IF v_schema <> 'public' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] 当前会话 schema 为 "%",必须显式为 "public"', v_schema;
END IF;
-- ==========================================================================
-- 2. 14 张 Phase 0.5 历史基线表双向集合差异检查 (精确存在且仅存在这 14 张 train_* 表)
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_tables(table_name) AS (
VALUES
('train_teacher'), ('train_student'), ('train_course'), ('train_class_plan'),
('train_dormitory_info'), ('train_dorm_auth'), ('train_classroom_info'), ('train_training_site'),
('train_class_report'), ('train_electronic_board'), ('train_student_checkin'), ('train_score_info'),
('train_study_duration'), ('train_student_portrait')
),
actual_tables AS (
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name LIKE 'train_%'
)
SELECT 'MISSING_TABLE' AS diff_type, e.table_name
FROM expected_tables e
LEFT JOIN actual_tables a ON e.table_name = a.table_name
WHERE a.table_name IS NULL
UNION ALL
SELECT 'UNEXPECTED_TABLE' AS diff_type, a.table_name
FROM actual_tables a
LEFT JOIN expected_tables e ON a.table_name = e.table_name
WHERE e.table_name IS NULL
) LOOP
v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '; ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 历史基线表集合全等校验失败: %', v_diff_text;
END IF;
-- ==========================================================================
-- 3. 检查 8 张 V2 新增表均不得存在 (杜绝半迁移脏环境)
-- ==========================================================================
v_diff_text := '';
FOR r IN (
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN (
'train_resource', 'train_course_resource', 'train_plan_course', 'train_enrollment',
'train_lesson', 'train_accommodation', 'train_score_item', 'train_student_score'
)
) LOOP
v_diff_text := v_diff_text || r.table_name || ', ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] 检测到已存在 V2 新表,当前非纯净 Phase 0.5 基线: %', v_diff_text;
END IF;
-- ==========================================================================
-- 4. 数据行数基线: train_teacher 精确 1 行,其余 13 张旧表精确 0 行
-- ==========================================================================
SELECT count(*) INTO v_cnt FROM train_teacher;
IF v_cnt <> 1 THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] train_teacher 行数期望为 1,实际为 %', v_cnt;
END IF;
SELECT count(*) INTO v_cnt FROM train_student;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_student 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_course;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_course 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_class_plan;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_plan 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_dormitory_info;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_dormitory_info 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_dorm_auth;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_dorm_auth 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_classroom_info;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_classroom_info 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_training_site;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_training_site 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_class_report;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_report 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_electronic_board;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_electronic_board 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_student_checkin;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_student_checkin 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_score_info;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_score_info 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_study_duration;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_study_duration 非空,包含 % 行', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM train_student_portrait;
IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_student_portrait 非空,包含 % 行', v_cnt; END IF;
-- ==========================================================================
-- ==========================================================================
-- 5. 14 个 Phase 0.5 旧业务主键 (PK) 精确骨架校验
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_pk(table_name, constraint_name, column_name) AS (
VALUES
('train_class_plan', 'train_class_plan_pkey', 'plan_id'),
('train_class_report', 'train_class_report_pkey', 'report_id'),
('train_classroom_info', 'train_classroom_info_pkey', 'class_room_id'),
('train_course', 'train_course_pkey', 'c_id'),
('train_dorm_auth', 'train_dorm_auth_pkey', 'auth_id'),
('train_dormitory_info', 'train_dormitory_info_pkey', 'dorm_id'),
('train_electronic_board', 'train_electronic_board_pkey', 'board_id'),
('train_score_info', 'train_score_info_pkey', 'score_id'),
('train_student', 'train_student_pkey', 's_id'),
('train_student_checkin', 'train_student_checkin_pkey', 'checkin_id'),
('train_student_portrait', 'train_student_portrait_pkey', 'portrait_id'),
('train_study_duration', 'train_study_duration_pkey', 'dur_id'),
('train_teacher', 'train_teacher_pkey', 't_id'),
('train_training_site', 'train_training_site_pkey', 'site_id')
),
actual_pk AS (
SELECT tc.table_name, tc.constraint_name, kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = 'public'
AND tc.table_name LIKE 'train_%'
)
SELECT 'MISSING_PK' AS diff_type, e.table_name, e.constraint_name, e.column_name
FROM expected_pk e
LEFT JOIN actual_pk a
ON e.table_name = a.table_name AND e.constraint_name = a.constraint_name AND e.column_name = a.column_name
WHERE a.table_name IS NULL
UNION ALL
SELECT 'UNEXPECTED_PK' AS diff_type, a.table_name, a.constraint_name, a.column_name
FROM actual_pk a
LEFT JOIN expected_pk e
ON a.table_name = e.table_name AND a.constraint_name = e.constraint_name AND a.column_name = e.column_name
WHERE e.table_name IS NULL
) LOOP
v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || '(' || r.column_name || '); ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 主键 (PK) 骨架不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 6. 6 个 Phase 0.5 Identity 自增主键精确校验
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_identity(table_name, column_name) AS (
VALUES
('train_class_report', 'report_id'),
('train_dorm_auth', 'auth_id'),
('train_score_info', 'score_id'),
('train_student_checkin', 'checkin_id'),
('train_student_portrait', 'portrait_id'),
('train_study_duration', 'dur_id')
),
actual_identity AS (
SELECT c.relname AS table_name, a.attname AS column_name
FROM sys_class c
JOIN sys_attribute a ON a.attrelid = c.oid
JOIN sys_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relname LIKE 'train_%'
AND a.attidentity = 'd'
)
SELECT 'MISSING_IDENTITY' AS diff_type, e.table_name, e.column_name
FROM expected_identity e
LEFT JOIN actual_identity a
ON e.table_name = a.table_name AND e.column_name = a.column_name
WHERE a.table_name IS NULL
UNION ALL
SELECT 'UNEXPECTED_IDENTITY' AS diff_type, a.table_name, a.column_name
FROM actual_identity a
LEFT JOIN expected_identity e
ON a.table_name = e.table_name AND a.column_name = e.column_name
WHERE e.table_name IS NULL
) LOOP
v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.column_name || '; ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 Identity 属性不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 7. 7 组 Phase 0.5 历史业务唯一约束 (UNIQUE) 骨架精确校验
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_unique(table_name, constraint_name, column_list) AS (
VALUES
('train_class_report', 'uk_plan_stu_report', 'plan_id, student_id'),
('train_dorm_auth', 'uk_student_plan_dormauth', 'student_id, plan_id'),
('train_dormitory_info', 'uk_dorm_build_room', 'dorm_building, dorm_room'),
('train_electronic_board', 'uk_board_sn', 'board_sn'),
('train_score_info', 'uk_score_plan_course_stu', 'plan_id, c_id, student_id'),
('train_student_checkin', 'uk_lesson_stu_checkin', 'plan_id, c_id, student_id, lesson_start_time'),
('train_student_portrait', 'uk_stu_plan_portrait', 'student_id, plan_id')
),
actual_unique AS (
SELECT
tc.table_name,
tc.constraint_name,
string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) AS column_list
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'UNIQUE'
AND tc.table_schema = 'public'
AND tc.table_name LIKE 'train_%'
GROUP BY tc.table_name, tc.constraint_name
)
SELECT 'MISSING_UNIQUE' AS diff_type, e.table_name, e.constraint_name, e.column_list
FROM expected_unique e
LEFT JOIN actual_unique a
ON e.table_name = a.table_name AND e.constraint_name = a.constraint_name AND e.column_list = a.column_list
WHERE a.table_name IS NULL
UNION ALL
SELECT 'UNEXPECTED_UNIQUE' AS diff_type, a.table_name, a.constraint_name, a.column_list
FROM actual_unique a
LEFT JOIN expected_unique e
ON a.table_name = e.table_name AND a.constraint_name = e.constraint_name AND a.column_list = e.column_list
WHERE e.table_name IS NULL
) LOOP
v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || '(' || r.column_list || '); ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 唯一约束 (UNIQUE) 骨架不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 8. 24 组 Phase 0.5 历史业务外键约束 (FK) 骨架精确校验 (名称+所属表+源列+引用表+引用列)
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_fk(table_name, constraint_name, column_name, foreign_table_name, foreign_column_name) AS (
VALUES
('train_class_plan', 'fk_classplan_course', 'c_id', 'train_course', 'c_id'),
('train_class_plan', 'fk_classplan_teacher', 't_id', 'train_teacher', 't_id'),
('train_class_report', 'fk_report_classroom', 'alloc_classroom_id', 'train_classroom_info', 'class_room_id'),
('train_class_report', 'fk_report_dorm', 'alloc_dorm_id', 'train_dormitory_info', 'dorm_id'),
('train_class_report', 'fk_report_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_class_report', 'fk_report_site', 'alloc_site_id', 'train_training_site', 'site_id'),
('train_class_report', 'fk_report_stu', 'student_id', 'train_student', 's_id'),
('train_dorm_auth', 'fk_dormauth_dorm', 'dorm_id', 'train_dormitory_info', 'dorm_id'),
('train_dorm_auth', 'fk_dormauth_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_dorm_auth', 'fk_dormauth_stu', 'student_id', 'train_student', 's_id'),
('train_electronic_board', 'fk_board_classroom', 'bind_classroom_id', 'train_classroom_info', 'class_room_id'),
('train_electronic_board', 'fk_board_plan', 'bind_plan_id', 'train_class_plan', 'plan_id'),
('train_score_info', 'fk_score_course', 'c_id', 'train_course', 'c_id'),
('train_score_info', 'fk_score_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_score_info', 'fk_score_stu', 'student_id', 'train_student', 's_id'),
('train_student_checkin', 'fk_checkin_board', 'board_id', 'train_electronic_board', 'board_id'),
('train_student_checkin', 'fk_checkin_course', 'c_id', 'train_course', 'c_id'),
('train_student_checkin', 'fk_checkin_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_student_checkin', 'fk_checkin_stu', 'student_id', 'train_student', 's_id'),
('train_student_portrait', 'fk_portrait_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_student_portrait', 'fk_portrait_stu', 'student_id', 'train_student', 's_id'),
('train_study_duration', 'fk_duration_course', 'c_id', 'train_course', 'c_id'),
('train_study_duration', 'fk_duration_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_study_duration', 'fk_duration_stu', 'student_id', 'train_student', 's_id')
),
actual_fk AS (
SELECT
tc.table_name,
tc.constraint_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage ccu
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND tc.table_name LIKE 'train_%'
)
SELECT 'MISSING_FK' AS diff_type, e.table_name, e.constraint_name, e.column_name, e.foreign_table_name, e.foreign_column_name
FROM expected_fk e
LEFT JOIN actual_fk a
ON e.table_name = a.table_name
AND e.constraint_name = a.constraint_name
AND e.column_name = a.column_name
AND e.foreign_table_name = a.foreign_table_name
AND e.foreign_column_name = a.foreign_column_name
WHERE a.table_name IS NULL
UNION ALL
SELECT 'UNEXPECTED_FK' AS diff_type, a.table_name, a.constraint_name, a.column_name, a.foreign_table_name, a.foreign_column_name
FROM actual_fk a
LEFT JOIN expected_fk e
ON a.table_name = e.table_name
AND a.constraint_name = e.constraint_name
AND a.column_name = e.column_name
AND a.foreign_table_name = e.foreign_table_name
AND a.foreign_column_name = e.foreign_column_name
WHERE e.table_name IS NULL
) LOOP
v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || ' (' || r.column_name || ' -> ' || r.foreign_table_name || '.' || r.foreign_column_name || '); ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 外键约束 (FK) 骨架不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 9. 9 个 Phase 0.5 历史 update_time 触发器精确校验
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_triggers(table_name, trigger_name) AS (
VALUES
('train_teacher', 'trig_train_teacher_modify'),
('train_student', 'trig_train_student_modify'),
('train_course', 'trig_train_course_modify'),
('train_classroom_info', 'trig_train_classroom_modify'),
('train_training_site', 'trig_train_trainsite_modify'),
('train_electronic_board', 'trig_train_board_modify'),
('train_dormitory_info', 'trig_train_dorminfo_modify'),
('train_class_plan', 'trig_train_classplan_modify'),
('train_dorm_auth', 'trig_train_dormauth_modify')
),
actual_triggers AS (
SELECT event_object_table AS table_name, trigger_name
FROM information_schema.triggers
WHERE trigger_schema = 'public'
AND event_object_table LIKE 'train_%'
)
SELECT 'MISSING_TRIGGER' AS diff_type, e.table_name, e.trigger_name
FROM expected_triggers e
LEFT JOIN actual_triggers a
ON e.table_name = a.table_name AND e.trigger_name = a.trigger_name
WHERE a.table_name IS NULL
UNION ALL
SELECT 'UNEXPECTED_TRIGGER' AS diff_type, a.table_name, a.trigger_name
FROM actual_triggers a
LEFT JOIN expected_triggers e
ON a.table_name = e.table_name AND a.trigger_name = e.trigger_name
WHERE e.table_name IS NULL
) LOOP
v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.trigger_name || '; ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 触发器骨架不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 10. 历史旧字段特征校验 (确认是旧模型结构,未提前被执行过 V2)
-- ==========================================================================
SELECT count(*) INTO v_cnt FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_class_plan' AND column_name IN ('c_id', 't_id');
IF v_cnt <> 2 THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_plan 未包含旧 c_id 与 t_id 列,结构已被篡改';
END IF;
SELECT count(*) INTO v_cnt FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_student_checkin' AND column_name = 'lesson_start_time';
IF v_cnt <> 1 THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] train_student_checkin 结构异常,未发现旧 lesson_start_time 列';
END IF;
SELECT count(*) INTO v_cnt FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_score_info'
AND column_name IN ('course_score', 'train_score', 'exam_score');
IF v_cnt <> 3 THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] train_score_info 结构异常,未包含旧成绩列';
END IF;
SELECT count(*) INTO v_cnt FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_class_report'
AND column_name IN ('alloc_dorm_id', 'alloc_classroom_id', 'alloc_site_id');
IF v_cnt <> 3 THEN
RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_report 结构异常,未包含旧预分配列';
END IF;
-- ==========================================================================
-- 11. public.sys_* 系统基础表零受损基线核验
-- ==========================================================================
SELECT count(*) INTO v_cnt FROM public.sys_user;
IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_user 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_menu;
IF v_cnt <> 106 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_menu 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_dept;
IF v_cnt <> 15 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_dept 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_dict_type;
IF v_cnt <> 44 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_dict_type 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_dict_data;
IF v_cnt <> 500 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_dict_data 行数受损,当前为 %', v_cnt; END IF;
RAISE NOTICE '==============================================================================';
RAISE NOTICE '>>> [Preflight-PASS] Phase 0.5 精确骨架校验全部通过!';
RAISE NOTICE '>>> 核验项: 14旧表 + 14 PK + 24 FK + 7 UNIQUE + 6 Identity + 9 Triggers + 行数基线';
RAISE NOTICE '>>> 判定结果: 当前数据库处于纯净 Phase 0.5 历史基线状态,允许执行 V2 迁移。';
RAISE NOTICE '==============================================================================';
END $$;