Skip to content

Commit

Permalink
Merge pull request #24 from spbu-coding-2023/feature/neo4j
Browse files Browse the repository at this point in the history
Add Neo4jReader for interacting with Neo4j DB
  • Loading branch information
homka122 authored Sep 24, 2024
2 parents 7c17efe + 1014e62 commit 5206b95
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 3 deletions.
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1")
implementation("org.xerial:sqlite-jdbc:3.41.2.2")

implementation("org.neo4j.driver", "neo4j-java-driver", "5.6.0")
}

compose.desktop {
Expand All @@ -49,9 +49,9 @@ tasks.test {
finalizedBy("jacocoTestReport")
}

tasks.jacocoTestReport{
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports{
reports {
xml.required.set(true)
html.required.set(true)
}
Expand Down
144 changes: 144 additions & 0 deletions src/main/kotlin/model/reader/Neo4jReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package model.reader

import model.graph.*
import org.neo4j.driver.AuthTokens
import org.neo4j.driver.GraphDatabase
import org.neo4j.driver.Transaction

class Neo4jReader(uri: String, user: String, password: String) : Reader {

private val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password))
private val session = driver.session()

private fun createNode(node: Vertex, graphName: String, txInput: Transaction?) {
val tx = txInput ?: session.beginTransaction()

tx.run(
"MERGE (n:Node {graphName: \$graphName, key: \$key})",
mapOf("key" to node.key, "graphName" to graphName)
)

if (txInput == null) {
tx.commit()
tx.close()
}
}

private fun createEdge(edge: Edge, nameGraph: String, txInput: Transaction?) {
val tx = txInput ?: session.beginTransaction()

tx.run(
"MERGE (v1:Node {graphName: \$graphName, key: \$key1})" +
"MERGE (v2:Node {graphName: \$graphName, key: \$key2})" +
"MERGE (v1)-[:DIRECTED_TO {weight: \$weight}]->(v2)",
mapOf(
"key1" to edge.first.key,
"key2" to edge.second.key,
"weight" to edge.weight,
"graphName" to nameGraph
)
)

if (txInput == null) {
tx.commit()
tx.close()
}
}

private fun deleteGraph(graphName: String, txInput: Transaction?) {
val tx = txInput ?: session.beginTransaction()

tx.run(
"MATCH (n:Node {graphName: \$graphName}) DETACH DELETE n",
mapOf(
"graphName" to graphName
)
)
tx.run(
"MATCH (g:Graph {graphName: \$graphName}) DETACH DELETE g",
mapOf(
"graphName" to graphName
)
)

if (txInput == null) {
tx.commit()
tx.close()
}
}

/**
* Save graph to Neo4j Database
*/
override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) {
val transaction = session.beginTransaction()

deleteGraph(nameGraph, transaction)

val graphType: String = when (graph) {
is WeightedDirectedGraph -> "WeightedUndirected"
is WeightedGraph -> "Weighted"
is DirectedGraph -> "Directed"
else -> "Undirected"
}

transaction.run(
"MERGE (g:Graph {graphName: \$graphName, type: \$graphType})",
mapOf(
"graphName" to nameGraph,
"graphType" to graphType
)
)

graph.vertices.forEach { v ->
createNode(v, nameGraph, transaction)

graph.adjacencyList[v]?.forEach { e ->
createEdge(e, nameGraph, transaction)
}
}

transaction.commit()
transaction.close()
}

/**
* Load graph to Neo4j Database
*
* @return the loaded graph
* @throws NoSuchRecordException if there is no graph with given graph name
*/
override fun loadGraph(filepath: String, nameGraph: String): Graph {
var graph: Graph = UndirectedGraph()

session.executeRead { tx ->
val graphType =
tx.run("MATCH (g:Graph {graphName: \$graphName}) return g", mapOf("graphName" to nameGraph)).single()
.get("g").get("type").asString()

graph = when (graphType) {
"Undirected" -> UndirectedGraph()
"Directed" -> DirectedGraph()
"Weighted" -> WeightedGraph()
else -> WeightedDirectedGraph()
}

tx.run("MATCH (n:Node {graphName: \$graphName}) return n", mapOf("graphName" to nameGraph))
.forEach { v -> graph.addVertex((v.get("n").get("key").asInt())) }

tx.run(
"MATCH p=(v1: Node {graphName: \$graphName})-[r]-(v2: Node {graphName: \$graphName}) return v1, v2, r",
mapOf("graphName" to nameGraph)
).forEach { v ->
val values = v.values()
graph.addEdge(
values[0].get("key").asInt(),
values[1].get("key").asInt(),
values[2].get("weight").asLong()
)
}
}

return graph
}
}
143 changes: 143 additions & 0 deletions src/test/kotlin/model/reader/Neo4jReaderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package model.reader

