From 2df6b3f26c68717ce9ef2cedbc4b1ad5e0b0134c Mon Sep 17 00:00:00 2001 From: wangziqi Date: Fri, 31 Jul 2026 10:45:55 +0800 Subject: [PATCH] fix(education): lock collection migration adoption --- .../V4110__add_manual_question_collection.sql | 7 +++++ ...ucationFlywayMigrationIntegrationTest.java | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/yudao-module-education/src/main/resources/db/migration/education/V4110__add_manual_question_collection.sql b/yudao-module-education/src/main/resources/db/migration/education/V4110__add_manual_question_collection.sql index 3b4b9a4f..e013790a 100644 --- a/yudao-module-education/src/main/resources/db/migration/education/V4110__add_manual_question_collection.sql +++ b/yudao-module-education/src/main/resources/db/migration/education/V4110__add_manual_question_collection.sql @@ -1,6 +1,13 @@ -- EDU-010: current-tenant manual Question Collection authoring lifecycle. -- JAVA_READ application authority is enforced in Java; database guards protect direct mutation. +-- Prevent concurrent collection/membership/question mutation across adoption validation, +-- backfill, and trigger installation while retaining ordinary reads. +LOCK TABLE education_question_collection, + education_question_collection_question, + education_question + IN SHARE ROW EXCLUSIVE MODE; + ALTER TABLE education_question_collection ADD COLUMN publication_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', ADD COLUMN authoring_version INTEGER NOT NULL DEFAULT 0, diff --git a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java index a557f9d2..70cb8244 100644 --- a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java +++ b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java @@ -675,6 +675,33 @@ class EducationFlywayMigrationIntegrationTest { } } + @Test + void shouldUseMigrationLocksThatBlockMembershipAndQuestionWrites() throws SQLException { + String schema = createSchema("collection_lock_modes"); + configureFlyway(schema, false).target("4100").load().migrate(); + try (Connection locker = DriverManager.getConnection(jdbcUrl(schema), USER, PASSWORD); + Connection writer = DriverManager.getConnection(jdbcUrl(schema), USER, PASSWORD)) { + locker.setAutoCommit(false); + try (var statement = locker.createStatement()) { + statement.execute("LOCK TABLE education_question_collection, education_question_collection_question, education_question IN SHARE ROW EXCLUSIVE MODE"); + } + assertThat(queryLong(schema, """ + SELECT count(*) FROM pg_locks lock + JOIN pg_class relation ON relation.oid=lock.relation + WHERE lock.pid<>pg_backend_pid() AND lock.mode='ShareRowExclusiveLock' AND lock.granted + AND relation.relname IN ('education_question_collection','education_question_collection_question','education_question') + """)).isEqualTo(3L); + try (var statement = writer.createStatement()) { + statement.execute("SET lock_timeout='250ms'"); + assertThatThrownBy(() -> statement.execute("DELETE FROM education_question_collection_question WHERE false")) + .hasMessageContaining("lock timeout"); + assertThatThrownBy(() -> statement.execute("UPDATE education_question SET deleted=deleted WHERE false")) + .hasMessageContaining("lock timeout"); + } + locker.rollback(); + } + } + @Test void shouldFailV4110ForHistoricalCollectionEntryDivergence() throws SQLException { String schema = createSchema("collection_entry_divergence");