提交 2ceb5ec6 authored 作者: Thomas Mueller's avatar Thomas Mueller

A persistent multi-version map (work in progress)

上级 a94da0f4
......@@ -104,6 +104,7 @@ import org.h2.test.server.TestNestedLoop;
import org.h2.test.server.TestWeb;
import org.h2.test.server.TestInit;
import org.h2.test.store.TestCacheLIRS;
import org.h2.test.store.TestConcurrent;
import org.h2.test.store.TestDataUtils;
import org.h2.test.store.TestMVStore;
import org.h2.test.store.TestMVRTree;
......@@ -664,11 +665,12 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1`
}
private void testUnit() {
// store
new TestMVStore().runTest(this);
// mv store
new TestCacheLIRS().runTest(this);
new TestConcurrent().runTest(this);
new TestDataUtils().runTest(this);
new TestMVRTree().runTest(this);
new TestMVStore().runTest(this);
// unit
new TestAutoReconnect().runTest(this);
......
......@@ -35,9 +35,6 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
@SuppressWarnings("unchecked")
public V get(Object key) {
checkOpen();
if (root == null) {
return null;
}
return (V) get(root, key);
}
......@@ -73,13 +70,10 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
}
protected Page getPage(K key) {
if (root == null) {
return null;
}
return getPage(root, key);
}
protected Page getPage(Page p, Object key) {
private Page getPage(Page p, Object key) {
if (!p.isLeaf()) {
for (int i = 0; i < p.getKeyCount(); i++) {
if (contains(p, i, key)) {
......@@ -155,26 +149,26 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
return (V) putOrAdd(key, value, false);
}
/**
* Add a given key-value pair. The key should not exist (if it exists, the
* result is undefined).
*
* @param key the key
* @param value the value
*/
public void add(K key, V value) {
putOrAdd(key, value, true);
}
public Object putOrAdd(K key, V value, boolean alwaysAdd) {
private Object putOrAdd(K key, V value, boolean alwaysAdd) {
checkWrite();
long writeVersion = store.getCurrentVersion();
Page p = root;
Page p = root.copyOnWrite(writeVersion);
Object result;
if (p == null) {
Object[] keys = { key };
Object[] values = { value };
p = Page.create(this, writeVersion, 1,
keys, values, null, null, null, 1, 0);
result = null;
} else if (alwaysAdd || get(key) == null) {
if (alwaysAdd || get(key) == null) {
if (p.getKeyCount() > store.getMaxPageSize()) {
// only possible if this is the root, else we would have split earlier
// (this requires maxPageSize is fixed)
p = p.copyOnWrite(writeVersion);
long totalCount = p.getTotalCount();
Page split = split(p, writeVersion);
Object[] keys = { getBounds(p), getBounds(split) };
......@@ -195,7 +189,16 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
return result;
}
protected Object set(Page p, long writeVersion, Object key, Object value) {
/**
* Update the value for the given key. The key must exist.
*
* @param p the page
* @param writeVersion the write version
* @param key the key
* @param value the value
* @return the old value
*/
private Object set(Page p, long writeVersion, Object key, Object value) {
if (!p.isLeaf()) {
for (int i = 0; i < p.getKeyCount(); i++) {
if (contains(p, i, key)) {
......@@ -217,7 +220,7 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
return null;
}
protected void add(Page p, long writeVersion, Object key, Object value) {
private void add(Page p, long writeVersion, Object key, Object value) {
if (p.isLeaf()) {
p.insertLeaf(p.getKeyCount(), key, value);
return;
......@@ -379,6 +382,13 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
source.remove(sourceIndex);
}
/**
* Add all node keys (including internal bounds) to the given list.
* This is mainly used to visualize the internal splits.
*
* @param list the list
* @param p the root page
*/
@SuppressWarnings("unchecked")
public void addNodeKeys(ArrayList<K> list, Page p) {
if (p != null && !p.isLeaf()) {
......@@ -395,6 +405,7 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
* @param p the current page
* @param cursor the cursor
* @param key the key
* @return the cursor position
*/
protected CursorPos min(Page p, Cursor<K, V> cursor, Object key) {
if (p == null) {
......
......@@ -21,7 +21,15 @@ import org.h2.dev.store.btree.DataType;
*/
public class SequenceMap<K, V> extends MVMap<K, V> {
int min = 1, max = 10;
/**
* The minimum value.
*/
int min = 1;
/**
* The maximum value.
*/
int max = 10;
SequenceMap(MVStore store, int id, String name, DataType keyType,
DataType valueType, long createVersion) {
......
......@@ -14,8 +14,8 @@ import java.util.Arrays;
*/
public class SpatialKey {
public long id;
private float[] minMax;
private final long id;
private final float[] minMax;
/**
* Create a new key.
......@@ -28,22 +28,50 @@ public class SpatialKey {
this.minMax = minMax;
}
/**
* Get the minimum value for the given dimension.
*
* @param dim the dimension
* @return the value
*/
public float min(int dim) {
return minMax[dim + dim];
}
/**
* Set the minimum value for the given dimension.
*
* @param dim the dimension
* @param x the value
*/
public void setMin(int dim, float x) {
minMax[dim + dim] = x;
}
/**
* Get the maximum value for the given dimension.
*
* @param dim the dimension
* @return the value
*/
public float max(int dim) {
return minMax[dim + dim + 1];
}
/**
* Set the maximum value for the given dimension.
*
* @param dim the dimension
* @param x the value
*/
public void setMax(int dim, float x) {
minMax[dim + dim + 1] = x;
}
public long getId() {
return id;
}
public String toString() {
StringBuilder buff = new StringBuilder();
buff.append(id).append(": (");
......
......@@ -27,20 +27,33 @@ public class SpatialType implements DataType {
this.dimensions = dimensions;
}
/**
* Read a value from a string.
*
* @param s the string
* @return the value
*/
public static SpatialType fromString(String s) {
return new SpatialType(Integer.parseInt(s.substring(1)));
}
@Override
public int compare(Object a, Object b) {
long la = ((SpatialKey) a).id;
long lb = ((SpatialKey) b).id;
long la = ((SpatialKey) a).getId();
long lb = ((SpatialKey) b).getId();
return la < lb ? -1 : la > lb ? 1 : 0;
}
/**
* Check whether two spatial values are equal.
*
* @param a the first value
* @param b the second value
* @return true if they are equal
*/
public boolean equals(Object a, Object b) {
long la = ((SpatialKey) a).id;
long lb = ((SpatialKey) b).id;
long la = ((SpatialKey) a).getId();
long lb = ((SpatialKey) b).getId();
return la == lb;
}
......@@ -70,7 +83,7 @@ public class SpatialType implements DataType {
buff.putFloat(k.max(i));
}
}
DataUtils.writeVarLong(buff, k.id);
DataUtils.writeVarLong(buff, k.getId());
}
@Override
......@@ -97,6 +110,13 @@ public class SpatialType implements DataType {
return "s" + dimensions;
}
/**
* Check whether the two objects overlap.
*
* @param objA the first object
* @param objB the second object
* @return true if they overlap
*/
public boolean isOverlap(Object objA, Object objB) {
SpatialKey a = (SpatialKey) objA;
SpatialKey b = (SpatialKey) objB;
......@@ -108,6 +128,12 @@ public class SpatialType implements DataType {
return true;
}
/**
* Increase the bounds in the given spatial object.
*
* @param bounds the bounds (may be modified)
* @param add the value
*/
public void increaseBounds(Object bounds, Object add) {
SpatialKey b = (SpatialKey) bounds;
SpatialKey a = (SpatialKey) add;
......@@ -144,7 +170,14 @@ public class SpatialType implements DataType {
return areaNew - areaOld;
}
public float getCombinedArea(Object objA, Object objB) {
/**
* Get the combined area of both objects.
*
* @param objA the first object
* @param objB the second object
* @return the area
*/
float getCombinedArea(Object objA, Object objB) {
SpatialKey a = (SpatialKey) objA;
SpatialKey b = (SpatialKey) objB;
float area = 1;
......@@ -193,7 +226,13 @@ public class SpatialType implements DataType {
return true;
}
public Object createBoundingBox(Object objA) {
/**
* Create a bounding box starting with the given object.
*
* @param objA the object
* @return the bounding box
*/
Object createBoundingBox(Object objA) {
float[] minMax = new float[dimensions * 2];
SpatialKey a = (SpatialKey) objA;
for (int i = 0; i < dimensions; i++) {
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.test.store;
import java.util.Map;
import java.util.Random;
import org.h2.dev.store.btree.MVMap;
import org.h2.dev.store.btree.MVStore;
import org.h2.test.TestBase;
import org.h2.util.Task;
/**
* Tests concurrently accessing a tree map store.
*/
public class TestConcurrent extends TestMVStore {
/**
* Run just this test.
*
* @param a ignored
*/
public static void main(String... a) throws Exception {
TestBase.createCaller().init().test();
}
public void test() throws InterruptedException {
testConcurrentWrite();
testConcurrentRead();
}
/**
* Test what happens on concurrent write. Concurrent write may corrupt the
* map, so that keys and values may become null.
*/
private void testConcurrentWrite() throws InterruptedException {
final MVStore s = openStore(null);
final MVMap<Integer, Integer> m = s.openMap("data", Integer.class, Integer.class);
final int size = 20;
final Random rand = new Random(1);
Task task = new Task() {
public void call() throws Exception {
while (!stop) {
try {
if (rand.nextBoolean()) {
m.put(rand.nextInt(size), 1);
} else {
m.remove(rand.nextInt(size));
}
m.get(rand.nextInt(size));
} catch (NullPointerException e) {
// ignore
}
}
}
};
task.execute();
Thread.sleep(1);
for (int j = 0; j < 10; j++) {
for (int i = 0; i < 10; i++) {
try {
if (rand.nextBoolean()) {
m.put(rand.nextInt(size), 2);
} else {
m.remove(rand.nextInt(size));
}
m.get(rand.nextInt(size));
} catch (NullPointerException e) {
// ignore
}
}
s.incrementVersion();
Thread.sleep(1);
}
task.get();
// verify the structure is still somewhat usable
for (int x : m.keySet()) {
try {
m.get(x);
} catch (NullPointerException e) {
// ignore
}
}
for (int i = 0; i < size; i++) {
try {
m.get(i);
} catch (NullPointerException e) {
// ignore
}
}
s.close();
}
private void testConcurrentRead() throws InterruptedException {
final MVStore s = openStore(null);
final MVMap<Integer, Integer> m = s.openMap("data", Integer.class, Integer.class);
final int size = 3;
int x = (int) s.getCurrentVersion();
for (int i = 0; i < size; i++) {
m.put(i, x);
}
s.incrementVersion();
Task task = new Task() {
public void call() throws Exception {
while (!stop) {
long v = s.getCurrentVersion() - 1;
Map<Integer, Integer> old = m.openVersion(v);
for (int i = 0; i < size; i++) {
Integer x = old.get(i);
if (x == null || (int) v != x) {
Map<Integer, Integer> old2 = m.openVersion(v);
throw new AssertionError(x + "<>" + v + " at " + i + " " + old2);
}
}
}
}
};
task.execute();
Thread.sleep(1);
for (int j = 0; j < 100; j++) {
x = (int) s.getCurrentVersion();
for (int i = 0; i < size; i++) {
m.put(i, x);
}
s.incrementVersion();
Thread.sleep(1);
}
task.get();
s.close();
}
}
......@@ -6,6 +6,7 @@
package org.h2.test.store;
import java.nio.ByteBuffer;
import java.util.HashMap;
import org.h2.dev.store.btree.DataUtils;
import org.h2.test.TestBase;
......@@ -24,12 +25,31 @@ public class TestDataUtils extends TestBase {
}
public void test() throws Exception {
testMap();
testVarIntVarLong();
testCheckValue();
testPagePos();
testEncodeLength();
}
private void testMap() {
StringBuilder buff = new StringBuilder();
DataUtils.appendMap(buff, "", "");
DataUtils.appendMap(buff, "a", "1");
DataUtils.appendMap(buff, "b", ",");
DataUtils.appendMap(buff, "c", "1,2");
DataUtils.appendMap(buff, "d", "\"test\"");
assertEquals(":,a:1,b:\",\",c:\"1,2\",d:\"\\\"test\\\"\"", buff.toString());
HashMap<String, String> m = DataUtils.parseMap(buff.toString());
assertEquals(5, m.size());
assertEquals("", m.get(""));
assertEquals("1", m.get("a"));
assertEquals(",", m.get("b"));
assertEquals("1,2", m.get("c"));
assertEquals("\"test\"", m.get("d"));
}
private void testVarIntVarLong() {
ByteBuffer buff = ByteBuffer.allocate(100);
for (long x = 0; x < 1000; x++) {
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.test.store;
import java.awt.AlphaComposite;
......@@ -34,14 +39,14 @@ public class TestMVRTree extends TestMVStore {
}
public void test() {
testRtreeMany();
testRtree();
testRandomRtree();
testMany();
testTree();
testRandom();
testCustomMapType();
}
private void testRtreeMany() {
String fileName = getBaseDir() + "/testRtree.h3";
private void testMany() {
String fileName = getBaseDir() + "/testMany.h3";
FileUtils.delete(fileName);
MVStore s;
s = openStore(fileName);
......@@ -103,8 +108,8 @@ public class TestMVRTree extends TestMVStore {
// System.out.println("remove: " + (System.currentTimeMillis() - t));
}
private void testRtree() {
String fileName = getBaseDir() + "/testRtree.h3";
private void testTree() {
String fileName = getBaseDir() + "/testTree.h3";
FileUtils.delete(fileName);
MVStore s;
s = openStore(fileName);
......@@ -195,8 +200,8 @@ public class TestMVRTree extends TestMVStore {
return rect;
}
private void testRandomRtree() {
String fileName = getBaseDir() + "/testRtreeRandom.h3";
private void testRandom() {
String fileName = getBaseDir() + "/testRandom.h3";
FileUtils.delete(fileName);
MVStore s = openStore(fileName);
MVRTreeMap<SpatialKey, String> m = s.openMap("data", "r", "s2", "");
......
......@@ -7,6 +7,7 @@ package org.h2.test.store;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import org.h2.dev.store.btree.MVMap;
......@@ -30,8 +31,10 @@ public class TestMVStore extends TestBase {
TestBase.createCaller().init().test();
}
public void test() {
public void test() throws InterruptedException {
testExample();
testIterateOverChanges();
testOpenStoreCloseLoop();
testVersion();
testTruncateFile();
testFastDelete();
......@@ -49,31 +52,105 @@ public class TestMVStore extends TestBase {
testSimple();
}
private void testExample() {
String fileName = getBaseDir() + "/testOpenClose.h3";
FileUtils.delete(fileName);
// open the store (in-memory if fileName is null)
MVStore s = MVStore.open(fileName);
// create/get the map "data"
// the String.class, String.class will be optional later
MVMap<String, String> map = s.openMap("data",
String.class, String.class);
// add some data
map.put("1", "Hello");
map.put("2", "World");
// get the current version, for later use
long oldVersion = s.getCurrentVersion();
// from now on, the old version is read-only
s.incrementVersion();
// more changes, in the new version
// changes can be rolled back if required
// changes always go into 'head' (the newest version)
map.put("1", "Hi");
map.remove("2");
// access the old data (before incrementVersion)
MVMap<String, String> oldMap =
map.openVersion(oldVersion);
// store the newest data to disk
s.store();
// print the old version (can be done
// concurrently with further modifications)
// this will print Hello World
// System.out.println(oldMap.get("1"));
// System.out.println(oldMap.get("2"));
oldMap.close();
// print the newest version ("Hi")
// System.out.println(map.get("1"));
// close the store - this doesn't write to disk
s.close();
}
private void testOpenStoreCloseLoop() {
String fileName = getBaseDir() + "/testOpenClose.h3";
FileUtils.delete(fileName);
for (int k = 0; k < 1; k++) {
// long t = System.currentTimeMillis();
for (int j = 0; j < 3; j++) {
MVStore s = openStore(fileName);
Map<String, Integer> m = s.openMap("data", String.class,
Integer.class);
for (int i = 0; i < 3; i++) {
Integer x = m.get("value");
m.put("value", x == null ? 0 : x + 1);
s.store();
}
s.close();
}
// System.out.println("open/close: " + (System.currentTimeMillis() - t));
// System.out.println("size: " + FileUtils.size(fileName));
}
}
private void testIterateOverChanges() {
String fileName = getBaseDir() + "/testVersion.h3";
String fileName = getBaseDir() + "/testIterate.h3";
FileUtils.delete(fileName);
MVStore s = openStore(fileName);
s.setMaxPageSize(6);
MVMap<Integer, String> m = s.openMap("data", Integer.class, String.class);
for (int i = 0; i < 60; i++) {
for (int i = 0; i < 100; i++) {
m.put(i, "Hi");
}
s.commit();
s.incrementVersion();
s.store();
for (int i = 20; i < 40; i++) {
assertEquals("Hi", m.put(i, "Hello"));
}
s.commit();
s.incrementVersion();
for (int i = 10; i < 15; i++) {
m.put(i, "Hallo");
}
m.put(50, "Hallo");
for (int i = 90; i < 100; i++) {
assertEquals("Hi", m.remove(i));
}
assertEquals(null, m.put(100, "Hallo"));
Iterator<Integer> it = m.changeIterator(s.getCurrentVersion());
ArrayList<Integer> list = New.arrayList();
while (it.hasNext()) {
list.add(it.next());
}
assertEquals("[9, 10, 11, 12, 13, 14, 48, 49, 50]", list.toString());
assertEquals("[9, 10, 11, 12, 13, 14, 48, 49, 50, 87, 88, 89, 100]", list.toString());
}
private void testVersion() {
......@@ -85,13 +162,14 @@ public class TestMVStore extends TestBase {
s = openStore(fileName);
m = s.openMap("data", String.class, String.class);
long first = s.getCurrentVersion();
s.incrementVersion();
m.put("1", "Hello");
m.put("2", "World");
for (int i = 10; i < 20; i++) {
m.put("" + i, "data");
}
s.commit();
long old = s.getCurrentVersion();
s.incrementVersion();
m.put("1", "Hallo");
m.put("2", "Welt");
MVMap<String, String> mFirst;
......@@ -192,7 +270,7 @@ public class TestMVStore extends TestBase {
assertTrue(s.hasUnsavedChanges());
MVMap<String, String> m0 = s.openMap("data0", String.class, String.class);
m.put("1", "Hello");
assertEquals(1, s.commit());
assertEquals(1, s.incrementVersion());
s.rollbackTo(1);
assertEquals("Hello", m.get("1"));
long v2 = s.store();
......@@ -235,7 +313,7 @@ public class TestMVStore extends TestBase {
assertEquals("Hello", m.get("1"));
assertFalse(m0.isReadOnly());
m.put("1", "Hallo");
s.commit();
s.incrementVersion();
assertEquals(4, s.getCurrentVersion());
long v4 = s.store();
assertEquals(4, v4);
......@@ -282,7 +360,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 10; i++) {
m2.put("" + i, "Test");
}
long v1 = s.commit();
long v1 = s.incrementVersion();
assertEquals(1, v1);
assertEquals(2, s.getCurrentVersion());
MVMap<String, String> m1 = s.openMap("data1", String.class, String.class);
......@@ -667,6 +745,12 @@ public class TestMVStore extends TestBase {
s.close();
}
/**
* Open a store for the given file name, using a small page size.
*
* @param fileName the file name (null for in-memory)
* @return the store
*/
protected static MVStore openStore(String fileName) {
MVStore store = MVStore.open(fileName, new TestMapFactory());
store.setMaxPageSize(10);
......
......@@ -47,6 +47,10 @@ public class FilePathCache extends FilePathWrapper {
this.size = base.size();
}
protected void implCloseChannel() throws IOException {
base.close();
}
public FileChannel position(long newPosition) throws IOException {
this.pos = newPosition;
return this;
......
......@@ -15,7 +15,7 @@ import java.util.Map;
import java.util.Set;
/**
* A scan resistent cache. It is meant to cache objects that are relatively
* A scan resistant cache. It is meant to cache objects that are relatively
* costly to acquire, for example file content.
* <p>
* This implementation is not multi-threading save. Null keys or null values are
......@@ -237,6 +237,7 @@ public class CacheLIRS<K, V> extends AbstractMap<K, V> implements Map<K, V> {
*
* @param key the key (may not be null)
* @param value the value (may not be null)
* @return the old value, or null if there is no resident entry
*/
public V put(K key, V value) {
return put(key, value, averageMemory);
......@@ -250,6 +251,7 @@ public class CacheLIRS<K, V> extends AbstractMap<K, V> implements Map<K, V> {
* @param key the key (may not be null)
* @param value the value (may not be null)
* @param memory the memory used for the given entry
* @return the old value, or null if there is no resident entry
*/
public V put(K key, V value, int memory) {
if (value == null) {
......@@ -484,6 +486,7 @@ public class CacheLIRS<K, V> extends AbstractMap<K, V> implements Map<K, V> {
/**
* Check whether there is a resident entry for the given key.
*
* @param key the key (may not be null)
* @return true if there is a resident entry
*/
public boolean containsKey(Object key) {
......
......@@ -6,9 +6,7 @@
*/
package org.h2.dev.store.btree;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.HashMap;
/**
* A chunk of data, containing one or multiple pages.
......@@ -23,7 +21,7 @@ import java.util.Properties;
* 8 bytes: metaRootPos
* [ Page ] *
*/
class Chunk {
public class Chunk {
/**
* The chunk id.
......@@ -65,7 +63,7 @@ class Chunk {
*/
long version;
Chunk(int id) {
public Chunk(int id) {
this.id = id;
}
......@@ -75,22 +73,17 @@ class Chunk {
* @param s the string
* @return the block
*/
static Chunk fromString(String s) {
Properties prop = new Properties();
try {
prop.load(new ByteArrayInputStream(s.getBytes("UTF-8")));
int id = Integer.parseInt(prop.get("id").toString());
Chunk c = new Chunk(id);
c.start = Long.parseLong(prop.get("start").toString());
c.length = Long.parseLong(prop.get("length").toString());
c.entryCount = Integer.parseInt(prop.get("entryCount").toString());
c.liveCount = Integer.parseInt(prop.get("liveCount").toString());
c.metaRootPos = Long.parseLong(prop.get("metaRoot").toString());
c.version = Long.parseLong(prop.get("version").toString());
return c;
} catch (IOException e) {
throw new RuntimeException(e);
}
public static Chunk fromString(String s) {
HashMap<String, String> map = DataUtils.parseMap(s);
int id = Integer.parseInt(map.get("id"));
Chunk c = new Chunk(id);
c.start = Long.parseLong(map.get("start"));
c.length = Long.parseLong(map.get("length"));
c.entryCount = Integer.parseInt(map.get("entryCount"));
c.liveCount = Integer.parseInt(map.get("liveCount"));
c.metaRootPos = Long.parseLong(map.get("metaRoot"));
c.version = Long.parseLong(map.get("version"));
return c;
}
public int getFillRate() {
......@@ -107,13 +100,13 @@ class Chunk {
public String toString() {
return
"id:" + id + "\n" +
"start:" + start + "\n" +
"length:" + length + "\n" +
"entryCount:" + entryCount + "\n" +
"liveCount:" + liveCount + "\n" +
"metaRoot:" + metaRootPos + "\n" +
"version:" + version + "\n";
"id:" + id + "," +
"start:" + start + "," +
"length:" + length + "," +
"entryCount:" + entryCount + "," +
"liveCount:" + liveCount + "," +
"metaRoot:" + metaRootPos + "," +
"version:" + version;
}
}
......
......@@ -26,6 +26,12 @@ public class Cursor<K, V> implements Iterator<K> {
this.map = map;
}
/**
* Fetch the first key.
*
* @param root the root page
* @param from the key, or null
*/
void start(Page root, K from) {
currentPos = min(root, from);
if (currentPos != null) {
......@@ -41,6 +47,9 @@ public class Cursor<K, V> implements Iterator<K> {
return c == null ? null : c;
}
/**
* Fetch the next key.
*/
@SuppressWarnings("unchecked")
protected void fetchNext() {
current = (K) map.nextKey(currentPos, this);
......@@ -54,19 +63,43 @@ public class Cursor<K, V> implements Iterator<K> {
throw new UnsupportedOperationException();
}
/**
* Add a cursor position to the stack.
*
* @param p the cursor position
*/
public void push(CursorPos p) {
parents.add(p);
}
/**
* Remove the latest cursor position from the stack and return it.
*
* @return the cursor position, or null if none
*/
public CursorPos pop() {
int size = parents.size();
return size == 0 ? null : parents.remove(size - 1);
}
/**
* Visit the first key that is greater or equal the given key.
*
* @param p the page
* @param from the key, or null
* @return the cursor position
*/
public CursorPos min(Page p, K from) {
return map.min(p, this, from);
}
/**
* Visit the first key within this child page.
*
* @param p the page
* @param childIndex the child index
* @return the cursor position
*/
public CursorPos visitChild(Page p, int childIndex) {
p = p.getChildPage(childIndex);
currentPos = min(p, null);
......
......@@ -9,6 +9,8 @@ package org.h2.dev.store.btree;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import org.h2.util.New;
/**
* Utility methods
......@@ -195,7 +197,14 @@ public class DataUtils {
}
}
static void readFully(FileChannel file, ByteBuffer buff) throws IOException {
/**
* Read from a file channel until the target buffer is full, or end-of-file
* has been reached.
*
* @param file the file channel
* @param buff the target buffer
*/
public static void readFully(FileChannel file, ByteBuffer buff) throws IOException {
do {
int len = file.read(buff);
if (len < 0) {
......@@ -302,4 +311,70 @@ public class DataUtils {
return (short) ((x >> 16) ^ x);
}
/**
* Append a key-value pair to the string buffer. Keys may not contain a
* colon. Values that contain a comma or a double quote are enclosed in
* double quotes, with special characters escaped using a backslash.
*
* @param buff the target buffer
* @param key the key
* @param value the value
*/
public static void appendMap(StringBuilder buff, String key, Object value) {
if (buff.length() > 0) {
buff.append(',');
}
buff.append(key).append(':');
String v = value.toString();
if (v.indexOf(',') < 0 && v.indexOf('\"') < 0) {
buff.append(value);
} else {
buff.append('\"');
for (int i = 0, size = v.length(); i < size; i++) {
char c = v.charAt(i);
if (c == '\"') {
buff.append('\\');
}
buff.append(c);
}
buff.append('\"');
}
}
/**
* Parse a key-value pair list.
*
* @param s the list
* @return the map
*/
public static HashMap<String, String> parseMap(String s) {
HashMap<String, String> map = New.hashMap();
for (int i = 0, size = s.length(); i < size;) {
int startKey = i;
i = s.indexOf(':', i);
String key = s.substring(startKey, i++);
StringBuilder buff = new StringBuilder();
while (i < size) {
char c = s.charAt(i++);
if (c == ',') {
break;
} else if (c == '\"') {
while (i < size) {
c = s.charAt(i++);
if (c == '\\') {
i++;
} else if (c == '\"') {
break;
}
buff.append(c);
}
} else {
buff.append(c);
}
}
map.put(key, buff.toString());
}
return map;
}
}
......@@ -23,9 +23,9 @@ public interface MapFactory {
* @param createVersion when the map was created
* @return the map
*/
<K, V> MVMap<K, V> buildMap(
String mapType, MVStore store, int id, String name,
DataType keyType, DataType valueType, long createVersion);
<K, V> MVMap<K, V> buildMap(
String mapType, MVStore store, int id, String name,
DataType keyType, DataType valueType, long createVersion);
/**
* Parse the data type.
......
......@@ -15,18 +15,26 @@ import org.h2.compress.Compressor;
/**
* A page (a node or a leaf).
* <p>
* For b-tree nodes, the key at a given index is larger than the largest key of the
* child at the same index.
* For b-tree nodes, the key at a given index is larger than the largest key of
* the child at the same index.
* <p>
* File format: page length (including length): int check value: short map id:
* varInt number of keys: varInt type: byte (0: leaf, 1: node; +2: compressed)
* compressed: bytes saved (varInt) keys leaf: values (one for each key) node:
* children (1 more than keys)
* File format:
* page length (including length): int
* check value: short
* map id: varInt
* number of keys: varInt
* type: byte (0: leaf, 1: node; +2: compressed)
* compressed: bytes saved (varInt)
* keys
* leaf: values (one for each key)
* node: children (1 more than keys)
*/
public class Page {
private static final int SHARED_KEYS = 1, SHARED_VALUES = 2, SHARED_CHILDREN = 4, SHARED_COUNTS = 8;
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
private final MVMap<?, ?> map;
private final long version;
private long pos;
......@@ -54,14 +62,32 @@ public class Page {
this.version = version;
}
/**
* Create a new, empty page.
*
* @param map the map
* @param version the version
* @return the new page
*/
public static Page createEmpty(MVMap<?, ?> map, long version) {
return create(map, version, 0,
EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY,
null, null, null, 0, 0);
}
/**
* Create a new page. The arrays are not cloned.
*
* @param map the map
* @param version the version
* @param keyCount the number of keys
* @param keys the keys
* @param values the values
* @param children the children
* @param childrenPages the children pages
* @param counts the children counts
* @param totalCount the total number of keys
* @param sharedFlags which arrays are shared
* @return the page
*/
public static Page create(MVMap<?, ?> map, long version,
......@@ -84,9 +110,10 @@ public class Page {
/**
* Read a page.
*
* @param file the file
* @param map the map
* @param filePos the position in the file
* @param pos the page position
* @param buff the source buffer
* @return the page
*/
static Page read(FileChannel file, MVMap<?, ?> map,
......@@ -114,27 +141,61 @@ public class Page {
return p;
}
/**
* Get the key at the given index.
*
* @param index the index
* @return the key
*/
public Object getKey(int index) {
return keys[index];
}
/**
* Get the child page at the given index.
*
* @param index the index
* @return the child page
*/
public Page getChildPage(int index) {
Page p = childrenPages[index];
return p != null ? p : map.readPage(children[index]);
}
/**
* Get the position of the child page at the given index.
*
* @param index the index
* @return the position
*/
long getChildPagePos(int index) {
return children[index];
}
public Object getValue(int x) {
return values[x];
/**
* Get the value at the given index.
*
* @param index the index
* @return the value
*/
public Object getValue(int index) {
return values[index];
}
/**
* Get the number of keys in this page.
*
* @return the number of keys
*/
public int getKeyCount() {
return keyCount;
}
/**
* Check whether this is a leaf page.
*
* @return true if it is a leaf
*/
public boolean isLeaf() {
return children == null;
}
......@@ -169,6 +230,13 @@ public class Page {
return buff.toString();
}
/**
* Create a copy of this page, if the write version is higher than the
* current version.
*
* @param writeVersion the write version
* @return a page with the given write version
*/
public Page copyOnWrite(long writeVersion) {
if (version == writeVersion) {
return this;
......@@ -198,8 +266,9 @@ public class Page {
if (x < 0 || x > high) {
x = (low + high) >>> 1;
}
Object[] k = keys;
while (low <= high) {
int compare = map.compare(key, keys[x]);
int compare = map.compare(key, k[x]);
if (compare > 0) {
low = x + 1;
} else if (compare < 0) {
......@@ -229,7 +298,13 @@ public class Page {
// return -(low + 1);
}
public Page split(int at) {
/**
* Split the page. This modifies the current page.
*
* @param at the split index
* @return the page with the entries after the split index
*/
Page split(int at) {
return isLeaf() ? splitLeaf(at) : splitNode(at);
}
......@@ -299,6 +374,11 @@ public class Page {
return newPage;
}
/**
* Get the total number of key-value pairs, including child pages.
*
* @return the number of key-value pairs
*/
public long getTotalCount() {
if (MVStore.ASSERT) {
long check = 0;
......@@ -317,6 +397,12 @@ public class Page {
return totalCount;
}
/**
* Replace the child page.
*
* @param index the index
* @param c the new child page
*/
public void setChild(int index, Page c) {
if (c != childrenPages[index] || c.getPos() != children[index]) {
if ((sharedFlags & SHARED_CHILDREN) != 0) {
......@@ -338,6 +424,12 @@ public class Page {
}
}
/**
* Replace the key.
*
* @param index the index
* @param key the new key
*/
public void setKey(int index, Object key) {
if ((sharedFlags & SHARED_KEYS) != 0) {
keys = Arrays.copyOf(keys, keys.length);
......@@ -346,6 +438,13 @@ public class Page {
keys[index] = key;
}
/**
* Replace the value.
*
* @param index the index
* @param value the new value
* @return the old value
*/
public Object setValue(int index, Object value) {
Object old = values[index];
if ((sharedFlags & SHARED_VALUES) != 0) {
......@@ -379,6 +478,13 @@ public class Page {
map.getStore().removePage(pos);
}
/**
* Insert a key-value pair into this leaf.
*
* @param index the index
* @param key the key
* @param value the value
*/
public void insertLeaf(int index, Object key, Object value) {
if (((sharedFlags & SHARED_KEYS) == 0) && keys.length > keyCount + 1) {
if (index < keyCount) {
......@@ -401,6 +507,13 @@ public class Page {
totalCount++;
}
/**
* Insert a child into this node.
*
* @param index the index
* @param key the key
* @param childPage the child page
*/
public void insertNode(int index, Object key, Page childPage) {
Object[] newKeys = new Object[keyCount + 1];
......@@ -428,6 +541,11 @@ public class Page {
totalCount += childPage.getTotalCount();
}
/**
* Remove the key and value (or child) at the given index.
*
* @param index the index
*/
public void remove(int index) {
int keyIndex = index >= keyCount ? index - 1 : index;
if ((sharedFlags & SHARED_KEYS) == 0 && keys.length > keyCount - 4) {
......@@ -478,41 +596,6 @@ public class Page {
}
}
// public void remove(int index) {
// Object[] newKeys = new Object[keyCount - 1];
// int keyIndex = index >= keyCount ? index - 1 : index;
// DataUtils.copyExcept(keys, newKeys, keyCount, keyIndex);
// keys = newKeys;
// sharedFlags &= ~SHARED_KEYS;
// if (values != null) {
// Object[] newValues = new Object[keyCount - 1];
// DataUtils.copyExcept(values, newValues, keyCount, index);
// values = newValues;
// sharedFlags &= ~SHARED_VALUES;
// totalCount--;
// }
// keyCount--;
// if (children != null) {
// long countOffset = counts[index];
//
// long[] newChildren = new long[children.length - 1];
// DataUtils.copyExcept(children, newChildren, children.length, index);
// children = newChildren;
//
// Page[] newChildrenPages = new Page[childrenPages.length - 1];
// DataUtils.copyExcept(childrenPages, newChildrenPages, childrenPages.length, index);
// childrenPages = newChildrenPages;
//
// long[] newCounts = new long[counts.length - 1];
// DataUtils.copyExcept(counts, newCounts,
// counts.length, index);
// counts = newCounts;
//
// sharedFlags &= ~(SHARED_CHILDREN | SHARED_COUNTS);
// totalCount -= countOffset;
// }
// }
private void read(ByteBuffer buff, int chunkId, int offset, int maxLength) {
int start = buff.position();
int pageLength = buff.getInt();
......@@ -701,10 +784,6 @@ public class Page {
return count;
}
public long getCounts(int index) {
return counts[index];
}
long getVersion() {
return version;
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论