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