-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix sorting and ambiguous join issue #1127
Changes from 8 commits
75fb02c
6c5760b
0e29d41
d74a853
1a9857c
84dfe53
3e6577d
2098cf8
e059910
b26bf25
0dc6072
5c0503f
221ff8a
c05c595
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
* Copyright 2019, Yahoo Inc. | ||
* Licensed under the Apache License, Version 2.0 | ||
* See LICENSE file in project root for terms. | ||
*/ | ||
package com.yahoo.elide.utils; | ||
|
||
import com.yahoo.elide.core.EntityDictionary; | ||
import com.yahoo.elide.core.Path; | ||
|
||
import javafx.util.Pair; | ||
import lombok.Data; | ||
|
||
import java.util.ArrayDeque; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Queue; | ||
import java.util.Set; | ||
import java.util.function.BiFunction; | ||
|
||
/** | ||
* This is a structure for storing and de-duplicating elide join paths. | ||
* Basically, it is a Trie which uses relationship field names to navigate through the path. | ||
*/ | ||
@Data | ||
public class JoinTrieNode { | ||
private final Class<?> type; | ||
private final Map<String, JoinTrieNode> fields = new HashMap<>(); | ||
|
||
public JoinTrieNode(Class<?> type) { | ||
this.type = type; | ||
} | ||
|
||
public void addPaths(Set<Path> paths, EntityDictionary dictionary) { | ||
paths.forEach(path -> addPath(path, dictionary)); | ||
} | ||
|
||
/** | ||
* Add all path elements into this Trie, starting from the root. | ||
* | ||
* @param path full Elide join path, i.e. <code>foo.bar.baz</code> | ||
* @param dictionary dictionary to use. | ||
*/ | ||
public void addPath(Path path, EntityDictionary dictionary) { | ||
JoinTrieNode node = this; | ||
|
||
for (Path.PathElement pathElement : path.getPathElements()) { | ||
Class<?> entityClass = pathElement.getType(); | ||
String fieldName = pathElement.getFieldName(); | ||
|
||
if (!dictionary.isRelation(entityClass, fieldName)) { | ||
break; | ||
} | ||
|
||
if (!fields.containsKey(fieldName)) { | ||
node.addField(fieldName, new JoinTrieNode(pathElement.getFieldType())); | ||
|
||
} | ||
|
||
node = fields.get(fieldName); | ||
} | ||
} | ||
|
||
/** | ||
* Attach a field to this node. | ||
* | ||
* @param fieldName field name | ||
* @param node field node | ||
*/ | ||
private void addField(String fieldName, JoinTrieNode node) { | ||
fields.put(fieldName, node); | ||
} | ||
|
||
/** | ||
* Traverse this Trie and project the result into a list in level-first-order. | ||
* This previous result-node pair would be carried through the traversal. | ||
* | ||
* @param generator function that generate new results from previous result-node pair and new trie field | ||
* @param traverser function that carry previous result for next level traversal | ||
* @param identity initial result value | ||
* @param <T> type of each individual result | ||
* @return resulted projected in a list in level-first-order. | ||
*/ | ||
public <T> List<T> levelOrderedTraverse( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this function should take no functional arguments, and instead just return a list or a stream of Paths. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My opinion is that if we already have a list of Paths, then we don't need to use this Trie. |
||
BiFunction<Pair<JoinTrieNode, T>, Map.Entry<String, JoinTrieNode>, T> generator, | ||
BiFunction<Pair<JoinTrieNode, T>, Map.Entry<String, JoinTrieNode>, T> traverser, | ||
T identity | ||
) { | ||
// node-result pairs queue | ||
Queue<Pair<JoinTrieNode, T>> todo = new ArrayDeque<>(); | ||
|
||
todo.add(new Pair<>(this, identity)); | ||
List<T> results = new ArrayList<>(); | ||
|
||
while (!todo.isEmpty()) { | ||
Pair<JoinTrieNode, T> parentResult = todo.remove(); | ||
|
||
parentResult.getKey().getFields().entrySet().forEach(childField -> { | ||
results.add(generator.apply(parentResult, childField)); | ||
|
||
if (childField.getValue().getFields().size() > 0) { | ||
todo.add(new Pair<>(childField.getValue(), traverser.apply(parentResult, childField))); | ||
} | ||
}); | ||
} | ||
|
||
return results; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright 2015, Yahoo Inc. | ||
* Licensed under the Apache License, Version 2.0 | ||
* See LICENSE file in project root for terms. | ||
*/ | ||
package com.yahoo.elide.utils; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
import com.yahoo.elide.core.EntityDictionary; | ||
import com.yahoo.elide.core.Path; | ||
|
||
import example.Book; | ||
import example.Editor; | ||
import example.Publisher; | ||
import org.junit.jupiter.api.BeforeAll; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
public class JoinTrieNodeTest { | ||
private static EntityDictionary dictionary; | ||
|
||
@BeforeAll | ||
public static void init() { | ||
dictionary = new EntityDictionary(new HashMap<>()); | ||
dictionary.bindEntity(Book.class); | ||
dictionary.bindEntity(Publisher.class); | ||
dictionary.bindEntity(Editor.class); | ||
} | ||
|
||
@Test | ||
public void testAddPath() { | ||
Path path = new Path(Book.class, dictionary, "publisher.editor.id"); | ||
JoinTrieNode node = new JoinTrieNode(Book.class); | ||
node.addPath(path, dictionary); | ||
|
||
Map<String, JoinTrieNode> firstLevel = node.getFields(); | ||
assertEquals(1, firstLevel.size()); | ||
assertEquals(Publisher.class, firstLevel.get("publisher").getType()); | ||
|
||
Map<String, JoinTrieNode> secondLevel = firstLevel.get("publisher").getFields(); | ||
assertEquals(1, secondLevel.size()); | ||
assertEquals(Editor.class, secondLevel.get("editor").getType()); | ||
|
||
Map<String, JoinTrieNode> thirdLevel = secondLevel.get("editor").getFields(); | ||
assertEquals(0, thirdLevel.size()); | ||
} | ||
|
||
@Test | ||
public void testTraversal() { | ||
Path path = new Path(Book.class, dictionary, "publisher.editor.id"); | ||
JoinTrieNode node = new JoinTrieNode(Book.class); | ||
node.addPath(path, dictionary); | ||
|
||
List<String> results = node.levelOrderedTraverse( | ||
(parentResult, childEntry) -> parentResult.getValue() + "." + childEntry.getKey(), | ||
(parentResult, childEntry) -> parentResult.getValue() + "." + childEntry.getKey(), | ||
"book" | ||
); | ||
|
||
assertEquals(2, results.size()); | ||
assertEquals("book.publisher", results.get(0)); | ||
assertEquals("book.publisher.editor", results.get(1)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a unit test class (JoinTrieNodeTest) to get test coverage to a high percentage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