503 lines
28 KiB
SQL
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 := concat(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 format('SELECT count(*) FROM public.%I', 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 := concat(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 := concat(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 := concat(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 $$;
|