This commit is contained in:
2026-07-20 18:17:49 +08:00 Unverified
parent 67863e7ebf
commit 6aeed6b880
2 changed files with 56 additions and 77 deletions
+38 -77
View File
@@ -36,71 +36,32 @@ export function createMysqlAdapter(context) {
enableKeepAlive: true enableKeepAlive: true
}); });
for (const statement of mysqlSchema) await pool.execute(statement); const mysqlTableNames = mysqlSchema.map(statement =>
const mysqlColumnMigrations = [ statement.match(/^CREATE TABLE IF NOT EXISTS\s+([a-z0-9_]+)/i)?.[1]
"ALTER TABLE users ADD COLUMN IF NOT EXISTS admin_level ENUM('super', 'school', 'class') NULL", ).filter(Boolean);
'ALTER TABLE users ADD COLUMN IF NOT EXISTS school_id VARCHAR(64) NULL', const [databaseTables] = await pool.execute(`
'ALTER TABLE users ADD COLUMN IF NOT EXISTS class_id VARCHAR(64) NULL', SELECT TABLE_NAME FROM information_schema.TABLES
'ALTER TABLE users ADD COLUMN IF NOT EXISTS active BOOLEAN NOT NULL DEFAULT TRUE', WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'
'ALTER TABLE users ADD COLUMN IF NOT EXISTS candidate_number VARCHAR(120) NULL', `);
'ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT FALSE', const existingTableNames = new Set(databaseTables.map(row => row.TABLE_NAME));
'ALTER TABLE users ADD COLUMN IF NOT EXISTS archived_at VARCHAR(35) NULL', const existingAppTables = mysqlTableNames.filter(table => existingTableNames.has(table));
'ALTER TABLE users ADD COLUMN IF NOT EXISTS archived_by VARCHAR(64) NULL', let hasSchemaMetadata = false;
'ALTER TABLE schema_metadata ADD COLUMN IF NOT EXISTS self_registration_enabled BOOLEAN NOT NULL DEFAULT FALSE', let existingSchemaVersion = null;
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS school_id VARCHAR(64) NULL', if (existingTableNames.has('schema_metadata')) {
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS class_id VARCHAR(64) NULL', const [metadataRows] = await pool.execute('SELECT id, schema_version FROM schema_metadata WHERE id = 1');
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS native_place VARCHAR(160) NULL', hasSchemaMetadata = metadataRows.length > 0;
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS birth_date VARCHAR(20) NULL', existingSchemaVersion = hasSchemaMetadata ? Number(metadataRows[0].schema_version) : null;
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS ethnicity VARCHAR(60) NULL', }
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS postal_code VARCHAR(20) NULL', if (existingAppTables.length && (!hasSchemaMetadata || existingSchemaVersion !== 15)) {
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS guardian_name VARCHAR(100) NULL', for (const table of [...mysqlTableNames].reverse()) {
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS guardian_phone VARCHAR(60) NULL', await pool.query(`DROP TABLE IF EXISTS \`${table}\``);
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS profile_completed BOOLEAN NOT NULL DEFAULT FALSE', }
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS province_code VARCHAR(6) NULL', }
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS province_name VARCHAR(80) NULL',
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS city_code VARCHAR(6) NULL', for (const statement of mysqlSchema) await pool.query(statement);
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS city_name VARCHAR(100) NULL', // Development schemas are created from the current DDL as a whole. MySQL 8.4 lacks
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS district_code VARCHAR(6) NULL', // MariaDB-style conditional column addition; outdated schemas are rejected by the
'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS district_name VARCHAR(100) NULL', // version check below and should be rebuilt instead of migrated column by column.
'ALTER TABLE registrations ADD COLUMN IF NOT EXISTS registration_number VARCHAR(120) NULL',
'ALTER TABLE registrations ADD COLUMN IF NOT EXISTS number_rule_id VARCHAR(64) NULL',
"ALTER TABLE exams ADD COLUMN IF NOT EXISTS pass_policy ENUM('fixed_score', 'score_ratio', 'rank_percent', 'subject_scores', 'none') NOT NULL DEFAULT 'rank_percent'",
'ALTER TABLE exams ADD COLUMN IF NOT EXISTS pass_value DOUBLE NOT NULL DEFAULT 60',
'ALTER TABLE exams ADD COLUMN IF NOT EXISTS archived_at VARCHAR(35) NULL',
'ALTER TABLE exams ADD COLUMN IF NOT EXISTS archived_by VARCHAR(64) NULL',
'ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS full_score DOUBLE NOT NULL DEFAULT 150',
'ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS pass_score DOUBLE NOT NULL DEFAULT 90',
"ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS pass_rule ENUM('fixed_score', 'score_ratio', 'none') NOT NULL DEFAULT 'fixed_score'",
'ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS pass_value DOUBLE NOT NULL DEFAULT 90',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS code VARCHAR(40) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS manager_name VARCHAR(100) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS manager_phone VARCHAR(60) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS emergency_phone VARCHAR(60) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS gate_open_time VARCHAR(40) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS transport VARCHAR(500) NULL',
"ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS status ENUM('active', 'inactive') NOT NULL DEFAULT 'active'",
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS notes VARCHAR(1000) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS province_code VARCHAR(6) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS province_name VARCHAR(80) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS city_code VARCHAR(6) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS city_name VARCHAR(100) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS district_code VARCHAR(6) NULL',
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS district_name VARCHAR(100) NULL',
'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS province_code VARCHAR(6) NULL',
'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS province_name VARCHAR(80) NULL',
'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS city_code VARCHAR(6) NULL',
'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS city_name VARCHAR(100) NULL',
'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS district_code VARCHAR(6) NULL',
'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS district_name VARCHAR(100) NULL',
'ALTER TABLE test_rooms ADD COLUMN IF NOT EXISTS seat_plan VARCHAR(500) NULL',
'ALTER TABLE center_change_rooms ADD COLUMN IF NOT EXISTS seat_plan VARCHAR(500) NULL',
"ALTER TABLE admit_cards ADD COLUMN IF NOT EXISTS center_code VARCHAR(40) NOT NULL DEFAULT ''",
"ALTER TABLE admit_cards ADD COLUMN IF NOT EXISTS center_address VARCHAR(500) NOT NULL DEFAULT ''",
"ALTER TABLE admit_card_subjects ADD COLUMN IF NOT EXISTS building VARCHAR(120) NOT NULL DEFAULT ''",
"ALTER TABLE admit_card_subjects ADD COLUMN IF NOT EXISTS floor VARCHAR(80) NOT NULL DEFAULT ''",
"ALTER TABLE workflow_definitions MODIFY COLUMN business_type ENUM('profile_change', 'registration_review', 'center_change', 'candidate_account_batch') NOT NULL"
];
for (const statement of mysqlColumnMigrations) await pool.execute(statement);
const [resultLockTriggers] = await pool.execute(` const [resultLockTriggers] = await pool.execute(`
SELECT TRIGGER_NAME FROM information_schema.TRIGGERS SELECT TRIGGER_NAME FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = DATABASE() AND TRIGGER_NAME LIKE 'trg_results_lock_archived_%' WHERE TRIGGER_SCHEMA = DATABASE() AND TRIGGER_NAME LIKE 'trg_results_lock_archived_%'
@@ -130,23 +91,23 @@ export function createMysqlAdapter(context) {
END` END`
}; };
for (const [name, statement] of Object.entries(mysqlResultLockTriggers)) { for (const [name, statement] of Object.entries(mysqlResultLockTriggers)) {
if (!existingResultLockTriggers.has(name)) await pool.execute(statement); if (!existingResultLockTriggers.has(name)) await pool.query(statement);
} }
const [legacyRegistrationNumberIndexes] = await pool.execute("SHOW INDEX FROM registrations WHERE Key_name = 'uq_registrations_number'"); const [legacyRegistrationNumberIndexes] = await pool.query("SHOW INDEX FROM registrations WHERE Key_name = 'uq_registrations_number'");
if (legacyRegistrationNumberIndexes.length) await pool.execute('ALTER TABLE registrations DROP INDEX uq_registrations_number'); if (legacyRegistrationNumberIndexes.length) await pool.query('ALTER TABLE registrations DROP INDEX uq_registrations_number');
const [existing] = await pool.execute('SELECT id FROM schema_metadata WHERE id = 1'); const [existing] = await pool.execute('SELECT id FROM schema_metadata WHERE id = 1');
if (existing.length) { if (existing.length) {
const [metadataRows] = await pool.execute('SELECT app_version, schema_version FROM schema_metadata WHERE id = 1'); const [metadataRows] = await pool.execute('SELECT app_version, schema_version FROM schema_metadata WHERE id = 1');
if (Number(metadataRows[0]?.schema_version || 1) < 9) { if (Number(metadataRows[0]?.schema_version || 1) < 9) {
await pool.execute('DROP TABLE IF EXISTS admit_card_subjects'); await pool.query('DROP TABLE IF EXISTS admit_card_subjects');
await pool.execute('DROP TABLE IF EXISTS admit_cards'); await pool.query('DROP TABLE IF EXISTS admit_cards');
await pool.execute('DROP TABLE IF EXISTS exam_arrangement_plans'); await pool.query('DROP TABLE IF EXISTS exam_arrangement_plans');
await pool.execute('DROP TABLE IF EXISTS admission_number_rules'); await pool.query('DROP TABLE IF EXISTS admission_number_rules');
const admissionTables = ['admission_number_rules', 'exam_arrangement_plans', 'admit_cards', 'admit_card_subjects']; const admissionTables = ['admission_number_rules', 'exam_arrangement_plans', 'admit_cards', 'admit_card_subjects'];
for (const table of admissionTables) { for (const table of admissionTables) {
const statement = mysqlSchema.find(item => item.includes(`CREATE TABLE IF NOT EXISTS ${table} (`)); const statement = mysqlSchema.find(item => item.includes(`CREATE TABLE IF NOT EXISTS ${table} (`));
if (!statement) throw new Error(`缺少 ${table} 的 MySQL 表定义`); if (!statement) throw new Error(`缺少 ${table} 的 MySQL 表定义`);
await pool.execute(statement); await pool.query(statement);
} }
const extension = seed(); const extension = seed();
for (const rule of extension.admissionNumberRules) await pool.execute( for (const rule of extension.admissionNumberRules) await pool.execute(
@@ -387,13 +348,13 @@ export function createMysqlAdapter(context) {
} }
} }
const [centerCodeIndexes] = await pool.execute("SHOW INDEX FROM test_centers WHERE Key_name = 'uq_centers_code'"); const [centerCodeIndexes] = await pool.query("SHOW INDEX FROM test_centers WHERE Key_name = 'uq_centers_code'");
if (!centerCodeIndexes.length) { if (!centerCodeIndexes.length) {
await pool.execute('ALTER TABLE test_centers MODIFY COLUMN code VARCHAR(40) NOT NULL, ADD UNIQUE KEY uq_centers_code (code)'); await pool.query('ALTER TABLE test_centers MODIFY COLUMN code VARCHAR(40) NOT NULL, ADD UNIQUE KEY uq_centers_code (code)');
} }
const [candidateNumberIndexes] = await pool.execute("SHOW INDEX FROM users WHERE Key_name = 'uq_users_candidate_number'"); const [candidateNumberIndexes] = await pool.query("SHOW INDEX FROM users WHERE Key_name = 'uq_users_candidate_number'");
if (!candidateNumberIndexes.length) { if (!candidateNumberIndexes.length) {
await pool.execute('ALTER TABLE users ADD UNIQUE KEY uq_users_candidate_number (candidate_number)'); await pool.query('ALTER TABLE users ADD UNIQUE KEY uq_users_candidate_number (candidate_number)');
} }
const transaction = async operations => { const transaction = async operations => {
+18
View File
@@ -18,6 +18,24 @@ for (const statement of mysqlRuleSchemas) {
} }
const mysqlRegistrationSchema = mysqlSchema.find(statement => /CREATE TABLE IF NOT EXISTS registrations\b/.test(statement)); const mysqlRegistrationSchema = mysqlSchema.find(statement => /CREATE TABLE IF NOT EXISTS registrations\b/.test(statement));
assert.match(mysqlRegistrationSchema || '', /paid_at VARCHAR\(35\).*paid_by VARCHAR\(64\)/s, 'MySQL '); assert.match(mysqlRegistrationSchema || '', /paid_at VARCHAR\(35\).*paid_by VARCHAR\(64\)/s, 'MySQL ');
const mysqlWorkflowSchema = mysqlSchema.find(statement => /CREATE TABLE IF NOT EXISTS workflow_definitions\b/.test(statement));
assert.match(mysqlWorkflowSchema || '', /'score_appeal'/, 'MySQL 工作流枚举应包含成绩复议');
const mysqlTableOrder = new Map(mysqlSchema.map((statement, index) => [
statement.match(/^CREATE TABLE IF NOT EXISTS\s+([a-z0-9_]+)/i)?.[1], index
]));
for (const [table, index] of mysqlTableOrder) {
const statement = mysqlSchema[index];
for (const match of statement.matchAll(/REFERENCES\s+([a-z0-9_]+)/gi)) {
assert.ok(mysqlTableOrder.has(match[1]) && mysqlTableOrder.get(match[1]) <= index, `MySQL 表 ${table} 的外键依赖必须先创建`);
}
}
const mysqlAdapterSource = await readFile(resolve(root, 'src', 'database', 'mysql-adapter.mjs'), 'utf8');
assert.doesNotMatch(mysqlAdapterSource, /ADD\s+COLUMN\s+IF\s+NOT\s+EXISTS/i, 'MySQL 8.4 不支持 ADD COLUMN IF NOT EXISTS');
assert.match(mysqlAdapterSource, /for \(const statement of mysqlSchema\) await pool\.query\(statement\)/, 'MySQL DDL 应使用文本协议执行');
assert.match(mysqlAdapterSource, /existingResultLockTriggers\.has\(name\)\) await pool\.query\(statement\)/, 'MySQL 触发器不得通过预处理协议创建');
assert.doesNotMatch(mysqlAdapterSource, /\.execute\(\s*['"`]\s*(?:CREATE|ALTER|DROP|SHOW)\b/i, 'MySQL DDL 和 SHOW 语句不得通过预处理协议执行');
assert.match(mysqlAdapterSource, /existingAppTables\.length && \(!hasSchemaMetadata \|\| existingSchemaVersion !== 15\)/, 'MySQL 应重建未完成或过期的开发结构');
assert.match(mysqlAdapterSource, /\[\.\.\.mysqlTableNames\]\.reverse\(\)/, 'MySQL 半成品表应按外键依赖逆序清理');
await rm(testDb, { force: true }); await rm(testDb, { force: true });
await rm(`${testDb}-shm`, { force: true }); await rm(`${testDb}-shm`, { force: true });
await rm(`${testDb}-wal`, { force: true }); await rm(`${testDb}-wal`, { force: true });