add visitor that find all values by a prefix of the key
This commit is contained in:
@@ -137,6 +137,34 @@ class NodeEntry {
|
|||||||
return key.length - otherKey.length;
|
return key.length - otherKey.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPrefix(final byte[] keyPrefix) {
|
||||||
|
|
||||||
|
return compareKeyPrefix(keyPrefix) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link #compare(byte[])}, but return 0 if prefix is a prefix of the
|
||||||
|
* key. {@link #compare(byte[])} return values >0 in that case, because key
|
||||||
|
* is longer than the prefix.
|
||||||
|
*
|
||||||
|
* @param prefix the prefix
|
||||||
|
* @return 0 if {@code prefix} is a prefix of the key otherwise the value is
|
||||||
|
* defined by {@link #compare(byte[])}
|
||||||
|
*/
|
||||||
|
public int compareKeyPrefix(final byte[] prefix) {
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < key.length && i < prefix.length) {
|
||||||
|
if (key[i] != prefix[i]) {
|
||||||
|
return key[i] - prefix[i];
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.length > prefix.length ? 0 : key.length - prefix.length;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public boolean equal(final byte[] otherKey) {
|
public boolean equal(final byte[] otherKey) {
|
||||||
return compare(otherKey) == 0;
|
return compare(otherKey) == 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import java.io.IOException;
|
|||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
|
||||||
import org.lucares.pdb.blockstorage.intsequence.VariableByteEncoder;
|
import org.lucares.pdb.blockstorage.intsequence.VariableByteEncoder;
|
||||||
@@ -28,6 +31,10 @@ public class PersistentMap<K, V> {
|
|||||||
void visit(PersistentMapDiskNode node, PersistentMapDiskNode parentNode, NodeEntry nodeEntry, int depth);
|
void visit(PersistentMapDiskNode node, PersistentMapDiskNode parentNode, NodeEntry nodeEntry, int depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Visitor<K, V> {
|
||||||
|
void visit(K key, V value);
|
||||||
|
}
|
||||||
|
|
||||||
public interface EncoderDecoder<O> {
|
public interface EncoderDecoder<O> {
|
||||||
public byte[] encode(O object);
|
public byte[] encode(O object);
|
||||||
|
|
||||||
@@ -109,6 +116,12 @@ public class PersistentMap<K, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void putAllValues(final Map<K, V> map) throws IOException {
|
||||||
|
for (final Entry<K, V> e : map.entrySet()) {
|
||||||
|
putValue(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public V putValue(final K key, final V value) throws IOException {
|
public V putValue(final K key, final V value) throws IOException {
|
||||||
final byte[] encodedKey = keyEncoder.encode(key);
|
final byte[] encodedKey = keyEncoder.encode(key);
|
||||||
final byte[] encodedValue = valueEncoder.encode(value);
|
final byte[] encodedValue = valueEncoder.encode(value);
|
||||||
@@ -305,6 +318,44 @@ public class PersistentMap<K, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum VisitByPrefixMode {
|
||||||
|
FIND, ITERATE
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visitValues(final K keyPrefix, final Visitor<K, V> visitor) throws IOException {
|
||||||
|
final byte[] encodedKeyPrefix = keyEncoder.encode(keyPrefix);
|
||||||
|
|
||||||
|
final long rootNodeOffset = readNodeOffsetOfRootNode();
|
||||||
|
iterateNodeEntryByPrefix(rootNodeOffset, encodedKeyPrefix, visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void iterateNodeEntryByPrefix(final long nodeOffest, final byte[] keyPrefix, final Visitor<K, V> visitor)
|
||||||
|
throws IOException {
|
||||||
|
final PersistentMapDiskNode node = getNode(nodeOffest);
|
||||||
|
|
||||||
|
// list of children that might contain a key with the keyPrefix
|
||||||
|
final List<NodeEntry> nodesForPrefix = node.getNodesByPrefix(keyPrefix);
|
||||||
|
|
||||||
|
for (final NodeEntry entry : nodesForPrefix) {
|
||||||
|
|
||||||
|
if (entry.isDataNode()) {
|
||||||
|
final int prefixCompareResult = entry.compareKeyPrefix(keyPrefix);
|
||||||
|
if (prefixCompareResult == 0) {
|
||||||
|
|
||||||
|
final K key = keyEncoder.decode(entry.getKey());
|
||||||
|
final V value = valueEncoder.decode(entry.getValue());
|
||||||
|
visitor.visit(key, value);
|
||||||
|
// System.out.println("--> " + key + "=" + value);
|
||||||
|
} else if (prefixCompareResult > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final long childNodeOffset = toNodeOffset(entry);
|
||||||
|
iterateNodeEntryByPrefix(childNodeOffset, keyPrefix, visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long readNodeOffsetOfRootNode() throws IOException {
|
private long readNodeOffsetOfRootNode() throws IOException {
|
||||||
final DiskBlock diskBlock = diskStore.getDiskBlock(NODE_OFFSET_TO_ROOT_NODE, diskStore.minAllocationSize());
|
final DiskBlock diskBlock = diskStore.getDiskBlock(NODE_OFFSET_TO_ROOT_NODE, diskStore.minAllocationSize());
|
||||||
|
|
||||||
@@ -316,4 +367,5 @@ public class PersistentMap<K, V> {
|
|||||||
diskBlock.getByteBuffer().putLong(0, newNodeOffsetToRootNode);
|
diskBlock.getByteBuffer().putLong(0, newNodeOffsetToRootNode);
|
||||||
diskBlock.force();
|
diskBlock.force();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,13 +102,32 @@ public class PersistentMapDiskNode {
|
|||||||
|
|
||||||
if (entry.compare(key) >= 0) {
|
if (entry.compare(key) >= 0) {
|
||||||
return entry;
|
return entry;
|
||||||
} else {
|
|
||||||
// break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<NodeEntry> getNodesByPrefix(final byte[] keyPrefix) {
|
||||||
|
final List<NodeEntry> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (final NodeEntry nodeEntry : entries) {
|
||||||
|
final int prefixCompareResult = nodeEntry.compareKeyPrefix(keyPrefix);
|
||||||
|
if (prefixCompareResult == 0) {
|
||||||
|
// add all entries where keyPrefix is a prefix of the key
|
||||||
|
result.add(nodeEntry);
|
||||||
|
} else if (prefixCompareResult > 0) {
|
||||||
|
// Only add the first entry where the keyPrefix is smaller (as defined by
|
||||||
|
// compareKeyPrefix) than the key.
|
||||||
|
// These are entries that might contain key with the keyPrefix. But only the
|
||||||
|
// first of those can really have such keys.
|
||||||
|
result.add(nodeEntry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public void addKeyValue(final byte[] key, final byte[] value) {
|
public void addKeyValue(final byte[] key, final byte[] value) {
|
||||||
addNode(ValueType.VALUE_INLINE, key, value);
|
addNode(ValueType.VALUE_INLINE, key, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,37 @@
|
|||||||
package org.lucares.pdb.map;
|
package org.lucares.pdb.map;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.lucares.pdb.map.NodeEntry.ValueType;
|
||||||
|
import org.testng.Assert;
|
||||||
|
import org.testng.annotations.DataProvider;
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public class NodeEntryTest {
|
public class NodeEntryTest {
|
||||||
|
@DataProvider
|
||||||
|
public Object[][] providerPrefixCompare() {
|
||||||
|
final List<Object[]> result = new ArrayList<>();
|
||||||
|
|
||||||
|
result.add(new Object[] { "ab", "abc", -1 });
|
||||||
|
result.add(new Object[] { "abb", "abc", -1 });
|
||||||
|
result.add(new Object[] { "abc", "abc", 0 });
|
||||||
|
result.add(new Object[] { "abcd", "abc", 0 });
|
||||||
|
result.add(new Object[] { "abd", "abc", 1 });
|
||||||
|
result.add(new Object[] { "abz", "abc", 23 });
|
||||||
|
|
||||||
|
return result.toArray(Object[][]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(dataProvider = "providerPrefixCompare")
|
||||||
|
public void testPrefixCompare(final String key, final String prefix, final int expected) {
|
||||||
|
|
||||||
|
final NodeEntry nodeEntry = new NodeEntry(ValueType.NODE_POINTER, key.getBytes(StandardCharsets.UTF_8),
|
||||||
|
new byte[0]);
|
||||||
|
|
||||||
|
final int actual = nodeEntry.compareKeyPrefix(prefix.getBytes(StandardCharsets.UTF_8));
|
||||||
|
Assert.assertEquals(actual, expected, key + " ? " + prefix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import java.nio.file.Path;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
@@ -14,6 +16,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.lucares.pdb.diskstorage.DiskStorage;
|
import org.lucares.pdb.diskstorage.DiskStorage;
|
||||||
|
import org.lucares.pdb.map.PersistentMap.Visitor;
|
||||||
import org.lucares.utils.file.FileUtils;
|
import org.lucares.utils.file.FileUtils;
|
||||||
import org.testng.Assert;
|
import org.testng.Assert;
|
||||||
import org.testng.annotations.AfterMethod;
|
import org.testng.annotations.AfterMethod;
|
||||||
@@ -238,4 +241,42 @@ public class PersistentMapTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindAllByPrefix() throws Exception {
|
||||||
|
final Path file = dataDirectory.resolve("map.db");
|
||||||
|
|
||||||
|
final Map<String, String> expectedBar = new HashMap<>();
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
// the value is a little bit longer to make sure that the values don't fit into
|
||||||
|
// a single leaf node
|
||||||
|
expectedBar.put("bar:" + i, "bar:" + i + "__##################################");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, String> input = new HashMap<>();
|
||||||
|
input.putAll(expectedBar);
|
||||||
|
for (int i = 0; i < 500; i++) {
|
||||||
|
input.put(UUID.randomUUID().toString(), UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final DiskStorage ds = new DiskStorage(file)) {
|
||||||
|
final PersistentMap<String, String> map = new PersistentMap<>(ds, PersistentMap.STRING_CODER,
|
||||||
|
PersistentMap.STRING_CODER);
|
||||||
|
|
||||||
|
map.putAllValues(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final DiskStorage ds = new DiskStorage(file)) {
|
||||||
|
final PersistentMap<String, String> map = new PersistentMap<>(ds, PersistentMap.STRING_CODER,
|
||||||
|
PersistentMap.STRING_CODER);
|
||||||
|
|
||||||
|
{
|
||||||
|
final LinkedHashMap<String, String> actualBar = new LinkedHashMap<>();
|
||||||
|
final Visitor<String, String> visitor = (key, value) -> actualBar.put(key, value);
|
||||||
|
map.visitValues("bar:", visitor);
|
||||||
|
|
||||||
|
Assert.assertEquals(actualBar, expectedBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user