diff --git a/doc/lab6design.txt b/doc/lab6design.txt new file mode 100644 index 0000000000000000000000000000000000000000..f5969a4281e50b68a5ca71ac90e77ac270b29846 --- /dev/null +++ b/doc/lab6design.txt @@ -0,0 +1,129 @@ +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? diff --git a/doc/lab6info.txt b/doc/lab6info.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b115da60745232b866c6d2fa17cd11fd1b32ba8 --- /dev/null +++ b/doc/lab6info.txt @@ -0,0 +1,33 @@ +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. + + diff --git a/pom.xml b/pom.xml index fdc5d4e1c1e1979556cd23ae61fc51c3c0a062f5..e665e32a43d5f470b4bb39d96333c08e2c1cf90c 100644 --- a/pom.xml +++ b/pom.xml @@ -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> diff --git a/src/main/java/edu/caltech/nanodb/storage/IndexedTableManager.java b/src/main/java/edu/caltech/nanodb/storage/IndexedTableManager.java index 54c5734c0401fd18d0dfe137fd00dbc80087c487..85fc88eb962910d36236a50abf1096f437f5761b 100644 --- a/src/main/java/edu/caltech/nanodb/storage/IndexedTableManager.java +++ b/src/main/java/edu/caltech/nanodb/storage/IndexedTableManager.java @@ -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); diff --git a/src/main/java/edu/caltech/nanodb/storage/StorageManager.java b/src/main/java/edu/caltech/nanodb/storage/StorageManager.java index a4f65ce659c80132137053cfedd60561c529f9e4..f4df532d0710a8928c12976f05b73126fef22a76 100755 --- a/src/main/java/edu/caltech/nanodb/storage/StorageManager.java +++ b/src/main/java/edu/caltech/nanodb/storage/StorageManager.java @@ -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); diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeFilePageTuple.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeFilePageTuple.java new file mode 100644 index 0000000000000000000000000000000000000000..751eef700d2c76ef3ca767e5545fec26b949c440 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeFilePageTuple.java @@ -0,0 +1,147 @@ +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(); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeFileVerifier.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeFileVerifier.java new file mode 100644 index 0000000000000000000000000000000000000000..2892ef1a9f7ba3fb91945f4087281ef6f9bceca6 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeFileVerifier.java @@ -0,0 +1,532 @@ +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)); + } + } + } + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreePageTypes.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreePageTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..1d5cccf45c56975212d7b5f832033ff2237e833b --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreePageTypes.java @@ -0,0 +1,37 @@ +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; +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFile.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFile.java new file mode 100644 index 0000000000000000000000000000000000000000..01ab84daf93870fc199069e72608e18bc7d9111d --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFile.java @@ -0,0 +1,541 @@ +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"); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFileException.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFileException.java new file mode 100644 index 0000000000000000000000000000000000000000..3edb458fc9875097a708a59bb79cc7ce064a7fb1 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFileException.java @@ -0,0 +1,39 @@ +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); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFileManager.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFileManager.java new file mode 100644 index 0000000000000000000000000000000000000000..dac5dcb5541edf075d10b52cc86e88353a5e732f --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/BTreeTupleFileManager.java @@ -0,0 +1,122 @@ +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()"); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/DataPage.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/DataPage.java new file mode 100644 index 0000000000000000000000000000000000000000..09bf037a8d4e7259f6c8e246e1d264120bbb6da9 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/DataPage.java @@ -0,0 +1,14 @@ +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; +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/FileOperations.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/FileOperations.java new file mode 100644 index 0000000000000000000000000000000000000000..705476e54956d0030cb49f4ba2360f12c8fb65f7 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/FileOperations.java @@ -0,0 +1,103 @@ +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()); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/HeaderPage.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/HeaderPage.java new file mode 100644 index 0000000000000000000000000000000000000000..62f35aa3fbd8c35ddb0562e54de0636ce671650e --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/HeaderPage.java @@ -0,0 +1,272 @@ +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); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/InnerPage.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/InnerPage.java new file mode 100644 index 0000000000000000000000000000000000000000..afaa09eadaf6f7c88712523171a9065d785cab39 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/InnerPage.java @@ -0,0 +1,998 @@ +package edu.caltech.nanodb.storage.btreefile; + + +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +import edu.caltech.nanodb.expressions.TupleLiteral; +import edu.caltech.nanodb.relations.Schema; +import edu.caltech.nanodb.relations.Tuple; +import edu.caltech.nanodb.storage.DBPage; +import edu.caltech.nanodb.storage.PageTuple; + +import static edu.caltech.nanodb.storage.btreefile.BTreePageTypes.*; + + +/** + * <p> + * This class wraps a {@link DBPage} object that is an inner page in the + * B<sup>+</sup> tree file implementation, to provide some of the basic + * inner-page-management operations necessary for the file structure. + * </p> + * <p> + * Operations involving individual leaf-pages are provided by the + * {@link LeafPage} wrapper-class. Higher-level operations involving multiple + * leaves and/or inner pages of the B<sup>+</sup> tree structure, are provided + * by the {@link LeafPageOperations} and {@link InnerPageOperations} classes. + * </p> + */ +public class InnerPage implements DataPage { + /** A logging object for reporting anything interesting that happens. */ + private static Logger logger = LogManager.getLogger(InnerPage.class); + + + /** + * The offset where the number of pointer entries is stored in the page. + * The page will hold one fewer tuples than pointers, since each tuple + * must be sandwiched between two pointers. + */ + public static final int OFFSET_NUM_POINTERS = 3; + + + /** The offset of the first pointer in the non-leaf page. */ + public static final int OFFSET_FIRST_POINTER = 5; + + + /** The actual data page that holds the B<sup>+</sup> tree inner node. */ + private DBPage dbPage; + + + /** The schema of the tuples in the leaf page. */ + private Schema schema; + + + /** The number of pointers stored within this non-leaf page. */ + private int numPointers; + + + /** + * An array of the offsets where the pointers are stored in this non-leaf + * page. Each pointer points to another page within the file. There is + * one more pointer than the number of tuples, since each tuple must be + * sandwiched between two pointers. + */ + private int[] pointerOffsets; + + + /** An array of the tuples stored in this non-leaf page. */ + private BTreeFilePageTuple[] keys; + + + /** + * The total size of all data (pointers + tuples + initial values) stored + * within this non-leaf page. This is also the offset at which we can + * start writing more data without overwriting anything. + */ + private int endOffset; + + + /** + * Initialize the inner-page wrapper class for the specified B<sup>+</sup> + * tree leaf page. The contents of the inner-page are cached in the + * fields of the wrapper object. + * + * @param dbPage the data page from the B<sup>+</sup> Tree file to wrap + * @param schema the schema of tuples stored in the data page + */ + public InnerPage(DBPage dbPage, Schema schema) { + if (dbPage.readUnsignedByte(0) != BTREE_INNER_PAGE) { + throw new IllegalArgumentException("Specified DBPage " + + dbPage.getPageNo() + " is not marked as an inner page."); + } + + this.dbPage = dbPage; + this.schema = schema; + + loadPageContents(); + } + + + /** + * This static helper function initializes a {@link DBPage} object's + * contents with the type and detail values that will allow a new + * {@code InnerPage} wrapper to be instantiated for the page, and then it + * returns a wrapper object for the page. This version of the {@code init} + * function creates an inner page that is initially empty. + * + * @param dbPage the page to initialize as an inner page. + * + * @param schema the schema of the tuples in the leaf page + * + * @return a newly initialized {@code InnerPage} object wrapping the page + */ + public static InnerPage init(DBPage dbPage, Schema schema) { + dbPage.writeByte(OFFSET_PAGE_TYPE, BTREE_INNER_PAGE); + dbPage.writeShort(OFFSET_NUM_POINTERS, 0); + + return new InnerPage(dbPage, schema); + } + + + /** + * This static helper function initializes a {@link DBPage} object's + * contents with the type and detail values that will allow a new + * {@code InnerPage} wrapper to be instantiated for the page, and then it + * returns a wrapper object for the page. This version of the {@code init} + * function creates an inner page that initially contains the specified + * page-pointers and key value. + * + * @param dbPage the page to initialize as an inner page. + * + * @param schema the schema of the tuples in the inner page + * + * @param pagePtr1 the first page-pointer to store in the inner page, to the + * left of {@code key1} + * + * @param key1 the first key to store in the inner page + * + * @param pagePtr2 the second page-pointer to store in the inner page, to + * the right of {@code key1} + * + * @return a newly initialized {@code InnerPage} object wrapping the page + */ + public static InnerPage init(DBPage dbPage, Schema schema, + int pagePtr1, Tuple key1, int pagePtr2) { + + dbPage.writeByte(OFFSET_PAGE_TYPE, BTREE_INNER_PAGE); + + // Write the first contents of the non-leaf page: [ptr0, key0, ptr1] + // Since key0 will usually be a BTreeFilePageTuple, we have to rely on + // the storeTuple() method to tell us where the new tuple's data ends. + + int offset = OFFSET_FIRST_POINTER; + + dbPage.writeShort(offset, pagePtr1); + offset += 2; + + offset = PageTuple.storeTuple(dbPage, offset, schema, key1); + + dbPage.writeShort(offset, pagePtr2); + + dbPage.writeShort(OFFSET_NUM_POINTERS, 2); + + return new InnerPage(dbPage, schema); + } + + + /** + * This private helper scans through the inner page's contents and caches + * the contents of the inner page in a way that makes it easy to use and + * manipulate. + */ + private void loadPageContents() { + numPointers = dbPage.readUnsignedShort(OFFSET_NUM_POINTERS); + if (numPointers > 0) { + pointerOffsets = new int[numPointers]; + keys = new BTreeFilePageTuple[numPointers - 1]; + + // Handle first pointer + key separately since we know their offsets + + pointerOffsets[0] = OFFSET_FIRST_POINTER; + + if (numPointers == 1) { + // This will happen when we are deleting values from a page. + // No keys, just 1 pointer, done! + endOffset = OFFSET_FIRST_POINTER + 2; + return; + } + + BTreeFilePageTuple key = new BTreeFilePageTuple(schema, dbPage, + OFFSET_FIRST_POINTER + 2, 0); + keys[0] = key; + + // Handle all the pointer/key pairs. This excludes the last + // pointer. + + int keyEndOffset; + for (int i = 1; i < numPointers - 1; i++) { + // Next pointer starts where the previous key ends. + keyEndOffset = key.getEndOffset(); + pointerOffsets[i] = keyEndOffset; + + // Next key starts after the next pointer. + key = new BTreeFilePageTuple(schema, dbPage, keyEndOffset + 2, i); + keys[i] = key; + } + + keyEndOffset = key.getEndOffset(); + pointerOffsets[numPointers - 1] = keyEndOffset; + endOffset = keyEndOffset + 2; + } + else { + // There are no entries (pointers + keys). + endOffset = OFFSET_FIRST_POINTER; + pointerOffsets = null; + keys = null; + } + } + + + /** + * Returns the {@code DBPage} that backs this leaf page. + * + * @return the {@code DBPage} that backs this leaf page. + */ + public DBPage getDBPage() { + return dbPage; + } + + + /** + * Returns the page-number of this leaf page. + * + * @return the page-number of this leaf page. + */ + public int getPageNo() { + return dbPage.getPageNo(); + } + + + /** + * Given a leaf page in the index, returns the page number of the left + * sibling, or -1 if there is no left sibling to this node. + * + * @param pagePath the page path from root to this leaf page + * @param innerOps the inner page ops that allows this method to + * load inner pages and navigate the tree + * + * @return the page number of the left sibling leaf-node, or -1 if there + * is no left sibling + * + * @review (Donnie) There is a lot of implementation-overlap between this + * function and the {@link InnerPage#getLeftSibling}. Maybe find + * a way to combine the implementations. + */ + public int getLeftSibling(List<Integer> pagePath, + InnerPageOperations innerOps) { + + // Verify that the last node in the page path is in fact this page. + if (pagePath.get(pagePath.size() - 1) != getPageNo()) { + throw new IllegalArgumentException( + "The page path provided does not terminate on this leaf page."); + } + + // If this leaf doesn't have a parent, we already know it doesn't + // have a sibling. + if (pagePath.size() <= 1) + return -1; + + int parentPageNo = pagePath.get(pagePath.size() - 2); + InnerPage inner = innerOps.loadPage(parentPageNo); + + // Get the index of the pointer that points to this page. If it + // doesn't appear in the parent, we have a serious problem... + int pageIndex = inner.getIndexOfPointer(getPageNo()); + if (pageIndex == -1) { + throw new IllegalStateException(String.format( + "Leaf node %d doesn't appear in parent inner node %d!", + getPageNo(), parentPageNo)); + } + + int leftSiblingIndex = pageIndex - 1; + int leftSiblingPageNo = -1; + + if (leftSiblingIndex >= 0) + leftSiblingPageNo = inner.getPointer(leftSiblingIndex); + + return leftSiblingPageNo; + } + + + /** + * Given a leaf page in the index, returns the page number of the right + * sibling, or -1 if there is no right sibling to this node. + * + * @param pagePath the page path from root to this leaf page + * @param innerOps the inner page ops that allows this method to + * load inner pages and navigate the tree + * + * @return the page number of the right sibling leaf-node, or -1 if there + * is no right sibling + * + * @review (Donnie) There is a lot of implementation-overlap between this + * function and the {@link InnerPage#getLeftSibling}. Maybe find + * a way to combine the implementations. + */ + public int getRightSibling(List<Integer> pagePath, + InnerPageOperations innerOps) { + + // Verify that the last node in the page path is in fact this page. + if (pagePath.get(pagePath.size() - 1) != getPageNo()) { + throw new IllegalArgumentException( + "The page path provided does not terminate on this inner page."); + } + + // If this leaf doesn't have a parent, we already know it doesn't + // have a sibling. + if (pagePath.size() <= 1) + return -1; + + int parentPageNo = pagePath.get(pagePath.size() - 2); + InnerPage inner = innerOps.loadPage(parentPageNo); + + // Get the index of the pointer that points to this page. If it + // doesn't appear in the parent, we have a serious problem... + int pageIndex = inner.getIndexOfPointer(getPageNo()); + if (pageIndex == -1) { + throw new IllegalStateException(String.format( + "Inner node %d doesn't appear in parent inner node %d!", + getPageNo(), parentPageNo)); + } + + int rightSiblingIndex = pageIndex + 1; + int rightSiblingPageNo = -1; + + if (rightSiblingIndex < inner.getNumPointers()) + rightSiblingPageNo = inner.getPointer(rightSiblingIndex); + + return rightSiblingPageNo; + } + + + /** + * Returns the number of pointers currently stored in this inner page. The + * number of keys is always one less than the number of pointers, since + * each key must have a pointer on both sides. + * + * @return the number of pointers in this inner page. + */ + public int getNumPointers() { + return numPointers; + } + + + /** + * Returns the number of keys currently stored in this inner page. The + * number of keys is always one less than the number of pointers, since + * each key must have a pointer on both sides. + * + * @return the number of keys in this inner page. + * + * @throws IllegalStateException if the inner page contains 0 pointers + */ + public int getNumKeys() { + if (numPointers < 1) { + throw new IllegalStateException("Inner page contains no " + + "pointers. Number of keys is meaningless."); + } + + return numPointers - 1; + } + + + /** + * Returns the total amount of space used in this page, in bytes. + * + * @return the total amount of space used in this page, in bytes. + */ + public int getUsedSpace() { + return endOffset; + } + + + /** + * Returns the amount of space used by key/pointer entries in this page, + * in bytes. + * + * @return the amount of space used by key/pointer entries in this page, + * in bytes. + */ + public int getSpaceUsedByEntries() { + return endOffset - OFFSET_FIRST_POINTER; + } + + + /** + * Returns the amount of space available in this inner page, in bytes. + * + * @return the amount of space available in this inner page, in bytes. + */ + public int getFreeSpace() { + return dbPage.getPageSize() - endOffset; + } + + + /** + * Returns the total space (page size) in bytes. + * + * @return the size of the page, in bytes. + */ + public int getTotalSpace() { + return dbPage.getPageSize(); + } + + + /** + * Returns the pointer at the specified index. + * + * @param index the index of the pointer to retrieve + * + * @return the pointer at that index + */ + public int getPointer(int index) { + return dbPage.readUnsignedShort(pointerOffsets[index]); + } + + + /** + * Replaces one page-pointer in the inner page with another page-pointer. + * This is used when the B<sup>+</sup> tree is being optimized, as the + * layout of the data on disk is rearranged. + * + * @param index the index of the pointer to replace + * @param newPageNo the page number to store at the specified index + */ + public void replacePointer(int index, int newPageNo) { + dbPage.writeShort(pointerOffsets[index], newPageNo); + loadPageContents(); + } + + + /** + * Returns the key at the specified index. + * + * @param index the index of the key to retrieve + * + * @return the key at that index + */ + public BTreeFilePageTuple getKey(int index) { + return keys[index]; + } + + + /** + * This helper method scans the inner page for the specified page-pointer, + * returning the index of the pointer if it is found, or -1 if the pointer + * is not found. + * + * @param pointer the page-pointer to find in this inner page + * + * @return the index of the page-pointer if found, or -1 if not found + */ + public int getIndexOfPointer(int pointer) { + for (int i = 0; i < getNumPointers(); i++) { + if (getPointer(i) == pointer) + return i; + } + + return -1; + } + + + public void replaceTuple(int index, Tuple key) { + int oldStart = keys[index].getOffset(); + int oldLen = keys[index].getEndOffset() - oldStart; + + int newLen = PageTuple.getTupleStorageSize(schema, key); + + if (newLen != oldLen) { + // Need to adjust the amount of space the key takes. + + if (endOffset + newLen - oldLen > dbPage.getPageSize()) { + throw new IllegalArgumentException( + "New key-value is too large to fit in non-leaf page."); + } + + dbPage.moveDataRange(oldStart + oldLen, oldStart + newLen, + endOffset - oldStart - oldLen); + } + + PageTuple.storeTuple(dbPage, oldStart, schema, key); + + // Reload the page contents. + // TODO: This is slow, but it should be fine for now. + loadPageContents(); + } + + + /** + * This method inserts a new key and page-pointer into the inner page, + * immediately following the page-pointer {@code pagePtr1}, which must + * already appear within the page. The caller is expected to have already + * verified that the new key and page-pointer are able to fit in the page. + * + * @param pagePtr1 the page-pointer which should appear before the new key + * in the inner page. <b>This is required to already appear within + * the inner page.</b> + * + * @param key1 the new key to add to the inner page, immediately after the + * {@code pagePtr1} value. + * + * @param pagePtr2 the new page-pointer to add to the inner page, + * immediately after the {@code key1} value. + * + * @throws IllegalArgumentException if the specified {@code pagePtr1} value + * cannot be found in the inner page, or if the new key and + * page-pointer won't fit within the space available in the page. + */ + public void addEntry(int pagePtr1, Tuple key1, int pagePtr2) { + + if (logger.isTraceEnabled()) { + logger.trace("Non-leaf page " + getPageNo() + + " contents before adding entry:\n" + toFormattedString()); + } + + int i; + for (i = 0; i < numPointers; i++) { + if (getPointer(i) == pagePtr1) + break; + } + + logger.debug(String.format("Found page-pointer %d in index %d", + pagePtr1, i)); + + if (i == numPointers) { + throw new IllegalArgumentException( + "Can't find initial page-pointer " + pagePtr1 + + " in non-leaf page " + getPageNo()); + } + + // Figure out where to insert the new key and value. + + int oldKeyStart; + if (i < numPointers - 1) { + // There's a key i associated with pointer i. Use the key's offset, + // since it's after the pointer. + oldKeyStart = keys[i].getOffset(); + } + else { + // The pageNo1 pointer is the last pointer in the sequence. Use + // the end-offset of the data in the page. + oldKeyStart = endOffset; + } + int len = endOffset - oldKeyStart; + + // Compute the size of the new key and pointer, and make sure they fit + // into the page. + + int newKeySize = PageTuple.getTupleStorageSize(schema, key1); + int newEntrySize = newKeySize + 2; + if (endOffset + newEntrySize > dbPage.getPageSize()) { + throw new IllegalArgumentException("New key-value and " + + "page-pointer are too large to fit in non-leaf page."); + } + + if (len > 0) { + // Move the data after the pageNo1 pointer to make room for + // the new key and pointer. + dbPage.moveDataRange(oldKeyStart, oldKeyStart + newEntrySize, len); + } + + // Write in the new key/pointer values. + PageTuple.storeTuple(dbPage, oldKeyStart, schema, key1); + dbPage.writeShort(oldKeyStart + newKeySize, pagePtr2); + + // Finally, increment the number of pointers in the page, then reload + // the cached data. + + dbPage.writeShort(OFFSET_NUM_POINTERS, numPointers + 1); + + loadPageContents(); + + if (logger.isTraceEnabled()) { + logger.trace("Non-leaf page " + getPageNo() + + " contents after adding entry:\n" + toFormattedString()); + } + } + + + /** + * This function will delete a page-pointer from this inner page, along + * with the key either to the left or to the right of the pointer. It is + * up to the caller to determine whether the left key or the right key + * should be deleted. + * + * @param pagePtr the page-pointer value to identify and remove + * + * @param removeRightKey a flag specifying whether the key to the right + * ({@code true}) or to the left ({@code false}) should be removed + */ + public void deletePointer(int pagePtr, boolean removeRightKey) { + logger.debug("Trying to delete page-pointer " + pagePtr + + " from inner page " + getPageNo() + ", and remove the " + + (removeRightKey ? "right" : "left") + " key."); + + int ptrIndex = getIndexOfPointer(pagePtr); + if (ptrIndex == -1) { + throw new IllegalArgumentException(String.format( + "Can't find page-pointer %d in inner page %d", pagePtr, + dbPage.getPageNo())); + } + + if (ptrIndex == 0 && !removeRightKey) { + throw new IllegalArgumentException(String.format( + "Tried to delete page-pointer %d and the key to the left," + + " in inner page %d, but the pointer has no left key.", + pagePtr, dbPage.getPageNo())); + } + + if (ptrIndex == numPointers - 1 && removeRightKey) { + throw new IllegalArgumentException(String.format( + "Tried to delete page-pointer %d and the key to the right," + + " in inner page %d, but the pointer has no right key.", + pagePtr, dbPage.getPageNo())); + } + + // Figure out the range of data that must be deleted from the page. + + // Page pointers are 2 bytes. + int start = pointerOffsets[ptrIndex]; + int end = start + 2; + + // We must always remove one key, either the left or the right key. + // Expand the data range that we are removing. + if (removeRightKey) { + // Remove the key to the right of the page-pointer. + end = keys[ptrIndex].getEndOffset(); + + logger.debug(String.format("Removing right key, with size %d." + + " Range being removed is [%d, %d).", keys[ptrIndex].getSize(), + start, end)); + } + else { + // Remove the key to the left of the page-pointer. + start = keys[ptrIndex - 1].getOffset(); + + logger.debug(String.format("Removing left key, with size %d." + + " Range being removed is [%d, %d).", keys[ptrIndex - 1].getSize(), + start, end)); + } + + logger.debug("Moving inner-page data in range [" + end + ", " + + endOffset + ") over by " + (end - start) + " bytes"); + dbPage.moveDataRange(end, start, endOffset - end); + + // Decrement the total number of pointers. + dbPage.writeShort(OFFSET_NUM_POINTERS, numPointers - 1); + + logger.debug("Loading altered page - had " + numPointers + + " pointers before delete."); + // Load new page. + loadPageContents(); + + logger.debug("After loading, have " + numPointers + " pointers"); + } + + + /** + * <p> + * This helper function moves the specified number of page-pointers to the + * left sibling of this inner node. The data is copied in one shot so that + * the transfer will be fast, and the various associated bookkeeping values + * in both inner pages are updated. + * </p> + * <p> + * Of course, moving a subset of the page-pointers to a sibling will leave + * a key without a pointer on one side; this key is promoted up to the + * parent of the inner node. Additionally, an existing parent-key can be + * provided by the caller, which should be inserted before the new pointers + * being moved into the sibling node. + * </p> + * + * @param leftSibling the left sibling of this inner node in the index file + * + * @param count the number of pointers to move to the left sibling + * + * @param parentKey If this inner node and the sibling already have a parent + * node, this is the key between the two nodes' page-pointers in the + * parent node. If the two nodes don't have a parent (i.e. because + * an inner node is being split into two nodes and the depth of the + * tree is being increased) then this value will be {@code null}. + * + * @return the key that should go into the parent node, between the + * page-pointers for this node and its sibling + * + * @todo (Donnie) When support for deletion is added to the index + * implementation, we will need to support the case when the incoming + * {@code parentKey} is non-{@code null}, but the returned key is + * {@code null} because one of the two siblings' pointers will be + * removed. + */ + public TupleLiteral movePointersLeft(InnerPage leftSibling, int count, + Tuple parentKey) { + + if (count < 0 || count > numPointers) { + throw new IllegalArgumentException("count must be in range (0, " + + numPointers + "), got " + count); + } + + // The parent-key can be null if we are splitting a page into two pages. + // However, this situation is only valid if the right sibling is EMPTY. + int parentKeyLen = 0; + if (parentKey != null) { + parentKeyLen = PageTuple.getTupleStorageSize(schema, parentKey); + } + else { + if (leftSibling.getNumPointers() != 0) { + throw new IllegalStateException("Cannot move pointers to " + + "non-empty sibling if no parent-key is specified!"); + } + } + + /* TODO: IMPLEMENT THE REST OF THIS METHOD. + * + * You can use PageTuple.storeTuple() to write a key into a DBPage. + * + * The DBPage.write() method is useful for copying a large chunk of + * data from one DBPage to another. + * + * Your implementation also needs to properly handle the incoming + * parent-key, and produce a new parent-key as well. + */ + logger.error("NOT YET IMPLEMENTED: movePointersLeft()"); + + // Update the cached info for both non-leaf pages. + loadPageContents(); + leftSibling.loadPageContents(); + + return null; + } + + + /** + * Returns the page path to the right sibling, including the right sibling + * itself. Empty list if there is none. + * + * @param pagePath the page path from root to this leaf + * @param innerOps the inner page ops that allows this method to + * load inner pages and navigate the tree + * + * @return the page path to the right sibling leaf node + * + public List<Integer> getRightSibling(List<Integer> pagePath, + InnerPageOperations innerOps) { + // Verify that the last node in the page path is this leaf page. + if (pagePath.get(pagePath.size() - 1) != getPageNo()) { + throw new IllegalArgumentException("The page path provided does" + + " not terminate on this leaf page."); + } + + ArrayList<Integer> rightPath = new ArrayList<Integer>(); + + InnerPage inner = null; + int index = 0; + int i = pagePath.size() - 2; + try { + while (i >= 0) { + inner = innerOps.loadPage(idxFileInfo, pagePath.get(i)); + index = inner.getIndexOfPointer(pagePath.get(i+1)); + if (index != inner.getNumPointers() - 1) { + // This means that the subtree this leaf is in has a right + // sibling subtree from the current inner node. + rightPath.addAll(pagePath.subList(0, i+1)); + break; + } + i--; + } + + int nextPage; + if (inner == null || i == -1) { + return rightPath; + } + + // Add to the rightPath the page corresponding to one to the + // right of the current index + rightPath.add(inner.getPointer(index + 1)); + i++; + + while (i <= pagePath.size() - 2) { + index = 0; + nextPage = inner.getPointer(index); + rightPath.add(nextPage); + inner = innerOps.loadPage(idxFileInfo, nextPage); + i++; + } + + } catch (IOException e) { + throw new IllegalArgumentException("A page failed to load!"); + } + + // Can assert that for the last entry, leaf.getNextLeafPage + // should be last pagePath entry here + return rightPath; + } + + + /** + * Returns the page path to the left sibling, including the left sibling + * itself. Empty list if there is none. + * + * @param pagePath the page path from root to this leaf + * @param innerOps the inner page ops that allows this method + * to load inner pages and navigate the tree + * + * @return the page path to the left sibling leaf node + * + public List<Integer> getLeftSibling(List<Integer> pagePath, InnerPageOperations innerOps) { + // Verify that the last node in the page path is this leaf page. + if (pagePath.get(pagePath.size() - 1) != getPageNo()) { + throw new IllegalArgumentException("The page path provided does" + + " not terminate on this leaf page."); + } + + ArrayList<Integer> leftPath = new ArrayList<Integer>(); + // Note to self - not sure on behavior for initializing a for loop + // with an i that does not satisfy condition. If it doesn't do any + // iterations, then that would be ideal behavior. That case should + // never occur anyways... + InnerPage inner = null; + int index = 0; + int i = pagePath.size() - 2; + try { + while (i >= 0) { + inner = innerOps.loadPage(idxFileInfo, pagePath.get(i)); + index = inner.getIndexOfPointer(pagePath.get(i+1)); + if (index != 0) { + // This means that the subtree this leaf is in has a left + // sibling subtree from the current inner node. + leftPath.addAll(pagePath.subList(0, i+1)); + break; + } + i--; + } + + int nextPage; + if (inner == null || i == -1) { + return leftPath; + } + // Add to the leftPath the page corresponding to one to the + // left of the current index + leftPath.add(inner.getPointer(index - 1)); + i++; + + while (i <= pagePath.size() - 2) { + index = inner.getNumPointers() - 1; + nextPage = inner.getPointer(index); + leftPath.add(nextPage); + inner = innerOps.loadPage(idxFileInfo, nextPage); + i++; + } + } + catch (IOException e) { + throw new IllegalArgumentException("A page failed to load!"); + } + return leftPath; + } +*/ + + + /** + * <p> + * This helper function moves the specified number of page-pointers to the + * right sibling of this inner node. The data is copied in one shot so that + * the transfer will be fast, and the various associated bookkeeping values + * in both inner pages are updated. + * </p> + * <p> + * Of course, moving a subset of the page-pointers to a sibling will leave + * a key without a pointer on one side; this key is promoted up to the + * parent of the inner node. Additionally, an existing parent-key can be + * provided by the caller, which should be inserted before the new pointers + * being moved into the sibling node. + * </p> + * + * @param rightSibling the right sibling of this inner node in the index file + * + * @param count the number of pointers to move to the right sibling + * + * @param parentKey If this inner node and the sibling already have a parent + * node, this is the key between the two nodes' page-pointers in the + * parent node. If the two nodes don't have a parent (i.e. because + * an inner node is being split into two nodes and the depth of the + * tree is being increased) then this value will be {@code null}. + * + * @return the key that should go into the parent node, between the + * page-pointers for this node and its sibling + * + * @todo (Donnie) When support for deletion is added to the index + * implementation, we will need to support the case when the incoming + * {@code parentKey} is non-{@code null}, but the returned key is + * {@code null} because one of the two siblings' pointers will be + * removed. + */ + public TupleLiteral movePointersRight(InnerPage rightSibling, int count, + Tuple parentKey) { + + if (count < 0 || count > numPointers) { + throw new IllegalArgumentException("count must be in range [0, " + + numPointers + "), got " + count); + } + + if (logger.isTraceEnabled()) { + logger.trace("Non-leaf page " + getPageNo() + + " contents before moving pointers right:\n" + toFormattedString()); + } + + int startPointerIndex = numPointers - count; + int startOffset = pointerOffsets[startPointerIndex]; + int len = endOffset - startOffset; + + logger.debug("Moving everything after pointer " + startPointerIndex + + " to right sibling. Start offset = " + startOffset + + ", end offset = " + endOffset + ", len = " + len); + + // The parent-key can be null if we are splitting a page into two pages. + // However, this situation is only valid if the right sibling is EMPTY. + int parentKeyLen = 0; + if (parentKey != null) { + parentKeyLen = PageTuple.getTupleStorageSize(schema, parentKey); + } + else { + if (rightSibling.getNumPointers() != 0) { + throw new IllegalStateException("Cannot move pointers to " + + "non-empty sibling if no parent-key is specified!"); + } + } + + /* TODO: IMPLEMENT THE REST OF THIS METHOD. + * + * You can use PageTuple.storeTuple() to write a key into a DBPage. + * + * The DBPage.write() method is useful for copying a large chunk of + * data from one DBPage to another. + * + * Your implementation also needs to properly handle the incoming + * parent-key, and produce a new parent-key as well. + */ + logger.error("NOT YET IMPLEMENTED: movePointersRight()"); + + // Update the cached info for both non-leaf pages. + loadPageContents(); + rightSibling.loadPageContents(); + + if (logger.isTraceEnabled()) { + logger.trace("Non-leaf page " + getPageNo() + + " contents after moving pointers right:\n" + toFormattedString()); + + logger.trace("Right-sibling page " + rightSibling.getPageNo() + + " contents after moving pointers right:\n" + + rightSibling.toFormattedString()); + } + + return null; + } + + + /** + * <p> + * This helper method creates a formatted string containing the contents of + * the inner page, including the pointers and the intervening keys. + * </p> + * <p> + * It is strongly suggested that this method should only be used for + * trace-level output, since otherwise the output will become overwhelming. + * </p> + * + * @return a formatted string containing the contents of the inner page + */ + public String toFormattedString() { + StringBuilder buf = new StringBuilder(); + + buf.append(String.format("Inner page %d contains %d pointers%n", + getPageNo(), numPointers)); + + if (numPointers > 0) { + for (int i = 0; i < numPointers - 1; i++) { + buf.append(String.format(" Pointer %d = page %d%n", i, + getPointer(i))); + buf.append(String.format(" Key %d = %s%n", i, getKey(i))); + } + buf.append(String.format(" Pointer %d = page %d%n", numPointers - 1, + getPointer(numPointers - 1))); + } + + return buf.toString(); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/InnerPageOperations.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/InnerPageOperations.java new file mode 100644 index 0000000000000000000000000000000000000000..c90d6fdc7973943cda290dd4f67705a8213cca5c --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/InnerPageOperations.java @@ -0,0 +1,968 @@ +package edu.caltech.nanodb.storage.btreefile; + + +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +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.PageTuple; +import edu.caltech.nanodb.storage.StorageManager; + + +/** + * This class provides high-level B<sup>+</sup> tree management operations + * performed on inner nodes. These operations are provided here and not on the + * {@link InnerPage} class since they sometimes involve splitting or merging + * inner nodes, updating parent nodes, and so forth. + */ +public class InnerPageOperations { + + /** + * When deleting a page-pointer from an inner page, either the left or + * right key must also be removed; this constant specifies removal of the + * left key. + * + * @see #deletePointer + */ + public static final int REMOVE_KEY_TO_LEFT = 0; + + + /** + * When deleting a page-pointer from an inner page, either the left or + * right key must also be removed; this constant specifies removal of the + * right key. + * + * @see #deletePointer + */ + public static final int REMOVE_KEY_TO_RIGHT = 1; + + + /** A logging object for reporting anything interesting that happens. */ + private static Logger logger = LogManager.getLogger(InnerPageOperations.class); + + + private StorageManager storageManager; + + + private BTreeTupleFile tupleFile; + + + private FileOperations fileOps; + + + public InnerPageOperations(StorageManager storageManager, + BTreeTupleFile tupleFile, + FileOperations fileOps) { + this.storageManager = storageManager; + this.tupleFile = tupleFile; + this.fileOps = fileOps; + } + + + public InnerPage loadPage(int pageNo) { + DBFile dbFile = tupleFile.getDBFile(); + DBPage dbPage = storageManager.loadDBPage(dbFile, pageNo); + return new InnerPage(dbPage, tupleFile.getSchema()); + } + + + /** + * This helper function is used to update a key between two existing + * pointers in an inner B<sup>+</sup> tree node. It is an error if the + * specified pair of pointers cannot be found in the node. + * + * @param page the inner page to update the key in + * @param pagePath the path to the page, from the root node + * @param pagePtr1 the pointer P<sub>i</sub> before the key to update + * @param key1 the new value of the key K<sub>i</sub> to store + * @param pagePtr2 the pointer P<sub>i+1</sub> after the key to update + * + * @todo (Donnie) This implementation has a major failing that will occur + * infrequently - if the inner page doesn't have room for the new key + * (e.g. if the page was already almost full, and then the new key is + * larger than the old key) then the inner page needs to be split, + * per usual. Right now it will just throw an exception in this case. + * This is why the {@code pagePath} argument is provided, so that when + * this bug is fixed, the page-path will be available. + */ + public void replaceTuple(InnerPage page, List<Integer> pagePath, + int pagePtr1, Tuple key1, int pagePtr2) { + + for (int i = 0; i < page.getNumPointers() - 1; i++) { + if (page.getPointer(i) == pagePtr1 && + page.getPointer(i + 1) == pagePtr2) { + + // Found the pair of pointers! Replace the key-value. + + BTreeFilePageTuple oldKey = page.getKey(i); + int oldKeySize = oldKey.getSize(); + + int newKeySize = + PageTuple.getTupleStorageSize(tupleFile.getSchema(), key1); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Inner page %d: replacing " + + "old key of %d bytes (between pointers %d and %d) " + + "with new key of %d bytes", page.getPageNo(), + oldKeySize, pagePtr1, pagePtr2, newKeySize)); + } + + if (logger.isTraceEnabled()) { + logger.trace("Contents of inner page " + page.getPageNo() + + " before replacement:\n" + page.toFormattedString()); + } + + if (page.getFreeSpace() + oldKeySize - newKeySize >= 0) { + // We have room - go ahead and do this. + page.replaceTuple(i, key1); + + // Make sure we didn't cause any brain damage... + assert page.getPointer(i) == pagePtr1; + assert page.getPointer(i + 1) == pagePtr2; + } + else { + // We need to make more room in this inner page, either by + // relocating records or by splitting this page. We will + // do this by deleting the old entry, and then adding the + // new entry, so that we can leverage all our good code + // for splitting/relocating/etc. + + logger.info(String.format("Not enough space in page %d;" + + " trying to relocate entries / split page.", + page.getPageNo())); + + // Delete pagePtr2, and the key to the LEFT of it. + page.deletePointer(pagePtr2, /* removeRightKey */ false); + + // Now add new key, and put pagePtr2 back after pagePtr1. + addTuple(page, pagePath, pagePtr1, key1, pagePtr2); + } + + if (logger.isTraceEnabled()) { + logger.trace("Contents of inner page " + page.getPageNo() + + " after replacement:\n" + page.toFormattedString()); + } + + return; + } + } + + // If we got here, we have a big problem. Couldn't find the expected + // pair of pointers we were handed. + + // Dump the page contents because presumably we want to figure out + // what is going on... + logger.error(String.format( + "Couldn't find pair of pointers %d and %d in inner page %d!", + pagePtr1, pagePtr2, page.getPageNo())); + logger.error("Page contents:\n" + page.toFormattedString()); + + throw new IllegalStateException( + "Couldn't find sequence of page-pointers [" + pagePtr1 + ", " + + pagePtr2 + "] in non-leaf page " + page.getPageNo()); + } + + + /** + * This helper function determines how many pointers must be relocated from + * one inner page to another, in order to free up the specified number of + * bytes. If it is possible, the number of pointers that must be relocated + * is returned. If it is not possible, the method returns 0. + * + * @param page the inner page to relocate entries from + * + * @param adjPage the adjacent page (predecessor or successor) to relocate + * entries to + * + * @param movingRight pass {@code true} if the sibling is to the right of + * {@code page} (and therefore we are moving entries right), or + * {@code false} if the sibling is to the left of {@code page} (and + * therefore we are moving entries left). + * + * @param bytesRequired the number of bytes that must be freed up in + * {@code page} by the operation + * + * @param parentKeySize the size of the parent key that must also be + * relocated into the adjacent page, and therefore affects how many + * pointers can be transferred + * + * @return the number of pointers that must be relocated to free up the + * required space, or 0 if it is not possible. + */ + private int tryNonLeafRelocateForSpace(InnerPage page, InnerPage adjPage, + boolean movingRight, int bytesRequired, int parentKeySize) { + + int numKeys = page.getNumKeys(); + int pageBytesFree = page.getFreeSpace(); + int adjBytesFree = adjPage.getFreeSpace(); + + logger.debug(String.format("Trying to relocate records from inner-" + + "page %d (%d bytes free) to adjacent inner-page %d (%d bytes " + + "free), moving %s, to free up %d bytes. Parent key size is %d " + + "bytes.", page.getPageNo(), pageBytesFree, adjPage.getPageNo(), + adjBytesFree, (movingRight ? "right" : "left"), bytesRequired, + parentKeySize)); + + // The parent key always has to move to the adjacent page, so if that + // won't fit, don't even try. + if (adjBytesFree < parentKeySize) { + logger.debug(String.format("Adjacent page %d has %d bytes free;" + + " not enough to hold %d-byte parent key. Giving up on " + + "relocation.", adjPage.getPageNo(), adjBytesFree, parentKeySize)); + return 0; + } + + // Since the parent key must always be rotated down into the adjacent + // inner page, we must account for that space. + adjBytesFree -= parentKeySize; + + int keyBytesMoved = 0; + + int numRelocated = 0; + while (true) { + // Figure out the index of the key we need the size of, based on + // the direction we are moving entries, and how many we have + // already moved. If we are moving entries right, we must start + // with the rightmost entry in page. If we are moving entries + // left, we must start with the leftmost entry in page. + int index; + if (movingRight) + index = numKeys - numRelocated - 1; + else + index = numRelocated; + + // Add two bytes to the key size for the page-pointer that follows + int entrySize = page.getKey(index).getSize() + 2; + logger.debug("Entry " + index + " is " + entrySize + " bytes"); + + // Did we run out of space to move entries before we hit our goal? + if (adjBytesFree < entrySize) { + numRelocated = 0; + break; + } + + numRelocated++; + + pageBytesFree += entrySize; + adjBytesFree -= entrySize; + + // Since we don't yet know which page the new pointer will go into, + // stop when we can put the pointer in either page. + if (pageBytesFree >= bytesRequired && + adjBytesFree >= bytesRequired) { + break; + } + } + + assert numRelocated >= 0; + return numRelocated; + } + + + /** + * This helper function adds an entry (a key and associated pointer) to + * this inner page, after the page-pointer {@code pagePtr1}. + * + * @param page the inner page to add the entry to + * + * @param pagePath the path of page-numbers to this inner page + * + * @param pagePtr1 the <u>existing</u> page that the new key and next-page + * number will be inserted after + * + * @param key1 the new key-value to insert after the {@code pagePtr1} value + * + * @param pagePtr2 the new page-pointer value to follow the {@code key1} + * value + */ + public void addTuple(InnerPage page, List<Integer> pagePath, + int pagePtr1, Tuple key1, int pagePtr2) { + + // The new entry will be the key, plus 2 bytes for the page-pointer. + int newEntrySize = + PageTuple.getTupleStorageSize(tupleFile.getSchema(), key1) + 2; + + logger.debug(String.format("Adding new %d-byte entry to inner page %d", + newEntrySize, page.getPageNo())); + + if (page.getFreeSpace() < newEntrySize) { + logger.debug("Not enough room in inner page " + page.getPageNo() + + "; trying to relocate entries to make room"); + + // Try to relocate entries from this inner page to either sibling, + // or if that can't happen, split the inner page into two. + if (!relocatePointersAndAddKey(page, pagePath, + pagePtr1, key1, pagePtr2, newEntrySize)) { + logger.debug("Couldn't relocate enough entries to make room;" + + " splitting page " + page.getPageNo() + " instead"); + splitAndAddKey(page, pagePath, pagePtr1, key1, pagePtr2); + } + } + else { + // There is room in the leaf for the new key. Add it there. + page.addEntry(pagePtr1, key1, pagePtr2); + } + + } + + + /** + * This function will delete the specified Key/Pointer pair from the + * passed-in inner page. + * + * @param page the inner page to delete the key/pointer from + * + * @param pagePath the page path to the passed page + * + * @param pagePtr the page-pointer value to identify and remove + * + * @param removeRightKey a flag specifying whether the key to the right + * ({@code true}) or to the left ({@code false}) should be removed + */ + public void deletePointer(InnerPage page, List<Integer> pagePath, + int pagePtr, boolean removeRightKey) { + + page.deletePointer(pagePtr, removeRightKey); + + if (page.getUsedSpace() >= page.getTotalSpace() / 2) { + // The page is at least half-full. Don't need to redistribute or + // coalesce. + return; + } + else if (pagePath.size() == 1) { + // The page is the root. Don't need to redistribute or coalesce, + // but if the root is now empty, need to shorten the tree depth. + + if (page.getNumKeys() == 0) { + logger.debug(String.format("Current root page %d is now " + + "empty, removing.", page.getPageNo())); + + // Set the index's new root page. + DBPage dbpHeader = + storageManager.loadDBPage(tupleFile.getDBFile(), 0); + HeaderPage.setRootPageNo(dbpHeader, page.getPointer(0)); + + // Free up this page in the index. + fileOps.releaseDataPage(page.getDBPage()); + } + return; + } + + // If we got to this part, we have to redistribute/coalesce stuff :( + + // Note: Assumed that at least one of the siblings has the same + // immediate parent. If that is not the case... we're doomed... + // TODO: VERIFY THIS AND THROW AN EXCEPTION IF NOT + + int pageNo = page.getPageNo(); + + int leftPageNo = page.getLeftSibling(pagePath, this); + int rightPageNo = page.getRightSibling(pagePath, this); + + if (leftPageNo == -1 && rightPageNo == -1) { + // We should never get to this point, since the earlier test + // should have caught this situation. + throw new IllegalStateException(String.format( + "Inner node %d doesn't have a left or right sibling!", + page.getPageNo())); + } + + // Now we know that at least one sibling is present. Load both + // siblings and coalesce/redistribute in the direction that makes + // the most sense... + + InnerPage leftSibling = null; + if (leftPageNo != -1) + leftSibling = loadPage(leftPageNo); + + InnerPage rightSibling = null; + if (rightPageNo != -1) + rightSibling = loadPage(rightPageNo); + + // Relocating or coalescing entries requires updating the parent node. + // Since the current node has a sibling, it must also have a parent. + + int parentPageNo = pagePath.get(pagePath.size() - 2); + + InnerPage parentPage = loadPage(parentPageNo); + // int numPointers = parentPage.getNumPointers(); + int indexInParentPage = parentPage.getIndexOfPointer(page.getPageNo()); + + // See if we can coalesce the node into its left or right sibling. + // When we do the check, we must not forget that each node contains a + // header, and we need to account for that space as well. This header + // space is included in the getUsedSpace() method, but is excluded by + // the getSpaceUsedByTuples() method. + + // TODO: SEE IF WE CAN SIMPLIFY THIS AT ALL... + if (leftSibling != null && + leftSibling.getUsedSpace() + page.getSpaceUsedByEntries() < + leftSibling.getTotalSpace()) { + + // Coalesce the current node into the left sibling. + logger.debug("Delete from inner page " + pageNo + + ": coalescing with left sibling page."); + + logger.debug(String.format("Before coalesce-left, page has %d " + + "pointers and left sibling has %d pointers.", + page.getNumPointers(), leftSibling.getNumPointers())); + + // The affected key in the parent page is to the left of this + // page's index in the parent page, since we are moving entries + // to the left sibling. + Tuple parentKey = parentPage.getKey(indexInParentPage - 1); + + // We don't care about the key returned by movePointersLeft(), + // since we are deleting the parent key anyway. + page.movePointersLeft(leftSibling, page.getNumPointers(), parentKey); + + logger.debug(String.format("After coalesce-left, page has %d " + + "pointers and left sibling has %d pointers.", + page.getNumPointers(), leftSibling.getNumPointers())); + + // Free up the page since it's empty now + fileOps.releaseDataPage(page.getDBPage()); + page = null; + + List<Integer> parentPagePath = pagePath.subList(0, pagePath.size() - 1); + deletePointer(parentPage, parentPagePath, pageNo, + /* delete right key */ false); + } + else if (rightSibling != null && + rightSibling.getUsedSpace() + page.getSpaceUsedByEntries() < + rightSibling.getTotalSpace()) { + + // Coalesce the current node into the right sibling. + logger.debug("Delete from leaf " + pageNo + + ": coalescing with right sibling leaf."); + + logger.debug(String.format("Before coalesce-right, page has %d " + + "keys and right sibling has %d pointers.", + page.getNumPointers(), rightSibling.getNumPointers())); + + // The affected key in the parent page is to the right of this + // page's index in the parent page, since we are moving entries + // to the right sibling. + Tuple parentKey = parentPage.getKey(indexInParentPage); + + // We don't care about the key returned by movePointersRight(), + // since we are deleting the parent key anyway. + page.movePointersRight(rightSibling, page.getNumPointers(), parentKey); + + logger.debug(String.format("After coalesce-right, page has %d " + + "entries and right sibling has %d pointers.", + page.getNumPointers(), rightSibling.getNumPointers())); + + // Free up the right page since it's empty now + fileOps.releaseDataPage(page.getDBPage()); + page = null; + + List<Integer> parentPagePath = pagePath.subList(0, pagePath.size() - 1); + deletePointer(parentPage, parentPagePath, pageNo, + /* delete right key */ true); + } + else { + // Can't coalesce the leaf node into either sibling. Redistribute + // entries from left or right sibling into the leaf. The strategy + // is as follows: + + // If the node has both left and right siblings, redistribute from + // the fuller sibling. Otherwise, just redistribute from + // whichever sibling we have. + + InnerPage adjPage = null; + if (leftSibling != null && rightSibling != null) { + // Both siblings are present. Choose the fuller one to + // relocate from. + if (leftSibling.getUsedSpace() > rightSibling.getUsedSpace()) + adjPage = leftSibling; + else + adjPage = rightSibling; + } + else if (leftSibling != null) { + // There is no right sibling. Use the left sibling. + adjPage = leftSibling; + } + else { + // There is no left sibling. Use the right sibling. + adjPage = rightSibling; + } + + PageTuple parentKey; + + if (adjPage == leftSibling) + parentKey = parentPage.getKey(indexInParentPage - 1); + else // adjPage == right sibling + parentKey = parentPage.getKey(indexInParentPage); + + int entriesToMove = tryNonLeafRelocateToFill(page, adjPage, + /* movingRight */ adjPage == leftSibling, parentKey.getSize()); + + if (entriesToMove == 0) { + // We really tried to satisfy the "minimum size" requirement, + // but we just couldn't. Log it and return. + + StringBuilder buf = new StringBuilder(); + + buf.append(String.format("Couldn't relocate pointers to" + + " satisfy minimum space requirement in leaf-page %d" + + " with %d entries!\n", pageNo, page.getNumPointers())); + + if (leftSibling != null) { + buf.append(String.format("\t- Left sibling page %d has " + + "%d pointers\n", leftSibling.getPageNo(), + leftSibling.getNumPointers())); + } + else { + buf.append("\t- No left sibling\n"); + } + + if (rightSibling != null) { + buf.append(String.format("\t- Right sibling page %d has " + + "%d pointers", rightSibling.getPageNo(), + rightSibling.getNumPointers())); + } + else { + buf.append("\t- No right sibling"); + } + + logger.warn(buf); + + return; + } + + logger.debug(String.format("Relocating %d pointers into page " + + "%d from %s sibling page %d", entriesToMove, pageNo, + (adjPage == leftSibling ? "left" : "right"), adjPage.getPageNo())); + + if (adjPage == leftSibling) { + adjPage.movePointersRight(page, entriesToMove, parentKey); + parentPage.replaceTuple(indexInParentPage - 1, page.getKey(0)); + } + else { // adjPage == right sibling + adjPage.movePointersLeft(page, entriesToMove, parentKey); + parentPage.replaceTuple(indexInParentPage, adjPage.getKey(0)); + } + } + } + + + private boolean relocatePointersAndAddKey(InnerPage page, + List<Integer> pagePath, int pagePtr1, Tuple key1, int pagePtr2, + int newEntrySize) { + + int pathSize = pagePath.size(); + if (pagePath.get(pathSize - 1) != page.getPageNo()) { + throw new IllegalArgumentException( + "Inner page number doesn't match last page-number in page path"); + } + + // See if we are able to relocate records either direction to free up + // space for the new key. + + if (pathSize == 1) // This node is also the root - no parent. + return false; // There aren't any siblings to relocate to. + + int parentPageNo = pagePath.get(pathSize - 2); + InnerPage parentPage = loadPage(parentPageNo); + + logger.debug(String.format("Parent of inner-page %d is page %d.", + page.getPageNo(), parentPageNo)); + + if (logger.isTraceEnabled()) { + logger.trace("Parent page contents:\n" + + parentPage.toFormattedString()); + } + + int numPointers = parentPage.getNumPointers(); + int pagePtrIndex = parentPage.getIndexOfPointer(page.getPageNo()); + + // Check each sibling in its own code block so that we can constrain + // the scopes of the variables a bit. This keeps us from accidentally + // reusing the "prev" variables in the "next" section. + + { + InnerPage prevPage = null; + if (pagePtrIndex - 1 >= 0) + prevPage = loadPage(parentPage.getPointer(pagePtrIndex - 1)); + + if (prevPage != null) { + // See if we can move some of this inner node's entries to the + // previous node, to free up space. + + BTreeFilePageTuple parentKey = parentPage.getKey(pagePtrIndex - 1); + int parentKeySize = parentKey.getSize(); + + int count = tryNonLeafRelocateForSpace(page, prevPage, false, + newEntrySize, parentKeySize); + + if (count > 0) { + // Yes, we can do it! + + logger.debug(String.format("Relocating %d entries from " + + "inner-page %d to left-sibling inner-page %d", count, + page.getPageNo(), prevPage.getPageNo())); + + logger.debug("Space before relocation: Inner = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + prevPage.getFreeSpace() + " bytes"); + + TupleLiteral newParentKey = + page.movePointersLeft(prevPage, count, parentKey); + + // Even with relocating entries, this could fail. + if (!addEntryToInnerPair(prevPage, page, pagePtr1, key1, pagePtr2)) + return false; + + logger.debug("New parent-key is " + newParentKey); + + pagePath.remove(pathSize - 1); + replaceTuple(parentPage, pagePath, prevPage.getPageNo(), + newParentKey, page.getPageNo()); + + logger.debug("Space after relocation: Inner = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + prevPage.getFreeSpace() + " bytes"); + + return true; + } + } + } + + { + InnerPage nextPage = null; + if (pagePtrIndex + 1 < numPointers) + nextPage = loadPage(parentPage.getPointer(pagePtrIndex + 1)); + + if (nextPage != null) { + // See if we can move some of this inner node's entries to the + // previous node, to free up space. + + BTreeFilePageTuple parentKey = parentPage.getKey(pagePtrIndex); + int parentKeySize = parentKey.getSize(); + + int count = tryNonLeafRelocateForSpace(page, nextPage, true, + newEntrySize, parentKeySize); + + if (count > 0) { + // Yes, we can do it! + + logger.debug(String.format("Relocating %d entries from " + + "inner-page %d to right-sibling inner-page %d", count, + page.getPageNo(), nextPage.getPageNo())); + + logger.debug("Space before relocation: Inner = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + nextPage.getFreeSpace() + " bytes"); + + TupleLiteral newParentKey = + page.movePointersRight(nextPage, count, parentKey); + + // Even with relocating entries, this could fail. + if (!addEntryToInnerPair(page, nextPage, pagePtr1, key1, pagePtr2)) + return false; + + logger.debug("New parent-key is " + newParentKey); + + pagePath.remove(pathSize - 1); + replaceTuple(parentPage, pagePath, page.getPageNo(), + newParentKey, nextPage.getPageNo()); + + logger.debug("Space after relocation: Inner = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + nextPage.getFreeSpace() + " bytes"); + + return true; + } + } + } + + // Couldn't relocate entries to either the previous or next page. We + // must split the leaf into two. + return false; + } + + + /** + * <p> + * This helper function splits the specified inner page into two pages, + * also updating the parent page in the process, and then inserts the + * specified key and page-pointer into the appropriate inner page. This + * method is used to add a key/pointer to an inner page that doesn't have + * enough space, when it isn't possible to relocate pointers to the left + * or right sibling of the page. + * </p> + * <p> + * When the inner node is split, half of the pointers are put into the new + * sibling, regardless of the size of the keys involved. In other words, + * this method doesn't try to keep the pages half-full based on bytes used. + * </p> + * + * @param page the inner node to split and then add the key/pointer to + * + * @param pagePath the sequence of page-numbers traversed to reach this + * inner node. + * + * @param pagePtr1 the existing page-pointer after which the new key and + * pointer should be inserted + * + * @param key1 the new key to insert into the inner page, immediately after + * the page-pointer value {@code pagePtr1}. + * + * @param pagePtr2 the new page-pointer value to insert after the new key + * value + */ + private void splitAndAddKey(InnerPage page, List<Integer> pagePath, + int pagePtr1, Tuple key1, int pagePtr2) { + + int pathSize = pagePath.size(); + if (pagePath.get(pathSize - 1) != page.getPageNo()) { + throw new IllegalArgumentException( + "Inner page number doesn't match last page-number in page path"); + } + + if (logger.isTraceEnabled()) { + logger.trace("Initial contents of inner page " + page.getPageNo() + + ":\n" + page.toFormattedString()); + } + + logger.debug("Splitting inner-page " + page.getPageNo() + + " into two inner pages."); + + // Get a new blank page in the index, with the same parent as the + // inner-page we were handed. + + DBPage newDBPage = fileOps.getNewDataPage(); + InnerPage newPage = InnerPage.init(newDBPage, tupleFile.getSchema()); + + // Figure out how many values we want to move from the old page to the + // new page. + + int numPointers = page.getNumPointers(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Relocating %d pointers from left-page %d" + + " to right-page %d", numPointers, page.getPageNo(), newPage.getPageNo())); + logger.debug(" Old left # of pointers: " + page.getNumPointers()); + logger.debug(" Old right # of pointers: " + newPage.getNumPointers()); + } + + Tuple parentKey = null; + InnerPage parentPage = null; + + int parentPageNo = 0; + if (pathSize > 1) + parentPageNo = pagePath.get(pathSize - 2); + + if (parentPageNo != 0) { + parentPage = loadPage(parentPageNo); + int parentPtrIndex = parentPage.getIndexOfPointer(page.getPageNo()); + if (parentPtrIndex < parentPage.getNumPointers() - 1) + parentKey = parentPage.getKey(parentPtrIndex); + } + Tuple newParentKey = + page.movePointersRight(newPage, numPointers / 2, parentKey); + + if (logger.isDebugEnabled()) { + logger.debug(" New parent key: " + newParentKey); + logger.debug(" New left # of pointers: " + page.getNumPointers()); + logger.debug(" New right # of pointers: " + newPage.getNumPointers()); + } + + if (logger.isTraceEnabled()) { + logger.trace("Final contents of inner page " + page.getPageNo() + + ":\n" + page.toFormattedString()); + + logger.trace("Final contents of new inner page " + + newPage.getPageNo() + ":\n" + newPage.toFormattedString()); + } + + if (!addEntryToInnerPair(page, newPage, pagePtr1, key1, pagePtr2)) { + // This is unexpected, but we had better report it if it happens. + throw new IllegalStateException("UNEXPECTED: Couldn't add " + + "entry to half-full inner page!"); + } + + // If the current node doesn't have a parent, it's because it's + // currently the root. + if (parentPageNo == 0) { + // Create a new root node and set both leaves to have it as their + // parent. + DBPage dbpParent = fileOps.getNewDataPage(); + parentPage = InnerPage.init(dbpParent, tupleFile.getSchema(), + page.getPageNo(), newParentKey, newPage.getPageNo()); + + parentPageNo = parentPage.getPageNo(); + + // We have a new root-page in the index! + DBFile dbFile = tupleFile.getDBFile(); + DBPage dbpHeader = storageManager.loadDBPage(dbFile, 0); + HeaderPage.setRootPageNo(dbpHeader, parentPageNo); + + logger.debug("Set index root-page to inner-page " + parentPageNo); + } + else { + // Add the new page into the parent non-leaf node. (This may cause + // the parent node's contents to be moved or split, if the parent + // is full.) + + // (We already set the new node's parent-page-number earlier.) + + pagePath.remove(pathSize - 1); + addTuple(parentPage, pagePath, page.getPageNo(), newParentKey, + newPage.getPageNo()); + + logger.debug("Parent page " + parentPageNo + " now has " + + parentPage.getNumPointers() + " page-pointers."); + } + + if (logger.isTraceEnabled()) { + logger.trace("Parent page contents:\n" + + parentPage.toFormattedString()); + } + } + + + /** + * This helper method takes a pair of inner nodes that are siblings to each + * other, and adds the specified key to whichever node the key should go + * into. + * + * @param prevPage the first page in the pair, left sibling of + * {@code nextPage} + * + * @param nextPage the second page in the pair, right sibling of + * {@code prevPage} + * + * @param pageNo1 the pointer to the left of the new key/pointer values that + * will be added to one of the pages + * + * @param key1 the new key-value to insert immediately after the existing + * {@code pageNo1} value + * + * @param pageNo2 the new pointer-value to insert immediately after the new + * {@code key1} value + * + * @return true if the entry was able to be added to one of the pages, or + * false if the entry couldn't be added. + */ + private boolean addEntryToInnerPair(InnerPage prevPage, InnerPage nextPage, + int pageNo1, Tuple key1, int pageNo2) { + InnerPage page; + + // See if pageNo1 appears in the left page. + int ptrIndex1 = prevPage.getIndexOfPointer(pageNo1); + if (ptrIndex1 != -1) { + page = prevPage; + } + else { + // The pointer *should be* in the next page. Verify this... + page = nextPage; + + if (nextPage.getIndexOfPointer(pageNo1) == -1) { + throw new IllegalStateException(String.format( + "Somehow lost page-pointer %d from inner pages %d and %d", + pageNo1, prevPage.getPageNo(), nextPage.getPageNo())); + } + } + + int entrySize = 2 + + PageTuple.getTupleStorageSize(tupleFile.getSchema(), key1); + + if (page.getFreeSpace() >= entrySize) { + page.addEntry(pageNo1, key1, pageNo2); + return true; + } + else { + return false; + } + } + + + /** + * This helper function determines how many entries must be relocated from + * one leaf-page to another, in order to satisfy the "minimum space" + * requirement of the B tree.free up the specified number of + * bytes. If it is possible, the number of entries that must be relocated + * is returned. If it is not possible, the method returns 0. + * + * @param page the inner node to relocate entries from + * + * @param adjPage the adjacent inner page (predecessor or successor) to + * relocate entries to + * + * @param movingRight pass {@code true} if the sibling is to the left of + * {@code page} (and therefore we are moving entries right), or + * {@code false} if the sibling is to the right of {@code page} + * (and therefore we are moving entries left). + * + * @return the number of entries that must be relocated to fill the node + * to a minimal level, or 0 if not possible. + */ + private int tryNonLeafRelocateToFill(InnerPage page, InnerPage adjPage, + boolean movingRight, int parentKeySize) { + + int adjKeys = adjPage.getNumKeys(); + int pageBytesFree = page.getFreeSpace(); + int adjBytesFree = adjPage.getFreeSpace(); + + // Should be the same for both page and adjPage. + int halfFull = page.getTotalSpace() / 2; + + // The parent key always has to move into this page, so if that + // won't fit, don't even try. + if (pageBytesFree < parentKeySize) + return 0; + + pageBytesFree -= parentKeySize; + + int keyBytesMoved = 0; + int lastKeySize = parentKeySize; + + int numRelocated = 0; + while (true) { + // If the key we wanted to move into this page overflows the free + // space in this page, back it up. + // TODO: IS THIS NECESSARY? + if (pageBytesFree < keyBytesMoved + 2 * numRelocated) { + numRelocated--; + break; + } + + // Figure out the index of the key we need the size of, based on the + // direction we are moving values. If we are moving values right, + // we need to look at the keys starting at the rightmost one. If we + // are moving values left, we need to start with the leftmost key. + int index; + if (movingRight) + index = adjKeys - numRelocated - 1; + else + index = numRelocated; + + keyBytesMoved += lastKeySize; + + lastKeySize = adjPage.getKey(index).getSize(); + logger.debug("Key " + index + " is " + lastKeySize + " bytes"); + + numRelocated++; + + // Since we don't yet know which page the new pointer will go into, + // stop when we can put the pointer in either page. + if (adjBytesFree <= halfFull && + (pageBytesFree + keyBytesMoved + 2 * numRelocated) <= halfFull) { + break; + } + } + + logger.debug("Can relocate " + numRelocated + + " keys to satisfy minimum space requirements."); + + assert numRelocated >= 0; + return numRelocated; + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/LeafPage.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/LeafPage.java new file mode 100644 index 0000000000000000000000000000000000000000..8b892d506847982f2cb4924c12aed04651a8dd8b --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/LeafPage.java @@ -0,0 +1,704 @@ +package edu.caltech.nanodb.storage.btreefile; + + +import java.util.ArrayList; +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.Schema; +import edu.caltech.nanodb.relations.Tuple; +import edu.caltech.nanodb.storage.DBPage; +import edu.caltech.nanodb.storage.PageTuple; + +import static edu.caltech.nanodb.storage.btreefile.BTreePageTypes.*; + + +/** + * <p> + * This class wraps a {@link DBPage} object that is a leaf page in the + * B<sup>+</sup> tree file implementation, to provide some of the basic + * leaf-management operations necessary for the file structure. + * </p> + * <p> + * Operations involving individual inner-pages are provided by the + * {@link InnerPage} wrapper-class. Higher-level operations involving + * multiple leaves and/or inner pages of the B<sup>+</sup> tree structure, + * are provided by the {@link LeafPageOperations} and + * {@link InnerPageOperations} classes. + * </p> + */ +public class LeafPage implements DataPage { + /** A logging object for reporting anything interesting that happens. */ + private static Logger logger = LogManager.getLogger(LeafPage.class); + + + /** + * The offset where the next-sibling page number is stored in this page. + * The only leaf page that doesn't have a next sibling is the last leaf + * in the file; its "next page" value will be set to 0. + */ + public static final int OFFSET_NEXT_PAGE_NO = 1; + + + /** + * The offset where the number of tuples is stored in the page. + */ + public static final int OFFSET_NUM_TUPLES = 3; + + + /** The offset of the first tuple in the leaf page. */ + public static final int OFFSET_FIRST_TUPLE = 5; + + + /** The actual data page that holds the B<sup>+</sup> tree leaf node. */ + private DBPage dbPage; + + + /** The schema of the tuples in the leaf page. */ + private Schema schema; + + + /** The number of tuples stored within this leaf page. */ + private int numTuples; + + + /** A list of the tuples stored in this leaf page. */ + private ArrayList<BTreeFilePageTuple> tuples; + + + /** + * The total size of all data (tuples + initial values) stored within this + * leaf page. This is also the offset at which we can start writing more + * data without overwriting anything. + */ + private int endOffset; + + + /** + * Initialize the leaf-page wrapper class for the specified B<sup>+</sup> + * tree leaf page. The contents of the leaf-page are cached in the fields + * of the wrapper object. + * + * @param dbPage the data page from the B<sup>+</sup> Tree file to wrap + * @param schema the schema of tuples stored in the data page + */ + public LeafPage(DBPage dbPage, Schema schema) { + if (dbPage.readUnsignedByte(0) != BTREE_LEAF_PAGE) { + throw new IllegalArgumentException("Specified DBPage " + + dbPage.getPageNo() + " is not marked as a leaf page."); + } + + this.dbPage = dbPage; + this.schema = schema; + + loadPageContents(); + } + + + /** + * This static helper function initializes a {@link DBPage} object's + * contents with the type and detail values that will allow a new + * {@code LeafPage} wrapper to be instantiated for the page, and then it + * returns a wrapper object for the page. + * + * @param dbPage the page to initialize as a leaf page. + * + * @param schema the schema of the tuples in the leaf page + * + * @return a newly initialized {@code LeafPage} object wrapping the page + */ + public static LeafPage init(DBPage dbPage, Schema schema) { + dbPage.writeByte(OFFSET_PAGE_TYPE, BTREE_LEAF_PAGE); + dbPage.writeShort(OFFSET_NUM_TUPLES, 0); + dbPage.writeShort(OFFSET_NEXT_PAGE_NO, 0); + + return new LeafPage(dbPage, schema); + } + + + /** + * This private helper scans through the leaf page's contents and caches + * the contents of the leaf page in a way that makes it easy to use and + * manipulate. + */ + private void loadPageContents() { + numTuples = dbPage.readUnsignedShort(OFFSET_NUM_TUPLES); + tuples = new ArrayList<>(numTuples); + + if (numTuples > 0) { + // Handle first tuple separately since we know its offset. + + BTreeFilePageTuple tuple = + new BTreeFilePageTuple(schema, dbPage, OFFSET_FIRST_TUPLE, 0); + + tuples.add(tuple); + + // Handle remaining tuples. + for (int i = 1; i < numTuples; i++) { + int tupleEndOffset = tuple.getEndOffset(); + tuple = new BTreeFilePageTuple(schema, dbPage, tupleEndOffset, i); + tuples.add(tuple); + } + + endOffset = tuple.getEndOffset(); + } + else { + // There are no tuples in the leaf page. + endOffset = OFFSET_FIRST_TUPLE; + } + } + + + /** + * Returns the schema of tuples in this page. + * + * @return the schema of tuples in this page + */ + public Schema getSchema() { + return schema; + } + + + /** + * Returns the {@code DBPage} that backs this leaf page. + * + * @return the {@code DBPage} that backs this leaf page. + */ + public DBPage getDBPage() { + return dbPage; + } + + + /** + * Returns the page-number of this leaf page. + * + * @return the page-number of this leaf page. + */ + public int getPageNo() { + return dbPage.getPageNo(); + } + + + /** + * Returns the page-number of the next leaf page in the sequence of leaf + * pages, or 0 if this is the last leaf-page in the B<sup>+</sup> tree + * file. + * + * @return the page-number of the next leaf page in the sequence of leaf + * pages, or 0 if this is the last leaf-page in the B<sup>+</sup> + * tree file. + */ + public int getNextPageNo() { + return dbPage.readUnsignedShort(OFFSET_NEXT_PAGE_NO); + } + + + /** + * Sets the page-number of the next leaf page in the sequence of leaf pages. + * + * @param pageNo the page-number of the next leaf-page in the index, or 0 + * if this is the last leaf-page in the B<sup>+</sup> tree file. + */ + public void setNextPageNo(int pageNo) { + if (pageNo < 0) { + throw new IllegalArgumentException( + "pageNo must be in range [0, 65535]; got " + pageNo); + } + + dbPage.writeShort(OFFSET_NEXT_PAGE_NO, pageNo); + } + + + /** + * Returns the number of tuples in this leaf-page. Note that this count + * does not include the pointer to the next leaf; it only includes the + * tuples themselves. + * + * @return the number of entries in this leaf-page. + */ + public int getNumTuples() { + return numTuples; + } + + + /** + * Returns the amount of space currently used in this leaf page, in bytes. + * + * @return the amount of space currently used in this leaf page, in bytes. + */ + public int getUsedSpace() { + return endOffset; + } + + + /** + * Returns the amount of space used by tuples in this page, in bytes. + * + * @return the amount of space used by tuples in this page, in bytes. + */ + public int getSpaceUsedByTuples() { + return endOffset - OFFSET_FIRST_TUPLE; + } + + /** + * Returns the amount of space available in this leaf page, in bytes. + * + * @return the amount of space available in this leaf page, in bytes. + */ + public int getFreeSpace() { + return dbPage.getPageSize() - endOffset; + } + + + /** + * Returns the total space (page size) in bytes. + * + * @return the size of the page, in bytes. + */ + public int getTotalSpace() { + return dbPage.getPageSize(); + } + + + /** + * Returns the tuple at the specified index. + * + * @param index the index of the tuple to retrieve + * + * @return the tuple at that index + */ + public BTreeFilePageTuple getTuple(int index) { + return tuples.get(index); + } + + + /** + * Returns the size of the tuple at the specified index, in bytes. + * + * @param index the index of the tuple to get the size of + * + * @return the size of the specified tuple, in bytes + */ + public int getTupleSize(int index) { + BTreeFilePageTuple tuple = getTuple(index); + return tuple.getEndOffset() - tuple.getOffset(); + } + + + /** + * Given a leaf page in the B<sup>+</sup> tree file, returns the page + * number of the left sibling, or -1 if there is no left sibling to this + * node. + * + * @param pagePath the page path from root to this leaf page + * @param innerOps the inner page ops that allows this method to + * load inner pages and navigate the tree + * + * @return the page number of the left sibling leaf-node, or -1 if there + * is no left sibling + * + * @review (Donnie) There is a lot of implementation-overlap between this + * function and the {@link InnerPage#getLeftSibling}. Maybe find + * a way to combine the implementations. + */ + public int getLeftSibling(List<Integer> pagePath, + InnerPageOperations innerOps) { + + // Verify that the last node in the page path is in fact this page. + if (pagePath.get(pagePath.size() - 1) != getPageNo()) { + throw new IllegalArgumentException( + "The page path provided does not terminate on this leaf page."); + } + + // If this leaf doesn't have a parent, we already know it doesn't + // have a sibling. + if (pagePath.size() <= 1) + return -1; + + int parentPageNo = pagePath.get(pagePath.size() - 2); + InnerPage inner = innerOps.loadPage(parentPageNo); + + // Get the index of the pointer that points to this page. If it + // doesn't appear in the parent, we have a serious problem... + int pageIndex = inner.getIndexOfPointer(getPageNo()); + if (pageIndex == -1) { + throw new IllegalStateException(String.format( + "Leaf node %d doesn't appear in parent inner node %d!", + getPageNo(), parentPageNo)); + } + + int leftSiblingIndex = pageIndex - 1; + int leftSiblingPageNo = -1; + + if (leftSiblingIndex >= 0) + leftSiblingPageNo = inner.getPointer(leftSiblingIndex); + + return leftSiblingPageNo; + } + + + /** + * Given a leaf page in the B<sup>+</sup> tree file, returns the page + * number of the right sibling, or -1 if there is no right sibling to + * this node. + * + * @param pagePath the page path from root to this leaf page + * + * @return the page number of the right sibling leaf-node, or -1 if there + * is no right sibling + */ + public int getRightSibling(List<Integer> pagePath) { + + // Verify that the last node in the page path is in fact this page. + if (pagePath.get(pagePath.size() - 1) != getPageNo()) { + throw new IllegalArgumentException( + "The page path provided does not terminate on this leaf page."); + } + + int rightSiblingPageNo = getNextPageNo(); + if (rightSiblingPageNo == 0) + rightSiblingPageNo = -1; + + return rightSiblingPageNo; + } + + + /** + * Returns the index of the specified tuple. + * + * @param tuple the tuple to retrieve the index for + * + * @return the integer index of the specified tuple, -1 if the tuple + * isn't in the page. + */ + public int getTupleIndex(Tuple tuple) { + int i; + for (i = 0; i < numTuples; i++) { + BTreeFilePageTuple pageTuple = tuples.get(i); + + /* This gets REALLY verbose... */ + logger.trace(i + ": comparing " + tuple + " to " + pageTuple); + + // Is this the key we're looking for? + if (TupleComparator.comparePartialTuples(tuple, pageTuple) == 0) { + logger.debug(String.format("Found tuple: %s is equal to " + + "%s at index %d (size = %d bytes)", tuple, pageTuple, i, + pageTuple.getSize())); + + return i; + } + } + return -1; + } + + + /** + * This method will delete a tuple from the leaf page. The method takes + * care of 'sliding' the remaining data to cover up the gap left. The + * method throws an exception if the specified tuple does not appear in + * the leaf page. + * + * @param tuple the tuple to delete from the leaf page + * + * @throws IllegalStateException if the specified tuple doesn't exist + */ + public void deleteTuple(Tuple tuple) { + logger.debug("Trying to delete tuple " + tuple + " from leaf page " + + getPageNo()); + + int index = getTupleIndex(tuple); + if (index == -1) { + throw new IllegalArgumentException("Specified tuple " + tuple + + " does not appear in leaf page " + getPageNo()); + } + + int tupleOffset = getTuple(index).getOffset(); + int len = getTupleSize(index); + + logger.debug("Moving leaf-page data in range [" + (tupleOffset+len) + + ", " + endOffset + ") over by " + len + " bytes"); + dbPage.moveDataRange(tupleOffset + len, tupleOffset, + endOffset - tupleOffset - len); + + // Decrement the total number of entries. + dbPage.writeShort(OFFSET_NUM_TUPLES, numTuples - 1); + + logger.debug("Loading altered page - had " + numTuples + + " tuples before delete."); + // Load new page. + loadPageContents(); + + logger.debug("After loading, have " + numTuples + " tuples"); + + if (tuple instanceof BTreeFilePageTuple) { + BTreeFilePageTuple btpt = (BTreeFilePageTuple) tuple; + btpt.setDeleted(); + + if (index < numTuples) + btpt.setNextTuplePosition(dbPage.getPageNo(), index); + else + btpt.setNextTuplePosition(getNextPageNo(), 0); + } + } + + + /** + * This method inserts a tuple into the leaf page, making sure to keep + * tuples in monotonically increasing order. This method will throw an + * exception if the leaf page already contains the specified tuple. + * + * @param newTuple the new tuple to add to the leaf page + * + * @throws IllegalStateException if the specified tuple already appears in + * the leaf page. + */ + public BTreeFilePageTuple addTuple(TupleLiteral newTuple) { + if (newTuple.getStorageSize() == -1) { + throw new IllegalArgumentException("New tuple's storage size " + + "must be computed before this method is called."); + } + + if (getFreeSpace() < newTuple.getStorageSize()) { + throw new IllegalArgumentException(String.format( + "Not enough space in this node to store the new tuple " + + "(%d bytes free; %d bytes required)", getFreeSpace(), + newTuple.getStorageSize())); + } + + BTreeFilePageTuple result = null; + + if (numTuples == 0) { + logger.debug("Leaf page is empty; storing new tuple at start."); + result = addTupleAtIndex(newTuple, 0); + } + else { + int i; + for (i = 0; i < numTuples; i++) { + BTreeFilePageTuple tuple = tuples.get(i); + + /* This gets REALLY verbose... */ + logger.trace(i + ": comparing " + newTuple + " to " + tuple); + + // Compare the new tuple to the current tuple. Once we find + // where the new tuple should go, copy the tuple into the page. + int cmp = TupleComparator.compareTuples(newTuple, tuple); + if (cmp < 0) { + logger.debug("Storing new tuple at index " + i + + " in the leaf page."); + result = addTupleAtIndex(newTuple, i); + break; + } + else if (cmp == 0) { + // TODO: Currently we require all tuples to be unique, + // but this isn't a realistic long-term constraint. + throw new IllegalStateException("Tuple " + newTuple + + " already appears in the index!"); + } + } + + if (i == numTuples) { + // The new tuple will go at the end of this page's entries. + logger.debug("Storing new tuple at end of leaf page."); + result = addTupleAtIndex(newTuple, numTuples); + } + } + + // The addTupleAtIndex() method updates the internal fields that cache + // where keys live, etc. So, we don't need to do that here. + + assert result != null; // Shouldn't be possible at this point... + return result; + } + + + /** + * This private helper takes care of inserting a tuple at a specific index + * in the leaf page. This method should be called with care, so as to + * ensure that tuples always remain in monotonically increasing order. + * + * @param newTuple the new tuple to insert into the leaf page + * @param index the index to insert the tuple at. Any existing tuples at + * or after this index will be shifted over to make room for the + * new tuple. + */ + private BTreeFilePageTuple addTupleAtIndex(TupleLiteral newTuple, + int index) { + + logger.debug("Leaf-page is starting with data ending at index " + + endOffset + ", and has " + numTuples + " tuples."); + + // Get the storage size of the new tuple. + int len = newTuple.getStorageSize(); + if (len == -1) { + throw new IllegalArgumentException("New tuple's storage size " + + "must be computed before this method is called."); + } + + logger.debug("New tuple's storage size is " + len + " bytes"); + + int tupleOffset; + if (index < numTuples) { + // Need to slide tuples after this index over, to make space. + + BTreeFilePageTuple tuple = getTuple(index); + + // Make space for the new tuple to be stored, then copy in + // the new values. + + tupleOffset = tuple.getOffset(); + + logger.debug("Moving leaf-page data in range [" + tupleOffset + + ", " + endOffset + ") over by " + len + " bytes"); + + dbPage.moveDataRange(tupleOffset, tupleOffset + len, + endOffset - tupleOffset); + } + else { + // The new tuple falls at the end of the data in the leaf index + // page. + tupleOffset = endOffset; + logger.debug("New tuple is at end of leaf-page data; not " + + "moving anything."); + } + + // Write the tuple value into the page. + PageTuple.storeTuple(dbPage, tupleOffset, schema, newTuple); + + // Increment the total number of tuples. + dbPage.writeShort(OFFSET_NUM_TUPLES, numTuples + 1); + + // Reload the page contents now that we have a new tuple in the mix. + // TODO: We could do this more efficiently, but this should be + // sufficient for now. + loadPageContents(); + + logger.debug("Wrote new tuple to leaf-page at offset " + tupleOffset + + "."); + logger.debug("Leaf-page is ending with data ending at index " + + endOffset + ", and has " + numTuples + " tuples."); + + // Return the actual tuple we just added to the page. + return getTuple(index); + } + + + /** + * This helper function moves the specified number of tuples to the left + * sibling of this leaf node. The data is copied in one shot so that the + * transfer will be fast, and the various associated bookkeeping values in + * both leaves are updated. + * + * @param leftSibling the left sibling of this leaf-node in the + * B<sup>+</sup> tree file + * + * @param count the number of tuples to move to the left sibling + */ + public void moveTuplesLeft(LeafPage leftSibling, int count) { + if (leftSibling == null) + throw new IllegalArgumentException("leftSibling cannot be null"); + + if (leftSibling.getNextPageNo() != getPageNo()) { + logger.error(String.format("Left sibling leaf %d says that " + + "page %d is its right sibling, not this page %d", + leftSibling.getPageNo(), leftSibling.getNextPageNo(), + getPageNo())); + + throw new IllegalArgumentException("leftSibling " + + leftSibling.getPageNo() + " isn't actually the left " + + "sibling of this leaf-node " + getPageNo()); + } + + if (count < 0 || count > numTuples) { + throw new IllegalArgumentException("count must be in range [0, " + + numTuples + "), got " + count); + } + + int moveEndOffset = getTuple(count - 1).getEndOffset(); //getTuple(count).getOffset() + int len = moveEndOffset - OFFSET_FIRST_TUPLE; + + // Copy the range of tuple-data to the destination page. Then update + // the count of tuples in the destination page. + // Don't need to move any data in the left sibling; we are appending! + leftSibling.dbPage.write(leftSibling.endOffset, dbPage.getPageData(), + OFFSET_FIRST_TUPLE, len); // Copy the tuple-data across + leftSibling.dbPage.writeShort(OFFSET_NUM_TUPLES, + leftSibling.numTuples + count); // Update the tuple-count + + // Remove that range of tuple-data from this page. + dbPage.moveDataRange(moveEndOffset, OFFSET_FIRST_TUPLE, + endOffset - moveEndOffset); + dbPage.writeShort(OFFSET_NUM_TUPLES, numTuples - count); + + // Only erase the old data in the leaf page if we are trying to make + // sure everything works properly. + if (BTreeTupleFile.CLEAR_OLD_DATA) + dbPage.setDataRange(endOffset - len, len, (byte) 0); + + // Update the cached info for both leaves. + loadPageContents(); + leftSibling.loadPageContents(); + } + + + /** + * This helper function moves the specified number of tuples to the right + * sibling of this leaf node. The data is copied in one shot so that the + * transfer will be fast, and the various associated bookkeeping values in + * both leaves are updated. + * + * @param rightSibling the right sibling of this leaf-node in the index + * file + * + * @param count the number of tuples to move to the right sibling + */ + public void moveTuplesRight(LeafPage rightSibling, int count) { + if (rightSibling == null) + throw new IllegalArgumentException("rightSibling cannot be null"); + + if (getNextPageNo() != rightSibling.getPageNo()) { + throw new IllegalArgumentException("rightSibling " + + rightSibling.getPageNo() + " isn't actually the right " + + "sibling of this leaf-node " + getPageNo()); + } + + if (count < 0 || count > numTuples) { + throw new IllegalArgumentException("count must be in range [0, " + + numTuples + "), got " + count); + } + + int startOffset = getTuple(numTuples - count).getOffset(); + int len = endOffset - startOffset; + + // Copy the range of tuple-data to the destination page. Then update + // the count of tuples in the destination page. + + // Make room for the data + rightSibling.dbPage.moveDataRange(OFFSET_FIRST_TUPLE, + OFFSET_FIRST_TUPLE + len, + rightSibling.endOffset - OFFSET_FIRST_TUPLE); + + // Copy the tuple-data across + rightSibling.dbPage.write(OFFSET_FIRST_TUPLE, dbPage.getPageData(), + startOffset, len); + + // Update the tuple-count + rightSibling.dbPage.writeShort(OFFSET_NUM_TUPLES, + rightSibling.numTuples + count); + + // Remove that range of tuple-data from this page. + dbPage.writeShort(OFFSET_NUM_TUPLES, numTuples - count); + + // Only erase the old data in the leaf page if we are trying to make + // sure everything works properly. + if (BTreeTupleFile.CLEAR_OLD_DATA) + dbPage.setDataRange(startOffset, len, (byte) 0); + + // Update the cached info for both leaves. + loadPageContents(); + rightSibling.loadPageContents(); + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/LeafPageOperations.java b/src/main/java/edu/caltech/nanodb/storage/btreefile/LeafPageOperations.java new file mode 100644 index 0000000000000000000000000000000000000000..29b3726fb58f91df99bb5c2fd1a5184daba9ccdf --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/LeafPageOperations.java @@ -0,0 +1,837 @@ +package edu.caltech.nanodb.storage.btreefile; + + +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; + + +/** + * This class provides high-level B<sup>+</sup> tree management operations + * performed on leaf nodes. These operations are provided here and not on the + * {@link LeafPage} class since they sometimes involve splitting or merging + * leaf nodes, updating parent nodes, and so forth. + */ +public class LeafPageOperations { + /** A logging object for reporting anything interesting that happens. */ + private static Logger logger = LogManager.getLogger(LeafPageOperations.class); + + private StorageManager storageManager; + + + private BTreeTupleFile tupleFile; + + + private FileOperations fileOps; + + private InnerPageOperations innerPageOps; + + + public LeafPageOperations(StorageManager storageManager, + BTreeTupleFile tupleFile, + FileOperations fileOps, + InnerPageOperations innerPageOps) { + this.storageManager = storageManager; + this.tupleFile = tupleFile; + this.fileOps = fileOps; + this.innerPageOps = innerPageOps; + } + + + /** + * This helper function provides the simple operation of loading a leaf page + * from its page-number, or if the page-number is 0 then {@code null} is + * returned. + * + * @param pageNo the page-number to load as a leaf-page. + * + * @return a newly initialized {@link LeafPage} instance if {@code pageNo} + * is positive, or {@code null} if {@code pageNo} is 0. + * + * @throws IllegalArgumentException if the specified page isn't a leaf-page + */ + private LeafPage loadLeafPage(int pageNo) { + + if (pageNo == 0) + return null; + + DBFile dbFile = tupleFile.getDBFile(); + DBPage dbPage = storageManager.loadDBPage(dbFile, pageNo); + return new LeafPage(dbPage, tupleFile.getSchema()); + } + + + /** + * This helper function will handle deleting a tuple in the index. + * + * @param leaf the leaf page to delete the tuple from + * + * @param tuple the tuple to delete from the leaf page + * + * @param pagePath the path of pages taken from the root page to the leaf + * page, represented as a list of page numbers + */ + public void deleteTuple(LeafPage leaf, Tuple tuple, + List<Integer> pagePath) { + + logger.debug(String.format("Deleting tuple %s from leaf page %d at " + + "page-path %s", tuple, leaf.getPageNo(), pagePath)); + + leaf.deleteTuple(tuple); + + if (leaf.getUsedSpace() >= leaf.getTotalSpace() / 2) { + // The page is at least half-full. Don't need to redistribute or + // coalesce. + return; + } + else if (pagePath.size() == 1) { + // The page is the root. Don't need to redistribute or coalesce, + // but if the root is now empty, need to shorten the tree depth. + // (Since this is a leaf page, the depth will go down to 0.) + + if (leaf.getNumTuples() == 0) { + logger.debug(String.format("Current root page %d is now " + + "empty, removing.", leaf.getPageNo())); + + // Set the index's root page to 0 (empty) since the only page + // in the index is now empty and being removed. + DBPage dbpHeader = + storageManager.loadDBPage(tupleFile.getDBFile(), 0); + HeaderPage.setRootPageNo(dbpHeader, 0); + + // Free up this page in the index. + fileOps.releaseDataPage(leaf.getDBPage()); + + if (tuple instanceof BTreeFilePageTuple) { + // Some sanity checks - if the btree is now empty, + // the "next tuple" info should be all 0. + BTreeFilePageTuple btpt = (BTreeFilePageTuple) tuple; + assert btpt.getNextTuplePageNo() == 0; + assert btpt.getNextTupleIndex() == 0; + } + } + + return; + } + + // If we got to this part, we have to redistribute/coalesce stuff :( + + // Note: Assumed that at least one of the siblings has the same + // immediate parent. If that is not the case... we're doomed... + // TODO: VERIFY THIS AND THROW AN EXCEPTION IF NOT + + int leafPageNo = leaf.getPageNo(); + + // Leaf pages know their right sibling, so that's why finding the + // right page doesn't require the innerPageOps object. + int leftPageNo = leaf.getLeftSibling(pagePath, innerPageOps); + int rightPageNo = leaf.getRightSibling(pagePath); + + logger.debug(String.format("Leaf page %d is too empty. Left " + + "sibling is %d, right sibling is %d.", leafPageNo, leftPageNo, + rightPageNo)); + + if (leftPageNo == -1 && rightPageNo == -1) { + // We should never get to this point, since the earlier test + // should have caught this situation. + throw new IllegalStateException(String.format( + "Leaf node %d doesn't have a left or right sibling!", + leaf.getPageNo())); + } + + // Now we know that at least one sibling is present. Load both + // siblings and coalesce/redistribute in the direction that makes + // the most sense... + + LeafPage leftSibling = null; + if (leftPageNo != -1) + leftSibling = loadLeafPage(leftPageNo); + + LeafPage rightSibling = null; + if (rightPageNo != -1) + rightSibling = loadLeafPage(rightPageNo); + + assert leftSibling != null || rightSibling != null; + + // See if we can coalesce the node into its left or right sibling. + // When we do the check, we must not forget that each node contains a + // header, and we need to account for that space as well. This header + // space is included in the getUsedSpace() method, but is excluded by + // the getSpaceUsedByTuples() method. + + // TODO: SEE IF WE CAN SIMPLIFY THIS AT ALL... + if (leftSibling != null && + leftSibling.getUsedSpace() + leaf.getSpaceUsedByTuples() < + leftSibling.getTotalSpace()) { + + // Coalesce the current node into the left sibling. + logger.debug("Delete from leaf " + leaf.getPageNo() + + ": coalescing with left sibling leaf."); + + logger.debug(String.format("Before coalesce-left, page has %d " + + "tuples and left sibling has %d tuples.", + leaf.getNumTuples(), leftSibling.getNumTuples())); + + if (tuple instanceof BTreeFilePageTuple) { + // The "next tuple" will end up in the left sibling, so we + // need to update this info in the deleted tuple. + BTreeFilePageTuple btpt = (BTreeFilePageTuple) tuple; + int index = btpt.getNextTupleIndex(); + index += leftSibling.getNumTuples(); + btpt.setNextTuplePosition(leftPageNo, index); + } + + leaf.moveTuplesLeft(leftSibling, leaf.getNumTuples()); + leftSibling.setNextPageNo(leaf.getNextPageNo()); + + logger.debug(String.format("After coalesce-left, page has %d " + + "tuples and left sibling has %d tuples.", + leaf.getNumTuples(), leftSibling.getNumTuples())); + + // Free up the leaf page since it's empty now + fileOps.releaseDataPage(leaf.getDBPage()); + + // Since the leaf page has been removed from the index structure, + // we need to remove it from the parent page. Also, since the + // page was coalesced into its left sibling, we need to remove + // the tuple to the left of the pointer being removed. + + InnerPage parent = + innerPageOps.loadPage(pagePath.get(pagePath.size() - 2)); + + List<Integer> parentPagePath = pagePath.subList(0, pagePath.size() - 1); + innerPageOps.deletePointer(parent, parentPagePath, leafPageNo, + /* remove right tuple */ false); + } + else if (rightSibling != null && + rightSibling.getUsedSpace() + leaf.getSpaceUsedByTuples() < + rightSibling.getTotalSpace()) { + + // Coalesce the current node into the right sibling. + logger.debug("Delete from leaf " + leaf.getPageNo() + + ": coalescing with right sibling leaf."); + + logger.debug(String.format("Before coalesce-right, page has %d " + + "tuples and right sibling has %d tuples.", + leaf.getNumTuples(), rightSibling.getNumTuples())); + + if (tuple instanceof BTreeFilePageTuple) { + // The "next tuple" will end up in the right sibling, so we + // need to update this info in the deleted tuple. We don't + // update the index, because this page's tuples will precede + // the right sibling's tuples. + BTreeFilePageTuple btpt = (BTreeFilePageTuple) tuple; + int index = btpt.getNextTupleIndex(); + btpt.setNextTuplePosition(rightPageNo, index); + } + + leaf.moveTuplesRight(rightSibling, leaf.getNumTuples()); + + // Left sibling can be null if leaf is the first leaf node in the + // sequence of the btree. + if (leftSibling != null) + leftSibling.setNextPageNo(rightPageNo); + + logger.debug(String.format("After coalesce-right, page has %d " + + "tuples and right sibling has %d tuples.", + leaf.getNumTuples(), rightSibling.getNumTuples())); + + // Free up the leaf page since it's empty now + fileOps.releaseDataPage(leaf.getDBPage()); + + // Since the leaf page has been removed from the index structure, + // we need to remove it from the parent page. Also, since the + // page was coalesced into its right sibling, we need to remove + // the tuple to the right of the pointer being removed. + + InnerPage parent = + innerPageOps.loadPage(pagePath.get(pagePath.size() - 2)); + + List<Integer> parentPagePath = pagePath.subList(0, pagePath.size() - 1); + innerPageOps.deletePointer(parent, parentPagePath, leafPageNo, + /* remove right tuple */ true); + } + else { + // Can't coalesce the leaf node into either sibling. Redistribute + // tuples from left or right sibling into the leaf. The strategy + // is as follows: + + // If the node has both left and right siblings, redistribute from + // the fuller sibling. Otherwise, just redistribute from + // whichever sibling we have. + + LeafPage adjPage; + if (leftSibling != null && rightSibling != null) { + // Both siblings are present. Choose the fuller one to + // relocate from. + if (leftSibling.getUsedSpace() > rightSibling.getUsedSpace()) + adjPage = leftSibling; + else + adjPage = rightSibling; + } + else if (leftSibling != null) { + // There is no right sibling. Use the left sibling. + adjPage = leftSibling; + } + else { + // There is no left sibling. Use the right sibling. + adjPage = rightSibling; + } + + int tuplesToMove = tryLeafRelocateToFill(leaf, adjPage, + /* movingRight */ adjPage == leftSibling); + + if (tuplesToMove == 0) { + // We really tried to satisfy the "minimum size" requirement, + // but we just couldn't. Log it and return. + + StringBuilder buf = new StringBuilder(); + + buf.append(String.format("Couldn't relocate tuples to satisfy" + + " minimum space requirement in leaf-page %d with %d tuples!\n", + leaf.getPageNo(), leaf.getNumTuples())); + + if (leftSibling != null) { + buf.append( + String.format("\t- Left sibling page %d has %d tuples\n", + leftSibling.getPageNo(), leftSibling.getNumTuples())); + } + else { + buf.append("\t- No left sibling\n"); + } + + if (rightSibling != null) { + buf.append( + String.format("\t- Right sibling page %d has %d tuples", + rightSibling.getPageNo(), rightSibling.getNumTuples())); + } + else { + buf.append("\t- No right sibling"); + } + + logger.warn(buf); + + return; + } + + logger.debug(String.format("Relocating %d tuples into leaf page " + + "%d from %s sibling page %d", tuplesToMove, leaf.getPageNo(), + (adjPage == leftSibling ? "left" : "right"), adjPage.getPageNo())); + + if (tuple instanceof BTreeFilePageTuple) { + // Since we are moving tuples into this page, we may need to + // modify the "next tuple" info. This is only necessary if + // tuples are moved from the left sibling, since those will + // precede the leaf page's current tuples. + BTreeFilePageTuple btpt = (BTreeFilePageTuple) tuple; + int nextPageNo = btpt.getNextTuplePageNo(); + int nextIndex = btpt.getNextTupleIndex(); + + if (adjPage == leftSibling) { + if (nextPageNo == leafPageNo) { + logger.debug(String.format("Moving %d tuples from left " + + "sibling %d to leaf %d. Deleted tuple has next " + + "tuple at [%d:%d]; updating to [%d:%d]", tuplesToMove, + leftPageNo, leafPageNo, nextPageNo, nextIndex, + nextPageNo, nextIndex + tuplesToMove)); + + btpt.setNextTuplePosition(nextPageNo, nextIndex + tuplesToMove); + } + else { + assert(nextIndex == 0); + + logger.debug(String.format("Moving %d tuples from " + + "left sibling %d to leaf %d. Deleted tuple " + + "has next tuple at [%d:%d]; not updating", + tuplesToMove, leftPageNo, leafPageNo, + nextPageNo, nextIndex)); + } + } + else { + assert adjPage == rightSibling; + + if (nextPageNo == rightPageNo) { + assert(nextIndex == 0); + + logger.debug(String.format("Moving %d tuples from " + + "right sibling %d to leaf %d. Deleted tuple " + + "has next tuple at [%d:%d]; not updating", + tuplesToMove, rightPageNo, leafPageNo, + nextPageNo, nextIndex)); + + btpt.setNextTuplePosition(leafPageNo, leaf.getNumTuples()); + } + else { + logger.debug(String.format("Moving %d tuples from " + + "right sibling %d to leaf %d. Deleted tuple " + + "has next tuple at [%d:%d]; not updating", + tuplesToMove, rightPageNo, leafPageNo, + nextPageNo, nextIndex)); + } + } + } + + InnerPage parent = + innerPageOps.loadPage(pagePath.get(pagePath.size() - 2)); + int index; + + if (adjPage == leftSibling) { + adjPage.moveTuplesRight(leaf, tuplesToMove); + index = parent.getIndexOfPointer(adjPage.getPageNo()); + parent.replaceTuple(index, leaf.getTuple(0)); + } + else { // adjPage == right sibling + adjPage.moveTuplesLeft(leaf, tuplesToMove); + index = parent.getIndexOfPointer(leaf.getPageNo()); + parent.replaceTuple(index, adjPage.getTuple(0)); + } + } + } + + + /** + * This helper function handles the operation of adding a new tuple to a + * leaf-page of the index. This operation is provided here and not on the + * {@link LeafPage} class, because adding the new tuple might require the + * leaf page to be split into two pages. + * + * @param leaf the leaf page to add the tuple to + * + * @param newTuple the new tuple to add to the leaf page + * + * @param pagePath the path of pages taken from the root page to this leaf + * page, represented as a list of page numbers in the data file + */ + public BTreeFilePageTuple addTuple(LeafPage leaf, TupleLiteral newTuple, + List<Integer> pagePath) { + + BTreeFilePageTuple result; + + // Figure out where the new tuple-value goes in the leaf page. + + int newTupleSize = newTuple.getStorageSize(); + if (leaf.getFreeSpace() < newTupleSize) { + // Try to relocate tuples from this leaf to either sibling, + // or if that can't happen, split the leaf page into two. + result = relocateTuplesAndAddTuple(leaf, pagePath, newTuple); + if (result == null) + result = splitLeafAndAddTuple(leaf, pagePath, newTuple); + } + else { + // There is room in the leaf for the new tuple. Add it there. + result = leaf.addTuple(newTuple); + } + + return result; + } + + + /** + * This method attempts to relocate tuples to the left or right sibling + * of the specified node, and then insert the specified tuple into the + * appropriate node. If it's not possible to relocate tuples (perhaps + * because there isn't space and/or because there is no sibling), the + * method returns {@code null}. + * + * @param page The leaf page to relocate tuples out of. + * + * @param pagePath The path from the index's root to the leaf page + * + * @param tuple The tuple to add to the index. + * + * @return a {@code BTreeFilePageTuple} representing the actual tuple + * added into the tree structure, or {@code null} if tuples + * couldn't be relocated to make space for the new tuple. + */ + private BTreeFilePageTuple relocateTuplesAndAddTuple(LeafPage page, + List<Integer> pagePath, TupleLiteral tuple) { + + // See if we are able to relocate records either direction to free up + // space for the new tuple. + + int bytesRequired = tuple.getStorageSize(); + + int pathSize = pagePath.size(); + if (pathSize == 1) // This node is also the root - no parent. + return null; // There aren't any siblings to relocate to. + + if (pagePath.get(pathSize - 1) != page.getPageNo()) { + throw new IllegalArgumentException( + "leaf page number doesn't match last page-number in page path"); + } + + int parentPageNo = 0; + if (pathSize >= 2) + parentPageNo = pagePath.get(pathSize - 2); + + InnerPage parentPage = innerPageOps.loadPage(parentPageNo); + int numPointers = parentPage.getNumPointers(); + int pagePtrIndex = parentPage.getIndexOfPointer(page.getPageNo()); + + // Check each sibling in its own code block so that we can constrain + // the scopes of the variables a bit. This keeps us from accidentally + // reusing the "prev" variables in the "next" section. + + { + LeafPage prevPage = null; + if (pagePtrIndex - 1 >= 0) + prevPage = loadLeafPage(parentPage.getPointer(pagePtrIndex - 1)); + + if (prevPage != null) { + // See if we can move some of this leaf's tuples to the + // previous leaf, to free up space. + + int count = tryLeafRelocateForSpace(page, prevPage, false, + bytesRequired); + + if (count > 0) { + // Yes, we can do it! + logger.debug(String.format("Relocating %d tuples from " + + "leaf-page %d to left-sibling leaf-page %d", count, + page.getPageNo(), prevPage.getPageNo())); + + logger.debug("Space before relocation: Leaf = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + prevPage.getFreeSpace() + " bytes"); + + page.moveTuplesLeft(prevPage, count); + + logger.debug("Space after relocation: Leaf = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + prevPage.getFreeSpace() + " bytes"); + + BTreeFilePageTuple result = + addTupleToLeafPair(prevPage, page, tuple); + + if (result == null) { + // Even with relocating tuples, we couldn't free up + // enough space. :-( + return null; + } + + // Since we relocated tuples between two nodes, update + // the parent page to reflect the tuple that is now at + // the start of the right page. + BTreeFilePageTuple firstRightTuple = page.getTuple(0); + pagePath.remove(pathSize - 1); + innerPageOps.replaceTuple(parentPage, pagePath, + prevPage.getPageNo(), firstRightTuple, page.getPageNo()); + + return result; + } + } + } + + { + LeafPage nextPage = null; + if (pagePtrIndex + 1 < numPointers) + nextPage = loadLeafPage(parentPage.getPointer(pagePtrIndex + 1)); + + if (nextPage != null) { + // See if we can move some of this leaf's tuples to the next + // leaf, to free up space. + + int count = tryLeafRelocateForSpace(page, nextPage, true, + bytesRequired); + + if (count > 0) { + // Yes, we can do it! + + logger.debug(String.format("Relocating %d tuples from " + + "leaf-page %d to right-sibling leaf-page %d", count, + page.getPageNo(), nextPage.getPageNo())); + + logger.debug("Space before relocation: Leaf = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + nextPage.getFreeSpace() + " bytes"); + + page.moveTuplesRight(nextPage, count); + + logger.debug("Space after relocation: Leaf = " + + page.getFreeSpace() + " bytes\t\tSibling = " + + nextPage.getFreeSpace() + " bytes"); + + BTreeFilePageTuple result = + addTupleToLeafPair(page, nextPage, tuple); + + if (result == null) { + // Even with relocating tuples, we couldn't free up + // enough space. :-( + return null; + } + + // Since we relocated tuples between two nodes, update + // the parent page to reflect the tuple that is now at + // the start of the right page. + BTreeFilePageTuple firstRightTuple = nextPage.getTuple(0); + pagePath.remove(pathSize - 1); + innerPageOps.replaceTuple(parentPage, pagePath, + page.getPageNo(), firstRightTuple, nextPage.getPageNo()); + + return result; + } + } + } + + // Couldn't relocate tuples to either the previous or next page. We + // must split the leaf into two. + return null; + } + + + /** + * This helper method takes a pair of leaf nodes that are siblings to each + * other, and adds the specified tuple to whichever leaf the tuple should + * go into. The method returns the {@code BTreeFilePageTuple} object + * representing the actual tuple in the tree once it has been added. + * + * @param prevLeaf the first leaf in the pair, left sibling of + * {@code nextLeaf} + * + * @param nextLeaf the second leaf in the pair, right sibling of + * {@code prevLeaf} + * + * @param tuple the tuple to insert into the pair of leaves + * + * @return the actual tuple in the page, after the insert is completed + */ + private BTreeFilePageTuple addTupleToLeafPair(LeafPage prevLeaf, + LeafPage nextLeaf, TupleLiteral tuple) { + + BTreeFilePageTuple result = null; + BTreeFilePageTuple firstRightTuple = nextLeaf.getTuple(0); + if (TupleComparator.compareTuples(tuple, firstRightTuple) < 0) { + // The new tuple goes in the left page. Hopefully there is room + // for it... + logger.debug("Adding tuple to left leaf " + prevLeaf.getPageNo() + + " in pair"); + if (prevLeaf.getFreeSpace() >= tuple.getStorageSize()) + result = prevLeaf.addTuple(tuple); + } + else { + // The new tuple goes in the right page. Again, hopefully there + // is room for it... + logger.debug("Adding tuple to right leaf " + nextLeaf.getPageNo() + + " in pair"); + if (nextLeaf.getFreeSpace() >= tuple.getStorageSize()) + result = nextLeaf.addTuple(tuple); + } + + return result; + } + + + /** + * This helper function determines how many tuples must be relocated from + * one leaf-page to another, in order to free up the specified number of + * bytes. If it is possible, the number of tuples that must be relocated + * is returned. If it is not possible, the method returns 0. + * + * @param leaf the leaf node to relocate tuples from + * + * @param adjLeaf the adjacent leaf (predecessor or successor) to relocate + * tuples to + * + * @param movingRight pass {@code true} if the sibling is to the right of + * {@code page} (and therefore we are moving tuples right), or + * {@code false} if the sibling is to the left of {@code page} (and + * therefore we are moving tuples left). + * + * @param bytesRequired the number of bytes that must be freed up in + * {@code leaf} by the operation + * + * @return the number of tuples that must be relocated to free up the + * required space, or 0 if it is not possible. + */ + private int tryLeafRelocateForSpace(LeafPage leaf, LeafPage adjLeaf, + boolean movingRight, int bytesRequired) { + + int numTuples = leaf.getNumTuples(); + int leafBytesFree = leaf.getFreeSpace(); + int adjBytesFree = adjLeaf.getFreeSpace(); + + logger.debug("Leaf bytes free: " + leafBytesFree + + "\t\tAdjacent leaf bytes free: " + adjBytesFree); + + // Subtract the bytes-required from the adjacent-bytes-free value so + // that we ensure we always have room to put the tuple in either node. + adjBytesFree -= bytesRequired; + + int numRelocated = 0; + while (true) { + // Figure out the index of the tuple we need the size of, based on + // the direction we are moving values. If we are moving values + // right, we need to look at the tuples starting at the rightmost + // one. If we are moving tuples left, we need to start with the + // leftmost tuple. + int index; + if (movingRight) + index = numTuples - numRelocated - 1; + else + index = numRelocated; + + int tupleSize = leaf.getTupleSize(index); + + logger.debug("Tuple " + index + " is " + tupleSize + " bytes"); + + // Did we run out of space to move tuples before we hit our goal? + if (adjBytesFree < tupleSize) { + numRelocated = 0; + break; + } + + numRelocated++; + + leafBytesFree += tupleSize; + adjBytesFree -= tupleSize; + + // Since we don't yet know which leaf the new tuple will go into, + // stop when we can put the tuple in either leaf. + if (leafBytesFree >= bytesRequired && + adjBytesFree >= bytesRequired) { + break; + } + } + + logger.debug("Can relocate " + numRelocated + " tuples to free up space."); + + return numRelocated; + } + + + /** + * This helper function splits the specified leaf-node into two nodes, + * also updating the parent node in the process, and then inserts the + * specified tuple into the appropriate leaf. This method is used to add + * a tuple to a leaf that doesn't have enough space, when it isn't + * possible to relocate values to the left or right sibling of the leaf. + * + * @todo (donnie) When the leaf node is split, half of the tuples are + * put into the new leaf, regardless of the size of individual + * tuples. In other words, this method doesn't try to keep the + * leaves half-full based on bytes used. It would almost + * certainly be better if it did. + * + * @param leaf the leaf node to split and then add the tuple to + * @param pagePath the sequence of page-numbers traversed to reach this + * leaf node. + * + * @param tuple the new tuple to insert into the leaf node + */ + private BTreeFilePageTuple splitLeafAndAddTuple(LeafPage leaf, + List<Integer> pagePath, TupleLiteral tuple) { + + int pathSize = pagePath.size(); + if (pagePath.get(pathSize - 1) != leaf.getPageNo()) { + throw new IllegalArgumentException( + "Leaf page number doesn't match last page-number in page path"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Splitting leaf-page " + leaf.getPageNo() + + " into two leaves."); + logger.debug(" Old next-page: " + leaf.getNextPageNo()); + } + + // Get a new blank page in the index, with the same parent as the + // leaf-page we were handed. + + DBPage newDBPage = fileOps.getNewDataPage(); + LeafPage newLeaf = LeafPage.init(newDBPage, tupleFile.getSchema()); + + /* TODO: IMPLEMENT THE REST OF THIS METHOD. + * + * The LeafPage class provides some helpful operations for moving leaf- + * entries to a left or right sibling. + * + * The parent page must also be updated. If the leaf node doesn't have + * a parent, the tree's depth will increase by one level. + */ + logger.error("NOT YET IMPLEMENTED: splitLeafAndAddKey()"); + return null; + } + + + /** + * This helper function determines how many tuples must be relocated from + * one leaf-page to another, in order to satisfy the "minimum space" + * requirement of the B tree.free up the specified number of + * bytes. If it is possible, the number of tuples that must be relocated + * is returned. If it is not possible, the method returns 0. + * + * @param leaf the leaf node to relocate tuples from + * + * @param adjLeaf the adjacent leaf (predecessor or successor) to relocate + * tuples to + * + * @param movingRight pass {@code true} if the sibling is to the left of + * {@code page} (and therefore we are moving tuples right), or + * {@code false} if the sibling is to the right of {@code page} + * (and therefore we are moving tuples left). + * + * @return the number of tuples that must be relocated to fill the node + * to a minimal level, or 0 if not possible. + */ + private int tryLeafRelocateToFill(LeafPage leaf, LeafPage adjLeaf, + boolean movingRight) { + + int adjTuples = adjLeaf.getNumTuples(); // Tuples available to move + int leafBytesFree = leaf.getFreeSpace(); + int adjBytesFree = adjLeaf.getFreeSpace(); + + // Should be the same for both leaf and adjLeaf. + int halfFull = leaf.getTotalSpace() / 2; + + logger.debug("Leaf bytes free: " + leafBytesFree + + "\t\tAdjacent leaf bytes free: " + adjBytesFree); + + int numRelocated = 0; + while (true) { + // Figure out the index of the tuple we need the size of, based on + // the direction we are moving values. If we are moving values + // right, we need to look at the tuples starting at the rightmost + // one. If we are moving values left, we need to start with the + // leftmost tuple. + int index; + if (movingRight) + index = adjTuples - numRelocated - 1; + else + index = numRelocated; + + int tupleSize = adjLeaf.getTupleSize(index); + + logger.debug("Tuple " + index + " is " + tupleSize + " bytes"); + + // If we don't have room to move the adjacent node's tuple into + // this node (unlikely), just stop there. + if (leafBytesFree < tupleSize) + break; + + // If the adjacent leaf would become too empty, stop relocating. + if (adjBytesFree > halfFull) + break; + + numRelocated++; + + leafBytesFree -= tupleSize; + adjBytesFree += tupleSize; + + // Stop if the leaf now has at least the minimal number of bytes. + if (leafBytesFree <= halfFull) + break; + } + + logger.debug("Can relocate " + numRelocated + + " tuples to satisfy minimum space requirements."); + + return numRelocated; + } +} diff --git a/src/main/java/edu/caltech/nanodb/storage/btreefile/package.html b/src/main/java/edu/caltech/nanodb/storage/btreefile/package.html new file mode 100644 index 0000000000000000000000000000000000000000..2a21bade8ab91b21d142a604f809b2c516b2fbf4 --- /dev/null +++ b/src/main/java/edu/caltech/nanodb/storage/btreefile/package.html @@ -0,0 +1,43 @@ +<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> diff --git a/src/test/java/edu/caltech/test/nanodb/storage/btreefile/TestBTreeFile.java b/src/test/java/edu/caltech/test/nanodb/storage/btreefile/TestBTreeFile.java new file mode 100644 index 0000000000000000000000000000000000000000..07dc6296ece4f8f17f1dd294524ad99f9c40fdc4 --- /dev/null +++ b/src/test/java/edu/caltech/test/nanodb/storage/btreefile/TestBTreeFile.java @@ -0,0 +1,207 @@ +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); + } +}