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