1+ package streams.service.sink.strategy
2+
3+ import streams.events.*
4+ import streams.extensions.quote
5+ import streams.utils.IngestionUtils.getLabelsAsString
6+ import streams.utils.IngestionUtils.getNodeKeysAsString
7+ import streams.utils.IngestionUtils.keySeparator
8+ import streams.utils.IngestionUtils.labelSeparator
9+ import streams.utils.SchemaUtils.getNodeKeys
10+ import streams.utils.StreamsUtils
11+
12+
13+ private fun prepareRelationshipEvents (events : List <StreamsTransactionEvent >, withProperties : Boolean = true): Map <RelationshipSchemaMetadata , List <Map <String , Any >>> = events
14+ .mapNotNull {
15+ val payload = it.payload as RelationshipPayload
16+
17+ if (payload.start.ids.isEmpty() || payload.end.ids.isEmpty()) {
18+ null
19+ } else {
20+ val properties = payload.after?.properties ? : payload.before?.properties ? : emptyMap()
21+
22+ val key = RelationshipSchemaMetadata (it.payload)
23+ val value = if (withProperties) {
24+ mapOf (" start" to payload.start.ids, " end" to payload.end.ids, " properties" to properties)
25+ } else {
26+ mapOf (" start" to payload.start.ids, " end" to payload.end.ids)
27+ }
28+
29+ key to value
30+ }
31+ }
32+ .groupBy { it.first }
33+ .mapValues { it.value.map { it.second } }
34+
35+ class SchemaIngestionStrategy : IngestionStrategy {
36+
37+ override fun mergeRelationshipEvents (events : List <StreamsTransactionEvent >): List <QueryEvents > {
38+ if (events.isNullOrEmpty()) {
39+ return emptyList()
40+ }
41+ return prepareRelationshipEvents(events.filter { it.payload.type == EntityType .relationship && it.meta.operation != OperationType .deleted })
42+ .map {
43+ val label = it.key.label.quote()
44+ val query = """
45+ |${StreamsUtils .UNWIND }
46+ |MERGE (start:${getLabelsAsString(it.key.startLabels)} {${getNodeKeysAsString(" start" , it.key.startKeys)} })
47+ |MERGE (end:${getLabelsAsString(it.key.endLabels)} {${getNodeKeysAsString(" end" , it.key.endKeys)} })
48+ |MERGE (start)-[r:${label} ]->(end)
49+ |SET r = event.properties
50+ """ .trimMargin()
51+ QueryEvents (query, it.value)
52+ }
53+ }
54+
55+ override fun deleteRelationshipEvents (events : List <StreamsTransactionEvent >): List <QueryEvents > {
56+ if (events.isNullOrEmpty()) {
57+ return emptyList()
58+ }
59+ return prepareRelationshipEvents(events.filter { it.payload.type == EntityType .relationship && it.meta.operation == OperationType .deleted }, false )
60+ .map {
61+ val label = it.key.label.quote()
62+ val query = """
63+ |${StreamsUtils .UNWIND }
64+ |MATCH (start:${getLabelsAsString(it.key.startLabels)} {${getNodeKeysAsString(" start" , it.key.startKeys)} })
65+ |MATCH (end:${getLabelsAsString(it.key.endLabels)} {${getNodeKeysAsString(" end" , it.key.endKeys)} })
66+ |MATCH (start)-[r:$label ]->(end)
67+ |DELETE r
68+ """ .trimMargin()
69+ QueryEvents (query, it.value)
70+ }
71+ }
72+
73+ override fun deleteNodeEvents (events : List <StreamsTransactionEvent >): List <QueryEvents > {
74+ if (events.isNullOrEmpty()) {
75+ return emptyList()
76+ }
77+ return events
78+ .filter { it.payload.type == EntityType .node && it.meta.operation == OperationType .deleted }
79+ .mapNotNull {
80+ val changeEvtBefore = it.payload.before as NodeChange
81+ val constraints = getNodeConstraints(it) { it.type == StreamsConstraintType .UNIQUE }
82+ if (constraints.isEmpty()) {
83+ null
84+ } else {
85+ constraints to mapOf (" properties" to changeEvtBefore.properties)
86+ }
87+ }
88+ .groupBy { it.first }
89+ .map {
90+ val labels = it.key.mapNotNull { it.label }
91+ val nodeKeys = it.key.flatMap { it.properties }.toSet()
92+ val query = """
93+ |${StreamsUtils .UNWIND }
94+ |MATCH (n:${getLabelsAsString(labels)} {${getNodeKeysAsString(keys = nodeKeys)} })
95+ |DETACH DELETE n
96+ """ .trimMargin()
97+ QueryEvents (query, it.value.map { it.second })
98+ }
99+ }
100+
101+ override fun mergeNodeEvents (events : List <StreamsTransactionEvent >): List <QueryEvents > {
102+ if (events.isNullOrEmpty()) {
103+ return emptyList()
104+ }
105+
106+ val filterLabels: (List <String >, List <Constraint >) -> List <String > = { labels, constraints ->
107+ labels.filter { label -> ! constraints.any { constraint -> constraint.label == label } }
108+ .map { it.quote() }
109+ }
110+ return events
111+ .filter { it.payload.type == EntityType .node && it.meta.operation != OperationType .deleted }
112+ .mapNotNull {
113+ val changeEvtAfter = it.payload.after as NodeChange
114+ val labelsAfter = changeEvtAfter.labels ? : emptyList()
115+ val labelsBefore = (it.payload.before as ? NodeChange )?.labels.orEmpty()
116+
117+ val constraints = getNodeConstraints(it) { it.type == StreamsConstraintType .UNIQUE }
118+ if (constraints.isEmpty()) {
119+ null
120+ } else {
121+ val labelsToAdd = filterLabels((labelsAfter - labelsBefore), constraints)
122+ val labelsToDelete = filterLabels((labelsBefore - labelsAfter), constraints)
123+
124+ val propertyKeys = changeEvtAfter.properties?.keys ? : emptySet()
125+ val keys = getNodeKeys(labelsAfter, propertyKeys, constraints)
126+
127+ if (keys.isEmpty()) {
128+ null
129+ } else {
130+ val key = NodeSchemaMetadata (constraints = constraints,
131+ labelsToAdd = labelsToAdd, labelsToDelete = labelsToDelete,
132+ keys = keys)
133+ val value = mapOf (" properties" to changeEvtAfter.properties)
134+ key to value
135+ }
136+ }
137+ }
138+ .groupBy { it.first }
139+ .map { map ->
140+ var query = """
141+ |${StreamsUtils .UNWIND }
142+ |MERGE (n:${getLabelsAsString(map.key.constraints.mapNotNull { it.label })} {${getNodeKeysAsString(keys = map.key.keys)} })
143+ |SET n = event.properties
144+ """ .trimMargin()
145+ if (map.key.labelsToAdd.isNotEmpty()) {
146+ query + = " \n SET n:${getLabelsAsString(map.key.labelsToAdd)} "
147+ }
148+ if (map.key.labelsToDelete.isNotEmpty()) {
149+ query + = " \n REMOVE n:${getLabelsAsString(map.key.labelsToDelete)} "
150+ }
151+ QueryEvents (query, map.value.map { it.second })
152+ }
153+ }
154+
155+ private fun getNodeConstraints (event : StreamsTransactionEvent ,
156+ filter : (Constraint ) -> Boolean ): List <Constraint > = event.schema.constraints.filter { filter(it) }
157+
158+ }
0 commit comments