fix(education): validate adopted unique indexes

This commit is contained in:
2026-07-30 14:23:36 +08:00
parent 191811b643
commit 4be9c8147e
4 changed files with 72 additions and 10 deletions

View File

@@ -184,13 +184,6 @@ public class EducationTenantController {
return EducationTenantRespVO.builder().tenantId(tenant.getId()).displayName(tenant.getName()).build();
}
private boolean isLocalHostnameClaim(String hostname) {
if (!educationProperties.getTenantResolution().isLocalDevelopmentEnabled() || StrUtil.isBlank(hostname)) {
return false;
}
return isLocalHost(normalizeHost(hostname));
}
static String normalizeHost(String value) {
if (StrUtil.isBlank(value)) {
return null;

View File

@@ -0,0 +1,44 @@
-- Fail closed when an adopted Practice schema already contains a same-named
-- index whose uniqueness or ordered key columns differ from the application contract.
DO $$
DECLARE
expected RECORD;
actual_columns TEXT[];
actual_unique BOOLEAN;
BEGIN
FOR expected IN
SELECT * FROM (VALUES
('uk_education_practice_session_tenant_client', 'education_practice_session', ARRAY['tenant_id', 'client_session_id']),
('uk_education_practice_question_session_sequence', 'education_practice_question', ARRAY['session_id', 'sequence']),
('uk_education_practice_report_tenant_session', 'education_practice_report', ARRAY['tenant_id', 'session_id']),
('uk_education_practice_report_detail_report_sequence', 'education_practice_report_detail', ARRAY['report_id', 'sequence']),
('uk_education_wrong_question_tenant_user_question', 'education_wrong_question', ARRAY['tenant_id', 'user_id', 'question_id']),
('uk_education_wrong_idempotency_tenant_user_question_report', 'education_wrong_question_idempotency', ARRAY['tenant_id', 'user_id', 'question_id', 'report_id']),
('uk_education_favorite_tenant_user_target', 'education_favorite', ARRAY['tenant_id', 'user_id', 'target_type', 'target_id']),
('uk_education_idempotency_tenant_user_operation_key', 'education_idempotency', ARRAY['tenant_id', 'user_id', 'operation', 'idempotency_key'])
) AS definitions(index_name, table_name, column_names)
LOOP
SELECT index_metadata.indisunique,
ARRAY_AGG(attribute.attname ORDER BY key_position.ordinality)
INTO actual_unique, actual_columns
FROM pg_class index_relation
JOIN pg_namespace index_namespace ON index_namespace.oid = index_relation.relnamespace
JOIN pg_index index_metadata ON index_metadata.indexrelid = index_relation.oid
JOIN pg_class table_relation ON table_relation.oid = index_metadata.indrelid
JOIN LATERAL UNNEST(index_metadata.indkey::SMALLINT[]) WITH ORDINALITY
AS key_position(attribute_number, ordinality) ON true
JOIN pg_attribute attribute
ON attribute.attrelid = table_relation.oid
AND attribute.attnum = key_position.attribute_number
WHERE index_namespace.nspname = current_schema()
AND index_relation.relname = expected.index_name
AND table_relation.relname = expected.table_name
GROUP BY index_metadata.indisunique;
IF actual_unique IS DISTINCT FROM true OR actual_columns IS DISTINCT FROM expected.column_names THEN
RAISE EXCEPTION 'Practice unique index % must be UNIQUE on %.%, found unique=% columns=%',
expected.index_name, expected.table_name, expected.column_names, actual_unique, actual_columns;
END IF;
END LOOP;
END $$;

View File

@@ -83,6 +83,31 @@ class EducationFlywayMigrationIntegrationTest {
.isEqualTo(1L);
}
@Test
void shouldFailClosedWhenAdoptedUniqueIndexHasWrongDefinition() throws SQLException {
String schema = createSchema("wrong_index");
createCompatibleManualPracticeFixture(schema);
execute(schema, """
CREATE TABLE education_favorite (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
target_type VARCHAR(32) NOT NULL, target_id VARCHAR(64) NOT NULL,
creator VARCHAR(64) DEFAULT '', create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '', update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
CREATE INDEX uk_education_favorite_tenant_user_target
ON education_favorite (tenant_id, user_id, target_type, target_id);
""");
assertThatThrownBy(() -> configureFlyway(schema, true).load().migrate())
.hasMessageContaining("Practice unique index uk_education_favorite_tenant_user_target")
.hasMessageContaining("must be UNIQUE");
assertThat(queryLong(schema,
"SELECT COUNT(*) FROM flyway_schema_history WHERE version = '4060' AND success = TRUE"))
.isZero();
}
@Test
void shouldMigratePlatformSchemaFromBaseline4009() throws SQLException {
String schema = createSchema("baseline");
@@ -94,7 +119,7 @@ class EducationFlywayMigrationIntegrationTest {
assertThat(queryStrings(schema,
"SELECT COALESCE(version, 'BASELINE') FROM flyway_schema_history ORDER BY installed_rank"))
.containsExactly("4009", "4010", "4020", "4030", "4040", "4050");
.containsExactly("4009", "4010", "4020", "4030", "4040", "4050", "4060");
assertThat(queryLong(schema,
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = current_schema() " +
"AND table_name = 'education_idempotency'"))
@@ -156,7 +181,7 @@ class EducationFlywayMigrationIntegrationTest {
assertThat(queryStrings(schema,
"SELECT version FROM flyway_schema_history WHERE success = TRUE ORDER BY installed_rank"))
.containsExactly("4010", "4020", "4030", "4040", "4050");
.containsExactly("4010", "4020", "4030", "4040", "4050", "4060");
assertThat(queryStrings(schema,
"SELECT table_name FROM information_schema.tables " +
"WHERE table_schema = current_schema() AND table_name IN (" +