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

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