View Javadoc
1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.IOException;
20  import java.io.UncheckedIOException;
21  import java.lang.System.Logger;
22  import java.nio.ByteBuffer;
23  import java.nio.ByteOrder;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.Comparator;
29  import java.util.List;
30  import java.util.Map;
31  
32  import com.healthmarketscience.jackcess.ConstraintViolationException;
33  import com.healthmarketscience.jackcess.Index;
34  import com.healthmarketscience.jackcess.IndexBuilder;
35  import static com.healthmarketscience.jackcess.impl.ByteUtil.ByteStream;
36  import static com.healthmarketscience.jackcess.impl.IndexCodes.*;
37  
38  /**
39   * Access table index data.  This is the actual data which backs a logical
40   * Index, where one or more logical indexes can be backed by the same index
41   * data.
42   *
43   * @author Tim McCune
44   */
45  public class IndexData {
46  
47    protected static final Logger LOG = System.getLogger(Index.class.getName());
48  
49    /** special entry which is less than any other entry */
50    public static final Entry FIRST_ENTRY =
51      createSpecialEntry(RowIdImpl.FIRST_ROW_ID);
52  
53    /** special entry which is greater than any other entry */
54    public static final Entry LAST_ENTRY =
55      createSpecialEntry(RowIdImpl.LAST_ROW_ID);
56  
57    /** special object which will always be greater than any other value, when
58        searching for an index entry range in a multi-value index */
59    public static final Object MAX_VALUE = new Object();
60  
61    /** special object which will always be greater than any other value, when
62        searching for an index entry range in a multi-value index */
63    public static final Object MIN_VALUE = new Object();
64  
65    private static final DataPage NEW_ROOT_DATA_PAGE = new RootDataPage();
66  
67    protected static final int INVALID_INDEX_PAGE_NUMBER = 0;
68  
69    /** Max number of columns in an index */
70    public static final int MAX_COLUMNS = 10;
71  
72    protected static final byte[] EMPTY_PREFIX = new byte[0];
73  
74    private static final byte[] ASC_EXT_DATE_TRAILER =
75      {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02};
76    private static final byte[] DESC_EXT_DATE_TRAILER =
77      flipBytes(ByteUtil.copyOf(ASC_EXT_DATE_TRAILER, ASC_EXT_DATE_TRAILER.length));
78  
79    static final short COLUMN_UNUSED = -1;
80  
81    public static final byte ASCENDING_COLUMN_FLAG = (byte)0x01;
82  
83    public static final byte UNIQUE_INDEX_FLAG = (byte)0x01;
84    public static final byte IGNORE_NULLS_INDEX_FLAG = (byte)0x02;
85    public static final byte REQUIRED_INDEX_FLAG = (byte)0x08;
86    public static final byte UNKNOWN_INDEX_FLAG = (byte)0x80; // always seems to be set on indexes in access 2000+
87  
88    /** the byte after the index flags, for an index over a complex column.
89        every other index has 0 there */
90    private static final byte COMPLEX_INDEX_MARKER = (byte)0x02;
91  
92    private static final int MAGIC_INDEX_NUMBER = 1923;
93  
94    private static final ByteOrder ENTRY_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
95  
96    /** maximum length of the key portion of an index entry, in the units a
97        text field is measured in.  a longer key is truncated, and the bytes
98        truncation discards are replaced with a KEY_DIGEST_LENGTH digest of
99        them.  the limit is the length of the longest text column, so a jet 3
100       key stops at 255 bytes and a jet 4 key at 510 */
101   private static final int MAX_KEY_UNITS = 255;
102   /** length of the digest which ends a truncated key */
103   private static final int KEY_DIGEST_LENGTH = 2;
104   /** polynomial behind KEY_DIGEST_TABLE, the CRC-16/ARC polynomial in its
105       MSB-first form */
106   private static final int KEY_DIGEST_POLY = 0x8005;
107   private static final int[] KEY_DIGEST_TABLE = createKeyDigestTable();
108 
109   /** type attributes for Entries which simplify comparisons */
110   public enum EntryType {
111     /** comparable type indicating this Entry should always compare less than
112         valid RowIds */
113     ALWAYS_FIRST,
114     /** comparable type indicating this Entry should always compare less than
115         other valid entries with equal entryBytes */
116     FIRST_VALID,
117     /** comparable type indicating this RowId should always compare
118         normally */
119     NORMAL,
120     /** comparable type indicating this Entry should always compare greater
121         than other valid entries with equal entryBytes */
122     LAST_VALID,
123     /** comparable type indicating this Entry should always compare greater
124         than valid RowIds */
125     ALWAYS_LAST;
126   }
127 
128   /** Support status for an index */
129   private enum IndexStatus {
130     /** index is fully functional for both reads and writes */
131     VALID,
132     /** index was written with a fallback sort order; writes are allowed but
133         the index is not suitable for lookups */
134     BROKEN_WRITE,
135     /** index cannot be written */
136     READ_ONLY
137   }
138 
139   public static final Comparator<byte[]> BYTE_CODE_COMPARATOR =
140     new Comparator<byte[]>() {
141       @Override
142       public int compare(byte[] left, byte[] right) {
143         if(left == right) {
144           return 0;
145         }
146         if(left == null) {
147           return -1;
148         }
149         if(right == null) {
150           return 1;
151         }
152 
153         int len = Math.min(left.length, right.length);
154         int pos = 0;
155         while((pos < len) && (left[pos] == right[pos])) {
156           ++pos;
157         }
158         if(pos < len) {
159           return ((ByteUtil.asUnsignedByte(left[pos]) <
160                    ByteUtil.asUnsignedByte(right[pos])) ? -1 : 1);
161         }
162         return ((left.length < right.length) ? -1 :
163                 ((left.length > right.length) ? 1 : 0));
164       }
165     };
166 
167 
168   /** name, generated on demand */
169   private String _name;
170   /** owning table */
171   private final TableImpl _table;
172   /** 0-based index data number */
173   private final int _number;
174   /** Page number of the root index data */
175   private int _rootPageNumber;
176   /** offset within the tableDefinition buffer of the uniqueEntryCount for
177       this index */
178   private final int _uniqueEntryCountOffset;
179   /** The number of unique entries which have been added to this index.  note,
180       however, that it is never decremented, only incremented (as observed in
181       Access). */
182   private int _uniqueEntryCount;
183   /** List of columns and flags */
184   private final List<ColumnDescriptor> _columns =
185     new ArrayList<>();
186   /** the logical indexes which this index data backs */
187   private final List<Index> _indexes = new ArrayList<>();
188   /** flags for this index */
189   private byte _indexFlags;
190   /** Usage map of pages that this index owns */
191   private UsageMap _ownedPages;
192   /** <code>true</code> if the index entries have been initialized,
193       <code>false</code> otherwise */
194   private boolean _initialized;
195   /** modification count for the table, keeps cursors up-to-date */
196   private int _modCount;
197   /** temp buffer used to read/write the index pages */
198   private final TempBufferHolder _indexBufferH =
199     TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true);
200   /** temp buffer used to create index entries */
201   private ByteStream _entryBuffer;
202   /** max size for all the entries written to a given index data page */
203   private final int _maxPageEntrySize;
204   /** whether or not this index data is backing a primary key logical index */
205   private boolean _primaryKey;
206   /** current support status of this index */
207   private IndexStatus _status = IndexStatus.VALID;
208   /** if non-null, the reason why this index has a non-VALID status */
209   private String _unsupportedReason;
210   /** Cache which manages the index pages */
211   private final IndexPageCache _pageCache;
212 
213   protected IndexData(TableImpl table, int number, int uniqueEntryCount,
214                       int uniqueEntryCountOffset)
215   {
216     _table  = table;
217     _number = number;
218     _uniqueEntryCount = uniqueEntryCount;
219     _uniqueEntryCountOffset = uniqueEntryCountOffset;
220     _maxPageEntrySize = calcMaxPageEntrySize(_table.getFormat());
221     _pageCache = new IndexPageCache(this);
222   }
223 
224   /**
225    * Creates an IndexData appropriate for the given table, using information
226    * from the given table definition buffer.
227    */
228   public static IndexData create(TableImpl table, ByteBuffer tableBuffer,
229                                  int number, JetFormat format)
230   {
231     int uniqueEntryCountOffset =
232       (format.OFFSET_INDEX_DEF_BLOCK +
233        (number * format.SIZE_INDEX_DEFINITION) + 4);
234     int uniqueEntryCount = tableBuffer.getInt(uniqueEntryCountOffset);
235 
236     return new IndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset);
237   }
238 
239   public String getName() {
240     if(_name == null) {
241       if(_indexes.size() == 1) {
242         _name = _indexes.get(0).getName();
243       } else if(!_indexes.isEmpty()) {
244         List<String> names = new ArrayList<>(_indexes.size());
245         for(Index idx : _indexes) {
246           names.add(idx.getName());
247         }
248         _name = names.toString();
249       } else {
250         _name = String.valueOf(_number);
251       }
252     }
253     return _name;
254   }
255 
256   public TableImpl getTable() {
257     return _table;
258   }
259 
260   public JetFormat getFormat() {
261     return getTable().getFormat();
262   }
263 
264   public PageChannel getPageChannel() {
265     return getTable().getPageChannel();
266   }
267 
268   /**
269    * @return the "main" logical index which is backed by this data.
270    */
271   public Index getPrimaryIndex() {
272     return _indexes.get(0);
273   }
274 
275   /**
276    * @return All of the Indexes backed by this data (unmodifiable List)
277    */
278   public List<Index> getIndexes() {
279     return Collections.unmodifiableList(_indexes);
280   }
281 
282   /**
283    * Adds a logical index which this data is backing.
284    */
285   void addIndex(Index index) {
286 
287     // we keep foreign key indexes at the back of the list.  this way the
288     // primary index will be a non-foreign key index (if any)
289     if(index.isForeignKey()) {
290       _indexes.add(index);
291     } else {
292       int pos = _indexes.size();
293       while(pos > 0) {
294         if(!_indexes.get(pos - 1).isForeignKey()) {
295           break;
296         }
297         --pos;
298       }
299       _indexes.add(pos, index);
300 
301       // also, keep track of whether or not this is a primary key index
302       _primaryKey |= index.isPrimaryKey();
303     }
304 
305     // force name to be regenerated
306     _name = null;
307   }
308 
309   public byte getIndexFlags() {
310     return _indexFlags;
311   }
312 
313   public int getIndexDataNumber() {
314     return _number;
315   }
316 
317   public int getUniqueEntryCount() {
318     return _uniqueEntryCount;
319   }
320 
321   public int getUniqueEntryCountOffset() {
322     return _uniqueEntryCountOffset;
323   }
324 
325   protected boolean isBackingPrimaryKey() {
326     return _primaryKey;
327   }
328 
329   /**
330    * Whether or not {@code null} values are actually recorded in the index.
331    */
332   public boolean shouldIgnoreNulls() {
333     return((_indexFlags & IGNORE_NULLS_INDEX_FLAG) != 0);
334   }
335 
336   /**
337    * Whether or not index entries must be unique.
338    * <p>
339    * Some notes about uniqueness:
340    * <ul>
341    * <li>Access does not seem to consider multiple {@code null} entries
342    *     invalid for a unique index</li>
343    * <li>text indexes collapse case, and Access seems to compare <b>only</b>
344    *     the index entry bytes, therefore two strings which differ only in
345    *     case <i>will violate</i> the unique constraint</li>
346    * </ul>
347    */
348   public boolean isUnique() {
349     return(isBackingPrimaryKey() || ((_indexFlags & UNIQUE_INDEX_FLAG) != 0));
350   }
351 
352   /**
353    * Whether or not values are required in the columns.
354    */
355   public boolean isRequired() {
356     return((_indexFlags & REQUIRED_INDEX_FLAG) != 0);
357   }
358 
359   /**
360    * Returns the Columns for this index (unmodifiable)
361    */
362   public List<ColumnDescriptor> getColumns() {
363     return Collections.unmodifiableList(_columns);
364   }
365 
366   public int getColumnCount() {
367     return _columns.size();
368   }
369 
370   /**
371    * Whether or not the complete index state has been read.
372    */
373   public boolean isInitialized() {
374     return _initialized;
375   }
376 
377   protected int getRootPageNumber() {
378     return _rootPageNumber;
379   }
380 
381   private void setUnsupportedReason(String reason, IndexStatus status,
382                                     ColumnImpl col) {
383     _status = status;
384     _unsupportedReason = withErrorContext(reason);
385     String suffix = (status == IndexStatus.READ_ONLY) ?
386       "making read-only" : "index not suitable for lookups";
387     if(!col.getTable().isSystem()) {
388       LOG.log(Logger.Level.WARNING, _unsupportedReason + ", " + suffix);
389     } else {
390       if(LOG.isLoggable(Logger.Level.DEBUG)) {
391         LOG.log(Logger.Level.DEBUG, _unsupportedReason + ", " + suffix);
392       }
393     }
394   }
395 
396   String getUnsupportedReason() {
397     return _unsupportedReason;
398   }
399 
400   boolean isValid() {
401     return _status == IndexStatus.VALID;
402   }
403 
404   boolean isReadOnly() {
405     return _status == IndexStatus.READ_ONLY;
406   }
407 
408   protected int getMaxPageEntrySize() {
409     return _maxPageEntrySize;
410   }
411 
412   /**
413    * Returns the number of database pages owned by this index data.
414    * @usage _intermediate_method_
415    */
416   public int getOwnedPageCount() {
417     return _ownedPages.getPageCount();
418   }
419 
420   void addOwnedPage(int pageNumber) throws IOException {
421     _ownedPages.addPageNumber(pageNumber);
422   }
423 
424   void removeOwnedPage(int pageNumber) throws IOException {
425     _ownedPages.removePageNumber(pageNumber);
426   }
427 
428   void collectUsageMapPages(Collection<Integer> pages) {
429     pages.add(_ownedPages.getTablePageNumber());
430   }
431 
432   /**
433    * Used by unit tests to validate the internal status of the index.
434    * @param forceLoad if {@code false} only validate currently loaded index
435    *                  data pages, otherwise, load and validate all index pages
436    * @usage _advanced_method_
437    */
438   public void validate(boolean forceLoad) throws IOException {
439     initialize();
440     _pageCache.validate(forceLoad);
441   }
442 
443   /**
444    * Returns the number of index entries in the index.  Only called by unit
445    * tests.
446    * <p>
447    * Forces index initialization.
448    * @usage _advanced_method_
449    */
450   public int getEntryCount()
451     throws IOException
452   {
453     initialize();
454     EntryCursor cursor = cursor();
455     Entry endEntry = cursor.getLastEntry();
456     int count = 0;
457     while(!endEntry.equals(cursor.getNextEntry())) {
458       ++count;
459     }
460     return count;
461   }
462 
463   /**
464    * Forces initialization of this index (actual parsing of index pages).
465    * normally, the index will not be initialized until the entries are
466    * actually needed.
467    */
468   public void initialize() throws IOException {
469     if(!_initialized) {
470       _pageCache.setRootPageNumber(getRootPageNumber());
471       _initialized = true;
472     }
473   }
474 
475   /**
476    * Writes the current index state to the database.
477    * <p>
478    * Forces index initialization.
479    */
480   public void update() throws IOException
481   {
482     // make sure we've parsed the entries
483     initialize();
484 
485     if(isReadOnly()) {
486       throw new UnsupportedOperationException(
487           "Cannot write indexes of this type due to " + _unsupportedReason);
488     }
489     _pageCache.write();
490   }
491 
492   /**
493    * Read the rest of the index info from a tableBuffer
494    * @param tableBuffer table definition buffer to read from initial info
495    * @param availableColumns Columns that this index may use
496    */
497   public void read(ByteBuffer tableBuffer, List<ColumnImpl> availableColumns)
498     throws IOException
499   {
500     ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown
501 
502     for (int i = 0; i < MAX_COLUMNS; i++) {
503       short columnNumber = tableBuffer.getShort();
504       byte colFlags = tableBuffer.get();
505       if (columnNumber != COLUMN_UNUSED) {
506         // find the desired column by column number (which is not necessarily
507         // the same as the column index)
508         ColumnImpl idxCol = null;
509         for(ColumnImpl col : availableColumns) {
510           if(col.getColumnNumber() == columnNumber) {
511             idxCol = col;
512             break;
513           }
514         }
515         if(idxCol == null) {
516           throw new IOException(withErrorContext(
517                   "Could not find column with number "
518                   + columnNumber + " for index"));
519         }
520         _columns.add(newColumnDescriptor(idxCol, colFlags));
521       }
522     }
523 
524     _ownedPages = UsageMap.read(getTable().getDatabase(), tableBuffer);
525 
526     _rootPageNumber = tableBuffer.getInt();
527 
528     ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_FLAGS); //Forward past Unknown
529     _indexFlags = tableBuffer.get();
530     ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_FLAGS); //Forward past other stuff
531   }
532 
533   /**
534    * Writes the index row count definitions into a table definition buffer.
535    * @param creator description of the indexes to write
536    * @param buffer Buffer to write to
537    */
538   protected static void writeRowCountDefinitions(
539       TableCreator creator, ByteBuffer buffer)
540   {
541     writeRowCountDefinitions(creator, buffer, creator.getIndexCount());
542   }
543 
544   /**
545    * Writes the index row count definitions into a table definition buffer.
546    * @param creator description of the indexes to write
547    * @param buffer Buffer to write to
548    * @param idxCount num indexes to write
549    */
550   protected static void writeRowCountDefinitions(
551       TableMutator creator, ByteBuffer buffer, int idxCount)
552   {
553     // index row counts (empty data)
554     ByteUtil.forward(buffer, (idxCount *
555                               creator.getFormat().SIZE_INDEX_DEFINITION));
556   }
557 
558   /**
559    * Writes the index definitions into a table definition buffer.
560    * @param creator description of the indexes to write
561    * @param buffer Buffer to write to
562    */
563   protected static void writeDefinitions(
564       TableCreator creator, ByteBuffer buffer)
565     throws IOException
566   {
567     ByteBuffer rootPageBuffer = createRootPageBuffer(creator);
568 
569     for(TableMutator.IndexDataState idxDataState : creator.getIndexDataStates()) {
570       writeDefinition(creator, buffer, idxDataState, rootPageBuffer);
571     }
572   }
573 
574   /**
575    * Writes the index definitions into a table definition buffer.
576    * @param creator description of the indexes to write
577    * @param buffer Buffer to write to
578    */
579   @SuppressWarnings("resource")
580   protected static void writeDefinition(
581       TableMutator creator, ByteBuffer buffer,
582       TableMutator.IndexDataState idxDataState, ByteBuffer rootPageBuffer)
583     throws IOException
584   {
585     if(rootPageBuffer == null) {
586       rootPageBuffer = createRootPageBuffer(creator);
587     }
588 
589     buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value
590 
591     // write column information (always MAX_COLUMNS entries)
592     IndexBuilder idx = idxDataState.getFirstIndex();
593     List<IndexBuilder.Column> idxColumns = idx.getColumns();
594     boolean isComplexIndex = false;
595     for(int i = 0; i < MAX_COLUMNS; ++i) {
596 
597       short columnNumber = COLUMN_UNUSED;
598       byte flags = 0;
599 
600       if(i < idxColumns.size()) {
601 
602         // determine column info
603         IndexBuilder.Column idxCol = idxColumns.get(i);
604         flags = idxCol.getFlags();
605 
606         // find actual table column number
607         columnNumber = creator.getColumnNumber(idxCol.getName());
608         isComplexIndex |= creator.isComplexColumn(idxCol.getName());
609         if(columnNumber == COLUMN_UNUSED) {
610           // should never happen as this is validated before
611           throw new IllegalArgumentException(
612               withErrorContext(
613                   "Column with name " + idxCol.getName() + " not found",
614                   creator.getDatabase(), creator.getTableName(), idx.getName()));
615         }
616       }
617 
618       buffer.putShort(columnNumber); // table column number
619       buffer.put(flags); // column flags (e.g. ordering)
620     }
621 
622     buffer.put(idxDataState.getUmapRowNumber()); // umap row
623     ByteUtil.put3ByteInt(buffer, idxDataState.getUmapPageNumber()); // umap page
624 
625     // write empty root index page
626     creator.getPageChannel().writePage(rootPageBuffer,
627                                        idxDataState.getRootPageNumber());
628 
629     buffer.putInt(idxDataState.getRootPageNumber());
630     buffer.putInt(0); // 4 bytes access leaves as page residue
631     buffer.put(idx.getFlags()); // index flags (unique, etc.)
632     buffer.put(isComplexIndex ? COMPLEX_INDEX_MARKER : 0);
633     ByteUtil.forward(buffer, 4); // constant zero
634   }
635 
636   private static ByteBuffer createRootPageBuffer(TableMutator creator)
637     throws IOException
638   {
639     ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer();
640     writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE,
641                   creator.getTdefPageNumber(), creator.getFormat());
642     return rootPageBuffer;
643   }
644 
645   /**
646    * Prepares to add a row to this index.  All constraints are checked before
647    * this method returns.
648    * <p>
649    * Forces index initialization.
650    *
651    * @param row Row to add
652    * @param rowId rowId of the row to be added
653    *
654    * @return a PendingChange which can complete the addition or roll it back
655    */
656   public PendingChange prepareAddRow(Object[] row, RowIdImpl rowId,
657                                      PendingChange nextChange)
658     throws IOException
659   {
660     return prepareAddRow(row, rowId, new AddRowPendingChange(nextChange));
661   }
662 
663   private PendingChange prepareAddRow(Object[] row, RowIdImpl rowId,
664                                       AddRowPendingChange change)
665     throws IOException
666   {
667     int nullCount = countNullValues(row);
668     boolean isNullEntry = (nullCount == _columns.size());
669     if(shouldIgnoreNulls() && isNullEntry) {
670       // nothing to do
671       return change;
672     }
673     if((nullCount > 0) && (isBackingPrimaryKey() || isRequired())) {
674       throw new ConstraintViolationException(withErrorContext(
675           "Null value found in row " + Arrays.asList(row) +
676           " for primary key or required index"));
677     }
678 
679     // make sure we've parsed the entries
680     initialize();
681 
682     return prepareAddEntry(new Entry(createEntryBytes(row), rowId), isNullEntry,
683                            row, change);
684   }
685 
686   /**
687    * Adds an entry to the correct index dataPage, maintaining the order.
688    */
689   private PendingChange prepareAddEntry(Entry newEntry, boolean isNullEntry,
690                                         Object[] row, AddRowPendingChange change)
691     throws IOException
692   {
693     DataPage dataPage = findDataPage(newEntry);
694     int idx = dataPage.findEntry(newEntry);
695     if(idx < 0) {
696 
697       // this is a new entry
698       idx = missingIndexToInsertionPoint(idx);
699 
700       Position newPos = new Position(dataPage, idx, newEntry, true);
701       Position nextPos = getNextPosition(newPos);
702       Position prevPos = getPreviousPosition(newPos);
703 
704       // determine if the addition of this entry would break the uniqueness
705       // constraint.  See isUnique() for some notes about uniqueness as
706       // defined by Access.
707       boolean isDupeEntry =
708         (((nextPos != null) &&
709           newEntry.equalsEntryBytes(nextPos.getEntry())) ||
710           ((prevPos != null) &&
711            newEntry.equalsEntryBytes(prevPos.getEntry())));
712       if(isUnique() && !isNullEntry && isDupeEntry) {
713         throw new ConstraintViolationException(withErrorContext(
714             "New row " + Arrays.asList(row) +
715             " violates uniqueness constraint for index"));
716       }
717 
718       change.setAddRow(newEntry, dataPage, idx, isDupeEntry);
719 
720     } else {
721 
722       change.setOldRow(newEntry);
723     }
724     return change;
725   }
726 
727   /**
728    * Completes a prepared row addition.
729    */
730   private void commitAddRow(Entry newEntry, DataPage dataPage, int idx,
731                             boolean isDupeEntry, Entry oldEntry)
732     throws IOException
733   {
734     if(newEntry != null) {
735       dataPage.addEntry(idx, newEntry);
736       // if we are adding a duplicate entry, or replacing an existing entry,
737       // then the unique entry count doesn't change
738       if(!isDupeEntry && (oldEntry == null)) {
739         ++_uniqueEntryCount;
740       }
741       ++_modCount;
742     } else {
743       LOG.log(Logger.Level.WARNING, withErrorContext("Added duplicate index entry " + oldEntry));
744     }
745   }
746 
747   /**
748    * Prepares to update a row in this index.  All constraints are checked
749    * before this method returns.
750    * <p>
751    * Forces index initialization.
752    *
753    * @param oldRow Row to be removed
754    * @param newRow Row to be added
755    * @param rowId rowId of the row to be updated
756    *
757    * @return a PendingChange which can complete the update or roll it back
758    */
759   public PendingChange prepareUpdateRow(Object[] oldRow, RowIdImpl rowId,
760                                         Object[] newRow,
761                                         PendingChange nextChange)
762     throws IOException
763   {
764     UpdateRowPendingChange change = new UpdateRowPendingChange(nextChange);
765     change.setOldRow(deleteRowImpl(oldRow, rowId));
766 
767     try {
768       prepareAddRow(newRow, rowId, change);
769       return change;
770     } catch(ConstraintViolationException e) {
771       // need to undo the deletion before bailing
772       change.rollback();
773       throw e;
774     }
775   }
776 
777   /**
778    * Removes a row from this index
779    * <p>
780    * Forces index initialization.
781    *
782    * @param row Row to remove
783    * @param rowId rowId of the row to be removed
784    */
785   public void deleteRow(Object[] row, RowIdImpl rowId)
786     throws IOException
787   {
788     deleteRowImpl(row, rowId);
789   }
790 
791   private Entry deleteRowImpl(Object[] row, RowIdImpl rowId)
792     throws IOException
793   {
794     int nullCount = countNullValues(row);
795     if(shouldIgnoreNulls() && (nullCount == _columns.size())) {
796       // nothing to do
797       return null;
798     }
799 
800     // make sure we've parsed the entries
801     initialize();
802 
803     Entry oldEntry = new Entry(createEntryBytes(row), rowId);
804     Entry removedEntry = removeEntry(oldEntry);
805     if(removedEntry != null) {
806       ++_modCount;
807     } else {
808       LOG.log(Logger.Level.WARNING, withErrorContext(
809           "Failed removing index entry " + oldEntry + " for row: " +
810           Arrays.asList(row)));
811     }
812     return removedEntry;
813   }
814 
815   /**
816    * Undoes a previous row deletion.
817    */
818   private void rollbackDeletedRow(Entry removedEntry)
819     throws IOException
820   {
821     if(removedEntry == null) {
822       // no change was made
823       return;
824     }
825 
826     // unfortunately, stuff might have shuffled around when we first removed
827     // the row, so in order to re-insert it, we need to re-find and insert it.
828     DataPage dataPage = findDataPage(removedEntry);
829     int idx = dataPage.findEntry(removedEntry);
830     if(idx < 0) {
831       dataPage.addEntry(missingIndexToInsertionPoint(idx), removedEntry);
832     }
833   }
834 
835   /**
836    * Removes an entry from the relevant index dataPage, maintaining the order.
837    * Will search by RowId if entry is not found (in case a partial entry was
838    * provided).
839    */
840   private Entry removeEntry(Entry oldEntry)
841     throws IOException
842   {
843     DataPage dataPage = findDataPage(oldEntry);
844     int idx = dataPage.findEntry(oldEntry);
845     boolean doRemove = false;
846     if(idx < 0) {
847       // the caller may have only read some of the row data, if this is the
848       // case, just search for the page/row numbers
849       // TODO, we could force caller to get relevant values?
850       EntryCursor cursor = cursor();
851       Position tmpPos = null;
852       Position endPos = cursor._lastPos;
853       while(!endPos.equals(
854                 tmpPos = cursor.getAnotherPosition(CursorImpl.MOVE_FORWARD))) {
855         if(tmpPos.getEntry().getRowId().equals(oldEntry.getRowId())) {
856           dataPage = tmpPos.getDataPage();
857           idx = tmpPos.getIndex();
858           doRemove = true;
859           break;
860         }
861       }
862     } else {
863       doRemove = true;
864     }
865 
866     Entry removedEntry = null;
867     if(doRemove) {
868       // found it!
869       removedEntry = dataPage.removeEntry(idx);
870     }
871 
872     return removedEntry;
873   }
874 
875   public static void commitAll(PendingChange change) throws IOException {
876     while(change != null) {
877       change.commit();
878       change = change.getNext();
879     }
880   }
881 
882   public static void rollbackAll(PendingChange change) throws IOException {
883     while(change != null) {
884       change.rollback();
885       change = change.getNext();
886     }
887   }
888 
889   /**
890    * Gets a new cursor for this index.
891    * <p>
892    * Forces index initialization.
893    */
894   public EntryCursor cursor()
895     throws IOException
896   {
897     return cursor(null, true, null, true);
898   }
899 
900   /**
901    * Gets a new cursor for this index, narrowed to the range defined by the
902    * given startRow and endRow.
903    * <p>
904    * Forces index initialization.
905    *
906    * @param startRow the first row of data for the cursor, or {@code null} for
907    *                 the first entry
908    * @param startInclusive whether or not startRow is inclusive or exclusive
909    * @param endRow the last row of data for the cursor, or {@code null} for
910    *               the last entry
911    * @param endInclusive whether or not endRow is inclusive or exclusive
912    */
913   public EntryCursor cursor(Object[] startRow,
914                             boolean startInclusive,
915                             Object[] endRow,
916                             boolean endInclusive)
917     throws IOException
918   {
919     initialize();
920     Entry startEntry = FIRST_ENTRY;
921     byte[] startEntryBytes = null;
922     if(startRow != null) {
923       startEntryBytes = createEntryBytes(startRow);
924       startEntry = new Entry(startEntryBytes,
925                              (startInclusive ?
926                               RowIdImpl.FIRST_ROW_ID : RowIdImpl.LAST_ROW_ID));
927     }
928     Entry endEntry = LAST_ENTRY;
929     if(endRow != null) {
930       // reuse startEntryBytes if startRow and endRow are same array.  this is
931       // common for "lookup" code
932       byte[] endEntryBytes = ((startRow == endRow) ?
933                               startEntryBytes :
934                               createEntryBytes(endRow));
935       endEntry = new Entry(endEntryBytes,
936                            (endInclusive ?
937                             RowIdImpl.LAST_ROW_ID : RowIdImpl.FIRST_ROW_ID));
938     }
939     return new EntryCursor(findEntryPosition(startEntry),
940                            findEntryPosition(endEntry));
941   }
942 
943   private Position findEntryPosition(Entry entry)
944     throws IOException
945   {
946     DataPage dataPage = findDataPage(entry);
947     int idx = dataPage.findEntry(entry);
948     boolean between = false;
949     if(idx < 0) {
950       // given entry was not found exactly.  our current position is now
951       // really between two indexes, but we cannot support that as an integer
952       // value, so we set a flag instead
953       idx = missingIndexToInsertionPoint(idx);
954       between = true;
955     }
956     return new Position(dataPage, idx, entry, between);
957   }
958 
959   private Position getNextPosition(Position curPos)
960     throws IOException
961   {
962     // get the next index (between-ness is handled internally)
963     int nextIdx = curPos.getNextIndex();
964     Position nextPos = null;
965     if(nextIdx < curPos.getDataPage().getEntries().size()) {
966       nextPos = new Position(curPos.getDataPage(), nextIdx);
967     } else {
968       int nextPageNumber = curPos.getDataPage().getNextPageNumber();
969       DataPage nextDataPage = null;
970       while(nextPageNumber != INVALID_INDEX_PAGE_NUMBER) {
971         DataPage dp = getDataPage(nextPageNumber);
972         if(!dp.isEmpty()) {
973           nextDataPage = dp;
974           break;
975         }
976         nextPageNumber = dp.getNextPageNumber();
977       }
978       if(nextDataPage != null) {
979         nextPos = new Position(nextDataPage, 0);
980       }
981     }
982     return nextPos;
983   }
984 
985   /**
986    * Returns the Position before the given one, or {@code null} if none.
987    */
988   private Position getPreviousPosition(Position curPos)
989     throws IOException
990   {
991     // get the previous index (between-ness is handled internally)
992     int prevIdx = curPos.getPrevIndex();
993     Position prevPos = null;
994     if(prevIdx >= 0) {
995       prevPos = new Position(curPos.getDataPage(), prevIdx);
996     } else {
997       int prevPageNumber = curPos.getDataPage().getPrevPageNumber();
998       DataPage prevDataPage = null;
999       while(prevPageNumber != INVALID_INDEX_PAGE_NUMBER) {
1000         DataPage dp = getDataPage(prevPageNumber);
1001         if(!dp.isEmpty()) {
1002           prevDataPage = dp;
1003           break;
1004         }
1005         prevPageNumber = dp.getPrevPageNumber();
1006       }
1007       if(prevDataPage != null) {
1008         prevPos = new Position(prevDataPage,
1009                                (prevDataPage.getEntries().size() - 1));
1010       }
1011     }
1012     return prevPos;
1013   }
1014 
1015   /**
1016    * Returns the valid insertion point for an index indicating a missing
1017    * entry.
1018    */
1019   protected static int missingIndexToInsertionPoint(int idx) {
1020     return -(idx + 1);
1021   }
1022 
1023   /**
1024    * Constructs an array of values appropriate for this index from the given
1025    * column values, expected to match the columns for this index.
1026    * @return the appropriate sparse array of data
1027    * @throws IllegalArgumentException if the wrong number of values are
1028    *         provided
1029    */
1030   public Object[] constructIndexRowFromEntry(Object... values)
1031   {
1032     if(values.length != _columns.size()) {
1033       throw new IllegalArgumentException(withErrorContext(
1034           "Wrong number of column values given " + values.length +
1035           ", expected " + _columns.size()));
1036     }
1037     int valIdx = 0;
1038     Object[] idxRow = new Object[getTable().getColumnCount()];
1039     for(ColumnDescriptor col : _columns) {
1040       idxRow[col.getColumnIndex()] = values[valIdx++];
1041     }
1042     return idxRow;
1043   }
1044 
1045   /**
1046    * Constructs an array of values appropriate for this index from the given
1047    * column values, possibly only providing a prefix subset of the index
1048    * columns (at least one value must be provided).  If a prefix entry is
1049    * provided, any missing, trailing index entry values will use the given
1050    * filler value.
1051    * @return the appropriate sparse array of data
1052    * @throws IllegalArgumentException if at least one value is not provided
1053    */
1054   public Object[] constructPartialIndexRowFromEntry(
1055       Object filler, Object... values)
1056   {
1057     if(values.length == 0) {
1058       throw new IllegalArgumentException(withErrorContext(
1059           "At least one column value must be provided"));
1060     }
1061     if(values.length > _columns.size()) {
1062       throw new IllegalArgumentException(withErrorContext(
1063           "Too many column values given " + values.length +
1064           ", expected at most " + _columns.size()));
1065     }
1066     int valIdx = 0;
1067     Object[] idxRow = new Object[getTable().getColumnCount()];
1068     for(ColumnDescriptor col : _columns) {
1069       idxRow[col.getColumnIndex()] =
1070         ((valIdx < values.length) ? values[valIdx] : filler);
1071       ++valIdx;
1072     }
1073     return idxRow;
1074   }
1075 
1076   /**
1077    * Constructs an array of values appropriate for this index from the given
1078    * column value.
1079    * @return the appropriate sparse array of data or {@code null} if not all
1080    *         columns for this index were provided
1081    */
1082   public Object[] constructIndexRow(String colName, Object value)
1083   {
1084     return constructIndexRow(Collections.singletonMap(colName, value));
1085   }
1086 
1087   /**
1088    * Constructs an array of values appropriate for this index from the given
1089    * column value, which must be the first column of the index.  Any missing,
1090    * trailing index entry values will use the given filler value.
1091    * @return the appropriate sparse array of data or {@code null} if no prefix
1092    *         list of columns for this index were provided
1093    */
1094   public Object[] constructPartialIndexRow(Object filler, String colName, Object value)
1095   {
1096     return constructPartialIndexRow(filler, Collections.singletonMap(colName, value));
1097   }
1098 
1099   /**
1100    * Constructs an array of values appropriate for this index from the given
1101    * column values.
1102    * @return the appropriate sparse array of data or {@code null} if not all
1103    *         columns for this index were provided
1104    */
1105   public Object[] constructIndexRow(Map<String,?> row)
1106   {
1107     for(ColumnDescriptor col : _columns) {
1108       if(!row.containsKey(col.getName())) {
1109         return null;
1110       }
1111     }
1112 
1113     Object[] idxRow = new Object[getTable().getColumnCount()];
1114     for(ColumnDescriptor col : _columns) {
1115       idxRow[col.getColumnIndex()] = row.get(col.getName());
1116     }
1117     return idxRow;
1118   }
1119 
1120   /**
1121    * Constructs an array of values appropriate for this index from the given
1122    * column values, possibly only using a subset of the given values.  A
1123    * partial row can be created if one or more prefix column values are
1124    * provided.  If a prefix can be found, any missing, trailing index entry
1125    * values will use the given filler value.
1126    * @return the appropriate sparse array of data or {@code null} if no prefix
1127    *         list of columns for this index were provided
1128    */
1129   public Object[] constructPartialIndexRow(Object filler, Map<String,?> row)
1130   {
1131     // see if we have at least one prefix column
1132     int numCols = 0;
1133     for(ColumnDescriptor col : _columns) {
1134       if(!row.containsKey(col.getName())) {
1135         if(numCols == 0) {
1136           // can't do it, need at least first column
1137           return null;
1138         }
1139         break;
1140       }
1141       ++numCols;
1142     }
1143 
1144     // fill in the row with either the prefix values or the filler value, as
1145     // appropriate
1146     Object[] idxRow = new Object[getTable().getColumnCount()];
1147     int valIdx = 0;
1148     for(ColumnDescriptor col : _columns) {
1149       idxRow[col.getColumnIndex()] =
1150         ((valIdx < numCols) ? row.get(col.getName()) : filler);
1151       ++valIdx;
1152     }
1153     return idxRow;
1154   }
1155 
1156   @Override
1157   public String toString() {
1158     ToStringBuilder sb = ToStringBuilder.builder(this)
1159       .append("dataNumber", _number)
1160       .append("pageNumber", _rootPageNumber)
1161       .append("isBackingPrimaryKey", isBackingPrimaryKey())
1162       .append("isUnique", isUnique())
1163       .append("ignoreNulls", shouldIgnoreNulls())
1164       .append("isRequired", isRequired())
1165       .append("columns", _columns)
1166       .append("initialized", _initialized);
1167     if(_initialized) {
1168       try {
1169         sb.append("entryCount", getEntryCount());
1170       } catch(IOException e) {
1171         throw new UncheckedIOException(e);
1172       }
1173     }
1174     sb.append("pageCache", _pageCache);
1175     return sb.toString();
1176   }
1177 
1178   /**
1179    * Write the given index page out to a buffer
1180    */
1181   protected void writeDataPage(DataPage dataPage)
1182     throws IOException
1183   {
1184     if(dataPage.getCompressedEntrySize() > _maxPageEntrySize) {
1185       throw new IllegalStateException(withErrorContext("data page is too large"));
1186     }
1187 
1188     ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());
1189 
1190     writeDataPage(buffer, dataPage, getTable().getTableDefPageNumber(),
1191                   getFormat());
1192 
1193     getPageChannel().writePage(buffer, dataPage.getPageNumber());
1194   }
1195 
1196   /**
1197    * Writes the data page info to the given buffer.
1198    */
1199   protected static void writeDataPage(ByteBuffer buffer, DataPage dataPage,
1200                                       int tdefPageNumber, JetFormat format)
1201     throws IOException
1202   {
1203     buffer.put(dataPage.isLeaf() ?
1204                PageTypes.INDEX_LEAF :
1205                PageTypes.INDEX_NODE );  //Page type
1206     buffer.put((byte) 0x01);  // constant 1 on every page type
1207     buffer.putShort((short) 0); //Free space
1208     buffer.putInt(tdefPageNumber);
1209 
1210     buffer.putInt(0); // write stamp, see TableImpl.newDataPage
1211     buffer.putInt(dataPage.getPrevPageNumber()); //Prev page
1212     buffer.putInt(dataPage.getNextPageNumber()); //Next page
1213     buffer.putInt(dataPage.getChildTailPageNumber()); //ChildTail page
1214 
1215     byte[] entryPrefix = dataPage.getEntryPrefix();
1216     buffer.putShort((short) entryPrefix.length); // entry prefix byte count
1217     buffer.put((byte)dataPage.getLevel()); // level in the index tree
1218 
1219     byte[] entryMask = new byte[format.SIZE_INDEX_ENTRY_MASK];
1220     // first entry includes the prefix
1221     int totalSize = entryPrefix.length;
1222     for(Entry entry : dataPage.getEntries()) {
1223       totalSize += (entry.size() - entryPrefix.length);
1224       int idx = totalSize / 8;
1225       entryMask[idx] |= (1 << (totalSize % 8));
1226     }
1227     buffer.put(entryMask);
1228 
1229     // first entry includes the prefix
1230     buffer.put(entryPrefix);
1231 
1232     for(Entry entry : dataPage.getEntries()) {
1233       entry.write(buffer, entryPrefix);
1234     }
1235 
1236     // update free space
1237     buffer.putShort(2, (short) (format.PAGE_SIZE - buffer.position()));
1238   }
1239 
1240   /**
1241    * Reads an index page, populating the correct collection based on the page
1242    * type (node or leaf).
1243    */
1244   protected void readDataPage(DataPage dataPage)
1245     throws IOException
1246   {
1247     ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());
1248     getPageChannel().readPage(buffer, dataPage.getPageNumber());
1249 
1250     boolean isLeaf = isLeafPage(buffer);
1251     dataPage.setLeaf(isLeaf);
1252     dataPage.setLevel(readLevel(buffer, getFormat()));
1253 
1254     // note, "header" data is in LITTLE_ENDIAN format, entry data is in
1255     // BIG_ENDIAN format
1256     // one byte on jet 3 and two on jet 4, with the page level in the byte
1257     // after
1258     int entryPrefixLength = ByteUtil.getUnsignedVarInt(
1259         buffer, getFormat().OFFSET_INDEX_COMPRESSED_BYTE_COUNT,
1260         getFormat().SIZE_INDEX_COMPRESSED_BYTE_COUNT);
1261     int entryMaskLength = getFormat().SIZE_INDEX_ENTRY_MASK;
1262     int entryMaskPos = getFormat().OFFSET_INDEX_ENTRY_MASK;
1263     int entryPos = entryMaskPos + entryMaskLength;
1264     int lastStart = 0;
1265     int totalEntrySize = 0;
1266     byte[] entryPrefix = null;
1267     List<Entry> entries = new ArrayList<>();
1268     TempBufferHolder tmpEntryBufferH =
1269       TempBufferHolder.newHolder(TempBufferHolder.Type.HARD, true,
1270                                  ENTRY_BYTE_ORDER);
1271 
1272     Entry prevEntry = FIRST_ENTRY;
1273     for (int i = 0; i < entryMaskLength; i++) {
1274       byte entryMask = buffer.get(entryMaskPos + i);
1275       for (int j = 0; j < 8; j++) {
1276         if ((entryMask & (1 << j)) != 0) {
1277           int length = (i * 8) + j - lastStart;
1278           buffer.position(entryPos + lastStart);
1279 
1280           // determine if we can read straight from the index page (if no
1281           // entryPrefix).  otherwise, create temp buf with complete entry.
1282           ByteBuffer curEntryBuffer = buffer;
1283           int curEntryLen = length;
1284           if(entryPrefix != null) {
1285             curEntryBuffer = getTempEntryBuffer(
1286                 buffer, length, entryPrefix, tmpEntryBufferH);
1287             curEntryLen += entryPrefix.length;
1288           }
1289           totalEntrySize += curEntryLen;
1290 
1291           Entry entry = newEntry(curEntryBuffer, curEntryLen, isLeaf);
1292           if(prevEntry.compareTo(entry) >= 0) {
1293             throw new IOException(withErrorContext(
1294                     "Unexpected order in index entries, " +
1295                     prevEntry + " >= " + entry));
1296           }
1297 
1298           entries.add(entry);
1299 
1300           if((entries.size() == 1) && (entryPrefixLength > 0)) {
1301             // read any shared entry prefix
1302             entryPrefix = new byte[entryPrefixLength];
1303             buffer.position(entryPos + lastStart);
1304             buffer.get(entryPrefix);
1305           }
1306 
1307           lastStart += length;
1308           prevEntry = entry;
1309         }
1310       }
1311     }
1312 
1313     dataPage.setEntryPrefix(entryPrefix != null ? entryPrefix : EMPTY_PREFIX);
1314     dataPage.setEntries(entries);
1315     dataPage.setTotalEntrySize(totalEntrySize);
1316 
1317     int prevPageNumber = buffer.getInt(getFormat().OFFSET_PREV_INDEX_PAGE);
1318     int nextPageNumber = buffer.getInt(getFormat().OFFSET_NEXT_INDEX_PAGE);
1319     int childTailPageNumber =
1320       buffer.getInt(getFormat().OFFSET_CHILD_TAIL_INDEX_PAGE);
1321 
1322     dataPage.setPrevPageNumber(prevPageNumber);
1323     dataPage.setNextPageNumber(nextPageNumber);
1324     dataPage.setChildTailPageNumber(childTailPageNumber);
1325   }
1326 
1327   /**
1328    * Returns a new Entry of the correct type for the given data and page type.
1329    */
1330   private static Entry newEntry(ByteBuffer buffer, int entryLength,
1331                                 boolean isLeaf)
1332   {
1333     if(isLeaf) {
1334       return new Entry(buffer, entryLength);
1335     }
1336     return new NodeEntry(buffer, entryLength);
1337   }
1338 
1339   /**
1340    * Returns an entry buffer containing the relevant data for an entry given
1341    * the valuePrefix.
1342    */
1343   private ByteBuffer getTempEntryBuffer(
1344       ByteBuffer indexPage, int entryLen, byte[] valuePrefix,
1345       TempBufferHolder tmpEntryBufferH)
1346   {
1347     ByteBuffer tmpEntryBuffer = tmpEntryBufferH.getBuffer(
1348         getPageChannel(), valuePrefix.length + entryLen);
1349 
1350     // combine valuePrefix and rest of entry from indexPage, then prep for
1351     // reading
1352     tmpEntryBuffer.put(valuePrefix);
1353     tmpEntryBuffer.put(indexPage.array(), indexPage.position(), entryLen);
1354     tmpEntryBuffer.flip();
1355 
1356     return tmpEntryBuffer;
1357   }
1358 
1359   /**
1360    * Reads the level of an index page, 0 if the format does not record one.
1361    */
1362   private static int readLevel(ByteBuffer buffer, JetFormat format)
1363   {
1364     int levelOffset = format.OFFSET_INDEX_LEVEL;
1365     return ((levelOffset >= 0) ? ByteUtil.getUnsignedByte(buffer, levelOffset)
1366             : 0);
1367   }
1368 
1369   /**
1370    * Determines if the given index page is a leaf or node page.
1371    */
1372   private boolean isLeafPage(ByteBuffer buffer)
1373     throws IOException
1374   {
1375     byte pageType = buffer.get(0);
1376     if(pageType == PageTypes.INDEX_LEAF) {
1377       return true;
1378     } else if(pageType == PageTypes.INDEX_NODE) {
1379       return false;
1380     }
1381     throw new IOException(withErrorContext("Unexpected page type " + pageType));
1382   }
1383 
1384   /**
1385    * Determines the number of {@code null} values for this index from the
1386    * given row.
1387    */
1388   private int countNullValues(Object[] values)
1389   {
1390     if(values == null) {
1391       return _columns.size();
1392     }
1393 
1394     // annoyingly, the values array could come from different sources, one
1395     // of which will make it a different size than the other.  we need to
1396     // handle both situations.
1397     int nullCount = 0;
1398     for(ColumnDescriptor col : _columns) {
1399       Object value = values[col.getColumnIndex()];
1400       if(col.isNullValue(value)) {
1401         ++nullCount;
1402       }
1403     }
1404 
1405     return nullCount;
1406   }
1407 
1408   /**
1409    * Creates the entry bytes for a row of values, truncated and digested where
1410    * they run past the key length.
1411    */
1412   byte[] createEntryBytes(Object[] values) throws IOException
1413   {
1414     if(values == null) {
1415       return null;
1416     }
1417 
1418     if(_entryBuffer == null) {
1419       _entryBuffer = new ByteStream();
1420     }
1421     _entryBuffer.reset();
1422 
1423     for(ColumnDescriptor col : _columns) {
1424 
1425       Object value = values[col.getColumnIndex()];
1426       if(ColumnImpl.isRawData(value)) {
1427         // ignore it, we could not parse it
1428         continue;
1429       }
1430 
1431       if(value == MIN_VALUE) {
1432         // null is the "least" value (note the column "ascending" flag is
1433         // irrelevant here because the entry bytes are _always_ interpreted
1434         // least to greatest)
1435         _entryBuffer.write(getNullEntryFlag(true));
1436         continue;
1437       }
1438       if(value == MAX_VALUE) {
1439         // the opposite null is the "greatest" value (note the column
1440         // "ascending" flag is irrelevant here because the entry bytes are
1441         // _always_ interpreted least to greatest)
1442         _entryBuffer.write(getNullEntryFlag(false));
1443         continue;
1444       }
1445 
1446       col.writeValue(value, _entryBuffer);
1447     }
1448 
1449     return truncateEntryBytes(_entryBuffer.toByteArray(),
1450                               MAX_KEY_UNITS * getFormat().SIZE_TEXT_FIELD_UNIT);
1451   }
1452 
1453   /**
1454    * Truncates the given entry bytes if they exceed the maximum key length,
1455    * replacing the discarded bytes with a digest of them.  Access stores long
1456    * text keys this way, and search keys have to be built the same way to
1457    * compare against them.
1458    */
1459   static byte[] truncateEntryBytes(byte[] entryBytes, int maxLength)
1460   {
1461     if(entryBytes.length <= maxLength) {
1462       return entryBytes;
1463     }
1464     int kept = maxLength - KEY_DIGEST_LENGTH;
1465 
1466     // every discarded byte feeds the digest, but the last byte of the entry
1467     // is xored in rather than clocked through the table.  this holds whatever
1468     // the byte is: a text terminator, a weight an odd nibble left behind, or
1469     // the low byte of a number in the last column of a multi column index
1470     int digest = 0;
1471     for(int i = kept; i < (entryBytes.length - 1); ++i) {
1472       digest = ((digest >>> 8) ^ KEY_DIGEST_TABLE[digest & 0xFF] ^
1473                 (entryBytes[i] & 0xFF)) & 0xFFFF;
1474     }
1475     digest = (digest ^ ((entryBytes[entryBytes.length - 1] & 0xFF) << 8)) &
1476       0xFFFF;
1477 
1478     byte[] truncated = ByteUtil.copyOf(entryBytes, maxLength);
1479     truncated[kept] = (byte)(digest >>> 8);
1480     truncated[kept + 1] = (byte)digest;
1481 
1482     return truncated;
1483   }
1484 
1485   /**
1486    * Builds the transition table for the truncated key digest.  Byte swapping
1487    * the digest register turns the transition into the MSB-first CRC-16 step
1488    * for KEY_DIGEST_POLY, so the table is that step with the register left in
1489    * on-disk byte order.
1490    */
1491   private static int[] createKeyDigestTable()
1492   {
1493     int[] table = new int[256];
1494     for(int i = 0; i < table.length; ++i) {
1495       int reg = i << 8;
1496       for(int j = 0; j < 8; ++j) {
1497         reg = ((reg << 1) & 0xFFFF) ^
1498           (((reg & 0x8000) != 0) ? KEY_DIGEST_POLY : 0);
1499       }
1500       table[i] = ((reg & 0xFF) << 8) | (reg >>> 8);
1501     }
1502     return table;
1503   }
1504 
1505   /**
1506    * Finds the data page for the given entry.
1507    */
1508   protected DataPage findDataPage(Entry entry)
1509     throws IOException
1510   {
1511     return _pageCache.findCacheDataPage(entry);
1512   }
1513 
1514   /**
1515    * Gets the data page for the pageNumber.
1516    */
1517   protected DataPage getDataPage(int pageNumber)
1518     throws IOException
1519   {
1520     return _pageCache.getCacheDataPage(pageNumber);
1521   }
1522 
1523   /**
1524    * Flips the first bit in the byte at the given index.
1525    */
1526   private static byte[] flipFirstBitInByte(byte[] value, int index)
1527   {
1528     value[index] = (byte)(value[index] ^ 0x80);
1529 
1530     return value;
1531   }
1532 
1533   /**
1534    * Flips all the bits in the byte array.
1535    */
1536   private static byte[] flipBytes(byte[] value) {
1537     return flipBytes(value, 0, value.length);
1538   }
1539 
1540   /**
1541    * Flips the bits in the specified bytes in the byte array.
1542    */
1543   static byte[] flipBytes(byte[] value, int offset, int length) {
1544     for(int i = offset; i < (offset + length); ++i) {
1545       value[i] = (byte)(~value[i]);
1546     }
1547     return value;
1548   }
1549 
1550   /**
1551    * Writes the value of the given column type to a byte array and returns it.
1552    */
1553   private static byte[] encodeNumberColumnValue(Object value, ColumnImpl column)
1554     throws IOException
1555   {
1556     // always write in big endian order
1557     return column.write(value, 0, ENTRY_BYTE_ORDER).array();
1558   }
1559 
1560   /**
1561    * Writes a binary value using the general binary entry encoding rules.
1562    */
1563   private static void writeGeneralBinaryEntry(byte[] valueBytes, boolean isAsc,
1564                                               ByteStream bout)
1565   {
1566     int dataLen = valueBytes.length;
1567     int extraLen = (dataLen + 7) / 8;
1568     int entryLen = ((dataLen + extraLen + 8) / 9) * 9;
1569 
1570     // reserve space for the full entry
1571     bout.ensureNewCapacity(entryLen);
1572 
1573     // binary data is written in 8 byte segments with a trailing length byte.
1574     // The length byte is the amount of valid bytes in the segment (where 9
1575     // indicates that there is more data _after_ this segment).
1576     byte[] partialEntryBytes = new byte[9];
1577 
1578     // bit twiddling rules:
1579     // - isAsc  => nothing
1580     // - !isAsc => flipBytes, _but keep intermediate 09 unflipped_!
1581 
1582     // first, write any intermediate segements
1583     int segmentLen = dataLen;
1584     int pos = 0;
1585     while(segmentLen > 8) {
1586 
1587       System.arraycopy(valueBytes, pos, partialEntryBytes, 0, 8);
1588       if(!isAsc) {
1589         // note, we do _not_ flip the length byte for intermediate segments
1590         flipBytes(partialEntryBytes, 0, 8);
1591       }
1592 
1593       // we are writing intermediate segments (there is more data after this
1594       // segment), so the length is always 9.
1595       partialEntryBytes[8] = (byte)9;
1596 
1597       pos += 8;
1598       segmentLen -= 8;
1599 
1600       bout.write(partialEntryBytes);
1601     }
1602 
1603     // write the last segment (with slightly different rules)
1604     if(segmentLen > 0) {
1605 
1606       System.arraycopy(valueBytes, pos, partialEntryBytes, 0, segmentLen);
1607 
1608       // clear out an intermediate bytes between the real data and the final
1609       // length byte
1610       for(int i = segmentLen; i < 8; ++i) {
1611         partialEntryBytes[i] = 0;
1612       }
1613 
1614       partialEntryBytes[8] = (byte)segmentLen;
1615 
1616       if(!isAsc) {
1617         // note, we _do_ flip the last length byte
1618         flipBytes(partialEntryBytes, 0, 9);
1619       }
1620 
1621       bout.write(partialEntryBytes);
1622     }
1623   }
1624 
1625   /**
1626    * Creates one of the special index entries.
1627    */
1628   private static Entry createSpecialEntry(RowIdImpl rowId) {
1629     return new Entry((byte[])null, rowId);
1630   }
1631 
1632   /**
1633    * Constructs a ColumnDescriptor of the relevant type for the given Column.
1634    */
1635   private ColumnDescriptor newColumnDescriptor(ColumnImpl col, byte flags)
1636   {
1637     switch(col.getType()) {
1638     case TEXT:
1639     case MEMO:
1640       ColumnImpl.SortOrder sortOrder = col.getTextSortOrder();
1641       GeneralLegacyIndexCodes textCodes = getTextIndexCodes(sortOrder);
1642       if(textCodes != null) {
1643         return new TextColumnDescriptor(col, flags, textCodes);
1644       }
1645       // unsupported sort order
1646       if(col.getTable().getDatabase().isWriteBrokenIndex()) {
1647         // write the index so the db can be created with the structure it
1648         // needs and then fixed via "compact and repair" in MS Access, and
1649         // mark it broken so nothing reads it.  the general legacy order is
1650         // the one every format can hold, where the general table exists only
1651         // from 2010 on and a jet 3 file takes the general 97 one, and the
1652         // entries are wrong for the collation whichever order writes them
1653         setUnsupportedReason("unsupported collating sort order " + sortOrder +
1654                              " for text index", IndexStatus.BROKEN_WRITE, col);
1655         return new TextColumnDescriptor(col, flags,
1656                                         GeneralLegacyIndexCodes.GEN_LEG_INSTANCE);
1657       }
1658       setUnsupportedReason("unsupported collating sort order " + sortOrder +
1659                            " for text index", IndexStatus.READ_ONLY, col);
1660       return new ReadOnlyColumnDescriptor(col, flags);
1661     case INT:
1662     case LONG:
1663     case MONEY:
1664     case COMPLEX_TYPE:
1665     case BIG_INT:
1666       return new IntegerColumnDescriptor(col, flags);
1667     case FLOAT:
1668     case DOUBLE:
1669     case SHORT_DATE_TIME:
1670       return new FloatingPointColumnDescriptor(col, flags);
1671     case NUMERIC:
1672       return (col.getFormat().LEGACY_NUMERIC_INDEXES ?
1673               new LegacyFixedPointColumnDescriptor(col, flags) :
1674               new FixedPointColumnDescriptor(col, flags));
1675     case BYTE:
1676       return new ByteColumnDescriptor(col, flags);
1677     case BOOLEAN:
1678       return new BooleanColumnDescriptor(col, flags);
1679     case GUID:
1680       return new GuidColumnDescriptor(col, flags);
1681     case BINARY:
1682       return new BinaryColumnDescriptor(col, flags);
1683     case EXT_DATE_TIME:
1684       return new ExtDateColumnDescriptor(col, flags);
1685 
1686     default:
1687       // we can't modify this index at this point in time
1688       setUnsupportedReason("unsupported data type " + col.getType() +
1689                            " for index", IndexStatus.READ_ONLY, col);
1690       return new ReadOnlyColumnDescriptor(col, flags);
1691     }
1692   }
1693 
1694   /**
1695    * Returns the EntryType based on the given entry info.
1696    */
1697   private static EntryType determineEntryType(byte[] entryBytes, RowIdImpl rowId)
1698   {
1699     if(entryBytes != null) {
1700       return ((rowId.getType() == RowIdImpl.Type.NORMAL) ?
1701               EntryType.NORMAL :
1702               ((rowId.getType() == RowIdImpl.Type.ALWAYS_FIRST) ?
1703                EntryType.FIRST_VALID : EntryType.LAST_VALID));
1704     } else if(!rowId.isValid()) {
1705       // this is a "special" entry (first/last)
1706       return ((rowId.getType() == RowIdImpl.Type.ALWAYS_FIRST) ?
1707               EntryType.ALWAYS_FIRST : EntryType.ALWAYS_LAST);
1708     }
1709     throw new IllegalArgumentException("Values was null for valid entry");
1710   }
1711 
1712   /**
1713    * Returns the maximum amount of entry data which can be encoded on any
1714    * index page.
1715    */
1716   private static int calcMaxPageEntrySize(JetFormat format)
1717   {
1718     // the max data we can fit on a page is the min of the space on the page
1719     // vs the number of bytes which can be encoded in the entry mask
1720     int pageDataSize = (format.PAGE_SIZE -
1721                         (format.OFFSET_INDEX_ENTRY_MASK +
1722                          format.SIZE_INDEX_ENTRY_MASK));
1723     int entryMaskSize = (format.SIZE_INDEX_ENTRY_MASK * 8);
1724     return Math.min(pageDataSize, entryMaskSize);
1725   }
1726 
1727   String withErrorContext(String msg) {
1728     return withErrorContext(msg, getTable().getDatabase(), getTable().getName(),
1729                             getName());
1730   }
1731 
1732   private static String withErrorContext(String msg, DatabaseImpl db,
1733                                          String tableName, String idxName) {
1734     return msg + " (Db=" + db.getName() + ";Table=" + tableName +
1735       ";Index=" + idxName + ")";
1736   }
1737 
1738   /**
1739    * Information about the columns in an index.  Also encodes new index
1740    * values.
1741    */
1742   public static abstract class ColumnDescriptor implements Index.Column
1743   {
1744     private final ColumnImpl _column;
1745     private final byte _flags;
1746 
1747     private ColumnDescriptor(ColumnImpl column, byte flags)
1748     {
1749       _column = column;
1750       _flags = flags;
1751     }
1752 
1753     @Override
1754     public ColumnImpl getColumn() {
1755       return _column;
1756     }
1757 
1758     public byte getFlags() {
1759       return _flags;
1760     }
1761 
1762     @Override
1763     public boolean isAscending() {
1764       return((getFlags() & ASCENDING_COLUMN_FLAG) != 0);
1765     }
1766 
1767     @Override
1768     public int getColumnIndex() {
1769       return getColumn().getColumnIndex();
1770     }
1771 
1772     @Override
1773     public String getName() {
1774       return getColumn().getName();
1775     }
1776 
1777     protected boolean isNullValue(Object value) {
1778       return (value == null);
1779     }
1780 
1781     protected final void writeValue(Object value, ByteStream bout)
1782       throws IOException
1783     {
1784       if(isNullValue(value)) {
1785         // write null value
1786         bout.write(getNullEntryFlag(isAscending()));
1787         return;
1788       }
1789 
1790       // write the start flag
1791       bout.write(getStartEntryFlag(isAscending()));
1792       // write the rest of the value
1793       writeNonNullValue(value, bout);
1794     }
1795 
1796     protected abstract void writeNonNullValue(Object value, ByteStream bout)
1797       throws IOException;
1798 
1799     @Override
1800     public String toString() {
1801       return ToStringBuilder.builder(this)
1802         .append("column", getColumn())
1803         .append("flags", getFlags() + " " + (isAscending() ? "(ASC)" : "(DSC)"))
1804         .toString();
1805     }
1806   }
1807 
1808   /**
1809    * ColumnDescriptor for integer based columns.
1810    */
1811   private static final class IntegerColumnDescriptor extends ColumnDescriptor
1812   {
1813     private IntegerColumnDescriptor(ColumnImpl column, byte flags)
1814     {
1815       super(column, flags);
1816     }
1817 
1818     @Override
1819     protected void writeNonNullValue(Object value, ByteStream bout)
1820       throws IOException
1821     {
1822       byte[] valueBytes = encodeNumberColumnValue(value, getColumn());
1823 
1824       // bit twiddling rules:
1825       // - isAsc  => flipFirstBit
1826       // - !isAsc => flipFirstBit, flipBytes
1827 
1828       flipFirstBitInByte(valueBytes, 0);
1829       if(!isAscending()) {
1830         flipBytes(valueBytes);
1831       }
1832 
1833       bout.write(valueBytes);
1834     }
1835   }
1836 
1837   /**
1838    * ColumnDescriptor for floating point based columns.
1839    */
1840   private static final class FloatingPointColumnDescriptor
1841     extends ColumnDescriptor
1842   {
1843     private FloatingPointColumnDescriptor(ColumnImpl column, byte flags)
1844     {
1845       super(column, flags);
1846     }
1847 
1848     @Override
1849     protected void writeNonNullValue(Object value, ByteStream bout)
1850       throws IOException
1851     {
1852       byte[] valueBytes = encodeNumberColumnValue(value, getColumn());
1853 
1854       // determine if the number is negative by testing if the first bit is
1855       // set
1856       boolean isNegative = ((valueBytes[0] & 0x80) != 0);
1857 
1858       // bit twiddling rules:
1859       // isAsc && !isNeg => flipFirstBit
1860       // isAsc && isNeg => flipBytes
1861       // !isAsc && !isNeg => flipFirstBit, flipBytes
1862       // !isAsc && isNeg => nothing
1863 
1864       if(!isNegative) {
1865         flipFirstBitInByte(valueBytes, 0);
1866       }
1867       if(isNegative == isAscending()) {
1868         flipBytes(valueBytes);
1869       }
1870 
1871       bout.write(valueBytes);
1872     }
1873   }
1874 
1875   /**
1876    * ColumnDescriptor for fixed point based columns (legacy sort order).
1877    */
1878   private static class LegacyFixedPointColumnDescriptor
1879     extends ColumnDescriptor
1880   {
1881     private LegacyFixedPointColumnDescriptor(ColumnImpl column, byte flags)
1882     {
1883       super(column, flags);
1884     }
1885 
1886     protected void handleNegationAndOrder(boolean isNegative,
1887                                           byte[] valueBytes)
1888     {
1889       if(isNegative == isAscending()) {
1890         flipBytes(valueBytes);
1891       }
1892 
1893       // reverse the sign byte (after any previous byte flipping)
1894       valueBytes[0] = (isNegative ? (byte)0x00 : (byte)0xFF);
1895     }
1896 
1897     @Override
1898     protected void writeNonNullValue(Object value, ByteStream bout)
1899       throws IOException
1900     {
1901       byte[] valueBytes = encodeNumberColumnValue(value, getColumn());
1902 
1903       // determine if the number is negative by testing if the first bit is
1904       // set
1905       boolean isNegative = ((valueBytes[0] & 0x80) != 0);
1906 
1907       // bit twiddling rules:
1908       // isAsc && !isNeg => setReverseSignByte             => FF 00 00 ...
1909       // isAsc && isNeg => flipBytes, setReverseSignByte   => 00 FF FF ...
1910       // !isAsc && !isNeg => flipBytes, setReverseSignByte => FF FF FF ...
1911       // !isAsc && isNeg => setReverseSignByte             => 00 00 00 ...
1912 
1913       // v2007 bit twiddling rules (old ordering was a bug, MS kb 837148):
1914       // isAsc && !isNeg => setSignByte 0xFF            => FF 00 00 ...
1915       // isAsc && isNeg => setSignByte 0xFF, flipBytes  => 00 FF FF ...
1916       // !isAsc && !isNeg => setSignByte 0xFF           => FF 00 00 ...
1917       // !isAsc && isNeg => setSignByte 0xFF, flipBytes => 00 FF FF ...
1918       handleNegationAndOrder(isNegative, valueBytes);
1919 
1920       bout.write(valueBytes);
1921     }
1922   }
1923 
1924   /**
1925    * ColumnDescriptor for new-style fixed point based columns.
1926    */
1927   private static final class FixedPointColumnDescriptor
1928     extends LegacyFixedPointColumnDescriptor
1929   {
1930     private FixedPointColumnDescriptor(ColumnImpl column, byte flags)
1931     {
1932       super(column, flags);
1933     }
1934 
1935     @Override
1936     protected void handleNegationAndOrder(boolean isNegative,
1937                                           byte[] valueBytes)
1938     {
1939       // see notes above in FixedPointColumnDescriptor for bit twiddling rules
1940 
1941       // reverse the sign byte (before any byte flipping)
1942       valueBytes[0] = (byte)0xFF;
1943 
1944       if(isNegative == isAscending()) {
1945         flipBytes(valueBytes);
1946       }
1947     }
1948   }
1949 
1950   /**
1951    * ColumnDescriptor for byte based columns.
1952    */
1953   private static final class ByteColumnDescriptor extends ColumnDescriptor
1954   {
1955     private ByteColumnDescriptor(ColumnImpl column, byte flags)
1956     {
1957       super(column, flags);
1958     }
1959 
1960     @Override
1961     protected void writeNonNullValue(Object value, ByteStream bout)
1962       throws IOException
1963     {
1964       byte[] valueBytes = encodeNumberColumnValue(value, getColumn());
1965 
1966       // bit twiddling rules:
1967       // - isAsc  => nothing
1968       // - !isAsc => flipBytes
1969       if(!isAscending()) {
1970         flipBytes(valueBytes);
1971       }
1972 
1973       bout.write(valueBytes);
1974     }
1975   }
1976 
1977   /**
1978    * ColumnDescriptor for boolean columns.
1979    */
1980   private static final class BooleanColumnDescriptor extends ColumnDescriptor
1981   {
1982     private BooleanColumnDescriptor(ColumnImpl column, byte flags)
1983     {
1984       super(column, flags);
1985     }
1986 
1987     @Override
1988     protected boolean isNullValue(Object value) {
1989       // null values are handled as booleans
1990       return false;
1991     }
1992 
1993     @Override
1994     protected void writeNonNullValue(Object value, ByteStream bout)
1995     {
1996       bout.write(
1997           ColumnImpl.toBooleanValue(value) ?
1998           (isAscending() ? ASC_BOOLEAN_TRUE : DESC_BOOLEAN_TRUE) :
1999           (isAscending() ? ASC_BOOLEAN_FALSE : DESC_BOOLEAN_FALSE));
2000     }
2001   }
2002 
2003   /**
2004    * ColumnDescriptor for text based columns, which encodes a value with the
2005    * index codes of the column's collation.
2006    */
2007   private static final class TextColumnDescriptor extends ColumnDescriptor
2008   {
2009     private final GeneralLegacyIndexCodes _codes;
2010 
2011     private TextColumnDescriptor(ColumnImpl column, byte flags,
2012                                  GeneralLegacyIndexCodes codes)
2013     {
2014       super(column, flags);
2015       _codes = codes;
2016     }
2017 
2018     @Override
2019     protected void writeNonNullValue(Object value, ByteStream bout)
2020       throws IOException
2021     {
2022       _codes.writeNonNullIndexTextValue(value, bout, isAscending());
2023     }
2024   }
2025 
2026   /**
2027    * ColumnDescriptor for guid columns.
2028    */
2029   private static final class GuidColumnDescriptor extends ColumnDescriptor
2030   {
2031     private GuidColumnDescriptor(ColumnImpl column, byte flags)
2032     {
2033       super(column, flags);
2034     }
2035 
2036     @Override
2037     protected void writeNonNullValue(Object value, ByteStream bout)
2038       throws IOException
2039     {
2040       writeGeneralBinaryEntry(
2041           encodeNumberColumnValue(value, getColumn()), isAscending(),
2042           bout);
2043     }
2044   }
2045 
2046 
2047   /**
2048    * ColumnDescriptor for BINARY columns.
2049    */
2050   private static final class BinaryColumnDescriptor extends ColumnDescriptor
2051   {
2052     private BinaryColumnDescriptor(ColumnImpl column, byte flags)
2053     {
2054       super(column, flags);
2055     }
2056 
2057     @Override
2058     protected void writeNonNullValue(Object value, ByteStream bout)
2059       throws IOException
2060     {
2061       writeGeneralBinaryEntry(
2062           ColumnImpl.toByteArray(value), isAscending(), bout);
2063     }
2064   }
2065 
2066   /**
2067    * ColumnDescriptor for extended date/time based columns.
2068    */
2069   private static final class ExtDateColumnDescriptor extends ColumnDescriptor
2070   {
2071     private ExtDateColumnDescriptor(ColumnImpl column, byte flags)
2072     {
2073       super(column, flags);
2074     }
2075 
2076     @Override
2077     protected void writeNonNullValue(Object value, ByteStream bout)
2078       throws IOException
2079     {
2080       byte[] valueBytes = encodeNumberColumnValue(value, getColumn());
2081 
2082       // entry (which is 42 bytes of data) is encoded in blocks of 8 bytes
2083       // separated by '\t' char
2084 
2085       // note that for desc, all bytes are flipped _except_ separator char
2086       byte[] trailer = ASC_EXT_DATE_TRAILER;
2087       if(!isAscending()) {
2088         flipBytes(valueBytes);
2089         trailer = DESC_EXT_DATE_TRAILER;
2090       }
2091 
2092       // first 5 blocks are all value data
2093       int valIdx = 0;
2094       for(int i = 0; i < 5; ++i) {
2095         bout.write(valueBytes, valIdx, 8);
2096         bout.write((byte)0x09);
2097         valIdx += 8;
2098       }
2099 
2100       // last two data bytes and then the trailer
2101       bout.write(valueBytes, valIdx, 2);
2102       bout.write(trailer);
2103     }
2104   }
2105 
2106   /**
2107    * ColumnDescriptor for columns which we cannot currently write.
2108    */
2109   private final class ReadOnlyColumnDescriptor extends ColumnDescriptor
2110   {
2111     private ReadOnlyColumnDescriptor(ColumnImpl column, byte flags)
2112     {
2113       super(column, flags);
2114     }
2115 
2116     @Override
2117     protected void writeNonNullValue(Object value, ByteStream bout)
2118     {
2119       throw new UnsupportedOperationException(
2120           "Cannot write indexes of this type due to " + _unsupportedReason);
2121     }
2122   }
2123 
2124   /**
2125    * A single leaf entry in an index (points to a single row)
2126    */
2127   public static class Entry implements Comparable<Entry>
2128   {
2129     /** page/row on which this row is stored */
2130     private final RowIdImpl _rowId;
2131     /** the entry value */
2132     private final byte[] _entryBytes;
2133     /** comparable type for the entry */
2134     private final EntryType _type;
2135 
2136     /**
2137      * Create a new entry
2138      * @param entryBytes encoded bytes for this index entry
2139      * @param rowId rowId in which the row is stored
2140      * @param type the type of the entry
2141      */
2142     private Entry(byte[] entryBytes, RowIdImpl rowId, EntryType type) {
2143       _rowId = rowId;
2144       _entryBytes = entryBytes;
2145       _type = type;
2146     }
2147 
2148     /**
2149      * Create a new entry
2150      * @param entryBytes encoded bytes for this index entry
2151      * @param rowId rowId in which the row is stored
2152      */
2153     private Entry(byte[] entryBytes, RowIdImpl rowId)
2154     {
2155       this(entryBytes, rowId, determineEntryType(entryBytes, rowId));
2156     }
2157 
2158     /**
2159      * Read an existing entry in from a buffer
2160      */
2161     private Entry(ByteBuffer buffer, int entryLen)
2162     {
2163       this(buffer, entryLen, 0);
2164     }
2165 
2166     /**
2167      * Read an existing entry in from a buffer
2168      */
2169     private Entry(ByteBuffer buffer, int entryLen, int extraTrailingLen)
2170     {
2171       // we need 4 trailing bytes for the rowId, plus whatever the caller
2172       // wants
2173       int colEntryLen = entryLen - (4 + extraTrailingLen);
2174 
2175       // read the entry bytes
2176       _entryBytes = ByteUtil.getBytes(buffer, colEntryLen);
2177 
2178       // read the rowId
2179       int page = ByteUtil.get3ByteInt(buffer, ENTRY_BYTE_ORDER);
2180       int row = ByteUtil.getUnsignedByte(buffer);
2181 
2182       _rowId = new RowIdImpl(page, row);
2183       _type = EntryType.NORMAL;
2184     }
2185 
2186     public RowIdImpl getRowId() {
2187       return _rowId;
2188     }
2189 
2190     public EntryType getType() {
2191       return _type;
2192     }
2193 
2194     public Integer getSubPageNumber() {
2195       throw new UnsupportedOperationException();
2196     }
2197 
2198     public boolean isLeafEntry() {
2199       return true;
2200     }
2201 
2202     public boolean isValid() {
2203       return(_entryBytes != null);
2204     }
2205 
2206     protected final byte[] getEntryBytes() {
2207       return _entryBytes;
2208     }
2209 
2210     /**
2211      * Size of this entry in the db.
2212      */
2213     protected int size() {
2214       // need 4 trailing bytes for the rowId
2215       return _entryBytes.length + 4;
2216     }
2217 
2218     /**
2219      * Write this entry into a buffer
2220      */
2221     protected void write(ByteBuffer buffer,
2222                          byte[] prefix)
2223     {
2224       if(prefix.length <= _entryBytes.length) {
2225 
2226         // write entry bytes, not including prefix
2227         buffer.put(_entryBytes, prefix.length,
2228                    (_entryBytes.length - prefix.length));
2229         ByteUtil.put3ByteInt(buffer, getRowId().getPageNumber(),
2230                              ENTRY_BYTE_ORDER);
2231 
2232       } else if(prefix.length <= (_entryBytes.length + 3)) {
2233 
2234         // the prefix includes part of the page number, write to temp buffer
2235         // and copy last bytes to output buffer
2236         ByteBuffer tmp = ByteBuffer.allocate(3);
2237         ByteUtil.put3ByteInt(tmp, getRowId().getPageNumber(),
2238                              ENTRY_BYTE_ORDER);
2239         tmp.flip();
2240         tmp.position(prefix.length - _entryBytes.length);
2241         buffer.put(tmp);
2242 
2243       } else {
2244 
2245         // since the row number would never be the same if the page number is
2246         // the same, nothing past the page number should ever be included in
2247         // the prefix.
2248         // FIXME, this could happen if page has only one row...
2249         throw new IllegalStateException("prefix should never be this long");
2250       }
2251 
2252       buffer.put((byte)getRowId().getRowNumber());
2253     }
2254 
2255     protected final ToStringBuilder entryBytesToStringBuilder(
2256         ToStringBuilder sb) {
2257       if(isValid()) {
2258         sb.append("bytes", _entryBytes);
2259       }
2260       return sb;
2261     }
2262 
2263     @Override
2264     public String toString() {
2265       return entryBytesToStringBuilder(
2266           ToStringBuilder.valueBuilder(this)
2267           .append("rowId", _rowId))
2268         .toString();
2269     }
2270 
2271     @Override
2272     public int hashCode() {
2273       return _rowId.hashCode();
2274     }
2275 
2276     @Override
2277     public boolean equals(Object o) {
2278       return((this == o) ||
2279              ((o != null) && (getClass() == o.getClass()) &&
2280               (compareTo((Entry)o) == 0)));
2281     }
2282 
2283     /**
2284      * @return {@code true} iff the entryBytes are equal between this
2285      *         Entry and the given Entry
2286      */
2287     public boolean equalsEntryBytes(Entry o) {
2288       return(BYTE_CODE_COMPARATOR.compare(_entryBytes, o._entryBytes) == 0);
2289     }
2290 
2291     @Override
2292     public int compareTo(Entry other) {
2293       if (this == other) {
2294         return 0;
2295       }
2296 
2297       if(isValid() && other.isValid()) {
2298 
2299         // comparing two valid entries.  first, compare by actual byte values
2300         int entryCmp = BYTE_CODE_COMPARATOR.compare(
2301             _entryBytes, other._entryBytes);
2302         if(entryCmp != 0) {
2303           return entryCmp;
2304         }
2305 
2306       } else {
2307 
2308         // if the entries are of mixed validity (or both invalid), we defer
2309         // next to the EntryType
2310         int typeCmp = _type.compareTo(other._type);
2311         if(typeCmp != 0) {
2312           return typeCmp;
2313         }
2314       }
2315 
2316       // at this point we let the RowId decide the final result
2317       return _rowId.compareTo(other.getRowId());
2318     }
2319 
2320     /**
2321      * Returns a copy of this entry as a node Entry with the given
2322      * subPageNumber.
2323      */
2324     protected Entry asNodeEntry(Integer subPageNumber) {
2325       return new NodeEntry(_entryBytes, _rowId, _type, subPageNumber);
2326     }
2327 
2328   }
2329 
2330   /**
2331    * A single node entry in an index (points to a sub-page in the index)
2332    */
2333   private static final class NodeEntry extends Entry {
2334 
2335     /** index page number of the page to which this node entry refers */
2336     private final Integer _subPageNumber;
2337 
2338     /**
2339      * Create a new node entry
2340      * @param entryBytes encoded bytes for this index entry
2341      * @param rowId rowId in which the row is stored
2342      * @param type the type of the entry
2343      * @param subPageNumber the sub-page to which this node entry refers
2344      */
2345     private NodeEntry(byte[] entryBytes, RowIdImpl rowId, EntryType type,
2346                       Integer subPageNumber) {
2347       super(entryBytes, rowId, type);
2348       _subPageNumber = subPageNumber;
2349     }
2350 
2351     /**
2352      * Read an existing node entry in from a buffer
2353      */
2354     private NodeEntry(ByteBuffer buffer, int entryLen)
2355     {
2356       // we need 4 trailing bytes for the sub-page number
2357       super(buffer, entryLen, 4);
2358 
2359       _subPageNumber = ByteUtil.getInt(buffer, ENTRY_BYTE_ORDER);
2360     }
2361 
2362     @Override
2363     public Integer getSubPageNumber() {
2364       return _subPageNumber;
2365     }
2366 
2367     @Override
2368     public boolean isLeafEntry() {
2369       return false;
2370     }
2371 
2372     @Override
2373     protected int size() {
2374       // need 4 trailing bytes for the sub-page number
2375       return super.size() + 4;
2376     }
2377 
2378     @Override
2379     protected void write(ByteBuffer buffer, byte[] prefix) {
2380       super.write(buffer, prefix);
2381       ByteUtil.putInt(buffer, _subPageNumber, ENTRY_BYTE_ORDER);
2382     }
2383 
2384     @Override
2385     public boolean equals(Object o) {
2386       return((this == o) ||
2387              ((o != null) && (getClass() == o.getClass()) &&
2388               (compareTo((Entry)o) == 0) &&
2389               (getSubPageNumber().equals(((Entry)o).getSubPageNumber()))));
2390     }
2391 
2392     @Override
2393     public String toString() {
2394       return entryBytesToStringBuilder(
2395           ToStringBuilder.valueBuilder(this)
2396           .append("rowId", getRowId())
2397           .append("subPage", _subPageNumber))
2398         .toString();
2399     }
2400   }
2401 
2402   /**
2403    * Utility class to traverse the entries in the Index.  Remains valid in the
2404    * face of index entry modifications.
2405    */
2406   public final class EntryCursor
2407   {
2408     /** handler for moving the page cursor forward */
2409     private final DirHandler _forwardDirHandler = new ForwardDirHandler();
2410     /** handler for moving the page cursor backward */
2411     private final DirHandler _reverseDirHandler = new ReverseDirHandler();
2412     /** the first (exclusive) row id for this cursor */
2413     private Position _firstPos;
2414     /** the last (exclusive) row id for this cursor */
2415     private Position _lastPos;
2416     /** the current entry */
2417     private Position _curPos;
2418     /** the previous entry */
2419     private Position _prevPos;
2420     /** the last read modification count on the Index.  we track this so that
2421         the cursor can detect updates to the index while traversing and act
2422         accordingly */
2423     private int _lastModCount;
2424 
2425     private EntryCursor(Position firstPos, Position lastPos)
2426     {
2427       _firstPos = firstPos;
2428       _lastPos = lastPos;
2429       _lastModCount = getIndexModCount();
2430       reset();
2431     }
2432 
2433     /**
2434      * Returns the DirHandler for the given direction
2435      */
2436     private DirHandler getDirHandler(boolean moveForward) {
2437       return (moveForward ? _forwardDirHandler : _reverseDirHandler);
2438     }
2439 
2440     public IndexData getIndexData() {
2441       return IndexData.this;
2442     }
2443 
2444     private int getIndexModCount() {
2445       return IndexData.this._modCount;
2446     }
2447 
2448     /**
2449      * Returns the first entry (exclusive) as defined by this cursor.
2450      */
2451     public Entry getFirstEntry() {
2452       return _firstPos.getEntry();
2453     }
2454 
2455     /**
2456      * Returns the last entry (exclusive) as defined by this cursor.
2457      */
2458     public Entry getLastEntry() {
2459       return _lastPos.getEntry();
2460     }
2461 
2462     /**
2463      * Returns {@code true} if this cursor is up-to-date with respect to its
2464      * index.
2465      */
2466     public boolean isUpToDate() {
2467       return(getIndexModCount() == _lastModCount);
2468     }
2469 
2470     public void reset() {
2471       beforeFirst();
2472     }
2473 
2474     public void beforeFirst() {
2475       reset(CursorImpl.MOVE_FORWARD);
2476     }
2477 
2478     public void afterLast() {
2479       reset(CursorImpl.MOVE_REVERSE);
2480     }
2481 
2482     protected void reset(boolean moveForward)
2483     {
2484       _curPos = getDirHandler(moveForward).getBeginningPosition();
2485       _prevPos = _curPos;
2486     }
2487 
2488     /**
2489      * Repositions the cursor so that the next row will be the first entry
2490      * &gt;= the given row.
2491      */
2492     public void beforeEntry(Object[] row)
2493       throws IOException
2494     {
2495       restorePosition(new Entry(IndexData.this.createEntryBytes(row),
2496                                 RowIdImpl.FIRST_ROW_ID));
2497     }
2498 
2499     /**
2500      * Repositions the cursor so that the previous row will be the first
2501      * entry &lt;= the given row.
2502      */
2503     public void afterEntry(Object[] row)
2504       throws IOException
2505     {
2506       restorePosition(new Entry(IndexData.this.createEntryBytes(row),
2507                                 RowIdImpl.LAST_ROW_ID));
2508     }
2509 
2510     /**
2511      * @return valid entry if there was a next entry,
2512      *         {@code #getLastEntry} otherwise
2513      */
2514     public Entry getNextEntry() throws IOException {
2515       return getAnotherPosition(CursorImpl.MOVE_FORWARD).getEntry();
2516     }
2517 
2518     /**
2519      * @return valid entry if there was a next entry,
2520      *         {@code #getFirstEntry} otherwise
2521      */
2522     public Entry getPreviousEntry() throws IOException {
2523       return getAnotherPosition(CursorImpl.MOVE_REVERSE).getEntry();
2524     }
2525 
2526     /**
2527      * Restores a current position for the cursor (current position becomes
2528      * previous position).
2529      */
2530     protected void restorePosition(Entry curEntry)
2531       throws IOException
2532     {
2533       restorePosition(curEntry, _curPos.getEntry());
2534     }
2535 
2536     /**
2537      * Restores a current and previous position for the cursor.
2538      */
2539     protected void restorePosition(Entry curEntry, Entry prevEntry)
2540       throws IOException
2541     {
2542       if(!_curPos.equalsEntry(curEntry) ||
2543          !_prevPos.equalsEntry(prevEntry))
2544       {
2545         if(!isUpToDate()) {
2546           updateBounds();
2547           _lastModCount = getIndexModCount();
2548         }
2549         _prevPos = updatePosition(prevEntry);
2550         _curPos = updatePosition(curEntry);
2551       } else {
2552         checkForModification();
2553       }
2554     }
2555 
2556     /**
2557      * Gets another entry in the given direction, returning the new entry.
2558      */
2559     private Position getAnotherPosition(boolean moveForward)
2560       throws IOException
2561     {
2562       DirHandler handler = getDirHandler(moveForward);
2563       if(_curPos.equals(handler.getEndPosition())) {
2564         if(!isUpToDate()) {
2565           restorePosition(_prevPos.getEntry());
2566           // drop through and retry moving to another entry
2567         } else {
2568           // at end, no more
2569           return _curPos;
2570         }
2571       }
2572 
2573       checkForModification();
2574 
2575       _prevPos = _curPos;
2576       _curPos = handler.getAnotherPosition(_curPos);
2577       return _curPos;
2578     }
2579 
2580     /**
2581      * Checks the index for modifications and updates state accordingly.
2582      */
2583     private void checkForModification()
2584       throws IOException
2585     {
2586       if(!isUpToDate()) {
2587         updateBounds();
2588         _prevPos = updatePosition(_prevPos.getEntry());
2589         _curPos = updatePosition(_curPos.getEntry());
2590         _lastModCount = getIndexModCount();
2591       }
2592     }
2593 
2594     /**
2595      * Updates the given position, taking boundaries into account.
2596      */
2597     private Position updatePosition(Entry entry)
2598       throws IOException
2599     {
2600       if(!entry.isValid()) {
2601         // no use searching if "updating" the first/last pos
2602         if(_firstPos.equalsEntry(entry)) {
2603           return _firstPos;
2604         } else if(_lastPos.equalsEntry(entry)) {
2605           return _lastPos;
2606         } else {
2607           throw new IllegalArgumentException(
2608               withErrorContext("Invalid entry given " + entry));
2609         }
2610       }
2611 
2612       Position pos = findEntryPosition(entry);
2613       if(pos.compareTo(_lastPos) >= 0) {
2614         return _lastPos;
2615       } else if(pos.compareTo(_firstPos) <= 0) {
2616         return _firstPos;
2617       }
2618       return pos;
2619     }
2620 
2621     /**
2622      * Updates any the boundary info (_firstPos/_lastPos).
2623      */
2624     private void updateBounds()
2625       throws IOException
2626     {
2627       _firstPos = findEntryPosition(_firstPos.getEntry());
2628       _lastPos = findEntryPosition(_lastPos.getEntry());
2629     }
2630 
2631     @Override
2632     public String toString() {
2633       return ToStringBuilder.valueBuilder(this)
2634         .append("curPosition", _curPos)
2635         .append("prevPosition", _prevPos)
2636         .toString();
2637     }
2638 
2639     /**
2640      * Handles moving the cursor in a given direction.  Separates cursor
2641      * logic from value storage.
2642      */
2643     private abstract class DirHandler {
2644       public abstract Position getAnotherPosition(Position curPos)
2645         throws IOException;
2646       public abstract Position getBeginningPosition();
2647       public abstract Position getEndPosition();
2648     }
2649 
2650     /**
2651      * Handles moving the cursor forward.
2652      */
2653     private final class ForwardDirHandler extends DirHandler {
2654       @Override
2655       public Position getAnotherPosition(Position curPos)
2656         throws IOException
2657       {
2658         Position newPos = getNextPosition(curPos);
2659         if((newPos == null) || (newPos.compareTo(_lastPos) >= 0)) {
2660           newPos = _lastPos;
2661         }
2662         return newPos;
2663       }
2664       @Override
2665       public Position getBeginningPosition() {
2666         return _firstPos;
2667       }
2668       @Override
2669       public Position getEndPosition() {
2670         return _lastPos;
2671       }
2672     }
2673 
2674     /**
2675      * Handles moving the cursor backward.
2676      */
2677     private final class ReverseDirHandler extends DirHandler {
2678       @Override
2679       public Position getAnotherPosition(Position curPos)
2680         throws IOException
2681       {
2682         Position newPos = getPreviousPosition(curPos);
2683         if((newPos == null) || (newPos.compareTo(_firstPos) <= 0)) {
2684           newPos = _firstPos;
2685         }
2686         return newPos;
2687       }
2688       @Override
2689       public Position getBeginningPosition() {
2690         return _lastPos;
2691       }
2692       @Override
2693       public Position getEndPosition() {
2694         return _firstPos;
2695       }
2696     }
2697   }
2698 
2699   /**
2700    * Simple value object for maintaining some cursor state.
2701    */
2702   private static final class Position implements Comparable<Position> {
2703     /** the last known page of the given entry */
2704     private final DataPage _dataPage;
2705     /** the last known index of the given entry */
2706     private final int _idx;
2707     /** the entry at the given index */
2708     private final Entry _entry;
2709     /** {@code true} if this entry does not currently exist in the entry list,
2710         {@code false} otherwise (this is equivalent to adding -0.5 to the
2711         _idx) */
2712     private final boolean _between;
2713 
2714     private Position(DataPage dataPage, int idx)
2715     {
2716       this(dataPage, idx, dataPage.getEntries().get(idx), false);
2717     }
2718 
2719     private Position(DataPage dataPage, int idx, Entry entry, boolean between)
2720     {
2721       _dataPage = dataPage;
2722       _idx = idx;
2723       _entry = entry;
2724       _between = between;
2725     }
2726 
2727     DataPage getDataPage() {
2728       return _dataPage;
2729     }
2730 
2731     int getIndex() {
2732       return _idx;
2733     }
2734 
2735     int getNextIndex() {
2736       // note, _idx does not need to be advanced if it was pointing at a
2737       // between position
2738       return(_between ? _idx : (_idx + 1));
2739     }
2740 
2741     int getPrevIndex() {
2742       // note, we ignore the between flag here because the index will be
2743       // pointing at the correct next index in either the between or
2744       // non-between case
2745       return(_idx - 1);
2746     }
2747 
2748     Entry getEntry() {
2749       return _entry;
2750     }
2751 
2752     boolean equalsEntry(Entry entry) {
2753       return _entry.equals(entry);
2754     }
2755 
2756     @Override
2757     public int compareTo(Position other)
2758     {
2759       if(this == other) {
2760         return 0;
2761       }
2762 
2763       if(_dataPage.equals(other._dataPage)) {
2764         // "simple" index comparison (handle between-ness)
2765         int idxCmp = ((_idx < other._idx) ? -1 :
2766                       ((_idx > other._idx) ? 1 :
2767                        ((_between == other._between) ? 0 :
2768                         (_between ? -1 : 1))));
2769         if(idxCmp != 0) {
2770           return idxCmp;
2771         }
2772       }
2773 
2774       // compare the entries.
2775       return _entry.compareTo(other._entry);
2776     }
2777 
2778     @Override
2779     public int hashCode() {
2780       return _entry.hashCode();
2781     }
2782 
2783     @Override
2784     public boolean equals(Object o) {
2785       return((this == o) ||
2786              ((o != null) && (getClass() == o.getClass()) &&
2787               (compareTo((Position)o) == 0)));
2788     }
2789 
2790     @Override
2791     public String toString() {
2792       return ToStringBuilder.valueBuilder(this)
2793         .append("page", _dataPage.getPageNumber())
2794         .append("idx", _idx)
2795         .append("entry", _entry)
2796         .append("between", _between)
2797         .toString();
2798     }
2799   }
2800 
2801   /**
2802    * Object used to maintain state about an Index page.
2803    */
2804   protected static abstract class DataPage {
2805 
2806     public abstract int getPageNumber();
2807 
2808     public abstract boolean isLeaf();
2809     public abstract void setLeaf(boolean isLeaf);
2810 
2811     /**
2812      * Returns the depth of this page below the leaves of the index tree, 0
2813      * for a leaf page.
2814      */
2815     public abstract int getLevel() throws IOException;
2816     public abstract void setLevel(int level);
2817 
2818     public abstract int getPrevPageNumber();
2819     public abstract void setPrevPageNumber(int pageNumber);
2820     public abstract int getNextPageNumber();
2821     public abstract void setNextPageNumber(int pageNumber);
2822     public abstract int getChildTailPageNumber();
2823     public abstract void setChildTailPageNumber(int pageNumber);
2824 
2825     public abstract int getTotalEntrySize();
2826     public abstract void setTotalEntrySize(int totalSize);
2827     public abstract byte[] getEntryPrefix();
2828     public abstract void setEntryPrefix(byte[] entryPrefix);
2829 
2830     public abstract List<Entry> getEntries();
2831     public abstract void setEntries(List<Entry> entries);
2832 
2833     public abstract void addEntry(int idx, Entry entry)
2834       throws IOException;
2835     public abstract Entry removeEntry(int idx)
2836       throws IOException;
2837 
2838     public final boolean isEmpty() {
2839       return getEntries().isEmpty();
2840     }
2841 
2842     public final int getCompressedEntrySize() {
2843       // when written to the index page, the entryPrefix bytes will only be
2844       // written for the first entry, so we subtract the entry prefix size
2845       // from all the other entries to determine the compressed size
2846       return getTotalEntrySize() -
2847         (getEntryPrefix().length * (getEntries().size() - 1));
2848     }
2849 
2850     public final int findEntry(Entry entry) {
2851       return Collections.binarySearch(getEntries(), entry);
2852     }
2853 
2854     @Override
2855     public final int hashCode() {
2856       return getPageNumber();
2857     }
2858 
2859     @Override
2860     public final boolean equals(Object o) {
2861       return((this == o) ||
2862              ((o != null) && (getClass() == o.getClass()) &&
2863               (getPageNumber() == ((DataPage)o).getPageNumber())));
2864     }
2865 
2866     @Override
2867     public final String toString() {
2868       List<Entry> entries = getEntries();
2869 
2870       String objName =
2871         (isLeaf() ? "Leaf" : "Node") + "DataPage[" + getPageNumber() +
2872         "] " + getPrevPageNumber() + ", " + getNextPageNumber() + ", (" +
2873         getChildTailPageNumber() + ")";
2874       ToStringBuilder sb = ToStringBuilder.valueBuilder(objName);
2875 
2876       if((isLeaf() && !entries.isEmpty())) {
2877         sb.append("entryRange", "[" + entries.get(0) + ", " +
2878                   entries.get(entries.size() - 1) + "]");
2879       } else {
2880         sb.append("entries", entries);
2881       }
2882       return sb.toString();
2883     }
2884   }
2885 
2886   /**
2887    * Simple implementation of a DataPage
2888    */
2889   private static final class RootDataPage extends DataPage {
2890 
2891     @Override
2892     public int getPageNumber() { return 0; }
2893 
2894     @Override
2895     public boolean isLeaf() { return true; }
2896     @Override
2897     public void setLeaf(boolean isLeaf) { }
2898 
2899     @Override
2900     public int getLevel() { return 0; }
2901     @Override
2902     public void setLevel(int level) { }
2903 
2904     @Override
2905     public int getPrevPageNumber() { return 0; }
2906     @Override
2907     public void setPrevPageNumber(int pageNumber) { }
2908 
2909     @Override
2910     public int getNextPageNumber() { return 0; }
2911     @Override
2912     public void setNextPageNumber(int pageNumber) { }
2913 
2914     @Override
2915     public int getChildTailPageNumber() { return 0; }
2916     @Override
2917     public void setChildTailPageNumber(int pageNumber) { }
2918 
2919     @Override
2920     public int getTotalEntrySize() { return 0; }
2921     @Override
2922     public void setTotalEntrySize(int totalSize) { }
2923 
2924     @Override
2925     public byte[] getEntryPrefix() { return EMPTY_PREFIX; }
2926     @Override
2927     public void setEntryPrefix(byte[] entryPrefix) { }
2928 
2929     @Override
2930     public List<Entry> getEntries() { return Collections.emptyList(); }
2931     @Override
2932     public void setEntries(List<Entry> entries) { }
2933     @Override
2934     public void addEntry(int idx, Entry entry) { }
2935     @Override
2936     public Entry removeEntry(int idx) { return null; }
2937   }
2938 
2939   /**
2940    * Utility class which maintains information about a pending index update.
2941    * An instance of this class can be used to complete the change (by calling
2942    * {@link #commit}) or undo the change (by calling {@link #rollback}).
2943    */
2944   public static abstract class PendingChange
2945   {
2946     private final PendingChange _next;
2947 
2948     private PendingChange(PendingChange next) {
2949       _next = next;
2950     }
2951 
2952     /**
2953      * Returns the next pending change, if any
2954      */
2955     public PendingChange getNext() {
2956       return _next;
2957     }
2958 
2959     /**
2960      * Completes the pending change.
2961      */
2962     public abstract void commit() throws IOException;
2963 
2964     /**
2965      * Undoes the pending change.
2966      */
2967     public abstract void rollback() throws IOException;
2968   }
2969 
2970   /**
2971    * PendingChange for a row addition.
2972    */
2973   private class AddRowPendingChange extends PendingChange
2974   {
2975     protected Entry _addEntry;
2976     protected DataPage _addDataPage;
2977     protected int _addIdx;
2978     protected boolean _isDupe;
2979     protected Entry _oldEntry;
2980 
2981     private AddRowPendingChange(PendingChange next) {
2982       super(next);
2983     }
2984 
2985     public void setAddRow(Entry addEntry, DataPage dataPage, int idx,
2986                           boolean isDupe) {
2987       _addEntry = addEntry;
2988       _addDataPage = dataPage;
2989       _addIdx = idx;
2990       _isDupe = isDupe;
2991     }
2992 
2993     public void setOldRow(Entry oldEntry) {
2994       _oldEntry = oldEntry;
2995     }
2996 
2997     @Override
2998     public void commit() throws IOException {
2999       commitAddRow(_addEntry, _addDataPage, _addIdx, _isDupe, _oldEntry);
3000     }
3001 
3002     @Override
3003     public void rollback() throws IOException {
3004       _addEntry = null;
3005       _addDataPage = null;
3006       _addIdx = -1;
3007     }
3008   }
3009 
3010   /**
3011    * PendingChange for a row update (which is essentially a deletion followed
3012    * by an addition).
3013    */
3014   private class UpdateRowPendingChange extends AddRowPendingChange
3015   {
3016     private UpdateRowPendingChange(PendingChange next) {
3017       super(next);
3018     }
3019 
3020     @Override
3021     public void rollback() throws IOException {
3022       super.rollback();
3023       rollbackDeletedRow(_oldEntry);
3024     }
3025   }
3026 
3027 }