From 32fa376d635e951febbb5a17050fc4cb8de77e9f Mon Sep 17 00:00:00 2001 From: let5sne Date: Sat, 18 Jul 2026 01:04:17 +0800 Subject: [PATCH] fix: align release table collations --- ...260718_release_collation_compat_mysql8.sql | 58 +++++++++++++++++++ docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md | 3 +- scripts/tests/aihr-release-collation.test.sh | 58 +++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql create mode 100755 scripts/tests/aihr-release-collation.test.sh diff --git a/backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql b/backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql new file mode 100644 index 00000000..aae2e4bf --- /dev/null +++ b/backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql @@ -0,0 +1,58 @@ +-- Align the tables introduced by the 2026-07-17 release with the collation +-- already used by the tenant identity column in the target database. +-- +-- Older production databases use utf8mb4_general_ci for tenant_id while a +-- clean MySQL 8 database uses utf8mb4_0900_ai_ci. Cross-table joins must not +-- assume either value, so derive the canonical collation at migration time. + +SET @aihr_release_collation := ( + SELECT collation_name + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'aihr_knowledge_info' + AND column_name = 'tenant_id' + LIMIT 1 +); +SET @aihr_release_collation := COALESCE(@aihr_release_collation, 'utf8mb4_0900_ai_ci'); + +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_knowledge_space_grant` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_knowledge_app` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_knowledge_app_space` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_knowledge_query_log` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_knowledge_admin_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; + +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_learning_question` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_practice_question_feedback` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_target` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_question` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_attempt` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_answer` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; + +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_web_search_provider` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_web_ai_query_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_community_question` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_community_answer` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_incentive_rule` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; +SET @aihr_release_ddl := CONCAT('ALTER TABLE `aihr_points_ledger` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_release_collation); +PREPARE aihr_release_stmt FROM @aihr_release_ddl; EXECUTE aihr_release_stmt; DEALLOCATE PREPARE aihr_release_stmt; + +SET @aihr_release_ddl := NULL; +SET @aihr_release_collation := NULL; diff --git a/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md b/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md index 2959505e..bda9855d 100644 --- a/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md +++ b/docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md @@ -45,9 +45,10 @@ mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aih mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_learning_closure_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_practice_five_position_catalog_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_web_ai_question_reward_mysql8.sql +mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql ``` -顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。 +顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。排序规则兼容迁移必须最后执行,它会读取目标库 `aihr_knowledge_info.tenant_id` 的实际排序规则,并将本次新建的 18 张表统一到同一规则,兼容历史生产库与全新 MySQL 8 数据库。 ## 迁移后只读核验 diff --git a/scripts/tests/aihr-release-collation.test.sh b/scripts/tests/aihr-release-collation.test.sh new file mode 100755 index 00000000..fdfc5296 --- /dev/null +++ b/scripts/tests/aihr-release-collation.test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v docker >/dev/null || ! docker ps --format '{{.Names}}' | grep -qx 'wygj-mysql'; then + echo "SKIP: AIHR release collation migration test requires wygj-mysql" + exit 0 +fi + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +MYSQL_CONTAINER="wygj-mysql" +DB="wygj_aihr_collation_test_$$" + +mysql() { + docker exec -i "$MYSQL_CONTAINER" mysql -uroot -proot --default-character-set=utf8mb4 "$@" +} + +cleanup() { + mysql -e "DROP DATABASE IF EXISTS \`$DB\`;" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +mysql -e "CREATE DATABASE \`$DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;" +mysql "$DB" <<'SQL' +CREATE TABLE aihr_knowledge_info (tenant_id varchar(20) COLLATE utf8mb4_general_ci) ENGINE=InnoDB; +CREATE TABLE aihr_knowledge_space_grant (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_knowledge_app (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_knowledge_app_space (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_knowledge_query_log (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_knowledge_admin_audit (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_learning_question (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_practice_question_feedback (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_onboard_exam (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_onboard_exam_target (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_onboard_exam_question (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_onboard_exam_attempt (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_onboard_exam_answer (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_web_search_provider (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_web_ai_query_audit (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_community_question (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_community_answer (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_incentive_rule (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +CREATE TABLE aihr_points_ledger (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB; +SQL + +mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql" +mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql" + +aligned_columns="$(mysql "$DB" -N -B -e " + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema='$DB' + AND table_name <> 'aihr_knowledge_info' + AND column_name IN ('tenant_id', 'label') + AND collation_name='utf8mb4_general_ci'; +")" +test "$aligned_columns" = '36' + +echo "PASS: AIHR release tables inherit the target database collation and the migration is idempotent"