All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.liquibase.groovy.delegate.ChangeSetDelegate.groovy Maven / Gradle / Ivy

/*
 * Copyright 2011-2017 Tim Berglund and Steven C. Saliman
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.liquibase.groovy.delegate

import liquibase.change.AddColumnConfig
import liquibase.change.ColumnConfig
import liquibase.change.core.*
import liquibase.change.custom.CustomChangeWrapper
import liquibase.exception.ChangeLogParseException
import liquibase.exception.RollbackImpossibleException
import liquibase.util.PatchedObjectUtil

/**
 * This class is the closure delegate for a ChangeSet.  It processes all the
 * refactoring changes for the ChangeSet.  it basically creates all the changes
 * that need to belong to the ChangeSet, but it doesn't worry too much about
 * validity of the change because Liquibase itself will deal with that.
 * 

* To keep the code simple, we don't worry too much about supporting things * that we know to be invalid. For example, if you try to use a change like * addColumn { column(columnName: 'newcolumn') }, you'll get a * wonderfully helpful MissingMethodException because of the missing map in * the change. We aren't going to muddy up the code trying to support * addColumn changes with no attributes because we know that at least a * table name is required. Similarly, it doesn't make sense to have an * addColumn change without at least one column, so we don't deal well with the * addColumn change without a closure. * * @author Steven C. Saliman */ class ChangeSetDelegate { def changeSet def databaseChangeLog def resourceAccessor def inRollback // ------------------------------------------------------------------------ // Non refactoring elements. void comment(String text) { changeSet.comments = DelegateUtil.expandExpressions(text, databaseChangeLog) } void preConditions(Map params = [:], Closure closure) { changeSet.preconditions = PreconditionDelegate.buildPreconditionContainer(databaseChangeLog, changeSet.id, params, closure) } void validCheckSum(String checksum) { changeSet.addValidCheckSum(checksum) } /** * Process an empty rollback. This doesn't actually do anything, but empty * rollbacks are allowed by the spec. */ void rollback() { // To support empty rollbacks (allowed by the spec) } void rollback(String sql) { changeSet.addRollBackSQL(DelegateUtil.expandExpressions(sql, databaseChangeLog)) } /** * Process a rollback when the rollback changes are passed in as a closure. * The closure can contain nested refactoring changes or raw sql statements. * I don't know what the XML parser will do, but if the closure contains * both refactorings and ends with SQL, the Groovy DSL parser will append the * SQL to list of rollback changes. * @param closure the closure to evaluate. */ void rollback(Closure closure) { def delegate = new ChangeSetDelegate(changeSet: changeSet, databaseChangeLog: databaseChangeLog, inRollback: true, resourceAccessor: resourceAccessor) closure.delegate = delegate closure.resolveStrategy = Closure.DELEGATE_FIRST def sql = DelegateUtil.expandExpressions(closure.call(), databaseChangeLog) if ( sql ) { changeSet.addRollBackSQL(sql) } } /** * Process a rollback when we're doing an attribute based rollback. The * Groovy DSL parser builds a little bit on the XML parser. With the XML * parser, if some attributes are given as attributes, but not a changeSetId, * the parser would just skip attribute processing and look for nested tags. * With the Groovy DSL parser, you can't have both a parameter map and a * closure, and all supported attributes are meant to find a change set. What * This means is that if a map was specified, we need to at least have a * valid changeSetId in the map. * @param params */ void rollback(Map params) { // Process map parameters in a way that will alert the user that we've got // an invalid key. This is a bit brute force, but we can clean it up later def id = null def author = null def filePath = null if ( params.containsKey('id') ) { throw new ChangeLogParseException("Error: ChangeSet '${changeSet.id}': the 'id' attribute of a rollback has been removed. Use 'changeSetId' instead.") } if ( params.containsKey('author') ) { throw new ChangeLogParseException("Error: ChangeSet '${changeSet.id}': the 'author' attribute of a rollback has been removed. Use 'changeSetAuthor' instead.") } params.each { key, value -> if ( key == "changeSetId" ) { id = DelegateUtil.expandExpressions(value, databaseChangeLog) } else if ( key == "changeSetAuthor" ) { author = DelegateUtil.expandExpressions(value, databaseChangeLog) } else if ( key == "changeSetPath" ) { filePath = DelegateUtil.expandExpressions(value, databaseChangeLog) } else { throw new ChangeLogParseException("ChangeSet '${changeSet.id}': '${key}' is not a valid rollback attribute.") } } // If we don't at least have an ID, we can't continue. if ( id == null ) { throw new RollbackImpossibleException("no changeSetId given for rollback in '${changeSet.id}'") } // If we weren't given a path, use the one from the databaseChangeLog if ( filePath == null ) { filePath = databaseChangeLog.filePath } def referencedChangeSet = databaseChangeLog.getChangeSet(filePath, author, id) if ( referencedChangeSet ) { referencedChangeSet.changes.each { change -> changeSet.addRollbackChange(change) } } else { throw new RollbackImpossibleException("Could not find changeSet to use for rollback: ${filePath}:${author}:${id}") } } void modifySql(Map params = [:], Closure closure) { if ( closure ) { def delegate = new ModifySqlDelegate(params, changeSet) closure.delegate = delegate closure.resolveStrategy = Closure.DELEGATE_FIRST closure.call() // No need to expand expressions, the ModifySqlDelegate will do it. delegate.sqlVisitors.each { changeSet.addSqlVisitor(it) } } } void groovyChange(Closure closure) { def delegate = new GroovyChangeDelegate(closure, changeSet, resourceAccessor) delegate.changeSet = changeSet delegate.resourceAccessor = resourceAccessor closure.delegate = delegate closure.resolveStrategy = Closure.DELEGATE_FIRST closure.call() } // ----------------------------------------------------------------------- // Refactoring changes void addAutoIncrement(Map params) { addMapBasedChange('addAutoIncrement', AddAutoIncrementChange, params) } void addColumn(Map params, Closure closure) { def change = makeColumnarChangeFromMap('addColumn', AddColumnChange, AddColumnConfig, params, closure) addChange(change) } void addDefaultValue(Map params) { addMapBasedChange('addDefaultValue', AddDefaultValueChange, params) } /** * process an addForeignKeyConstraint change. This change has a deprecated * property for which we need a warning. * @param params the properties to set on the new changes. */ void addForeignKeyConstraint(Map params) { if ( params['referencesUniqueColumn'] != null ) { println "Warning: ChangeSet '${changeSet.id}': addForeignKeyConstraint's referencesUniqueColumn parameter has been deprecated, and may be removed in a future release." println "Consider removing it, as Liquibase ignores it anyway." } addMapBasedChange('addForeignKeyConstraint', AddForeignKeyConstraintChange, params) } void addLookupTable(Map params) { addMapBasedChange('addLookupTable', AddLookupTableChange, params) } void addNotNullConstraint(Map params) { addMapBasedChange('addNotNullConstraint', AddNotNullConstraintChange, params) } void addPrimaryKey(Map params) { addMapBasedChange('addPrimaryKey', AddPrimaryKeyChange, params) } void addUniqueConstraint(Map params) { addMapBasedChange('addUniqueConstraint', AddUniqueConstraintChange, params) } void alterSequence(Map params) { addMapBasedChange('alterSequence', AlterSequenceChange, params) } void createIndex(Map params, Closure closure) { def change = makeColumnarChangeFromMap('createIndex', CreateIndexChange, AddColumnConfig, params, closure) addChange(change) } void createProcedure(Map params = [:], Closure closure) { def change = makeChangeFromMap('createProcedure', CreateProcedureChange, params) change.procedureText = DelegateUtil.expandExpressions(closure.call(), databaseChangeLog) addChange(change) } void createProcedure(String storedProc) { def change = new CreateProcedureChange() change.procedureText = DelegateUtil.expandExpressions(storedProc, databaseChangeLog) change.setResourceAccessor(resourceAccessor) addChange(change) } void createSequence(Map params) { addMapBasedChange('createSequence', CreateSequenceChange, params) } /** * This method only remains to let users know the correct name for this * change. */ @Deprecated void createStoredProcedure(Map params = [:], Closure closure) { throw new ChangeLogParseException("Error: ChangeSet '${changeSet.id}': 'createStoredProcedure' changes have been removed. Use 'createProcedure' instead.") } @Deprecated void createStoredProcedure(String storedProc) { throw new ChangeLogParseException("Error: ChangeSet '${changeSet.id}': 'createStoredProcedure' changes have been removed. Use 'createProcedure' instead.") } void createTable(Map params, Closure closure) { def change = makeColumnarChangeFromMap('createTable', CreateTableChange, ColumnConfig, params, closure) addChange(change) } void createView(Map params, Closure closure) { def change = makeChangeFromMap('createView', CreateViewChange, params) change.selectQuery = DelegateUtil.expandExpressions(closure.call(), databaseChangeLog) addChange(change) } void customChange(Map params, Closure closure = null) { def change = new CustomChangeWrapper() if ( closure ) { change.classLoader = closure.getClass().getClassLoader() } else { change.classLoader = this.class.classLoader } String className = DelegateUtil.expandExpressions(params['class'], databaseChangeLog) change.setClass(className) change.setResourceAccessor(resourceAccessor) if ( closure ) { def delegate = new KeyValueDelegate() closure.delegate = delegate closure.resolveStrategy = Closure.DELEGATE_FIRST closure.call() delegate.map.each { key, value -> // expandExpressions because the delegate won't change.setParam(key, DelegateUtil.expandExpressions(value, databaseChangeLog)) } } addChange(change) } /** * A Groovy-specific extension that allows a closure to be provided, * implementing the change. The closure is passed the instance of * Database. */ void customChange(Closure closure) { //TODO Figure out how to implement closure-based custom changes // It's not easy, since the closure would probably need the Database object to be // interesting, and that's not available at parse time. Perhaps we could keep this closure // around somewhere to run later when the Database is alive. } void delete(Map params, Closure closure) { def change = makeColumnarChangeFromMap('delete', DeleteDataChange, ColumnConfig, params, closure) addChange(change) } void delete(Map params) { addMapBasedChange('delete', DeleteDataChange, params) } void dropAllForeignKeyConstraints(Map params) { addMapBasedChange('dropAllForeignKeyConstraints', DropAllForeignKeyConstraintsChange, params) } void dropColumn(Map params) { addMapBasedChange('dropColumn', DropColumnChange, params) } void dropColumn(Map params, Closure closure) { def change = makeColumnarChangeFromMap('dropColumn', DropColumnChange, ColumnConfig, params, closure) addChange(change) } void dropDefaultValue(Map params) { addMapBasedChange('dropDefaultValue', DropDefaultValueChange, params) } void dropForeignKeyConstraint(Map params) { addMapBasedChange('dropForeignKeyConstraint', DropForeignKeyConstraintChange, params) } void dropIndex(Map params) { addMapBasedChange('dropIndex', DropIndexChange, params) } void dropNotNullConstraint(Map params) { addMapBasedChange('dropNotNullConstraint', DropNotNullConstraintChange, params) } void dropPrimaryKey(Map params) { addMapBasedChange('dropPrimaryKey', DropPrimaryKeyChange, params) } void dropProcedure(Map params) { addMapBasedChange('dropProcedure', DropProcedureChange, params) } void dropSequence(Map params) { addMapBasedChange('dropSequence', DropSequenceChange, params) } void dropTable(Map params) { addMapBasedChange('dropTable', DropTableChange, params) } void dropUniqueConstraint(Map params) { addMapBasedChange('dropUniqueConstraint', DropUniqueConstraintChange, params) } void dropView(Map params) { addMapBasedChange('dropView', DropViewChange, params) } /** * Process an "empty" changes. It doesn't do anything, but it is allowed * by the spec. */ void empty() { // To support empty changes (allowed by the spec) } void executeCommand(Map params) { addMapBasedChange('executeCommand', ExecuteShellCommandChange, params) } void executeCommand(Map params, Closure closure) { def change = makeChangeFromMap('executeCommand', ExecuteShellCommandChange, params) def delegate = new ArgumentDelegate(changeSetId: changeSet.id, changeName: 'executeCommand') closure.delegate = delegate closure.resolveStrategy = Closure.DELEGATE_FIRST closure.call() delegate.args.each { arg -> // expand expressions because the argument delegate won't... change.addArg(DelegateUtil.expandExpressions(arg, databaseChangeLog)) } addChange(change) } void insert(Map params, Closure closure) { def change = makeColumnarChangeFromMap('insert', InsertDataChange, ColumnConfig, params, closure) addChange(change) } void loadData(Map params, Closure closure) { if ( params.file instanceof File ) { throw new ChangeLogParseException("Warning: ChangeSet '${changeSet.id}': using a File object for loadData's 'file' attribute is no longer supported. Use the path to the file instead.") } def change = makeColumnarChangeFromMap('loadData', LoadDataChange, LoadDataColumnConfig, params, closure) addChange(change) } void loadUpdateData(Map params, Closure closure) { if ( params.file instanceof File ) { throw new ChangeLogParseException("Warning: ChangeSet '${changeSet.id}': using a File object for loadUpdateData's 'file' attribute is no longer supported. Use the path to the file instead.") } def change = makeColumnarChangeFromMap('loadUpdateData', LoadUpdateDataChange, LoadDataColumnConfig, params, closure) addChange(change) } void mergeColumns(Map params) { addMapBasedChange('mergeColumns', MergeColumnChange, params) } void modifyDataType(Map params) { addMapBasedChange('modifyDataType', ModifyDataTypeChange, params) } void output(Map params) { // Workaround for Issue #28: Liquibase initializes the target to the // invalid value of an empty string instead of null, then checks for // null when deciding if it wants to use the default of STDERR. // workaround this by explicitly setting the default if no target was // given. if ( !params.containsKey('target') ) { params.target = 'STDERR' } addMapBasedChange('output', OutputChange, params) } void renameColumn(Map params) { addMapBasedChange('renameColumn', RenameColumnChange, params) } void renameSequence(Map params) { addMapBasedChange('renameSequence', RenameSequenceChange, params) } void renameTable(Map params) { addMapBasedChange('renameTable', RenameTableChange, params) } void renameView(Map params) { addMapBasedChange('renameView', RenameViewChange, params) } void setColumnRemarks(Map params) { addMapBasedChange('setColumnRemarks', SetColumnRemarksChange, params) } void setTableRemarks(Map params) { addMapBasedChange('setColumnRemarks', SetTableRemarksChange, params) } void sql(Map params = [:], Closure closure) { def change = makeChangeFromMap('sql', RawSQLChange, params) def delegate = new CommentDelegate(changeSetId: changeSet.id, changeName: 'sql') closure.delegate = delegate closure.resolveStrategy = Closure.DELEGATE_FIRST // expand expressions because the comment delegate won't... change.sql = DelegateUtil.expandExpressions(closure.call(), databaseChangeLog) change.comment = (DelegateUtil.expandExpressions(delegate.comment, databaseChangeLog)) addChange(change) } void sql(String sql) { def change = new RawSQLChange() change.sql = DelegateUtil.expandExpressions(sql, databaseChangeLog) change.setResourceAccessor(resourceAccessor) addChange(change) } void sqlFile(Map params) { // It doesn't make sense to have SQL in a sqlFile change, even though // liquibase allows it. if ( params.containsKey('sql') ) { throw new ChangeLogParseException("ChangeSet '${changeSet.id}': 'sql' is an invalid property for 'sqlFile' changes.") } def change = makeChangeFromMap('sqlFile', SQLFileChange, params) // Before we add the change, work around the Liquibase bug where sqlFile // change sets don't load the SQL until it is too late to calculate // checksums properly after a clearChecksum command. See // https://liquibase.jira.com/browse/CORE-1293 change.finishInitialization() addChange(change) } /** * Parse a stop change. This version of the method follows the XML by taking * a 'message' parameter * @param params the parameter map */ void stop(Map params) { addMapBasedChange('stop', StopChange, params) } /** * Parse a stop change. This version of the method is syntactic sugar that * allows {@code stop 'some message'} in stead of the usual parameter based * change. * @param message the stop message. */ void stop(String message) { def change = new StopChange() change.message = DelegateUtil.expandExpressions(message, databaseChangeLog) change.setResourceAccessor(resourceAccessor) addChange(change) } /** * Parse a tagDatabase change. This version of the method follows the XML * by taking a 'tag' parameter. * @param params params the parameter map */ void tagDatabase(Map params) { addMapBasedChange('tagDatabase', TagDatabaseChange, params) } /** * Parse a tagDatabase change. This version of the method is syntactic sugar * that allows {@code tagDatabase 'my-tag-name'} in stead of the usual * parameter based change. * @param tagName the name of the tag to create. */ void tagDatabase(String tagName) { def change = new TagDatabaseChange() change.tag = DelegateUtil.expandExpressions(tagName, databaseChangeLog) change.setResourceAccessor(resourceAccessor) addChange(change) } void update(Map params, Closure closure) { def change = makeColumnarChangeFromMap('update', UpdateDataChange, ColumnConfig, params, closure) addChange(change) } /** * Groovy calls methodMissing when it can't find a matching method to call. * We use it to tell the user which changeSet had the invalid element. * @param name the name of the method Groovy wanted to call. * @param args the original arguments to that method. */ def methodMissing(String name, args) { throw new ChangeLogParseException("ChangeSet '${changeSet.id}': '${name}' is not a valid element of a ChangeSet") } /** * Create a Liquibase change for the types of changes that can have a nested * closure of columns and where clauses. * @param name the name of the change to make, used for improved error messages. * @param changeClass the Liquibase class to create. * @param columnConfigClass the class for the nested column configuration. * @param closure the closure with column information * @param params a map containing attributes of the new change * @param paramNames a list of valid properties for the new change * @return the newly created change */ private def makeColumnarChangeFromMap(String name, Class changeClass, columnConfigClass, Map params, Closure closure) { def change = makeChangeFromMap(name, changeClass, params) def columnDelegate = new ColumnDelegate(columnConfigClass: columnConfigClass, databaseChangeLog: databaseChangeLog, changeSetId: changeSet.id, changeName: name) closure.delegate = columnDelegate closure.resolveStrategy = Closure.DELEGATE_FIRST closure.call() // Try to add the columns to the change. If we're dealing with something // like a "delete" change, we'll get an exception, which we'll rethrow as // a parse exception to tell the user that columns are not allowed in that // change. columnDelegate.columns.each { column -> try { change.addColumn(column) } catch (MissingMethodException e) { throw new ChangeLogParseException("ChangeSet '${changeSet.id}': columns are not allowed in '${name}' changes.", e) } } // If we have a where clause, try to set it in the change. We'll get an // exception if a where clause is not supported by the change. if ( columnDelegate.whereClause != null ) { try { // The columnDelegate DOES take care of expansion. PatchedObjectUtil.setProperty(change, 'where', columnDelegate.whereClause) } catch (RuntimeException e) { throw new ChangeLogParseException("ChangeSet '${changeSet.id}': a where clause is invalid for '${name}' changes.", e) } } return change } /** * Create a new Liquibase change and set its properties from the given * map of parameters. * @param klass the type of change to create/ * @param sourceMap a map of parameter names and values for the new change * @return the newly create change, with the appropriate properties set. * @throws ChangeLogParseException if the source map contains any keys that * are not in the list of valid paramNames. */ private def makeChangeFromMap(String name, Class klass, Map sourceMap) { def change = klass.newInstance() change.resourceAccessor = resourceAccessor sourceMap.each { key, value -> try { PatchedObjectUtil.setProperty(change, key, DelegateUtil.expandExpressions(value, databaseChangeLog)) } catch (NumberFormatException ex) { change[key] = value.toBigInteger() } catch (RuntimeException re) { throw new ChangeLogParseException("ChangeSet '${changeSet.id}': '${key}' is an invalid property for '${name}' changes.", re) } } return change } /** * Helper method used by changes that don't have closures, just attributes * that get set from the parameter map. This method will add the newly * created change to the current change set. * @param name the name of the change. Used for improved error messages. * @param klass the Liquibase class to make for the change. * @param sourceMap the map of attributes to set on the Liquibase change. * @param paramNames a list of valid attribute names. */ private def addMapBasedChange(String name, Class klass, Map sourceMap) { addChange(makeChangeFromMap(name, klass, sourceMap)) } /** * Helper method to add a change to the current change set. * @param change the change to add * @return the modified change set. */ private def addChange(change) { if ( inRollback ) { changeSet.addRollbackChange(change) } else { changeSet.addChange(change) } return changeSet } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy