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:
Marcel
2026-05-28 15:20:24 +02:00
parent 1cb05697cc
commit f22508ca91
3 changed files with 66 additions and 0 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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();
}
}