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.lang.System.Logger;
21  import java.nio.ByteBuffer;
22  import java.util.Collections;
23  import java.util.Comparator;
24  import java.util.List;
25  import java.util.Map;
26  
27  import com.healthmarketscience.jackcess.CursorBuilder;
28  import com.healthmarketscience.jackcess.Index;
29  import com.healthmarketscience.jackcess.IndexBuilder;
30  import com.healthmarketscience.jackcess.PropertyMap;
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
39  {
40    protected static final Logger LOG = System.getLogger(IndexImpl.class.getName());
41  
42    /** comparator which sorts indexes based on their persisted index */
43    static final Comparator<IndexImpl> DEFAULT_ORDER_COMPARATOR =
44      Comparator.comparingInt(IndexImpl::getIndexNumber);
45  
46    /** index type for primary key indexes */
47    public static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1;
48  
49    /** index type for foreign key indexes */
50    public static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2;
51  
52    /** flag for indicating that updates should cascade in a foreign key index */
53    private static final byte CASCADE_UPDATES_FLAG = (byte)1;
54    /** flag for indicating that deletes should cascade in a foreign key index */
55    private static final byte CASCADE_DELETES_FLAG = (byte)1;
56    /** flag for indicating that deletes should cascade a null in a foreign key
57        index */
58    private static final byte CASCADE_NULL_FLAG = (byte)2;
59  
60    /** index table type for the "primary" table in a foreign key index */
61    static final byte FK_PRIMARY_TABLE_TYPE = (byte)1;
62    /** index table type for the "secondary" table in a foreign key index */
63    static final byte FK_SECONDARY_TABLE_TYPE = (byte)2;
64  
65    /** indicate an invalid index number for foreign key field */
66    private static final int INVALID_INDEX_NUMBER = -1;
67  
68    /** the actual data backing this index (more than one index may be backed by
69        the same data */
70    private final IndexData _data;
71    /** properties for this index, if any */
72    private PropertyMap _props;
73    /** 0-based index number */
74    private final int _indexNumber;
75    /** the type of the index */
76    private final byte _indexType;
77    /** Index name */
78    private String _name;
79    /** foreign key reference info, if any */
80    private final ForeignKeyReference _reference;
81  
82    protected IndexImpl(ByteBuffer tableBuffer, List<IndexData> indexDatas,
83                        JetFormat format)
84    {
85      ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
86      _indexNumber = tableBuffer.getInt();
87      int indexDataNumber = tableBuffer.getInt();
88  
89      // read foreign key reference info
90      byte relIndexType = tableBuffer.get();
91      int relIndexNumber = tableBuffer.getInt();
92      int relTablePageNumber = tableBuffer.getInt();
93      byte cascadeUpdatesFlag = tableBuffer.get();
94      byte cascadeDeletesFlag = tableBuffer.get();
95  
96      _indexType = tableBuffer.get();
97  
98      if((_indexType == FOREIGN_KEY_INDEX_TYPE) &&
99         (relIndexNumber != INVALID_INDEX_NUMBER)) {
100       _reference = new ForeignKeyReference(
101           relIndexType, relIndexNumber, relTablePageNumber,
102           ((cascadeUpdatesFlag & CASCADE_UPDATES_FLAG) != 0),
103           ((cascadeDeletesFlag & CASCADE_DELETES_FLAG) != 0),
104           ((cascadeDeletesFlag & CASCADE_NULL_FLAG) != 0));
105     } else {
106       _reference = null;
107     }
108 
109     ByteUtil.forward(tableBuffer, format.SKIP_AFTER_INDEX_SLOT); //Skip past Unknown
110 
111     _data = indexDatas.get(indexDataNumber);
112 
113     _data.addIndex(this);
114   }
115 
116   public IndexData getIndexData() {
117     return _data;
118   }
119 
120   @Override
121   public TableImpl getTable() {
122     return getIndexData().getTable();
123   }
124 
125   public JetFormat getFormat() {
126     return getTable().getFormat();
127   }
128 
129   public PageChannel getPageChannel() {
130     return getTable().getPageChannel();
131   }
132 
133   public int getIndexNumber() {
134     return _indexNumber;
135   }
136 
137   public byte getIndexFlags() {
138     return getIndexData().getIndexFlags();
139   }
140 
141   public int getUniqueEntryCount() {
142     return getIndexData().getUniqueEntryCount();
143   }
144 
145   public int getUniqueEntryCountOffset() {
146     return getIndexData().getUniqueEntryCountOffset();
147   }
148 
149   @Override
150   public String getName() {
151     return _name;
152   }
153 
154   void setName(String name) {
155     _name = name;
156   }
157 
158   @Override
159   public PropertyMap getProperties() throws IOException {
160     if(_props == null) {
161       _props = getTable().getPropertyMaps().getIndex(getName());
162     }
163     return _props;
164   }
165 
166   @Override
167   public boolean isPrimaryKey() {
168     return _indexType == PRIMARY_KEY_INDEX_TYPE;
169   }
170 
171   @Override
172   public boolean isForeignKey() {
173     return _indexType == FOREIGN_KEY_INDEX_TYPE;
174   }
175 
176   public ForeignKeyReference getReference() {
177     return _reference;
178   }
179 
180   @Override
181   public IndexImpl getReferencedIndex() throws IOException {
182 
183     if(_reference == null) {
184       return null;
185     }
186 
187     TableImpl refTable = getTable().getDatabase().getTable(
188         _reference.getOtherTablePageNumber());
189 
190     if(refTable == null) {
191       throw new IOException(withErrorContext(
192               "Reference to missing table " + _reference.getOtherTablePageNumber()));
193     }
194 
195     IndexImpl refIndex = null;
196     int idxNumber = _reference.getOtherIndexNumber();
197     for(IndexImpl idx : refTable.getIndexes()) {
198       if(idx.getIndexNumber() == idxNumber) {
199         refIndex = idx;
200         break;
201       }
202     }
203 
204     if(refIndex == null) {
205       throw new IOException(withErrorContext(
206               "Reference to missing index " + idxNumber +
207               " on table " + refTable.getName()));
208     }
209 
210     // finally verify that we found the expected index (should reference this
211     // index)
212     ForeignKeyReference otherRef = refIndex.getReference();
213     if((otherRef == null) ||
214        (otherRef.getOtherTablePageNumber() !=
215         getTable().getTableDefPageNumber()) ||
216        (otherRef.getOtherIndexNumber() != _indexNumber)) {
217       throw new IOException(withErrorContext(
218               "Found unexpected index " + refIndex.getName() +
219               " on table " + refTable.getName() + " with reference " + otherRef));
220     }
221 
222     return refIndex;
223   }
224 
225   @Override
226   public boolean shouldIgnoreNulls() {
227     return getIndexData().shouldIgnoreNulls();
228   }
229 
230   @Override
231   public boolean isUnique() {
232     return getIndexData().isUnique();
233   }
234 
235   @Override
236   public boolean isRequired() {
237     return getIndexData().isRequired();
238   }
239 
240   @Override
241   public List<IndexData.ColumnDescriptor> getColumns() {
242     return getIndexData().getColumns();
243   }
244 
245   @Override
246   public int getColumnCount() {
247     return getIndexData().getColumnCount();
248   }
249 
250   @Override
251   public CursorBuilder newCursor() {
252     return getTable().newCursor().setIndex(this);
253   }
254 
255   /**
256    * Whether or not the complete index state has been read.
257    */
258   public boolean isInitialized() {
259     return getIndexData().isInitialized();
260   }
261 
262   /**
263    * Forces initialization of this index (actual parsing of index pages).
264    * normally, the index will not be initialized until the entries are
265    * actually needed.
266    */
267   public void initialize() throws IOException {
268     getIndexData().initialize();
269   }
270 
271   /**
272    * Gets a new cursor for this index.
273    * <p>
274    * Forces index initialization.
275    */
276   public IndexData.EntryCursor cursor()
277     throws IOException
278   {
279     return cursor(null, true, null, true);
280   }
281 
282   /**
283    * Gets a new cursor for this index, narrowed to the range defined by the
284    * given startRow and endRow.
285    * <p>
286    * Forces index initialization.
287    *
288    * @param startRow the first row of data for the cursor, or {@code null} for
289    *                 the first entry
290    * @param startInclusive whether or not startRow is inclusive or exclusive
291    * @param endRow the last row of data for the cursor, or {@code null} for
292    *               the last entry
293    * @param endInclusive whether or not endRow is inclusive or exclusive
294    */
295   public IndexData.EntryCursor cursor(Object[] startRow,
296                                       boolean startInclusive,
297                                       Object[] endRow,
298                                       boolean endInclusive)
299     throws IOException
300   {
301     return getIndexData().cursor(startRow, startInclusive, endRow,
302                                  endInclusive);
303   }
304 
305   /**
306    * Constructs an array of values appropriate for this index from the given
307    * column values, expected to match the columns for this index.
308    * @return the appropriate sparse array of data
309    * @throws IllegalArgumentException if the wrong number of values are
310    *         provided
311    */
312   public Object[] constructIndexRowFromEntry(Object... values)
313   {
314     return getIndexData().constructIndexRowFromEntry(values);
315   }
316 
317   /**
318    * Constructs an array of values appropriate for this index from the given
319    * column values, possibly only providing a prefix subset of the index
320    * columns (at least one value must be provided).  If a prefix entry is
321    * provided, any missing, trailing index entry values will use the given
322    * filler value.
323    * @return the appropriate sparse array of data
324    * @throws IllegalArgumentException if at least one value is not provided
325    */
326   public Object[] constructPartialIndexRowFromEntry(
327       Object filler, Object... values)
328   {
329     return getIndexData().constructPartialIndexRowFromEntry(filler, values);
330   }
331 
332   /**
333    * Constructs an array of values appropriate for this index from the given
334    * column value.
335    * @return the appropriate sparse array of data or {@code null} if not all
336    *         columns for this index were provided
337    */
338   public Object[] constructIndexRow(String colName, Object value)
339   {
340     return constructIndexRow(Collections.singletonMap(colName, value));
341   }
342 
343   /**
344    * Constructs an array of values appropriate for this index from the given
345    * column value, which must be the first column of the index.  Any missing,
346    * trailing index entry values will use the given filler value.
347    * @return the appropriate sparse array of data or {@code null} if no prefix
348    *         list of columns for this index were provided
349    */
350   public Object[] constructPartialIndexRow(Object filler, String colName, Object value)
351   {
352     return constructPartialIndexRow(filler, Collections.singletonMap(colName, value));
353   }
354 
355   /**
356    * Constructs an array of values appropriate for this index from the given
357    * column values.
358    * @return the appropriate sparse array of data or {@code null} if not all
359    *         columns for this index were provided
360    */
361   public Object[] constructIndexRow(Map<String,?> row)
362   {
363     return getIndexData().constructIndexRow(row);
364   }
365 
366   /**
367    * Constructs an array of values appropriate for this index from the given
368    * column values, possibly only using a subset of the given values.  A
369    * partial row can be created if one or more prefix column values are
370    * provided.  If a prefix can be found, any missing, trailing index entry
371    * values will use the given filler value.
372    * @return the appropriate sparse array of data or {@code null} if no prefix
373    *         list of columns for this index were provided
374    */
375   public Object[] constructPartialIndexRow(Object filler, Map<String,?> row)
376   {
377     return getIndexData().constructPartialIndexRow(filler, row);
378   }
379 
380   @Override
381   public String toString() {
382     ToStringBuilder sb = ToStringBuilder.builder(this)
383       .append("name", "(" + getTable().getName() + ") " + _name)
384       .append("number", _indexNumber)
385       .append("isPrimaryKey", isPrimaryKey())
386       .append("isForeignKey", isForeignKey());
387     if(_reference != null) {
388       sb.append("foreignKeyReference", _reference);
389     }
390     sb.append("data", _data);
391     return sb.toString();
392   }
393 
394   /**
395    * Writes the logical index definitions into a table definition buffer.
396    * @param creator description of the indexes to write
397    * @param buffer Buffer to write to
398    */
399   protected static void writeDefinitions(
400       TableCreator creator, ByteBuffer buffer)
401   {
402     // write logical index information
403     for(IndexBuilder idx : creator.getIndexes()) {
404       writeDefinition(creator, idx, buffer);
405     }
406 
407     // write index names
408     for(IndexBuilder idx : creator.getIndexes()) {
409       TableImpl.writeName(buffer, idx.getName(), creator.getCharset());
410     }
411   }
412 
413   protected static void writeDefinition(
414       TableMutator mutator, IndexBuilder idx, ByteBuffer buffer)
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); // constant zero
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 ToStringBuilder.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 }