From 51fe1bbdec90f68eb9014047ee01b166668e7008 Mon Sep 17 00:00:00 2001 From: Florent Biville Date: Wed, 25 Sep 2024 20:03:40 +0100 Subject: [PATCH 1/4] refactor: define extractor for family relationships --- .../neo4j/data/importer/GedcomImporter.java | 44 +++++++------------ .../extractors/DefaultFamilyExtractor.java | 23 ++++++++++ .../importer/extractors/FamilyExtractor.java | 22 ++++++++++ .../importer/extractors/FamilyExtractors.java | 12 +++++ .../data/importer/{ => extractors}/Lists.java | 2 +- 5 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java create mode 100644 src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java create mode 100644 src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java rename src/main/java/com/neo4j/data/importer/{ => extractors}/Lists.java (89%) diff --git a/src/main/java/com/neo4j/data/importer/GedcomImporter.java b/src/main/java/com/neo4j/data/importer/GedcomImporter.java index b6789bc..61a9a00 100644 --- a/src/main/java/com/neo4j/data/importer/GedcomImporter.java +++ b/src/main/java/com/neo4j/data/importer/GedcomImporter.java @@ -1,14 +1,12 @@ package com.neo4j.data.importer; -import com.neo4j.data.importer.Lists.Pair; +import com.neo4j.data.importer.extractors.FamilyExtractors; import com.neo4j.data.importer.extractors.PersonExtractors; import java.io.File; import java.io.IOException; -import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.folg.gedcom.model.Gedcom; -import org.folg.gedcom.model.SpouseRef; import org.folg.gedcom.parser.ModelParser; import org.neo4j.common.DependencyResolver; import org.neo4j.configuration.Config; @@ -49,33 +47,23 @@ public Stream loadGedcom(@Name("file") String file) throws IOExcepti statistics.addNodesCreated(personsStats.getNodesCreated()); }); + var familyExtractors = new FamilyExtractors(); model.getFamilies().forEach(family -> { - List spouseReferences1 = - family.getHusbandRefs().stream().map(SpouseRef::getRef).toList(); - List spouseReferences2 = - family.getWifeRefs().stream().map(SpouseRef::getRef).toList(); - List> couples = Lists.crossProduct(spouseReferences1, spouseReferences2); - List childrenReferences = - family.getChildRefs().stream().map(SpouseRef::getRef).toList(); - couples.forEach(couple -> { - var stats = tx.execute( - """ - MATCH (spouse1:Person {id: $spouseId1}), (spouse2:Person {id: $spouseId2}) - CREATE (spouse1)-[:IS_MARRIED_TO]->(spouse2) - WITH spouse1, spouse2 - UNWIND $childIds AS childId - MATCH (child:Person {id: childId}) - CREATE (child)-[:IS_CHILD_OF]->(spouse1) - CREATE (child)-[:IS_CHILD_OF]->(spouse2) - """, - Map.of( - "spouseId1", couple.left(), - "spouseId2", couple.right(), - "childIds", childrenReferences)) - .getQueryStatistics(); + var stats = tx.execute( + """ + UNWIND $spouseIdPairs AS spousePair + MATCH (spouse1:Person {id: spousePair.id1}), (spouse2:Person {id: spousePair.id2}) + CREATE (spouse1)-[:IS_MARRIED_TO]->(spouse2) + WITH spouse1, spouse2 + UNWIND $childIds AS childId + MATCH (child:Person {id: childId}) + CREATE (child)-[:IS_CHILD_OF]->(spouse1) + CREATE (child)-[:IS_CHILD_OF]->(spouse2) + """, + familyExtractors.get().apply(family)) + .getQueryStatistics(); - statistics.addRelationshipsCreated(stats.getRelationshipsCreated()); - }); + statistics.addRelationshipsCreated(stats.getRelationshipsCreated()); }); tx.commit(); diff --git a/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java b/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java new file mode 100644 index 0000000..651cb3a --- /dev/null +++ b/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java @@ -0,0 +1,23 @@ +package com.neo4j.data.importer.extractors; + +import com.neo4j.data.importer.extractors.Lists.Pair; +import java.util.List; +import org.folg.gedcom.model.Family; +import org.folg.gedcom.model.SpouseRef; + +class DefaultFamilyExtractor implements FamilyExtractor { + + @Override + public List> spouseReferences(Family family) { + List spouseReferences1 = + family.getHusbandRefs().stream().map(SpouseRef::getRef).toList(); + List spouseReferences2 = + family.getWifeRefs().stream().map(SpouseRef::getRef).toList(); + return Lists.crossProduct(spouseReferences1, spouseReferences2); + } + + @Override + public List childReferences(Family family) { + return family.getChildRefs().stream().map(SpouseRef::getRef).toList(); + } +} diff --git a/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java new file mode 100644 index 0000000..5cec06d --- /dev/null +++ b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java @@ -0,0 +1,22 @@ +package com.neo4j.data.importer.extractors; + +import com.neo4j.data.importer.extractors.Lists.Pair; +import java.util.List; +import java.util.Map; +import org.folg.gedcom.model.Family; + +public interface FamilyExtractor extends AttributeExtractor { + + List> spouseReferences(Family family); + + List childReferences(Family family); + + default Map apply(Family family) { + var spouseIds = spouseReferences(family).stream() + .map(couple -> Map.of( + "id1", couple.left(), + "id2", couple.right())) + .toList(); + return Map.of("spouseIdPairs", spouseIds, "childIds", childReferences(family)); + } +} diff --git a/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java new file mode 100644 index 0000000..9d94b01 --- /dev/null +++ b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java @@ -0,0 +1,12 @@ +package com.neo4j.data.importer.extractors; + +import java.util.function.Supplier; +import org.folg.gedcom.model.Family; + +public class FamilyExtractors implements Supplier> { + + @Override + public AttributeExtractor get() { + return new DefaultFamilyExtractor(); + } +} diff --git a/src/main/java/com/neo4j/data/importer/Lists.java b/src/main/java/com/neo4j/data/importer/extractors/Lists.java similarity index 89% rename from src/main/java/com/neo4j/data/importer/Lists.java rename to src/main/java/com/neo4j/data/importer/extractors/Lists.java index dc53c2c..75759b5 100644 --- a/src/main/java/com/neo4j/data/importer/Lists.java +++ b/src/main/java/com/neo4j/data/importer/extractors/Lists.java @@ -1,4 +1,4 @@ -package com.neo4j.data.importer; +package com.neo4j.data.importer.extractors; import java.util.List; From 9a4c911f00eb92a47189214218ffdcd750e7b7d4 Mon Sep 17 00:00:00 2001 From: Florent Biville Date: Wed, 25 Sep 2024 21:35:43 +0100 Subject: [PATCH 2/4] feat: support divorces, add marriage information The existing `IS_MARRIED_TO` has been changed to `IS_SPOUSE_OF`. `IS_SPOUSE_OF` is inferred from family's `HUSB` / `WIFE` tags. `IS_MARRIED_TO` relationships are now only created if there are marriage family event information (`MARR` Gedcom tag). `DIVORCED` relationships are created from family divorce event information (`DIV` Gedcom tag). --- .../neo4j/data/importer/GedcomImporter.java | 24 +++-- .../extractors/DefaultFamilyExtractor.java | 13 +++ .../extractors/DefaultPersonExtractor.java | 44 +-------- .../data/importer/extractors/EventFacts.java | 88 +++++++++++++++++ .../importer/extractors/FamilyExtractor.java | 10 +- .../importer/extractors/FamilyExtractors.java | 9 +- .../importer/extractors/PersonExtractors.java | 7 +- .../data/importer/GedcomImporterTest.java | 95 +++++++++++++++---- .../ged-files/DetailedMarriageDivorceInfo.ged | 36 +++++++ 9 files changed, 256 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/neo4j/data/importer/extractors/EventFacts.java create mode 100644 src/test/resources/ged-files/DetailedMarriageDivorceInfo.ged diff --git a/src/main/java/com/neo4j/data/importer/GedcomImporter.java b/src/main/java/com/neo4j/data/importer/GedcomImporter.java index 61a9a00..9795ae2 100644 --- a/src/main/java/com/neo4j/data/importer/GedcomImporter.java +++ b/src/main/java/com/neo4j/data/importer/GedcomImporter.java @@ -1,5 +1,6 @@ package com.neo4j.data.importer; +import com.joestelmach.natty.Parser; import com.neo4j.data.importer.extractors.FamilyExtractors; import com.neo4j.data.importer.extractors.PersonExtractors; import java.io.File; @@ -36,7 +37,8 @@ public Stream loadGedcom(@Name("file") String file) throws IOExcepti var filePath = rebuildPath(file); var model = loadModel(filePath); - var personExtractors = new PersonExtractors(model); + var dateParser = new Parser(); + var personExtractors = new PersonExtractors(dateParser, model); var statistics = new Statistics(); try (Transaction tx = db.beginTx()) { model.getPeople().forEach(person -> { @@ -47,20 +49,30 @@ public Stream loadGedcom(@Name("file") String file) throws IOExcepti statistics.addNodesCreated(personsStats.getNodesCreated()); }); - var familyExtractors = new FamilyExtractors(); + var familyExtractors = new FamilyExtractors(dateParser); model.getFamilies().forEach(family -> { + var attributes = familyExtractors.get().apply(family); var stats = tx.execute( """ - UNWIND $spouseIdPairs AS spousePair - MATCH (spouse1:Person {id: spousePair.id1}), (spouse2:Person {id: spousePair.id2}) - CREATE (spouse1)-[:IS_MARRIED_TO]->(spouse2) + UNWIND $spouseIdPairs AS spouseInfo + MATCH (spouse1:Person {id: spouseInfo.id1}), + (spouse2:Person {id: spouseInfo.id2}) + CREATE (spouse1)-[r:IS_SPOUSE_OF]->(spouse2) + FOREACH (marriageInfo IN spouseInfo.events["MARR"] | + CREATE (spouse1)-[r:IS_MARRIED_TO]->(spouse2) + SET r = marriageInfo + ) + FOREACH (divorceInfo IN spouseInfo.events["DIV"] | + CREATE (spouse1)-[r:DIVORCED]->(spouse2) + SET r = divorceInfo + ) WITH spouse1, spouse2 UNWIND $childIds AS childId MATCH (child:Person {id: childId}) CREATE (child)-[:IS_CHILD_OF]->(spouse1) CREATE (child)-[:IS_CHILD_OF]->(spouse2) """, - familyExtractors.get().apply(family)) + attributes) .getQueryStatistics(); statistics.addRelationshipsCreated(stats.getRelationshipsCreated()); diff --git a/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java b/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java index 651cb3a..6c71bce 100644 --- a/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java +++ b/src/main/java/com/neo4j/data/importer/extractors/DefaultFamilyExtractor.java @@ -1,12 +1,20 @@ package com.neo4j.data.importer.extractors; +import com.joestelmach.natty.Parser; import com.neo4j.data.importer.extractors.Lists.Pair; import java.util.List; +import java.util.Map; import org.folg.gedcom.model.Family; import org.folg.gedcom.model.SpouseRef; class DefaultFamilyExtractor implements FamilyExtractor { + private final Parser dateParser; + + DefaultFamilyExtractor(Parser dateParser) { + this.dateParser = dateParser; + } + @Override public List> spouseReferences(Family family) { List spouseReferences1 = @@ -16,6 +24,11 @@ public List> spouseReferences(Family family) { return Lists.crossProduct(spouseReferences1, spouseReferences2); } + @Override + public Map>> familyEvents(Family family) { + return EventFacts.extract(family.getEventsFacts(), dateParser); + } + @Override public List childReferences(Family family) { return family.getChildRefs().stream().map(SpouseRef::getRef).toList(); diff --git a/src/main/java/com/neo4j/data/importer/extractors/DefaultPersonExtractor.java b/src/main/java/com/neo4j/data/importer/extractors/DefaultPersonExtractor.java index b99d804..8eed062 100644 --- a/src/main/java/com/neo4j/data/importer/extractors/DefaultPersonExtractor.java +++ b/src/main/java/com/neo4j/data/importer/extractors/DefaultPersonExtractor.java @@ -1,10 +1,7 @@ package com.neo4j.data.importer.extractors; import com.joestelmach.natty.Parser; -import java.time.LocalDate; -import java.time.ZoneId; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -19,8 +16,8 @@ class DefaultPersonExtractor implements PersonExtractor { private final Parser dateParser; - public DefaultPersonExtractor() { - this.dateParser = new Parser(); + public DefaultPersonExtractor(Parser dateParser) { + this.dateParser = dateParser; } @Override @@ -50,42 +47,7 @@ public Optional gender(Person person) { @Override public Map facts(Person person) { - Map attributes = new HashMap<>(); - person.getEventsFacts().forEach(eventFact -> { - String factName = eventFact.getDisplayType().toLowerCase(Locale.ROOT); - String date = eventFact.getDate(); - if (date != null) { - attributes.put(String.format("raw_%s_date", factName), date); - var localDate = parseLocalDate(date); - if (localDate != null) { - attributes.put(String.format("%s_date", factName), localDate); - } - } - - String place = eventFact.getPlace(); - if (place != null) { - attributes.put(factName + "_" + "location", place); - } - }); - return attributes; - } - - private LocalDate parseLocalDate(String date) { - var parse = dateParser.parse(date); - if (parse.size() != 1) { - return null; - } - - var dateGroup = parse.get(0); - if (dateGroup.getDates().size() != 1 || dateGroup.isDateInferred()) { - // Dates should be parsed explicitly from input. - // Inferred dates are likely to be set using current time and therefore incorrect. - return null; - } - - var parsedDate = dateGroup.getDates().get(0); - - return LocalDate.ofInstant(parsedDate.toInstant(), ZoneId.systemDefault()); + return EventFacts.extractFlat(person.getEventsFacts(), dateParser); } private static List extractNames(Person person, Function nameFn) { diff --git a/src/main/java/com/neo4j/data/importer/extractors/EventFacts.java b/src/main/java/com/neo4j/data/importer/extractors/EventFacts.java new file mode 100644 index 0000000..cb16256 --- /dev/null +++ b/src/main/java/com/neo4j/data/importer/extractors/EventFacts.java @@ -0,0 +1,88 @@ +package com.neo4j.data.importer.extractors; + +import com.joestelmach.natty.Parser; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import org.folg.gedcom.model.EventFact; + +class EventFacts { + + /** + * extractFlat extracts all events' place and location into a single, "flat" map + */ + public static Map extractFlat(List facts, Parser dateParser) { + var attributes = new HashMap(); + facts.forEach(fact -> { + attributes.putAll(extractFact( + fact, + dateParser, + (eventFact) -> + String.format("%s_", eventFact.getDisplayType().toLowerCase(Locale.ROOT)))); + }); + return attributes; + } + + /** + * extract all events' place and location, categorized by event tag + */ + public static Map>> extract(List facts, Parser dateParser) { + var attributes = new HashMap>>(); + for (EventFact fact : facts) { + var eventsPerTag = + attributes.computeIfAbsent(fact.getTag().toUpperCase(Locale.ROOT), (key) -> new ArrayList<>()); + eventsPerTag.add(extractFact(fact, dateParser)); + } + return attributes; + } + + private static Map extractFact(EventFact eventFact, Parser dateParser) { + return extractFact(eventFact, dateParser, (fact) -> ""); + } + + private static Map extractFact( + EventFact fact, Parser dateParser, Function keyQualifierFn) { + var attributes = new HashMap(2); + String date = fact.getDate(); + String keyQualifier = keyQualifierFn.apply(fact); + String type = fact.getType(); + if (type != null) { + attributes.put(String.format("%stype", keyQualifier), type); + } + if (date != null) { + attributes.put(String.format("raw_%sdate", keyQualifier), date); + var localDate = parseLocalDate(dateParser, date); + if (localDate != null) { + attributes.put(String.format("%sdate", keyQualifier), localDate); + } + } + String place = fact.getPlace(); + if (place != null) { + attributes.put(String.format("%slocation", keyQualifier), place); + } + return attributes; + } + + private static LocalDate parseLocalDate(Parser dateParser, String date) { + var parse = dateParser.parse(date); + if (parse.size() != 1) { + return null; + } + + var dateGroup = parse.get(0); + if (dateGroup.getDates().size() != 1 || dateGroup.isDateInferred()) { + // Dates should be parsed explicitly from input. + // Inferred dates are likely to be set using current time and therefore incorrect. + return null; + } + + var parsedDate = dateGroup.getDates().get(0); + + return LocalDate.ofInstant(parsedDate.toInstant(), ZoneId.systemDefault()); + } +} diff --git a/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java index 5cec06d..d2545e1 100644 --- a/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java +++ b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractor.java @@ -9,14 +9,18 @@ public interface FamilyExtractor extends AttributeExtractor { List> spouseReferences(Family family); + Map>> familyEvents(Family family); + List childReferences(Family family); default Map apply(Family family) { - var spouseIds = spouseReferences(family).stream() + var familyEvents = familyEvents(family); + var spouseInfo = spouseReferences(family).stream() .map(couple -> Map.of( "id1", couple.left(), - "id2", couple.right())) + "id2", couple.right(), + "events", familyEvents)) .toList(); - return Map.of("spouseIdPairs", spouseIds, "childIds", childReferences(family)); + return Map.of("spouseIdPairs", spouseInfo, "childIds", childReferences(family)); } } diff --git a/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java index 9d94b01..1df6e75 100644 --- a/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java +++ b/src/main/java/com/neo4j/data/importer/extractors/FamilyExtractors.java @@ -1,12 +1,19 @@ package com.neo4j.data.importer.extractors; +import com.joestelmach.natty.Parser; import java.util.function.Supplier; import org.folg.gedcom.model.Family; public class FamilyExtractors implements Supplier> { + private final Parser dateParser; + + public FamilyExtractors(Parser dateParser) { + this.dateParser = dateParser; + } + @Override public AttributeExtractor get() { - return new DefaultFamilyExtractor(); + return new DefaultFamilyExtractor(dateParser); } } diff --git a/src/main/java/com/neo4j/data/importer/extractors/PersonExtractors.java b/src/main/java/com/neo4j/data/importer/extractors/PersonExtractors.java index f97eb7c..fb974d1 100644 --- a/src/main/java/com/neo4j/data/importer/extractors/PersonExtractors.java +++ b/src/main/java/com/neo4j/data/importer/extractors/PersonExtractors.java @@ -1,5 +1,6 @@ package com.neo4j.data.importer.extractors; +import com.joestelmach.natty.Parser; import java.util.Locale; import java.util.function.Supplier; import org.folg.gedcom.model.Gedcom; @@ -7,15 +8,17 @@ public class PersonExtractors implements Supplier> { + private final Parser dateParser; private final String generatorName; - public PersonExtractors(Gedcom model) { + public PersonExtractors(Parser dateParser, Gedcom model) { + this.dateParser = dateParser; this.generatorName = model.getHeader().getGenerator().getName().toLowerCase(Locale.ROOT); } @Override public AttributeExtractor get() { - var defaultExtractor = new DefaultPersonExtractor(); + var defaultExtractor = new DefaultPersonExtractor(dateParser); if ("heredis pc".equals(generatorName)) { return new HeredisPersonExtractor(defaultExtractor); } diff --git a/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java b/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java index a3e568f..873a4a5 100644 --- a/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java +++ b/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; +import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -44,7 +45,7 @@ void afterEach() { } } - private EagerResult executeProcedure(Driver driver, String fileName) { + private EagerResult loadGedcom(Driver driver, String fileName) { return driver.executableQuery( "CALL genealogy.loadGedcom($fileName) yield nodesCreated, relationshipsCreated return *") .withParameters(Map.of("fileName", fileName)) @@ -54,7 +55,7 @@ private EagerResult executeProcedure(Driver driver, String fileName) { @Test void loads_individuals() { try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { - executeProcedure(driver, "SimpsonsCartoon.ged"); + loadGedcom(driver, "SimpsonsCartoon.ged"); var individuals = driver.executableQuery("MATCH (person:Person) RETURN person").execute(Collectors.toList()).stream() @@ -79,7 +80,7 @@ void loads_individuals() { @Test void loads_relationships() { try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { - var result = executeProcedure(driver, "SimpsonsCartoon.ged"); + var result = loadGedcom(driver, "SimpsonsCartoon.ged"); var statistics = result.records().get(0); var nodesCreated = statistics.get("nodesCreated").asLong(); @@ -94,7 +95,7 @@ void loads_relationships() { assertThat(relationships) .hasSize(17) - .filteredOn(r -> r.type().equals("IS_MARRIED_TO")) + .filteredOn(r -> r.type().equals("IS_SPOUSE_OF")) .hasSize(3); assertThat(relationships) @@ -118,9 +119,9 @@ void loads_relationships() { var patty = new Person(List.of("Patty"), List.of("Bouvier"), "F"); assertThat(relationships) .containsExactlyInAnyOrder( - familyRel(homer, "IS_MARRIED_TO", marge), - familyRel(abraham, "IS_MARRIED_TO", mona), - familyRel(clancy, "IS_MARRIED_TO", jacqueline), + familyRel(homer, "IS_SPOUSE_OF", marge), + familyRel(abraham, "IS_SPOUSE_OF", mona), + familyRel(clancy, "IS_SPOUSE_OF", jacqueline), familyRel(homer, "IS_CHILD_OF", abraham), familyRel(homer, "IS_CHILD_OF", mona), familyRel(marge, "IS_CHILD_OF", clancy), @@ -139,9 +140,9 @@ void loads_relationships() { } @Test - void parses_date() { + void parses_person_event_dates() { try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { - executeProcedure(driver, "555Sample.ged"); + loadGedcom(driver, "555Sample.ged"); var relationships = driver.executableQuery( "MATCH (i:Person) WHERE i.birth_date > date({ year: 1800 }) return i") @@ -158,12 +159,12 @@ void parses_date() { @Test void parses_same_sex_marriages() { try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { - executeProcedure(driver, "SSMARR.ged"); + loadGedcom(driver, "SSMARR.ged"); var relationships = driver .executableQuery( """ - MATCH (i: Person)-[r:IS_MARRIED_TO]->(j: Person) + MATCH (i: Person)-[r:IS_SPOUSE_OF]->(j: Person) WHERE i.gender = j.gender RETURN i, r, j """) @@ -174,20 +175,20 @@ void parses_same_sex_marriages() { var john = new Person(List.of("John"), List.of("Smith"), "M"); var steven = new Person(List.of("Steven"), List.of("Stevens"), "M"); - assertThat(relationships).containsExactlyInAnyOrder(familyRel(john, "IS_MARRIED_TO", steven)); + assertThat(relationships).containsExactlyInAnyOrder(familyRel(john, "IS_SPOUSE_OF", steven)); } } @Test void processes_remarriages() { try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { - executeProcedure(driver, "REMARR.ged"); + loadGedcom(driver, "REMARR.ged"); var relationships = driver .executableQuery( """ - MATCH (i: Person)-[r:IS_MARRIED_TO]-(j: Person) - MATCH (i)-[:IS_MARRIED_TO]-(k: Person) + MATCH (i: Person)-[r:IS_SPOUSE_OF]-(j: Person) + MATCH (i)-[:IS_SPOUSE_OF]-(k: Person) WHERE id(j) <> id(k) RETURN i, r, j""") .execute(Collectors.toList()) @@ -200,7 +201,7 @@ WHERE id(j) <> id(k) var juan = new Person(List.of("Juan"), List.of("Donalds"), "M"); assertThat(relationships) - .contains(familyRel(mary, "IS_MARRIED_TO", peter), familyRel(mary, "IS_MARRIED_TO", juan)); + .contains(familyRel(mary, "IS_SPOUSE_OF", peter), familyRel(mary, "IS_SPOUSE_OF", juan)); ; } } @@ -208,7 +209,7 @@ WHERE id(j) <> id(k) @Test void parses_Heredis_preferred_name() { try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { - executeProcedure(driver, "HeredisPreferredName.ged"); + loadGedcom(driver, "HeredisPreferredName.ged"); var preferredFirstNames = driver .executableQuery( @@ -228,6 +229,66 @@ void parses_Heredis_preferred_name() { } } + @Test + void parses_detailed_marriage_information() { + try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { + loadGedcom(driver, "DetailedMarriageDivorceInfo.ged"); + + var marriages = driver + .executableQuery( + """ + MATCH (john:Person {first_names: ["John"], last_names: ["DOE"]}), + (jane:Person {first_names: ["Jane"], last_names: ["DOE"]}), + (john)-[r:IS_MARRIED_TO]->(jane) + RETURN r + """) + .execute(Collectors.toList()) + .stream() + .map(record -> record.get("r").asRelationship().asMap()) + .toList(); + + assertThat(marriages) + .containsExactlyInAnyOrder( + Map.of( + "type", "Religious marriage", + "date", LocalDate.of(1989, 3, 2), + "raw_date", "2 MAR 1989", + "location", "Colmar,68000,Haut Rhin,Alsace,FRANCE,"), + Map.of( + "date", LocalDate.of(1989, 3, 1), + "raw_date", "1 MAR 1989", + "location", "Colmar,68000,Haut Rhin,Alsace,FRANCE,")); + } + ; + } + + @Test + void parses_divorce_information() { + try (Driver driver = GraphDatabase.driver(neo4j.boltURI())) { + loadGedcom(driver, "DetailedMarriageDivorceInfo.ged"); + + var divorces = driver + .executableQuery( + """ + MATCH (john:Person {first_names: ["John"], last_names: ["DOE"]}), + (jane:Person {first_names: ["Jane"], last_names: ["DOE"]}), + (john)-[r:DIVORCED]->(jane) + RETURN r + """) + .execute(Collectors.toList()) + .stream() + .map(record -> record.get("r").asRelationship().asMap()) + .toList(); + + assertThat(divorces) + .containsExactlyInAnyOrder(Map.of( + "date", LocalDate.of(2017, 10, 23), + "raw_date", "23 OCT 2017", + "location", "Strasbourg,67000,Bas Rhin,Alsace,FRANCE,")); + } + ; + } + private static FamilyRelation familyRel(Person person1, String relType, Person person2) { return new FamilyRelation(relType, person1, person2); } diff --git a/src/test/resources/ged-files/DetailedMarriageDivorceInfo.ged b/src/test/resources/ged-files/DetailedMarriageDivorceInfo.ged new file mode 100644 index 0000000..5adbd5b --- /dev/null +++ b/src/test/resources/ged-files/DetailedMarriageDivorceInfo.ged @@ -0,0 +1,36 @@ +0 HEAD +1 SOUR HEREDIS 15 PC +2 VERS 15 +2 NAME HEREDIS PC +2 CORP BSD Concept © +3 ADDR www.heredis.com +1 DATE 2 DEC 2018 +2 TIME 20:20:46 +1 GEDC +2 VERS 5.5 +2 FORM LINEAGE-LINKED +1 CHAR UTF-8 +0 @236I@ INDI +1 NAME John/DOE/ +2 GIVN John +2 SURN DOE +1 SEX M +0 @294I@ INDI +1 NAME Jane/DOE/ +2 GIVN Jane +2 SURN DOE +1 SEX F +0 @487U@ FAM +1 HUSB @236I@ +1 WIFE @294I@ +1 MARR +2 DATE 1 MAR 1989 +2 PLAC Colmar,68000,Haut Rhin,Alsace,FRANCE, +1 MARR +2 TYPE Religious marriage +2 DATE 2 MAR 1989 +2 PLAC Colmar,68000,Haut Rhin,Alsace,FRANCE, +1 DIV +2 DATE 23 OCT 2017 +2 PLAC Strasbourg,67000,Bas Rhin,Alsace,FRANCE, +0 TRLR From eb8f7b8bd99d871ed71fb1a9a11255ed8ccd2a1f Mon Sep 17 00:00:00 2001 From: Dhru Devalia Date: Thu, 21 Nov 2024 14:49:26 +0100 Subject: [PATCH 3/4] feat: make relationship names time-neutral Signed-off-by: Florent Biville --- .../neo4j/data/importer/GedcomImporter.java | 8 +-- .../data/importer/GedcomImporterTest.java | 51 +++++++++---------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/neo4j/data/importer/GedcomImporter.java b/src/main/java/com/neo4j/data/importer/GedcomImporter.java index 9795ae2..67f27b9 100644 --- a/src/main/java/com/neo4j/data/importer/GedcomImporter.java +++ b/src/main/java/com/neo4j/data/importer/GedcomImporter.java @@ -57,9 +57,9 @@ public Stream loadGedcom(@Name("file") String file) throws IOExcepti UNWIND $spouseIdPairs AS spouseInfo MATCH (spouse1:Person {id: spouseInfo.id1}), (spouse2:Person {id: spouseInfo.id2}) - CREATE (spouse1)-[r:IS_SPOUSE_OF]->(spouse2) + CREATE (spouse1)-[r:SPOUSE_OF]->(spouse2) FOREACH (marriageInfo IN spouseInfo.events["MARR"] | - CREATE (spouse1)-[r:IS_MARRIED_TO]->(spouse2) + CREATE (spouse1)-[r:MARRIED_TO]->(spouse2) SET r = marriageInfo ) FOREACH (divorceInfo IN spouseInfo.events["DIV"] | @@ -69,8 +69,8 @@ public Stream loadGedcom(@Name("file") String file) throws IOExcepti WITH spouse1, spouse2 UNWIND $childIds AS childId MATCH (child:Person {id: childId}) - CREATE (child)-[:IS_CHILD_OF]->(spouse1) - CREATE (child)-[:IS_CHILD_OF]->(spouse2) + CREATE (child)-[:CHILD_OF]->(spouse1) + CREATE (child)-[:CHILD_OF]->(spouse2) """, attributes) .getQueryStatistics(); diff --git a/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java b/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java index 873a4a5..99391bf 100644 --- a/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java +++ b/src/test/java/com/neo4j/data/importer/GedcomImporterTest.java @@ -95,11 +95,11 @@ void loads_relationships() { assertThat(relationships) .hasSize(17) - .filteredOn(r -> r.type().equals("IS_SPOUSE_OF")) + .filteredOn(r -> r.type().equals("SPOUSE_OF")) .hasSize(3); assertThat(relationships) - .filteredOn(r -> r.type().equals("IS_CHILD_OF")) + .filteredOn(r -> r.type().equals("CHILD_OF")) .hasSize(14); assertThat(nodesCreated).isEqualTo(11); @@ -119,23 +119,23 @@ void loads_relationships() { var patty = new Person(List.of("Patty"), List.of("Bouvier"), "F"); assertThat(relationships) .containsExactlyInAnyOrder( - familyRel(homer, "IS_SPOUSE_OF", marge), - familyRel(abraham, "IS_SPOUSE_OF", mona), - familyRel(clancy, "IS_SPOUSE_OF", jacqueline), - familyRel(homer, "IS_CHILD_OF", abraham), - familyRel(homer, "IS_CHILD_OF", mona), - familyRel(marge, "IS_CHILD_OF", clancy), - familyRel(marge, "IS_CHILD_OF", jacqueline), - familyRel(lisa, "IS_CHILD_OF", marge), - familyRel(lisa, "IS_CHILD_OF", homer), - familyRel(bart, "IS_CHILD_OF", marge), - familyRel(bart, "IS_CHILD_OF", homer), - familyRel(maggie, "IS_CHILD_OF", marge), - familyRel(maggie, "IS_CHILD_OF", homer), - familyRel(selma, "IS_CHILD_OF", jacqueline), - familyRel(selma, "IS_CHILD_OF", clancy), - familyRel(patty, "IS_CHILD_OF", jacqueline), - familyRel(patty, "IS_CHILD_OF", clancy)); + familyRel(homer, "SPOUSE_OF", marge), + familyRel(abraham, "SPOUSE_OF", mona), + familyRel(clancy, "SPOUSE_OF", jacqueline), + familyRel(homer, "CHILD_OF", abraham), + familyRel(homer, "CHILD_OF", mona), + familyRel(marge, "CHILD_OF", clancy), + familyRel(marge, "CHILD_OF", jacqueline), + familyRel(lisa, "CHILD_OF", marge), + familyRel(lisa, "CHILD_OF", homer), + familyRel(bart, "CHILD_OF", marge), + familyRel(bart, "CHILD_OF", homer), + familyRel(maggie, "CHILD_OF", marge), + familyRel(maggie, "CHILD_OF", homer), + familyRel(selma, "CHILD_OF", jacqueline), + familyRel(selma, "CHILD_OF", clancy), + familyRel(patty, "CHILD_OF", jacqueline), + familyRel(patty, "CHILD_OF", clancy)); } } @@ -164,7 +164,7 @@ void parses_same_sex_marriages() { var relationships = driver .executableQuery( """ - MATCH (i: Person)-[r:IS_SPOUSE_OF]->(j: Person) + MATCH (i: Person)-[r:SPOUSE_OF]->(j: Person) WHERE i.gender = j.gender RETURN i, r, j """) @@ -175,7 +175,7 @@ void parses_same_sex_marriages() { var john = new Person(List.of("John"), List.of("Smith"), "M"); var steven = new Person(List.of("Steven"), List.of("Stevens"), "M"); - assertThat(relationships).containsExactlyInAnyOrder(familyRel(john, "IS_SPOUSE_OF", steven)); + assertThat(relationships).containsExactlyInAnyOrder(familyRel(john, "SPOUSE_OF", steven)); } } @@ -187,8 +187,8 @@ void processes_remarriages() { var relationships = driver .executableQuery( """ - MATCH (i: Person)-[r:IS_SPOUSE_OF]-(j: Person) - MATCH (i)-[:IS_SPOUSE_OF]-(k: Person) + MATCH (i: Person)-[r:SPOUSE_OF]-(j: Person) + MATCH (i)-[:SPOUSE_OF]-(k: Person) WHERE id(j) <> id(k) RETURN i, r, j""") .execute(Collectors.toList()) @@ -200,8 +200,7 @@ WHERE id(j) <> id(k) var peter = new Person(List.of("Peter"), List.of("Sweet"), "M"); var juan = new Person(List.of("Juan"), List.of("Donalds"), "M"); - assertThat(relationships) - .contains(familyRel(mary, "IS_SPOUSE_OF", peter), familyRel(mary, "IS_SPOUSE_OF", juan)); + assertThat(relationships).contains(familyRel(mary, "SPOUSE_OF", peter), familyRel(mary, "SPOUSE_OF", juan)); ; } } @@ -239,7 +238,7 @@ void parses_detailed_marriage_information() { """ MATCH (john:Person {first_names: ["John"], last_names: ["DOE"]}), (jane:Person {first_names: ["Jane"], last_names: ["DOE"]}), - (john)-[r:IS_MARRIED_TO]->(jane) + (john)-[r:MARRIED_TO]->(jane) RETURN r """) .execute(Collectors.toList()) From 147ff2a5297c582a7175284d1610d9ccb52ba7e6 Mon Sep 17 00:00:00 2001 From: Dhru Devalia Date: Thu, 21 Nov 2024 14:51:17 +0100 Subject: [PATCH 4/4] ci: add new Neo4j releases Signed-off-by: Florent Biville --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0ac3bb..fe4058d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: neo4j_version: - - "4.4.37" + - "4.4.38" - "5.1.0" - "5.2.0" - "5.3.0" @@ -36,6 +36,8 @@ jobs: - "5.21.2" - "5.22.0" - "5.23.0" + - "5.24.2" + - "5.25.1" steps: - uses: actions/checkout@v4 - name: Set up JDK 17