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.util.Collections;
22  import java.util.List;
23  import java.util.Map;
24  
25  import com.healthmarketscience.jackcess.CursorBuilder;
26  import com.healthmarketscience.jackcess.Index;
27  import com.healthmarketscience.jackcess.IndexBuilder;
28  import org.apache.commons.lang3.builder.ToStringBuilder;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  
32  /**
33   * Access table (logical) index.  Logical indexes are backed for IndexData,
34   * where one or more logical indexes could be backed by the same data.
35   *
36   * @author Tim McCune
37   */
38  public class IndexImpl implements Index, Comparable<IndexImpl>
39  {
40    protected static final Log LOG = LogFactory.getLog(IndexImpl.class);
41  
42    /** index type for primary key indexes */
43    public static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1;
44  
45    /** index type for foreign key indexes */
46    public static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2;
47  
48    /** flag for indicating that updates should cascade in a foreign key index */
49    private static final byte CASCADE_UPDATES_FLAG = (byte)1;
50    /** flag for indicating that deletes should cascade in a foreign key index */
51    private static final byte CASCADE_DELETES_FLAG = (byte)1;
52    /** flag for indicating that deletes should cascade a null in a foreign key
53        index */
54    private static final byte CASCADE_NULL_FLAG = (byte)2;
55  
56    /** index table type for the "primary" table in a foreign key index */
57    static final byte FK_PRIMARY_TABLE_TYPE = (byte)1;
58    /** index table type for the "secondary" table in a foreign key index */
59    static final byte FK_SECONDARY_TABLE_TYPE = (byte)2;
60  
61    /** indicate an invalid index number for foreign key field */
62    private static final int INVALID_INDEX_NUMBER = -1;
63  
64    /** the actual data backing this index (more than one index may be backed by
65        the same data */
66    private final IndexData _data;
67    /** 0-based index number */
68    private final int _indexNumber;
69    /** the type of the index */
70    private final byte _indexType;
71    /** Index name */
72    private String _name;
73    /** foreign key reference info, if any */
74    private final ForeignKeyReference _reference;
75  
76    protected IndexImpl(ByteBuffer tableBuffer, List<IndexData> indexDatas,
77                        JetFormat format)
78    {
79      ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
80      _indexNumber = tableBuffer.getInt();
81      int indexDataNumber = tableBuffer.getInt();
82  
83      // read foreign key reference info
84      byte relIndexType = tableBuffer.get();
85      int relIndexNumber = tableBuffer.getInt();
86      int relTablePageNumber = tableBuffer.getInt();
87      byte cascadeUpdatesFlag = tableBuffer.get();
88      byte cascadeDeletesFlag = tableBuffer.get();
89  
90      _indexType = tableBuffer.get();
91  
92      if((_indexType == FOREIGN_KEY_INDEX_TYPE) &&
93         (relIndexNumber != INVALID_INDEX_NUMBER)) {
94        _reference = new ForeignKeyReference(
95            relIndexType, relIndexNumber, relTablePageNumber,
96            ((cascadeUpdatesFlag & CASCADE_UPDATES_FLAG) != 0),
97            ((cascadeDeletesFlag & CASCADE_DELETES_FLAG) != 0),
98            ((cascadeDeletesFlag & CASCADE_NULL_FLAG) != 0));
99      } else {
100       _reference = null;
101     }
102 
103     ByteUtil.forward(tableBuffer, format.SKIP_AFTER_INDEX_SLOT); //Skip past Unknown
104 
105     _data = indexDatas.get(indexDataNumber);
106 
107     _data.addIndex(this);
108   }
109 
110   public IndexData getIndexData() {
111     return _data;
112   }
113 
114   @Override
115   public TableImpl getTable() {
116     return getIndexData().getTable();
117   }
118 
119   public JetFormat getFormat() {
120     return getTable().getFormat();
121   }
122 
123   public PageChannel getPageChannel() {
124     return getTable().getPageChannel();
125   }
126 
127   public int getIndexNumber() {
128     return _indexNumber;
129   }
130 
131   public byte getIndexFlags() {
132     return getIndexData().getIndexFlags();
133   }
134 
135   public int getUniqueEntryCount() {
136     return getIndexData().getUniqueEntryCount();
137   }
138 
139   public int getUniqueEntryCountOffset() {
140     return getIndexData().getUniqueEntryCountOffset();
141   }
142 
143   @Override
144   public String getName() {
145     return _name;
146   }
147 
148   void setName(String name) {
149     _name = name;
150   }
151 
152   @Override
153   public boolean isPrimaryKey() {
154     return _indexType == PRIMARY_KEY_INDEX_TYPE;
155   }
156 
157   @Override
158   public boolean isForeignKey() {
159     return _indexType == FOREIGN_KEY_INDEX_TYPE;
160   }
161 
162   public ForeignKeyReference getReference() {
163     return _reference;
164   }
165 
166   @Override
167   public IndexImpl getReferencedIndex() throws IOException {
168 
169     if(_reference == null) {
170       return null;
171     }
172 
173     TableImpl refTable = getTable().getDatabase().getTable(
174         _reference.getOtherTablePageNumber());
175 
176     if(refTable == null) {
177       throw new IOException(withErrorContext(
178               "Reference to missing table " + _reference.getOtherTablePageNumber()));
179     }
180 
181     IndexImpl refIndex = null;
182     int idxNumber = _reference.getOtherIndexNumber();
183     for(IndexImpl idx : refTable.getIndexes()) {
184       if(idx.getIndexNumber() == idxNumber) {
185         refIndex = idx;
186         break;
187       }
188     }
189 
190     if(refIndex == null) {
191       throw new IOException(withErrorContext(
192               "Reference to missing index " + idxNumber +
193               " on table " + refTable.getName()));
194     }
195 
196     // finally verify that we found the expected index (should reference this
197     // index)
198     ForeignKeyReference otherRef = refIndex.getReference();
199     if((otherRef == null) ||
200        (otherRef.getOtherTablePageNumber() !=
201         getTable().getTableDefPageNumber()) ||
202        (otherRef.getOtherIndexNumber() != _indexNumber)) {
203       throw new IOException(withErrorContext(
204               "Found unexpected index " + refIndex.getName() +
205               " on table " + refTable.getName() + " with reference " + otherRef));
206     }
207 
208     return refIndex;
209   }
210 
211   @Override
212   public boolean shouldIgnoreNulls() {
213     return getIndexData().shouldIgnoreNulls();
214   }
215 
216   @Override
217   public boolean isUnique() {
218     return getIndexData().isUnique();
219   }
220 
221   @Override
222   public boolean isRequired() {
223     return getIndexData().isRequired();
224   }
225 
226   @Override
227   public List<IndexData.ColumnDescriptor> getColumns() {
228     return getIndexData().getColumns();
229   }
230 
231   @Override
232   public int getColumnCount() {
233     return getIndexData().getColumnCount();
234   }
235 
236   @Override
237   public CursorBuilder newCursor() {
238     return getTable().newCursor().setIndex(this);
239   }
240 
241   /**
242    * Whether or not the complete index state has been read.
243    */
244   public boolean isInitialized() {
245     return getIndexData().isInitialized();
246   }
247 
248   /**
249    * Forces initialization of this index (actual parsing of index pages).
250    * normally, the index will not be initialized until the entries are
251    * actually needed.
252    */
253   public void initialize() throws IOException {
254     getIndexData().initialize();
255   }
256 
257   /**
258    * Gets a new cursor for this index.
259    * <p>
260    * Forces index initialization.
261    */
262   public IndexData.EntryCursor cursor()
263     throws IOException
264   {
265     return cursor(null, true, null, true);
266   }
267 
268   /**
269    * Gets a new cursor for this index, narrowed to the range defined by the
270    * given startRow and endRow.
271    * <p>
272    * Forces index initialization.
273    *
274    * @param startRow the first row of data for the cursor, or {@code null} for
275    *                 the first entry
276    * @param startInclusive whether or not startRow is inclusive or exclusive
277    * @param endRow the last row of data for the cursor, or {@code null} for
278    *               the last entry
279    * @param endInclusive whether or not endRow is inclusive or exclusive
280    */
281   public IndexData.EntryCursor cursor(Object[] startRow,
282                                       boolean startInclusive,
283                                       Object[] endRow,
284                                       boolean endInclusive)
285     throws IOException
286   {
287     return getIndexData().cursor(startRow, startInclusive, endRow,
288                                  endInclusive);
289   }
290 
291   /**
292    * Constructs an array of values appropriate for this index from the given
293    * column values, expected to match the columns for this index.
294    * @return the appropriate sparse array of data
295    * @throws IllegalArgumentException if the wrong number of values are
296    *         provided
297    */
298   public Object[] constructIndexRowFromEntry(Object... values)
299   {
300     return getIndexData().constructIndexRowFromEntry(values);
301   }
302 
303   /**
304    * Constructs an array of values appropriate for this index from the given
305    * column values, possibly only providing a prefix subset of the index
306    * columns (at least one value must be provided).  If a prefix entry is
307    * provided, any missing, trailing index entry values will use the given
308    * filler value.
309    * @return the appropriate sparse array of data
310    * @throws IllegalArgumentException if at least one value is not provided
311    */
312   public Object[] constructPartialIndexRowFromEntry(
313       Object filler, Object... values)
314   {
315     return getIndexData().constructPartialIndexRowFromEntry(filler, values);
316   }
317 
318   /**
319    * Constructs an array of values appropriate for this index from the given
320    * column value.
321    * @return the appropriate sparse array of data or {@code null} if not all
322    *         columns for this index were provided
323    */
324   public Object[] constructIndexRow(String colName, Object value)
325   {
326     return constructIndexRow(Collections.singletonMap(colName, value));
327   }
328 
329   /**
330    * Constructs an array of values appropriate for this index from the given
331    * column value, which must be the first column of the index.  Any missing,
332    * trailing index entry values will use the given filler value.
333    * @return the appropriate sparse array of data or {@code null} if no prefix
334    *         list of columns for this index were provided
335    */
336   public Object[] constructPartialIndexRow(Object filler, String colName, Object value)
337   {
338     return constructPartialIndexRow(filler, Collections.singletonMap(colName, value));
339   }
340 
341   /**
342    * Constructs an array of values appropriate for this index from the given
343    * column values.
344    * @return the appropriate sparse array of data or {@code null} if not all
345    *         columns for this index were provided
346    */
347   public Object[] constructIndexRow(Map<String,?> row)
348   {
349     return getIndexData().constructIndexRow(row);
350   }
351 
352   /**
353    * Constructs an array of values appropriate for this index from the given
354    * column values, possibly only using a subset of the given values.  A
355    * partial row can be created if one or more prefix column values are
356    * provided.  If a prefix can be found, any missing, trailing index entry
357    * values will use the given filler value.
358    * @return the appropriate sparse array of data or {@code null} if no prefix
359    *         list of columns for this index were provided
360    */
361   public Object[] constructPartialIndexRow(Object filler, Map<String,?> row)
362   {
363     return getIndexData().constructPartialIndexRow(filler, row);
364   }
365 
366   @Override
367   public String toString() {
368     ToStringBuilder sb = CustomToStringStyle.builder(this)
369       .append("name", "(" + getTable().getName() + ") " + _name)
370       .append("number", _indexNumber)
371       .append("isPrimaryKey", isPrimaryKey())
372       .append("isForeignKey", isForeignKey());
373     if(_reference != null) {
374       sb.append("foreignKeyReference", _reference);
375     }
376     sb.append("data", _data);
377     return sb.toString();
378   }
379 
380   @Override
381   public int compareTo(IndexImpl other) {
382     if (_indexNumber > other.getIndexNumber()) {
383       return 1;
384     } else if (_indexNumber < other.getIndexNumber()) {
385       return -1;
386     } else {
387       return 0;
388     }
389   }
390 
391   /**
392    * Writes the logical index definitions into a table definition buffer.
393    * @param creator description of the indexes to write
394    * @param buffer Buffer to write to
395    */
396   protected static void writeDefinitions(
397       TableCreator creator, ByteBuffer buffer)
398   {
399     // write logical index information
400     for(IndexBuilder idx : creator.getIndexes()) {
401       writeDefinition(creator, idx, buffer);
402     }
403 
404     // write index names
405     for(IndexBuilder idx : creator.getIndexes()) {
406       TableImpl.writeName(buffer, idx.getName(), creator.getCharset());
407     }
408   }
409 
410   protected static void writeDefinition(
411       TableMutator mutator, IndexBuilder idx, ByteBuffer buffer)
412   {
413     TableMutator.IndexDataState idxDataState = mutator.getIndexDataState(idx);
414 
415     // write logical index information
416     buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def
417     buffer.putInt(idx.getIndexNumber()); // index num
418     buffer.putInt(idxDataState.getIndexDataNumber()); // index data num
419 
420     byte idxType = idx.getType();
421     if(idxType != FOREIGN_KEY_INDEX_TYPE) {
422       buffer.put((byte)0); // related table type
423       buffer.putInt(INVALID_INDEX_NUMBER); // related index num
424       buffer.putInt(0); // related table definition page number
425       buffer.put((byte)0); // cascade updates flag
426       buffer.put((byte)0); // cascade deletes flag
427     } else {
428       ForeignKeyReference reference = mutator.getForeignKey(idx);
429       buffer.put(reference.getTableType()); // related table type
430       buffer.putInt(reference.getOtherIndexNumber()); // related index num
431       buffer.putInt(reference.getOtherTablePageNumber()); // related table definition page number
432       byte updateFlags = 0;
433       if(reference.isCascadeUpdates()) {
434         updateFlags |= CASCADE_UPDATES_FLAG;
435       }
436       byte deleteFlags = 0;
437       if(reference.isCascadeDeletes()) {
438         deleteFlags |= CASCADE_DELETES_FLAG;
439       }
440       if(reference.isCascadeNullOnDelete()) {
441         deleteFlags |= CASCADE_NULL_FLAG;
442       }
443       buffer.put(updateFlags); // cascade updates flag
444       buffer.put(deleteFlags); // cascade deletes flag
445     }
446     buffer.put(idxType); // index type flags
447     buffer.putInt(0); // unknown
448   }
449 
450   private String withErrorContext(String msg) {
451     return withErrorContext(msg, getTable().getDatabase(), getName());
452   }
453 
454   private static String withErrorContext(String msg, DatabaseImpl db,
455                                          String idxName) {
456     return msg + " (Db=" + db.getName() + ";Index=" + idxName + ")";
457   }
458 
459 
460   /**
461    * Information about a foreign key reference defined in an index (when
462    * referential integrity should be enforced).
463    */
464   public static class ForeignKeyReference
465   {
466     private final byte _tableType;
467     private final int _otherIndexNumber;
468     private final int _otherTablePageNumber;
469     private final boolean _cascadeUpdates;
470     private final boolean _cascadeDeletes;
471     private final boolean _cascadeNull;
472 
473     public ForeignKeyReference(
474         byte tableType, int otherIndexNumber, int otherTablePageNumber,
475         boolean cascadeUpdates, boolean cascadeDeletes, boolean cascadeNull)
476     {
477       _tableType = tableType;
478       _otherIndexNumber = otherIndexNumber;
479       _otherTablePageNumber = otherTablePageNumber;
480       _cascadeUpdates = cascadeUpdates;
481       _cascadeDeletes = cascadeDeletes;
482       _cascadeNull = cascadeNull;
483     }
484 
485     public byte getTableType() {
486       return _tableType;
487     }
488 
489     public boolean isPrimaryTable() {
490       return(getTableType() == FK_PRIMARY_TABLE_TYPE);
491     }
492 
493     public int getOtherIndexNumber() {
494       return _otherIndexNumber;
495     }
496 
497     public int getOtherTablePageNumber() {
498       return _otherTablePageNumber;
499     }
500 
501     public boolean isCascadeUpdates() {
502       return _cascadeUpdates;
503     }
504 
505     public boolean isCascadeDeletes() {
506       return _cascadeDeletes;
507     }
508 
509     public boolean isCascadeNullOnDelete() {
510       return _cascadeNull;
511     }
512 
513     @Override
514     public String toString() {
515       return CustomToStringStyle.builder(this)
516         .append("otherIndexNumber", _otherIndexNumber)
517         .append("otherTablePageNum", _otherTablePageNumber)
518         .append("isPrimaryTable", isPrimaryTable())
519         .append("isCascadeUpdates", isCascadeUpdates())
520         .append("isCascadeDeletes", isCascadeDeletes())
521         .append("isCascadeNullOnDelete", isCascadeNullOnDelete())
522         .toString();
523     }
524   }
525 }