feat(person): add nullable generation column to persons (#689)
Flyway V70: SMALLINT generation column with CHECK(0..10) and partial index over non-null rows. Person.generation field surfaces it through the JPA model. Pre-import rows and persons outside the curated family graph legitimately stay null; the canonical importer (next commits) back-fills via preferHuman so a human-edited value is never lost. Refs #689 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,13 @@ public class Person {
|
||||
private Integer birthYear;
|
||||
private Integer deathYear;
|
||||
|
||||
// Hand-curated generation index from canonical-persons.xlsx (G 0 = oldest).
|
||||
// Nullable for persons outside the curated family graph. Drives the
|
||||
// Stammbaum strict-rank seed (see #689) and re-import preserves human
|
||||
// edits via PersonService.preferHuman (ADR-025).
|
||||
@Column(name = "generation")
|
||||
private Integer generation;
|
||||
|
||||
@Column(name = "family_member", nullable = false)
|
||||
@Builder.Default
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- #689: persist the hand-curated "G 0…G 5" generation index from
|
||||
-- canonical-persons.xlsx so the Stammbaum layout can use it as a strict
|
||||
-- rank anchor (replacing the current iterative longest-path heuristic that
|
||||
-- silently misplaces loose spouses with their own parents in the graph).
|
||||
--
|
||||
-- Nullable: pre-import rows and persons outside the curated family graph
|
||||
-- legitimately have no generation. The canonical importer back-fills via
|
||||
-- preferHuman on the next run; a human-edited value is never overwritten
|
||||
-- (see ADR-025).
|
||||
|
||||
ALTER TABLE persons ADD COLUMN generation SMALLINT;
|
||||
|
||||
-- Allowlist of valid generation indices. The upper bound is intentionally
|
||||
-- loose — current data tops out at G 5, but a future G 6 → G 10 widening
|
||||
-- needs no migration. A G −1 ancestor would require a separate one-shot
|
||||
-- shift migration (out of scope here; the layout's normalise step already
|
||||
-- handles negative seeds at render time).
|
||||
ALTER TABLE persons ADD CONSTRAINT chk_generation_range
|
||||
CHECK (generation IS NULL OR generation BETWEEN 0 AND 10);
|
||||
|
||||
-- Partial index: only the curated rows (≈ 163 of 1,105) ever get a value,
|
||||
-- and the layout only ever queries for non-null rows.
|
||||
CREATE INDEX idx_persons_generation ON persons (generation)
|
||||
WHERE generation IS NOT NULL;
|
||||
@@ -672,4 +672,39 @@ class PersonRepositoryTest {
|
||||
|
||||
assertThat(slice.get(0).getDocumentCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
// ─── generation column (#689) ──────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void save_persistsGeneration_andFindByIdReturnsSameGeneration() {
|
||||
Person person = Person.builder()
|
||||
.firstName("Walter")
|
||||
.lastName("Raddatz")
|
||||
.generation(3)
|
||||
.build();
|
||||
|
||||
Person saved = personRepository.save(person);
|
||||
entityManager.flush();
|
||||
entityManager.clear();
|
||||
|
||||
Optional<Person> found = personRepository.findById(saved.getId());
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getGeneration()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_allowsNullGeneration_existingRowsRemainNull() {
|
||||
Person person = Person.builder()
|
||||
.firstName("Anonym")
|
||||
.lastName("Person")
|
||||
.build();
|
||||
|
||||
Person saved = personRepository.save(person);
|
||||
entityManager.flush();
|
||||
entityManager.clear();
|
||||
|
||||
Optional<Person> found = personRepository.findById(saved.getId());
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getGeneration()).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user