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:
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
V3 baseline: pet_health schema (M2 宠物健康档案), extracted from
|
||||
patbond-doc/docs/database/patbond_postgresql.sql (reviewed target model,
|
||||
lines 333-554) per iteration-2 T2-01.
|
||||
|
||||
Scope notes:
|
||||
- 8 of the 9 pet_health objects are created here: breeds, pets, pet_owners,
|
||||
pet_weight_records, vaccine_catalog, pet_vaccinations, health_events,
|
||||
care_reminders.
|
||||
- pet_health.health_event_media is NOT created (ADR-010: media/attachments
|
||||
are cut from M2; the table's asset_id is a NOT NULL FK to media.assets and
|
||||
the media upload flow does not exist yet). It is a pure additive table and
|
||||
will be introduced by a later migration together with the media work.
|
||||
- Cross-schema FKs into marketplace are STRIPPED (mandatory cut, T2-01):
|
||||
the target model adds these four constraints at lines 1156-1166 —
|
||||
* fk_vaccinations_provider (pet_vaccinations.provider_id -> marketplace.providers)
|
||||
* fk_vaccinations_booking (pet_vaccinations.booking_id -> marketplace.bookings)
|
||||
* fk_health_events_provider (health_events.provider_id -> marketplace.providers)
|
||||
* fk_health_events_booking (health_events.booking_id -> marketplace.bookings)
|
||||
The marketplace schema belongs to M5 and is not migrated in M2. The four
|
||||
columns are kept as bare nullable uuid; the constraints will be re-added
|
||||
by the M5 migration that creates the marketplace schema (M5 补回).
|
||||
- FKs to identity.users and media.assets are kept as-is: both schemas exist
|
||||
since V1 (same precedent as media.assets.owner_user_id in V1).
|
||||
- updated_at triggers reuse platform.set_updated_at() created in V1.
|
||||
- Structure only; the breeds / vaccine_catalog dictionary seed rows are
|
||||
production reference data and live in V4 (not in db/dev).
|
||||
- Never edit this file after release; subsequent changes go into V4+.
|
||||
*/
|
||||
|
||||
CREATE SCHEMA pet_health;
|
||||
|
||||
COMMENT ON SCHEMA pet_health IS 'Pet profile, health facts, vaccines and reminders';
|
||||
|
||||
CREATE TABLE pet_health.breeds (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
species varchar(16) NOT NULL,
|
||||
code varchar(64) NOT NULL UNIQUE,
|
||||
display_name varchar(64) NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (id, species),
|
||||
CONSTRAINT ck_breeds_species CHECK (species IN ('dog', 'cat', 'other')),
|
||||
CONSTRAINT ck_breeds_names CHECK (
|
||||
code = btrim(code) AND display_name = btrim(display_name)
|
||||
AND char_length(code) BETWEEN 2 AND 64
|
||||
AND char_length(display_name) BETWEEN 1 AND 64
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_breeds_species_order ON pet_health.breeds (species, sort_order, id) WHERE enabled;
|
||||
|
||||
CREATE TABLE pet_health.pets (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name varchar(64) NOT NULL,
|
||||
species varchar(16) NOT NULL,
|
||||
breed_id uuid,
|
||||
custom_breed_name varchar(64),
|
||||
sex varchar(8) NOT NULL DEFAULT 'unknown',
|
||||
birth_date date,
|
||||
birth_date_estimated boolean NOT NULL DEFAULT false,
|
||||
personality varchar(64),
|
||||
avatar_asset_id uuid REFERENCES media.assets(id) ON DELETE SET NULL,
|
||||
microchip_no citext,
|
||||
sterilized_on date,
|
||||
status varchar(16) NOT NULL DEFAULT 'active',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
CONSTRAINT ck_pets_name CHECK (name = btrim(name) AND char_length(name) BETWEEN 1 AND 64),
|
||||
CONSTRAINT ck_pets_species CHECK (species IN ('dog', 'cat', 'other')),
|
||||
CONSTRAINT ck_pets_sex CHECK (sex IN ('male', 'female', 'unknown')),
|
||||
FOREIGN KEY (breed_id, species)
|
||||
REFERENCES pet_health.breeds(id, species) ON DELETE RESTRICT,
|
||||
CONSTRAINT ck_pets_breed CHECK (
|
||||
(breed_id IS NOT NULL AND custom_breed_name IS NULL)
|
||||
OR (
|
||||
breed_id IS NULL AND custom_breed_name IS NOT NULL
|
||||
AND char_length(btrim(custom_breed_name)) BETWEEN 1 AND 64
|
||||
)
|
||||
),
|
||||
CONSTRAINT ck_pets_birth_date CHECK (birth_date IS NULL OR birth_date >= DATE '1990-01-01'),
|
||||
CONSTRAINT ck_pets_status CHECK (status IN ('active', 'lost', 'deceased', 'archived', 'deleted')),
|
||||
CONSTRAINT ck_pets_version CHECK (version >= 0),
|
||||
CONSTRAINT ck_pets_deleted CHECK ((status = 'deleted') = (deleted_at IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_pets_microchip
|
||||
ON pet_health.pets (microchip_no)
|
||||
WHERE microchip_no IS NOT NULL;
|
||||
CREATE INDEX ix_pets_breed_species ON pet_health.pets (breed_id, species);
|
||||
CREATE INDEX ix_pets_avatar ON pet_health.pets (avatar_asset_id);
|
||||
CREATE INDEX ix_pets_active_updated ON pet_health.pets (updated_at DESC, id) WHERE status = 'active';
|
||||
|
||||
CREATE TABLE pet_health.pet_owners (
|
||||
pet_id uuid NOT NULL REFERENCES pet_health.pets(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES identity.users(id) ON DELETE RESTRICT,
|
||||
role varchar(16) NOT NULL DEFAULT 'owner',
|
||||
is_primary boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (pet_id, user_id),
|
||||
CONSTRAINT ck_pet_owners_role CHECK (role IN ('owner', 'caregiver', 'viewer'))
|
||||
);
|
||||
|
||||
CREATE INDEX ix_pet_owners_user ON pet_health.pet_owners (user_id, pet_id);
|
||||
CREATE UNIQUE INDEX uq_pet_primary_owner
|
||||
ON pet_health.pet_owners (pet_id)
|
||||
WHERE is_primary;
|
||||
|
||||
CREATE TABLE pet_health.pet_weight_records (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pet_id uuid NOT NULL REFERENCES pet_health.pets(id) ON DELETE CASCADE,
|
||||
weight_kg numeric(6,2) NOT NULL,
|
||||
measured_at timestamptz NOT NULL,
|
||||
source varchar(16) NOT NULL DEFAULT 'manual',
|
||||
note varchar(500),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ck_pet_weight CHECK (weight_kg > 0 AND weight_kg <= 500),
|
||||
CONSTRAINT ck_pet_weight_source CHECK (source IN ('manual', 'clinic', 'device'))
|
||||
);
|
||||
|
||||
CREATE INDEX ix_pet_weight_pet_measured
|
||||
ON pet_health.pet_weight_records (pet_id, measured_at DESC, id DESC);
|
||||
|
||||
CREATE TABLE pet_health.vaccine_catalog (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code varchar(64) NOT NULL UNIQUE,
|
||||
name varchar(128) NOT NULL,
|
||||
species varchar(16) NOT NULL,
|
||||
description varchar(500),
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ck_vaccine_species CHECK (species IN ('dog', 'cat', 'other')),
|
||||
CONSTRAINT ck_vaccine_names CHECK (
|
||||
code = btrim(code) AND name = btrim(name)
|
||||
AND char_length(code) BETWEEN 2 AND 64
|
||||
AND char_length(name) BETWEEN 1 AND 128
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_vaccine_catalog_species ON pet_health.vaccine_catalog (species, name) WHERE enabled;
|
||||
|
||||
CREATE TABLE pet_health.pet_vaccinations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pet_id uuid NOT NULL REFERENCES pet_health.pets(id) ON DELETE CASCADE,
|
||||
vaccine_id uuid NOT NULL REFERENCES pet_health.vaccine_catalog(id) ON DELETE RESTRICT,
|
||||
series_key varchar(64) NOT NULL,
|
||||
dose_no smallint NOT NULL,
|
||||
dose_label varchar(64),
|
||||
status varchar(16) NOT NULL DEFAULT 'scheduled',
|
||||
planned_on date,
|
||||
administered_on date,
|
||||
next_due_on date,
|
||||
-- provider_id / booking_id: bare nullable uuid, FKs to marketplace stripped (M5 补回)
|
||||
provider_id uuid,
|
||||
provider_name_snapshot varchar(128),
|
||||
manufacturer varchar(128),
|
||||
batch_no varchar(64),
|
||||
certificate_asset_id uuid REFERENCES media.assets(id) ON DELETE SET NULL,
|
||||
booking_id uuid,
|
||||
notes varchar(1000),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
CONSTRAINT ck_vaccination_series CHECK (series_key = btrim(series_key) AND char_length(series_key) BETWEEN 1 AND 64),
|
||||
CONSTRAINT ck_vaccination_dose CHECK (dose_no > 0),
|
||||
CONSTRAINT ck_vaccination_status CHECK (status IN ('scheduled', 'completed', 'cancelled')),
|
||||
CONSTRAINT ck_vaccination_dates CHECK (
|
||||
(status = 'completed' AND administered_on IS NOT NULL)
|
||||
OR (status = 'scheduled' AND administered_on IS NULL AND planned_on IS NOT NULL)
|
||||
OR (status = 'cancelled' AND administered_on IS NULL)
|
||||
),
|
||||
CONSTRAINT ck_vaccination_next_due CHECK (
|
||||
next_due_on IS NULL OR administered_on IS NULL OR next_due_on >= administered_on
|
||||
),
|
||||
CONSTRAINT ck_vaccination_version CHECK (version >= 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_pet_vaccination_dose
|
||||
ON pet_health.pet_vaccinations (pet_id, vaccine_id, series_key, dose_no)
|
||||
WHERE status <> 'cancelled';
|
||||
CREATE INDEX ix_vaccinations_pet ON pet_health.pet_vaccinations (pet_id);
|
||||
CREATE INDEX ix_vaccinations_vaccine ON pet_health.pet_vaccinations (vaccine_id);
|
||||
CREATE INDEX ix_vaccinations_certificate ON pet_health.pet_vaccinations (certificate_asset_id);
|
||||
CREATE INDEX ix_vaccinations_provider ON pet_health.pet_vaccinations (provider_id);
|
||||
CREATE INDEX ix_vaccinations_booking ON pet_health.pet_vaccinations (booking_id);
|
||||
CREATE INDEX ix_vaccinations_pet_completed
|
||||
ON pet_health.pet_vaccinations (pet_id, administered_on DESC, id DESC)
|
||||
WHERE status = 'completed';
|
||||
CREATE INDEX ix_vaccinations_due
|
||||
ON pet_health.pet_vaccinations (planned_on, pet_id)
|
||||
WHERE status = 'scheduled';
|
||||
|
||||
CREATE TABLE pet_health.health_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pet_id uuid NOT NULL REFERENCES pet_health.pets(id) ON DELETE CASCADE,
|
||||
event_type varchar(24) NOT NULL,
|
||||
occurred_at timestamptz NOT NULL,
|
||||
title varchar(160) NOT NULL,
|
||||
notes text,
|
||||
amount_cents bigint,
|
||||
-- provider_id / booking_id: bare nullable uuid, FKs to marketplace stripped (M5 补回)
|
||||
provider_id uuid,
|
||||
provider_name_snapshot varchar(128),
|
||||
booking_id uuid,
|
||||
created_by_user_id uuid NOT NULL REFERENCES identity.users(id) ON DELETE RESTRICT,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
CONSTRAINT ck_health_event_type CHECK (
|
||||
event_type IN ('medical', 'feeding', 'deworming', 'grooming', 'measurement', 'note')
|
||||
),
|
||||
CONSTRAINT ck_health_event_title CHECK (title = btrim(title) AND char_length(title) BETWEEN 1 AND 160),
|
||||
CONSTRAINT ck_health_event_amount CHECK (amount_cents IS NULL OR amount_cents >= 0),
|
||||
CONSTRAINT ck_health_event_version CHECK (version >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_health_events_pet_time ON pet_health.health_events (pet_id, occurred_at DESC, id DESC);
|
||||
CREATE INDEX ix_health_events_creator ON pet_health.health_events (created_by_user_id, created_at DESC);
|
||||
CREATE INDEX ix_health_events_provider ON pet_health.health_events (provider_id);
|
||||
CREATE INDEX ix_health_events_booking ON pet_health.health_events (booking_id);
|
||||
|
||||
CREATE TABLE pet_health.care_reminders (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pet_id uuid NOT NULL REFERENCES pet_health.pets(id) ON DELETE CASCADE,
|
||||
reminder_type varchar(24) NOT NULL,
|
||||
title varchar(160) NOT NULL,
|
||||
due_at timestamptz NOT NULL,
|
||||
status varchar(16) NOT NULL DEFAULT 'pending',
|
||||
completed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ck_care_reminder_type CHECK (
|
||||
reminder_type IN ('deworming', 'checkup', 'medication', 'other')
|
||||
),
|
||||
CONSTRAINT ck_care_reminder_status CHECK (status IN ('pending', 'completed', 'dismissed')),
|
||||
CONSTRAINT ck_care_reminder_completed CHECK ((status = 'completed') = (completed_at IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE INDEX ix_care_reminders_pet ON pet_health.care_reminders (pet_id);
|
||||
CREATE INDEX ix_care_reminders_due
|
||||
ON pet_health.care_reminders (due_at, pet_id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- Automatic updated_at maintenance (function created in V1). Business version
|
||||
-- increments remain explicit so optimistic locking stays visible in
|
||||
-- repository update statements.
|
||||
CREATE TRIGGER trg_pets_updated_at BEFORE UPDATE ON pet_health.pets
|
||||
FOR EACH ROW EXECUTE FUNCTION platform.set_updated_at();
|
||||
CREATE TRIGGER trg_vaccinations_updated_at BEFORE UPDATE ON pet_health.pet_vaccinations
|
||||
FOR EACH ROW EXECUTE FUNCTION platform.set_updated_at();
|
||||
CREATE TRIGGER trg_health_events_updated_at BEFORE UPDATE ON pet_health.health_events
|
||||
FOR EACH ROW EXECUTE FUNCTION platform.set_updated_at();
|
||||
CREATE TRIGGER trg_reminders_updated_at BEFORE UPDATE ON pet_health.care_reminders
|
||||
FOR EACH ROW EXECUTE FUNCTION platform.set_updated_at();
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
V4: breeds / vaccine_catalog dictionary seed (iteration-2 T2-01, D2-6).
|
||||
|
||||
These are production reference data (business rows the app FKs against),
|
||||
not development fixtures, so they live in the versioned chain rather than
|
||||
db/dev. The set is a curated development-grade starter list (10~20 common
|
||||
entries per species); the authoritative catalog is a separate content task
|
||||
owned by the product side (D2-6) and will land as later migrations.
|
||||
|
||||
Rows use the column DEFAULT gen_random_uuid() for ids: dictionary rows are
|
||||
server-born reference data, the application-side UUIDv7 rule applies to
|
||||
rows created through the API.
|
||||
*/
|
||||
|
||||
INSERT INTO pet_health.breeds (species, code, display_name, sort_order) VALUES
|
||||
('dog', 'mixed_dog', '中华田园犬', 10),
|
||||
('dog', 'golden_retriever', '金毛寻回犬', 20),
|
||||
('dog', 'labrador_retriever', '拉布拉多犬', 30),
|
||||
('dog', 'poodle', '贵宾犬', 40),
|
||||
('dog', 'bichon_frise', '比熊犬', 50),
|
||||
('dog', 'pomeranian', '博美犬', 60),
|
||||
('dog', 'welsh_corgi', '威尔士柯基犬', 70),
|
||||
('dog', 'shiba_inu', '柴犬', 80),
|
||||
('dog', 'siberian_husky', '哈士奇', 90),
|
||||
('dog', 'samoyed', '萨摩耶犬', 100),
|
||||
('dog', 'border_collie', '边境牧羊犬', 110),
|
||||
('dog', 'french_bulldog', '法国斗牛犬', 120),
|
||||
('dog', 'chihuahua', '吉娃娃', 130),
|
||||
('dog', 'dachshund', '腊肠犬', 140),
|
||||
('dog', 'schnauzer', '雪纳瑞', 150),
|
||||
('dog', 'german_shepherd', '德国牧羊犬', 160),
|
||||
('cat', 'mixed_cat', '中华田园猫', 10),
|
||||
('cat', 'british_shorthair', '英国短毛猫', 20),
|
||||
('cat', 'american_shorthair', '美国短毛猫', 30),
|
||||
('cat', 'ragdoll', '布偶猫', 40),
|
||||
('cat', 'siamese', '暹罗猫', 50),
|
||||
('cat', 'persian', '波斯猫', 60),
|
||||
('cat', 'maine_coon', '缅因猫', 70),
|
||||
('cat', 'scottish_fold', '苏格兰折耳猫', 80),
|
||||
('cat', 'exotic_shorthair', '异国短毛猫', 90),
|
||||
('cat', 'russian_blue', '俄罗斯蓝猫', 100),
|
||||
('cat', 'bengal', '孟加拉豹猫', 110),
|
||||
('cat', 'sphynx', '斯芬克斯猫', 120);
|
||||
|
||||
INSERT INTO pet_health.vaccine_catalog (code, name, species, description) VALUES
|
||||
('canine_2in1', '犬二联疫苗', 'dog', '预防犬瘟热、犬细小病毒'),
|
||||
('canine_4in1', '犬四联疫苗', 'dog', '预防犬瘟热、犬细小病毒、犬传染性肝炎、副流感'),
|
||||
('canine_5in1', '犬五联疫苗', 'dog', '预防犬瘟热、犬细小病毒、犬传染性肝炎、副流感、腺病毒 II 型'),
|
||||
('canine_8in1', '犬八联疫苗', 'dog', '五联基础上增加钩端螺旋体等'),
|
||||
('rabies_dog', '狂犬疫苗(犬)', 'dog', '狂犬病毒灭活疫苗,首免后按年加强'),
|
||||
('kennel_cough', '犬窝咳疫苗', 'dog', '预防支气管败血波氏杆菌引起的犬窝咳'),
|
||||
('feline_3in1', '猫三联疫苗', 'cat', '预防猫瘟、猫杯状病毒、猫疱疹病毒'),
|
||||
('rabies_cat', '狂犬疫苗(猫)', 'cat', '狂犬病毒灭活疫苗,首免后按年加强'),
|
||||
('felv', '猫白血病疫苗', 'cat', '预防猫白血病病毒感染'),
|
||||
('feline_chlamydia', '猫衣原体疫苗', 'cat', '预防猫衣原体感染');
|
||||
+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