001// Copyright 2011 Leo Przybylski. All rights reserved. 002// 003// Redistribution and use in source and binary forms, with or without modification, are 004// permitted provided that the following conditions are met: 005// 006// 1. Redistributions of source code must retain the above copyright notice, this list of 007// conditions and the following disclaimer. 008// 009// 2. Redistributions in binary form must reproduce the above copyright notice, this list 010// of conditions and the following disclaimer in the documentation and/or other materials 011// provided with the distribution. 012// 013// THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED 014// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 015// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR 016// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 017// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 018// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 019// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 020// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 021// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 022// 023// The views and conclusions contained in the software and documentation are those of the 024// authors and should not be interpreted as representing official policies, either expressed 025// or implied, of Leo Przybylski. 026package org.kualigan.tools.liquibase; 027 028import liquibase.diff.*; 029import liquibase.database.Database; 030import liquibase.database.structure.*; 031import liquibase.exception.DatabaseException; 032import liquibase.snapshot.DatabaseSnapshot; 033import liquibase.snapshot.DatabaseSnapshotGeneratorFactory; 034import liquibase.util.StringUtils; 035 036import java.util.*; 037 038public class Diff { 039 040 private Database referenceDatabase; 041 private Database targetDatabase; 042 043 private DatabaseSnapshot referenceSnapshot; 044 private DatabaseSnapshot targetSnapshot; 045 046 private Set<DiffStatusListener> statusListeners = new HashSet<DiffStatusListener>(); 047 048 private boolean diffTables = true; 049 private boolean diffColumns = true; 050 private boolean diffViews = true; 051 private boolean diffPrimaryKeys = true; 052 private boolean diffUniqueConstraints = true; 053 private boolean diffIndexes = true; 054 private boolean diffForeignKeys = true; 055 private boolean diffSequences = true; 056 private boolean diffData = false; 057 058 public Diff(Database referenceDatabase, Database targetDatabase) { 059 this.referenceDatabase = referenceDatabase; 060 061 this.targetDatabase = targetDatabase; 062 } 063 064 public Diff(Database originalDatabase, String schema) 065 throws DatabaseException { 066 targetDatabase = null; 067 068 referenceDatabase = originalDatabase; 069 referenceDatabase.setDefaultSchemaName(schema); 070 } 071 072 public Diff(DatabaseSnapshot referenceSnapshot, 073 DatabaseSnapshot targetDatabaseSnapshot) { 074 this.referenceSnapshot = referenceSnapshot; 075 076 this.targetSnapshot = targetDatabaseSnapshot; 077 } 078 079 public void addStatusListener(DiffStatusListener listener) { 080 statusListeners.add(listener); 081 } 082 083 public void removeStatusListener(DiffStatusListener listener) { 084 statusListeners.remove(listener); 085 } 086 087 public DiffResult compare() throws DatabaseException { 088 if (referenceSnapshot == null) { 089 referenceSnapshot = DatabaseSnapshotGeneratorFactory.getInstance() 090 .createSnapshot(referenceDatabase, referenceDatabase.getDefaultSchemaName(), statusListeners); 091 } 092 093 if (targetSnapshot == null) { 094 if (targetDatabase == null) { 095 targetSnapshot = new DatabaseSnapshot(referenceDatabase, null); 096 } else { 097 targetSnapshot = DatabaseSnapshotGeneratorFactory.getInstance() 098 .createSnapshot(targetDatabase, referenceDatabase.getDefaultSchemaName(), statusListeners); 099 } 100 } 101 102 DiffResult diffResult = new DiffResult(referenceSnapshot, 103 targetSnapshot); 104 checkVersionInfo(diffResult); 105 if (shouldDiffTables()) { 106 checkTables(diffResult); 107 } 108 if (shouldDiffViews()) { 109 checkViews(diffResult); 110 } 111 if (shouldDiffColumns()) { 112 checkColumns(diffResult); 113 } 114 if (shouldDiffForeignKeys()) { 115 checkForeignKeys(diffResult); 116 } 117 if (shouldDiffPrimaryKeys()) { 118 checkPrimaryKeys(diffResult); 119 } 120 if (shouldDiffUniqueConstraints()) { 121 checkUniqueConstraints(diffResult); 122 } 123 if (shouldDiffIndexes()) { 124 checkIndexes(diffResult); 125 } 126 if (shouldDiffSequences()) { 127 checkSequences(diffResult); 128 } 129 diffResult.setDiffData(shouldDiffData()); 130 131 // Hack: Sometimes Indexes or Unique Constraints with multiple columns get added twice (1 for each column), 132 // so we're combining them back to a single Index or Unique Constraint here. 133 removeDuplicateIndexes( diffResult.getMissingIndexes() ); 134 removeDuplicateIndexes( diffResult.getUnexpectedIndexes() ); 135 removeDuplicateUniqueConstraints( diffResult.getMissingUniqueConstraints() ); 136 removeDuplicateUniqueConstraints( diffResult.getUnexpectedUniqueConstraints() ); 137 138 return diffResult; 139 } 140 141 public void setDiffTypes(String diffTypes) { 142 if (StringUtils.trimToNull(diffTypes) != null) { 143 Set<String> types = new HashSet<String>(Arrays.asList(diffTypes.toLowerCase().split("\\s*,\\s*"))); 144 145 diffTables = types.contains("tables"); 146 diffColumns = types.contains("columns"); 147 diffViews = types.contains("views"); 148 diffPrimaryKeys = types.contains("primaryKeys".toLowerCase()); 149 diffUniqueConstraints = types.contains("uniqueConstraints".toLowerCase()); 150 diffIndexes = types.contains("indexes"); 151 diffForeignKeys = types.contains("foreignKeys".toLowerCase()); 152 diffSequences = types.contains("sequences"); 153 diffData = types.contains("data"); 154 } 155 } 156 157 public boolean shouldDiffTables() { 158 return diffTables; 159 } 160 161 public void setDiffTables(boolean diffTables) { 162 this.diffTables = diffTables; 163 } 164 165 public boolean shouldDiffColumns() { 166 return diffColumns; 167 } 168 169 public void setDiffColumns(boolean diffColumns) { 170 this.diffColumns = diffColumns; 171 } 172 173 public boolean shouldDiffViews() { 174 return diffViews; 175 } 176 177 public void setDiffViews(boolean diffViews) { 178 this.diffViews = diffViews; 179 } 180 181 public boolean shouldDiffPrimaryKeys() { 182 return diffPrimaryKeys; 183 } 184 185 public void setDiffPrimaryKeys(boolean diffPrimaryKeys) { 186 this.diffPrimaryKeys = diffPrimaryKeys; 187 } 188 189 public boolean shouldDiffIndexes() { 190 return diffIndexes; 191 } 192 193 public void setDiffIndexes(boolean diffIndexes) { 194 this.diffIndexes = diffIndexes; 195 } 196 197 public boolean shouldDiffForeignKeys() { 198 return diffForeignKeys; 199 } 200 201 public void setDiffForeignKeys(boolean diffForeignKeys) { 202 this.diffForeignKeys = diffForeignKeys; 203 } 204 205 public boolean shouldDiffSequences() { 206 return diffSequences; 207 } 208 209 public void setDiffSequences(boolean diffSequences) { 210 this.diffSequences = diffSequences; 211 } 212 213 public boolean shouldDiffData() { 214 return diffData; 215 } 216 217 public void setDiffData(boolean diffData) { 218 this.diffData = diffData; 219 } 220 221 public boolean shouldDiffUniqueConstraints() { 222 return this.diffUniqueConstraints; 223 } 224 225 public void setDiffUniqueConstraints(boolean diffUniqueConstraints) { 226 this.diffUniqueConstraints = diffUniqueConstraints; 227 } 228 229 private void checkVersionInfo(DiffResult diffResult) 230 throws DatabaseException { 231 232 if (targetDatabase != null) { 233 diffResult.setProductName(new DiffComparison(referenceDatabase 234 .getDatabaseProductName(), targetDatabase 235 .getDatabaseProductName())); 236 diffResult.setProductVersion(new DiffComparison(referenceDatabase 237 .getDatabaseProductVersion(), targetDatabase 238 .getDatabaseProductVersion())); 239 } 240 241 } 242 243 private void checkTables(DiffResult diffResult) { 244 for (Table baseTable : referenceSnapshot.getTables()) { 245 if (!targetSnapshot.getTables().contains(baseTable)) { 246 diffResult.addMissingTable(baseTable); 247 } 248 } 249 250 for (Table targetTable : targetSnapshot.getTables()) { 251 if (!referenceSnapshot.getTables().contains(targetTable)) { 252 diffResult.addUnexpectedTable(targetTable); 253 } 254 } 255 } 256 257 private void checkViews(DiffResult diffResult) { 258 for (View baseView : referenceSnapshot.getViews()) { 259 if (!targetSnapshot.getViews().contains(baseView)) { 260 diffResult.addMissingView(baseView); 261 } 262 } 263 264 for (View targetView : targetSnapshot.getViews()) { 265 if (!referenceSnapshot.getViews().contains(targetView)) { 266 diffResult.addUnexpectedView(targetView); 267 } else { 268 for (View referenceView : referenceSnapshot.getViews()) { 269 if (referenceView.getName().equals(targetView.getName())) { 270 if (!referenceView.getDefinition().equals(targetView.getDefinition())) { 271 diffResult.addChangedView(referenceView); 272 } 273 } 274 } 275 } 276 } 277 } 278 279 private void checkColumns(DiffResult diffResult) { 280 for (Column baseColumn : referenceSnapshot.getColumns()) { 281 if (!targetSnapshot.getColumns().contains(baseColumn) 282 && (baseColumn.getTable() == null || !diffResult 283 .getMissingTables().contains(baseColumn.getTable())) 284 && (baseColumn.getView() == null || !diffResult 285 .getMissingViews().contains(baseColumn.getView()))) { 286 diffResult.addMissingColumn(baseColumn); 287 } 288 } 289 290 for (Column targetColumn : targetSnapshot.getColumns()) { 291 if (!referenceSnapshot.getColumns().contains(targetColumn) 292 && (targetColumn.getTable() == null || !diffResult 293 .getUnexpectedTables().contains( 294 targetColumn.getTable())) 295 && (targetColumn.getView() == null || !diffResult 296 .getUnexpectedViews().contains( 297 targetColumn.getView()))) { 298 diffResult.addUnexpectedColumn(targetColumn); 299 } else if (targetColumn.getTable() != null 300 && !diffResult.getUnexpectedTables().contains( 301 targetColumn.getTable())) { 302 Column baseColumn = referenceSnapshot.getColumn(targetColumn 303 .getTable().getName(), targetColumn.getName()); 304 305 if (baseColumn == null || targetColumn.isDifferent(baseColumn)) { 306 diffResult.addChangedColumn(targetColumn); 307 } 308 } 309 } 310 } 311 312 private void checkForeignKeys(DiffResult diffResult) { 313 for (ForeignKey baseFK : referenceSnapshot.getForeignKeys()) { 314 if (!targetSnapshot.getForeignKeys().contains(baseFK)) { 315 diffResult.addMissingForeignKey(baseFK); 316 } 317 } 318 319 for (ForeignKey targetFK : targetSnapshot.getForeignKeys()) { 320 if (!referenceSnapshot.getForeignKeys().contains(targetFK)) { 321 diffResult.addUnexpectedForeignKey(targetFK); 322 } 323 } 324 } 325 326 private void checkUniqueConstraints(DiffResult diffResult) { 327 for (UniqueConstraint baseIndex : referenceSnapshot 328 .getUniqueConstraints()) { 329 if (!targetSnapshot.getUniqueConstraints().contains(baseIndex)) { 330 diffResult.addMissingUniqueConstraint(baseIndex); 331 } 332 } 333 334 for (UniqueConstraint targetIndex : targetSnapshot 335 .getUniqueConstraints()) { 336 if (!referenceSnapshot.getUniqueConstraints().contains(targetIndex)) { 337 diffResult.addUnexpectedUniqueConstraint(targetIndex); 338 } 339 } 340 } 341 342 private void checkIndexes(DiffResult diffResult) { 343 for (Index baseIndex : referenceSnapshot.getIndexes()) { 344 if (!targetSnapshot.getIndexes().contains(baseIndex)) { 345 diffResult.addMissingIndex(baseIndex); 346 } 347 } 348 349 for (Index targetIndex : targetSnapshot.getIndexes()) { 350 if (!referenceSnapshot.getIndexes().contains(targetIndex)) { 351 diffResult.addUnexpectedIndex(targetIndex); 352 } 353 } 354 } 355 356 private void checkPrimaryKeys(DiffResult diffResult) { 357 for (PrimaryKey basePrimaryKey : referenceSnapshot.getPrimaryKeys()) { 358 if (!targetSnapshot.getPrimaryKeys().contains(basePrimaryKey)) { 359 diffResult.addMissingPrimaryKey(basePrimaryKey); 360 } 361 } 362 363 for (PrimaryKey targetPrimaryKey : targetSnapshot.getPrimaryKeys()) { 364 if (!referenceSnapshot.getPrimaryKeys().contains(targetPrimaryKey)) { 365 diffResult.addUnexpectedPrimaryKey(targetPrimaryKey); 366 } 367 } 368 } 369 370 private void checkSequences(DiffResult diffResult) { 371 for (Sequence baseSequence : referenceSnapshot.getSequences()) { 372 if (!targetSnapshot.getSequences().contains(baseSequence)) { 373 diffResult.addMissingSequence(baseSequence); 374 } 375 } 376 377 for (Sequence targetSequence : targetSnapshot.getSequences()) { 378 if (!referenceSnapshot.getSequences().contains(targetSequence)) { 379 diffResult.addUnexpectedSequence(targetSequence); 380 } 381 } 382 } 383 384 /** 385 * Removes duplicate Indexes from the DiffResult object. 386 * 387 * @param indexes [IN/OUT] - A set of Indexes to be updated. 388 */ 389 private void removeDuplicateIndexes( SortedSet<Index> indexes ) 390 { 391 SortedSet<Index> combinedIndexes = new TreeSet<Index>(); 392 SortedSet<Index> indexesToRemove = new TreeSet<Index>(); 393 394 // Find Indexes with the same name, copy their columns into the first one, 395 // then remove the duplicate Indexes. 396 for ( Index idx1 : indexes ) 397 { 398 if ( !combinedIndexes.contains( idx1 ) ) 399 { 400 for ( Index idx2 : indexes.tailSet( idx1 ) ) 401 { 402 if ( idx1 == idx2 ) { 403 continue; 404 } 405 406 String index1Name = StringUtils.trimToEmpty(idx1.getName()); 407 String index2Name = StringUtils.trimToEmpty(idx2.getName()); 408 if ( index1Name.equalsIgnoreCase(index2Name) 409 && idx1.getTable().getName().equalsIgnoreCase( idx2.getTable().getName() ) ) 410 { 411 for ( String column : idx2.getColumns() ) 412 { 413 if ( !idx1.getColumns().contains( column ) ) { 414 idx1.getColumns().add( column ); 415 } 416 } 417 418 indexesToRemove.add( idx2 ); 419 } 420 } 421 422 combinedIndexes.add( idx1 ); 423 } 424 } 425 426 indexes.removeAll( indexesToRemove ); 427 } 428 429 /** 430 * Removes duplicate Unique Constraints from the DiffResult object. 431 * 432 * @param uniqueConstraints [IN/OUT] - A set of Unique Constraints to be updated. 433 */ 434 private void removeDuplicateUniqueConstraints( SortedSet<UniqueConstraint> uniqueConstraints ) { 435 SortedSet<UniqueConstraint> combinedConstraints = new TreeSet<UniqueConstraint>(); 436 SortedSet<UniqueConstraint> constraintsToRemove = new TreeSet<UniqueConstraint>(); 437 438 // Find UniqueConstraints with the same name, copy their columns into the first one, 439 // then remove the duplicate UniqueConstraints. 440 for ( UniqueConstraint uc1 : uniqueConstraints ) 441 { 442 if ( !combinedConstraints.contains( uc1 ) ) 443 { 444 for ( UniqueConstraint uc2 : uniqueConstraints.tailSet( uc1 ) ) 445 { 446 if ( uc1 == uc2 ) { 447 continue; 448 } 449 450 if ( uc1.getName().equalsIgnoreCase( uc2.getName() ) 451 && uc1.getTable().getName().equalsIgnoreCase( uc2.getTable().getName() ) ) 452 { 453 for ( String column : uc2.getColumns() ) 454 { 455 if ( !uc1.getColumns().contains( column ) ) { 456 uc1.getColumns().add( column ); 457 } 458 } 459 460 constraintsToRemove.add( uc2 ); 461 } 462 } 463 464 combinedConstraints.add( uc1 ); 465 } 466 } 467 468 uniqueConstraints.removeAll( constraintsToRemove ); 469 } 470 471}