feat: Flyway V3 pet_health 结构基线 + V4 字典种子 + pet 域错误码(T2-01)
- V3:pet_health schema 8 张表(breeds/pets/pet_owners/pet_weight_records/ vaccine_catalog/pet_vaccinations/health_events/care_reminders),列、CHECK、 部分唯一索引、updated_at 触发器与目标模型一致 - 强制裁剪:剥离 bootstrap SQL 1156~1166 行 4 条指向 marketplace 的跨 schema 外键(provider_id/booking_id 保留为裸可空 uuid,M5 迁移 marketplace 时补回) - ADR-010:health_event_media 不建(media 剪出 M2,纯增量表后续补零成本) - V4:breeds(28 条)与 vaccine_catalog(10 条)字典种子,生产参考数据走正式 迁移链不放 db/dev;正典目录内容由产品侧供稿(D2-6) - ErrorCode 预置 pets 域四码:40300 PET_ACCESS_DENIED、40401 PET_NOT_FOUND、 40402 RECORD_NOT_FOUND、40902 VERSION_CONFLICT(第二波接口直接消费) - 新增 PetHealthMigrationIntegrationTest:干净 postgres:18 上全量迁移验证 表数、结构抽查、4 条 FK 确不存在、触发器与种子数据 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
package com.patbond.patbond.user.persistence;
|
||||
|
||||
import com.patbond.patbond.user.TestcontainersConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Flyway V3/V4 (pet_health schema baseline + dictionary seed) apply cleanly
|
||||
* on a postgres:18 container and produce the expected structure. Validates
|
||||
* the mandatory cross-schema FK cuts (ADR-013, T2-01): provider_id/booking_id
|
||||
* exist as bare nullable uuid columns without FKs to marketplace.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@Import(TestcontainersConfiguration.class)
|
||||
class PetHealthMigrationIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private JdbcClient jdbcClient;
|
||||
|
||||
@Test
|
||||
void v3CreatesPetHealthSchema() {
|
||||
String exists = jdbcClient.sql(
|
||||
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'pet_health'")
|
||||
.query(String.class)
|
||||
.optional()
|
||||
.orElse(null);
|
||||
assertThat(exists).isEqualTo("pet_health");
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3CreatesAllPetHealthTables() {
|
||||
int count = jdbcClient.sql(
|
||||
"SELECT COUNT(*) FROM information_schema.tables " +
|
||||
"WHERE table_schema = 'pet_health' AND table_type = 'BASE TABLE'")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
// 8 tables: breeds, pets, pet_owners, pet_weight_records, vaccine_catalog,
|
||||
// pet_vaccinations, health_events, care_reminders.
|
||||
// health_event_media is NOT created (ADR-010: media cut from M2).
|
||||
assertThat(count).isEqualTo(8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3PetsTableHasExpectedStructure() {
|
||||
// Sample columns and constraints spot-check
|
||||
String microchipType = jdbcClient.sql(
|
||||
"SELECT udt_name FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'pet_health' AND table_name = 'pets' " +
|
||||
"AND column_name = 'microchip_no'")
|
||||
.query(String.class)
|
||||
.single();
|
||||
assertThat(microchipType).isEqualTo("citext");
|
||||
|
||||
String versionDefault = jdbcClient.sql(
|
||||
"SELECT column_default FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'pet_health' AND table_name = 'pets' " +
|
||||
"AND column_name = 'version'")
|
||||
.query(String.class)
|
||||
.single();
|
||||
assertThat(versionDefault).isEqualTo("0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3VaccinationsColumnsExistWithoutMarketplaceFKs() {
|
||||
// provider_id and booking_id columns exist (bare nullable uuid)
|
||||
int colCount = jdbcClient.sql(
|
||||
"SELECT COUNT(*) FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'pet_health' AND table_name = 'pet_vaccinations' " +
|
||||
"AND column_name IN ('provider_id', 'booking_id')")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(colCount).isEqualTo(2);
|
||||
|
||||
// No FK constraints targeting marketplace (marketplace schema does not exist in M2)
|
||||
int fkCount = jdbcClient.sql(
|
||||
"SELECT COUNT(*) FROM information_schema.table_constraints " +
|
||||
"WHERE table_schema = 'pet_health' AND table_name = 'pet_vaccinations' " +
|
||||
"AND constraint_type = 'FOREIGN KEY' " +
|
||||
"AND (constraint_name LIKE '%provider%' OR constraint_name LIKE '%booking%')")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(fkCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3HealthEventsColumnsExistWithoutMarketplaceFKs() {
|
||||
// provider_id and booking_id columns exist (bare nullable uuid)
|
||||
int colCount = jdbcClient.sql(
|
||||
"SELECT COUNT(*) FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'pet_health' AND table_name = 'health_events' " +
|
||||
"AND column_name IN ('provider_id', 'booking_id')")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(colCount).isEqualTo(2);
|
||||
|
||||
// No FK constraints targeting marketplace
|
||||
int fkCount = jdbcClient.sql(
|
||||
"SELECT COUNT(*) FROM information_schema.table_constraints " +
|
||||
"WHERE table_schema = 'pet_health' AND table_name = 'health_events' " +
|
||||
"AND constraint_type = 'FOREIGN KEY' " +
|
||||
"AND (constraint_name LIKE '%provider%' OR constraint_name LIKE '%booking%')")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(fkCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3TriggersAreCreated() {
|
||||
// The four updated_at triggers on pets, pet_vaccinations, health_events, care_reminders
|
||||
int triggerCount = jdbcClient.sql(
|
||||
"SELECT COUNT(*) FROM information_schema.triggers " +
|
||||
"WHERE trigger_schema = 'pet_health' " +
|
||||
"AND trigger_name IN ('trg_pets_updated_at', 'trg_vaccinations_updated_at', " +
|
||||
"'trg_health_events_updated_at', 'trg_reminders_updated_at')")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(triggerCount).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void v4SeedsBreedsAndVaccineCatalog() {
|
||||
int breedCount = jdbcClient.sql("SELECT COUNT(*) FROM pet_health.breeds")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(breedCount).isGreaterThan(0).describedAs("V4 应插入 breeds 种子数据");
|
||||
|
||||
int vaccineCount = jdbcClient.sql("SELECT COUNT(*) FROM pet_health.vaccine_catalog")
|
||||
.query(Integer.class)
|
||||
.single();
|
||||
assertThat(vaccineCount).isGreaterThan(0).describedAs("V4 应插入 vaccine_catalog 种子数据");
|
||||
}
|
||||
|
||||
@Test
|
||||
void v4SeedsIncludeMixedDogAndCat() {
|
||||
// Spot-check a couple of known rows from V4
|
||||
String mixedDog = jdbcClient.sql(
|
||||
"SELECT display_name FROM pet_health.breeds WHERE code = 'mixed_dog'")
|
||||
.query(String.class)
|
||||
.optional()
|
||||
.orElse(null);
|
||||
assertThat(mixedDog).isEqualTo("中华田园犬");
|
||||
|
||||
String rabiesDog = jdbcClient.sql(
|
||||
"SELECT name FROM pet_health.vaccine_catalog WHERE code = 'rabies_dog'")
|
||||
.query(String.class)
|
||||
.optional()
|
||||
.orElse(null);
|
||||
assertThat(rabiesDog).isEqualTo("狂犬疫苗(犬)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user