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