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}