-- ============================================================================== -- 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 $$;