Skip to content

Commit 40256da

Browse files
authored
Merge pull request #327 from eclipse-jnosql/enhance-graph
Enhance Graph databases
2 parents cf250f6 + e752228 commit 40256da

File tree

44 files changed

+735
-320
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+735
-320
lines changed

jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/DefaultNeo4JDatabaseManager.java

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.neo4j.driver.Session;
2626
import org.neo4j.driver.Transaction;
2727
import org.neo4j.driver.Values;
28+
import org.neo4j.driver.types.TypeSystem;
2829

2930
import java.time.Duration;
3031
import java.util.ArrayList;
@@ -164,22 +165,50 @@ public long count(String entity) {
164165
}
165166

166167
@Override
167-
public Stream<CommunicationEntity> executeQuery(String cypher, Map<String, Object> parameters) {
168+
public Stream<CommunicationEntity> cypher(String cypher, Map<String, Object> parameters) {
168169
Objects.requireNonNull(cypher, "Cypher query is required");
169170
Objects.requireNonNull(parameters, "Parameters map is required");
170171

171172
try (Transaction tx = session.beginTransaction()) {
172-
Stream<CommunicationEntity> result = tx.run(cypher, Values.parameters(flattenMap(parameters)))
173-
.list(record -> extractEntity("QueryResult", record, false))
174-
.stream();
173+
var result = tx.run(cypher, Values.parameters(flattenMap(parameters)));
174+
175+
List<CommunicationEntity> entities = result
176+
.stream()
177+
.map(record -> record.keys().stream()
178+
.map(key -> {
179+
var value = record.get(key);
180+
if (value.hasType(TypeSystem.getDefault().NODE())) {
181+
return extractEntity(key, record, false);
182+
} else if (value.hasType(TypeSystem.getDefault().RELATIONSHIP())) {
183+
var rel = value.asRelationship();
184+
List<Element> elements = new ArrayList<>();
185+
rel.asMap().forEach((k, v) -> elements.add(Element.of(k, v)));
186+
elements.add(Element.of(ID, rel.elementId()));
187+
elements.add(Element.of("start", rel.startNodeElementId()));
188+
elements.add(Element.of("end", rel.endNodeElementId()));
189+
return CommunicationEntity.of(key, elements);
190+
}
191+
return null;
192+
})
193+
.filter(Objects::nonNull)
194+
.findFirst()
195+
.orElse(null)
196+
)
197+
.filter(Objects::nonNull)
198+
.toList();
175199
LOGGER.fine("Executed Cypher query: " + cypher);
176200
tx.commit();
177-
return result;
201+
return entities.stream();
178202
} catch (Exception e) {
179203
throw new CommunicationException("Error executing Cypher query", e);
180204
}
181205
}
182206

207+
@Override
208+
public Stream<CommunicationEntity> cypher(String cypher) {
209+
return cypher(cypher, Collections.emptyMap());
210+
}
211+
183212
@Override
184213
public Stream<CommunicationEntity> traverse(String startNodeId, String label, int depth) {
185214
Objects.requireNonNull(startNodeId, "Start node ID is required");
@@ -376,22 +405,45 @@ var record = result.hasNext() ? result.next() : null;
376405
return entitiesResult;
377406
}
378407

379-
private CommunicationEntity extractEntity(String entityName, org.neo4j.driver.Record record, boolean isFullNode) {
408+
private CommunicationEntity extractEntity(String alias, org.neo4j.driver.Record record, boolean isFullNode) {
380409
List<Element> elements = new ArrayList<>();
381410

382411
for (String key : record.keys()) {
383412
var value = record.get(key);
384413

385-
if (value.hasType(org.neo4j.driver.types.TypeSystem.getDefault().NODE())) {
414+
if (value.hasType(TypeSystem.getDefault().NODE())) {
386415
var node = value.asNode();
387-
node.asMap().forEach((k, v) -> elements.add(Element.of(k, v))); // Extract properties
416+
417+
node.asMap().forEach((k, v) -> elements.add(Element.of(k, v)));
418+
388419
elements.add(Element.of(ID, node.elementId()));
389-
} else {
390-
String fieldName = key.contains(".") ? key.substring(key.indexOf('.') + 1) : key;
391-
elements.add(Element.of(fieldName, value.asObject()));
420+
elements.add(Element.of("_alias", key));
421+
422+
var label = node.labels().iterator().hasNext()
423+
? node.labels().iterator().next()
424+
: key;
425+
426+
return CommunicationEntity.of(label, elements);
392427
}
428+
429+
if (value.hasType(TypeSystem.getDefault().RELATIONSHIP())) {
430+
var rel = value.asRelationship();
431+
432+
rel.asMap().forEach((k, v) -> elements.add(Element.of(k, v)));
433+
434+
elements.add(Element.of(ID, rel.elementId()));
435+
elements.add(Element.of("start", rel.startNodeElementId()));
436+
elements.add(Element.of("end", rel.endNodeElementId()));
437+
elements.add(Element.of("_alias", key));
438+
439+
return CommunicationEntity.of(rel.type(), elements);
440+
}
441+
442+
String fieldName = key.contains(".") ? key.substring(key.indexOf('.') + 1) : key;
443+
elements.add(Element.of(fieldName, value.asObject()));
393444
}
394445

395-
return CommunicationEntity.of(entityName, elements);
446+
// No node or relationship found: use alias as fallback
447+
return CommunicationEntity.of(alias, elements);
396448
}
397449
}

jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManager.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,16 @@ public interface Neo4JDatabaseManager extends GraphDatabaseManager {
5959
* @return a stream of {@link CommunicationEntity} representing the query result.
6060
* @throws NullPointerException if {@code cypher} or {@code parameters} is null.
6161
*/
62-
Stream<CommunicationEntity> executeQuery(String cypher, Map<String, Object> parameters);
62+
Stream<CommunicationEntity> cypher(String cypher, Map<String, Object> parameters);
63+
64+
/**
65+
* Executes a custom Cypher query without parameters and returns a stream of {@link CommunicationEntity}.
66+
*
67+
* @param cypher the Cypher query to execute.
68+
* @return a stream of {@link CommunicationEntity} representing the query result.
69+
* @throws NullPointerException if {@code cypher} is null.
70+
*/
71+
Stream<CommunicationEntity> cypher(String cypher);
6372

6473
/**
6574
* Traverses the graph starting from a node and follows the specified label type up to a given depth.

jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/DefaultNeo4JTemplate.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,15 @@ class DefaultNeo4JTemplate extends AbstractGraphTemplate implements Neo4JTemplat
6666
public <T> Stream<T> cypher(String cypher, Map<String, Object> parameters) {
6767
Objects.requireNonNull(cypher, "cypher is required");
6868
Objects.requireNonNull(parameters, "parameters is required");
69-
return manager.get().executeQuery(cypher, parameters)
69+
return manager.get().cypher(cypher, parameters)
70+
.map(e -> (T) converter.toEntity(e));
71+
}
72+
73+
@SuppressWarnings("unchecked")
74+
@Override
75+
public <T> Stream<T> cypher(String cypher) {
76+
Objects.requireNonNull(cypher, "cypher is required");
77+
return manager.get().cypher(cypher)
7078
.map(e -> (T) converter.toEntity(e));
7179
}
7280

jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ protected Neo4JTemplate template() {
8383
return template;
8484
}
8585

86+
@SuppressWarnings("unchecked")
8687
@Override
8788
public Object invoke(Object instance, Method method, Object[] args) throws Throwable {
8889

jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JTemplate.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*
3131
*/
3232
public interface Neo4JTemplate extends GraphTemplate {
33+
3334
/**
3435
* Executes a Cypher query and returns a stream of results mapped to the given entity type.
3536
*
@@ -41,6 +42,16 @@ public interface Neo4JTemplate extends GraphTemplate {
4142
*/
4243
<T> Stream<T> cypher(String cypher, Map<String, Object> parameters);
4344

45+
/**
46+
* Executes a Cypher query and returns a stream of results mapped to the given entity type.
47+
*
48+
* @param cypher The Cypher query string.
49+
* @param <T> The entity type representing nodes or relationships within the graph database.
50+
* @return A stream of entities representing the query result.
51+
* @throws NullPointerException if {@code cypher} is null.
52+
*/
53+
<T> Stream<T> cypher(String cypher);
54+
4455
/**
4556
* Traverses relationships from a given start node up to a specified depth.
4657
*

jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/communication/Neo4JDatabaseManagerTest.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,11 @@ void shouldExecuteCustomQuery() {
463463
entityManager.insert(entity);
464464

465465
String cypher = "MATCH (e:person) RETURN e";
466-
var result = entityManager.executeQuery(cypher, new HashMap<>()).toList();
466+
var result = entityManager.cypher(cypher, new HashMap<>()).toList();
467467

468468
SoftAssertions.assertSoftly(softly -> {
469469
softly.assertThat(result).isNotEmpty();
470+
softly.assertThat(result.get(0).name()).isEqualTo(COLLECTION_NAME);
470471
softly.assertThat(result.get(0).find("name")).isPresent();
471472
softly.assertThat(result.get(0).find("city")).isPresent();
472473
softly.assertThat(result.get(0).find("_id")).isPresent(); // Ensuring _id exists
@@ -524,7 +525,7 @@ void shouldCreateEdge() {
524525
"id2", person2Id
525526
);
526527

527-
var result = entityManager.executeQuery(cypher, parameters).toList();
528+
var result = entityManager.cypher(cypher, parameters).toList();
528529
SoftAssertions.assertSoftly(softly -> softly.assertThat(result).isNotEmpty());
529530

530531
entityManager.remove(person1, "FRIEND", person2);
@@ -546,7 +547,7 @@ void shouldRemoveEdge() {
546547
String cypher = "MATCH (p1:person { _id: $_id1 })-[r:FRIEND]-(p2:person { _id: $_id2 }) RETURN r";
547548
Map<String, Object> parameters = Map.of("_id1", startNodeId, "_id2", targetNodeId);
548549

549-
var result = entityManager.executeQuery(cypher, parameters).toList();
550+
var result = entityManager.cypher(cypher, parameters).toList();
550551
SoftAssertions.assertSoftly(softly -> softly.assertThat(result).isEmpty());
551552
}
552553

@@ -564,7 +565,7 @@ void shouldDeleteEdgeById() {
564565
String cypher = "MATCH ()-[r]-() WHERE elementId(r) = $id RETURN r";
565566
Map<String, Object> parameters = Map.of("id", edgeId);
566567

567-
var result = entityManager.executeQuery(cypher, parameters).toList();
568+
var result = entityManager.cypher(cypher, parameters).toList();
568569
SoftAssertions.assertSoftly(softly -> softly.assertThat(result).isEmpty());
569570
}
570571

@@ -598,7 +599,7 @@ void shouldCreateEdgeWithProperties() {
598599
"WHERE elementId(r) = $edgeId RETURN r";
599600
Map<String, Object> parameters = Map.of("edgeId", edge.id());
600601

601-
var result = entityManager.executeQuery(cypher, parameters).toList();
602+
var result = entityManager.cypher(cypher, parameters).toList();
602603
SoftAssertions.assertSoftly(softly -> {
603604
softly.assertThat(result).isNotEmpty();
604605
softly.assertThat(edge.properties()).containsEntry("since", 2019);
@@ -623,7 +624,7 @@ private void removeAllEdges() {
623624
String cypher = "MATCH ()-[r]-() DELETE r";
624625

625626
try {
626-
entityManager.executeQuery(cypher, new HashMap<>()).toList();
627+
entityManager.cypher(cypher, new HashMap<>()).toList();
627628
} catch (Exception e) {
628629
throw new RuntimeException("Failed to remove edges before node deletion", e);
629630
}

jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/GraphTemplateIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ private void removeAllEdges() {
160160

161161
try {
162162
var entityManager = DatabaseContainer.INSTANCE.get("neo4j");
163-
entityManager.executeQuery(cypher, new HashMap<>()).toList();
163+
entityManager.cypher(cypher, new HashMap<>()).toList();
164164
} catch (Exception e) {
165165
throw new RuntimeException("Failed to remove edges before node deletion", e);
166166
}

jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/MagazineRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,19 @@
1515

1616
package org.eclipse.jnosql.databases.neo4j.integration;
1717

18+
import jakarta.data.repository.Param;
1819
import jakarta.data.repository.Repository;
20+
import org.eclipse.jnosql.databases.neo4j.mapping.Cypher;
1921
import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JRepository;
2022

23+
import java.util.List;
24+
2125
@Repository
2226
public interface MagazineRepository extends Neo4JRepository<Magazine, String> {
27+
28+
@Cypher("MATCH (m:Magazine) RETURN m")
29+
List<Magazine> findAllByCypher();
30+
31+
@Cypher("MATCH (m:Magazine{title: $title}) RETURN m")
32+
List<Magazine> findByTitle(@Param("title") String title);
2333
}

jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/RepositoryIntegrationTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717

1818
import jakarta.inject.Inject;
19+
import org.assertj.core.api.SoftAssertions;
1920
import org.eclipse.jnosql.databases.neo4j.communication.DatabaseContainer;
2021
import org.eclipse.jnosql.databases.neo4j.communication.Neo4JConfigurations;
2122
import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JExtension;
@@ -28,6 +29,7 @@
2829
import org.jboss.weld.junit5.auto.AddExtensions;
2930
import org.jboss.weld.junit5.auto.AddPackages;
3031
import org.jboss.weld.junit5.auto.EnableAutoWeld;
32+
import org.junit.jupiter.api.BeforeEach;
3133
import org.junit.jupiter.api.Test;
3234
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
3335

@@ -53,6 +55,11 @@ public class RepositoryIntegrationTest {
5355
@Inject
5456
private MagazineRepository repository;
5557

58+
@BeforeEach
59+
void beforeEach() {
60+
repository.deleteAll();
61+
}
62+
5663
@Test
5764
void shouldSave() {
5865
Magazine magazine = new Magazine(null, "Effective Java", 1);
@@ -61,4 +68,30 @@ void shouldSave() {
6168

6269
}
6370

71+
@Test
72+
void shouldFindAll() {
73+
for (int index = 0; index < 5; index++) {
74+
Magazine magazine = repository.save(new Magazine(null, "Effective Java", index));
75+
assertThat(magazine).isNotNull();
76+
}
77+
var result = repository.findAllByCypher();
78+
SoftAssertions.assertSoftly(soft -> {
79+
assertThat(result).isNotNull();
80+
assertThat(result).hasSize(5);
81+
});
82+
}
83+
84+
@Test
85+
void shouldFindByName() {
86+
for (int index = 0; index < 5; index++) {
87+
Magazine magazine = repository.save(new Magazine(null, "Effective Java", index));
88+
assertThat(magazine).isNotNull();
89+
}
90+
var result = repository.findByTitle("Effective Java");
91+
SoftAssertions.assertSoftly(soft -> {
92+
assertThat(result).isNotNull();
93+
assertThat(result).hasSize(5);
94+
});
95+
}
96+
6497
}

jnosql-neo4j/src/test/java/org/eclipse/jnosql/databases/neo4j/integration/TemplateIntegrationTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package org.eclipse.jnosql.databases.neo4j.integration;
1616

1717
import jakarta.inject.Inject;
18+
import org.assertj.core.api.SoftAssertions;
1819
import org.eclipse.jnosql.databases.neo4j.communication.DatabaseContainer;
1920
import org.eclipse.jnosql.databases.neo4j.communication.Neo4JConfigurations;
2021
import org.eclipse.jnosql.databases.neo4j.mapping.Neo4JTemplate;
@@ -30,6 +31,7 @@
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
3233

34+
import java.util.Map;
3335
import java.util.Optional;
3436

3537
import static org.assertj.core.api.Assertions.assertThat;
@@ -110,4 +112,32 @@ void shouldDeleteAll(){
110112
template.delete(Magazine.class).execute();
111113
assertThat(template.select(Magazine.class).result()).isEmpty();
112114
}
115+
116+
@Test
117+
void shouldFindUsingCypher() {
118+
for (int index = 0; index < 5; index++) {
119+
Magazine magazine = template.insert(new Magazine(null, "Effective Java", index));
120+
assertThat(magazine).isNotNull();
121+
}
122+
var result = template.cypher("MATCH (m:Magazine) RETURN m").toList();
123+
SoftAssertions.assertSoftly(soft -> {
124+
assertThat(result).isNotNull();
125+
assertThat(result).hasSize(5);
126+
});
127+
}
128+
129+
@Test
130+
void shouldFindUsingCypherParameter() {
131+
for (int index = 0; index < 5; index++) {
132+
Magazine magazine = template.insert(new Magazine(null, "Effective Java", index));
133+
assertThat(magazine).isNotNull();
134+
}
135+
136+
Map<String, Object> parameters = Map.of("title", "Effective Java");
137+
var result = template.cypher("MATCH (m:Magazine{title: $title}) RETURN m", parameters).toList();
138+
SoftAssertions.assertSoftly(soft -> {
139+
assertThat(result).isNotNull();
140+
assertThat(result).hasSize(5);
141+
});
142+
}
113143
}

0 commit comments

Comments
 (0)