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