Files
railwaysystem/RuoYi-Vue/sql/train/v2/010_verify.sql
T

503 lines
28 KiB
SQL

-- ==============================================================================
-- RailwaySystem 数据库 V2 迁移验收脚本: 010_verify.sql
-- 适用数据库: KingbaseES V009R003C018 (schema: public)
-- 执行目标: 实施“定义级深度验收”,核验数据库 V2 迁移后对象存在性与真实结构定义
-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 2 审查要求
-- 核心原则:
-- 1. 【失败即失败】所有验证逻辑均通过 PL/pgSQL DO 块强制断言,出现任何 FAIL 立即抛出 EXCEPTION
-- 2. 【定义级验收】拒绝仅查名字,必须校验: 表名、列列表、目标外键与删除规则、CHECK定义、索引定义
-- 3. 【默认值校验】强断言 accommodation_status 默认值为 0,强断言业务选择型字段无 DEFAULT
-- 4. 【系统零破坏】显式核验 public.sys_* 系统表行数与基线完全吻合
-- ==============================================================================
-- 步骤 1: 打印 21 张表状态清单 (供人工审计屏幕阅读)
SELECT t.table_name,
CASE WHEN c.table_name IS NOT NULL THEN 'EXISTS' ELSE 'MISSING' END AS table_status
FROM (
VALUES
('train_teacher'), ('train_student'), ('train_course'), ('train_resource'),
('train_course_resource'), ('train_classroom_info'), ('train_training_site'),
('train_electronic_board'), ('train_dormitory_info'), ('train_class_plan'),
('train_plan_course'), ('train_enrollment'), ('train_lesson'),
('train_accommodation'), ('train_dorm_auth'), ('train_class_report'),
('train_student_checkin'), ('train_score_item'), ('train_student_score'),
('train_study_duration'), ('train_student_portrait')
) AS t(table_name)
LEFT JOIN information_schema.tables c
ON c.table_schema = 'public' AND c.table_name = t.table_name
ORDER BY t.table_name;
-- 步骤 2: 核心自动化强断言块 (任何项不满足立即抛出 EXCEPTION 中止)
DO $$
DECLARE
v_cnt INT;
v_diff_text TEXT := '';
r RECORD;
BEGIN
-- ==========================================================================
-- 1. 逐名断言 21 张正式业务表完全就绪且无冗余 train_* 杂表
-- ==========================================================================
FOR r IN (
WITH expected_tables(table_name) AS (
VALUES
('train_teacher'), ('train_student'), ('train_course'), ('train_resource'),
('train_course_resource'), ('train_classroom_info'), ('train_training_site'),
('train_electronic_board'), ('train_dormitory_info'), ('train_class_plan'),
('train_plan_course'), ('train_enrollment'), ('train_lesson'),
('train_accommodation'), ('train_dorm_auth'), ('train_class_report'),
('train_student_checkin'), ('train_score_item'), ('train_student_score'),
('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_%'
AND table_name NOT LIKE 'bak_%'
)
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 '>>> [Verify-FATAL] 21 张正式业务表集合不匹配: %', v_diff_text;
END IF;
-- 确认旧淘汰表 train_score_info 彻底不存在
SELECT count(*) INTO v_cnt FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'train_score_info';
IF v_cnt > 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_score_info 仍残留在 public schema 中,淘汰下线失败!';
END IF;
-- ==========================================================================
-- 2. 数据行数基线强断言 (教师精确 1 行,新字段全部填充,其余 20 表精确 0 行)
-- ==========================================================================
SELECT count(*) INTO v_cnt FROM train_teacher;
IF v_cnt <> 1 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 行数期望为 1,实际为 % 行', v_cnt;
END IF;
SELECT count(*) INTO v_cnt
FROM train_teacher
WHERE org_name IS NULL OR status IS NULL OR del_flag IS NULL;
IF v_cnt > 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 记录中新主档字段存在非法 NULL 值';
END IF;
SELECT count(*) INTO v_cnt
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_teacher'
AND column_name IN ('t_department', 't_title', 'create_user');
IF v_cnt > 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 仍残留旧字段 (t_department/t_title/create_user)';
END IF;
FOR r IN (
SELECT unnest(ARRAY[
'train_student', 'train_course', 'train_resource', 'train_course_resource',
'train_classroom_info', 'train_training_site', 'train_electronic_board',
'train_dormitory_info', 'train_class_plan', 'train_plan_course',
'train_enrollment', 'train_lesson', 'train_accommodation',
'train_dorm_auth', 'train_class_report', 'train_student_checkin',
'train_score_item', 'train_student_score', 'train_study_duration',
'train_student_portrait'
]) AS tbl
) LOOP
EXECUTE 'SELECT count(*) FROM ' || r.tbl INTO v_cnt;
IF v_cnt <> 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] 表 % 包含非预期数据,当前行数为 % (期望 0 行)', r.tbl, v_cnt;
END IF;
END LOOP;
-- ==========================================================================
-- 3. 13 组核心唯一约束 (UNIQUE) 定义级校验 (表名 + 约束名 + 组成列列表)
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_unique(table_name, constraint_name, column_list) AS (
VALUES
('train_student', 'uq_student_idcard_hash', 'id_card_hash'),
('train_course', 'uq_course_code', 'c_code'),
('train_electronic_board', 'uq_board_sn', 'board_sn'),
('train_dormitory_info', 'uq_dorm_building_room', 'dorm_building, dorm_room'),
('train_course_resource', 'uq_course_resource', 'c_id, resource_id'),
('train_plan_course', 'uq_plan_course', 'plan_id, c_id'),
('train_enrollment', 'uq_enrollment', 'plan_id, student_id'),
('train_accommodation', 'uq_accommodation_enroll', 'enrollment_id'),
('train_dorm_auth', 'uq_dorm_auth_accomm', 'accommodation_id'),
('train_class_report', 'uq_class_report_enroll', 'enrollment_id'),
('train_student_checkin', 'uq_student_checkin_enroll_lesson', 'enrollment_id, lesson_id'),
('train_student_score', 'uq_student_score_enroll_item', 'enrollment_id, score_item_id'),
('train_student_portrait', 'uq_student_portrait_enroll', 'enrollment_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_UQ' 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_UQ' 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 '>>> [Verify-FATAL] 13 组唯一约束 (UNIQUE) 定义不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 4. 26 组外键约束 (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_course_resource', 'fk_cr_course', 'c_id', 'train_course', 'c_id'),
('train_course_resource', 'fk_cr_resource', 'resource_id', 'train_resource', 'resource_id'),
('train_electronic_board', 'fk_board_classroom', 'bind_classroom_id', 'train_classroom_info', 'class_room_id'),
('train_plan_course', 'fk_plancourse_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_plan_course', 'fk_plancourse_course', 'c_id', 'train_course', 'c_id'),
('train_enrollment', 'fk_enrollment_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_enrollment', 'fk_enrollment_student', 'student_id', 'train_student', 's_id'),
('train_lesson', 'fk_lesson_plancourse', 'plan_course_id', 'train_plan_course', 'plan_course_id'),
('train_lesson', 'fk_lesson_teacher', 't_id', 'train_teacher', 't_id'),
('train_lesson', 'fk_lesson_classroom', 'class_room_id', 'train_classroom_info', 'class_room_id'),
('train_lesson', 'fk_lesson_site', 'site_id', 'train_training_site', 'site_id'),
('train_accommodation', 'fk_accomm_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'),
('train_accommodation', 'fk_accomm_dorm', 'dorm_id', 'train_dormitory_info', 'dorm_id'),
('train_dorm_auth', 'fk_dormauth_accomm', 'accommodation_id', 'train_accommodation', 'accommodation_id'),
('train_class_report', 'fk_report_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'),
('train_student_checkin', 'fk_checkin_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'),
('train_student_checkin', 'fk_checkin_lesson', 'lesson_id', 'train_lesson', 'lesson_id'),
('train_student_checkin', 'fk_checkin_board', 'board_id', 'train_electronic_board', 'board_id'),
('train_score_item', 'fk_scoreitem_plan', 'plan_id', 'train_class_plan', 'plan_id'),
('train_score_item', 'fk_scoreitem_plancourse', 'plan_course_id', 'train_plan_course', 'plan_course_id'),
('train_student_score', 'fk_score_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'),
('train_student_score', 'fk_score_item', 'score_item_id', 'train_score_item', 'score_item_id'),
('train_study_duration', 'fk_study_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'),
('train_study_duration', 'fk_study_plancourse', 'plan_course_id', 'train_plan_course', 'plan_course_id'),
('train_study_duration', 'fk_study_resource', 'resource_id', 'train_resource', 'resource_id'),
('train_student_portrait', 'fk_portrait_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_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,
rc.delete_rule
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
JOIN information_schema.referential_constraints rc
ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_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
AND a.delete_rule = 'NO ACTION'
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 || '; ';
END LOOP;
IF v_diff_text <> '' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] 26 组外键约束 (FK) 定义或删除规则不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 5. 9 组单行检查约束 (CHECK) 定义逻辑校验 (逐名检查并核验包含预期核心表达式)
-- ==========================================================================
FOR r IN (
SELECT
cc.constraint_name,
tc.table_name,
cc.check_clause
FROM information_schema.check_constraints cc
JOIN information_schema.table_constraints tc
ON cc.constraint_name = tc.constraint_name AND cc.constraint_schema = tc.constraint_schema
WHERE tc.table_schema = 'public' AND tc.table_name LIKE 'train_%'
) LOOP
CASE r.constraint_name
WHEN 'chk_lesson_time' THEN
IF r.table_name <> 'train_lesson' OR r.check_clause NOT LIKE '%start_time < end_time%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_lesson_time 定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_lesson_venue' THEN
IF r.table_name <> 'train_lesson' OR (r.check_clause NOT LIKE '%venue_type = 1%' OR r.check_clause NOT LIKE '%venue_type = 2%') THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_lesson_venue 场地互斥定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_score_item_weight' THEN
IF r.table_name <> 'train_score_item' OR (r.check_clause NOT LIKE '%weight >= 0%' OR r.check_clause NOT LIKE '%weight <= 100%') THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_score_item_weight 权重范围定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_score_item_full' THEN
IF r.table_name <> 'train_score_item' OR r.check_clause NOT LIKE '%full_score > 0%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_score_item_full 满分定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_student_score_nonneg' THEN
IF r.table_name <> 'train_student_score' OR r.check_clause NOT LIKE '%score >= 0%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_student_score_nonneg 成绩非负定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_study_seconds' THEN
IF r.table_name <> 'train_study_duration' OR r.check_clause NOT LIKE '%study_seconds >= 0%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_study_seconds 时长非负定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_study_time' THEN
IF r.table_name <> 'train_study_duration' OR r.check_clause NOT LIKE '%end_time >= start_time%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_study_time 时间先后定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_study_source_system' THEN
IF r.table_name <> 'train_study_duration' OR r.check_clause NOT LIKE '%source_system IS NOT NULL%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_study_source_system 来源系统一致性定义不合预期: %', r.check_clause;
END IF;
WHEN 'chk_dorm_capacity' THEN
IF r.table_name <> 'train_dormitory_info' OR r.check_clause NOT LIKE '%capacity > 0%' THEN
RAISE EXCEPTION '>>> [Verify-FATAL] chk_dorm_capacity 容量正数定义不合预期: %', r.check_clause;
END IF;
ELSE
NULL; -- 忽略其他系统生成的非空检查
END CASE;
END LOOP;
-- 确认 9 个显式命名的 CHECK 约束一个不少
SELECT count(*) INTO v_cnt
FROM information_schema.table_constraints
WHERE table_schema = 'public'
AND constraint_type = 'CHECK'
AND constraint_name IN (
'chk_lesson_time', 'chk_lesson_venue', 'chk_score_item_weight',
'chk_score_item_full', 'chk_student_score_nonneg', 'chk_study_seconds',
'chk_study_time', 'chk_study_source_system', 'chk_dorm_capacity'
);
IF v_cnt <> 9 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] 9 个核心 CHECK 检查约束总数不匹配,实际发现 % 个', v_cnt;
END IF;
-- ==========================================================================
-- 6. 12 张业务表 Identity 自增主键精确校验 (表名 + 指定列名 + is_identity = 'YES')
-- ==========================================================================
v_diff_text := '';
FOR r IN (
WITH expected_identity(table_name, column_name) AS (
VALUES
('train_course_resource', 'id'),
('train_plan_course', 'plan_course_id'),
('train_enrollment', 'enrollment_id'),
('train_lesson', 'lesson_id'),
('train_accommodation', 'accommodation_id'),
('train_score_item', 'score_item_id'),
('train_student_score', 'score_id'),
('train_dorm_auth', 'auth_id'),
('train_class_report', 'report_id'),
('train_student_checkin', 'checkin_id'),
('train_study_duration', 'dur_id'),
('train_student_portrait', 'portrait_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 '>>> [Verify-FATAL] 12 张表 Identity 自增主键属性不匹配: %', v_diff_text;
END IF;
-- ==========================================================================
-- 7. 关键复合索引与部分唯一索引定义级校验
-- ==========================================================================
-- 7.1 uq_idx_study_source_event 必须为 UNIQUE 且具有双非空 WHERE 条件
SELECT count(*) INTO v_cnt FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'train_study_duration'
AND indexname = 'uq_idx_study_source_event'
AND indexdef LIKE '%UNIQUE INDEX%'
AND indexdef LIKE '%(source_system, source_event_id)%'
AND indexdef LIKE '%source_system IS NOT NULL%'
AND indexdef LIKE '%source_event_id IS NOT NULL%';
IF v_cnt <> 1 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] 部分唯一索引 uq_idx_study_source_event 定义不合要求';
END IF;
-- 7.2 排课表三大时间检索复合索引
SELECT count(*) INTO v_cnt FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'train_lesson'
AND indexname = 'idx_lesson_teacher_time' AND indexdef LIKE '%(t_id, start_time, end_time)%';
IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] 索引 idx_lesson_teacher_time 复合列定义不合要求'; END IF;
SELECT count(*) INTO v_cnt FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'train_lesson'
AND indexname = 'idx_lesson_classroom_time' AND indexdef LIKE '%(class_room_id, start_time, end_time)%';
IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] 索引 idx_lesson_classroom_time 复合列定义不合要求'; END IF;
SELECT count(*) INTO v_cnt FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'train_lesson'
AND indexname = 'idx_lesson_site_time' AND indexdef LIKE '%(site_id, start_time, end_time)%';
IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] 索引 idx_lesson_site_time 复合列定义不合要求'; END IF;
-- ==========================================================================
-- 8. 默认值严格校验 (R2-01 住宿默认0预分配; R2-02 业务选择型字段严禁有DEFAULT)
-- ==========================================================================
-- 8.1 accommodation_status 默认值必须为 0 (预分配)
SELECT count(*) INTO v_cnt
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_accommodation'
AND column_name = 'accommodation_status' AND column_default LIKE '%0%';
IF v_cnt <> 1 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_accommodation.accommodation_status 默认值必须为 0 (预分配)';
END IF;
-- 8.2 业务选择型字段严禁存在默认值 (column_default 必须为 NULL)
FOR r IN (
SELECT table_name, column_name, column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND (
(table_name = 'train_enrollment' AND column_name IN ('source_type', 'accommodation_required')) OR
(table_name = 'train_lesson' AND column_name = 'venue_type') OR
(table_name = 'train_student_score' AND column_name = 'source_type') OR
(table_name = 'train_student_checkin' AND column_name = 'checkin_way') OR
(table_name = 'train_class_report' AND column_name = 'verify_result')
)
) LOOP
IF r.column_default IS NOT NULL THEN
RAISE EXCEPTION '>>> [Verify-FATAL] 业务选择型字段 %.% 非法配置了默认值: %,必须取消!', r.table_name, r.column_name, r.column_default;
END IF;
END LOOP;
-- ==========================================================================
-- 9. 核心领域架构解耦列特征强断言
-- ==========================================================================
-- train_class_plan 必须无 c_id 与 t_id
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 > 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_class_plan 仍包含旧单课或单教师外键列 c_id/t_id';
END IF;
-- train_student_checkin 必须包含 enrollment_id 与 lesson_id,且无旧冗余列
SELECT count(*) INTO v_cnt FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_student_checkin'
AND column_name IN ('enrollment_id', 'lesson_id');
IF v_cnt <> 2 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_student_checkin 未包含核心列 enrollment_id 与 lesson_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 IN ('plan_id', 'c_id', 'student_id', 'lesson_start_time', 'lesson_end_time');
IF v_cnt > 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_student_checkin 仍残留历史冗余列';
END IF;
-- train_class_report 必须无 alloc_* 旧预分配列
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 > 0 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_class_report 仍残留旧预分配列';
END IF;
-- train_lesson.t_id 必须为 NOT NULL
SELECT count(*) INTO v_cnt FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'train_lesson'
AND column_name = 't_id' AND is_nullable = 'NO';
IF v_cnt <> 1 THEN
RAISE EXCEPTION '>>> [Verify-FATAL] train_lesson.t_id 必须为 NOT NULL';
END IF;
-- ==========================================================================
-- 10. public.sys_* 系统基线表行数与零受损核验
-- ==========================================================================
SELECT count(*) INTO v_cnt FROM public.sys_user;
IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_user 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_menu;
IF v_cnt <> 106 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_menu 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_dept;
IF v_cnt <> 15 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_dept 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_dict_type;
IF v_cnt <> 44 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_dict_type 行数受损,当前为 %', v_cnt; END IF;
SELECT count(*) INTO v_cnt FROM public.sys_dict_data;
IF v_cnt <> 500 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_dict_data 行数受损,当前为 %', v_cnt; END IF;
RAISE NOTICE '==============================================================================';
RAISE NOTICE '>>> [Verify-PASS] RailwaySystem 数据库 V2 定义级验收全量通过!';
RAISE NOTICE '>>> 验收内容: 21表 + 13 UNIQUE(定义) + 26 FK(定义+NO ACTION) + 9 CHECK(表达式) + 12 Identity + 关键复合索引 + 默认值严格断言 + sys_*零破坏';
RAISE NOTICE '==============================================================================';
END $$;