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 birthYear;
|
||||||
private Integer deathYear;
|
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)
|
@Column(name = "family_member", nullable = false)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@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);
|
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