Skip to content

Commit 97eb0a7

Browse files
committed
Validate config options for CypherAggregation
1 parent f2a27f1 commit 97eb0a7

File tree

3 files changed

+97
-28
lines changed

3 files changed

+97
-28
lines changed

annotations/src/main/java/org/neo4j/gds/core/ConfigKeyValidation.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ public final class ConfigKeyValidation {
3232

3333
private ConfigKeyValidation() {}
3434

35-
public static void requireOnlyKeysFrom(Collection<String> allowedKeys, Collection<String> configKeys) {
35+
public static void requireOnlyKeysFrom(Collection<String> allowedKeys, Iterable<String> configKeys) {
3636
var unexpectedKeys = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
37-
unexpectedKeys.addAll(configKeys);
37+
if (configKeys instanceof Collection<?>) {
38+
unexpectedKeys.addAll((Collection<String>) configKeys);
39+
} else {
40+
configKeys.forEach(unexpectedKeys::add);
41+
}
42+
3843

3944
// We are doing the equivalent of `removeAll` here.
4045
// As of jdk16, TreeSet does not have a specialized removeAll implementation and reverts to use
@@ -54,15 +59,25 @@ public static void requireOnlyKeysFrom(Collection<String> allowedKeys, Collectio
5459
List<String> suggestions = unexpectedKeys.stream()
5560
.map(invalid -> {
5661
List<String> candidates = StringSimilarity.similarStringsIgnoreCase(invalid, allowedKeys);
57-
candidates.removeAll(configKeys);
62+
if (configKeys instanceof Collection<?>) {
63+
candidates.removeAll((Collection<String>) configKeys);
64+
} else {
65+
configKeys.forEach(candidates::remove);
66+
}
67+
5868

5969
if (candidates.isEmpty()) {
6070
return invalid;
6171
}
6272
if (candidates.size() == 1) {
6373
return String.format(Locale.ENGLISH, "%s (Did you mean [%s]?)", invalid, candidates.get(0));
6474
}
65-
return String.format(Locale.ENGLISH, "%s (Did you mean one of [%s]?)", invalid, String.join(", ", candidates));
75+
return String.format(
76+
Locale.ENGLISH,
77+
"%s (Did you mean one of [%s]?)",
78+
invalid,
79+
String.join(", ", candidates)
80+
);
6681
})
6782
.collect(Collectors.toList());
6883

cypher-aggregation/src/main/java/org/neo4j/gds/projection/CypherAggregation.java

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.jetbrains.annotations.NotNull;
2323
import org.jetbrains.annotations.Nullable;
24+
import org.jetbrains.annotations.TestOnly;
2425
import org.neo4j.gds.NodeLabel;
2526
import org.neo4j.gds.Orientation;
2627
import org.neo4j.gds.RelationshipType;
@@ -39,6 +40,7 @@
3940
import org.neo4j.gds.api.schema.RelationshipSchema;
4041
import org.neo4j.gds.config.GraphProjectConfig;
4142
import org.neo4j.gds.core.Aggregation;
43+
import org.neo4j.gds.core.ConfigKeyValidation;
4244
import org.neo4j.gds.core.CypherMapWrapper;
4345
import org.neo4j.gds.core.Username;
4446
import org.neo4j.gds.core.compress.AdjacencyCompressor;
@@ -87,6 +89,7 @@
8789
import java.util.Optional;
8890
import java.util.Set;
8991
import java.util.concurrent.ConcurrentHashMap;
92+
import java.util.concurrent.atomic.AtomicBoolean;
9093
import java.util.concurrent.locks.Lock;
9194
import java.util.concurrent.locks.ReentrantLock;
9295

@@ -121,6 +124,8 @@ public static class GraphAggregator {
121124
private final DatabaseId databaseId;
122125
private final String username;
123126

127+
private final ConfigValidator configValidator;
128+
124129
// #result() may be called twice, we cache the result of the first call to return it again in the second invocation
125130
private @Nullable AggregationResult result;
126131

@@ -137,6 +142,7 @@ public static class GraphAggregator {
137142
this.databaseId = databaseId;
138143
this.username = username;
139144
this.lock = new ReentrantLock();
145+
this.configValidator = new ConfigValidator();
140146
}
141147

142148
@UserAggregationUpdate
@@ -168,15 +174,7 @@ public void update(
168174
targetNodeLabels = labelsConfig("targetNodeLabels", (MapValue) nodesConfig);
169175
}
170176

171-
// TODO:
172-
// if (!nodesConfig.isEmpty()) {
173-
// CypherMapWrapper.create(nodesConfig).requireOnlyKeysFrom(List.of(
174-
// "sourceNodeProperties",
175-
// "sourceNodeLabels",
176-
// "targetNodeProperties",
177-
// "targetNodeLabels"
178-
// ));
179-
// }
177+
this.configValidator.validateNodesConfig((MapValue) nodesConfig);
180178
}
181179

182180
var data = initGraphData(
@@ -196,7 +194,8 @@ public void update(
196194
targetNodePropertyValues,
197195
sourceNodeLabels,
198196
targetNodeLabels,
199-
relationshipConfig
197+
relationshipConfig,
198+
this.configValidator
200199
);
201200
}
202201

@@ -238,7 +237,6 @@ private LazyImporter initGraphData(
238237
}
239238

240239
private static NodeLabelToken labelsConfig(String nodeLabelKey, @NotNull MapValue nodesConfig) {
241-
// TODO: was remove
242240
var nodeLabelsEntry = nodesConfig.get(nodeLabelKey);
243241
return tryLabelsConfig(nodeLabelsEntry, nodeLabelKey);
244242
}
@@ -400,7 +398,8 @@ void update(
400398
@Nullable MapValue targetNodePropertyValues,
401399
NodeLabelToken sourceNodeLabels,
402400
NodeLabelToken targetNodeLabels,
403-
AnyValue relationshipConfig
401+
AnyValue relationshipConfig,
402+
ConfigValidator configValidator
404403
) {
405404
MapValue relationshipProperties = null;
406405
RelationshipType relationshipType = RelationshipType.ALL_RELATIONSHIPS;
@@ -409,13 +408,7 @@ void update(
409408
relationshipProperties = propertiesConfig("properties", (MapValue) relationshipConfig);
410409
relationshipType = typeConfig("relationshipType", (MapValue) relationshipConfig);
411410

412-
// TODO:
413-
// if (!relationshipConfig.isEmpty()) {
414-
// CypherMapWrapper.create(relationshipConfig).requireOnlyKeysFrom(List.of(
415-
// "properties",
416-
// "relationshipType"
417-
// ));
418-
// }
411+
configValidator.validateRelationshipsConfig((MapValue) relationshipConfig);
419412
}
420413

421414
var intermediateSourceId = loadNode(sourceNode, sourceNodeLabels, sourceNodePropertyValues);
@@ -678,7 +671,6 @@ private static MapValue propertiesConfig(
678671
String propertyKey,
679672
@NotNull MapValue propertiesConfig
680673
) {
681-
// TODO: was remove
682674
var nodeProperties = propertiesConfig.get(propertyKey);
683675
if (nodeProperties instanceof MapValue) {
684676
return (MapValue) nodeProperties;
@@ -698,7 +690,6 @@ private static RelationshipType typeConfig(
698690
@SuppressWarnings("SameParameterValue") String relationshipTypeKey,
699691
@NotNull MapValue relationshipConfig
700692
) {
701-
// TODO: was remove
702693
var relationshipTypeEntry = relationshipConfig.get(relationshipTypeKey);
703694
if (relationshipTypeEntry instanceof TextValue) {
704695
return RelationshipType.of(((TextValue) relationshipTypeEntry).stringValue());
@@ -719,6 +710,34 @@ private static long extractNodeId(@NotNull AnyValue node) {
719710
}
720711
}
721712

713+
private static final class ConfigValidator {
714+
private static final Set<String> NODES_CONFIG_KEYS = Set.of(
715+
"sourceNodeProperties",
716+
"sourceNodeLabels",
717+
"targetNodeProperties",
718+
"targetNodeLabels"
719+
);
720+
721+
private static final Set<String> RELATIONSHIPS_CONFIG_KEYS = Set.of(
722+
"properties",
723+
"relationshipType"
724+
);
725+
726+
private final AtomicBoolean validateNodes = new AtomicBoolean(true);
727+
private final AtomicBoolean validateRelationships = new AtomicBoolean(true);
728+
729+
void validateNodesConfig(MapValue nodesConfig) {
730+
if (this.validateNodes.getAndSet(false)) {
731+
ConfigKeyValidation.requireOnlyKeysFrom(NODES_CONFIG_KEYS, nodesConfig.keySet());
732+
}
733+
}
734+
735+
void validateRelationshipsConfig(MapValue relationshipConfig) {
736+
if (this.validateRelationships.getAndSet(false)) {
737+
ConfigKeyValidation.requireOnlyKeysFrom(RELATIONSHIPS_CONFIG_KEYS, relationshipConfig.keySet());
738+
}
739+
}
740+
}
722741

723742
@ValueClass
724743
@Configuration
@@ -770,6 +789,7 @@ static GraphProjectFromCypherAggregationConfig of(String userName, String graphN
770789
);
771790
}
772791

792+
@TestOnly
773793
static GraphProjectFromCypherAggregationConfig of(
774794
String userName,
775795
String graphName,

cypher-aggregation/src/test/java/org/neo4j/gds/projection/CypherAggregationTest.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,9 @@ void testEmptyNodeLabel() {
334334
}
335335

336336
@Test
337-
void testDirectLabelMapping() {
338-
assertThatThrownBy(() -> runQuery(
339-
"MATCH (s) RETURN gds.alpha.graph.project('g', s, null, { sourceNodeLabels: true })"))
337+
void testInvalidDirectLabelMapping() {
338+
var query = "MATCH (s) RETURN gds.alpha.graph.project('g', s, null, { sourceNodeLabels: true })";
339+
assertThatThrownBy(() -> runQuery(query))
340340
.rootCause()
341341
.isInstanceOf(IllegalArgumentException.class)
342342
.hasMessage("Using `true` to load all labels is deprecated, use `{ sourceNodeLabels: labels(s) }` instead");
@@ -711,4 +711,38 @@ void testReadConcurrencyIsParsedCorrectly() {
711711
assertThat(config.readConcurrency()).isEqualTo(2);
712712
}
713713

714+
@Test
715+
void testUnknownNodeConfigKeys() {
716+
var query = "MATCH (s) RETURN gds.alpha.graph.project('g', s, null, {foo: 'bar'})";
717+
assertThatThrownBy(() -> runQuery(query))
718+
.rootCause()
719+
.hasMessage("Unexpected configuration key: foo");
720+
}
721+
722+
@Test
723+
void testUnknownNodeConfigKeysWithSuggestion() {
724+
var query = "MATCH (s) RETURN gds.alpha.graph.project('g', s, null, {sourceNode: 'bar'})";
725+
assertThatThrownBy(() -> runQuery(query))
726+
.rootCause()
727+
.hasMessage(
728+
"Unexpected configuration key: sourceNode (Did you mean one of [sourceNodeLabels, sourceNodeProperties]?)");
729+
}
730+
731+
@Test
732+
void testUnknownRelationshipConfigKeys() {
733+
var query = "MATCH (s)-->(t) RETURN gds.alpha.graph.project('g', s, t, null, {foo: 'bar'})";
734+
assertThatThrownBy(() -> runQuery(query))
735+
.rootCause()
736+
.hasMessage("Unexpected configuration key: foo");
737+
}
738+
739+
@Test
740+
void testUnknownRelationshipConfigKeysWithSuggestion() {
741+
var query = "MATCH (s)--(t) RETURN gds.alpha.graph.project('g', s, null, null, {property: 'bar'})";
742+
assertThatThrownBy(() -> runQuery(query))
743+
.rootCause()
744+
.hasMessage(
745+
"Unexpected configuration key: property (Did you mean [properties]?)");
746+
}
747+
714748
}

0 commit comments

Comments
 (0)