Commit 2b4d8c1f authored by Donald H. (Donnie) Pinkston, III's avatar Donald H. (Donnie) Pinkston, III
Browse files

Files for B+ tree implementation / Lab 6

Design document and logistics document are in doc directory.

The pom.xml file is updated to include HW6 tests.

The B+ tree implementation is provided under the storage/btreefile
package.
parent 726cbe18
Showing with 5735 additions and 1 deletion
+5735 -1
CS122 Assignment 6 - B+ Tree Indexes - Design Document
======================================================
A: Analysis of Implementation
------------------------------
Given NanoDB's B+ tree implementation, consider a simple schema where an
index is built against a single integer column:
CREATE TABLE t (
-- An index is automatically built on the id column by NanoDB.
id INTEGER PRIMARY KEY,
value VARCHAR(20)
);
Answer the following questions.
A1. What is the total size of the index's search-key for the primary-key
index, in bytes? Break down this size into its individual components;
be as detailed as possible. (You don't need to go lower than the
byte-level in your answer, but you should show what each byte is a
part of.)
A2. What is the maximum number of search-keys that can be stored in leaf
nodes of NanoDB's B+ tree implementation? You should assume a page-
size of 8192 bytes.
A3. What is the maximum number of keys that can be stored in inner nodes
of this particular implementation? (Recall that every key must have
a page-pointer on either side of the key.)
A4. In this implementation, leaf nodes do not reference the previous
leaf, only the next leaf. When splitting a leaf into two leaves,
what is the maximum number of leaf nodes that must be read or written,
in order to properly manage the next-leaf pointers?
If leaves also contained a previous-leaf pointer, what would the
answer be instead?
Make sure to explain your answers.
A5. In this implementation, nodes do not store a page-pointer to their
parent node. This makes the update process somewhat complicated, as
we must save the sequence of page-numbers we traverse as we navigate
from root to leaf. If a node must be split, or if entries are to be
relocated from a node to its siblings, the node’s parent-node must
be retrieved, and the parent’s contents must be scanned to determine
the node’s sibling(s).
Consider an alternate B+ tree implementation in which every node
stores a page-pointer to the node’s parent. In the case of splitting
an inner node, what performance-related differences are there between
this alternate representation and the given implementation, where
nodes do not record their parents? Which one would you recommend?
Justify your answer.
A6. It should be obvious how indexes can be used to enforce primary keys,
but what role might they play with foreign keys? For example, given
this schema:
CREATE TABLE t1 (
id INTEGER PRIMARY KEY
);
CREATE TABLE t2 (
id INTEGER REFERENCES t1;
);
Why might we want to build an index on t2.id?
A7. Over time, a B+ tree's pages, its leaf pages in particular, may become
severely out of order, causing a significant number of seeks as the
leaves are traversed in sequence. Additionally, some of the blocks
within the B+ tree file may be empty.
An easy mechanism for regenerating a B+ tree file is to traverse the file's
tuples in sequential order (i.e. from the leftmost leaf page, through
all leaf pages in the file), adding each tuple to a new B+ tree file.
The new file may then replace the old file.
Imagine that this operation is performed on a B+ tree file containing
many records, where all records are the same size. On average, how full
will the leaf pages in the newly generated file be? You can state your
answer as a percentage, e.g. "0% full". Explain your answer.
A8. Consider a variation of the approach in A7: Instead of making one pass
through the initial B+ tree file, two passes are made. On the first pass,
the 1st, 3rd, 5th, 7th, ... tuples are added to the new file. Then, on the
second pass, the 2nd, 4th, 6th, 8th, ... tuples are added to the new file.
On average, how full will the leaf pages in the newly generated file be?
Explain your answer.
D: Extra Credit [OPTIONAL]
---------------------------
If you implemented any extra-credit tasks for this assignment, describe
them here. The description should be like this, with stuff in "<>" replaced.
(The value i starts at 1 and increments...)
D<i>: <one-line description>
<brief summary of what you did, including the specific classes that
we should look at for your implementation>
<brief summary of test-cases that demonstrate/exercise your extra work>
E: Feedback [OPTIONAL]
-----------------------
WE NEED YOUR FEEDBACK! Thoughtful and constructive input will help us to
improve future versions of the course. These questions are OPTIONAL, and
your answers will not affect your grade in any way (including if you hate
everything about the assignment and databases in general, or Donnie and/or
the TAs in particular). Feel free to answer as many or as few of them as
you wish.
E1. What parts of the assignment were most time-consuming? Why?
E2. Did you find any parts of the assignment particularly instructive?
Correspondingly, did any parts feel like unnecessary busy-work?
E3. Did you particularly enjoy any parts of the assignment? Were there
any parts that you particularly disliked?
E4. Were there any critical details that you wish had been provided with the
assignment, that we should consider including in subsequent versions of
the assignment?
E5. Do you have any other suggestions for how future versions of the
assignment can be improved?
CS122 Assignment 6 - B+ Tree Indexes
====================================
Please completely fill out this document so that we know who participated on
the assignment, any late extensions received, and how much time the assignment
took for your team. Thank you!
L1. List your team name and the people who worked on this assignment.
<team name>
<name>
<name>
...
L2. Specify the tag and commit-hash of the Git commit you are submitting for
your assignment. (You can list the hashes of all tags with the command
"git show-ref --tags".)
Tag: <tag>
Commit hash: <hash>
L3. Specify how many late tokens you are applying to this assignment, if any.
Similarly, if your team received an extension from Donnie then please
indicate how many days extension you received. You may leave this blank
if it is not relevant to this submission.
<tokens / extension>
L4. For each teammate, briefly describe what parts of the assignment each
teammate focused on, along with the total hours spent on the assignment.
......@@ -144,7 +144,7 @@
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
-->
<groups>framework,parser,hw1,hw2</groups>
<groups>framework,parser,hw1,hw2,hw5,hw6</groups>
</configuration>
</plugin>
......
......@@ -101,6 +101,9 @@ public class IndexedTableManager implements TableManager {
if ("heap".equals(storageType)) {
type = DBFileType.HEAP_TUPLE_FILE;
}
else if ("btree".equals(storageType)) {
type = DBFileType.BTREE_TUPLE_FILE;
}
else {
throw new IllegalArgumentException("Unrecognized table file " +
"type: " + storageType);
......
......@@ -16,6 +16,8 @@ import edu.caltech.nanodb.server.EventDispatcher;
import edu.caltech.nanodb.server.NanoDBServer;
import edu.caltech.nanodb.server.properties.PropertyRegistry;
import edu.caltech.nanodb.server.properties.ServerProperties;
import edu.caltech.nanodb.storage.btreefile.BTreeTupleFileManager;
import edu.caltech.nanodb.storage.heapfile.HeapTupleFileManager;
import edu.caltech.nanodb.transactions.TransactionManager;
......@@ -139,6 +141,9 @@ public class StorageManager {
tupleFileManagers.put(DBFileType.HEAP_TUPLE_FILE,
new HeapTupleFileManager(this));
tupleFileManagers.put(DBFileType.BTREE_TUPLE_FILE,
new BTreeTupleFileManager(this));
if (enableTransactions) {
logger.info("Initializing transaction manager.");
transactionManager = new TransactionManager(server);
......
package edu.caltech.nanodb.storage.btreefile;
import edu.caltech.nanodb.relations.Schema;
import edu.caltech.nanodb.storage.DBPage;
import edu.caltech.nanodb.storage.PageTuple;
/**
* <p>
* This class uses the <tt>PageTuple</tt> class functionality to access and
* manipulate keys stored in a B<sup>+</sup> tree tuple file. There is one
* extension, which is to allow the tuple to remember its index within the
* leaf page it is from; this makes it easy to move to the next tuple within
* the page very easily.
* </p>
* <p>
* B<sup>+</sup> tree tuple deletion is interesting, since all the tuples form
* a linear sequence in the page. When a given tuple T is deleted, the next
* tuple ends up at the same position that T was at. Therefore, to implement
* the tuple file's "get next tuple" functionality properly, we must keep
* track of whether the previous tuple was deleted or not; if it was deleted,
* we don't advance in the page.
* </p>
*/
public class BTreeFilePageTuple extends PageTuple {
private int tupleIndex;
/**
* Records if this tuple has been deleted or not. This affects navigation
* to the next tuple in the current page, since removal of the current
* tuple causes this object to point to the next tuple.
*/
private boolean deleted = false;
/**
* If this tuple is deleted, this field will be set to the page number of
* the next tuple. If there are no more tuples then this will be set to
* -1.
*/
private int nextTuplePageNo;
/**
* If this tuple is deleted, this field will be set to the index of the
* next tuple. If there are no more tuples then this will be set to -1.
*/
private int nextTupleIndex;
public BTreeFilePageTuple(Schema schema, DBPage dbPage, int pageOffset,
int tupleIndex) {
super(dbPage, pageOffset, schema);
if (tupleIndex < 0) {
throw new IllegalArgumentException(
"tupleIndex must be at least 0, got " + tupleIndex);
}
this.tupleIndex = tupleIndex;
}
public int getTupleIndex() {
return tupleIndex;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted() {
deleted = true;
}
public void setNextTuplePosition(int pageNo, int tupleIndex) {
if (!deleted)
throw new IllegalStateException("Tuple must be deleted");
nextTuplePageNo = pageNo;
nextTupleIndex = tupleIndex;
}
public int getNextTuplePageNo() {
if (!deleted)
throw new IllegalStateException("Tuple must be deleted");
return nextTuplePageNo;
}
public int getNextTupleIndex() {
if (!deleted)
throw new IllegalStateException("Tuple must be deleted");
return nextTupleIndex;
}
@Override
protected void insertTupleDataRange(int off, int len) {
throw new UnsupportedOperationException(
"B+ Tree index tuples don't support resizing.");
}
@Override
protected void deleteTupleDataRange(int off, int len) {
throw new UnsupportedOperationException(
"B+ Tree index tuples don't support resizing.");
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("BTPT[");
if (deleted) {
buf.append("deleted");
}
else {
boolean first = true;
for (int i = 0; i < getColumnCount(); i++) {
if (first)
first = false;
else
buf.append(',');
Object obj = getColumnValue(i);
if (obj == null)
buf.append("NULL");
else
buf.append(obj);
}
}
buf.append(']');
return buf.toString();
}
}
package edu.caltech.nanodb.storage.btreefile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import edu.caltech.nanodb.expressions.TupleComparator;
import edu.caltech.nanodb.expressions.TupleLiteral;
import edu.caltech.nanodb.relations.Tuple;
import edu.caltech.nanodb.storage.DBFile;
import edu.caltech.nanodb.storage.DBPage;
import edu.caltech.nanodb.storage.StorageManager;
import static edu.caltech.nanodb.storage.btreefile.BTreePageTypes.*;
/**
* This class provides some simple verification operations for B<sup>+</sup>
* tree tuple files.
*/
public class BTreeFileVerifier {
/** A logging object for reporting anything interesting that happens. */
private static Logger logger = LogManager.getLogger(BTreeFileVerifier.class);
/** This runtime exception class is used to abort the verification scan. */
private static class ScanAbortedException extends RuntimeException { }
/**
* This helper class is used to keep track of details of pages within the
* B<sup>+</sup> tree file.
*/
private static class PageInfo {
/** The page's number. */
public int pageNo;
/** The type of the tuple-file page. */
int pageType;
/** A flag indicating whether the page is accessible from the root. */
boolean accessibleFromRoot;
/**
* Records how many times the page has been referenced in the file's
* tree structure.
*/
int numTreeReferences;
/**
* Records how many times the page has been referenced in the file's
* leaf page list.
*/
int numLeafListReferences;
/**
* Records how many times the page has been referenced in the file's
* empty page list.
*/
int numEmptyListReferences;
PageInfo(int pageNo, int pageType) {
this.pageNo = pageNo;
this.pageType = pageType;
accessibleFromRoot = false;
numTreeReferences = 0;
numLeafListReferences = 0;
numEmptyListReferences = 0;
}
}
/** A reference to the storage manager since we use it so much. */
private StorageManager storageManager;
/** The B<sup>+</sup> tree tuple file to verify. */
private BTreeTupleFile tupleFile;
/**
* The actual {@code DBFile} object backing the tuple-file, since it is
* used so frequently in the verification.
*/
private DBFile dbFile;
/**
* This collection maps individual B<sup>+</sup> tree pages to the details
* that the verifier collects for each page.
*/
private HashMap<Integer, PageInfo> pages;
/** The collection of errors found during the verification process. */
private ArrayList<String> errors;
/**
* Initialize a verifier object to verify a specific B<sup>+</sup> tree
* tuple file.
*/
public BTreeFileVerifier(StorageManager storageManager,
BTreeTupleFile tupleFile) {
this.storageManager = storageManager;
this.tupleFile = tupleFile;
this.dbFile = tupleFile.getDBFile();
}
/**
* This method is the entry-point for the verification process. It
* performs multiple passes through the file structure to identify various
* issues. Any errors that are identified are returned in the collection
* of error messages.
*
* @return a list of error messages describing issues found with the
* tuple file's structure. If no errors are detected, this will
* be an empty list, not {@code null}.
*/
public List<String> verify() {
errors = new ArrayList<>();
try {
pass1ScanThruAllPages();
pass2TreeScanThruFile();
pass3ScanThruLeafList();
pass4ScanThruEmptyList();
pass5ExaminePageReachability();
/* These passes are specifically part of index validation, not
* just the file format validation, so we may need to move this
* somewhere else.
*
pass6VerifyTableTuplesInIndex();
pass7VerifyIndexTuplesInTable();
*/
}
catch (ScanAbortedException e) {
// Do nothing.
logger.warn("Tuple-file verification scan aborted.");
}
return errors;
}
/**
* This method implements pass 1 of the verification process: scanning
* through all pages in the tuple file, collecting basic details about
* each page.
*/
private void pass1ScanThruAllPages() {
logger.debug("Pass 1: Linear scan through pages to collect info");
pages = new HashMap<>();
for (int pageNo = 1; pageNo < dbFile.getNumPages(); pageNo++) {
DBPage dbPage = storageManager.loadDBPage(dbFile, pageNo);
int pageType = dbPage.readUnsignedByte(0);
PageInfo info = new PageInfo(pageNo, pageType);
pages.put(pageNo, info);
}
}
/**
* This method implements pass 2 of the verification process: performing
* a tree-scan through the entire tuple file, starting with the root page
* of the file.
*/
private void pass2TreeScanThruFile() {
logger.debug("Pass 2: Tree scan from root to verify nodes");
DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0);
int rootPageNo = HeaderPage.getRootPageNo(dbpHeader);
scanTree(rootPageNo, 0, null, null);
}
/**
* This helper function traverses the B<sup>+</sup> tree structure,
* verifying various invariants that should hold on the file structure.
*/
private void scanTree(int pageNo, int parentPageNo, Tuple parentLeftKey,
Tuple parentRightKey) {
PageInfo info = pages.get(pageNo);
info.accessibleFromRoot = true;
info.numTreeReferences++;
if (info.numTreeReferences > 10) {
errors.add(String.format("Pass 2: Stopping scan! I've visited " +
"page %d %d times; there may be a cycle in your B+ tree " +
"structure.", pageNo, info.numTreeReferences));
throw new ScanAbortedException();
}
logger.trace("Examining page " + pageNo);
DBPage dbPage = storageManager.loadDBPage(dbFile, pageNo);
switch (info.pageType) {
case BTREE_INNER_PAGE:
{
logger.trace("It's an inner page.");
InnerPage inner = new InnerPage(dbPage, tupleFile.getSchema());
ArrayList<Integer> refPages = new ArrayList<>();
int refInner = 0;
int refLeaf = 0;
int refOther = 0;
// Check the pages referenced from this page using the basic info
// collected in Pass 1.
for (int p = 0; p < inner.getNumPointers(); p++) {
int refPageNo = inner.getPointer(p);
refPages.add(refPageNo);
PageInfo refPageInfo = pages.get(refPageNo);
switch (refPageInfo.pageType) {
case BTREE_INNER_PAGE:
refInner++;
break;
case BTREE_LEAF_PAGE:
refLeaf++;
break;
default:
refOther++;
}
if (refInner != 0 && refLeaf != 0) {
errors.add(String.format("Pass 2: Inner page %d " +
"references both inner and leaf pages.", pageNo));
}
if (refOther != 0) {
errors.add(String.format("Pass 2: Inner page %d references " +
"pages that are neither inner pages nor leaf pages.", pageNo));
}
}
// Make sure the keys are in the proper order in the page.
int numKeys = inner.getNumKeys();
ArrayList<TupleLiteral> keys = new ArrayList<>(numKeys);
if (numKeys > 1) {
Tuple prevKey = inner.getKey(0);
keys.add(new TupleLiteral(prevKey));
if (parentLeftKey != null) {
int cmp = TupleComparator.compareTuples(parentLeftKey, prevKey);
// It is possible that the parent's left-key would be the
// same as the first key in this page.
if (cmp > 0) {
errors.add(String.format("Pass 2: Parent page %d's " +
"left key is greater than inner page %d's first key",
parentPageNo, pageNo));
}
}
for (int k = 1; k < numKeys; k++) {
Tuple key = inner.getKey(k);
keys.add(new TupleLiteral(key));
int cmp = TupleComparator.compareTuples(prevKey, key);
if (cmp == 0) {
errors.add(String.format("Pass 2: Inner page %d keys " +
"%d and %d are duplicates!", pageNo, k - 1, k));
}
else if (cmp > 0) {
errors.add(String.format("Pass 2: Inner page %d keys " +
"%d and %d are out of order!", pageNo, k - 1, k));
}
prevKey = key;
}
if (parentRightKey != null) {
int cmp = TupleComparator.compareTuples(prevKey, parentRightKey);
// The parent's right-key should be greater than the last
// key in this page.
if (cmp >= 0) {
errors.add(String.format("Pass 2: Parent page %d's " +
"right key is less than or equal to inner page " +
"%d's last key", parentPageNo, pageNo));
}
}
}
// Now that we are done with this page, check each child-page.
int p = 0;
Tuple prevKey = parentLeftKey;
for (int refPageNo : refPages) {
Tuple nextKey;
if (p < keys.size())
nextKey = keys.get(p);
else
nextKey = parentRightKey;
scanTree(refPageNo, pageNo, prevKey, nextKey);
prevKey = nextKey;
p++;
}
break;
}
case BTREE_LEAF_PAGE:
{
logger.trace("It's a leaf page.");
LeafPage leaf = new LeafPage(dbPage, tupleFile.getSchema());
// Make sure the keys are in the proper order in the page.
int numKeys = leaf.getNumTuples();
if (numKeys >= 1) {
Tuple prevKey = leaf.getTuple(0);
if (parentLeftKey != null) {
int cmp = TupleComparator.compareTuples(parentLeftKey, prevKey);
// It is possible that the parent's left-key would be the
// same as the first key in this page.
if (cmp > 0) {
errors.add(String.format("Pass 2: Parent page %d's " +
"left key is greater than inner page %d's first key",
parentPageNo, pageNo));
}
}
for (int k = 1; k < numKeys; k++) {
Tuple key = leaf.getTuple(k);
int cmp = TupleComparator.compareTuples(prevKey, key);
if (cmp == 0) {
errors.add(String.format("Pass 2: Leaf page %d keys " +
"%d and %d are duplicates!", pageNo, k - 1, k));
}
else if (cmp > 0) {
errors.add(String.format("Pass 2: Leaf page %d keys " +
"%d and %d are out of order!", pageNo, k - 1, k));
}
prevKey = key;
}
if (parentRightKey != null) {
int cmp = TupleComparator.compareTuples(prevKey, parentRightKey);
// The parent's right-key should be greater than the last
// key in this page.
if (cmp >= 0) {
errors.add(String.format("Pass 2: Parent page %d's " +
"right key is less than or equal to inner page " +
"%d's last key", parentPageNo, pageNo));
}
}
}
break;
}
default:
errors.add(String.format("Pass 2: Can reach page %d from root, " +
"but it's not a leaf or an inner page! Type = %d", pageNo,
info.pageType));
}
}
private void pass3ScanThruLeafList() {
logger.debug("Pass 3: Scan through leaf page list");
DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0);
int pageNo = HeaderPage.getRootPageNo(dbpHeader);
// Walk down the leftmost pointers in the inner pages until we reach
// the leftmost leaf page. Then we can walk across the leaves and
// check the constraints that should hold on leaves.
DBPage dbPage = storageManager.loadDBPage(dbFile, pageNo);
int pageType = dbPage.readUnsignedByte(0);
while (pageType != BTREE_LEAF_PAGE) {
if (pageType != BTREE_INNER_PAGE) {
errors.add(String.format("Pass 3: Page %d should be an inner " +
"page, but its type is %d instead", pageNo, pageType));
}
InnerPage innerPage = new InnerPage(dbPage, tupleFile.getSchema());
pageNo = innerPage.getPointer(0);
dbPage = storageManager.loadDBPage(dbFile, pageNo);
pageType = dbPage.readUnsignedByte(0);
}
// Now we should be at the leftmost leaf in the sequence of leaves.
Tuple prevKey = null;
int prevKeyPageNo = 0;
while (true) {
PageInfo info = pages.get(pageNo);
info.numLeafListReferences++;
if (info.numLeafListReferences > 10) {
errors.add(String.format("Pass 3: Stopping scan! I've visited " +
"leaf page %d %d times; there may be a cycle in your leaf list.",
pageNo, info.numLeafListReferences));
throw new ScanAbortedException();
}
LeafPage leafPage = new LeafPage(dbPage, tupleFile.getSchema());
for (int k = 0; k < leafPage.getNumTuples(); k++) {
Tuple key = leafPage.getTuple(k);
if (prevKey != null) {
int cmp = TupleComparator.compareTuples(prevKey, key);
if (cmp == 0) {
if (prevKeyPageNo == pageNo) {
errors.add(String.format("Pass 3: Leaf page %d " +
"keys %d and %d are duplicates!", pageNo,
k - 1, k));
}
else {
errors.add(String.format("Pass 3: Leaf page %d " +
"key 0 is a duplicate to previous leaf %d's " +
"last key!", pageNo, prevKeyPageNo));
}
}
else if (cmp > 0) {
if (prevKeyPageNo == pageNo) {
errors.add(String.format("Pass 3: Leaf page %d " +
"keys %d and %d are out of order!", pageNo,
k - 1, k));
}
else {
errors.add(String.format("Pass 3: Leaf page %d " +
"key 0 is out of order with previous leaf %d's " +
"last key!", pageNo, prevKeyPageNo));
}
}
}
prevKey = key;
prevKeyPageNo = pageNo;
}
// Go to the next leaf in the sequence.
pageNo = leafPage.getNextPageNo();
if (pageNo == 0)
break;
dbPage = storageManager.loadDBPage(dbFile, pageNo);
pageType = dbPage.readUnsignedByte(0);
if (pageType != BTREE_LEAF_PAGE) {
errors.add(String.format("Pass 3: Page %d should be a leaf " +
"page, but its type is %d instead", pageNo, pageType));
}
}
}
private void pass4ScanThruEmptyList() {
logger.debug("Pass 4: Scan through empty page list");
DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0);
int emptyPageNo = HeaderPage.getFirstEmptyPageNo(dbpHeader);
while (emptyPageNo != 0) {
PageInfo info = pages.get(emptyPageNo);
info.numEmptyListReferences++;
if (info.numEmptyListReferences > 10) {
errors.add(String.format("Pass 4: Stopping scan! I've visited " +
"empty page %d %d times; there may be a cycle in your empty list.",
emptyPageNo, info.numEmptyListReferences));
throw new ScanAbortedException();
}
if (info.pageType != BTREE_EMPTY_PAGE) {
errors.add(String.format("Page %d is in the empty-page list, " +
"but it isn't an empty page! Type = %d", emptyPageNo,
info.pageType));
}
DBPage dbPage = storageManager.loadDBPage(dbFile, emptyPageNo);
emptyPageNo = dbPage.readUnsignedShort(1);
}
}
private void pass5ExaminePageReachability() {
logger.debug("Pass 5: Find pages with reachability issues");
for (int pageNo = 1; pageNo < pages.size(); pageNo++) {
PageInfo info = pages.get(pageNo);
if (info.pageType == BTREE_INNER_PAGE ||
info.pageType == BTREE_LEAF_PAGE) {
if (info.numTreeReferences != 1) {
errors.add(String.format("B+ tree page %d should have " +
"exactly one tree-reference, but has %d instead",
info.pageNo, info.numTreeReferences));
}
if (info.pageType == BTREE_LEAF_PAGE &&
info.numLeafListReferences != 1) {
errors.add(String.format("Leaf page %d should have " +
"exactly one leaf-list reference, but has %d instead",
info.pageNo, info.numLeafListReferences));
}
}
else if (info.pageType == BTREE_EMPTY_PAGE) {
if (info.numEmptyListReferences != 1) {
errors.add(String.format("Empty page %d should have " +
"exactly one empty-list reference, but has %d instead",
info.pageNo, info.numEmptyListReferences));
}
}
}
}
}
package edu.caltech.nanodb.storage.btreefile;
/**
* This interface specifies the page-type values that may appear within the
* B<sup>+</sup> Tree implementation.
*
* @design (donnie) We use this instead of an {@code enum} since the values
* are actually read and written against pages in the B<sup>+</sup>
* Tree file. Note that there is no page-type value for the root
* page; that page is considered separately.
*
* @design (donnie) This class is package-private since it is an internal
* implementation detail and we want to keep it local to the
* {@code btreefile} package.
*/
final class BTreePageTypes {
/**
* This value is stored in a B-tree page's byte 0, to indicate that the
* page is an inner (i.e. non-leaf) page.
*/
public static final int BTREE_INNER_PAGE = 1;
/**
* This value is stored in a B-tree page's byte 0, to indicate that the
* page is a leaf page.
*/
public static final int BTREE_LEAF_PAGE = 2;
/**
* This value is stored in a B-tree page's byte 0, to indicate that the
* page is empty.
*/
public static final int BTREE_EMPTY_PAGE = 3;
}
package edu.caltech.nanodb.storage.btreefile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import edu.caltech.nanodb.expressions.OrderByExpression;
import edu.caltech.nanodb.expressions.TupleComparator;
import edu.caltech.nanodb.expressions.TupleLiteral;
import edu.caltech.nanodb.queryeval.TableStats;
import edu.caltech.nanodb.relations.Schema;
import edu.caltech.nanodb.relations.Tuple;
import edu.caltech.nanodb.storage.DBFile;
import edu.caltech.nanodb.storage.DBPage;
import edu.caltech.nanodb.storage.FilePointer;
import edu.caltech.nanodb.storage.InvalidFilePointerException;
import edu.caltech.nanodb.storage.PageTuple;
import edu.caltech.nanodb.storage.SequentialTupleFile;
import edu.caltech.nanodb.storage.StorageManager;
import edu.caltech.nanodb.storage.TupleFileManager;
import static edu.caltech.nanodb.storage.btreefile.BTreePageTypes.*;
/**
* <p>
* This class, along with a handful of helper classes in this package,
* provides support for B<sup>+</sup> tree tuple files. B<sup>+</sup> tree
* tuple files can be used to provide a sequential storage format for ordered
* tuples. They are also used to implement indexes for enforcing primary,
* candidate and foreign keys, and also for providing optimized access to
* tuples with specific values.
* </p>
* <p>
* Here is a brief overview of the NanoDB B<sup>+</sup> tree file format:
* </p>
* <ul>
* <li>Page 0 is always a header page, and specifies the entry-points in the
* hierarchy: the root page of the tree, and the leftmost leaf of the
* tree. Page 0 also maintains a free-list of empty pages in the tree, so
* that adding new pages to the tree is fast. (See the {@link HeaderPage}
* class for details.)</li>
* <li>The remaining pages are either leaf pages, inner pages, or empty pages.
* The first byte of the page always indicates the kind of page. For
* details about the internal structure of leaf and inner pages, see the
* {@link InnerPage} and {@link LeafPage} classes.</li>
* <li>Empty pages are organized into a simple singly linked list. Each empty
* page holds a page-pointer to the next empty page in the sequence, using
* an unsigned short stored at index 1 (after the page-type value in index
* 0). The final empty page stores 0 as its next-page pointer value.</li>
* </ul>
*/
public class BTreeTupleFile implements SequentialTupleFile {
/** A logging object for reporting anything interesting that happens. */
private static Logger logger = LogManager.getLogger(BTreeTupleFile.class);
/**
* If this flag is set to true, all data in data-pages that is no longer
* necessary is cleared. This will increase the cost of write-ahead
* logging, but it also exposes bugs more quickly because old data won't
* still be present if someone erroneously accesses it.
*/
public static final boolean CLEAR_OLD_DATA = true;
/**
* The storage manager to use for reading and writing file pages, pinning
* and unpinning pages, write-ahead logging, and so forth.
*/
private StorageManager storageManager;
/**
* The manager for B<sup>+</sup> tree tuple files provides some
* higher-level operations such as saving the metadata of a tuple file,
* so it's useful to have a reference to it.
*/
private BTreeTupleFileManager btreeFileManager;
/** The schema of tuples in this tuple file. */
private Schema schema;
/** Statistics for this tuple file. */
private TableStats stats;
/** The file that stores the tuples. */
private DBFile dbFile;
/**
* A helper class that manages file-level operations on the B+ tree file.
*/
private FileOperations fileOps;
/**
* A helper class that manages the larger-scale operations involving leaf
* nodes of the B+ tree.
*/
private LeafPageOperations leafPageOps;
/**
* A helper class that manages the larger-scale operations involving inner
* nodes of the B+ tree.
*/
private InnerPageOperations innerPageOps;
// private IndexInfo idxFileInfo;
public BTreeTupleFile(StorageManager storageManager,
BTreeTupleFileManager btreeFileManager, DBFile dbFile,
Schema schema, TableStats stats) {
if (storageManager == null)
throw new IllegalArgumentException("storageManager cannot be null");
if (btreeFileManager == null)
throw new IllegalArgumentException("btreeFileManager cannot be null");
if (dbFile == null)
throw new IllegalArgumentException("dbFile cannot be null");
if (schema == null)
throw new IllegalArgumentException("schema cannot be null");
if (stats == null)
throw new IllegalArgumentException("stats cannot be null");
this.storageManager = storageManager;
this.btreeFileManager = btreeFileManager;
this.dbFile = dbFile;
this.schema = schema;
this.stats = stats;
fileOps = new FileOperations(storageManager, dbFile);
innerPageOps = new InnerPageOperations(storageManager, this, fileOps);
leafPageOps = new LeafPageOperations(storageManager, this, fileOps,
innerPageOps);
}
@Override
public TupleFileManager getManager() {
return btreeFileManager;
}
@Override
public Schema getSchema() {
return schema;
}
@Override
public TableStats getStats() {
return stats;
}
public DBFile getDBFile() {
return dbFile;
}
@Override
public List<OrderByExpression> getOrderSpec() {
throw new UnsupportedOperationException("NYI");
}
@Override
public Tuple getFirstTuple() {
BTreeFilePageTuple tup = null;
// By passing a completely empty Tuple (no columns), we can cause the
// navigateToLeafPage() method to choose the leftmost leaf page.
TupleLiteral noTup = new TupleLiteral();
LeafPage leaf = navigateToLeafPage(noTup, false, null);
if (leaf != null && leaf.getNumTuples() > 0)
tup = leaf.getTuple(0);
return tup;
}
@Override
public Tuple getNextTuple(Tuple tup) {
BTreeFilePageTuple tuple = (BTreeFilePageTuple) tup;
DBPage dbPage;
int nextIndex;
LeafPage leaf;
BTreeFilePageTuple nextTuple = null;
if (tuple.isDeleted()) {
// The tuple was deleted, so we need to find out the page number
// and index of the next tuple.
int nextPageNo = tuple.getNextTuplePageNo();
if (nextPageNo != 0) {
dbPage = storageManager.loadDBPage(dbFile, nextPageNo);
nextIndex = tuple.getNextTupleIndex();
leaf = new LeafPage(dbPage, schema);
if (nextIndex >= leaf.getNumTuples()) {
throw new IllegalStateException(String.format(
"The \"next tuple\" field of deleted tuple is too " +
"large (must be less than %d; got %d)",
leaf.getNumTuples(), nextIndex));
}
nextTuple = leaf.getTuple(nextIndex);
}
}
else {
// Get the page that holds the current entry, and see where it
// falls within the page.
dbPage = tuple.getDBPage();
leaf = new LeafPage(dbPage, schema);
// Use the offset of the passed-in entry to find the next entry.
// The next tuple follows the current tuple, unless the current
// tuple was deleted! In that case, the next tuple is actually
// where the current tuple used to be.
nextIndex = tuple.getTupleIndex() + 1;
if (nextIndex < leaf.getNumTuples()) {
// Still more entries in this leaf.
nextTuple = leaf.getTuple(nextIndex);
}
else {
// No more entries in this leaf. Must go to the next leaf.
int nextPageNo = leaf.getNextPageNo();
if (nextPageNo != 0) {
dbPage = storageManager.loadDBPage(dbFile, nextPageNo);
leaf = new LeafPage(dbPage, schema);
if (leaf.getNumTuples() > 0) {
nextTuple = leaf.getTuple(0);
}
else {
// This would be *highly* unusual. Leaves are
// supposed to be at least 1/2 full, always!
logger.error(String.format(
"Next leaf node %d has no entries?!", nextPageNo));
}
}
}
}
return nextTuple;
}
@Override
public Tuple getTuple(FilePointer fptr)
throws InvalidFilePointerException {
DBPage dbPage = storageManager.loadDBPage(dbFile, fptr.getPageNo());
if (dbPage == null) {
throw new InvalidFilePointerException("Specified page " +
fptr.getPageNo() + " doesn't exist in file " + dbFile);
}
// In the B+ tree file format, the file-pointer points to the actual
// tuple itself.
int fpOffset = fptr.getOffset();
LeafPage leaf = new LeafPage(dbPage, schema);
for (int i = 0; i < leaf.getNumTuples(); i++) {
BTreeFilePageTuple tup = leaf.getTuple(i);
if (tup.getOffset() == fpOffset)
return tup;
// Tuple offsets within a page will be monotonically increasing.
if (tup.getOffset() > fpOffset)
break;
}
throw new InvalidFilePointerException("No tuple at offset " + fptr);
}
@Override
public Tuple findFirstTupleEquals(Tuple searchKey) {
logger.debug("Finding first tuple that equals " + searchKey +
" in BTree file " + dbFile);
LeafPage leaf = navigateToLeafPage(searchKey, false, null);
if (leaf == null) {
// This case is handled by the below loop, but it will make for
// more understandable logging.
logger.debug("BTree file is empty!");
return null;
}
logger.debug("Navigated to leaf page " + leaf.getPageNo());
while (leaf != null) {
// We have at least one tuple to look at, so scan through to
// find the first tuple that equals what we are looking for.
for (int i = 0; i < leaf.getNumTuples(); i++) {
BTreeFilePageTuple tup = leaf.getTuple(i);
int cmp = TupleComparator.comparePartialTuples(tup, searchKey,
TupleComparator.CompareMode.IGNORE_LENGTH);
// (donnie) This is just way too much logging!
// logger.debug("Comparing search key to tuple " + tup +
// ", got cmp = " + cmp);
if (cmp == 0) {
// Found it!
return tup;
}
else if (cmp > 0) {
// Subsequent tuples will appear after the search key, so
// there's no point in going on.
leaf.getDBPage().unpin();
return null;
}
}
int nextPageNo = leaf.getNextPageNo();
logger.debug("Scanned through entire leaf page %d without " +
"finding tuple. Next page is %d.", leaf.getPageNo(),
nextPageNo);
// If we get here, we need to go to the next leaf-page.
leaf.getDBPage().unpin();
if (nextPageNo > 0) {
DBPage dbpNextLeaf =
storageManager.loadDBPage(dbFile, nextPageNo);
byte pageType = dbpNextLeaf.readByte(0);
if (pageType != BTREE_LEAF_PAGE) {
throw new BTreeTupleFileException(String.format(
"Expected page %d to be a leaf; found %d instead",
nextPageNo, pageType));
}
leaf = new LeafPage(dbpNextLeaf, schema);
}
else {
logger.debug("Reached end of leaf pages");
leaf = null;
}
}
// If we reach here, we reached the end of the leaf pages without
// finding the tuple.
return null;
}
@Override
public PageTuple findFirstTupleGreaterThan(Tuple searchKey) {
LeafPage leaf = navigateToLeafPage(searchKey, false, null);
if (leaf != null && leaf.getNumTuples() > 0) {
// We have at least one tuple to look at, so scan through to find
// the first tuple that equals what we are looking for.
for (int i = 0; i < leaf.getNumTuples(); i++) {
BTreeFilePageTuple tup = leaf.getTuple(i);
int cmp = TupleComparator.comparePartialTuples(tup, searchKey);
if (cmp > 0)
return tup; // Found it!
}
leaf.getDBPage().unpin();
}
return null;
}
@Override
public Tuple addTuple(Tuple tup) {
logger.debug("Adding tuple " + tup + " to BTree file " + dbFile);
// Navigate to the leaf-page, creating one if the BTree file is
// currently empty.
ArrayList<Integer> pagePath = new ArrayList<>();
LeafPage leaf = navigateToLeafPage(tup, true, pagePath);
// TODO: This is definitely not ideal, but should get us going.
TupleLiteral tupLit;
if (tup instanceof TupleLiteral)
tupLit = (TupleLiteral) tup;
else
tupLit = new TupleLiteral(tup);
tupLit.setStorageSize(PageTuple.getTupleStorageSize(schema, tupLit));
return leafPageOps.addTuple(leaf, tupLit, pagePath);
}
@Override
public void updateTuple(Tuple tup, Map<String, Object> newValues) {
throw new UnsupportedOperationException("NYI");
}
@Override
public void deleteTuple(Tuple tup) {
BTreeFilePageTuple tuple = (BTreeFilePageTuple) tup;
ArrayList<Integer> pagePath = new ArrayList<>();
LeafPage leaf = navigateToLeafPage(tup, false, pagePath);
logger.debug("Deleting tuple " + tuple + " from file " + dbFile);
leafPageOps.deleteTuple(leaf, tuple, pagePath);
tuple.setDeleted();
}
/**
* This helper method performs the common task of navigating from the root
* of the B<sup>+</sup> tree down to the appropriate leaf node, based on
* the search-key provided by the caller. Note that this method does not
* determine whether the search-key actually exists; rather, it simply
* navigates to the leaf in the file where the search-key would appear.
*
* @param searchKey the search-key being used to navigate the
* B<sup>+</sup> tree structure
*
* @param createIfNeeded If the B<sup>+</sup> tree is currently empty
* (i.e. not even containing leaf pages) then this argument can be
* used to create a new leaf page where the search-key can be
* stored. This allows the method to be used for adding tuples to
* the file.
*
* @param pagePath If this optional argument is specified, then the method
* stores the sequence of page-numbers it visits as it navigates
* from root to leaf. If {@code null} is passed then nothing is
* stored as the method traverses the B<sup>+</sup> tree structure.
*
* @return the leaf-page where the search-key would appear, or
* {@code null} if the B<sup>+</sup> tree file is currently empty
* and {@code createIfNeeded} is {@code false}.
*/
private LeafPage navigateToLeafPage(Tuple searchKey,
boolean createIfNeeded, List<Integer> pagePath) {
// The header page tells us where the root page starts.
DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0);
// Get the root page of the BTree file.
int rootPageNo = HeaderPage.getRootPageNo(dbpHeader);
DBPage dbpRoot;
if (rootPageNo == 0) {
// The file doesn't have any data-pages at all yet. Create one if
// the caller wants it.
if (!createIfNeeded)
return null;
// We need to create a brand new leaf page and make it the root.
logger.debug("BTree file currently has no data pages; " +
"finding/creating one to use as the root!");
dbpRoot = fileOps.getNewDataPage();
rootPageNo = dbpRoot.getPageNo();
HeaderPage.setRootPageNo(dbpHeader, rootPageNo);
HeaderPage.setFirstLeafPageNo(dbpHeader, rootPageNo);
dbpRoot.writeByte(0, BTREE_LEAF_PAGE);
LeafPage.init(dbpRoot, schema);
logger.debug("New root pageNo is " + rootPageNo);
}
else {
// The BTree file has a root page; load it.
dbpRoot = storageManager.loadDBPage(dbFile, rootPageNo);
logger.debug("BTree file root pageNo is " + rootPageNo);
}
// Next, descend down the file's structure until we find the proper
// leaf-page based on the key value(s).
DBPage dbPage = dbpRoot;
int pageType = dbPage.readByte(0);
if (pageType != BTREE_INNER_PAGE && pageType != BTREE_LEAF_PAGE) {
throw new BTreeTupleFileException(
"Invalid page type encountered: " + pageType);
}
if (pagePath != null)
pagePath.add(rootPageNo);
/* TODO: IMPLEMENT THE REST OF THIS METHOD.
*
* Don't forget to update the page-path as you navigate the index
* structure, if it is provided by the caller.
*
* Use the TupleComparator.comparePartialTuples() method for comparing
* the index's keys with the passed-in search key.
*
* It's always a good idea to code defensively: if you see an invalid
* page-type, flag it with an IOException, as done earlier.
*/
logger.error("NOT YET IMPLEMENTED: navigateToLeafPage()");
return null;
}
@Override
public void analyze() {
throw new UnsupportedOperationException("NYI");
}
@Override
public List<String> verify() {
BTreeFileVerifier verifier =
new BTreeFileVerifier(storageManager, this);
return verifier.verify();
}
@Override
public void optimize() {
throw new UnsupportedOperationException("NYI");
}
}
package edu.caltech.nanodb.storage.btreefile;
import edu.caltech.nanodb.storage.TupleFileException;
/**
* This class represents errors that occur while manipulating B<sup>+</sup>
* tree tuple files.
*/
public class BTreeTupleFileException extends TupleFileException {
/** Construct a tuple-file exception with no message. */
public BTreeTupleFileException() {
super();
}
/**
* Construct a tuple-file exception with the specified message.
*/
public BTreeTupleFileException(String msg) {
super(msg);
}
/**
* Construct a tuple-file exception with the specified cause and no message.
*/
public BTreeTupleFileException(Throwable cause) {
super(cause);
}
/**
* Construct a tuple-file exception with the specified message and cause.
*/
public BTreeTupleFileException(String msg, Throwable cause) {
super(msg, cause);
}
}
package edu.caltech.nanodb.storage.btreefile;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import edu.caltech.nanodb.queryeval.TableStats;
import edu.caltech.nanodb.relations.Schema;
import edu.caltech.nanodb.storage.DBFile;
import edu.caltech.nanodb.storage.DBFileType;
import edu.caltech.nanodb.storage.DBPage;
import edu.caltech.nanodb.storage.PageReader;
import edu.caltech.nanodb.storage.PageWriter;
import edu.caltech.nanodb.storage.SchemaWriter;
import edu.caltech.nanodb.storage.StatsWriter;
import edu.caltech.nanodb.storage.StorageManager;
import edu.caltech.nanodb.storage.TupleFile;
import edu.caltech.nanodb.storage.TupleFileManager;
/**
* This class provides high-level operations on B<sup>+</sup> tree tuple files.
*/
public class BTreeTupleFileManager implements TupleFileManager {
/** A logging object for reporting anything interesting that happens. */
private static Logger logger = LogManager.getLogger(BTreeTupleFileManager.class);
/** A reference to the storage manager. */
private StorageManager storageManager;
public BTreeTupleFileManager(StorageManager storageManager) {
if (storageManager == null)
throw new IllegalArgumentException("storageManager cannot be null");
this.storageManager = storageManager;
}
@Override
public DBFileType getDBFileType() {
return DBFileType.BTREE_TUPLE_FILE;
}
@Override
public String getShortName() {
return "btree";
}
@Override
public TupleFile createTupleFile(DBFile dbFile, Schema schema) {
logger.info(String.format(
"Initializing new btree tuple file %s with %d columns",
dbFile, schema.numColumns()));
// Table schema is stored into the header page, so get it and prepare
// to write out the schema information.
DBPage headerPage = storageManager.loadDBPage(dbFile, 0);
PageWriter hpWriter = new PageWriter(headerPage);
// Skip past the page-size value.
hpWriter.setPosition(HeaderPage.OFFSET_SCHEMA_START);
// Write out the schema details now.
SchemaWriter schemaWriter = new SchemaWriter();
schemaWriter.writeSchema(schema, hpWriter);
// Compute and store the schema's size.
int schemaEndPos = hpWriter.getPosition();
int schemaSize = schemaEndPos - HeaderPage.OFFSET_SCHEMA_START;
HeaderPage.setSchemaSize(headerPage, schemaSize);
// Write in empty statistics, so that the values are at least
// initialized to something.
TableStats stats = new TableStats(schema.numColumns());
StatsWriter.writeTableStats(schema, stats, hpWriter);
int statsSize = hpWriter.getPosition() - schemaEndPos;
HeaderPage.setStatsSize(headerPage, statsSize);
return new BTreeTupleFile(storageManager, this, dbFile, schema, stats);
}
@Override
public TupleFile openTupleFile(DBFile dbFile) {
logger.info("Opening existing btree tuple file " + dbFile);
// Table schema is stored into the header page, so get it and prepare
// to write out the schema information.
DBPage headerPage = storageManager.loadDBPage(dbFile, 0);
PageReader hpReader = new PageReader(headerPage);
// Skip past the page-size value.
hpReader.setPosition(HeaderPage.OFFSET_SCHEMA_START);
// Read in the schema details.
SchemaWriter schemaWriter = new SchemaWriter();
Schema schema = schemaWriter.readSchema(hpReader);
// Read in the statistics.
TableStats stats = StatsWriter.readTableStats(hpReader, schema);
return new BTreeTupleFile(storageManager, this, dbFile, schema, stats);
}
@Override
public void saveMetadata(TupleFile tupleFile) {
// TODO
throw new UnsupportedOperationException("NYI: deleteTupleFile()");
}
@Override
public void deleteTupleFile(TupleFile tupleFile) {
// TODO
throw new UnsupportedOperationException("NYI: deleteTupleFile()");
}
}
package edu.caltech.nanodb.storage.btreefile;
/**
* Created with IntelliJ IDEA.
* User: donnie
* Date: 2/4/13
* Time: 6:51 PM
* To change this template use File | Settings | File Templates.
*/
public interface DataPage {
/** The page type always occupies the first byte of the page. */
public static final int OFFSET_PAGE_TYPE = 0;
}
package edu.caltech.nanodb.storage.btreefile;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import edu.caltech.nanodb.storage.DBFile;
import edu.caltech.nanodb.storage.DBPage;
import edu.caltech.nanodb.storage.StorageManager;
/**
* This class provides file-level operations for the B<sup>+</sup> tree
* implementation, such as acquiring a new data page or releasing a data page.
*/
class FileOperations {
/** A logging object for reporting anything interesting that happens. */
private static Logger logger = LogManager.getLogger(FileOperations.class);
/** The storage manager to use for loading pages. */
private StorageManager storageManager;
/** The {@code DBFile} that the B<sup>+</sup> tree is stored in. */
private DBFile dbFile;
public FileOperations(StorageManager storageManager, DBFile dbFile) {
this.storageManager = storageManager;
this.dbFile = dbFile;
}
/**
* This helper function finds and returns a new data page, either by
* taking it from the empty-pages list in the file, or if the list is
* empty, creating a brand new page at the end of the file.
*
* @return an empty {@code DBPage} that can be used for storing tuple data
*/
public DBPage getNewDataPage() {
DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0);
DBPage newPage;
int pageNo = HeaderPage.getFirstEmptyPageNo(dbpHeader);
if (pageNo == 0) {
// There are no empty pages. Create a new page to use.
logger.debug("No empty pages. Extending BTree file " + dbFile +
" by one page.");
int numPages = dbFile.getNumPages();
newPage = storageManager.loadDBPage(dbFile, numPages, true);
}
else {
// Load the empty page, and remove it from the chain of empty pages.
logger.debug("First empty page number is " + pageNo);
newPage = storageManager.loadDBPage(dbFile, pageNo);
int nextEmptyPage = newPage.readUnsignedShort(1);
HeaderPage.setFirstEmptyPageNo(dbpHeader, nextEmptyPage);
}
logger.debug("Found data page to use: page " + newPage.getPageNo());
// TODO: Increment the number of data pages?
return newPage;
}
/**
* This helper function marks a data page in the B<sup>+</sup> tree file
* as "empty", and adds it to the list of empty pages in the file.
*
* @param dbPage the data-page that is no longer used.
*/
public void releaseDataPage(DBPage dbPage) {
// TODO: If this page is the last page of the index file, we could
// truncate pages off the end until we hit a non-empty page.
// Instead, we'll leave all the pages around forever...
DBFile dbFile = dbPage.getDBFile();
// Record in the page that it is empty.
dbPage.writeByte(0, BTreePageTypes.BTREE_EMPTY_PAGE);
DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0);
// Retrieve the old "first empty page" value, and store it in this page.
int prevEmptyPageNo = HeaderPage.getFirstEmptyPageNo(dbpHeader);
dbPage.writeShort(1, prevEmptyPageNo);
if (BTreeTupleFile.CLEAR_OLD_DATA) {
// Clear out the remainder of the data-page since it's now unused.
dbPage.setDataRange(3, dbPage.getPageSize() - 3, (byte) 0);
}
// Store the new "first empty page" value into the header.
HeaderPage.setFirstEmptyPageNo(dbpHeader, dbPage.getPageNo());
}
}
package edu.caltech.nanodb.storage.btreefile;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import edu.caltech.nanodb.storage.DBFileType;
import edu.caltech.nanodb.storage.DBPage;
/**
* This class manipulates the header page for a B<sup>+</sup> tree index file.
* The header page has the following structure:
*
* <ul>
* <li><u>Byte 0:</u> {@link DBFileType#BTREE_TUPLE_FILE} (unsigned byte)</li>
* <li><u>Byte 1:</u> page size <i>p</i> (unsigned byte) - file's page
* size is <i>P</i> = 2<sup>p</sup></li>
*
* <li>Byte 2-M: Specification of index key-columns and column ordering.</li>
* <li>Byte P-2 to P-1: the page of the file that is the root of the index</li>
* </ul>
*/
public class HeaderPage {
/** A logging object for reporting anything interesting that happens. */
private static Logger logger = LogManager.getLogger(HeaderPage.class);
/**
* The offset in the header page where the page number of the index's root
* page is stored. This value is an unsigned short.
*/
public static final int OFFSET_ROOT_PAGE = 2;
/**
* The offset in the header page where the page number of the first leaf
* page of the file is stored. This allows the leaves of the tuple file
* to be iterated through in sequential order. This value is an unsigned
* short.
*/
public static final int OFFSET_FIRST_LEAF_PAGE = 4;
/**
* The offset in the header page where the page number of the first empty
* page in the free list is stored. This value is an unsigned short.
*/
public static final int OFFSET_FIRST_EMPTY_PAGE = 6;
/**
* The offset in the header page where the length of the file's schema is
* stored. The statistics follow immediately after the schema.
*/
public static final int OFFSET_SCHEMA_SIZE = 8;
/**
* The offset in the header page where the size of the table statistics
* are stored. This value is an unsigned short.
*/
public static final int OFFSET_STATS_SIZE = 10;
/**
* The offset in the header page where the table schema starts. This
* value is an unsigned short.
*/
public static final int OFFSET_SCHEMA_START = 12;
/**
* This helper method simply verifies that the data page provided to the
* <tt>HeaderPage</tt> class is in fact a header-page (i.e. page 0 in the
* data file).
*
* @param dbPage the page to check
*
* @throws IllegalArgumentException if <tt>dbPage</tt> is <tt>null</tt>, or
* if it's not actually page 0 in the table file
*/
private static void verifyIsHeaderPage(DBPage dbPage) {
if (dbPage == null)
throw new IllegalArgumentException("dbPage cannot be null");
if (dbPage.getPageNo() != 0) {
throw new IllegalArgumentException(
"Page 0 is the header page in this storage format; was given page " +
dbPage.getPageNo());
}
}
/**
* Returns the page-number of the root page in the index file.
*
* @param dbPage the header page of the index file
* @return the page-number of the root page, or 0 if the index file doesn't
* contain a root page.
*/
public static int getRootPageNo(DBPage dbPage) {
verifyIsHeaderPage(dbPage);
return dbPage.readUnsignedShort(OFFSET_ROOT_PAGE);
}
/**
* Sets the page-number of the root page in the header page of the index
* file.
*
* @param dbPage the header page of the heap table file
* @param rootPageNo the page-number of the root page, or 0 if the index
* file doesn't contain a root page.
*/
public static void setRootPageNo(DBPage dbPage, int rootPageNo) {
verifyIsHeaderPage(dbPage);
if (rootPageNo < 0) {
throw new IllegalArgumentException(
"rootPageNo must be > 0; got " + rootPageNo);
}
dbPage.writeShort(OFFSET_ROOT_PAGE, rootPageNo);
}
/**
* Returns the page-number of the first leaf page in the index file.
*
* @param dbPage the header page of the index file
* @return the page-number of the first leaf page in the index file, or 0
* if the index file doesn't contain any leaf pages.
*/
public static int getFirstLeafPageNo(DBPage dbPage) {
verifyIsHeaderPage(dbPage);
return dbPage.readUnsignedShort(OFFSET_FIRST_LEAF_PAGE);
}
/**
* Sets the page-number of the first leaf page in the header page of the
* index file.
*
* @param dbPage the header page of the heap table file
* @param firstLeafPageNo the page-number of the first leaf page in the
* index file, or 0 if the index file doesn't contain any leaf pages.
*/
public static void setFirstLeafPageNo(DBPage dbPage, int firstLeafPageNo) {
verifyIsHeaderPage(dbPage);
if (firstLeafPageNo < 0) {
throw new IllegalArgumentException(
"firstLeafPageNo must be >= 0; got " + firstLeafPageNo);
}
dbPage.writeShort(OFFSET_FIRST_LEAF_PAGE, firstLeafPageNo);
}
/**
* Returns the page-number of the first empty page in the index file.
* Empty pages form a linked chain in the index file, so that they are
* easy to locate.
*
* @param dbPage the header page of the index file
* @return the page-number of the last leaf page in the index file.
*/
public static int getFirstEmptyPageNo(DBPage dbPage) {
verifyIsHeaderPage(dbPage);
return dbPage.readUnsignedShort(OFFSET_FIRST_EMPTY_PAGE);
}
/**
* Sets the page-number of the first empty page in the header page of the
* index file. Empty pages form a linked chain in the index file, so that
* they are easy to locate.
*
* @param dbPage the header page of the heap table file
* @param firstEmptyPageNo the page-number of the first empty page
*/
public static void setFirstEmptyPageNo(DBPage dbPage, int firstEmptyPageNo) {
verifyIsHeaderPage(dbPage);
if (firstEmptyPageNo < 0) {
throw new IllegalArgumentException(
"firstEmptyPageNo must be >= 0; got " + firstEmptyPageNo);
}
dbPage.writeShort(OFFSET_FIRST_EMPTY_PAGE, firstEmptyPageNo);
}
/**
* Returns the number of bytes that the table's schema occupies for storage
* in the header page.
*
* @param dbPage the header page of the heap table file
* @return the number of bytes that the table's schema occupies
*/
public static int getSchemaSize(DBPage dbPage) {
verifyIsHeaderPage(dbPage);
return dbPage.readUnsignedShort(OFFSET_SCHEMA_SIZE);
}
/**
* Sets the number of bytes that the table's schema occupies for storage
* in the header page.
*
* @param dbPage the header page of the heap table file
* @param numBytes the number of bytes that the table's schema occupies
*/
public static void setSchemaSize(DBPage dbPage, int numBytes) {
verifyIsHeaderPage(dbPage);
if (numBytes < 0) {
throw new IllegalArgumentException(
"numButes must be >= 0; got " + numBytes);
}
dbPage.writeShort(OFFSET_SCHEMA_SIZE, numBytes);
}
/**
* Returns the number of bytes that the table's statistics occupy for
* storage in the header page.
*
* @param dbPage the header page of the heap table file
* @return the number of bytes that the table's statistics occupy
*/
public static int getStatsSize(DBPage dbPage) {
verifyIsHeaderPage(dbPage);
return dbPage.readUnsignedShort(OFFSET_STATS_SIZE);
}
/**
* Sets the number of bytes that the table's statistics occupy for storage
* in the header page.
*
* @param dbPage the header page of the heap table file
* @param numBytes the number of bytes that the table's statistics occupy
*/
public static void setStatsSize(DBPage dbPage, int numBytes) {
verifyIsHeaderPage(dbPage);
if (numBytes < 0) {
throw new IllegalArgumentException(
"numButes must be >= 0; got " + numBytes);
}
dbPage.writeShort(OFFSET_STATS_SIZE, numBytes);
}
/**
* Returns the offset in the header page that the table statistics start at.
* This value changes because the table schema resides before the stats, and
* therefore the stats don't live at a fixed location.
*
* @param dbPage the header page of the heap table file
* @return the offset within the header page that the table statistics
* reside at
*/
public static int getStatsOffset(DBPage dbPage) {
verifyIsHeaderPage(dbPage);
return OFFSET_SCHEMA_START + getSchemaSize(dbPage);
}
}
<html>
<body>
<p>
This package contains an implementation of B<sup>+</sup> tree
tuple files, which can be used for sequential tuple files, as
well as table indexes.
</p>
<!--
<h2>Leaf Page Internal Structure</h2>
<table>
<tr><th>Offset</th><th>Size</th><th>Description</th></tr>
<tr>
<td>0</td>
<td>byte</td>
<td>Page type, always 2 for leaf pages.
(See {@link BTreeIndexManager#BTREE_LEAF_PAGE}.)</td>
</tr>
<tr>
<td>1</td>
<td>unsigned short</td>
<td>Page number of next leaf page.
(See {@link LeafPage#OFFSET_NEXT_PAGE_NO}.)</td>
</tr>
<tr>
<td>3</td>
<td>unsigned short</td>
<td>The number of key+pointer entries is stored in the page.
(See {@link LeafPage#OFFSET_NUM_ENTRIES}.)</td>
</tr>
<tr>
</tr>
</table>
-->
</body>
</html>
package edu.caltech.test.nanodb.storage.btreefile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Random;
import edu.caltech.nanodb.expressions.ColumnName;
import edu.caltech.nanodb.expressions.ColumnValue;
import edu.caltech.nanodb.expressions.OrderByExpression;
import edu.caltech.nanodb.expressions.TupleComparator;
import edu.caltech.nanodb.expressions.TupleLiteral;
import edu.caltech.nanodb.relations.ColumnInfo;
import edu.caltech.nanodb.relations.ColumnType;
import edu.caltech.nanodb.relations.SQLDataType;
import edu.caltech.nanodb.relations.Schema;
import edu.caltech.nanodb.server.CommandResult;
import org.testng.annotations.*;
import edu.caltech.test.nanodb.sql.SqlTestCase;
/**
* This test class exercises the B<sup>+</sup>-tree file format so that we can
* have some confidence that it actually works correctly.
*/
@Test(groups={"storage", "hw6"})
public class TestBTreeFile extends SqlTestCase {
/**
* If set to true, this causes the tests to verify the contents of the
* B tree file after every insertion or deletion. This obviously greatly
* slows down the test, but it allows issues to be identified exactly when
* they appear.
*/
public static final boolean CHECK_AFTER_EACH_CHANGE = false;
/**
* A source of randomness to generate tuples from. Set the seed so we
* have reproducible test cases.
*/
private Random rand = new Random(12345);
private String makeRandomString(int minChars, int maxChars) {
StringBuilder buf = new StringBuilder();
int num = minChars + rand.nextInt(maxChars - minChars + 1);
for (int i = 0; i < num; i++)
buf.append((char) ('A' + rand.nextInt('Z' - 'A' + 1)));
return buf.toString();
}
private void sortTupleLiteralArray(ArrayList<TupleLiteral> tuples) {
Schema schema = new Schema();
schema.addColumnInfo(new ColumnInfo("a", new ColumnType(SQLDataType.INTEGER)));
schema.addColumnInfo(new ColumnInfo("b", new ColumnType(SQLDataType.VARCHAR)));
ArrayList<OrderByExpression> orderSpec = new ArrayList<>();
orderSpec.add(new OrderByExpression(new ColumnValue(new ColumnName("a"))));
orderSpec.add(new OrderByExpression(new ColumnValue(new ColumnName("b"))));
TupleComparator comp = new TupleComparator(schema, orderSpec);
Collections.sort(tuples, comp);
}
private void runBTreeTest(String tableName, int numRowsToInsert,
int maxAValue, int minBLen, int maxBLen,
double probDeletion) throws Exception {
ArrayList<TupleLiteral> inserted = new ArrayList<>();
CommandResult result;
for (int i = 0; i < numRowsToInsert; i++) {
int a = rand.nextInt(maxAValue);
String b = makeRandomString(minBLen, maxBLen);
tryDoCommand(String.format(
"INSERT INTO %s VALUES (%d, '%s');", tableName, a, b), false);
inserted.add(new TupleLiteral(a, b));
if (CHECK_AFTER_EACH_CHANGE) {
sortTupleLiteralArray(inserted);
result = tryDoCommand(String.format("SELECT * FROM %s;",
tableName), true);
assert checkOrderedResults(inserted.toArray(new TupleLiteral[inserted.size()]), result);
}
if (probDeletion > 0.0 && rand.nextDouble() < probDeletion) {
// Delete some rows from the table we are populating.
int minAToDel = rand.nextInt(maxAValue);
int maxAToDel = rand.nextInt(maxAValue);
if (minAToDel > maxAToDel) {
int tmp = minAToDel;
minAToDel = maxAToDel;
maxAToDel = tmp;
}
tryDoCommand(String.format(
"DELETE FROM %s WHERE a BETWEEN %d AND %d;", tableName,
minAToDel, maxAToDel), false);
// Apply the same deletion to our in-memory collection of
// tuples, so that we can mirror what the table should contain
Iterator<TupleLiteral> iter = inserted.iterator();
while (iter.hasNext()) {
TupleLiteral tup = iter.next();
int aVal = (Integer) tup.getColumnValue(0);
if (aVal >= minAToDel && aVal <= maxAToDel)
iter.remove();
}
if (CHECK_AFTER_EACH_CHANGE) {
sortTupleLiteralArray(inserted);
result = tryDoCommand(String.format("SELECT * FROM %s;",
tableName), true);
assert checkOrderedResults(inserted.toArray(new TupleLiteral[inserted.size()]), result);
}
}
}
sortTupleLiteralArray(inserted);
result = tryDoCommand(String.format("SELECT * FROM %s;",
tableName), true);
assert checkOrderedResults(inserted.toArray(new TupleLiteral[inserted.size()]), result);
// TODO: This is necessary because the btree code doesn't unpin
// pages properly...
// server.getStorageManager().flushAllData();
}
public void testBTreeTableOnePageInsert() throws Exception {
tryDoCommand("CREATE TABLE btree_one_page (a INTEGER, b VARCHAR(20)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_one_page", 300, 200, 3, 20, 0.0);
}
public void testBTreeTableTwoPageInsert() throws Exception {
tryDoCommand("CREATE TABLE btree_two_page (a INTEGER, b VARCHAR(50)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_two_page", 400, 200, 20, 50, 0.0);
}
public void testBTreeTableTwoLevelInsert() throws Exception {
tryDoCommand("CREATE TABLE btree_two_level (a INTEGER, b VARCHAR(50)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_two_level", 10000, 1000, 20, 50, 0.0);
}
public void testBTreeTableThreeLevelInsert() throws Exception {
tryDoCommand("CREATE TABLE btree_three_level (a INTEGER, b VARCHAR(250)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_three_level", 100000, 5000, 150, 250, 0.0);
}
public void testBTreeTableOnePageInsertDelete() throws Exception {
tryDoCommand("CREATE TABLE btree_one_page_del (a INTEGER, b VARCHAR(20)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_one_page_del", 400, 200, 3, 20, 0.05);
}
public void testBTreeTableTwoPageInsertDelete() throws Exception {
tryDoCommand("CREATE TABLE btree_two_page_del (a INTEGER, b VARCHAR(50)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_two_page_del", 500, 200, 20, 50, 0.05);
}
public void testBTreeTableTwoLevelInsertDelete() throws Exception {
tryDoCommand("CREATE TABLE btree_two_level_del (a INTEGER, b VARCHAR(50)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_two_level_del", 12000, 1000, 20, 50, 0.05);
}
public void testBTreeTableThreeLevelInsertDelete() throws Exception {
tryDoCommand("CREATE TABLE btree_three_level_del (a INTEGER, b VARCHAR(250)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_three_level_del", 120000, 5000, 150, 250, 0.05);
}
public void testBTreeTableMultiLevelInsertDelete() throws Exception {
tryDoCommand("CREATE TABLE btree_multi_level_del (a INTEGER, b VARCHAR(400)) " +
"PROPERTIES (storage = 'btree');", false);
runBTreeTest("btree_multi_level_del", 250000, 5000, 50, 400, 0.01);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment