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.change.Change;
029import liquibase.change.ColumnConfig;
030import liquibase.change.ConstraintsConfig;
031import liquibase.change.core.*;
032import liquibase.changelog.ChangeSet;
033import liquibase.database.Database;
034import liquibase.database.jvm.JdbcConnection;
035import liquibase.database.structure.*;
036import liquibase.database.typeconversion.TypeConverter;
037import liquibase.database.typeconversion.TypeConverterFactory;
038import liquibase.diff.*;
039import liquibase.exception.DatabaseException;
040import liquibase.executor.ExecutorService;
041import liquibase.logging.LogFactory;
042import liquibase.parser.core.xml.LiquibaseEntityResolver;
043import liquibase.parser.core.xml.XMLChangeLogSAXParser;
044import liquibase.serializer.ChangeLogSerializer;
045import liquibase.serializer.ChangeLogSerializerFactory;
046import liquibase.serializer.core.xml.XMLChangeLogSerializer;
047import liquibase.snapshot.DatabaseSnapshot;
048import liquibase.statement.DatabaseFunction;
049import liquibase.statement.core.RawSqlStatement;
050import liquibase.statement.ext.DescribeSequenceStatement;
051import liquibase.util.ISODateFormat;
052import liquibase.util.StringUtils;
053import liquibase.util.csv.CSVWriter;
054import liquibase.util.xml.DefaultXmlWriter;
055import liquibase.util.xml.XmlWriter;
056import org.w3c.dom.Document;
057import org.w3c.dom.Element;
058
059import javax.xml.parsers.DocumentBuilder;
060import javax.xml.parsers.DocumentBuilderFactory;
061import javax.xml.parsers.ParserConfigurationException;
062import java.io.*;
063import java.math.BigInteger;
064import java.sql.ResultSet;
065import java.sql.Statement;
066import java.util.*;
067
068public class DiffResult {
069
070        private String idRoot = String.valueOf(new Date().getTime());
071        private int changeNumber = 1;
072
073        private DatabaseSnapshot referenceSnapshot;
074        private DatabaseSnapshot targetSnapshot;
075
076        private DiffComparison productName;
077        private DiffComparison productVersion;
078
079        private SortedSet<Table> missingTables = new TreeSet<Table>();
080        private SortedSet<Table> unexpectedTables = new TreeSet<Table>();
081
082        private SortedSet<View> missingViews = new TreeSet<View>();
083        private SortedSet<View> unexpectedViews = new TreeSet<View>();
084        private SortedSet<View> changedViews = new TreeSet<View>();
085
086        private SortedSet<Column> missingColumns = new TreeSet<Column>();
087        private SortedSet<Column> unexpectedColumns = new TreeSet<Column>();
088        private SortedSet<Column> changedColumns = new TreeSet<Column>();
089
090        private SortedSet<ForeignKey> missingForeignKeys = new TreeSet<ForeignKey>();
091        private SortedSet<ForeignKey> unexpectedForeignKeys = new TreeSet<ForeignKey>();
092
093        private SortedSet<Index> missingIndexes = new TreeSet<Index>();
094        private SortedSet<Index> unexpectedIndexes = new TreeSet<Index>();
095
096        private SortedSet<PrimaryKey> missingPrimaryKeys = new TreeSet<PrimaryKey>();
097        private SortedSet<PrimaryKey> unexpectedPrimaryKeys = new TreeSet<PrimaryKey>();
098
099        private SortedSet<UniqueConstraint> missingUniqueConstraints = new TreeSet<UniqueConstraint>();
100        private SortedSet<UniqueConstraint> unexpectedUniqueConstraints = new TreeSet<UniqueConstraint>();
101
102        private SortedSet<Sequence> missingSequences = new TreeSet<Sequence>();
103        private SortedSet<Sequence> unexpectedSequences = new TreeSet<Sequence>();
104
105        private boolean diffData = false;
106        private String dataDir = null;
107        private String changeSetContext;
108        private String changeSetAuthor;
109
110        private ChangeLogSerializerFactory serializerFactory = ChangeLogSerializerFactory.getInstance();
111
112        public DiffResult(DatabaseSnapshot referenceDatabaseSnapshot,
113                        DatabaseSnapshot targetDatabaseSnapshot) {
114                this.referenceSnapshot = referenceDatabaseSnapshot;
115
116                if (targetDatabaseSnapshot == null) {
117                        targetDatabaseSnapshot = new DatabaseSnapshot(
118                                        referenceDatabaseSnapshot.getDatabase(), null);
119                }
120                this.targetSnapshot = targetDatabaseSnapshot;
121        }
122
123        public DiffComparison getProductName() {
124                return productName;
125        }
126
127        public void setProductName(DiffComparison productName) {
128                this.productName = productName;
129        }
130
131        public DiffComparison getProductVersion() {
132                return productVersion;
133        }
134
135        public void setProductVersion(DiffComparison product) {
136                this.productVersion = product;
137        }
138
139        public void addMissingTable(Table table) {
140                missingTables.add(table);
141        }
142
143        public SortedSet<Table> getMissingTables() {
144                return missingTables;
145        }
146
147        public void addUnexpectedTable(Table table) {
148                unexpectedTables.add(table);
149        }
150
151        public SortedSet<Table> getUnexpectedTables() {
152                return unexpectedTables;
153        }
154
155        public void addMissingView(View viewName) {
156                missingViews.add(viewName);
157        }
158
159        public SortedSet<View> getMissingViews() {
160                return missingViews;
161        }
162
163        public void addUnexpectedView(View viewName) {
164                unexpectedViews.add(viewName);
165        }
166
167        public SortedSet<View> getUnexpectedViews() {
168                return unexpectedViews;
169        }
170
171        public void addChangedView(View viewName) {
172                changedViews.add(viewName);
173        }
174
175        public SortedSet<View> getChangedViews() {
176                return changedViews;
177        }
178
179        public void addMissingColumn(Column columnName) {
180                missingColumns.add(columnName);
181        }
182
183        public SortedSet<Column> getMissingColumns() {
184                return missingColumns;
185        }
186
187        public void addUnexpectedColumn(Column columnName) {
188                unexpectedColumns.add(columnName);
189        }
190
191        public SortedSet<Column> getUnexpectedColumns() {
192                return unexpectedColumns;
193        }
194
195        public void addChangedColumn(Column columnName) {
196                changedColumns.add(columnName);
197        }
198
199        public SortedSet<Column> getChangedColumns() {
200                return changedColumns;
201        }
202
203        public void addMissingForeignKey(ForeignKey fkName) {
204                missingForeignKeys.add(fkName);
205        }
206
207        public SortedSet<ForeignKey> getMissingForeignKeys() {
208                return missingForeignKeys;
209        }
210
211        public void addUnexpectedForeignKey(ForeignKey fkName) {
212                unexpectedForeignKeys.add(fkName);
213        }
214
215        public SortedSet<ForeignKey> getUnexpectedForeignKeys() {
216                return unexpectedForeignKeys;
217        }
218
219        public void addMissingIndex(Index fkName) {
220                missingIndexes.add(fkName);
221        }
222
223        public SortedSet<Index> getMissingIndexes() {
224                return missingIndexes;
225        }
226
227        public void addUnexpectedIndex(Index fkName) {
228                unexpectedIndexes.add(fkName);
229        }
230
231        public SortedSet<Index> getUnexpectedIndexes() {
232                return unexpectedIndexes;
233        }
234
235        public void addMissingPrimaryKey(PrimaryKey primaryKey) {
236                missingPrimaryKeys.add(primaryKey);
237        }
238
239        public SortedSet<PrimaryKey> getMissingPrimaryKeys() {
240                return missingPrimaryKeys;
241        }
242
243        public void addUnexpectedPrimaryKey(PrimaryKey primaryKey) {
244                unexpectedPrimaryKeys.add(primaryKey);
245        }
246
247        public SortedSet<PrimaryKey> getUnexpectedPrimaryKeys() {
248                return unexpectedPrimaryKeys;
249        }
250
251        public void addMissingSequence(Sequence sequence) {
252                missingSequences.add(sequence);
253        }
254
255        public SortedSet<Sequence> getMissingSequences() {
256                return missingSequences;
257        }
258
259        public void addUnexpectedSequence(Sequence sequence) {
260                unexpectedSequences.add(sequence);
261        }
262
263        public SortedSet<Sequence> getUnexpectedSequences() {
264                return unexpectedSequences;
265        }
266
267        public void addMissingUniqueConstraint(UniqueConstraint uniqueConstraint) {
268                missingUniqueConstraints.add(uniqueConstraint);
269        }
270
271        public SortedSet<UniqueConstraint> getMissingUniqueConstraints() {
272                return this.missingUniqueConstraints;
273        }
274
275        public void addUnexpectedUniqueConstraint(UniqueConstraint uniqueConstraint) {
276                unexpectedUniqueConstraints.add(uniqueConstraint);
277        }
278
279        public SortedSet<UniqueConstraint> getUnexpectedUniqueConstraints() {
280                return unexpectedUniqueConstraints;
281        }
282
283        public boolean shouldDiffData() {
284                return diffData;
285        }
286
287        public void setDiffData(boolean diffData) {
288                this.diffData = diffData;
289        }
290
291        public String getDataDir() {
292                return dataDir;
293        }
294
295        public void setDataDir(String dataDir) {
296                this.dataDir = dataDir;
297        }
298
299        public String getChangeSetContext() {
300                return changeSetContext;
301        }
302
303        public void setChangeSetContext(String changeSetContext) {
304                this.changeSetContext = changeSetContext;
305        }
306
307        public boolean differencesFound() throws DatabaseException,IOException{
308            boolean differencesInData=false;
309            if(shouldDiffData()) {
310                List<ChangeSet> changeSets = new ArrayList<ChangeSet>();
311                addInsertDataChanges(changeSets, dataDir);
312                differencesInData=!changeSets.isEmpty();
313            }
314
315            return getMissingColumns().size()>0 ||
316                    getMissingForeignKeys().size()>0 ||
317                    getMissingIndexes().size()>0 ||
318                    getMissingPrimaryKeys().size()>0 ||
319                    getMissingSequences().size()>0 ||
320                    getMissingTables().size()>0 ||
321                    getMissingUniqueConstraints().size()>0 ||
322                    getMissingViews().size()>0 ||
323                    getUnexpectedColumns().size()>0 ||
324                    getUnexpectedForeignKeys().size()>0 ||
325                    getUnexpectedIndexes().size()>0 ||
326                    getUnexpectedPrimaryKeys().size()>0 ||
327                    getUnexpectedSequences().size()>0 ||
328                    getUnexpectedTables().size()>0 ||
329                    getUnexpectedUniqueConstraints().size()>0 ||
330                    getUnexpectedViews().size()>0 ||
331                    differencesInData;
332        }
333
334
335        public void printResult(PrintStream out) throws DatabaseException {
336                out.println("Reference Database: " + referenceSnapshot.getDatabase());
337                out.println("Target Database: " + targetSnapshot.getDatabase());
338
339                printComparision("Product Name", productName, out);
340                printComparision("Product Version", productVersion, out);
341                printSetComparison("Missing Tables", getMissingTables(), out);
342                printSetComparison("Unexpected Tables", getUnexpectedTables(), out);
343                printSetComparison("Missing Views", getMissingViews(), out);
344                printSetComparison("Unexpected Views", getUnexpectedViews(), out);
345                printSetComparison("Changed Views", getChangedViews(), out);
346                printSetComparison("Missing Columns", getMissingColumns(), out);
347                printSetComparison("Unexpected Columns", getUnexpectedColumns(), out);
348                printColumnComparison(getChangedColumns(), out);
349                printSetComparison("Missing Foreign Keys", getMissingForeignKeys(), out);
350                printSetComparison("Unexpected Foreign Keys",
351                                getUnexpectedForeignKeys(), out);
352                printSetComparison("Missing Primary Keys", getMissingPrimaryKeys(), out);
353                printSetComparison("Unexpected Primary Keys",
354                                getUnexpectedPrimaryKeys(), out);
355        printSetComparison("Unexpected Unique Constraints",
356                getUnexpectedUniqueConstraints(), out);
357                printSetComparison("Missing Unique Constraints",
358                                getMissingUniqueConstraints(), out);
359                printSetComparison("Missing Indexes", getMissingIndexes(), out);
360                printSetComparison("Unexpected Indexes", getUnexpectedIndexes(), out);
361                printSetComparison("Missing Sequences", getMissingSequences(), out);
362                printSetComparison("Unexpected Sequences", getUnexpectedSequences(),
363                                out);
364        }
365
366        private void printSetComparison(String title, SortedSet<?> objects,
367                        PrintStream out) {
368                out.print(title + ": ");
369                if (objects.size() == 0) {
370                        out.println("NONE");
371                } else {
372                        out.println();
373                        for (Object object : objects) {
374                                out.println("     " + object);
375                        }
376                }
377        }
378
379        private void printColumnComparison(SortedSet<Column> changedColumns,
380                        PrintStream out) {
381                out.print("Changed Columns: ");
382                if (changedColumns.size() == 0) {
383                        out.println("NONE");
384                } else {
385                        out.println();
386                        for (Column column : changedColumns) {
387                                out.println("     " + column);
388                                Column baseColumn = referenceSnapshot.getColumn(column
389                                                .getTable().getName(), column.getName());
390                                if (baseColumn != null) {
391                                        if (baseColumn.isDataTypeDifferent(column)) {
392                                                out.println("           from "
393                                                                + TypeConverterFactory.getInstance().findTypeConverter(referenceSnapshot.getDatabase()).convertToDatabaseTypeString(baseColumn, referenceSnapshot.getDatabase())
394                                                                + " to "
395                                                                + TypeConverterFactory.getInstance().findTypeConverter(targetSnapshot.getDatabase()).convertToDatabaseTypeString(targetSnapshot.getColumn(column.getTable().getName(), column.getName()), targetSnapshot.getDatabase()));
396                                        }
397                                        if (baseColumn.isNullabilityDifferent(column)) {
398                                                Boolean nowNullable = targetSnapshot.getColumn(
399                                                                column.getTable().getName(), column.getName())
400                                                                .isNullable();
401                                                if (nowNullable == null) {
402                                                        nowNullable = Boolean.TRUE;
403                                                }
404                                                if (nowNullable) {
405                                                        out.println("           now nullable");
406                                                } else {
407                                                        out.println("           now not null");
408                                                }
409                                        }
410                                }
411                        }
412                }
413        }
414
415        private void printComparision(String title, DiffComparison comparison, PrintStream out) {
416                out.print(title + ":");
417
418        if (comparison == null) {
419            out.print("NULL");
420            return;
421        }
422
423                if (comparison.areTheSame()) {
424                        out.println(" EQUAL");
425                } else {
426                        out.println();
427                        out.println("     Reference:   '"
428                                        + comparison.getReferenceVersion() + "'");
429                        out.println("     Target: '" + comparison.getTargetVersion() + "'");
430                }
431
432        }
433
434        public void printChangeLog(String changeLogFile, Database targetDatabase)
435                        throws ParserConfigurationException, IOException, DatabaseException {
436                ChangeLogSerializer changeLogSerializer = serializerFactory.getSerializer(changeLogFile);
437                this.printChangeLog(changeLogFile, targetDatabase, changeLogSerializer);
438        }
439
440        public void printChangeLog(PrintStream out, Database targetDatabase)
441                        throws ParserConfigurationException, IOException, DatabaseException {
442                this.printChangeLog(out, targetDatabase, new XMLChangeLogSerializer());
443        }
444
445        public void printChangeLog(String changeLogFile, Database targetDatabase,
446                        ChangeLogSerializer changeLogSerializer) throws ParserConfigurationException,
447                        IOException, DatabaseException {
448                File file = new File(changeLogFile);
449                if (!file.exists()) {
450                        LogFactory.getLogger().info(file + " does not exist, creating");
451                        FileOutputStream stream = new FileOutputStream(file);
452                        printChangeLog(new PrintStream(stream), targetDatabase, changeLogSerializer);
453                        stream.close();
454                } else {
455                        LogFactory.getLogger().info(file + " exists, appending");
456                        ByteArrayOutputStream out = new ByteArrayOutputStream();
457                        printChangeLog(new PrintStream(out), targetDatabase, changeLogSerializer);
458
459                        String xml = new String(out.toByteArray());
460                        xml = xml.replaceFirst("(?ms).*<databaseChangeLog[^>]*>", "");
461                        xml = xml.replaceFirst("</databaseChangeLog>", "");
462                        xml = xml.trim();
463                        if ("".equals( xml )) {
464                            LogFactory.getLogger().info("No changes found, nothing to do");
465                            return;
466                        }
467
468                        String lineSeparator = System.getProperty("line.separator");
469                        BufferedReader fileReader = new BufferedReader(new FileReader(file));
470                        String line;
471                        long offset = 0;
472                        while ((line = fileReader.readLine()) != null) {
473                                int index = line.indexOf("</databaseChangeLog>");
474                                if (index >= 0) {
475                                        offset += index;
476                                } else {
477                                        offset += line.getBytes().length;
478                                        offset += lineSeparator.getBytes().length;
479                                }
480                        }
481                        fileReader.close();
482
483                        fileReader = new BufferedReader(new FileReader(file));
484                        fileReader.skip(offset);
485
486                        fileReader.close();
487
488                        // System.out.println("resulting XML: " + xml.trim());
489
490                        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
491                        randomAccessFile.seek(offset);
492                        randomAccessFile.writeBytes("    "); 
493                        randomAccessFile.write( xml.getBytes() );
494                        randomAccessFile.writeBytes(lineSeparator);
495                        randomAccessFile.writeBytes("</databaseChangeLog>" + lineSeparator);
496                        randomAccessFile.close();
497
498                        // BufferedWriter fileWriter = new BufferedWriter(new
499                        // FileWriter(file));
500                        // fileWriter.append(xml);
501                        // fileWriter.close();
502                }
503        }
504
505        /**
506         * Prints changeLog that would bring the target database to be the same as
507         * the reference database
508         */
509        public void printChangeLog(PrintStream out, Database targetDatabase,
510                        ChangeLogSerializer changeLogSerializer)
511                        throws ParserConfigurationException,
512                        IOException, DatabaseException {
513                List<ChangeSet> changeSets = new ArrayList<ChangeSet>();
514                addMissingTableChanges(changeSets, targetDatabase);
515                addMissingColumnChanges(changeSets, targetDatabase);
516                addChangedColumnChanges(changeSets);
517                addMissingPrimaryKeyChanges(changeSets);
518                addUnexpectedPrimaryKeyChanges(changeSets);
519        addUnexpectedForeignKeyChanges(changeSets);
520                addMissingUniqueConstraintChanges(changeSets);
521        addMissingIndexChanges(changeSets);
522                addUnexpectedUniqueConstraintChanges(changeSets);
523
524                if (diffData) {
525                        addInsertDataChanges(changeSets, dataDir);
526                }
527
528                addMissingForeignKeyChanges(changeSets);
529                addUnexpectedIndexChanges(changeSets);
530                addUnexpectedColumnChanges(changeSets);
531                addMissingSequenceChanges(changeSets);
532                addUnexpectedSequenceChanges(changeSets);
533                addMissingViewChanges(changeSets);
534                addUnexpectedViewChanges(changeSets);
535                addChangedViewChanges(changeSets);
536                addUnexpectedTableChanges(changeSets);
537
538                changeLogSerializer.write(changeSets, out);
539
540                out.flush();
541        }
542
543        private ChangeSet generateChangeSet(Change change) {
544                ChangeSet changeSet = generateChangeSet();
545                changeSet.addChange(change);
546
547                return changeSet;
548        }
549
550        private ChangeSet generateChangeSet() {
551                return new ChangeSet(generateId(), getChangeSetAuthor(), false, false,
552                                null, getChangeSetContext(), null);
553        }
554
555        private String getChangeSetAuthor() {
556                if (changeSetAuthor != null) {
557                        return changeSetAuthor;
558                }
559                String author = System.getProperty("user.name");
560                if (StringUtils.trimToNull(author) == null) {
561                        return "diff-generated";
562                } else {
563                        return author + " (generated)";
564                }
565        }
566
567        public void setChangeSetAuthor(String changeSetAuthor) {
568                this.changeSetAuthor = changeSetAuthor;
569        }
570
571    public void setIdRoot(String idRoot) {
572        this.idRoot = idRoot;
573    }
574
575    protected String generateId() {
576                return idRoot + "-" + changeNumber++;
577        }
578
579        private void addUnexpectedIndexChanges(List<ChangeSet> changes) {
580                for (Index index : getUnexpectedIndexes()) {
581
582            if (index.getAssociatedWith().contains(Index.MARK_PRIMARY_KEY) || index.getAssociatedWith().contains(Index.MARK_FOREIGN_KEY) || index.getAssociatedWith().contains(Index.MARK_UNIQUE_CONSTRAINT)) {
583                continue;
584            }
585
586            DropIndexChange change = new DropIndexChange();
587                        change.setTableName(index.getTable().getName());
588                        change.setSchemaName(index.getTable().getSchema());
589                        change.setIndexName(index.getName());
590            change.setAssociatedWith(index.getAssociatedWithAsString());
591
592                        changes.add(generateChangeSet(change));
593                }
594        }
595
596        private void addMissingIndexChanges(List<ChangeSet> changes) {
597                for (Index index : getMissingIndexes()) {
598
599                        CreateIndexChange change = new CreateIndexChange();
600                        change.setTableName(index.getTable().getName());
601                        change.setTablespace(index.getTablespace());
602                        change.setSchemaName(index.getTable().getSchema());
603                        change.setIndexName(index.getName());
604                        change.setUnique(index.isUnique());
605                        change.setAssociatedWith(index.getAssociatedWithAsString());
606
607            if (index.getAssociatedWith().contains(Index.MARK_PRIMARY_KEY) || index.getAssociatedWith().contains(Index.MARK_FOREIGN_KEY) || index.getAssociatedWith().contains(Index.MARK_UNIQUE_CONSTRAINT)) {
608                continue;
609            }
610
611                        for (String columnName : index.getColumns()) {
612                                ColumnConfig column = new ColumnConfig();
613                                column.setName(columnName);
614                                change.addColumn(column);
615                        }
616                        changes.add(generateChangeSet(change));
617                }
618        }
619
620        private void addUnexpectedPrimaryKeyChanges(List<ChangeSet> changes) {
621                for (PrimaryKey pk : getUnexpectedPrimaryKeys()) {
622
623                        if (!getUnexpectedTables().contains(pk.getTable())) {
624                                DropPrimaryKeyChange change = new DropPrimaryKeyChange();
625                                change.setTableName(pk.getTable().getName());
626                                change.setSchemaName(pk.getTable().getSchema());
627                                change.setConstraintName(pk.getName());
628
629                                changes.add(generateChangeSet(change));
630                        }
631                }
632        }
633
634        private void addMissingPrimaryKeyChanges(List<ChangeSet> changes) {
635                for (PrimaryKey pk : getMissingPrimaryKeys()) {
636
637                        AddPrimaryKeyChange change = new AddPrimaryKeyChange();
638                        change.setTableName(pk.getTable().getName());
639                        change.setSchemaName(pk.getTable().getSchema());
640                        change.setConstraintName(pk.getName());
641                        change.setColumnNames(pk.getColumnNames());
642                        change.setTablespace(pk.getTablespace());
643
644                        changes.add(generateChangeSet(change));
645                }
646        }
647
648        private void addUnexpectedUniqueConstraintChanges(List<ChangeSet> changes) {
649                for (UniqueConstraint uc : getUnexpectedUniqueConstraints()) {
650                        // Need check for nulls here due to NullPointerException using Postgres
651                        if (null != uc) {
652                                if (null != uc.getTable()) {
653                    DropUniqueConstraintChange change = new DropUniqueConstraintChange();
654                    change.setTableName(uc.getTable().getName());
655                    change.setSchemaName(uc.getTable().getSchema());
656                    change.setConstraintName(uc.getName());
657
658                    changes.add(generateChangeSet(change));
659                                }
660                        }
661                }
662        }
663
664        private void addMissingUniqueConstraintChanges(List<ChangeSet> changes) {
665                for (UniqueConstraint uc : getMissingUniqueConstraints()) {
666                        // Need check for nulls here due to NullPointerException using Postgres
667                        if (null != uc)
668                                if (null != uc.getTable()) {
669                                        AddUniqueConstraintChange change = new AddUniqueConstraintChange();
670                                        change.setTableName(uc.getTable().getName());
671                                        change.setTablespace(uc.getTablespace());
672                                        change.setSchemaName(uc.getTable().getSchema());
673                                        change.setConstraintName(uc.getName());
674                                        change.setColumnNames(uc.getColumnNames());
675                                        change.setDeferrable(uc.isDeferrable());
676                                        change.setInitiallyDeferred(uc.isInitiallyDeferred());
677                                        change.setDisabled(uc.isDisabled());
678                                        changes.add(generateChangeSet(change));
679                                }
680                }
681        }
682
683        private void addUnexpectedForeignKeyChanges(List<ChangeSet> changes) {
684                for (ForeignKey fk : getUnexpectedForeignKeys()) {
685
686                        DropForeignKeyConstraintChange change = new DropForeignKeyConstraintChange();
687                        change.setConstraintName(fk.getName());
688                        change.setBaseTableName(fk.getForeignKeyTable().getName());
689                        change.setBaseTableSchemaName(fk.getForeignKeyTable().getSchema());
690
691                        changes.add(generateChangeSet(change));
692                }
693        }
694
695        private void addMissingForeignKeyChanges(List<ChangeSet> changes) {
696                for (ForeignKey fk : getMissingForeignKeys()) {
697
698                        AddForeignKeyConstraintChange change = new AddForeignKeyConstraintChange();
699                        change.setConstraintName(fk.getName());
700
701                        change.setReferencedTableName(fk.getPrimaryKeyTable().getName());
702                        change.setReferencedTableSchemaName(fk.getPrimaryKeyTable()
703                                        .getSchema());
704                        change.setReferencedColumnNames(fk.getPrimaryKeyColumns());
705
706                        change.setBaseTableName(fk.getForeignKeyTable().getName());
707                        change.setBaseTableSchemaName(fk.getForeignKeyTable().getSchema());
708                        change.setBaseColumnNames(fk.getForeignKeyColumns());
709
710                        change.setDeferrable(fk.isDeferrable());
711                        change.setInitiallyDeferred(fk.isInitiallyDeferred());
712                        change.setOnUpdate(fk.getUpdateRule());
713                        change.setOnDelete(fk.getDeleteRule());
714
715                        change.setReferencesUniqueColumn(fk.getReferencesUniqueColumn());
716
717                        changes.add(generateChangeSet(change));
718                }
719        }
720
721        private void addUnexpectedSequenceChanges(List<ChangeSet> changes) {
722                for (Sequence sequence : getUnexpectedSequences()) {
723
724                        DropSequenceChange change = new DropSequenceChange();
725                        change.setSequenceName(sequence.getName());
726                        change.setSchemaName(sequence.getSchema());
727
728                        changes.add(generateChangeSet(change));
729                }
730        }
731
732        private void addMissingSequenceChanges(List<ChangeSet> changes) {
733                for (Sequence sequence : getMissingSequences()) {
734
735                        CreateSequenceChange change = new CreateSequenceChange();
736                        change.setSequenceName(sequence.getName());
737                        change.setSchemaName(sequence.getSchema());
738            try {
739                final DescribeSequenceStatement statement = new DescribeSequenceStatement(sequence.getName());
740                final BigInteger startValue = (BigInteger) ExecutorService.getInstance().getExecutor(referenceSnapshot.getDatabase()).queryForObject(statement, BigInteger.class);
741                change.setStartValue(startValue);
742            }
743            catch (Exception e) {
744                e.printStackTrace();
745                // quietly don't set start value
746            }
747                        changes.add(generateChangeSet(change));
748                }
749        }
750    
751
752        private void addUnexpectedColumnChanges(List<ChangeSet> changes) {
753                for (Column column : getUnexpectedColumns()) {
754                        if (!shouldModifyColumn(column)) {
755                                continue;
756                        }
757
758                        DropColumnChange change = new DropColumnChange();
759                        change.setTableName(column.getTable().getName());
760                        change.setSchemaName(column.getTable().getSchema());
761                        change.setColumnName(column.getName());
762
763                        changes.add(generateChangeSet(change));
764                }
765        }
766
767        private void addMissingViewChanges(List<ChangeSet> changes) {
768                for (View view : getMissingViews()) {
769
770                        CreateViewChange change = new CreateViewChange();
771                        change.setViewName(view.getName());
772                        change.setSchemaName(view.getSchema());
773                        String selectQuery = view.getDefinition();
774                        if (selectQuery == null) {
775                                selectQuery = "COULD NOT DETERMINE VIEW QUERY";
776                        }
777                        change.setSelectQuery(selectQuery);
778
779                        changes.add(generateChangeSet(change));
780                }
781        }
782
783        private void addChangedViewChanges(List<ChangeSet> changes) {
784                for (View view : getChangedViews()) {
785
786                        CreateViewChange change = new CreateViewChange();
787                        change.setViewName(view.getName());
788                        change.setSchemaName(view.getSchema());
789                        String selectQuery = view.getDefinition();
790                        if (selectQuery == null) {
791                                selectQuery = "COULD NOT DETERMINE VIEW QUERY";
792                        }
793                        change.setSelectQuery(selectQuery);
794                        change.setReplaceIfExists(true);
795
796                        changes.add(generateChangeSet(change));
797                }
798        }
799
800        private void addChangedColumnChanges(List<ChangeSet> changes) {
801                for (Column column : getChangedColumns()) {
802                        if (!shouldModifyColumn(column)) {
803                                continue;
804                        }
805
806            TypeConverter targetTypeConverter = TypeConverterFactory.getInstance().findTypeConverter(targetSnapshot.getDatabase());
807                        boolean foundDifference = false;
808                        Column referenceColumn = referenceSnapshot.getColumn(column.getTable().getName(), column.getName());
809                        if (column.isDataTypeDifferent(referenceColumn)) {
810                                ModifyDataTypeChange change = new ModifyDataTypeChange();
811                                change.setTableName(column.getTable().getName());
812                                change.setSchemaName(column.getTable().getSchema());
813                                change.setColumnName(column.getName());
814                change.setNewDataType(targetTypeConverter.convertToDatabaseTypeString(referenceColumn, targetSnapshot.getDatabase()));
815                                changes.add(generateChangeSet(change));
816                                foundDifference = true;
817                        }
818                        if (column.isNullabilityDifferent(referenceColumn)) {
819                                if (referenceColumn.isNullable() == null
820                                                || referenceColumn.isNullable()) {
821                                        DropNotNullConstraintChange change = new DropNotNullConstraintChange();
822                                        change.setTableName(column.getTable().getName());
823                                        change.setSchemaName(column.getTable().getSchema());
824                                        change.setColumnName(column.getName());
825                                        change.setColumnDataType(targetTypeConverter.convertToDatabaseTypeString(referenceColumn, targetSnapshot.getDatabase()));
826
827                                        changes.add(generateChangeSet(change));
828                                        foundDifference = true;
829                                } else {
830                                        AddNotNullConstraintChange change = new AddNotNullConstraintChange();
831                                        change.setTableName(column.getTable().getName());
832                                        change.setSchemaName(column.getTable().getSchema());
833                                        change.setColumnName(column.getName());
834                                        change.setColumnDataType(targetTypeConverter.convertToDatabaseTypeString(referenceColumn, targetSnapshot.getDatabase()));
835
836                    Object defaultValue = column.getDefaultValue();
837                    String defaultValueString;
838                    if (defaultValue != null) {
839                        defaultValueString = targetTypeConverter.getDataType(defaultValue).convertObjectToString(defaultValue, targetSnapshot.getDatabase());
840
841                        if (defaultValueString != null) {
842                            change.setDefaultNullValue(defaultValueString);
843                        }
844                    }
845
846
847                    changes.add(generateChangeSet(change));
848                                        foundDifference = true;
849                                }
850
851                        }
852                        if (!foundDifference) {
853                                throw new RuntimeException("Unknown difference");
854                        }
855                }
856        }
857
858        private boolean shouldModifyColumn(Column column) {
859                return column.getView() == null
860                                && !referenceSnapshot.getDatabase().isLiquibaseTable(
861                                                column.getTable().getName());
862
863        }
864
865        private void addUnexpectedViewChanges(List<ChangeSet> changes) {
866                for (View view : getUnexpectedViews()) {
867
868                        DropViewChange change = new DropViewChange();
869                        change.setViewName(view.getName());
870                        change.setSchemaName(view.getSchema());
871
872                        changes.add(generateChangeSet(change));
873                }
874        }
875
876        private void addMissingColumnChanges(List<ChangeSet> changes,
877                        Database database) {
878                for (Column column : getMissingColumns()) {
879                        if (!shouldModifyColumn(column)) {
880                                continue;
881                        }
882
883                        AddColumnChange change = new AddColumnChange();
884                        change.setTableName(column.getTable().getName());
885                        change.setSchemaName(column.getTable().getSchema());
886
887                        ColumnConfig columnConfig = new ColumnConfig();
888                        columnConfig.setName(column.getName());
889
890                        String dataType = TypeConverterFactory.getInstance().findTypeConverter(database).convertToDatabaseTypeString(column, database);
891
892                        columnConfig.setType(dataType);
893
894                        Object defaultValue = column.getDefaultValue();
895                        if (defaultValue != null) {
896                                String defaultValueString = TypeConverterFactory.getInstance()
897                                                .findTypeConverter(database).getDataType(defaultValue)
898                                                .convertObjectToString(defaultValue, database);
899                                if (defaultValueString != null) {
900                                        defaultValueString = defaultValueString.replaceFirst("'",
901                                                        "").replaceAll("'$", "");
902                                }
903                                columnConfig.setDefaultValue(defaultValueString);
904                        }
905
906                        if (column.getRemarks() != null) {
907                                columnConfig.setRemarks(column.getRemarks());
908                        }
909            ConstraintsConfig constraintsConfig = columnConfig.getConstraints();
910                        if (column.isNullable() != null && !column.isNullable()) {
911                                if (constraintsConfig == null) {
912                                        constraintsConfig = new ConstraintsConfig();
913                                }
914                                constraintsConfig.setNullable(false);
915                        }
916            if (column.isUnique()) {
917                                if (constraintsConfig == null) {
918                                        constraintsConfig = new ConstraintsConfig();
919                                }
920                                constraintsConfig.setUnique(true);
921                        }
922                        if (constraintsConfig != null) {
923                                columnConfig.setConstraints(constraintsConfig);
924                        }
925
926                        change.addColumn(columnConfig);
927
928                        changes.add(generateChangeSet(change));
929                }
930        }
931
932        private void addMissingTableChanges(List<ChangeSet> changes,
933                                        Database database) {
934                for (Table missingTable : getMissingTables()) {
935                        if (referenceSnapshot.getDatabase().isLiquibaseTable(
936                                        missingTable.getName())) {
937                                continue;
938                        }
939
940                        CreateTableChange change = new CreateTableChange();
941                        change.setTableName(missingTable.getName());
942                        change.setSchemaName(missingTable.getSchema());
943                        if (missingTable.getRemarks() != null) {
944                                change.setRemarks(missingTable.getRemarks());
945                        }
946
947                        for (Column column : missingTable.getColumns()) {
948                                ColumnConfig columnConfig = new ColumnConfig();
949                                columnConfig.setName(column.getName());
950                                columnConfig.setType(TypeConverterFactory.getInstance().findTypeConverter(database).convertToDatabaseTypeString(column, database));
951
952                                ConstraintsConfig constraintsConfig = null;
953                                if (column.isPrimaryKey()) {
954                                        PrimaryKey primaryKey = null;
955                                        for (PrimaryKey pk : getMissingPrimaryKeys()) {
956                                                if (pk.getTable().getName().equalsIgnoreCase(missingTable.getName())) {
957                                                        primaryKey = pk;
958                                                }
959                                        }
960
961                                        if (primaryKey == null || primaryKey.getColumnNamesAsList().size() == 1) {
962                                                constraintsConfig = new ConstraintsConfig();
963                                                constraintsConfig.setPrimaryKey(true);
964                                                constraintsConfig.setPrimaryKeyTablespace(column.getTablespace());
965
966                                                if (primaryKey != null) {
967                                                        constraintsConfig.setPrimaryKeyName(primaryKey.getName());
968                                                        getMissingPrimaryKeys().remove(primaryKey);
969                                                }
970                                        }
971                                }
972
973                                if (column.isAutoIncrement()) {
974                                        columnConfig.setAutoIncrement(true);
975                                }
976
977                                if (column.isNullable() != null && !column.isNullable()) {
978                                        if (constraintsConfig == null) {
979                                                constraintsConfig = new ConstraintsConfig();
980                                        }
981
982                                        constraintsConfig.setNullable(false);
983                                }
984                if (column.isUnique()) {
985                                        if (constraintsConfig == null) {
986                                                constraintsConfig = new ConstraintsConfig();
987                                        }
988                                        constraintsConfig.setUnique(true);
989                                }
990                                if (constraintsConfig != null) {
991                                        columnConfig.setConstraints(constraintsConfig);
992                                }
993
994                                Object defaultValue = column.getDefaultValue();
995                                if (defaultValue == null) {
996                                        // do nothing
997                                } else if (column.isAutoIncrement()) {
998                                        // do nothing
999                                } else if (defaultValue instanceof Date) {
1000                                        columnConfig.setDefaultValueDate((Date) defaultValue);
1001                                } else if (defaultValue instanceof Boolean) {
1002                                        columnConfig.setDefaultValueBoolean(((Boolean) defaultValue));
1003                                } else if (defaultValue instanceof Number) {
1004                                        columnConfig.setDefaultValueNumeric(((Number) defaultValue));
1005                                } else if (defaultValue instanceof DatabaseFunction) {
1006                                    columnConfig.setDefaultValueComputed((DatabaseFunction) defaultValue);
1007                                } else {
1008                                        columnConfig.setDefaultValue(defaultValue.toString());
1009                                }
1010
1011                                if (column.getRemarks() != null) {
1012                                        columnConfig.setRemarks(column.getRemarks());
1013                                }
1014
1015                                change.addColumn(columnConfig);
1016                        }
1017
1018                        changes.add(generateChangeSet(change));
1019                }
1020        }
1021
1022        private void addUnexpectedTableChanges(List<ChangeSet> changes) {
1023                for (Table unexpectedTable : getUnexpectedTables()) {
1024                        DropTableChange change = new DropTableChange();
1025                        change.setTableName(unexpectedTable.getName());
1026                        change.setSchemaName(unexpectedTable.getSchema());
1027
1028                        changes.add(generateChangeSet(change));
1029                }
1030        }
1031
1032        private void addInsertDataChanges(List<ChangeSet> changeSets, String dataDir)
1033                        throws DatabaseException, IOException {
1034                try {
1035                        String schema = referenceSnapshot.getSchema();
1036                        for (Table table : referenceSnapshot.getTables()) {
1037                                List<Change> changes = new ArrayList<Change>();
1038                                List<Map> rs = ExecutorService.getInstance().getExecutor(referenceSnapshot.getDatabase()).queryForList(new RawSqlStatement("SELECT * FROM "+ referenceSnapshot.getDatabase().escapeTableName(schema,table.getName())));
1039
1040                                if (rs.size() == 0) {
1041                                        continue;
1042                                }
1043
1044                List<String> columnNames = new ArrayList<String>();
1045                for (Column column : table.getColumns()) {
1046                    columnNames.add(column.getName());
1047                }
1048
1049                                // if dataDir is not null, print out a csv file and use loadData
1050                                // tag
1051                                if (dataDir != null) {
1052                                        String fileName = table.getName().toLowerCase() + ".csv";
1053                                        if (dataDir != null) {
1054                                                fileName = dataDir + "/" + fileName;
1055                                        }
1056
1057                                        File parentDir = new File(dataDir);
1058                                        if (!parentDir.exists()) {
1059                                                parentDir.mkdirs();
1060                                        }
1061                                        if (!parentDir.isDirectory()) {
1062                                                throw new RuntimeException(parentDir
1063                                                                + " is not a directory");
1064                                        }
1065
1066                                        CSVWriter outputFile = new CSVWriter(new FileWriter(
1067                                                        fileName));
1068                                        String[] dataTypes = new String[columnNames.size()];
1069                                        String[] line = new String[columnNames.size()];
1070                                        for (int i = 0; i < columnNames.size(); i++) {
1071                                                line[i] = columnNames.get(i);
1072                                        }
1073                                        outputFile.writeNext(line);
1074
1075                                        for (Map row : rs) {
1076                                                line = new String[columnNames.size()];
1077
1078                                                for (int i = 0; i < columnNames.size(); i++) {
1079                                                        Object value = row.get(columnNames.get(i).toUpperCase());
1080                                                        if (dataTypes[i] == null && value != null) {
1081                                                                if (value instanceof Number) {
1082                                                                        dataTypes[i] = "NUMERIC";
1083                                                                } else if (value instanceof Boolean) {
1084                                                                        dataTypes[i] = "BOOLEAN";
1085                                                                } else if (value instanceof Date) {
1086                                                                        dataTypes[i] = "DATE";
1087                                                                } else {
1088                                                                        dataTypes[i] = "STRING";
1089                                                                }
1090                                                        }
1091                                                        if (value == null) {
1092                                                                line[i] = "NULL";
1093                                                        } else {
1094                                if (value instanceof Date) {
1095                                    line[i] = new ISODateFormat().format(((Date) value));
1096                                } else {
1097                                    line[i] = value.toString();
1098                                }
1099                            }
1100                                                }
1101                                                outputFile.writeNext(line);
1102                                        }
1103                                        outputFile.flush();
1104                                        outputFile.close();
1105
1106                                        LoadDataChange change = new LoadDataChange();
1107                                        change.setFile(fileName);
1108                                        change.setEncoding("UTF-8");
1109                                        change.setSchemaName(schema);
1110                                        change.setTableName(table.getName());
1111
1112                                        for (int i = 0; i < columnNames.size(); i++) {
1113                                                String colName = columnNames.get(i);
1114                                                LoadDataColumnConfig columnConfig = new LoadDataColumnConfig();
1115                                                columnConfig.setHeader(colName);
1116                                                columnConfig.setName(colName);
1117                                                columnConfig.setType(dataTypes[i]);
1118
1119                                                change.addColumn(columnConfig);
1120                                        }
1121
1122                                        changes.add(change);
1123                                } else { // if dataDir is null, build and use insert tags
1124                                        for (Map row : rs) {
1125                                                InsertDataChange change = new InsertDataChange();
1126                                                change.setSchemaName(schema);
1127                                                change.setTableName(table.getName());
1128
1129                                                // loop over all columns for this row
1130                                                for (int i = 0; i < columnNames.size(); i++) {
1131                                                        ColumnConfig column = new ColumnConfig();
1132                                                        column.setName(columnNames.get(i));
1133
1134                                                        Object value = row.get(columnNames.get(i).toUpperCase());
1135                                                        if (value == null) {
1136                                                                column.setValue(null);
1137                                                        } else if (value instanceof Number) {
1138                                                                column.setValueNumeric((Number) value);
1139                                                        } else if (value instanceof Boolean) {
1140                                                                column.setValueBoolean((Boolean) value);
1141                                                        } else if (value instanceof Date) {
1142                                                                column.setValueDate((Date) value);
1143                                                        } else { // string
1144                                                                column.setValue(value.toString().replace("\\","\\\\"));
1145                                                        }
1146
1147                                                        change.addColumn(column);
1148
1149                                                }
1150
1151                                                // for each row, add a new change
1152                                                // (there will be one group per table)
1153                                                changes.add(change);
1154                                        }
1155
1156                                }
1157                                if (changes.size() > 0) {
1158                                        ChangeSet changeSet = generateChangeSet();
1159                                        for (Change change : changes) {
1160                                                changeSet.addChange(change);
1161                                        }
1162                                        changeSets.add(changeSet);
1163                                }
1164                        }
1165
1166                } catch (Exception e) {
1167                        throw new RuntimeException(e);
1168                }
1169        }
1170}