提交 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;
}
}
......@@ -8,11 +8,11 @@ package org.h2.dev.store.btree;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* A stored map.
......@@ -22,7 +22,14 @@ import java.util.TreeMap;
*/
public class MVMap<K, V> extends AbstractMap<K, V> {
/**
* The store.
*/
protected final MVStore store;
/**
* The root page (may not be null).
*/
protected Page root;
private final int id;
......@@ -30,11 +37,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
private final DataType keyType;
private final DataType valueType;
private final long createVersion;
/**
* The map of old roots. The key is the new version, the value is the root
* before this version.
*/
private final TreeMap<Long, Page> oldRoots = new TreeMap<Long, Page>();
private ArrayList<Page> oldRoots = new ArrayList<Page>();
private boolean closed;
private boolean readOnly;
......@@ -47,44 +50,35 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
this.keyType = keyType;
this.valueType = valueType;
this.createVersion = createVersion;
this.root = Page.createEmpty(this, createVersion);
}
/**
* Store a key-value pair.
* Add or replace a key-value pair.
*
* @param key the key
* @param value the value
* @param key the key (may not be null)
* @param value the value (may not be null)
* @return the old value if the key existed, or null otherwise
*/
@SuppressWarnings("unchecked")
public V put(K key, V value) {
checkWrite();
long writeVersion = store.getCurrentVersion();
Page p = root;
Object result;
if (p == null) {
Object[] keys = { key };
Object[] values = { value };
Page p = root.copyOnWrite(writeVersion);
if (p.getKeyCount() > store.getMaxPageSize()) {
int at = p.getKeyCount() / 2;
long totalCount = p.getTotalCount();
Object k = p.getKey(at);
Page split = p.split(at);
Object[] keys = { k };
long[] children = { p.getPos(), split.getPos() };
Page[] childrenPages = { p, split };
long[] counts = { p.getTotalCount(), split.getTotalCount() };
p = Page.create(this, writeVersion, 1,
keys, values, null, null, null, 1, 0);
result = null;
} else {
p = p.copyOnWrite(writeVersion);
if (p.getKeyCount() > store.getMaxPageSize()) {
int at = p.getKeyCount() / 2;
long totalCount = p.getTotalCount();
Object k = p.getKey(at);
Page split = p.split(at);
Object[] keys = { k };
long[] children = { p.getPos(), split.getPos() };
Page[] childrenPages = { p, split };
long[] counts = { p.getTotalCount(), split.getTotalCount() };
p = Page.create(this, writeVersion, 1,
keys, null, children, childrenPages, counts, totalCount, 0);
// now p is a node; insert continues
}
result = put(p, writeVersion, key, value);
keys, null, children, childrenPages, counts, totalCount, 0);
// now p is a node; insert continues
}
Object result = put(p, writeVersion, key, value);
setRoot(p);
return (V) result;
}
......@@ -92,11 +86,11 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
/**
* Add or update a key-value pair.
*
* @param map the map
* @param p the page (may be null)
* @param p the page
* @param writeVersion the write version
* @param key the key
* @param key the key (may not be null)
* @param value the value (may not be null)
* @return the old value, or null
*/
protected Object put(Page p, long writeVersion, Object key, Object value) {
if (p.isLeaf()) {
......@@ -140,9 +134,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
@SuppressWarnings("unchecked")
public V get(Object key) {
checkOpen();
if (root == null) {
return null;
}
return (V) binarySearch(root, key);
}
......@@ -152,9 +143,10 @@ public class MVMap<K, V> extends AbstractMap<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) {
while (p != null) {
while (true) {
if (p.isLeaf()) {
int x = key == null ? 0 : p.binarySearch(key);
if (x < 0) {
......@@ -177,7 +169,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
cursor.push(c);
p = p.getChildPage(x);
}
return null;
}
/**
......@@ -216,6 +207,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
/**
* Get the value for the given key, or null if not found.
*
* @param p the page
* @param key the key
* @return the value or null
*/
......@@ -247,9 +239,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
* @return the value, or null if not found
*/
protected Page getPage(K key) {
if (root == null) {
return null;
}
return binarySearchPage(root, key);
}
......@@ -282,10 +271,8 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
*/
public void clear() {
checkWrite();
if (root != null) {
root.removeAllRecursive();
setRoot(null);
}
root.removeAllRecursive();
setRoot(Page.createEmpty(this, store.getCurrentVersion()));
}
/**
......@@ -293,17 +280,18 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
*/
public void removeMap() {
checkWrite();
if (root != null) {
root.removeAllRecursive();
}
root.removeAllRecursive();
store.removeMap(name);
close();
}
/**
* Close the map, making it read only and release the memory.
*/
public void close() {
closed = true;
readOnly = true;
oldRoots.clear();
clearOldVersions();
root = null;
}
......@@ -314,22 +302,15 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
/**
* Remove a key-value pair, if the key exists.
*
* @param key the key
* @param key the key (may not be null)
* @return the old value if the key existed, or null otherwise
*/
public V remove(Object key) {
checkWrite();
Page p = root;
if (p == null) {
return null;
}
long writeVersion = store.getCurrentVersion();
p = p.copyOnWrite(writeVersion);
Page p = root.copyOnWrite(writeVersion);
@SuppressWarnings("unchecked")
V result = (V) remove(p, writeVersion, key);
if (p.getTotalCount() == 0) {
p = null;
}
setRoot(p);
return result;
}
......@@ -340,6 +321,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
* @param p the page (may not be null)
* @param writeVersion the write version
* @param key the key
* @return the old value, or null if the key did not exist
*/
protected Object remove(Page p, long writeVersion, Object key) {
int index = p.binarySearch(key);
......@@ -384,17 +366,29 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
protected void setRoot(Page newRoot) {
if (root != newRoot) {
long v = store.getCurrentVersion();
if (!oldRoots.containsKey(v)) {
oldRoots.put(v, root);
if (root.getVersion() != newRoot.getVersion()) {
ArrayList<Page> list = oldRoots;
if (list.size() > 0) {
Page last = list.get(list.size() - 1);
if (last.getVersion() != root.getVersion()) {
list.add(root);
}
} else {
list.add(root);
}
store.markChanged(this);
}
root = newRoot;
store.markChanged(this);
}
}
/**
* Check whether this map has any unsaved changes.
*
* @return true if there are unsaved changes.
*/
public boolean hasUnsavedChanges() {
return oldRoots.size() > 0;
return !oldRoots.isEmpty();
}
/**
......@@ -442,7 +436,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
* @param rootPos the position, 0 for empty
*/
void setRootPos(long rootPos) {
root = rootPos == 0 ? null : readPage(rootPos);
root = rootPos == 0 ? Page.createEmpty(this, 0) : readPage(rootPos);
}
/**
......@@ -460,6 +454,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
/**
* Iterate over all keys in changed pages.
* This does not include deleted deleted pages.
*
* @param minVersion the minimum version
* @return the iterator
......@@ -530,30 +525,38 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
return id;
}
/**
* Rollback to the given version.
*
* @param version the version
*/
void rollbackTo(long version) {
checkWrite();
if (version < createVersion) {
removeMap();
} else {
// iterating in ascending order, and pick the last version -
} else if (root.getVersion() != version) {
// iterating in descending order -
// this is not terribly efficient if there are many versions
// but it is a simple algorithm
Long newestOldVersion = null;
for (Iterator<Long> it = oldRoots.keySet().iterator(); it.hasNext();) {
Long x = it.next();
if (x > version) {
if (newestOldVersion == null) {
newestOldVersion = x;
root = oldRoots.get(x);
}
it.remove();
ArrayList<Page> list = oldRoots;
while (list.size() > 0) {
int i = list.size() - 1;
Page p = list.get(i);
root = p;
list.remove(i);
if (p.getVersion() <= version) {
break;
}
}
}
}
void revertTemp() {
oldRoots.clear();
/**
* Forget all old versions.
*/
void clearOldVersions() {
// create a new instance
// because another thread might iterate over it
oldRoots = new ArrayList<Page>();
}
public void setReadOnly(boolean readOnly) {
......@@ -564,12 +567,22 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
return readOnly;
}
/**
* Check whether the map is open.
*
* @throws IllegalStateException if the map is closed
*/
protected void checkOpen() {
if (closed) {
throw new IllegalStateException("This map is closed");
}
}
/**
* Check whether writing is allowed.
*
* @throws IllegalStateException if the map is read-only
*/
protected void checkWrite() {
if (readOnly) {
checkOpen();
......@@ -603,28 +616,58 @@ public class MVMap<K, V> extends AbstractMap<K, V> {
}
public long getSize() {
return root == null ? 0 : root.getTotalCount();
return root.getTotalCount();
}
long getCreateVersion() {
return createVersion;
}
/**
* Remove the given page (make the space available).
*
* @param p the page
*/
protected void removePage(Page p) {
store.removePage(p.getPos());
}
/**
* Open an old version for the given map.
*
* @param version the version
* @return the map
*/
public MVMap<K, V> openVersion(long version) {
if (readOnly) {
throw new IllegalArgumentException("This map is read-only - need to call the method on the writable map");
}
if (version < createVersion) {
throw new IllegalArgumentException("Unknown version");
}
if (!oldRoots.containsKey(version)) {
Page newest = null;
// need to copy because it can change
Page r = root;
if (r.getVersion() == version) {
newest = r;
} else {
// TODO could do a binary search
ArrayList<Page> list = oldRoots;
for (int i = 0; i < list.size(); i++) {
Page p = list.get(i);
if (p.getVersion() <= version) {
newest = p;
} else {
break;
}
}
}
if (newest == null) {
return store.openMapVersion(version, name);
}
Page root = oldRoots.get(version);
MVMap<K, V> m = new MVMap<K, V>(store, id, name, keyType, valueType, createVersion);
m.readOnly = true;
m.root = root;
m.root = newest;
return m;
}
......
......@@ -7,7 +7,6 @@
package org.h2.dev.store.btree;
import java.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
......@@ -17,7 +16,6 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import org.h2.compress.CompressLZF;
import org.h2.compress.Compressor;
import org.h2.dev.store.FilePathCache;
......@@ -29,27 +27,23 @@ import org.h2.util.StringUtils;
/*
File format:
header: 4096 bytes
header: 4096 bytes
header: (blockSize) bytes
header: (blockSize) bytes
[ chunk ] *
(there are two headers for security)
header:
# H3 store #
blockSize=4096
H:3,blockSize=4096,...
TODO:
- test with very small chunks, possibly speed up very small transactions
- check what happens on concurrent reads and 1 write; multiple writes
- support all objects (using serialization)
- concurrent iterator (when to call commit; read on first hasNext())
- concurrent iterator (when to increment version; read on first hasNext())
- how to iterate (just) over deleted pages / entries
- compact: use total max length instead of page count (liveCount)
- support background writes (store old version)
- avoid using java.util.Properties (it allocates quite a lot of memory)
- support large binaries
- support database version / schema version
- support database version / app version
- limited support for writing to old versions (branches)
- atomic test-and-set (when supporting concurrent writes)
- file header could be a regular chunk, end of file the second
- possibly split chunk data into immutable and mutable
- support stores that span multiple files (chunks stored in other files)
- triggers
......@@ -57,15 +51,27 @@ TODO:
- merge pages if small
- r-tree: add missing features (NN search for example)
- compression: maybe hash table reset speeds up compression
- use file level locking to ensure there is only one writer
- pluggable cache (specially for in-memory file systems)
- store the factory class in the file header
- support custom fields in the header
- auto-server: store port in the header
- recovery: keep a list of old chunks
- recovery: ensure data is not overwritten for 1 minute
- pluggable caching (specially for in-memory file systems)
- file locking
*/
/**
* A persistent storage for tree maps.
* A persistent storage for maps.
*/
public class MVStore {
public static final boolean ASSERT = false;
/**
* Whether assertions are enabled.
*/
public static final boolean ASSERT = true;
private static final StringType STRING_TYPE = new StringType();
......@@ -138,6 +144,13 @@ public class MVStore {
return s;
}
/**
* Open an old, stored version of a map.
*
* @param version the version
* @param name the map name
* @return the read-only map
*/
@SuppressWarnings("unchecked")
<T extends MVMap<?, ?>> T openMapVersion(long version, String name) {
// TODO reduce copy & pasted source code
......@@ -198,7 +211,8 @@ public class MVStore {
return (T) m;
}
private MVMap<?, ?> buildMap(String mapType, int id, String name, String keyType, String valueType, long createVersion) {
private MVMap<?, ?> buildMap(String mapType, int id, String name,
String keyType, String valueType, long createVersion) {
DataType k = buildDataType(keyType);
DataType v = buildDataType(valueType);
if (mapType.equals("")) {
......@@ -263,6 +277,11 @@ public class MVStore {
return m;
}
/**
* Remove a map.
*
* @param name the map name
*/
void removeMap(String name) {
MVMap<?, ?> m = maps.remove(name);
mapsChanged.remove(m);
......@@ -344,14 +363,16 @@ public class MVStore {
private void writeHeader() {
try {
ByteBuffer header = ByteBuffer.wrap((
"# H2 1.5\n" +
"versionRead:1\n" +
"versionWrite:1\n" +
"blockSize:" + blockSize + "\n" +
"rootChunk:" + rootChunkStart + "\n" +
"lastMapId:" + lastMapId + "\n" +
"version:" + currentVersion + "\n").getBytes("UTF-8"));
ByteBuffer header = ByteBuffer.allocate(blockSize);
String h = "H:3," +
"versionRead:1," +
"versionWrite:1," +
"blockSize:" + blockSize + "," +
"rootChunk:" + rootChunkStart + "," +
"lastMapId:" + lastMapId + "," +
"version:" + currentVersion;
header.put(h.getBytes("UTF-8"));
header.rewind();
writeCount++;
file.position(0);
file.write(header);
......@@ -364,16 +385,15 @@ public class MVStore {
private void readHeader() {
try {
byte[] header = new byte[blockSize];
byte[] headers = new byte[blockSize * 2];
readCount++;
file.position(0);
// TODO read fully; read both headers
file.read(ByteBuffer.wrap(header));
Properties prop = new Properties();
prop.load(new StringReader(new String(header, "UTF-8")));
rootChunkStart = Long.parseLong(prop.get("rootChunk").toString());
currentVersion = Long.parseLong(prop.get("version").toString());
lastMapId = Integer.parseInt(prop.get("lastMapId").toString());
file.read(ByteBuffer.wrap(headers));
String s = new String(headers, 0, blockSize, "UTF-8").trim();
HashMap<String, String> map = DataUtils.parseMap(s);
rootChunkStart = Long.parseLong(map.get("rootChunk"));
currentVersion = Long.parseLong(map.get("version"));
lastMapId = Integer.parseInt(map.get("lastMapId"));
} catch (Exception e) {
throw convert(e);
}
......@@ -389,6 +409,7 @@ public class MVStore {
public void close() {
if (file != null) {
try {
shrinkFileIfPossible(0);
log("file close");
file.close();
for (MVMap<?, ?> m : New.arrayList(maps.values())) {
......@@ -407,6 +428,12 @@ public class MVStore {
}
}
/**
* Get the chunk for the given position.
*
* @param pos the position
* @return the chunk
*/
Chunk getChunk(long pos) {
return chunks.get(DataUtils.getPageChunkId(pos));
}
......@@ -422,11 +449,11 @@ public class MVStore {
}
/**
* Commit the changes, incrementing the current version.
* Increment the current version.
*
* @return the version before the commit
* @return the old version
*/
public long commit() {
public long incrementVersion() {
return currentVersion++;
}
......@@ -478,12 +505,12 @@ public class MVStore {
continue;
}
Page p = m.getRoot();
if (p != null) {
if (p.getTotalCount() == 0) {
meta.put("root." + m.getId(), "0");
} else {
maxLength += p.getMaxLengthTempRecursive();
count += p.countTempRecursive();
meta.put("root." + m.getId(), String.valueOf(Long.MAX_VALUE));
} else {
meta.put("root." + m.getId(), "0");
}
}
maxLength += meta.getRoot().getMaxLengthTempRecursive();
......@@ -500,7 +527,7 @@ public class MVStore {
continue;
}
Page p = m.getRoot();
if (p != null) {
if (p.getTotalCount() > 0) {
long root = p.writeTempRecursive(buff, chunkId);
meta.put("root." + m.getId(), "" + root);
}
......@@ -544,10 +571,10 @@ public class MVStore {
c.start = filePos;
c.length = length;
long version = commit();
long version = incrementVersion();
// write the new version (after the commit)
writeHeader();
shrinkFileIfPossible();
shrinkFileIfPossible(1);
return version;
}
......@@ -562,12 +589,27 @@ public class MVStore {
freedChunks.clear();
}
private void shrinkFileIfPossible() {
/**
* Shrink the file if possible, and if at least a given percentage can be
* saved.
*
* @param minPercent the minimum percentage to save
*/
private void shrinkFileIfPossible(int minPercent) {
long used = getFileLengthUsed();
try {
if (used < file.size()) {
file.truncate(used);
long size = file.size();
if (used >= size) {
return;
}
if (minPercent > 0 && size - used < blockSize) {
return;
}
int savedPercent = (int) (100 - (used * 100 / size));
if (savedPercent < minPercent) {
return;
}
file.truncate(used);
} catch (Exception e) {
throw convert(e);
}
......@@ -911,7 +953,7 @@ public class MVStore {
this.retainChunk = retainChunk;
}
public boolean isKnownVersion(long version) {
private boolean isKnownVersion(long version) {
if (version > currentVersion || version < 0) {
return false;
}
......@@ -946,6 +988,7 @@ public class MVStore {
* Revert to the given version. All later changes (stored or not) are
* forgotten. All maps that were created later are closed. A rollback to
* a version before the last stored version is immediately persisted.
* Before this method returns, the current version is incremented.
*
* @param version the version to revert to
*/
......@@ -1001,7 +1044,7 @@ public class MVStore {
private void revertTemp() {
freedChunks.clear();
for (MVMap<?, ?> m : mapsChanged.values()) {
m.revertTemp();
m.clearOldVersions();
}
mapsChanged.clear();
}
......
......@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论