View Javadoc
1   /*
2   Copyright (c) 2016 James Ahlborn
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.ArrayList;
22  import java.util.EnumSet;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
26  
27  import com.healthmarketscience.jackcess.ColumnBuilder;
28  import com.healthmarketscience.jackcess.DataType;
29  import com.healthmarketscience.jackcess.IndexBuilder;
30  import com.healthmarketscience.jackcess.PropertyMap;
31  
32  /**
33   * Helper class used to maintain state during table mutation.
34   *
35   * @author James Ahlborn
36   * @usage _advanced_class_
37   */
38  public class TableUpdater extends TableMutator
39  {
40    private final TableImpl _table;
41  
42    private ColumnBuilder _column;
43    private IndexBuilder _index;
44    private int _origTdefLen;
45    private int _addedTdefLen;
46    private final List<Integer> _nextPages = new ArrayList<>(1);
47    private ColumnState _colState;
48    private IndexDataState _idxDataState;
49    private IndexImpl.ForeignKeyReference _fkReference;
50  
51    public TableUpdater(TableImpl table) {
52      super(table.getDatabase());
53      _table = table;
54    }
55  
56    public ColumnBuilder getColumn() {
57      return _column;
58    }
59  
60    public IndexBuilder getIndex() {
61      return _index;
62    }
63  
64    @Override
65    String getTableName() {
66      return _table.getName();
67    }
68  
69    @Override
70    public int getTdefPageNumber() {
71      return _table.getTableDefPageNumber();
72    }
73  
74    @Override
75    short getColumnNumber(String colName) {
76      for(ColumnImpl col : _table.getColumns()) {
77        if(col.getName().equalsIgnoreCase(colName)) {
78          return col.getColumnNumber();
79        }
80      }
81      return IndexData.COLUMN_UNUSED;
82    }
83  
84    @Override
85    boolean isComplexColumn(String colName) {
86      for(ColumnImpl col : _table.getColumns()) {
87        if(col.getName().equalsIgnoreCase(colName)) {
88          return (col.getType() == DataType.COMPLEX_TYPE);
89        }
90      }
91      return false;
92    }
93  
94    @Override
95    public ColumnState getColumnState(ColumnBuilder col) {
96      return ((col == _column) ? _colState : null);
97    }
98  
99    @Override
100   public IndexDataState getIndexDataState(IndexBuilder idx) {
101     return ((idx == _index) ? _idxDataState : null);
102   }
103 
104   void setForeignKey(IndexImpl.ForeignKeyReference fkReference) {
105     _fkReference = fkReference;
106   }
107 
108   @Override
109   public IndexImpl.ForeignKeyReference getForeignKey(IndexBuilder idx) {
110     return ((idx == _index) ? _fkReference : null);
111   }
112 
113   int getAddedTdefLen() {
114     return _addedTdefLen;
115   }
116 
117   void addTdefLen(int add) {
118     _addedTdefLen += add;
119   }
120 
121   void setOrigTdefLen(int len) {
122     _origTdefLen = len;
123   }
124 
125   List<Integer> getNextPages() {
126     return _nextPages;
127   }
128 
129   void resetTdefInfo() {
130     _addedTdefLen = 0;
131     _origTdefLen = 0;
132     _nextPages.clear();
133   }
134 
135   public ColumnImpl addColumn(ColumnBuilder column) throws IOException {
136     return addColumn(column, false);
137   }
138 
139   /**
140    * @param isInternal whether this column is part of a larger operation which
141    *                   already holds an exclusive write
142    */
143   private ColumnImpl addColumn(ColumnBuilder column, boolean isInternal)
144     throws IOException
145   {
146     _column = column;
147 
148     validateAddColumn();
149 
150     // assign column number and do some assorted column bookkeeping
151     short columnNumber = (short)_table.getMaxColumnCount();
152     _column.setColumnNumber(columnNumber);
153     _column.setColumnId(getNextColumnId());
154     if(_column.getType().isLongValue() ||
155        (_column.getType() == DataType.COMPLEX_TYPE)) {
156       // a long value column needs its usage maps and a complex column its id
157       _colState = new ColumnState();
158     }
159 
160     if(!isInternal) {
161       getPageChannel().startExclusiveWrite();
162     } else {
163       // this column is part of a larger operation which already holds an
164       // exclusive write, and finishWrite flushes once the outermost one ends
165       getPageChannel().startWrite();
166     }
167     try {
168 
169       if(_column.getComplexDesc() != null) {
170         // the supporting tables and the row which names them have to exist
171         // before the column which carries their id is read back
172         ComplexColumnCreator.create(this, _table.getName(),
173                                     _table.getTableDefPageNumber(), _column);
174       }
175 
176       ColumnImpl newCol = _table.mutateAddColumn(this);
177 
178       if(_column.getComplexDesc() != null) {
179         getDatabase().addToSystemCatalogFlags(
180             _table.getTableDefPageNumber(),
181             ComplexColumnCreator.CATALOG_FLAGS_HAS_COMPLEX);
182       }
183 
184       if(_column.isAppendOnly()) {
185         // the versions of a memo column live in a complex column beside it
186         new TableUpdater(_table).addColumn(
187             ComplexColumnCreator.newVersionHistoryColumn(_column), true);
188         PropertyMap tableProps = _table.getProperties();
189         tableProps.put(PropertyMap.APPEND_ONLY_PROP, DataType.BOOLEAN,
190                        Boolean.TRUE);
191         tableProps.save();
192       }
193 
194       return newCol;
195 
196     } finally {
197       getPageChannel().finishWrite();
198     }
199   }
200 
201   /**
202    * Access assigns each column an id which it never reuses and never
203    * renumbers, so a table which has had a column deleted has gaps in its ids
204    * and its highest id can be past the last column number.
205    *
206    * @return an id which no column of this table holds
207    */
208   private short getNextColumnId() {
209     short maxId = -1;
210     for(ColumnImpl col : _table.getColumns()) {
211       if(col.getColumnId() > maxId) {
212         maxId = col.getColumnId();
213       }
214     }
215     return (short)(maxId + 1);
216   }
217 
218   public IndexImpl addIndex(IndexBuilder index) throws IOException {
219     return addIndex(index, false, (byte)0, (byte)0);
220   }
221 
222   IndexImpl addIndex(IndexBuilder index, boolean isInternal, byte ignoreIdxFlags,
223                      byte ignoreColFlags)
224     throws IOException
225   {
226     _index = index;
227 
228     if(!isInternal) {
229       validateAddIndex();
230     }
231 
232     // assign index number and do some assorted index bookkeeping
233     int indexNumber = _table.getLogicalIndexCount();
234     _index.setIndexNumber(indexNumber);
235 
236     // initialize backing index state
237     initIndexDataState(ignoreIdxFlags, ignoreColFlags);
238 
239     if(!isInternal) {
240       getPageChannel().startExclusiveWrite();
241     } else {
242       // if "internal" update, this is part of a larger operation which
243       // already holds an exclusive write lock
244       getPageChannel().startWrite();
245     }
246     try {
247 
248       if(_idxDataState.getIndexDataNumber() == _table.getIndexCount()) {
249         // we need a new backing index data
250         _table.mutateAddIndexData(this);
251 
252         // we need to modify the table def again when adding the Index, so reset
253         resetTdefInfo();
254       }
255 
256       return _table.mutateAddIndex(this);
257 
258     } finally {
259       getPageChannel().finishWrite();
260     }
261   }
262 
263   boolean validateUpdatedTdef(ByteBuffer tableBuffer) {
264     // sanity check the updates
265     return((_origTdefLen + _addedTdefLen) == tableBuffer.limit());
266   }
267 
268   private void validateAddColumn() {
269 
270     if(_column == null) {
271       throw new IllegalArgumentException(withErrorContext(
272           "Cannot add column with no column"));
273     }
274     if((_table.getColumnCount() + 1) > getFormat().MAX_COLUMNS_PER_TABLE) {
275       throw new IllegalArgumentException(withErrorContext(
276           "Cannot add column to table with " +
277           getFormat().MAX_COLUMNS_PER_TABLE + " columns"));
278     }
279 
280     Set<String> colNames = getColumnNames();
281     // next, validate the column definition
282     validateColumn(colNames, _column);
283 
284     if(_column.isAppendOnly() &&
285        colNames.contains(DatabaseImpl.toLookupName(
286                              ComplexColumnCreator.VERSION_HISTORY_COL_NAME))) {
287       throw new IllegalArgumentException(withErrorContext(
288           "Only one memo column of a table can keep a version history, " +
289           "because every version history column has the same name"));
290     }
291 
292     if(_column.isAutoNumber()) {
293       // for most autonumber types, we can only have one of each type
294       Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
295       for(ColumnImpl column : _table.getAutoNumberColumns()) {
296         autoTypes.add(column.getType());
297       }
298 
299       validateAutoNumberColumn(autoTypes, _column);
300     }
301   }
302 
303   private void validateAddIndex() {
304 
305     if(_index == null) {
306       throw new IllegalArgumentException(withErrorContext(
307           "Cannot add index with no index"));
308     }
309     if((_table.getLogicalIndexCount() + 1) > getFormat().MAX_INDEXES_PER_TABLE) {
310       throw new IllegalArgumentException(withErrorContext(
311           "Cannot add index to table with " +
312           getFormat().MAX_INDEXES_PER_TABLE + " indexes"));
313     }
314 
315     boolean foundPk[] = new boolean[1];
316     Set<String> idxNames = getIndexNames(_table, foundPk);
317     // next, validate the index definition
318     validateIndex(getColumnNames(), idxNames, foundPk, _index);
319   }
320 
321   private Set<String> getColumnNames() {
322     Set<String> colNames = new HashSet<>();
323     for(ColumnImpl column : _table.getColumns()) {
324       colNames.add(DatabaseImpl.toLookupName(column.getName()));
325     }
326     return colNames;
327   }
328 
329   static Set<String> getIndexNames(TableImpl table, boolean[] foundPk) {
330     Set<String> idxNames = new HashSet<>();
331     for(IndexImpl index : table.getIndexes()) {
332       idxNames.add(DatabaseImpl.toLookupName(index.getName()));
333       if(index.isPrimaryKey() && (foundPk != null)) {
334         foundPk[0] = true;
335       }
336     }
337     return idxNames;
338   }
339 
340   private void initIndexDataState(byte ignoreIdxFlags, byte ignoreColFlags) {
341 
342     _idxDataState = new IndexDataState();
343     _idxDataState.addIndex(_index);
344 
345     // search for an existing index which matches the given index (in terms of
346     // the backing data)
347     IndexData idxData = findIndexData(
348         _index, _table, ignoreIdxFlags, ignoreColFlags);
349 
350     int idxDataNumber = ((idxData != null) ?
351                          idxData.getIndexDataNumber() :
352                          _table.getIndexCount());
353 
354     _idxDataState.setIndexDataNumber(idxDataNumber);
355   }
356 
357   static IndexData findIndexData(IndexBuilder idx, TableImpl table,
358                                  byte ignoreIdxFlags, byte ignoreColFlags)
359   {
360     for(IndexData idxData : table.getIndexDatas()) {
361       if(sameIndexData(idx, idxData, ignoreIdxFlags, ignoreColFlags)) {
362         return idxData;
363       }
364     }
365     return null;
366   }
367 
368   private static boolean sameIndexData(IndexBuilder idx1, IndexData idx2,
369                                        byte ignoreIdxFlags, byte ignoreColFlags) {
370     // index data can be combined if flags match and columns (and col flags)
371     // match
372     if((idx1.getFlags() | ignoreIdxFlags) !=
373        (idx2.getIndexFlags() | ignoreIdxFlags)) {
374       return false;
375     }
376 
377     if(idx1.getColumns().size() != idx2.getColumnCount()) {
378       return false;
379     }
380 
381     for(int i = 0; i < idx1.getColumns().size(); ++i) {
382       IndexBuilder.Column col1 = idx1.getColumns().get(i);
383       IndexData.ColumnDescriptor col2 = idx2.getColumns().get(i);
384 
385       if(!sameIndexData(col1, col2, ignoreColFlags)) {
386         return false;
387       }
388     }
389 
390     return true;
391   }
392 
393   private static boolean sameIndexData(
394       IndexBuilder.Column col1, IndexData.ColumnDescriptor col2,
395       int ignoreColFlags) {
396     return (col1.getName().equals(col2.getName()) &&
397             ((col1.getFlags() | ignoreColFlags) ==
398              (col2.getFlags() | ignoreColFlags)));
399   }
400 
401   @Override
402   protected String withErrorContext(String msg) {
403     String objStr = "";
404     if(_column != null) {
405       objStr = ";Column=" + _column.getName();
406     } else if(_index != null) {
407       objStr = ";Index=" + _index.getName();
408     }
409     return msg + "(Table=" + _table.getName() + objStr + ")";
410   }
411 }