import model.algorithm.Dijkstra
import model.graph.Graph
import model.graph.UndirectedGraph
import model.graph.Vertex
import model.graph.WeightedGraph
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.neo4j.driver.exceptions.NoSuchRecordException
import kotlin.test.assertEquals
import kotlin.test.assertTrue

// Unfortunately tests don't work without local database, and mocking database it useless while you check work with DB
// TODO: use this tests for integrate tests
const val IS_ENABLED = false

class Neo4jReaderTest {
lateinit var testGraph: Graph
val neo4jReader = Neo4jReader("bolt://localhost:7687", "neo4j", "qwertyui")
val testGraphName = "testGraph"

private fun isSameNodes(graph1: Graph, graph2: Graph): Boolean =
graph1.vertices.sortedBy { v -> v.key } == graph2.vertices.sortedBy { v -> v.key }

private fun isSameEdges(graph1: Graph, graph2: Graph): Boolean {
listOf(graph1, graph2).forEach { graph ->
graph.vertices.forEach { v ->
val graph1EdgeNodesWithWeights =
graph1.adjacencyList[v]?.map { edge -> Pair(edge.second, edge.weight) }
?.sortedBy { node -> node.first.key }

val graph2EdgeNodesWithWeights =
graph2.adjacencyList[v]?.map { edge -> Pair(edge.second, edge.weight) }
?.sortedBy { node -> node.first.key }

if (graph1EdgeNodesWithWeights != graph2EdgeNodesWithWeights) return false
}
}

return true
}

@Nested
inner class `Save and load graph` {
@BeforeEach
fun setup() {
testGraph = WeightedGraph()
}

@Test
fun `save and load empty graph one time`() {
if (!IS_ENABLED) return

neo4jReader.saveGraph(testGraph, "", testGraphName)
val graph = neo4jReader.loadGraph("", testGraphName)

assertEquals(graph.vertices.size, testGraph.vertices.size)
assertEquals(graph.adjacencyList.values.size, testGraph.adjacencyList.size)
}

@Test
fun `save and load empty graph 100 times in a row`() {
if (!IS_ENABLED) return

var graph: Graph = testGraph

for (i in 1..100) {
neo4jReader.saveGraph(testGraph, "", testGraphName)
graph = neo4jReader.loadGraph("", testGraphName)
}

assertEquals(graph.vertices.size, testGraph.vertices.size)
assertEquals(graph.adjacencyList.values.size, testGraph.adjacencyList.size)
}

@Test
fun `save and load non-empty graph one time`() {
if (!IS_ENABLED) return

for (i in 1..5) {
testGraph.addVertex(i)
}
testGraph.addEdge(1, 2, 2)
testGraph.addEdge(2, 5, 4)
testGraph.addEdge(1, 4, 4)
testGraph.addEdge(4, 2, 1)
testGraph.addEdge(1, 3, 3)
testGraph.addEdge(4, 5, 1)
testGraph.addEdge(3, 5, 5)

neo4jReader.saveGraph(testGraph, "", testGraphName)
val graph = neo4jReader.loadGraph("", testGraphName)

assertEquals(graph.vertices.size, testGraph.vertices.size)
assertEquals(graph.adjacencyList.size, testGraph.adjacencyList.size)

assertTrue(isSameNodes(graph, testGraph))
assertTrue(isSameEdges(graph, testGraph))
}

@Test
fun `save and load non-empty graph 100 times in a row`() {
if (!IS_ENABLED) return

for (i in 1..5) {
testGraph.addVertex(i)
}
testGraph.addEdge(1, 2, 2)
testGraph.addEdge(2, 5, 4)
testGraph.addEdge(1, 4, 4)
testGraph.addEdge(4, 2, 1)
testGraph.addEdge(1, 3, 3)
testGraph.addEdge(4, 5, 1)
testGraph.addEdge(3, 5, 5)

var graph = testGraph
for (i in 1..100) {
neo4jReader.saveGraph(testGraph, "", testGraphName)
graph = neo4jReader.loadGraph("", testGraphName)

}
assertEquals(graph.vertices.size, testGraph.vertices.size)
assertEquals(graph.adjacencyList.size, testGraph.adjacencyList.size)

assertTrue(isSameNodes(graph, testGraph))
assertTrue(isSameEdges(graph, testGraph))
}

@Test
fun `load graph that don't exist in DB`() {
if (!IS_ENABLED) return

try {
neo4jReader.loadGraph("", "Homka")
} catch (_: NoSuchRecordException) {

}
}
}
}

0 comments on commit 5206b95

Please sign in to comment.