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.util.ArrayList;
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Set;
25  
26  import com.healthmarketscience.jackcess.ConstraintViolationException;
27  import com.healthmarketscience.jackcess.IndexBuilder;
28  import com.healthmarketscience.jackcess.IndexCursor;
29  import com.healthmarketscience.jackcess.RelationshipBuilder;
30  import com.healthmarketscience.jackcess.Row;
31  
32  /**
33   * Helper class used to maintain state during relationship creation.
34   *
35   * @author James Ahlborn
36   */
37  public class RelationshipCreator extends DBMutator
38  {
39    private final static int CASCADE_FLAGS =
40      RelationshipImpl.CASCADE_DELETES_FLAG |
41      RelationshipImpl.CASCADE_UPDATES_FLAG |
42      RelationshipImpl.CASCADE_NULL_FLAG;
43  
44    // for the purposes of choosing a backing index for a foreign key, there are
45    // certain index flags that can be ignored (we don't care how they are set)
46    private final static byte IGNORED_PRIMARY_INDEX_FLAGS = 
47      IndexData.IGNORE_NULLS_INDEX_FLAG | IndexData.REQUIRED_INDEX_FLAG;
48    private final static byte IGNORED_SECONDARY_INDEX_FLAGS = 
49      IGNORED_PRIMARY_INDEX_FLAGS | IndexData.UNIQUE_INDEX_FLAG;
50    
51    private TableImpl _primaryTable;
52    private TableImpl _secondaryTable;
53    private RelationshipBuilder _relationship;
54    private List<ColumnImpl> _primaryCols; 
55    private List<ColumnImpl> _secondaryCols;
56    private int _flags;
57    private String _name;
58      
59    public RelationshipCreator(DatabaseImpl database) 
60    {
61      super(database);
62    }
63    
64    public String getName() {
65      return _name;
66    }
67  
68    public TableImpl getPrimaryTable() {
69      return _primaryTable;
70    }
71  
72    public TableImpl getSecondaryTable() {
73      return _secondaryTable;
74    }
75  
76    public boolean hasReferentialIntegrity() {
77      return _relationship.hasReferentialIntegrity();
78    }
79  
80    public RelationshipImpl createRelationshipImpl(String name) {
81      _name = name;
82      RelationshipImpl newRel = new RelationshipImpl(
83          name, _primaryTable, _secondaryTable, _flags, 
84          _primaryCols, _secondaryCols);
85      return newRel;
86    }
87  
88    /**
89     * Creates the relationship in the database.
90     * @usage _advanced_method_
91     */
92    public RelationshipImpl createRelationship(RelationshipBuilder relationship) 
93      throws IOException 
94    {
95      _relationship = relationship;
96      _name = relationship.getName();
97      
98      validate();
99  
100     _flags = _relationship.getFlags();
101     // need to determine the one-to-one flag on our own
102     if(isOneToOne()) {
103       _flags |= RelationshipImpl.ONE_TO_ONE_FLAG;
104     }
105 
106     getPageChannel().startExclusiveWrite();
107     try {
108 
109       RelationshipImpl newRel = getDatabase().writeRelationship(this);
110 
111       if(hasReferentialIntegrity()) {
112         addPrimaryIndex();
113         addSecondaryIndex();
114       }
115 
116       return newRel;
117 
118     } finally {
119       getPageChannel().finishWrite();
120     }
121   }
122 
123   private void addPrimaryIndex() throws IOException {
124     TableUpdater updater = new TableUpdater(_primaryTable);
125     updater.setForeignKey(createFKReference(true));
126     updater.addIndex(createPrimaryIndex(), true, 
127                      IGNORED_PRIMARY_INDEX_FLAGS, (byte)0);
128   }
129 
130   private void addSecondaryIndex() throws IOException {
131     TableUpdater updater = new TableUpdater(_secondaryTable);
132     updater.setForeignKey(createFKReference(false));
133     updater.addIndex(createSecondaryIndex(), true, 
134                      IGNORED_SECONDARY_INDEX_FLAGS, (byte)0);
135   }
136 
137   private IndexImpl.ForeignKeyReference createFKReference(boolean isPrimary) {
138     byte tableType = 0;
139     int otherTableNum = 0;
140     int otherIdxNum = 0;
141     if(isPrimary) {
142       tableType = IndexImpl.FK_PRIMARY_TABLE_TYPE;
143       otherTableNum = _secondaryTable.getTableDefPageNumber();
144       // we create the primary index first, so the secondary index does not
145       // exist yet
146       otherIdxNum = _secondaryTable.getLogicalIndexCount();
147     } else {
148       tableType = IndexImpl.FK_SECONDARY_TABLE_TYPE;
149       otherTableNum = _primaryTable.getTableDefPageNumber();
150       // at this point, we've already created the primary index, it's the last
151       // one on the primary table
152       otherIdxNum = _primaryTable.getLogicalIndexCount() - 1;
153     }
154     boolean cascadeUpdates = ((_flags & RelationshipImpl.CASCADE_UPDATES_FLAG) != 0);
155     boolean cascadeDeletes = ((_flags & RelationshipImpl.CASCADE_DELETES_FLAG) != 0);
156     boolean cascadeNull = ((_flags & RelationshipImpl.CASCADE_NULL_FLAG) != 0);
157 
158     return new IndexImpl.ForeignKeyReference(
159         tableType, otherIdxNum, otherTableNum, cascadeUpdates, cascadeDeletes, 
160         cascadeNull);
161   }
162 
163   private void validate() throws IOException {
164 
165     _primaryTable = getDatabase().getTable(_relationship.getFromTable());
166     _secondaryTable = getDatabase().getTable(_relationship.getToTable());
167     
168     if((_primaryTable == null) || (_secondaryTable == null)) {
169       throw new IllegalArgumentException(withErrorContext(
170           "Two valid tables are required in relationship"));
171     }
172 
173     if(_name != null) {
174       DatabaseImpl.validateIdentifierName(
175           _name, _primaryTable.getFormat().MAX_INDEX_NAME_LENGTH, "relationship");
176     }
177 
178     _primaryCols = getColumns(_primaryTable, _relationship.getFromColumns());
179     _secondaryCols = getColumns(_secondaryTable, _relationship.getToColumns());
180     
181     if((_primaryCols == null) || (_primaryCols.isEmpty()) || 
182        (_secondaryCols == null) || (_secondaryCols.isEmpty())) {
183       throw new IllegalArgumentException(withErrorContext(
184           "Missing columns in relationship"));
185     }
186 
187     if(_primaryCols.size() != _secondaryCols.size()) {
188       throw new IllegalArgumentException(withErrorContext(
189           "Must have same number of columns on each side of relationship"));
190     }
191 
192     for(int i = 0; i < _primaryCols.size(); ++i) {
193       ColumnImpl pcol = _primaryCols.get(i);
194       ColumnImpl scol = _primaryCols.get(i);
195 
196       if(pcol.getType() != scol.getType()) {
197         throw new IllegalArgumentException(withErrorContext(
198             "Matched columns must have the same data type"));
199       }
200     }
201 
202     if(!hasReferentialIntegrity()) {
203 
204       if((_relationship.getFlags() & CASCADE_FLAGS) != 0) {
205         throw new IllegalArgumentException(withErrorContext(
206             "Cascade flags cannot be enabled if referential integrity is not enforced"));
207       }
208       
209       return;
210     }
211 
212     // for now, we will require the unique index on the primary table (just
213     // like access does).  we could just create it auto-magically...
214     IndexImpl primaryIdx = getUniqueIndex(_primaryTable, _primaryCols);
215     if(primaryIdx == null) {
216       throw new IllegalArgumentException(withErrorContext(
217           "Missing unique index on primary table required to enforce integrity"));
218     }
219 
220     // while relationships can have "dupe" columns, indexes (and therefore
221     // integrity enforced relationships) cannot
222     if((new HashSet<String>(getColumnNames(_primaryCols)).size() != 
223         _primaryCols.size()) ||
224        (new HashSet<String>(getColumnNames(_secondaryCols)).size() != 
225         _secondaryCols.size())) {
226       throw new IllegalArgumentException(withErrorContext(
227           "Cannot have duplicate columns in an integrity enforced relationship"));
228     }
229     
230     // TODO: future, check for enforce cycles?
231 
232     // check referential integrity
233     IndexCursor primaryCursor = primaryIdx.newCursor().toIndexCursor();
234     Object[] entryValues = new Object[_secondaryCols.size()];
235     for(Row row : _secondaryTable.newCursor().toCursor()
236           .newIterable().addColumns(_secondaryCols)) {
237       // grab the secondary table values
238       boolean hasValues = false;
239       for(int i = 0; i < _secondaryCols.size(); ++i) {
240         entryValues[i] = _secondaryCols.get(i).getRowValue(row);
241         hasValues = hasValues || (entryValues[i] != null);
242       }
243 
244       if(!hasValues) {
245         // we can ignore null entries
246         continue;
247       }
248 
249       // check that they exist in the primary table
250       if(!primaryCursor.findFirstRowByEntry(entryValues)) {
251         throw new ConstraintViolationException(withErrorContext(
252             "Integrity constraint violation found for relationship"));
253       }
254     }
255 
256   }
257 
258   private IndexBuilder createPrimaryIndex() {
259     String name = createPrimaryIndexName();
260     return createIndex(name, _primaryCols)
261       .setUnique()
262       .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
263   }
264   
265   private IndexBuilder createSecondaryIndex() {
266     // secondary index uses relationship name
267     return createIndex(_name, _secondaryCols)
268       .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
269   }
270   
271   private static IndexBuilder createIndex(String name, List<ColumnImpl> cols) {
272     IndexBuilder idx = new IndexBuilder(name);
273     for(ColumnImpl col : cols) {
274       idx.addColumns(col.getName());
275     }
276     return idx;
277   }
278 
279   private String createPrimaryIndexName() {
280     Set<String> idxNames = TableUpdater.getIndexNames(_primaryTable, null);
281 
282     // primary naming scheme: ".rB", .rC", ".rD", "rE" ...
283     String baseName = ".r";
284     String suffix = "B";
285 
286     while(true) {
287       String idxName = baseName + suffix;
288       if(!idxNames.contains(DatabaseImpl.toLookupName(idxName))) {
289         return idxName;
290       }
291 
292       char c = (char)(suffix.charAt(0) + 1);
293       if(c == '[') {
294         c = 'a';
295       }
296       suffix = "" + c;
297     }    
298   }
299 
300   private static List<ColumnImpl> getColumns(TableImpl table, 
301                                              List<String> colNames) {
302     List<ColumnImpl> cols = new ArrayList<ColumnImpl>();
303     for(String colName : colNames) {
304       cols.add(table.getColumn(colName));
305     }
306     return cols;
307   }
308 
309   private static List<String> getColumnNames(List<ColumnImpl> cols) {
310     List<String> colNames = new ArrayList<String>();
311     for(ColumnImpl col : cols) {
312       colNames.add(col.getName());
313     }
314     return colNames;
315   }
316 
317   private boolean isOneToOne() {
318     // a relationship is one to one if the two sides of the relationship have
319     // unique indexes on the relevant columns
320     if(getUniqueIndex(_primaryTable, _primaryCols) == null) {
321       return false;
322     }
323     IndexImpl idx = getUniqueIndex(_secondaryTable, _secondaryCols);
324     return (idx != null);
325   }
326 
327   private static IndexImpl getUniqueIndex(
328       TableImpl table, List<ColumnImpl> cols) {
329     return table.findIndexForColumns(getColumnNames(cols), 
330                                      TableImpl.IndexFeature.EXACT_UNIQUE_ONLY);
331   }
332 
333   private static String getTableErrorContext(
334       TableImpl table, List<ColumnImpl> cols,
335       String tableName, Collection<String> colNames) {
336     if(table != null) {
337       tableName = table.getName();
338     }
339     if(cols != null) {
340       colNames = getColumnNames(cols);
341     }
342 
343     return CustomToStringStyle.valueBuilder(tableName)
344       .append(null, colNames)
345       .toString();
346   }
347   
348   private String withErrorContext(String msg) {
349     return msg + "(Rel=" +
350       getTableErrorContext(_primaryTable, _primaryCols, 
351                            _relationship.getFromTable(),
352                            _relationship.getFromColumns()) + " -> " +
353       getTableErrorContext(_secondaryTable, _secondaryCols, 
354                            _relationship.getToTable(),
355                            _relationship.getToColumns()) + ")";
356   }
357 }