Coverage Report - liquibase.changelog.ChangeSet
 
Classes in this File Line Coverage Branch Coverage Complexity
ChangeSet
51%
121/237
44%
51/114
3.15
ChangeSet$ExecType
100%
8/8
N/A
3.15
ChangeSet$RunStatus
100%
2/2
N/A
3.15
ChangeSet$ValidationFailOption
83%
5/6
N/A
3.15
 
 1  
 package liquibase.changelog;
 2  
 
 3  
 import java.util.ArrayList;
 4  
 import java.util.Arrays;
 5  
 import java.util.Collections;
 6  
 import java.util.Date;
 7  
 import java.util.HashSet;
 8  
 import java.util.List;
 9  
 import java.util.Set;
 10  
 
 11  
 import liquibase.change.Change;
 12  
 import liquibase.change.CheckSum;
 13  
 import liquibase.change.core.EmptyChange;
 14  
 import liquibase.change.core.RawSQLChange;
 15  
 import liquibase.database.Database;
 16  
 import liquibase.exception.DatabaseException;
 17  
 import liquibase.exception.MigrationFailedException;
 18  
 import liquibase.exception.PreconditionErrorException;
 19  
 import liquibase.exception.PreconditionFailedException;
 20  
 import liquibase.exception.RollbackFailedException;
 21  
 import liquibase.exception.SetupException;
 22  
 import liquibase.exception.UnexpectedLiquibaseException;
 23  
 import liquibase.exception.UnsupportedChangeException;
 24  
 import liquibase.executor.Executor;
 25  
 import liquibase.executor.ExecutorService;
 26  
 import liquibase.logging.LogFactory;
 27  
 import liquibase.logging.Logger;
 28  
 import liquibase.precondition.Conditional;
 29  
 import liquibase.precondition.core.ErrorPrecondition;
 30  
 import liquibase.precondition.core.FailedPrecondition;
 31  
 import liquibase.precondition.core.PreconditionContainer;
 32  
 import liquibase.sql.visitor.SqlVisitor;
 33  
 import liquibase.statement.SqlStatement;
 34  
 import liquibase.util.StreamUtil;
 35  
 import liquibase.util.StringUtils;
 36  
 
 37  
 /**
 38  
  * Encapsulates a changeSet and all its associated changes.
 39  
  */
 40  
 public class ChangeSet implements Conditional {
 41  
 
 42  6
     public enum RunStatus {
 43  1
         NOT_RAN, ALREADY_RAN, RUN_AGAIN, MARK_RAN, INVALID_MD5SUM
 44  
     }
 45  
 
 46  1
     public enum ExecType {
 47  1
         EXECUTED("EXECUTED", false, true), FAILED("FAILED", false, false), SKIPPED("SKIPPED", false, false), RERAN(
 48  1
                 "RERAN", true, true), MARK_RAN("MARK_RAN", false, true);
 49  
 
 50  5
         ExecType(String value, boolean ranBefore, boolean ran) {
 51  5
             this.value = value;
 52  5
             this.ranBefore = ranBefore;
 53  5
             this.ran = ran;
 54  5
         }
 55  
 
 56  
         public final String value;
 57  
         public final boolean ranBefore;
 58  
         public final boolean ran;
 59  
     }
 60  
 
 61  1
     public enum ValidationFailOption {
 62  1
         HALT("HALT"), MARK_RAN("MARK_RAN");
 63  
 
 64  
         String key;
 65  
 
 66  2
         ValidationFailOption(String key) {
 67  2
             this.key = key;
 68  2
         }
 69  
 
 70  
         @Override
 71  
         public String toString() {
 72  0
             return key;
 73  
         }
 74  
     }
 75  
 
 76  
     /**
 77  
      * List of change objects defined in this changeset
 78  
      */
 79  
     private List<Change> changes;
 80  
 
 81  
     /**
 82  
      * "id" specified in changeLog file. Combination of id+author+filePath must be unique
 83  
      */
 84  
     private String id;
 85  
 
 86  
     /**
 87  
      * "author" defined in changeLog file. Having each developer use a unique author tag allows duplicates of "id"
 88  
      * attributes between developers.
 89  
      */
 90  
     private String author;
 91  
 
 92  
     /**
 93  
      * File changeSet is defined in. May be a logical/non-physical string. It is included in the unique identifier to
 94  
      * allow duplicate id+author combinations in different files
 95  
      */
 96  164
     private String filePath = "UNKNOWN CHANGE LOG";
 97  
 
 98  
     private Logger log;
 99  
 
 100  
     /**
 101  
      * If set to true, the changeSet will be executed on every update. Defaults to false
 102  
      */
 103  
     private boolean alwaysRun;
 104  
 
 105  
     /**
 106  
      * If set to true, the changeSet will be executed when the checksum changes. Defaults to false.
 107  
      */
 108  
     private boolean runOnChange;
 109  
 
 110  
     /**
 111  
      * Runtime contexts in which the changeSet will be executed. If null or empty, will execute regardless of contexts
 112  
      * set
 113  
      */
 114  
     private Set<String> contexts;
 115  
 
 116  
     /**
 117  
      * Databases for which this changeset should run. The string values should match the value returned from
 118  
      * Database.getTypeName()
 119  
      */
 120  
     private Set<String> dbmsSet;
 121  
 
 122  
     /**
 123  
      * If false, do not stop liquibase update execution if an error is thrown executing the changeSet. Defaults to true
 124  
      */
 125  
     private Boolean failOnError;
 126  
 
 127  
     /**
 128  
      * List of checksums that are assumed to be valid besides the one stored in the database. Can include the string
 129  
      * "any"
 130  
      */
 131  164
     private Set<CheckSum> validCheckSums = new HashSet<CheckSum>();
 132  
 
 133  
     /**
 134  
      * If true, the changeSet will run in a database transaction. Defaults to true
 135  
      */
 136  
     private boolean runInTransaction;
 137  
 
 138  
     /**
 139  
      * Behavior if the validation of any of the changeSet changes fails. Does not include checksum validation
 140  
      */
 141  164
     private ValidationFailOption onValidationFail = ValidationFailOption.HALT;
 142  
 
 143  
     /**
 144  
      * Stores if validation failed on this chhangeSet
 145  
      */
 146  
     private boolean validationFailed;
 147  
 
 148  
     /**
 149  
      * Changes defined to roll back this changeSet
 150  
      */
 151  164
     private List<Change> rollBackChanges = new ArrayList<Change>();
 152  
 
 153  
     /**
 154  
      * ChangeSet comments defined in changeLog file
 155  
      */
 156  
     private String comments;
 157  
 
 158  
     /**
 159  
      * ChangeSet level precondtions defined for this changeSet
 160  
      */
 161  
     private PreconditionContainer preconditions;
 162  
 
 163  
     /**
 164  
      * SqlVisitors defined for this changeset. SqlVisitors will modify the SQL generated by the changes before sending
 165  
      * it to the database.
 166  
      */
 167  164
     private List<SqlVisitor> sqlVisitors = new ArrayList<SqlVisitor>();
 168  
 
 169  
     public boolean shouldAlwaysRun() {
 170  6
         return alwaysRun;
 171  
     }
 172  
 
 173  
     public boolean shouldRunOnChange() {
 174  5
         return runOnChange;
 175  
     }
 176  
 
 177  
     public ChangeSet(String id, String author, boolean alwaysRun, boolean runOnChange, String filePath,
 178  
             String contextList, String dbmsList) {
 179  132
         this(id, author, alwaysRun, runOnChange, filePath, contextList, dbmsList, true);
 180  132
     }
 181  
 
 182  
     public ChangeSet(String id, String author, boolean alwaysRun, boolean runOnChange, String filePath,
 183  164
             String contextList, String dbmsList, boolean runInTransaction) {
 184  164
         this.changes = new ArrayList<Change>();
 185  164
         log = LogFactory.getLogger();
 186  164
         this.id = id;
 187  164
         this.author = author;
 188  164
         this.filePath = filePath;
 189  164
         this.alwaysRun = alwaysRun;
 190  164
         this.runOnChange = runOnChange;
 191  164
         this.runInTransaction = runInTransaction;
 192  164
         if (StringUtils.trimToNull(contextList) != null) {
 193  41
             String[] strings = contextList.toLowerCase().split(",");
 194  41
             contexts = new HashSet<String>();
 195  92
             for (String string : strings) {
 196  51
                 contexts.add(string.trim().toLowerCase());
 197  
             }
 198  
         }
 199  164
         if (StringUtils.trimToNull(dbmsList) != null) {
 200  20
             String[] strings = dbmsList.toLowerCase().split(",");
 201  20
             dbmsSet = new HashSet<String>();
 202  41
             for (String string : strings) {
 203  21
                 dbmsSet.add(string.trim().toLowerCase());
 204  
             }
 205  
         }
 206  164
     }
 207  
 
 208  
     public String getFilePath() {
 209  36
         return filePath;
 210  
     }
 211  
 
 212  
     public CheckSum generateCheckSum() {
 213  19
         StringBuffer stringToMD5 = new StringBuffer();
 214  19
         for (Change change : getChanges()) {
 215  5
             stringToMD5.append(change.generateCheckSum()).append(":");
 216  
         }
 217  
 
 218  19
         for (SqlVisitor visitor : this.getSqlVisitors()) {
 219  0
             stringToMD5.append(visitor.generateCheckSum()).append(";");
 220  
         }
 221  
 
 222  19
         return CheckSum.compute(stringToMD5.toString());
 223  
     }
 224  
 
 225  
     /**
 226  
      * This method will actually execute each of the changes in the list against the specified database.
 227  
      * 
 228  
      * @return should change set be marked as ran
 229  
      */
 230  
     public ExecType execute(DatabaseChangeLog databaseChangeLog, Database database) throws MigrationFailedException {
 231  4
         if (validationFailed) {
 232  0
             return ExecType.MARK_RAN;
 233  
         }
 234  
 
 235  4
         long startTime = new Date().getTime();
 236  
 
 237  4
         ExecType execType = null;
 238  
 
 239  4
         boolean skipChange = false;
 240  
 
 241  4
         Executor executor = ExecutorService.getInstance().getExecutor(database);
 242  
         try {
 243  
             // set auto-commit based on runInTransaction if database supports DDL in transactions
 244  4
             if (database.supportsDDLInTransaction()) {
 245  2
                 database.setAutoCommit(!runInTransaction);
 246  
             }
 247  
 
 248  4
             executor.comment("Changeset " + toString());
 249  4
             if (StringUtils.trimToNull(getComments()) != null) {
 250  0
                 String comments = getComments();
 251  0
                 String[] lines = comments.split("\n");
 252  0
                 for (int i = 0; i < lines.length; i++) {
 253  0
                     if (i > 0) {
 254  0
                         lines[i] = database.getLineComment() + " " + lines[i];
 255  
                     }
 256  
                 }
 257  0
                 executor.comment(StringUtils.join(Arrays.asList(lines), "\n"));
 258  
             }
 259  
 
 260  
             try {
 261  4
                 if (preconditions != null) {
 262  0
                     preconditions.check(database, databaseChangeLog, this);
 263  
                 }
 264  0
             } catch (PreconditionFailedException e) {
 265  0
                 StringBuffer message = new StringBuffer();
 266  0
                 message.append(StreamUtil.getLineSeparator());
 267  0
                 for (FailedPrecondition invalid : e.getFailedPreconditions()) {
 268  0
                     message.append("          ").append(invalid.toString());
 269  0
                     message.append(StreamUtil.getLineSeparator());
 270  
                 }
 271  
 
 272  0
                 if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.HALT)) {
 273  0
                     throw new MigrationFailedException(this, message.toString(), e);
 274  0
                 } else if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.CONTINUE)) {
 275  0
                     skipChange = true;
 276  0
                     execType = ExecType.SKIPPED;
 277  
 
 278  0
                     LogFactory.getLogger().info(
 279  
                             "Continuing past: " + toString()
 280  
                                     + " despite precondition failure due to onFail='CONTINUE': " + message);
 281  0
                 } else if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.MARK_RAN)) {
 282  0
                     execType = ExecType.MARK_RAN;
 283  0
                     skipChange = true;
 284  
 
 285  0
                     log.info("Marking ChangeSet: " + toString()
 286  
                             + " ran despite precondition failure due to onFail='MARK_RAN': " + message);
 287  0
                 } else if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.WARN)) {
 288  0
                     execType = null; // already warned
 289  
                 } else {
 290  0
                     throw new UnexpectedLiquibaseException("Unexpected precondition onFail attribute: "
 291  
                             + preconditions.getOnFail(), e);
 292  
                 }
 293  0
             } catch (PreconditionErrorException e) {
 294  0
                 StringBuffer message = new StringBuffer();
 295  0
                 message.append(StreamUtil.getLineSeparator());
 296  0
                 for (ErrorPrecondition invalid : e.getErrorPreconditions()) {
 297  0
                     message.append("          ").append(invalid.toString());
 298  0
                     message.append(StreamUtil.getLineSeparator());
 299  
                 }
 300  
 
 301  0
                 if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.HALT)) {
 302  0
                     throw new MigrationFailedException(this, message.toString(), e);
 303  0
                 } else if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.CONTINUE)) {
 304  0
                     skipChange = true;
 305  0
                     execType = ExecType.SKIPPED;
 306  
 
 307  0
                 } else if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.MARK_RAN)) {
 308  0
                     execType = ExecType.MARK_RAN;
 309  0
                     skipChange = true;
 310  
 
 311  0
                     log.info("Marking ChangeSet: " + toString() + " ran despite precondition error: " + message);
 312  0
                 } else if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.WARN)) {
 313  0
                     execType = null; // already logged
 314  
                 } else {
 315  0
                     throw new UnexpectedLiquibaseException("Unexpected precondition onError attribute: "
 316  
                             + preconditions.getOnError(), e);
 317  
                 }
 318  
 
 319  0
                 database.rollback();
 320  
             } finally {
 321  4
                 database.rollback();
 322  4
             }
 323  
 
 324  4
             if (!skipChange) {
 325  4
                 for (Change change : changes) {
 326  
                     try {
 327  0
                         change.init();
 328  0
                     } catch (SetupException se) {
 329  0
                         throw new MigrationFailedException(this, se);
 330  0
                     }
 331  
                 }
 332  
 
 333  4
                 log.debug("Reading ChangeSet: " + toString());
 334  4
                 for (Change change : getChanges()) {
 335  0
                     database.executeStatements(change, databaseChangeLog, sqlVisitors);
 336  0
                     log.debug(change.getConfirmationMessage());
 337  
                 }
 338  
 
 339  4
                 if (runInTransaction) {
 340  2
                     database.commit();
 341  
                 }
 342  4
                 log.info("ChangeSet " + toString(false) + " ran successfully in "
 343  
                         + (new Date().getTime() - startTime + "ms"));
 344  4
                 if (execType == null) {
 345  4
                     execType = ExecType.EXECUTED;
 346  
                 }
 347  
             } else {
 348  0
                 log.debug("Skipping ChangeSet: " + toString());
 349  
             }
 350  
 
 351  0
         } catch (Exception e) {
 352  
             try {
 353  0
                 database.rollback();
 354  0
             } catch (Exception e1) {
 355  0
                 throw new MigrationFailedException(this, e);
 356  0
             }
 357  0
             if (getFailOnError() != null && !getFailOnError()) {
 358  0
                 log.info("Change set " + toString(false) + " failed, but failOnError was false.  Error: "
 359  
                         + e.getMessage());
 360  0
                 log.debug("Failure Stacktrace", e);
 361  0
                 execType = ExecType.FAILED;
 362  
             } else {
 363  0
                 log.severe("Change Set " + toString(false) + " failed.  Error: " + e.getMessage(), e);
 364  0
                 if (e instanceof MigrationFailedException) {
 365  0
                     throw ((MigrationFailedException) e);
 366  
                 } else {
 367  0
                     throw new MigrationFailedException(this, e);
 368  
                 }
 369  
             }
 370  
         } finally {
 371  
             // restore auto-commit to false if this ChangeSet was not run in a transaction,
 372  
             // but only if the database supports DDL in transactions
 373  4
             if (!runInTransaction && database.supportsDDLInTransaction()) {
 374  
                 try {
 375  1
                     database.setAutoCommit(false);
 376  0
                 } catch (DatabaseException e) {
 377  0
                     throw new MigrationFailedException(this, "Could not reset autocommit", e);
 378  1
                 }
 379  
             }
 380  
         }
 381  4
         return execType;
 382  
     }
 383  
 
 384  
     public void rollback(Database database) throws RollbackFailedException {
 385  
         try {
 386  0
             Executor executor = ExecutorService.getInstance().getExecutor(database);
 387  0
             executor.comment("Rolling Back ChangeSet: " + toString());
 388  0
             RanChangeSet ranChangeSet = database.getRanChangeSet(this);
 389  0
             if (rollBackChanges != null && rollBackChanges.size() > 0) {
 390  0
                 for (Change rollback : rollBackChanges) {
 391  0
                     SqlStatement[] statements = rollback.generateStatements(database);
 392  0
                     if (statements == null) {
 393  0
                         continue;
 394  
                     }
 395  0
                     for (SqlStatement statement : statements) {
 396  
                         try {
 397  0
                             executor.execute(statement, sqlVisitors);
 398  0
                         } catch (DatabaseException e) {
 399  0
                             throw new RollbackFailedException("Error executing custom SQL [" + statement + "]", e);
 400  0
                         }
 401  
                     }
 402  0
                 }
 403  
 
 404  
             } else {
 405  0
                 List<Change> changes = getChanges();
 406  0
                 for (int i = changes.size() - 1; i >= 0; i--) {
 407  0
                     Change change = changes.get(i);
 408  0
                     database.executeRollbackStatements(change, sqlVisitors);
 409  0
                     log.debug(change.getConfirmationMessage());
 410  
                 }
 411  
             }
 412  
 
 413  0
             database.commit();
 414  0
             log.debug("ChangeSet " + toString() + " has been successfully rolled back.");
 415  0
         } catch (Exception e) {
 416  
             try {
 417  0
                 database.rollback();
 418  0
             } catch (DatabaseException e1) {
 419  
                 // ok
 420  0
             }
 421  0
             throw new RollbackFailedException(e);
 422  0
         }
 423  
 
 424  0
     }
 425  
 
 426  
     /**
 427  
      * Returns an unmodifiable list of changes. To add one, use the addRefactoing method.
 428  
      */
 429  
     public List<Change> getChanges() {
 430  93
         return Collections.unmodifiableList(changes);
 431  
     }
 432  
 
 433  
     public void addChange(Change change) {
 434  39
         changes.add(change);
 435  39
         change.setChangeSet(this);
 436  39
     }
 437  
 
 438  
     public String getId() {
 439  78
         return id;
 440  
     }
 441  
 
 442  
     public String getAuthor() {
 443  64
         return author;
 444  
     }
 445  
 
 446  
     public Set<String> getContexts() {
 447  65
         return contexts;
 448  
     }
 449  
 
 450  
     public Set<String> getDbmsSet() {
 451  25
         return dbmsSet;
 452  
     }
 453  
 
 454  
     public String toString(boolean includeMD5Sum) {
 455  22
         return filePath + "::" + getId() + "::" + getAuthor()
 456  
                 + (includeMD5Sum ? ("::(Checksum: " + generateCheckSum() + ")") : "");
 457  
     }
 458  
 
 459  
     @Override
 460  
     public String toString() {
 461  11
         return toString(true);
 462  
     }
 463  
 
 464  
     public String getComments() {
 465  9
         return comments;
 466  
     }
 467  
 
 468  
     public void setComments(String comments) {
 469  7
         this.comments = comments;
 470  7
     }
 471  
 
 472  
     public boolean isAlwaysRun() {
 473  2
         return alwaysRun;
 474  
     }
 475  
 
 476  
     public boolean isRunOnChange() {
 477  2
         return runOnChange;
 478  
     }
 479  
 
 480  
     public boolean isRunInTransaction() {
 481  2
         return runInTransaction;
 482  
     }
 483  
 
 484  
     public Change[] getRollBackChanges() {
 485  12
         return rollBackChanges.toArray(new Change[rollBackChanges.size()]);
 486  
     }
 487  
 
 488  
     public void addRollBackSQL(String sql) {
 489  1
         if (StringUtils.trimToNull(sql) == null) {
 490  0
             rollBackChanges.add(new EmptyChange());
 491  0
             return;
 492  
         }
 493  
 
 494  3
         for (String statment : StringUtils.splitSQL(sql, null)) {
 495  2
             rollBackChanges.add(new RawSQLChange(statment.trim()));
 496  
         }
 497  1
     }
 498  
 
 499  
     public void addRollbackChange(Change change) throws UnsupportedChangeException {
 500  4
         rollBackChanges.add(change);
 501  4
     }
 502  
 
 503  
     public boolean supportsRollback(Database database) {
 504  0
         if (rollBackChanges != null && rollBackChanges.size() > 0) {
 505  0
             return true;
 506  
         }
 507  
 
 508  0
         for (Change change : getChanges()) {
 509  0
             if (!change.supportsRollback(database)) {
 510  0
                 return false;
 511  
             }
 512  
         }
 513  0
         return true;
 514  
     }
 515  
 
 516  
     public String getDescription() {
 517  4
         List<Change> changes = getChanges();
 518  4
         if (changes.size() == 0) {
 519  1
             return "Empty";
 520  
         }
 521  
 
 522  3
         StringBuffer returnString = new StringBuffer();
 523  3
         Class<? extends Change> lastChangeClass = null;
 524  3
         int changeCount = 0;
 525  3
         for (Change change : changes) {
 526  6
             if (change.getClass().equals(lastChangeClass)) {
 527  2
                 changeCount++;
 528  4
             } else if (changeCount > 1) {
 529  1
                 returnString.append(" (x").append(changeCount).append(")");
 530  1
                 returnString.append(", ");
 531  1
                 returnString.append(change.getChangeMetaData().getDescription());
 532  1
                 changeCount = 1;
 533  
             } else {
 534  3
                 returnString.append(", ").append(change.getChangeMetaData().getDescription());
 535  3
                 changeCount = 1;
 536  
             }
 537  6
             lastChangeClass = change.getClass();
 538  
         }
 539  
 
 540  3
         if (changeCount > 1) {
 541  1
             returnString.append(" (x").append(changeCount).append(")");
 542  
         }
 543  
 
 544  3
         return returnString.toString().replaceFirst("^, ", "");
 545  
     }
 546  
 
 547  
     public Boolean getFailOnError() {
 548  0
         return failOnError;
 549  
     }
 550  
 
 551  
     public void setFailOnError(Boolean failOnError) {
 552  5
         this.failOnError = failOnError;
 553  5
     }
 554  
 
 555  
     public ValidationFailOption getOnValidationFail() {
 556  1
         return onValidationFail;
 557  
     }
 558  
 
 559  
     public void setOnValidationFail(ValidationFailOption onValidationFail) {
 560  0
         this.onValidationFail = onValidationFail;
 561  0
     }
 562  
 
 563  
     public void setValidationFailed(boolean validationFailed) {
 564  0
         this.validationFailed = validationFailed;
 565  0
     }
 566  
 
 567  
     public void addValidCheckSum(String text) {
 568  1
         validCheckSums.add(CheckSum.parse(text));
 569  1
     }
 570  
 
 571  
     public boolean isCheckSumValid(CheckSum storedCheckSum) {
 572  3
         CheckSum currentMd5Sum = generateCheckSum();
 573  3
         if (currentMd5Sum == null) {
 574  0
             return true;
 575  
         }
 576  3
         if (storedCheckSum == null) {
 577  0
             return true;
 578  
         }
 579  3
         if (currentMd5Sum.equals(storedCheckSum)) {
 580  1
             return true;
 581  
         }
 582  
 
 583  2
         for (CheckSum validCheckSum : validCheckSums) {
 584  1
             if (validCheckSum.toString().equalsIgnoreCase("1:any")) {
 585  0
                 return true;
 586  
             }
 587  1
             if (currentMd5Sum.equals(validCheckSum)) {
 588  1
                 return true;
 589  
             }
 590  
         }
 591  1
         return false;
 592  
     }
 593  
 
 594  
     @Override
 595  
     public PreconditionContainer getPreconditions() {
 596  0
         return preconditions;
 597  
     }
 598  
 
 599  
     @Override
 600  
     public void setPreconditions(PreconditionContainer preconditionContainer) {
 601  3
         this.preconditions = preconditionContainer;
 602  3
     }
 603  
 
 604  
     public void addSqlVisitor(SqlVisitor sqlVisitor) {
 605  0
         sqlVisitors.add(sqlVisitor);
 606  0
     }
 607  
 
 608  
     public List<SqlVisitor> getSqlVisitors() {
 609  147
         return sqlVisitors;
 610  
     }
 611  
 }