/*
*
* Copyright 2011 Future Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.krakenapps.btree;
import java.io.File;
import java.io.IOException;
import org.krakenapps.btree.types.IntegerValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/*
* Known issues:
* * linear bruteforce search for a key in page.
* * ...
*/
public class BtreeImpl implements CursorCallback, Btree {
private final Logger logger = LoggerFactory.getLogger(BtreeImpl.class.getName());
private PageManager pageManager;
private PageFile pf;
public static Btree create(File file, Schema schema) throws IOException {
PageFile pf = PageFile.create(file, schema);
pf.close();
return new BtreeImpl(file);
}
public BtreeImpl(File file) throws IOException {
this.pf = new PageFile(file);
this.pageManager = new PageManager(pf.getSchema(), pf);
}
@Override
public void setRowValueFactory(RowValueFactory valueFactory) {
pf.setRowValueFactory(valueFactory);
}
@Override
public PageManager getPageManager() {
return pageManager;
}
@Override
public PageFile getPageFile() {
return pf;
}
@Override
public Cursor openCursor(int order) throws IOException {
Page p = pageManager.get(pf.getRootPage());
if (order == Cursor.ASC) {
// find leftmost page in ascending
while (!p.getFlag(PageType.LEAF)) {
int left = ((IntegerValue) p.getValue(0)).getValue();
p = pageManager.get(left);
}
CursorContext context = new CursorContext(this, p, 0, true, null);
context.addListener(this);
return new Cursor(context);
} else if (order == Cursor.DESC) {
// find rightmost page in descending
while (!p.getFlag(PageType.LEAF)) {
if (p.getFlag(PageType.INDEX) && p.getRightChildPage() != 0)
p = pageManager.get(p.getRightChildPage());
}
CursorContext context = new CursorContext(this, p, p.getRecordCount() - 1, false, null);
context.addListener(this);
return new Cursor(context);
}
throw new IllegalArgumentException("invalid sort order: " + order);
}
@Override
public Cursor openCursor(RowKey searchKey, int order) throws IOException {
return get(pf.getRootPage(), searchKey, order == Cursor.ASC);
}
@Override
public void insert(RowKey key, RowEntry value) throws IOException {
Page root = pageManager.get(pf.getRootPage());
Page newPage = insert(root, key, value);
if (newPage != null) {
// create new root
Page newRoot = pageManager.allocate(PageType.INDEX);
RowKey smallestKey = newPage.getKey(0);
newRoot.insert(smallestKey, new IntegerValue(root.getNumber()));
newRoot.setRightChildPage(newPage.getNumber());
int newRootPageNumber = newRoot.getNumber();
root.setUpperPage(newRootPageNumber);
newPage.setUpperPage(newRootPageNumber);
// change root pointer
pageManager.setRootPage(newRootPageNumber);
}
}
private Page insert(Page page, RowKey key, RowEntry value) throws IOException {
if (page.getFlag(PageType.LEAF))
return insertLeaf(page, key, value);
else if (page.getFlag(PageType.INDEX))
return insertIndex(page, key, value);
else
throw new IllegalArgumentException("unsupported page type: " + page.getFlag());
}
@Override
public void delete(RowKey key) throws IOException {
if (key == null)
throw new IllegalArgumentException("key must not be null");
Cursor cursor = openCursor(key, Cursor.ASC);
if (cursor == null)
return;
try {
cursor.delete();
} finally {
if (cursor != null)
cursor.close();
}
}
private Page split(int pageType, Page page, RowKey key, RowEntry value) throws IOException {
Page newPage = pageManager.allocate(pageType);
redistribute(page, newPage);
RowKey smallestKey = newPage.getKey(0);
if (key.compareTo(smallestKey) <= 0)
page.insert(key, value);
else
newPage.insert(key, value);
// set left/right link
if (page.getRightPage() != 0) {
Page right = pageManager.get(page.getRightPage());
newPage.setRightPage(right.getNumber());
right.setLeftPage(newPage.getNumber());
}
page.setRightPage(newPage.getNumber());
newPage.setLeftPage(page.getNumber());
newPage.setUpperPage(page.getUpperPage());
if (logger.isDebugEnabled())
traceAfterSplit(page, newPage);
return newPage;
}
private void traceAfterSplit(Page page, Page newPage) {
String buffer = "";
buffer += "OLD=" + page.getNumber() + "\n";
for (int k = 0; k < page.getRecordCount(); k++)
buffer += page.getKey(k) + "," + page.getValue(k) + "\n";
buffer += "NEW=" + newPage.getNumber() + "\n";
for (int k = 0; k < newPage.getRecordCount(); k++)
buffer += newPage.getKey(k) + "," + newPage.getValue(k) + "\n";
logger.debug(buffer);
}
private Page insertLeaf(Page page, RowKey key, RowEntry value) throws IOException {
boolean added = page.insert(key, value);
if (!added)
return split(PageType.LEAF, page, key, value);
return null;
}
private Page insertIndex(Page page, RowKey key, RowEntry value) throws IOException {
int nextPageNumber = -1;
// traverse and find page
for (int i = 0; i < page.getRecordCount(); i++) {
RowKey k = page.getKey(i);
if (key.compareTo(k) <= 0) {
nextPageNumber = ((IntegerValue) page.getValue(i)).getValue();
break;
}
}
int currentRightChildPageNumber = page.getRightChildPage();
if (nextPageNumber == -1)
nextPageNumber = currentRightChildPageNumber;
Page nextPage = pageManager.get(nextPageNumber);
if (nextPage == null)
throw new IOException("page not found: " + nextPageNumber);
// try insert to leaf page
Page newPage = insert(nextPage, key, value);
if (newPage != null) {
// replace right child if it was rightmost page
if (nextPageNumber == currentRightChildPageNumber) {
page.setRightChildPage(newPage.getNumber());
newPage.setUpperPage(page.getNumber());
currentRightChildPageNumber = newPage.getNumber();
}
// prepare new index item
RowKey smallestKey = newPage.getKey(0);
RowEntry pageLink = new IntegerValue(newPage.getNumber());
// find slot to append or insert
int slot = page.findSlotBefore(smallestKey) + 1;
RowKey oldKey = page.getKey(slot);
IntegerValue oldLeft = (IntegerValue) page.getValue(slot);
// check if duplicated key exists
if (oldKey != null && oldKey.equals(smallestKey)) {
int minimum = Math.min(newPage.getNumber(), oldLeft.getValue());
// replace page link to minimum (logic should be enhanced later)
page.delete(slot);
page.insert(oldKey, new IntegerValue(minimum));
} else {
// update old key
if (slot < page.getRecordCount()) {
// replace page link (logic should be enhanced later)
page.delete(slot);
page.insert(oldKey, pageLink);
pageLink = oldLeft;
}
// try insert new key to index page
boolean added = page.insert(smallestKey, pageLink);
if (!added) {
Page splitPage = split(PageType.INDEX, page, smallestKey, pageLink);
// update right child
int newRightChildPageNumber = ((IntegerValue) splitPage.getValue(0)).getValue();
page.setRightChildPage(newRightChildPageNumber);
splitPage.setRightChildPage(currentRightChildPageNumber);
// update up link
Page currentRightChildPage = pageManager.get(currentRightChildPageNumber);
currentRightChildPage.setUpperPage(splitPage.getNumber());
return splitPage;
}
}
}
return null;
}
private void redistribute(Page oldPage, Page newPage) throws IOException {
int total = oldPage.getSchema().getPageSize() - Page.PAGE_HEADER_SIZE;
int half = total / 2;
int count = oldPage.getRecordCount();
long sum = 0;
// calculate half position
int p = 0;
for (int i = 0; i < count; i++) {
sum += Page.SLOT_SIZE + Page.RECORD_HEADER_SIZE;
sum += oldPage.getKey(i).getBytes().length;
sum += oldPage.getValue(i).getBytes().length;
if (sum > half) {
p = i;
break;
}
}
// copy to new page
for (int i = p; i < count; i++) {
oldPage.getKey(i);
RowKey key = oldPage.getKey(i);
RowEntry value = oldPage.getValue(i);
newPage.insert(key, value);
// update up link
if (newPage.getFlag() == PageType.INDEX) {
Page child = pageManager.get(((IntegerValue) value).getValue());
child.setUpperPage(newPage.getNumber());
}
}
// delete (descending delete is efficient)
for (int j = count - 1; j >= p; j--)
oldPage.delete(j);
}
@Override
public RowEntry get(RowKey searchKey) throws IOException {
Cursor cursor = null;
try {
cursor = get(pf.getRootPage(), searchKey, true);
if (cursor == null)
return null;
return cursor.getValue();
} finally {
if (cursor != null)
cursor.close();
}
}
private Cursor get(int pageNumber, RowKey searchKey, boolean asc) throws IOException {
Page page = pageManager.get(pageNumber);
if (page.getFlag(PageType.LEAF)) {
return findFirstKey(page, searchKey, asc);
} else if (page.getFlag(PageType.INDEX)) {
for (int i = 0; i < page.getRecordCount(); i++) {
RowKey k = page.getKey(i);
if (searchKey.compareTo(k) <= 0) {
int nextPageNumber = ((IntegerValue) page.getValue(i)).getValue();
return get(nextPageNumber, searchKey, asc);
}
}
return get(page.getRightChildPage(), searchKey, asc);
}
return null;
}
private Cursor findFirstKey(Page page, RowKey searchKey, boolean asc) throws IOException {
int recordCount = page.getRecordCount();
Page lastPage = null;
int lastSlot = -1;
while (true) {
for (int i = 0; i < recordCount; i++) {
RowKey key = page.getKey(i);
int ret = key.compareTo(searchKey);
if (ret == 0) {
if (asc) {
CursorContext context = new CursorContext(this, page, i, asc, searchKey);
context.addListener(this);
return new Cursor(context);
} else {
// do not return immediately, find all duplicates in
// descending mode
lastPage = page;
lastSlot = i;
}
} else if (ret > 0) {
// if key is larger than search key, you should stop here
if (!asc && lastPage != null && lastSlot != -1) {
CursorContext context = new CursorContext(this, lastPage, lastSlot, asc, searchKey);
context.addListener(this);
return new Cursor(context);
} else
return null;
}
}
if (page.getRightPage() == 0)
return null;
page = pageManager.get(page.getRightPage());
}
}
@Override
public void sync() throws IOException {
pageManager.sync();
}
@Override
public void close() throws IOException {
sync();
pf.close();
}
//
// Handle cursor events
//
@Override
public void onDelete(CursorContext context) throws IOException {
Page page = context.getPage();
int slot = context.getSlot();
RowKey currentKey = page.getKey(slot);
// delete slot
page.delete(slot);
// begin index update
int upper = page.getUpperPage();
if (upper == 0)
return;
Page upperPage = pageManager.get(upper);
Page currentPage = page;
// record count after deletion
int count = page.getRecordCount();
// if slot was 0, update smallest key recursively
if (slot == 0 && count > 0) {
while (true) {
// find current smallest key at index page
int upperSlot = upperPage.findSlot(currentKey);
if (upperSlot < 0)
throw new IllegalStateException("bug check. cannot find index slot for " + currentKey);
RowKey newSmallestKey = currentPage.getKey(0);
// update page link (can be enhanced later)
upperPage.delete(upperSlot);
upperPage.insert(newSmallestKey, new IntegerValue(currentPage.getNumber()));
// recursive update if upper slot is also smallest
if (upperSlot != 0 || upperPage.getUpperPage() == 0)
break;
currentKey = upperPage.getKey(0);
currentPage = upperPage;
upperPage = pageManager.get(upperPage.getUpperPage());
}
}
// check empty page
if (count > 0)
return;
cascadeDelete(page, currentKey);
}
private void cascadeDelete(Page page, RowKey currentKey) throws IOException {
Page currentPage = page;
while (true) {
// break condition
if (currentPage.getRecordCount() != 0)
break;
int rightPage = currentPage.getRightPage();
int rightChildPage = currentPage.getRightChildPage();
int upPage = currentPage.getUpperPage();
deletePage(currentPage);
// change root and break
if (upPage == 0) {
pageManager.setRootPage(rightChildPage);
break;
}
Page upperPage = pageManager.get(upPage);
int upperSlot = upperPage.findSlot(currentKey);
if (upperSlot < 0) {
// update page link (e.g. leftmost page deletion)
upperSlot = upperPage.findSlotBefore(currentKey) + 1;
RowKey firstKey = upperPage.getKey(upperSlot);
// you should use right page. because right page may not be
// found from index node. (especially in case of many
// duplicated keys)
Page right = null;
if (rightPage != 0) {
right = pageManager.get(rightPage);
RowKey smallest = right.getKey(0);
// if first key of upper keys is larger than smallest of
// right page, replace page link
if (smallest.compareTo(firstKey) <= 0) {
// replace it (can be enhanced later)
upperPage.delete(upperSlot);
upperPage.insert(smallest, new IntegerValue(rightPage));
}
}
break;
}
upperPage.delete(upperSlot);
// update right-child if current right child is deleted
if (upperPage.getRightChildPage() == currentPage.getNumber()) {
RowEntry value = upperPage.getValue(upperPage.getRecordCount() - 1);
int newRightmostChild = ((IntegerValue) value).getValue();
upperPage.setRightChildPage(newRightmostChild);
}
// up!
currentPage = upperPage;
}
}
private void deletePage(Page page) throws IOException {
// unlink between sibling pages
Page leftPage = null;
if (page.getLeftPage() != 0) {
leftPage = pageManager.get(page.getLeftPage());
leftPage.setRightPage(page.getRightPage());
}
Page rightPage = null;
if (page.getRightPage() != 0) {
rightPage = pageManager.get(page.getRightPage());
rightPage.setLeftPage(page.getLeftPage());
}
// purge page (add to free page list)
pageManager.purge(page.getNumber());
}
}