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