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