You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RelationshipCreator.java 12KB

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