├── .gitignore ├── CHANGES.md ├── README.md ├── doc └── intro.md ├── java-src └── clj_liquibase │ └── CustomDBDocVisitor.java ├── project.clj ├── src └── clj_liquibase │ ├── change.clj │ ├── cli.clj │ ├── core.clj │ ├── internal.clj │ ├── precondition.clj │ └── sql_visitor.clj └── test ├── child1.edn ├── clj_liquibase ├── example.clj ├── test_change.clj ├── test_cli.clj ├── test_core.clj ├── test_internal.clj ├── test_parser.clj ├── test_precondition.clj ├── test_sql_visitor.clj └── test_util.clj └── example.edn /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | .emacs.desktop 12 | .classpath 13 | .project 14 | .idea 15 | .lein-repl-history 16 | /derby.log 17 | /pom.xml.asc 18 | /.settings 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes and TODO 2 | 3 | * [TODO] Liquibase Functionality (commands) 4 | * Diff Changelog 5 | * Generate Changelog (i.e. reverse-engineer DB as a changelog) 6 | * [TODO] Column type vars/inference functions 7 | * Database-independent columns: http://www.liquibase.org/manual/column 8 | * Infer from java.sql.Types instance (use liquibase.database.structure.Column) 9 | * By example: (example "Joe Backer") or (eg 269.8) 10 | * [TODO] 'Change' implementations: 11 | * Custom Refactorings 12 | * Custom Refactoring Class 13 | * Execute Shell Command 14 | 15 | 16 | ## 0.6.0 / 2015-Nov-26 17 | 18 | * Support for parsing changelog files 19 | * Include EDN changelog parser 20 | * Deprecate changelog/changeset DSL API 21 | 22 | 23 | ## 0.5.3 / 2015-Aug-04 24 | 25 | * Exclude `clojure.core/update` to avoid shadow warning in Clojure 1.7 (by Jake McCrary, @jakemcc) 26 | * Fix type hints 27 | * Drop support for Clojure 1.2 28 | 29 | 30 | ## 0.5.2 / 2014-Sep-04 31 | 32 | * Make CLI command accessible (by Christopher Mark Gore, @cgore) 33 | 34 | 35 | ## 0.5.1 / 2014-Jan-28 36 | 37 | * Custom Refactorings 38 | * SQL File 39 | 40 | 41 | ## 0.5.0 / 2014-Jan-24 42 | 43 | * Use Liquibase 3.0.8 (by Jonathan Rojas, @john-roj87) 44 | * Custom Refactorings 45 | * Custom/Raw SQL (by Jonathan Rojas, @john-roj87) 46 | * Upgrade test dependencies 47 | * clj-dbcp from 0.8.0 to 0.8.1 48 | 49 | 50 | ## 0.4.0 / 2012-Sep-25 51 | 52 | * Move to Github (from Bitbucket) 53 | * Move to Leiningen build (from Maven) 54 | * Move to Liquibase 2.0.5 (from 2.0.2) 55 | * Introduce required argument _logical-schema_ in `defchangelog` 56 | * Support for CLI integration (pulled from Lein-LB) 57 | * Upgrade dependencies 58 | * clj-miscutil from 0.3 to 0.4.1 59 | * Drop clj-dbspec, use clj-jdbcutil 0.1.0 60 | * Upgrade test dependencies 61 | * OSS-JDBC from 0.5 to 0.8.0 62 | * clj-dbcp from 0.5 to 0.8.0 63 | * Improve documentation 64 | 65 | 66 | ## 0.3 / 2011-Nov-20 67 | 68 | * Use Clj-DBSpec 0.3 69 | * Liquibase Functionality (commands) 70 | # Diff (Regular database diff - output to STDOUT) 71 | * `make-changeset` now accepts SQL-visitors as :visitors optional argument 72 | # defaults `create-table` changes to InnoDB for MySQL unless overridden 73 | * 'Change' implementations 74 | # Create table: `create-table-withid` (updated) 75 | * Allow user to specify ID column name via an optional argument :idcol 76 | # Custom Refactorings (new implementation) 77 | * Modifying Generated SQL (Append, Prepend, Replace SQL visitors) 78 | 79 | 80 | ## 0.2 / 2011-Apr-01 81 | 82 | - Use Clj-DBSpec 0.2 83 | - Remove dependency on Clojure-contrib 84 | - Argument verification in functions/macros - action, change etc. 85 | - Pre-conditions 86 | - 'Change' implementations 87 | # `create-table` variant that auto-includes a bigint (auto-incr) primary key 88 | - Liquibase actions 89 | # Generate SQL statements for individual Change instances 90 | # DBDoc 91 | # SQL Output 92 | 93 | 94 | ## 0.1 / 2011-Mar-06 95 | 96 | - Liquibase Functionality (commands) 97 | # Update 98 | # Tagging 99 | # Rollback 100 | - Dynamic vars - DataSource/Connection, schema(name), etc. 101 | - Clojuresque schema/table/column names, data types, attributes and constraints 102 | - Building Change-Logs 103 | # 104 | # 105 | # Contexts 106 | # ChangeLog Parameters 107 | - Liquibase 'Change' implementations: 108 | # Structural Refactorings 109 | * Add Column 110 | * Rename Column 111 | * Modify Column 112 | * Drop Column 113 | * Alter Sequence 114 | * Create Table 115 | * Rename Table 116 | * Drop Table 117 | * Create View 118 | * Rename View 119 | * Drop View 120 | * Merge Columns 121 | * Create Stored Procedure 122 | # Data Quality Refactorings 123 | * Add Lookup Table 124 | * Add Not-Null Constraint 125 | * Remove Not-Null Constraint 126 | * Add Unique Constraint 127 | * Drop Unique Constraint 128 | * Create Sequence 129 | * Drop Sequence 130 | * Add Auto-Increment 131 | * Add Default Value 132 | * Drop Default Value 133 | # Referential Integrity Refactorings 134 | * Add Foreign Key Constraint 135 | * Drop Foreign Key Constraint 136 | * Drop All Foreign Key Constraints 137 | * Add Primary Key Constraint 138 | * Drop Primary Key Constraint 139 | # Non-Refactoring Transformations 140 | * Insert Data 141 | * Load Data 142 | * Load Update Data 143 | * Update Data 144 | * Delete Data 145 | * Tag Database 146 | * Stop 147 | # Architectural Refactorings 148 | * Create Index 149 | * Drop Index 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-liquibase 2 | 3 | Clj-Liquibase is a Clojure wrapper for [Liquibase](http://www.liquibase.org/) 4 | for database change management and migration. 5 | 6 | Supported actions: 7 | 8 | * update 9 | * tag 10 | * rollback 11 | * generate SQL for actions 12 | * generate DB doc 13 | * database diff 14 | 15 | 16 | ## Usage 17 | 18 | On Clojars: https://clojars.org/clj-liquibase 19 | 20 | Leiningen dependency: `[clj-liquibase "0.6.0"]` 21 | 22 | 23 | ### Quickstart 24 | 25 | Create a new project e.g. `fooapp` using [Leiningen](http://leiningen.org/) and 26 | include the following dependencies in `project.clj`: 27 | 28 | ```clojure 29 | [clj-dbcp "0.8.1"] ; to create connection-pooling DataSource 30 | [clj-liquibase "0.6.0"] ; for this library 31 | [oss-jdbc "0.8.0"] ; for Open Source JDBC drivers 32 | ``` 33 | 34 | #### Defining changes via a changelog file 35 | 36 | Create an [EDN](https://github.com/edn-format/edn) file `resources/changelog.edn` with the following changelog details: 37 | 38 | ```edn 39 | {:database-change-log 40 | [{:change-set 41 | {:id "101" 42 | :author "shantanu" 43 | :changes [{:create-table {:table-name "sample-table1" 44 | :columns [{:column {:name "id" :type "int" :auto-increment true :constraints {:primary-key? true 45 | :nullable? false}}} 46 | {:column {:name "name" :type "varchar(40)" :constraints {:nullable? false}}} 47 | {:column {:name "gender" :type "char(1)" :constraints {:nullable? false}}}]}}]}}]} 48 | ``` 49 | 50 | _Note: You may alternatively create YAML, JSON, SQL or XML file (refer Liquibase schema) instead of EDN._ 51 | 52 | Then create a Clojure source file for managing the DB schema: 53 | 54 | ```clojure 55 | (ns fooapp.dbschema 56 | (:require 57 | [clj-dbcp.core :as cp] 58 | [clj-liquibase.cli :as cli]) 59 | (:use 60 | [clj-liquibase.core :refer (defparser)])) 61 | 62 | (defparser app-changelog "changelog.edn") 63 | 64 | ;; keep the DataSource handy and invoke the CLI 65 | 66 | (def ds (cp/make-datasource :mysql {:host "localhost" :database "people" 67 | :user "dbuser" :password "s3cr3t"})) 68 | 69 | (defn -main 70 | [& [cmd & args]] 71 | (apply cli/entry cmd {:datasource ds :changelog app-changelog} 72 | args)) 73 | ``` 74 | 75 | #### Defining changes programmatically (DEPRECATED) 76 | 77 | **(Defining changelog/changesets programmatically is deprecated and will be removed in future.)** 78 | 79 | Create a Clojure source file for managing the DB schema. Include the required 80 | namespaces define the _change_, _changeset_ and _changelog_ objects: 81 | 82 | ```clojure 83 | (ns fooapp.dbschema 84 | (:require 85 | [clj-dbcp.core :as cp] 86 | [clj-liquibase.change :as ch] 87 | [clj-liquibase.cli :as cli]) 88 | (:use 89 | [clj-liquibase.core :refer (defchangelog)])) 90 | 91 | ;; define the changes, changesets and the changelog 92 | 93 | (def ct-change1 (ch/create-table :sample-table1 94 | [[:id :int :null false :pk true :autoinc true] 95 | [:name [:varchar 40] :null false] 96 | [:gender [:char 1] :null false]])) 97 | 98 | ; recommended: one change per changeset 99 | (def changeset-1 ["id=1" "author=shantanu" [ct-change1]]) 100 | 101 | 102 | ; you can add more changesets later to the changelog 103 | (defchangelog app-changelog "fooapp" [changeset-1]) 104 | 105 | 106 | ;; keep the DataSource handy and invoke the CLI 107 | 108 | (def ds (cp/make-datasource :mysql {:host "localhost" :database "people" 109 | :user "dbuser" :password "s3cr3t"})) 110 | 111 | (defn -main 112 | [& [cmd & args]] 113 | (apply cli/entry cmd {:datasource ds :changelog app-changelog} 114 | args)) 115 | ``` 116 | 117 | #### Applying changelog 118 | 119 | After defining the changelog, you need to apply the changes: 120 | 121 | ```bash 122 | lein run -m fooapp.dbschema help 123 | lein run -m fooapp.dbschema update 124 | ``` 125 | 126 | After running the above `update` command, we can rollback our change: 127 | ```bash 128 | lein run -m fooapp.dbschema rollback -n1 129 | ``` 130 | 131 | ### Documentation 132 | 133 | For more documentation please refer the file `doc/intro.md` in this repo. 134 | 135 | 136 | ## Contributors 137 | 138 | * Shantanu Kumar (author) 139 | * [Jonathan Rojas](https://github.com/john-roj87) 140 | * [Christopher Mark Gore](https://github.com/cgore) 141 | * [Jake McCrary](https://github.com/jakemcc) 142 | 143 | 144 | ## License 145 | 146 | Copyright © 2012-2015 Shantanu Kumar and contributors 147 | 148 | Distributed under the Eclipse Public License, the same as Clojure. 149 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to clj-liquibase 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | 5 | [Liquibase](http://liquibase.org/) is an Open Source (Apache 2 license) database 6 | change management library. Clj-Liquibase provides a way to write Liquibase 7 | changesets and changelogs in Clojure, while inheriting other attributes of 8 | Liquibase. 9 | 10 | In order to work with Clj-Liquibase, you need to know the Liquibase abstractions 11 | _change_, _changeset_ and _changelog_ objects, which constitute the changes that 12 | can be tracked and applied to JDBC databases via various _commands_. These terms 13 | are described in the sections below. 14 | 15 | Supported commands: 16 | 17 | * Update 18 | * Tag 19 | * Rollback 20 | * By tag 21 | * By date 22 | * By changeset count 23 | * DB documentation 24 | * Diff 25 | 26 | ## Understanding change, changeset, changelog 27 | 28 | A _change_ (instance of class `liquibase.change.Change`) is the smallest unit of 29 | a database change. A _change_ cannot be tracked as it is; hence it must be 30 | wrapped in a _changeset_ (instance of class `liquibase.changelog.ChangeSet`.) A 31 | _changeset_ can contain several _change_ objects in the desired order, and is 32 | marked with `id` and `author` attributes. 33 | 34 | A _changelog_ (instance of the class `liquibase.changelog.DatabaseChangeLog`) 35 | contains **all** changesets in the desired order meant for a database schema. 36 | The database may actually be up to date with, or behind the _changelog_ at a 37 | certain point of time. A Clj-Liquibase _command_ lets you apply the changelog 38 | to the database in the intended way. 39 | 40 | Important points to note: 41 | 42 | * The _change_ and _changeset_ definition (in code) cannot be modified once 43 | applied to the database. 44 | * A _changelog_ definition (in code) cannot be modified after being applied to 45 | the database. However, you can add more _changeset_ objects to it later. 46 | 47 | 48 | ### Change 49 | 50 | **Note: Defining changesets programmatically is deprecated as of version 0.6.0, and will be removed in the future.** 51 | 52 | _Change_ objects can be constructed by using the factory functions in the 53 | `clj-liquibase.change` namespace, which are described in the sub-sections below: 54 | 55 | 56 | #### Structural Refactorings 57 | 58 | | Function name | Required args | Optional kwargs | Description | 59 | |---------------------------|---------------------|------------------------|-------------| 60 | | `add-columns` | `table-name` | `:schema-name` | [Add columns to an existing table](http://www.liquibase.org/documentation/changes/add_column) | 61 | | | `columns` | | [Column definition](http://www.liquibase.org/documentation/column) | 62 | | `rename-column` | `table-name` | `:schema-name` | [Rename column in an existing table](http://www.liquibase.org/documentation/changes/rename_column) | 63 | | | `old-column-name` | `:column-data-type` || 64 | | | `new-column-name` ||| 65 | | `modify-column` | `table-name` | `:schema-name` | [Modify data type of a column in an existing table](http://www.liquibase.org/documentation/changes/modify_data_type) | 66 | | | `column-name` | || 67 | | | `new-data-type` ||| 68 | | `drop-column` | `table-name` | `:schema-name` | [Drop specified column from an existing table](http://www.liquibase.org/documentation/changes/drop_column) | 69 | | | `column-name` | || 70 | | `alter-sequence` | `seq-name` | `:schema-name` | [Modifies a database sequence](http://www.liquibase.org/documentation/changes/alter_sequence) | 71 | | | `increment-by` | `:max-value` || 72 | | | | `:min-value` || 73 | | | | `:ordered` || 74 | | `create-table` | `table-name` | `:schema-name` | [Create a new table](http://www.liquibase.org/documentation/changes/create_table) | 75 | | | `columns` | `:table-space` | [Column definition](http://www.liquibase.org/documentation/column) | 76 | | | | `:remarks` || 77 | | `create-table-withid` | same as above | same as above | Same as above, except it creates auto-incremented ID column | 78 | | | | `:idcol` || 79 | | `rename-table` | `old-table-name` | `:schema-name` | [Rename an existing table](http://www.liquibase.org/documentation/changes/rename_table) | 80 | | | `new-table-name` | || 81 | | `drop-table` | `table-name` | `:schema-name` | [Drop an existing table](http://www.liquibase.org/documentation/changes/drop_table) | 82 | | | | `:cascade-constraints` || 83 | | `create-view` | `view-name` | `:schema-name` | [Create a database view](http://www.liquibase.org/documentation/changes/create_view) | 84 | | | `select-query` | `:replace-if-exists` || 85 | | `rename-view` | `old-view-name` | `:schema-name` | [Rename an existing database view](http://www.liquibase.org/documentation/changes/rename_view) | 86 | | | `new-view-name` | || 87 | | `drop-view` | `view-name` | `:schema-name` | [Drop an existing database view](http://www.liquibase.org/documentation/changes/drop_view) | 88 | | `merge-columns` | `table-name` | `:schema-name` | [Merge two columns of the same table into one](http://www.liquibase.org/documentation/changes/merge_columns) | 89 | | | `column1-name` | || 90 | | | `join-string` ||| 91 | | | `column2-name` ||| 92 | | | `final-column-name` ||| 93 | | | `final-column-type` ||| 94 | | `create-stored-procedure` | `procedure-body` | `:comments` | [Create database stored procedure](http://www.liquibase.org/documentation/changes/create_procedure) | 95 | 96 | 97 | ##### Column config 98 | 99 | The functions `add-columns`, `create-table` and `create-table-withid` accept a 100 | `columns` argument, which is a collection of 101 | [_column-config_](http://www.liquibase.org/documentation/column) elements. Each 102 | column-config is a vector of 2 required args followed by optional keyword args. 103 | 104 | Required args: `column-name`, `column-type` 105 | Optional kwargs: 106 | 107 | | Long name | Short name |Allowed types | 108 | |---------------------------|-------------|------------------------| 109 | | `:default-value` | `:default` | String/Number/java.util.Date/Boolean/DatabaseFunction | 110 | | `:auto-increment` | `:autoinc` | Boolean | 111 | | `:remarks` | | String | 112 | | `:nullable` | `:null` | Boolean | 113 | | `:primary-key` | `:pk` | Boolean | 114 | | `:primary-key-name` | `:pkname` | String/Keyword | 115 | | `:primary-key-tablespace` | `:pktspace` | String/Keyword | 116 | | `:references` | `:refs` | String (Foreign key definition) | 117 | | `:unique` | `:uniq` | Boolean | 118 | | `:unique-constraint-name` | `:ucname` | String/Keyword | 119 | | `:check` | | String | 120 | | `:delete-cascade` | `:dcascade` | Boolean | 121 | | `:foreign-key-name` | `:fkname` | String/Keyword | 122 | | `:initially-deferred` | `:idefer` | Boolean | 123 | | `:deferrable` | `:defer` | Boolean | 124 | 125 | ##### Example 126 | 127 | Example of creating a _change_ object: 128 | 129 | ```clojure 130 | (clj-liquibase.change/create-table "sampletable1" 131 | [[:id :int :null false :pk true :autoinc true] 132 | [:name [:varchar 40] :null false] 133 | [:gender [:char 1] :null false]]) 134 | ``` 135 | 136 | 137 | #### Data Quality Refactorings 138 | 139 | | Function name | Required args | Optional kwargs | Description | 140 | |----------------------------|------------------------|-------------------------------|-------------| 141 | | `add-lookup-table` | `existing-table-name` | `:existing-table-schema-name` | [Add a lookup table](http://www.liquibase.org/documentation/changes/add_lookup_table) | 142 | | | `existing-column-name` | `:new-table-schema-name` || 143 | | | `new-table-name` | `:new-column-data-type` || 144 | | | `new-column-name` ||| 145 | | | `constraint-name` ||| 146 | | `add-not-null-constraint` | `table-name` | `:schema-name` | [Add NOT NULL constraint on specified column in a table](http://www.liquibase.org/documentation/changes/add_not_null_constraint) | 147 | | | `column-name` | `:default-null-value` || 148 | | | `column-data-type` ||| 149 | | `drop-not-null-constraint` | `table-name` | `:schema-name` | [Drop NOT NULL constraint for specified column](http://www.liquibase.org/documentation/changes/drop_not_null_constraint) | 150 | | | `column-name` | `:column-data-type` || 151 | | `add-unique-constraint` | `table-name` | `:schema-name` | [Add UNIQUE constraint for specified columns](http://www.liquibase.org/documentation/changes/add_unique_constraint) | 152 | | | `column-names` | `:table-space` || 153 | | | `constraint-name` | `:deferrable` || 154 | | | | `:initially-deferred` || 155 | | | | `:disabled` || 156 | | `drop-unique-constraint` | `table-name` | `:schema-name` | [Drop specified UNIQUE constraint](http://www.liquibase.org/documentation/changes/drop_unique_constraint) | 157 | | | `constraint-name` ||| 158 | | `create-sequence` | `sequence-name` | `:schema-name` | [Create a database sequence](http://www.liquibase.org/documentation/changes/create_sequence) | 159 | | | | `:start-value` || 160 | | | | `:increment-by` || 161 | | | | `:max-value` || 162 | | | | `:min-value` || 163 | | | | `:ordered` || 164 | | | | `:cycle` || 165 | | `drop-sequence` | `sequence-name` | `:schema-name` | [Drop specified database sequence](http://www.liquibase.org/documentation/changes/drop_sequence) | 166 | | `add-auto-increment` | `table-name` | `:schema-name` | [Convert an existing column to auto-increment type](http://www.liquibase.org/documentation/changes/add_auto_increment) | 167 | | | `column-name` ||| 168 | | | `column-data-type` ||| 169 | | `add-default-value` | `table-name` | `:schema-name` | [Add default value for specified column](http://www.liquibase.org/documentation/changes/add_default_value) | 170 | | | `column-name` | `:column-data-type` || 171 | | | `default-value` ||| 172 | | `drop-default-value` | `table-name` | `:schema-name` | [Drop default value for specified column](http://www.liquibase.org/documentation/changes/drop_default_value) | 173 | | | `column-name` | `:column-data-type` || 174 | 175 | 176 | #### Referential Integrity Refactorings 177 | 178 | | Function name | Required args | Optional kwargs | Description | 179 | |-------------------------------|---------------------------|---------------------------------|-------------| 180 | | `add-foreign-key-constraint` | `constraint-name` | `:base-table-schema-name` | [Add foreign key constraint to an existing column](http://www.liquibase.org/documentation/changes/add_foreign_key_constraint) | 181 | | | `base-table-name` | `:referenced-table-schema-name` || 182 | | | `base-column-names` | `:deferrable` || 183 | | | `referenced-table-name` | `:initially-deferred` || 184 | | | `referenced-column-names` | `:on-delete` || 185 | | | | `:on-update` || 186 | | `drop-foreign-key-constraint` | `constraint-name` | `:schema-name` | [Drop a foreign key constraint](http://www.liquibase.org/documentation/changes/drop_foreign_key_constraint) | 187 | | | `base-table-name` ||| 188 | | `add-primary-key` | `table-name` | `:schema-name` | [Add primary key from one or more columns](http://www.liquibase.org/documentation/changes/add_primary_key) | 189 | | | `column-names` | `:table-space` || 190 | | | `constraint-name` ||| 191 | | `drop-primary-key` | `table-name` | `:schema-name` | [Drop an existing primary key](http://www.liquibase.org/documentation/changes/drop_primary_key) | 192 | | | | `:constraint-name` || 193 | 194 | 195 | #### Non-Refactoring Transformations 196 | 197 | | Function name | Required args | Type | Optional kwargs | Description | 198 | |-------------------------------|-------------------------|----------|---------------------------------|-------------| 199 | | `insert-data` | `table-name` | str/kw | `:schema-name` | [Insert data into specified table](http://www.liquibase.org/documentation/changes/insert) | 200 | | | `column-value-map` | map ||| 201 | | `load-data` | `table-name` | str/kw | `:schema-name` | [Load data from CSV file into specified table](http://www.liquibase.org/documentation/changes/load_data) | 202 | | | `csv-filename` | string | `:encoding` || 203 | | | `columns-spec` | coll/map ||| 204 | | `load-update-data` | `table-name` | str/kw | `:schema-name` | [Load and save (insert/update) data from CSV file into specified table](http://www.liquibase.org/documentation/changes/load_update_data) | 205 | | | `csv-filename` | string | `:encoding` || 206 | | | `primary-key-cols` | | || 207 | | | `columns-spec` | coll/map | || 208 | | `update-data` | `table-name` | | `:schema-name` | [Update data in existing table](http://www.liquibase.org/documentation/changes/update) | 209 | | | `column-name-value-map` | | `:where-clause` || 210 | | `delete-data` | `table-name` | | `:schema-name` | [Delete data from specified table](http://www.liquibase.org/documentation/changes/delete) | 211 | | | | | `:where-clause` || 212 | | `tag-database` | `tag` | | | [Tag the database with specified tag](http://www.liquibase.org/documentation/changes/tag_database) | 213 | | `stop` | | | | [Stop Liquibase execution immediately, useful for debugging](http://www.liquibase.org/documentation/changes/stop) | 214 | 215 | ##### Columns config for loading data 216 | 217 | Loading data from CSV files into the database requires translation rules. The 218 | functions `load-data` and `load-update-data` accept an argument `columns-spec` 219 | that is a collection of column-config elements. Every column-config is a 220 | collection of 2 required arguments followed by optional keyword args: 221 | 222 | Required arguments: 223 | 224 | * First element: `colname` (keyword/string) 225 | * Second element: `coltype` (either of "STRING", "NUMERIC", "DATE", "BOOLEAN") 226 | 227 | Optional keyword args with corresponding values: 228 | 229 | `:index` (number) 230 | `:header` (Keyword/String) 231 | 232 | #### Architectural Refactorings 233 | 234 | | Function name | Required args | Type | Optional kwargs | Description | 235 | |-------------------------------|-------------------------|------------|---------------------------------|-------------| 236 | | `create-index` | `table-name` | stringable | `:schema-name` | [Create index with specified column names](http://www.liquibase.org/documentation/changes/create_index) | 237 | | | `column-names` | collection | `:index-name` || 238 | | | | | `:unique` || 239 | | | | | `:table-space` || 240 | | `drop-index` | `index-name` | stringable | `:schema-name` | [Drop an existing index](http://www.liquibase.org/documentation/changes/drop_index) | 241 | | | `table-name` | stringable ||| 242 | 243 | 244 | #### Custom Refactorings 245 | 246 | | Function name | Required args | Type | Optional kwargs | Description | 247 | |---------------|---------------|------------|---------------------|-------------| 248 | | `sql` | `sql` | string | `:comment` | [Execute given SQL](http://www.liquibase.org/documentation/changes/sql) | 249 | | | | | `:dbms` || 250 | | | | | `:encoding` || 251 | | | | | `:end-delimiter` || 252 | | | | | `:split-statements` || 253 | | | | | `:strip-comments` || 254 | | `sql-file` | `file-path` | string | `:dbms` | [Execute SQL from file](http://www.liquibase.org/documentation/changes/sql_file) | 255 | | | | | `:encoding` || 256 | | | | | `:end-delimiter` || 257 | | | | | `:split-statements` || 258 | | | | | `:strip-comments` || 259 | 260 | 261 | #### Short names for keyword args 262 | 263 | Note that you can use the following short names for corresponding keyword args: 264 | 265 | | Keyword arg (long name) | Short name | Value type | Default | 266 | |---------------------------------|--------------------|-----------------------|---------| 267 | | `:schema-name` | `:schema` | string/keyword || 268 | | `:existing-table-schema-name` | `:existing-schema` | string/keyword || 269 | | `:new-table-schema-name` | `:new-schema` | string/keyword || 270 | | `:column-data-type` | `:data-type` | string/keyword/vector || 271 | | `:new-column-data-type` | `:new-data-type` | string/keyword/vector || 272 | | `:max-value` | `:max` | number or string || 273 | | `:min-value` | `:min` | number or string || 274 | | `:ordered` | `:ord` | true or false || 275 | | `:table-space` | `:tspace` | string/keyword || 276 | | `:cascade-constraints` | `:cascade` | logical boolean || 277 | | `:replace-if-exists` | `:replace` | logical boolean || 278 | | `:default-null-value` | `:default` | string || 279 | | `:deferrable` | `:defer` | logical boolean || 280 | | `:initially-deferred` | `:idefer` | logical boolean || 281 | | `:start-value` | `:start` | coerced as BigInteger || 282 | | `:increment-by` | `:incby` | coerced as BigInteger || 283 | | `:cycle` | `:cyc` | logical boolean || 284 | | `:encoding` | `:enc` | string | "UTF-8" | 285 | | `:base-table-schema-name` | `:base-schema` | string || 286 | | `:referenced-table-schema-name` | `:ref-schema` | string || 287 | | `:on-delete` | `:ondel` | string || 288 | | `:on-update` | `:onupd` | string || 289 | | `:where-clause` | `:where` | string || 290 | | `:index-name` | `:index` | string || 291 | | `:unique` | `:uniq` | logical boolean || 292 | 293 | 294 | ### Constructing Changeset objects 295 | 296 | A [_changeset_](http://www.liquibase.org/documentation/changeset) can be constructed 297 | using the function `clj-liquibase.core/make-changeset`. 298 | Required args: `id` (string), `author` (string), `changes` (collection of _change_ objects) 299 | Optional kwargs: 300 | 301 | | Long name | Short name | Type | 302 | |-----------------------|---------------|--------------| 303 | | `:dbms` | | String/Keyword/vector-of-multiple | 304 | | `:run-always` | `:always` | Boolean | 305 | | `:run-on-change` | `:on-change` | Boolean | 306 | | `:context` | `:ctx` | String | 307 | | `:run-in-transaction` | `:in-txn` | Boolean (true by default) | 308 | | `:fail-on-error` | `:fail-err` | Boolean | 309 | | `:comment` | | String | 310 | | `:pre-conditions` | `:pre-cond` | list of Precondition objects, or PreconditionContainer object | 311 | | `:valid-checksum` | `:valid-csum` | String | 312 | | `:visitors` | | collection of SqlVisitor objects | 313 | 314 | An example changeset-construction look like this: 315 | 316 | ```clojure 317 | ;; assume `ch1` is a change object 318 | (clj-liquibase.core/make-changeset "id=1" "author=shantanu" [ch1]) 319 | ``` 320 | 321 | A shorter way to define a _changeset_ for use in a _changelog_ is to only store 322 | the arguments in a vector -- `defchangelog` automatically creates a _changeset_ 323 | from the arguments in the vector: 324 | 325 | ```clojure 326 | (def ch-set1 ["id=1" "author=shantanu" [ch1]]) 327 | ``` 328 | _Note:_ Changeset ID should be unique per user across all changesets in a 329 | changelog, i.e. several users can have identical changeset ID. 330 | 331 | The recommended way to create a changeset is to wrap only one _change_ object, 332 | the main reason being pre-conditions and SQL-visitors can be applied only at the 333 | changeset level. Since changesets cannot be modified after being applied to the 334 | database, it would be impossible to go back and refactor the changesets. 335 | However, one can add conditional SQL-visitor to a changeset later to modify the 336 | generated SQL statement a little to suit a different database type. 337 | 338 | #### Precondition 339 | 340 | TODO 341 | 342 | #### SQL Visitor 343 | 344 | TODO 345 | 346 | ### Defining Changelog 347 | 348 | #### Defining changelog via external file 349 | 350 | If you have a changelog defined in a EDN, YAML, SQL, JSON or XML file in classpath or file system, you can define the 351 | changelog as follows: 352 | 353 | ```clojure 354 | (clj-liquibase.core/defparser "changelog.edn") ; reads from classpath 355 | 356 | (clj-liquibase.core/defparser "changelog.yml" {:source :filesystem}) ; reads from file system 357 | ``` 358 | 359 | #### Defining changelog programmatically (DEPRECATED) 360 | 361 | **Note: Defining changelog programmatically is deprecated as of version 0.6.0, and will be removed in the future.** 362 | 363 | A _changelog_ can be defined using the `defchangelog` macro, which essentially 364 | defines a partially applied function such that when executed with no args it 365 | returns a `liquibase.changelog.DatabaseChangeLog` object. 366 | 367 | ```clojure 368 | (clj-liquibase.core/defchangelog changelog-name 369 | "logical-schema-name" [changeset-1 changeset-2 changeset-3]) 370 | ``` 371 | 372 | Alternatively, you can also create a changelog using the factory function 373 | `clj-liquibase.core/make-changelog`. The macro `defchangelog` returns a higher 374 | order function that calls `make-changelog`. 375 | 376 | The function `make-changelog` and (hence) the `defchangelog` macro accept an 377 | optional keyword argument `:pre-conditions` (short name `:pre-cond`) to specify 378 | the pre-condition checks for the changelog. 379 | 380 | A changelog definition cannot be modified once applied to the database; however, 381 | you can incrementally add changesets to a changelog as time goes. 382 | 383 | ## Command Line Interface (CLI) integration 384 | 385 | The _Command-Line Interface_ is the easiest integration option for applications 386 | that want to use Clj-Liquibase. The `clj-liquibase.cli` namespace has a built-in 387 | command-line argument parser that knows about the CLI commands and their various 388 | switches respectively. 389 | 390 | An application simply needs to collect user-provided command line arguments and 391 | invoke: 392 | 393 | ```clojure 394 | (clj-liquibase.cli/entry cmd opts & args) 395 | ``` 396 | 397 | The `clj-liquibase.cli/entry` arguments are described below: 398 | 399 | | Argument | Description | 400 | |----------|-------------| 401 | | `cmd` | any of "help" "version" "update" "rollback" "tag" "dbdoc" "diff" | 402 | | `opts` | default options | 403 | | `args` | user provided arguments | 404 | 405 | The various switches for their respective commands are listed below: 406 | 407 | | Command | Required | Optional | Opt no-value | Description | 408 | |------------|-------------------|--------------|--------------|-------------| 409 | | `help` | | | | Show help text | 410 | | `version` | | | | Show Clj-Liquibase version | 411 | | `update` | `:datasource` | `:chs-count` | `:sql-only` | [Update database to specified changelog](http://www.liquibase.org/documentation/update) | 412 | | | `:changelog` | `:contexts` ||| 413 | | `rollback` | `:datasource` | `:chs-count` | `:sql-only` | [Rollback database to specified changeset-count/tag/ISO-date](http://www.liquibase.org/documentation/rollback) | 414 | | | `:changelog` | `:tag` ||| 415 | | | | `:date` ||| 416 | | | | `:contexts` ||| 417 | | `tag` | `:datasource` | | | Tag the database on _ad hoc_ basis | 418 | | | `:tag` |||| 419 | | `dbdoc` | `:datasource` | `:contexts` | | [Generate database/changelog documentation](http://www.liquibase.org/documentation/dbdoc) | 420 | | | `:changelog` |||| 421 | | | `:output-dir` |||| 422 | | `diff` | `:datasource` | | | [Report difference between 2 database instances](http://www.liquibase.org/documentation/diff) | 423 | | | `:ref-datasource` |||| 424 | 425 | The switches listed above may either be provided as part of the `opts` map, or 426 | as command-line arguments in `args` as follows: 427 | 428 | | Switch | Long-name example | Short-name example | 429 | |-------------------|-----------------------------|--------------------| 430 | | `:changelog` | `"--changelog=a.schema/cl"` | `"-c=a.schema/cl"` | 431 | | `:chs-count` | `"--chs-count=10"` | `"-n10"` | 432 | | `:contexts` | `"--contexts=foo,bar"` | `"-tfoo,bar"` | 433 | | `:datasource` | `"--datasource=foo.bar/ds"` | `"-dfoo.bar/ds"` | 434 | | `:date` | `"--date=2012-09-16"` | `"-e2012-09-16"` | 435 | | `:output-dir` | `"--output-dir=target/doc"` | `"-otarget/doc"` | 436 | | `:ref-datasource` | `"--ref-datasource=foo/ds"` | `"-rfoo/ds"` | 437 | | `:sql-only` | `"--sql-only"` (no value) | `"-s"` (no value) | 438 | | `:tag` | `"--tag=v0.1.0"` | `"-gv0.1.0"` | 439 | 440 | Please note that `:datasource`, `:changelog` and `:ref-datasource` may point to 441 | var names that would be resolved at runtime to obtain the corresponding values. 442 | 443 | ### Integrating in an app 444 | 445 | The following example shows how to integrate an app with the Clj-Liquibase CLI: 446 | 447 | ```clojure 448 | (ns foo.schema 449 | (:require 450 | [foo.globals :as globals] 451 | [clj-liquibase.change :as ch] 452 | [clj-liquibase.core :as lb] 453 | [clj-liquibase.cli :as cli])) 454 | 455 | ;; assuming that globals/ds is bound to a DataSource 456 | 457 | (defchangelog ch-log "foo" [..change-sets..]) 458 | 459 | (defn -main 460 | [& [cmd & args]] 461 | (apply cli/entry cmd {:datasource globals/ds :changelog ch-log} args)) 462 | ``` 463 | 464 | 465 | You can run this example as follows: 466 | 467 | ```bash 468 | $ lein run -m foo.schema update 469 | $ lein run -m foo.schema tag --tag=v0.1.0 470 | ``` 471 | 472 | ## Core functions 473 | 474 | The CLI commands shown above are implemented via corresponding functions in the 475 | `clj-liquibase.core` namespace listed below: 476 | 477 | * `update` `update-by-count` 478 | * `tag` 479 | * `rollback-to-tag` `rollback-to-date` `rollback-by-count` 480 | * `generate-doc` 481 | * `diff` 482 | 483 | The core functions that implement the commands are supposed to be invoked in 484 | a context where certain dynamic vars are bound to appropriate values. Feel 485 | encouraged to inspect the source code in the namespace `clj-liquibase.core`. 486 | -------------------------------------------------------------------------------- /java-src/clj_liquibase/CustomDBDocVisitor.java: -------------------------------------------------------------------------------- 1 | package clj_liquibase; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.SortedSet; 13 | import java.util.TreeSet; 14 | 15 | import liquibase.change.Change; 16 | import liquibase.changelog.ChangeSet; 17 | import liquibase.changelog.DatabaseChangeLog; 18 | import liquibase.changelog.visitor.ChangeSetVisitor; 19 | import liquibase.database.Database; 20 | import liquibase.snapshot.InvalidExampleException; 21 | import liquibase.snapshot.SnapshotControl; 22 | import liquibase.structure.core.Column; 23 | import liquibase.structure.DatabaseObject; 24 | import liquibase.structure.core.Schema; 25 | import liquibase.structure.core.Table; 26 | import liquibase.dbdoc.AuthorListWriter; 27 | import liquibase.dbdoc.AuthorWriter; 28 | import liquibase.dbdoc.ChangeLogListWriter; 29 | import liquibase.dbdoc.ColumnWriter; 30 | import liquibase.dbdoc.HTMLWriter; 31 | import liquibase.dbdoc.PendingChangesWriter; 32 | import liquibase.dbdoc.PendingSQLWriter; 33 | import liquibase.dbdoc.RecentChangesWriter; 34 | import liquibase.dbdoc.TableListWriter; 35 | import liquibase.dbdoc.TableWriter; 36 | import liquibase.exception.DatabaseException; 37 | import liquibase.exception.DatabaseHistoryException; 38 | import liquibase.exception.LiquibaseException; 39 | import liquibase.resource.ResourceAccessor; 40 | import liquibase.snapshot.DatabaseSnapshot; 41 | import liquibase.snapshot.SnapshotGeneratorFactory; 42 | import liquibase.util.StreamUtil; 43 | 44 | public class CustomDBDocVisitor implements ChangeSetVisitor { 45 | 46 | private Database database; 47 | 48 | private SortedSet changeLogs; 49 | private Map> changesByObject; 50 | private Map> changesByAuthor; 51 | 52 | private Map> changesToRunByObject; 53 | private Map> changesToRunByAuthor; 54 | private List changesToRun; 55 | private List recentChanges; 56 | 57 | private String rootChangeLogName; 58 | private DatabaseChangeLog rootChangeLog; 59 | 60 | private static final int MAX_RECENT_CHANGE = 50; 61 | 62 | public CustomDBDocVisitor(Database database) { 63 | this.database = database; 64 | 65 | changesByObject = new HashMap>(); 66 | changesByAuthor = new HashMap>(); 67 | changeLogs = new TreeSet(); 68 | 69 | changesToRunByObject = new HashMap>(); 70 | changesToRunByAuthor = new HashMap>(); 71 | changesToRun = new ArrayList(); 72 | recentChanges = new ArrayList(); 73 | } 74 | 75 | public ChangeSetVisitor.Direction getDirection() { 76 | return ChangeSetVisitor.Direction.FORWARD; 77 | } 78 | 79 | public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database) throws LiquibaseException { 80 | ChangeSet.RunStatus runStatus = this.database.getRunStatus(changeSet); 81 | if (rootChangeLogName == null) { 82 | rootChangeLogName = changeSet.getFilePath(); 83 | } 84 | 85 | if (rootChangeLog == null) { 86 | this.rootChangeLog = databaseChangeLog; 87 | } 88 | 89 | if (!changesByAuthor.containsKey(changeSet.getAuthor())) { 90 | changesByAuthor.put(changeSet.getAuthor(), new ArrayList()); 91 | } 92 | if (!changesToRunByAuthor.containsKey(changeSet.getAuthor())) { 93 | changesToRunByAuthor.put(changeSet.getAuthor(), new ArrayList()); 94 | } 95 | 96 | boolean toRun = runStatus.equals(ChangeSet.RunStatus.NOT_RAN) || runStatus.equals(ChangeSet.RunStatus.RUN_AGAIN); 97 | for (Change change : changeSet.getChanges()) { 98 | if (toRun) { 99 | changesToRunByAuthor.get(changeSet.getAuthor()).add(change); 100 | changesToRun.add(change); 101 | } else { 102 | changesByAuthor.get(changeSet.getAuthor()).add(change); 103 | recentChanges.add(0, change); 104 | } 105 | } 106 | 107 | 108 | ChangeLogInfo changeLogInfo = new ChangeLogInfo(changeSet.getFilePath(), databaseChangeLog.getPhysicalFilePath()); 109 | if (!changeLogs.contains(changeLogInfo)) { 110 | changeLogs.add(changeLogInfo); 111 | } 112 | 113 | for (Change change : changeSet.getChanges()) { 114 | Set affectedDatabaseObjects = change.getAffectedDatabaseObjects(database); 115 | if (affectedDatabaseObjects != null) { 116 | for (DatabaseObject dbObject : affectedDatabaseObjects) { 117 | if (toRun) { 118 | if (!changesToRunByObject.containsKey(dbObject)) { 119 | changesToRunByObject.put(dbObject, new ArrayList()); 120 | } 121 | changesToRunByObject.get(dbObject).add(change); 122 | } 123 | 124 | if (!changesByObject.containsKey(dbObject)) { 125 | changesByObject.put(dbObject, new ArrayList()); 126 | } 127 | changesByObject.get(dbObject).add(change); 128 | } 129 | } 130 | } 131 | } 132 | 133 | public void writeHTML(File rootOutputDir, ResourceAccessor resourceAccessor) throws IOException, DatabaseException, DatabaseHistoryException, InvalidExampleException { 134 | //ChangeLogWriter changeLogWriter = new ChangeLogWriter(resourceAccessor, rootOutputDir); 135 | HTMLWriter authorWriter = new AuthorWriter(rootOutputDir, database); 136 | HTMLWriter tableWriter = new TableWriter(rootOutputDir, database); 137 | HTMLWriter columnWriter = new ColumnWriter(rootOutputDir, database); 138 | HTMLWriter pendingChangesWriter = new PendingChangesWriter(rootOutputDir, database); 139 | HTMLWriter recentChangesWriter = new RecentChangesWriter(rootOutputDir, database); 140 | HTMLWriter pendingSQLWriter = new PendingSQLWriter(rootOutputDir, database, rootChangeLog); 141 | 142 | copyFile("liquibase/dbdoc/stylesheet.css", rootOutputDir); 143 | copyFile("liquibase/dbdoc/index.html", rootOutputDir); 144 | copyFile("liquibase/dbdoc/globalnav.html", rootOutputDir); 145 | copyFile("liquibase/dbdoc/overview-summary.html", rootOutputDir); 146 | 147 | DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(database.getDefaultSchema(), database, new SnapshotControl(database)); 148 | 149 | new ChangeLogListWriter(rootOutputDir).writeHTML(changeLogs); 150 | new TableListWriter(rootOutputDir).writeHTML(new TreeSet(snapshot.get(Table.class))); 151 | new AuthorListWriter(rootOutputDir).writeHTML(new TreeSet(changesByAuthor.keySet())); 152 | 153 | for (String author : changesByAuthor.keySet()) { 154 | authorWriter.writeHTML(author, changesByAuthor.get(author), changesToRunByAuthor.get(author), rootChangeLogName); 155 | } 156 | 157 | for (Table table : snapshot.get(Table.class)) { 158 | tableWriter.writeHTML(table, changesByObject.get(table), changesToRunByObject.get(table), rootChangeLogName); 159 | } 160 | 161 | for (Column column : snapshot.get(Column.class)) { 162 | columnWriter.writeHTML(column, changesByObject.get(column), changesToRunByObject.get(column), rootChangeLogName); 163 | } 164 | 165 | // for (ChangeLogInfo changeLog : changeLogs) { 166 | // changeLogWriter.writeChangeLog(changeLog.logicalPath, changeLog.physicalPath); 167 | // } 168 | 169 | pendingChangesWriter.writeHTML("index", null, changesToRun, rootChangeLogName); 170 | pendingSQLWriter.writeHTML("sql", null, changesToRun, rootChangeLogName); 171 | 172 | if (recentChanges.size() > MAX_RECENT_CHANGE) { 173 | recentChanges = recentChanges.subList(0, MAX_RECENT_CHANGE); 174 | } 175 | recentChangesWriter.writeHTML("index", recentChanges, null, rootChangeLogName); 176 | 177 | } 178 | 179 | private void copyFile(String fileToCopy, File rootOutputDir) throws IOException { 180 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileToCopy); 181 | FileOutputStream outputStream = null; 182 | try { 183 | if (inputStream == null) { 184 | throw new IOException("Can not find " + fileToCopy); 185 | } 186 | outputStream = new FileOutputStream(new File(rootOutputDir, fileToCopy.replaceFirst(".*\\/", "")), false); 187 | StreamUtil.copy(inputStream, outputStream); 188 | } finally { 189 | if (outputStream != null) { 190 | outputStream.close(); 191 | } 192 | } 193 | } 194 | 195 | private static class ChangeLogInfo implements Comparable { 196 | public String logicalPath; 197 | public String physicalPath; 198 | 199 | 200 | private ChangeLogInfo(String logicalPath, String physicalPath) { 201 | this.logicalPath = logicalPath; 202 | this.physicalPath = physicalPath; 203 | } 204 | 205 | @Override 206 | public boolean equals(Object o) { 207 | if (this == o) return true; 208 | if (o == null || getClass() != o.getClass()) return false; 209 | 210 | ChangeLogInfo that = (ChangeLogInfo) o; 211 | 212 | return logicalPath.equals(that.logicalPath); 213 | 214 | } 215 | 216 | @Override 217 | public int hashCode() { 218 | return logicalPath.hashCode(); 219 | } 220 | 221 | public int compareTo(ChangeLogInfo o) { 222 | return this.logicalPath.compareTo(o.logicalPath); 223 | } 224 | 225 | @Override 226 | public String toString() { 227 | return logicalPath; 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-liquibase "0.6.0" 2 | :description "Clojure wrapper for Liquibase" 3 | :url "https://github.com/kumarshantanu/clj-liquibase" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :mailing-list {:name "Bitumen Framework discussion group" 7 | :archive "https://groups.google.com/group/bitumenframework" 8 | :other-archives ["https://groups.google.com/group/clojure"] 9 | :post "bitumenframework@googlegroups.com"} 10 | :java-source-paths ["java-src"] 11 | :javac-options {:destdir "target/classes/" 12 | :source "1.6" 13 | :target "1.6"} 14 | :dependencies [[org.liquibase/liquibase-core "3.0.8"] 15 | [liquibase-edn "3.0.8-0.1.1"] 16 | [clj-jdbcutil "0.1.0"] 17 | [clj-miscutil "0.4.1"]] 18 | :profiles {:dev {:dependencies [[oss-jdbc "0.8.0"] 19 | [clj-dbcp "0.8.1"]]} 20 | :1.3 {:dependencies [[org.clojure/clojure "1.3.0"]]} 21 | :1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]} 22 | :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} 23 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 24 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]] 25 | :global-vars {*unchecked-math* :warn-on-boxed}} 26 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0-RC2"]] 27 | :global-vars {*unchecked-math* :warn-on-boxed}}} 28 | :aliases {"dev" ["with-profile" "dev,1.7"] 29 | "all" ["with-profile" "dev,1.3:dev,1.4:dev,1.5:dev,1.6:dev,1.7:dev,1.8"]} 30 | :global-vars {*warn-on-reflection* true} 31 | :min-lein-version "2.0.0" 32 | :jvm-opts ["-Xmx1g"]) 33 | -------------------------------------------------------------------------------- /src/clj_liquibase/cli.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.cli 2 | (:refer-clojure :exclude [update]) 3 | (:require 4 | [clojure.java.io :as io] 5 | [clojure.string :as sr] 6 | [clojure.pprint :as pp] 7 | [clj-miscutil.core :as mu] 8 | [clj-jdbcutil.core :as sp] 9 | [clj-liquibase.core :as lb] 10 | [clj-liquibase.change :as ch]) 11 | (:import 12 | (java.util.regex Pattern) 13 | (javax.sql DataSource))) 14 | 15 | 16 | (def available-commands [{:cli-string "dbdoc" 17 | :short-desc "Generates documentation for database/changelogs"} 18 | {:cli-string "diff" 19 | :short-desc "Reports differences between two database instances"} 20 | {:cli-string "help" 21 | :short-desc "Shows this help screen"} 22 | {:cli-string "rollback" 23 | :short-desc "Rolls back database"} 24 | {:cli-string "tag" 25 | :short-desc "Tags the database"} 26 | {:cli-string "update" 27 | :short-desc "Updates the database"} 28 | {:cli-string "version" 29 | :short-desc "Shows clj-liquibase version"}]) 30 | 31 | 32 | (defn help 33 | [] 34 | (println "The following commands are available 35 | ") 36 | (doseq [command available-commands] 37 | (printf "%-12s - %s\n" (:cli-string command) (:short-desc command))) 38 | (println " 39 | For help on individual command, append with `--help`, e.g.: 40 | update --help")) 41 | 42 | 43 | (defn as-string 44 | ^String [s] 45 | (if (keyword? s) (name s) 46 | (str s))) 47 | 48 | 49 | (defn opt? 50 | [^String s] {:pre [(string? s)]} 51 | (some #(re-matches % s) [(re-pattern "--.+") 52 | (re-pattern "-.+")])) 53 | 54 | 55 | (defn opt-string 56 | ([^String elem] {:post [(string? %)] 57 | :pre [(string? elem)]} 58 | (format (if (> (count elem) 1) 59 | "--%s" 60 | "-%s") 61 | elem)) 62 | ([^String elem ^String value] 63 | (format (if (> (count elem) 1) 64 | "--%s=%s" 65 | "-%s%s") 66 | elem value))) 67 | 68 | 69 | (defn opt-pattern 70 | [^String elem] {:post [(instance? Pattern %)] 71 | :pre [(string? elem)]} 72 | (re-pattern (opt-string elem "(.*)"))) 73 | 74 | 75 | (defn opt-match-value 76 | "Return option value 77 | Example: 78 | (opt-match-value (opt-pattern \"foo\") \"--foo=bar\") 79 | => returns \"bar\" 80 | See also: opt-pattern" 81 | [^Pattern re ^String arg] 82 | (second (re-matches re arg))) 83 | 84 | 85 | (defn noarg-pattern 86 | [^String elem] {:pre [(string? elem)]} 87 | (-> (if (> (count elem) 1) "--%s" "-%s") 88 | (format elem) 89 | re-pattern)) 90 | 91 | 92 | (def arg-types #{:with-arg :opt-arg :no-arg}) 93 | 94 | 95 | (defn print-usage 96 | "Print command usage" 97 | [cmd-prefix spec] 98 | (println "Usage: " cmd-prefix "\n") 99 | (mu/print-table 100 | ["Option" "Must" "Description"] 101 | (map (fn [row] 102 | (let [[desc opt-type & keywds] row 103 | takes-arg (contains? #{:with-arg :opt-arg} opt-type) 104 | ks (map #(if takes-arg 105 | (opt-string (as-string %) "") 106 | (opt-string (as-string %))) 107 | keywds)] 108 | [(mu/comma-sep-str ks) 109 | (if (= :with-arg opt-type) "Yes" "...") 110 | desc])) 111 | spec)) 112 | (println)) 113 | 114 | 115 | (defn assert-spec 116 | "Assert spec as a spec" 117 | [spec-coll] 118 | (assert (or (nil? spec-coll) (coll? spec-coll))) 119 | (doseq [each spec-coll] 120 | (assert (coll? each)) 121 | (let [[docstr argtype] each] 122 | (assert (string? docstr)) 123 | (assert (arg-types argtype)))) 124 | true) 125 | 126 | 127 | (defn parse-opts 128 | "Spec can be: 129 | [[docstring :opt-arg :datasource :d] 130 | [docstring :no-arg :sql-only :s] 131 | [docstring :with-arg :a]] 132 | `args` is a collection of argument bodies: 133 | \"--foo=bar\" \"-fbar\" \"--simulate\" \"-s\" 134 | Note: Evaluated every time" 135 | [opts cmd-prefix args & spec] 136 | {:post [(map? %)] 137 | :pre [(map? opts) 138 | (assert-spec spec)]} 139 | (let [spec-opts (map #(map as-string (drop 2 %)) spec) 140 | rev-opts (->> spec-opts 141 | (map (fn [opt-row] 142 | (let [sentinel (keyword (first opt-row))] 143 | (map #(array-map % sentinel) opt-row)))) 144 | flatten 145 | (reduce into {})) 146 | with-arg (map (partial drop 2) (filter #(= (second %) :with-arg) spec)) 147 | opt-arg (map (partial drop 2) (filter #(= (second %) :opt-arg) spec)) 148 | no-arg (map (partial drop 2) (filter #(= (second %) :no-arg) spec)) 149 | ;; fn to convert arg into map entries 150 | get-opts (fn [acc arg] {:post [(map? %)] 151 | :pre [(map? acc) 152 | (string? arg)]} 153 | (or 154 | ;; with-arg and opt-arg 155 | (some (fn [row] 156 | (some #(let [v (-> (as-string %) 157 | opt-pattern ;; FIXME with-arg opt-pattern should accompany '=(.*)'? 158 | (opt-match-value arg))] 159 | (and v 160 | (into acc 161 | {(get rev-opts (as-string %)) v}))) 162 | row)) 163 | (into with-arg opt-arg)) 164 | ;; no-arg 165 | (some (fn [row] 166 | (some (fn [opt] 167 | (if (opt? arg) 168 | (if (re-matches (noarg-pattern 169 | (as-string opt)) arg) 170 | (into acc 171 | {(get rev-opts (as-string opt)) nil})) 172 | (if (contains? acc :more) 173 | {:more [arg]}))) 174 | row)) 175 | no-arg) 176 | ;; special or bad args 177 | (if (some #(= arg %) ["--help" "-h" "/?"]) 178 | (do (print-usage cmd-prefix spec) 179 | {:help nil}) 180 | (into acc {:more (cons arg (:more acc))})) 181 | (throw (IllegalArgumentException. 182 | (str "Illegal option: " arg)))))] 183 | (let [opt-map (reduce get-opts {} args) 184 | with-arg? (fn [] 185 | (-> (fn [row] 186 | (or (contains? opts (first row)) 187 | (-> (fn [opt] 188 | (-> #(re-matches 189 | (opt-pattern (as-string opt)) 190 | (as-string %)) 191 | (some args))) 192 | (some row)))) 193 | (every? with-arg)))] 194 | (cond 195 | ;; ignore validations if help was sought 196 | (contains? 197 | opt-map :help) opt-map 198 | ;; ensure that `with-arg` options are supplied 199 | (not (with-arg?)) (let [optfn #(let [x (as-string %)] 200 | (if (> (count x) 1) 201 | (str "--" x) (str "-" x))) 202 | optsr #(format "Either of %s\n" 203 | (mu/comma-sep-str (map optfn %)))] 204 | (throw (IllegalArgumentException. 205 | (str "Must supply the following:\n" 206 | (apply str (map optsr with-arg)))))) 207 | :else (merge opts opt-map))))) 208 | 209 | 210 | (defn resolve-var 211 | "Given a qualified/un-qualified var name (string), resolve and return value. 212 | Throw NullPointerException if var cannot be resolved." 213 | [^String var-name] {:pre [(string? var-name)]} 214 | @(let [tokens (sr/split var-name #"/") 215 | var-ns (first tokens)] 216 | (when (and (> (count tokens) 1) 217 | (not (find-ns (symbol var-ns)))) 218 | (require (symbol var-ns))) 219 | (resolve (symbol var-name)))) 220 | 221 | 222 | (defn opt-value 223 | [k opt & opts] {:pre [(map? opt) 224 | (every? #(or (map? %) (nil? %)) opts)]} 225 | (-> #(when (contains? % k) 226 | [(get % k)]) 227 | (some (concat [opt] opts)) 228 | first)) 229 | 230 | 231 | (defn opt-datasource 232 | [opt & opts] 233 | (when-let [ds (apply opt-value :datasource opt opts)] 234 | (cond (string? ds) (resolve-var ds) 235 | (symbol? ds) (resolve-var (name ds)) 236 | :otherwise ds))) 237 | 238 | 239 | (defn opt-changelog 240 | [clog] 241 | (if (string? clog) 242 | (resolve-var clog) 243 | clog)) 244 | 245 | 246 | (defn ctx-list 247 | "Generate context list from a given comma-separated context list (string)" 248 | [contexts] {:post [(vector? %)] 249 | :pre [(or (nil? contexts) 250 | (string? contexts))]} 251 | (if contexts 252 | (sr/split contexts #",") 253 | [])) 254 | 255 | 256 | (defn parse-update-args 257 | [opts & args] 258 | (parse-opts opts "update" 259 | args 260 | ["JDBC Datasource" :with-arg :datasource :d] 261 | ["Changelog var name to apply update on" :with-arg :changelog :c] 262 | ["How many Changesets to apply update on" :opt-arg :chs-count :n] 263 | ["Contexts (comma separated)" :opt-arg :contexts :t] 264 | ["Only generate SQL, do not commit" :no-arg :sql-only :s])) 265 | 266 | 267 | (defn update 268 | [opts & args] {:pre [(map? opts)]} 269 | (let [opt (apply parse-update-args opts args)] 270 | (when-not (contains? opt :help) 271 | (let [changelog (opt-changelog (:changelog opt)) 272 | chs-count (:chs-count opt) 273 | contexts (:contexts opt) 274 | sql-only (contains? opt :sql-only) 275 | datasource (opt-datasource opts opt)] 276 | (sp/with-connection {:datasource datasource} 277 | (lb/with-lb 278 | (if chs-count 279 | (let [chs-num (Integer/parseInt chs-count)] 280 | (if sql-only 281 | (lb/update-by-count changelog chs-num (ctx-list contexts) *out*) 282 | (lb/update-by-count changelog chs-num (ctx-list contexts)))) 283 | (if sql-only 284 | (lb/update changelog (ctx-list contexts) *out*) 285 | (lb/update changelog (ctx-list contexts)))))))))) 286 | 287 | 288 | (defn parse-rollback-args 289 | [opts & args] 290 | (parse-opts opts "rollback" 291 | args 292 | ["JDBC Datasource" :with-arg :datasource :d] 293 | ["Changelog var name to apply rollback on" :with-arg :changelog :c] 294 | ["How many Changesets to rollback" :opt-arg :chs-count :n] 295 | ["Which tag to rollback to" :opt-arg :tag :g] 296 | ["Rollback ISO-date (yyyy-MM-dd'T'HH:mm:ss)" :opt-arg :date :e] 297 | ["Contexts (comma separated)" :opt-arg :contexts :t] 298 | ["Only generate SQL, do not commit" :no-arg :sql-only :s])) 299 | 300 | 301 | (defn rollback 302 | [opts & args] 303 | (let [opt (apply parse-rollback-args opts args)] 304 | (when-not (contains? opt :help) 305 | (let [changelog (opt-changelog (:changelog opt)) 306 | chs-count (:chs-count opt) 307 | tag (:tag opt) 308 | date (:date opt) 309 | c-t-d [chs-count tag date] ; either of 3 is required 310 | contexts (:contexts opt) 311 | sql-only (contains? opt :sql-only) 312 | datasource (opt-datasource opts opt)] 313 | (when (not (= 1 (count (filter identity c-t-d)))) 314 | (throw 315 | (IllegalArgumentException. 316 | (format 317 | "Expected only either of --chs-count/-n, --tag/-g and --date/-d 318 | arguments, but found %s" 319 | (with-out-str (pp/pprint args)))))) 320 | (sp/with-connection {:datasource datasource} 321 | (lb/with-lb 322 | (cond 323 | chs-count (let [chs-num (Integer/parseInt chs-count)] 324 | (if sql-only 325 | (lb/rollback-by-count changelog chs-num (ctx-list contexts) *out*) 326 | (lb/rollback-by-count changelog chs-num (ctx-list contexts)))) 327 | tag (if sql-only 328 | (lb/rollback-to-tag changelog tag (ctx-list contexts) *out*) 329 | (lb/rollback-to-tag changelog tag (ctx-list contexts))) 330 | date (if sql-only 331 | (lb/rollback-to-date changelog (ch/iso-date date) (ctx-list contexts) *out*) 332 | (lb/rollback-to-date changelog (ch/iso-date date) (ctx-list contexts))) 333 | :else (throw 334 | (IllegalStateException. 335 | (format 336 | "Neither of changeset-count, tag and date found to 337 | roll back to: %s" 338 | (with-out-str (pp/pprint args)))))))))))) 339 | 340 | 341 | (defn parse-tag-args 342 | [opts & args] 343 | (parse-opts opts "tag" 344 | args 345 | ["JDBC Datasource" :with-arg :datasource :d] 346 | ["Tag name to apply" :with-arg :tag :g])) 347 | 348 | 349 | (defn tag 350 | "Tag the database manually (recommended: create a Change object of type tag)" 351 | [opts & args] 352 | (let [opt (apply parse-tag-args opts args)] 353 | (when-not (contains? opt :help) 354 | (let [tag (:tag opt) 355 | datasource (opt-datasource opts opt)] 356 | (sp/with-connection {:datasource datasource} 357 | (lb/with-lb 358 | (lb/tag tag))))))) 359 | 360 | 361 | (defn parse-dbdoc-args 362 | "Parse arguments for `dbdoc` command." 363 | [opts & args] 364 | (parse-opts opts "dbdoc" 365 | args 366 | ["JDBC Datasource" :with-arg :datasource :d] 367 | ["Changelog var name to apply tag on" :with-arg :changelog :c] 368 | ["Output directory to generate doc files into" :with-arg :output-dir :o] 369 | ["Contexts (comma separated)" :opt-arg :contexts :t])) 370 | 371 | 372 | (defn dbdoc 373 | "Generate database/changelog documentation" 374 | [opts & args] 375 | (let [opt (apply parse-dbdoc-args opts args)] 376 | (when-not (contains? opt :help) 377 | (let [changelog (opt-changelog (:changelog opt)) 378 | out-dir (:output-dir opt) 379 | contexts (:contexts opt) 380 | datasource (opt-datasource opts opt)] 381 | (sp/with-connection {:datasource datasource} 382 | (lb/with-lb 383 | (lb/generate-doc changelog out-dir (ctx-list contexts)))))))) 384 | 385 | 386 | (defn parse-diff-args 387 | "Parse arguments for `diff` command." 388 | [opts & args] 389 | (parse-opts opts "diff" 390 | args 391 | ["JDBC Datasource" :with-arg :datasource :d] 392 | ["Reference JDBC Datasource" :with-arg :ref-datasource :r])) 393 | 394 | 395 | (defn opt-ref-datasource 396 | [opt & opts] 397 | (when-let [ds (apply opt-value :ref-datasource opt opts)] 398 | (cond (string? ds) (resolve-var ds) 399 | (symbol? ds) (resolve-var (name ds)) 400 | :otherwise ds))) 401 | 402 | 403 | (defn diff 404 | "Report differences between two database instances" 405 | [opts & args] 406 | (let [opt (apply parse-diff-args opts args)] 407 | (when-not (contains? opt :help) 408 | (let [ref-datasource (opt-ref-datasource opts opt)] 409 | ;; begin with reference DB profile 410 | (sp/with-connection 411 | (merge sp/*dbspec* {:datasource ref-datasource :connection nil}) 412 | (let [ref-db (lb/make-db-instance (:connection sp/*dbspec*)) 413 | datasource (opt-datasource opts opt)] 414 | ;; go on to target DB profile 415 | (sp/with-connection 416 | {:datasource datasource :connection nil} 417 | (lb/with-lb 418 | (lb/diff ref-db))))))))) 419 | 420 | 421 | (defn entry 422 | "Entry point for clj-liquibase CLI" 423 | [cmd opts & args] 424 | (let [argc (count args) 425 | call #(apply % opts args)] 426 | ;; check for commands 427 | (case cmd 428 | "help" (help) 429 | "version" (println (format "clj-liquibase version %s" 430 | (apply str (interpose "." lb/version)))) 431 | "update" (call update) ; :datasource :changelog :chs-count :contexts :sql-only 432 | "rollback" (call rollback) ; :datasource :changelog :chs-count :tag :date :contexts :sql-only 433 | "tag" (call tag) ; :datasource :tag 434 | "dbdoc" (call dbdoc) ; :datasource :changelog :output-dir :contexts 435 | "diff" (call diff) ; :datasource :ref-datasource 436 | (do 437 | (println (format "Invalid command: %s" cmd)) 438 | (help))))) 439 | -------------------------------------------------------------------------------- /src/clj_liquibase/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.core 2 | "Expose functions from the Liquibase library. 3 | See also: 4 | http://www.liquibase.org/documentation/index.html" 5 | (:refer-clojure :exclude [update]) 6 | (:require 7 | [clojure.string :as sr] 8 | [clj-jdbcutil.core :as sp] 9 | [clj-miscutil.core :as mu] 10 | [clj-liquibase.internal :as in] 11 | [clj-liquibase.change :as ch] 12 | [clj-liquibase.precondition :as pc] 13 | [clj-liquibase.sql-visitor :as vis]) 14 | (:import 15 | (java.io File IOException Writer) 16 | (java.sql Connection) 17 | (java.text DateFormat) 18 | (java.util Date List) 19 | (javax.sql DataSource) 20 | (clj_liquibase CustomDBDocVisitor) 21 | (liquibase.changelog ChangeLogIterator ChangeSet ChangeLogParameters 22 | DatabaseChangeLog) 23 | (liquibase.change Change) 24 | (liquibase.change.core CreateTableChange) 25 | (liquibase.changelog.filter AfterTagChangeSetFilter AlreadyRanChangeSetFilter 26 | ChangeSetFilter ContextChangeSetFilter 27 | CountChangeSetFilter DbmsChangeSetFilter 28 | ExecutedAfterChangeSetFilter ShouldRunChangeSetFilter) 29 | (liquibase.changelog.visitor DBDocVisitor RollbackVisitor UpdateVisitor) 30 | (liquibase.database Database DatabaseFactory) 31 | (liquibase.database.jvm JdbcConnection) 32 | (liquibase.executor Executor ExecutorService LoggingExecutor) 33 | (liquibase.exception LiquibaseException LockException) 34 | (liquibase.integration.commandline CommandLineUtils) 35 | (liquibase.lockservice LockService LockServiceFactory) 36 | (liquibase.logging LogFactory Logger) 37 | (liquibase.parser ChangeLogParser ChangeLogParserFactory) 38 | (liquibase.precondition Precondition) 39 | (liquibase.precondition.core PreconditionContainer) 40 | (liquibase.resource ClassLoaderResourceAccessor FileSystemResourceAccessor ResourceAccessor) 41 | (liquibase.sql Sql) 42 | (liquibase.sql.visitor SqlVisitor) 43 | (liquibase.sqlgenerator SqlGeneratorFactory) 44 | (liquibase.statement SqlStatement) 45 | (liquibase.util LiquibaseUtil))) 46 | 47 | 48 | (def ^{:doc "Clj-Liquibase version"} 49 | version [0 5 1]) 50 | 51 | 52 | ;; ===== Dynamic vars for Integration ===== 53 | 54 | 55 | (def ^{:doc "Logical filepath use by ChangeSet and ChangeLog instances." 56 | :tag String 57 | :dynamic true} 58 | *logical-filepath* nil) 59 | 60 | 61 | (def ^{:doc "Database (liquibase.database.Database) instance." 62 | :tag Database 63 | :dynamic true} 64 | *db-instance* nil) 65 | 66 | 67 | (def ^{:doc "Changelog params (liquibase.changelog.ChangeLogParameters) instance." 68 | :tag ChangeLogParameters 69 | :dynamic true} 70 | *changelog-params* nil) 71 | 72 | 73 | (defn verify-valid-logical-filepath 74 | "Verify whether the *logical-filepath* var has a valid value. Return true if 75 | all OK, throw IllegalStateException otherwise." 76 | [] 77 | (when (not (string? *logical-filepath*)) 78 | (throw (IllegalStateException. 79 | ^String (format 80 | "Expected %s but found %s - not wrapped in 'defchangelog'?" 81 | "var *logical-filepath* to be string" 82 | (mu/val-dump *logical-filepath*))))) 83 | true) 84 | 85 | 86 | ;; ===== ChangeSet ===== 87 | 88 | 89 | (defn changeset? 90 | "Return true if specified argument is a liquibase.changelog.ChangeSet 91 | instance, false otherwise." 92 | [x] 93 | (instance? ChangeSet x)) 94 | 95 | 96 | (defn make-changeset 97 | "Return a ChangeSet instance. Use MySQL InnoDB for `create-table` changes by 98 | default (unless overridden by :visitors argument.) 99 | Arguments: 100 | id (String) Author-assigned ID, which can be sequential 101 | author (String) Author name (must be kept same across changesets) 102 | changes (collection) List of Change objects 103 | Optional arguments: 104 | :dbms ; String/Keyword/vector-of-multiple 105 | :run-always :always ; Boolean 106 | :run-on-change :on-change ; Boolean 107 | :context :ctx ; String 108 | :run-in-transaction :in-txn ; Boolean (true by default) 109 | :fail-on-error :fail-err ; Boolean 110 | ;; sub tags 111 | :comment ; String 112 | :pre-conditions :pre-cond ; list of Precondition objects, or PreconditionContainer object 113 | :valid-checksum :valid-csum ; String 114 | :visitors ; list of SqlVisitor objects 115 | See also: 116 | http://www.liquibase.org/documentation/changeset" 117 | ^ChangeSet 118 | [^String id ^String author ^List changes 119 | & {:keys [logical-filepath filepath 120 | dbms 121 | run-always always 122 | run-on-change on-change 123 | context ctx 124 | run-in-transaction in-txn 125 | fail-on-error fail-err 126 | comment 127 | pre-conditions pre-cond 128 | rollback-changes rollback 129 | valid-checksum valid-csum 130 | visitors 131 | ] :as opt}] {:post [(instance? ChangeSet %)] 132 | :pre [(mu/verify-opt #{:logical-filepath :filepath 133 | :dbms 134 | :run-always :always 135 | :run-on-change :on-change 136 | :context :ctx 137 | :run-in-transaction :in-txn 138 | :fail-on-error :fail-err 139 | :comment 140 | :pre-conditions :pre-cond 141 | :rollback-changes :rollback 142 | :valid-checksum :valid-csum 143 | :visitors} opt) 144 | (mu/verify-arg (string? id)) 145 | (mu/verify-arg (string? author)) 146 | (mu/verify-arg (coll? changes)) 147 | (mu/verify-arg (mu/not-empty? changes)) 148 | (mu/verify-arg (every? vis/visitor? visitors))]} 149 | (when-not (or logical-filepath filepath) 150 | (verify-valid-logical-filepath)) 151 | (let [s-filepath (or logical-filepath filepath *logical-filepath*) 152 | s-dbms (in/as-dbident-names dbms) 153 | b-always (or run-always always false) 154 | b-change (or run-on-change on-change false) 155 | s-contxt (or context ctx) 156 | b-in-txn (let [x (or run-in-transaction in-txn)] 157 | (if (nil? x) true (or x false))) 158 | b-fail-err (or fail-on-error fail-err false) 159 | ;; sub tags 160 | s-comment comment 161 | v-pre-cond (or pre-conditions pre-cond) 162 | v-rollback (or rollback-changes rollback) 163 | s-val-csum (or valid-checksum valid-csum) 164 | v-visitors (or visitors (if (every? #(instance? CreateTableChange %) 165 | changes) 166 | [vis/mysql-innodb] 167 | [])) 168 | _ (do 169 | (mu/verify-arg (string? id)) 170 | (mu/verify-arg (string? author)) 171 | (mu/verify-arg (string? s-filepath)) 172 | (mu/verify-arg (mu/not-empty? changes)) 173 | (doseq [each changes] 174 | (mu/verify-arg (instance? Change each))) 175 | (mu/verify-arg (mu/boolean? b-always)) 176 | (mu/verify-arg (mu/boolean? b-change)) 177 | (mu/verify-arg (or (nil? s-contxt) (string? s-contxt))) 178 | (mu/verify-arg (string? s-dbms)) 179 | (mu/verify-arg (mu/boolean? b-in-txn)) 180 | (mu/verify-arg (or (nil? v-pre-cond) 181 | (instance? PreconditionContainer v-pre-cond) 182 | (and (coll? v-pre-cond) 183 | (every? #(instance? Precondition %) v-pre-cond))))) 184 | ;; String id, String author, boolean alwaysRun, boolean runOnChange, 185 | ;; String filePath, String contextList, String dbmsList, boolean runInTransaction 186 | _ (do (println b-always "@" b-change "@" b-in-txn)) 187 | c-set (ChangeSet. 188 | ^String id ^String author ^Boolean b-always ^Boolean b-change 189 | ^String (mu/java-filepath s-filepath) 190 | ^String s-contxt ^String s-dbms ^Boolean b-in-txn (DatabaseChangeLog. ""))] 191 | (doseq [each changes] 192 | (.addChange c-set each)) 193 | (if b-fail-err (.setFailOnError c-set b-fail-err)) 194 | (if s-comment (.setComments c-set s-comment)) 195 | (if v-pre-cond (.setPreconditions c-set (if (coll? v-pre-cond) 196 | (pc/pre-cond v-pre-cond) 197 | v-pre-cond))) 198 | (if v-rollback (doseq [each (mu/as-vector v-rollback)] 199 | (if (string? each) (.addRollBackSQL c-set ^String each) 200 | (.addRollbackChange c-set ^Change each)))) 201 | (if s-val-csum (doseq [each (mu/as-vector s-val-csum)] 202 | (.addValidCheckSum c-set each))) 203 | (doseq [each v-visitors] 204 | (.addSqlVisitor c-set ^SqlVisitor each)) 205 | c-set)) 206 | 207 | 208 | ;; ===== DatabaseChangeLog helpers ===== 209 | 210 | 211 | (defn make-db-instance 212 | "Return a Database instance for current connection." 213 | ^Database [^Connection conn] 214 | ;; DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(conn)) 215 | (.findCorrectDatabaseImplementation (DatabaseFactory/getInstance) 216 | (JdbcConnection. conn))) 217 | 218 | 219 | (defn make-changelog-params 220 | "Return a ChangeLogParameters instance." 221 | ^ChangeLogParameters 222 | [^Database db-instance 223 | & {:keys [contexts ; list of string 224 | ]}] 225 | (let [clp (ChangeLogParameters. db-instance)] 226 | (doseq [each (mu/as-vector contexts)] 227 | (.addContext clp (mu/as-string each))) 228 | clp)) 229 | 230 | 231 | ;; ===== Integration ===== 232 | 233 | 234 | (defmacro with-lb 235 | "Execute body of code in the context of initialized Liquibase settings." 236 | [& body] 237 | `(do (assert (:connection sp/*dbspec*)) 238 | (if (mu/not-nil? *db-instance*) (do ~@body) 239 | (binding [*db-instance* (make-db-instance (:connection sp/*dbspec*)) 240 | *changelog-params* (make-changelog-params *db-instance*)] 241 | ~@body)))) 242 | 243 | 244 | ;; ===== DatabaseChangeLog ===== 245 | 246 | 247 | (defn changelog? 248 | "Return true if specified argument is a liquibase.changelog.DatabaseChangeLog 249 | instance, false otherwise." 250 | [x] 251 | (instance? DatabaseChangeLog x)) 252 | 253 | 254 | (defn parse-changelog 255 | "Return a DatabaseChangeLog instance. 256 | Arguments: 257 | filepath - name of the changelog file 258 | Optional args: 259 | :source - either :classpath (default) or :filesystem" 260 | {:added "0.6.0"} 261 | ^DatabaseChangeLog 262 | ([^String filepath] 263 | (parse-changelog filepath {})) 264 | ([^String filepath {:keys [source] 265 | :or {source :classpath} 266 | :as options}] 267 | (let [^ResourceAccessor ra (case source 268 | :filesystem (FileSystemResourceAccessor.) 269 | :classpath (ClassLoaderResourceAccessor. 270 | (.getContextClassLoader (Thread/currentThread))) 271 | (if (instance? ResourceAccessor source) 272 | source 273 | (throw (IllegalArgumentException. 274 | (str "Expected source argument to be :filesystem, :classpath or a valid" 275 | " liquibase.resource.ResourceAccessor instance. but found (" 276 | (class source) ") " (pr-str source)))))) 277 | ^ChangeLogParser parser (.getParser (ChangeLogParserFactory/getInstance) filepath ra)] 278 | (.parse parser filepath ^ChangeLogParameters *changelog-params* ra)))) 279 | 280 | 281 | (defmacro defparser 282 | "Define a parser for a changelog file, typically a XML, YAML, JSON or EDN file." 283 | {:added "0.6.0"} 284 | ([var-name filepath] 285 | `(def ~var-name (partial parse-changelog ~filepath))) 286 | ([var-name filepath options] 287 | `(def ~var-name (partial parse-changelog ~filepath ~options)))) 288 | 289 | 290 | (defn make-changelog 291 | "DEPRECATED: Use 'parse-changelog' instead. 292 | Return a DatabaseChangeLog instance. 293 | Arguments: 294 | change-sets (collection/list) List of ChangeSet instances, or 295 | List of arg-lists (for 'make-changeset' fn) 296 | Optional args: 297 | :pre-conditions :pre-cond ; PreconditionContainer object, or list of Precondition objects 298 | See also: 299 | http://www.liquibase.org/documentation/databasechangelog 300 | make-changelog-params" 301 | ^DatabaseChangeLog 302 | {:deprecated "0.6.0"} 303 | [^String filepath ^List change-sets 304 | & {:keys [pre-conditions pre-cond ; vector 305 | ] :as opt}] {:post [(instance? DatabaseChangeLog %)] 306 | :pre [(mu/verify-opt #{:pre-conditions :pre-cond} opt) 307 | (mu/verify-arg (string? filepath)) 308 | (mu/verify-arg (coll? change-sets)) 309 | (mu/verify-arg (mu/not-empty? change-sets))]} 310 | (let [dbcl (DatabaseChangeLog.) 311 | v-pre-cond (or pre-conditions pre-cond) 312 | _ (mu/verify-arg 313 | (or (nil? v-pre-cond) 314 | (instance? PreconditionContainer v-pre-cond) 315 | (and (coll? v-pre-cond) 316 | (every? #(instance? Precondition %) v-pre-cond))))] 317 | (doto dbcl 318 | (.setLogicalFilePath (mu/java-filepath filepath)) 319 | (.setChangeLogParameters ^ChangeLogParameters *changelog-params*)) 320 | (doseq [each change-sets] 321 | (binding [*logical-filepath* (mu/java-filepath filepath)] 322 | (cond 323 | (changeset? each) (.addChangeSet dbcl ^ChangeSet each) 324 | (and (coll? each) 325 | (not (map? each))) (.addChangeSet dbcl 326 | ^ChangeSet (apply make-changeset each)) 327 | :else 328 | (mu/illegal-argval "change-sets#element" 329 | "ChangeSet object or arg-lists for 'make-changeset' fn" 330 | each)))) 331 | (if v-pre-cond 332 | (.setPreconditions dbcl 333 | ^PreconditionContainer (if (coll? v-pre-cond) (pc/pre-cond v-pre-cond) 334 | v-pre-cond))) 335 | dbcl)) 336 | 337 | 338 | (defmacro defchangelog 339 | "DEPRECATED: Use 'defparser' instead. 340 | Define a function that when executed with no arguments, returns a database 341 | changelog (DatabaseChangeLog instance) while binding *logical-filepath* to 342 | `logical-schema`. 343 | See also: 344 | make-changelog" 345 | {:deprecated "0.6.0"} 346 | [var-name logical-schema change-sets & var-args] {:pre [(symbol? var-name)]} 347 | `(def ~var-name 348 | (partial make-changelog ~logical-schema ~change-sets ~@var-args))) 349 | 350 | 351 | ;; ===== Actions helpers ===== 352 | 353 | 354 | (def ^{:doc "Liquibase logger" 355 | :tag Logger} 356 | log (LogFactory/getLogger)) 357 | 358 | (defn check-database-changelog-table 359 | "Check database changelog table. 360 | See also: 361 | liquibase.Liquibase/checkDatabaseChangeLogTable" 362 | [^Database db ^Boolean update-existing-null-checksums 363 | ^DatabaseChangeLog db-changelog contexts] 364 | (when (and update-existing-null-checksums (nil? db-changelog)) 365 | (throw 366 | (LiquibaseException. 367 | "'db-changelog' parameter is required if updating existing checksums"))) 368 | (.checkDatabaseChangeLogTable db 369 | update-existing-null-checksums db-changelog (into-array String contexts)) 370 | (when (not (.hasChangeLogLock (.getLockService (LockServiceFactory/getInstance) db))) 371 | (.checkDatabaseChangeLogLockTable db))) 372 | 373 | 374 | (defn make-changelog-iterator 375 | "Return a ChangeLogIterator instance. 376 | See also: 377 | liquibase.Liquibase/getStandardChangelogIterator" 378 | (^ChangeLogIterator [^DatabaseChangeLog changelog ^List changeset-filters] 379 | (ChangeLogIterator. changelog 380 | (into-array ChangeSetFilter changeset-filters))) 381 | (^ChangeLogIterator [^List ran-changesets ^DatabaseChangeLog changelog ^List changeset-filters] 382 | (ChangeLogIterator. ran-changesets changelog 383 | (into-array ChangeSetFilter 384 | changeset-filters)))) 385 | 386 | 387 | (defn get-db-executor 388 | ^Executor [] 389 | (let [ex (ExecutorService/getInstance)] 390 | (.getExecutor ex *db-instance*))) 391 | 392 | 393 | (defn output-header 394 | [^String message] 395 | (let [ex (get-db-executor)] 396 | (doto ex 397 | (.comment "*********************************************************************") 398 | (.comment message) 399 | (.comment "*********************************************************************") 400 | (.comment (format "Change Log: %s" *file*)) ; TODO get the logical filename 401 | (.comment (format "Ran at: %s" (.format (DateFormat/getDateTimeInstance 402 | DateFormat/SHORT DateFormat/SHORT) 403 | (Date.)))) 404 | (.comment (format "Against: %s@%s" 405 | (-> *db-instance* .getConnection .getConnectionUserName) 406 | (-> *db-instance* .getConnection .getURL))) 407 | (.comment (format "Liquibase version: %s" 408 | (LiquibaseUtil/getBuildVersion))) 409 | (.comment "*********************************************************************")))) 410 | 411 | 412 | (defmacro with-writer 413 | [^Writer output & body] 414 | `(let [old-template# (get-db-executor) 415 | log-executor# (LoggingExecutor. 416 | (get-db-executor) ~output *db-instance*)] 417 | (.setExecutor (ExecutorService/getInstance) *db-instance* log-executor#) 418 | ~@body 419 | (try 420 | (.flush ~output) 421 | (catch IOException e# 422 | (throw (LiquibaseException. e#)))) 423 | (.setExecutor (ExecutorService/getInstance) *db-instance* old-template#))) 424 | 425 | 426 | (defmacro do-locked 427 | "Acquire lock and execute body of code in that context. Make sure the lock is 428 | released (or log an error if it can't be) before exit." 429 | [& body] 430 | `(let [ls# (.getLockService (LockServiceFactory/getInstance) *db-instance*)] 431 | (.waitForLock ls#) 432 | (try ~@body 433 | (finally 434 | (try (.releaseLock ls#) 435 | (catch LockException e# 436 | (.severe log "Could not release lock" e#))))))) 437 | 438 | 439 | ;; ===== Actions ===== 440 | 441 | 442 | (defn change-sql 443 | "Return a list of SQL statements (string) that would be required to execute 444 | the given Change object instantly for current database without versioning." 445 | ^List [^Change change] {:post [(mu/verify-cond (vector? %)) 446 | (mu/verify-cond (every? string? %))] 447 | :pre [(mu/verify-arg (instance? Change change)) 448 | (mu/verify-cond (instance? Database *db-instance*))]} 449 | (let [sgf (SqlGeneratorFactory/getInstance) 450 | sql (map (fn [^SqlStatement stmt] 451 | (map (fn [^Sql sql] 452 | ^String (.toSql sql)) 453 | (.generateSql sgf stmt *db-instance*))) 454 | (.generateStatements change *db-instance*))] 455 | (into [] (flatten sql)))) 456 | 457 | 458 | (defmacro with-writable 459 | "Set spec with :read-only? as false and execute body of code in that context." 460 | [& body] 461 | `(sp/with-connection (sp/assoc-readonly sp/*dbspec* false) 462 | ~@body)) 463 | 464 | 465 | (defn update 466 | "Run the Liquibase Update command. 467 | See also: 468 | liquibase.Liquibase/update 469 | make-db-instance 470 | http://www.liquibase.org/documentation/update" 471 | ([changelog-fn] 472 | (update changelog-fn [])) 473 | ([changelog-fn ^List contexts] {:pre [(mu/verify-arg (fn? changelog-fn)) 474 | (mu/verify-arg (coll? contexts))]} 475 | (sp/verify-writable) 476 | (do-locked 477 | (.setContexts *changelog-params* contexts) 478 | (let [changelog ^DatabaseChangeLog (changelog-fn)] 479 | (check-database-changelog-table *db-instance* true changelog contexts) 480 | (.validate changelog *db-instance* (into-array String contexts)) 481 | (let [changelog-it (make-changelog-iterator changelog 482 | [(ShouldRunChangeSetFilter. *db-instance*) 483 | (ContextChangeSetFilter. 484 | (into-array String 485 | [(mu/comma-sep-str contexts)])) 486 | (DbmsChangeSetFilter. *db-instance*) 487 | ])] 488 | (.run changelog-it (UpdateVisitor. *db-instance*) *db-instance*))))) 489 | ([changelog-fn ^List contexts ^Writer output] 490 | {:pre [(mu/verify-arg (instance? Writer output))]} 491 | (.setContexts *changelog-params* contexts) 492 | (with-writer output 493 | (output-header "Update Database Script") 494 | (with-writable 495 | (update changelog-fn contexts))))) 496 | 497 | 498 | (defn update-by-count 499 | "Run Liquibase Update command restricting number of changes to howmany-changesets. 500 | See also: 501 | liquibase.Liquibase/update 502 | make-db-instance 503 | http://www.liquibase.org/documentation/update" 504 | ([changelog-fn ^Integer howmany-changesets] 505 | (update-by-count changelog-fn howmany-changesets [])) 506 | ([changelog-fn ^Integer howmany-changesets ^List contexts] 507 | {:pre [(mu/verify-arg (fn? changelog-fn)) 508 | (mu/verify-arg (mu/posnum? howmany-changesets)) 509 | (mu/verify-arg (coll? contexts))]} 510 | (sp/verify-writable) 511 | (.setContexts *changelog-params* contexts) 512 | (do-locked 513 | (let [changelog ^DatabaseChangeLog (changelog-fn)] 514 | (check-database-changelog-table *db-instance* true changelog contexts) 515 | (.validate changelog *db-instance* (into-array String contexts)) 516 | (let [changelog-it (make-changelog-iterator changelog 517 | [(ShouldRunChangeSetFilter. *db-instance*) 518 | (ContextChangeSetFilter. 519 | (into-array String 520 | [(mu/comma-sep-str contexts)])) 521 | (DbmsChangeSetFilter. *db-instance*) 522 | (CountChangeSetFilter. howmany-changesets) 523 | ])] 524 | (.run changelog-it (UpdateVisitor. *db-instance*) *db-instance*))))) 525 | ([changelog-fn ^Integer howmany-changesets ^List contexts ^Writer output] 526 | (.setContexts *changelog-params* contexts) 527 | (with-writer output 528 | (output-header (str "Update " howmany-changesets " Change-sets Database Script")) 529 | (with-writable 530 | (update-by-count changelog-fn howmany-changesets contexts))))) 531 | 532 | 533 | (defn tag 534 | "Tag the database schema with specified tag (coerced as string)." 535 | [the-tag] {:pre [(mu/verify-arg (or (string? the-tag) (keyword? the-tag)))]} 536 | (sp/verify-writable) 537 | (do-locked 538 | (check-database-changelog-table *db-instance* false nil nil) 539 | (.tag *db-instance* (mu/as-string the-tag)))) 540 | 541 | 542 | (defn rollback-to-tag 543 | "Rollback schema to specified tag. 544 | See also: 545 | liquibase.Liquibase/rollback 546 | http://www.liquibase.org/documentation/rollback" 547 | ([changelog-fn ^String tag] 548 | (rollback-to-tag changelog-fn tag [])) 549 | ([changelog-fn ^String tag ^List contexts] 550 | {:pre [(mu/verify-arg (fn? changelog-fn)) 551 | (mu/verify-arg (coll? contexts))]} 552 | (sp/verify-writable) 553 | (do-locked 554 | (.setContexts *changelog-params* contexts) 555 | (let [changelog ^DatabaseChangeLog (changelog-fn)] 556 | (check-database-changelog-table *db-instance* false changelog contexts) 557 | (.validate changelog *db-instance* (into-array String contexts)) 558 | (let [ran-changesets (.getRanChangeSetList *db-instance*) 559 | changelog-it (make-changelog-iterator ran-changesets changelog 560 | [(AfterTagChangeSetFilter. tag ran-changesets) 561 | (AlreadyRanChangeSetFilter. ran-changesets) 562 | (ContextChangeSetFilter. 563 | (into-array String 564 | [(mu/comma-sep-str contexts)])) 565 | (DbmsChangeSetFilter. *db-instance*)])] 566 | (.run changelog-it (RollbackVisitor. *db-instance*) *db-instance*))))) 567 | ([changelog-fn ^String tag ^List contexts ^Writer output] 568 | {:pre [(mu/verify-arg (instance? Writer output))]} 569 | (.setContexts *changelog-params* contexts) 570 | (with-writer output 571 | (output-header (str "Rollback to '" tag "' Script")) 572 | (with-writable 573 | (rollback-to-tag changelog-fn tag contexts))))) 574 | 575 | 576 | (defn rollback-to-date 577 | "Rollback schema to specified date. 578 | See also: 579 | liquibase.Liquibase/rollback 580 | http://www.liquibase.org/documentation/rollback" 581 | ([changelog-fn ^Date date] 582 | (rollback-to-date changelog-fn date [])) 583 | ([changelog-fn ^Date date ^List contexts] 584 | {:pre [(mu/verify-arg (fn? changelog-fn)) 585 | (mu/verify-arg (mu/date? date)) 586 | (mu/verify-arg (coll? contexts))]} 587 | (sp/verify-writable) 588 | (do-locked 589 | (.setContexts *changelog-params* contexts) 590 | (let [changelog ^DatabaseChangeLog (changelog-fn)] 591 | (check-database-changelog-table *db-instance* false changelog contexts) 592 | (.validate changelog *db-instance* (into-array String contexts)) 593 | (let [ran-changesets (.getRanChangeSetList *db-instance*) 594 | changelog-it (make-changelog-iterator ran-changesets changelog 595 | [(ExecutedAfterChangeSetFilter. date ran-changesets) 596 | (AlreadyRanChangeSetFilter. ran-changesets) 597 | (ContextChangeSetFilter. 598 | (into-array String 599 | [(mu/comma-sep-str contexts)])) 600 | (DbmsChangeSetFilter. *db-instance*)])] 601 | (.run changelog-it (RollbackVisitor. *db-instance*) *db-instance*))))) 602 | ([changelog-fn ^Date date contexts ^Writer output] 603 | {:pre [(mu/verify-arg (instance? Writer output))]} 604 | (.setContexts *changelog-params* contexts) 605 | (with-writer output 606 | (output-header (str "Rollback to " date " Script")) 607 | (with-writable 608 | (rollback-to-date changelog-fn date contexts))))) 609 | 610 | 611 | (defn rollback-by-count 612 | "Rollback schema by specified count of changes. 613 | See also: 614 | liquibase.Liquibase/rollback 615 | http://www.liquibase.org/documentation/rollback" 616 | ([changelog-fn ^Integer howmany-changesets] 617 | (rollback-by-count changelog-fn ^Integer howmany-changesets [])) 618 | ([changelog-fn ^Integer howmany-changesets ^List contexts] 619 | {:pre [(mu/verify-arg (fn? changelog-fn)) 620 | (mu/verify-arg (mu/posnum? howmany-changesets)) 621 | (mu/verify-arg (coll? contexts))]} 622 | (sp/verify-writable) 623 | (.setContexts *changelog-params* contexts) 624 | (do-locked 625 | (let [changelog ^DatabaseChangeLog (changelog-fn)] 626 | (check-database-changelog-table *db-instance* false changelog contexts) 627 | (.validate changelog *db-instance* (into-array String contexts)) 628 | (let [ran-changesets (.getRanChangeSetList *db-instance*) 629 | changelog-it (make-changelog-iterator ran-changesets changelog 630 | [(AlreadyRanChangeSetFilter. ran-changesets) 631 | (ContextChangeSetFilter. 632 | (into-array String 633 | [(mu/comma-sep-str contexts)])) 634 | (DbmsChangeSetFilter. *db-instance*) 635 | (CountChangeSetFilter. howmany-changesets)])] 636 | (.run changelog-it (RollbackVisitor. *db-instance*) *db-instance*))))) 637 | ([changelog-fn ^Integer howmany-changesets ^List contexts ^Writer output] 638 | {:pre [(mu/verify-arg (instance? Writer output))]} 639 | (.setContexts *changelog-params* contexts) 640 | (with-writer output 641 | (output-header (str "Rollback to " howmany-changesets " Change-sets Script")) 642 | (with-writable 643 | (rollback-by-count changelog-fn howmany-changesets contexts))))) 644 | 645 | 646 | (defn generate-doc 647 | "Generate documentation for changelog. 648 | See also: 649 | http://www.liquibase.org/documentation/dbdoc 650 | http://www.liquibase.org/dbdoc/index.html" 651 | ([changelog-fn ^String output-dir ^List contexts] 652 | {:pre [(mu/verify-arg (fn? changelog-fn)) 653 | (mu/verify-arg (string? output-dir)) 654 | (mu/verify-arg (coll? contexts)) 655 | (mu/verify-cond (mu/not-nil? *db-instance*))]} 656 | (.setContexts *changelog-params* contexts) 657 | (do-locked 658 | (let [changelog ^DatabaseChangeLog (changelog-fn)] 659 | (check-database-changelog-table *db-instance* false changelog nil) 660 | (.validate changelog *db-instance* (into-array String contexts)) 661 | (let [changelog-it (make-changelog-iterator changelog 662 | [(DbmsChangeSetFilter. *db-instance*)]) 663 | dbdoc-visitor (DBDocVisitor. *db-instance*)] 664 | (.run changelog-it dbdoc-visitor *db-instance*) 665 | (.writeHTML (CustomDBDocVisitor. *db-instance*) 666 | (File. output-dir) nil))))) 667 | ([changelog-fn ^String output-dir] 668 | (generate-doc changelog-fn output-dir []))) 669 | 670 | 671 | (defn diff 672 | "Report a description of the differences between two databases to standard out. 673 | See also: 674 | http://www.liquibase.org/documentation/diff" 675 | [^Database ref-db-instance] 676 | (CommandLineUtils/doDiff ref-db-instance *db-instance*)) 677 | -------------------------------------------------------------------------------- /src/clj_liquibase/internal.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.internal 2 | (:require 3 | [clojure.string :as sr] 4 | [clj-jdbcutil.core :as sp] 5 | [clj-miscutil.core :as mu]) 6 | (:import 7 | (liquibase.structure.core Column) 8 | (liquibase.change ColumnConfig ConstraintsConfig) 9 | (liquibase.change.core LoadDataColumnConfig) 10 | (liquibase.statement DatabaseFunction) 11 | (liquibase.util ISODateFormat) 12 | (java.util Date))) 13 | 14 | 15 | (defn dbfn? 16 | ^Boolean [x] 17 | (instance? DatabaseFunction x)) 18 | 19 | 20 | (defn as-coltype 21 | "Create column-type (string - subject to sp/db-iden). 22 | Examples: 23 | (coltype :int) => \"INT\" 24 | (coltype \"BIGINT\") => \"BIGINT\" 25 | (coltype :char 80) => \"char(80)\" 26 | (coltype :float 17 5) => \"float(17, 5)\" 27 | Note: This function is called during constructing column-config. 28 | See also: http://www.liquibase.org/documentation/column" 29 | ^String [t & more] 30 | (str (sp/db-iden t) 31 | (if (mu/not-empty? more) (apply str "(" (mu/comma-sep-str more) ")")))) 32 | 33 | 34 | (defmacro if-nn 35 | "Execute body if arg not nil" 36 | [arg & body] 37 | `(if (mu/not-nil? ~arg) 38 | (do ~@body))) 39 | 40 | 41 | (defmacro set-column-value 42 | "Set column value for a container object. 43 | Arguments: 44 | cont The container object (e.g. ColumnConfig) 45 | value The column value" 46 | [cont value] 47 | `(cond 48 | (string? ~value) (.setValue ~cont ~(with-meta value {:tag 'String})) 49 | (number? ~value) (.setValueNumeric ~cont ~(with-meta value {:tag 'Number})) 50 | (mu/boolean? ~value) (.setValueBoolean ~cont ~(with-meta value {:tag 'Boolean})) 51 | (mu/date? ~value) (.setValueDate ~cont ~(with-meta value {:tag 'java.util.Date})) 52 | (dbfn? ~value) (.setValueComputed ~cont ~(with-meta value {:tag 'DatabaseFunction})) 53 | :else (mu/illegal-arg "Bad column value: " ~value (type ~value) 54 | ", allowed types are: String, Number, Boolean, java.util.Date" 55 | ", liquibase.statement.DatabaseFunction (see 'dbfn' function)"))) 56 | 57 | 58 | (defn new-column-value 59 | "Return a ColumnConfig instance with column name and value set." 60 | [colname value] 61 | (let [col (ColumnConfig.)] 62 | (.setName col (sp/db-iden colname)) 63 | (set-column-value col value) 64 | col)) 65 | 66 | 67 | (defn load-data-column-config 68 | "Return a new LoadDataColumnConfig instance from supplied arguments. 69 | Arguments: 70 | colname (keyword/String) column name 71 | coltype (keyword/String) either of STRING, NUMERIC, DATE, BOOLEAN 72 | Optional arguments: 73 | :index (Number) 74 | :header (Keyword/String) 75 | See also: 76 | http://www.liquibase.org/documentation/changes/load_data" 77 | [colname coltype 78 | & {:keys [index ; Integer 79 | header 80 | ]}] 81 | (let [s-coltype ^String (mu/as-string coltype)] 82 | (when-not (some #(.equalsIgnoreCase s-coltype %) ["STRING" "NUMERIC" "DATE" "BOOLEAN"]) 83 | (mu/illegal-argval "coltype" 84 | "Either of \"STRING\", \"NUMERIC\", \"DATE\" or \"BOOLEAN\"" coltype)) 85 | (let [ldcc (LoadDataColumnConfig.)] 86 | (doto ldcc 87 | (.setName (sp/db-iden colname)) 88 | (.setType (sr/upper-case s-coltype))) 89 | (if index (.setIndex ldcc index)) 90 | (if header (.setHeader ldcc header)) 91 | ldcc))) 92 | 93 | 94 | (defn- dbfn 95 | ^DatabaseFunction [^String value] 96 | (DatabaseFunction. value)) 97 | 98 | 99 | (defn- iso-date 100 | "Parse date from ISO-date-format string." 101 | ^java.util.Date [^String date-str] 102 | (.parse (ISODateFormat.) date-str)) 103 | 104 | 105 | (defmacro iso-date-str 106 | "Generate ISO-Date string from the following types: 107 | java.sql.Date 108 | java.sql.Time 109 | java.sql.Timestamp" 110 | ^String [date] 111 | `(let [idf# ^ISODateFormat (ISODateFormat.) 112 | sd# (cond 113 | (instance? java.sql.Date ~date) (.format idf# ~(with-meta date {:tag 'java.sql.Date})) 114 | (instance? java.sql.Time ~date) (.format idf# ~(with-meta date {:tag 'java.sql.Time})) 115 | (instance? java.sql.Timestamp ~date) (.format idf# ~(with-meta date {:tag 'java.sql.Timestamp})) 116 | :else (mu/illegal-arg 117 | "Allowed types: java.sql.Date, java.sql.Time, java.sql.Timestamp" 118 | " -- Found: " (class ~date)))] 119 | (str \" sd# \"))) 120 | 121 | 122 | (defn any-sqldate? 123 | [d] 124 | (or 125 | (instance? java.sql.Timestamp d) 126 | (instance? java.sql.Date d) 127 | (instance? java.sql.Time d))) 128 | 129 | 130 | (defn sqldate 131 | [^Date d] 132 | (java.sql.Date. 133 | (.getTime d))) 134 | 135 | 136 | (defmacro add-default-value 137 | "Add default value for a container object. 138 | (Meant for add-default-value change.) 139 | Arguments: 140 | cont The container object 141 | default The default value" 142 | [cont default] 143 | `(cond 144 | (string? ~default) (let [s-default# 145 | (str \" ~default \")] (.setDefaultValue ~cont s-default#)) 146 | (number? ~default) (let [n-default# 147 | (str ~default)] (.setDefaultValueNumeric ~cont n-default#)) 148 | (mu/boolean? ~default) (.setDefaultValueBoolean 149 | ~cont ~(with-meta default {:tag 'Boolean})) 150 | (mu/date? ~default) (let [d-default# 151 | (iso-date-str 152 | (sqldate ~default))] (.setDefaultValueDate ~cont d-default#)) 153 | (any-sqldate? ~default) (let [d-default# 154 | (iso-date-str 155 | ~default)] (.setDefaultValueDate ~cont d-default#)) 156 | (dbfn? ~default) (.setDefaultValueComputed 157 | ~cont ~(with-meta default {:tag 'DatabaseFunction})) 158 | :else (mu/illegal-arg "Bad default value: " ~default (type ~default) 159 | ", allowed types are: String, Number, Boolean" 160 | ", java.util.Date/java.sql.Date/java.sql.Time/java.sql.Timestamp" 161 | ", liquibase.statement.DatabaseFunction (see 'dbfn' function)"))) 162 | 163 | 164 | (defmacro set-default-value 165 | "Set default value for a container object. 166 | Arguments: 167 | cont The container object 168 | default The default value" 169 | [cont default] 170 | `(cond 171 | (string? ~default) (.setDefaultValue ~cont ~(with-meta default {:tag 'String})) 172 | (number? ~default) (.setDefaultValueNumeric ~cont ~(with-meta default {:tag 'Number})) 173 | (mu/boolean? ~default) (.setDefaultValueBoolean ~cont ~(with-meta default {:tag 'Boolean})) 174 | (mu/date? ~default) (.setDefaultValueDate ~cont ~(with-meta default {:tag 'java.util.Date})) 175 | (dbfn? ~default) (.setDefaultValueComputed ~cont ~(with-meta default {:tag 'DatabaseFunction})) 176 | :else (mu/illegal-arg "Bad default value: " ~default (type ~default) 177 | ", allowed types are: String, Number, Boolean, java.util.Date" 178 | ", liquibase.statement.DatabaseFunction (see 'dbfn' function)"))) 179 | 180 | 181 | (defn as-column-config 182 | "Create column-configuration. This function is called by change/create-table. 183 | Arguments: 184 | colname (String/Keyword) column name - subject to db-iden 185 | coltype (String/Keyword/Vector) column type 186 | Optional arguments (can use either long/short name): 187 | Long name |Short name |Allowed types 188 | -----------------------|-----------|------------------------ 189 | :default-value |:default | String/Number/java.util.Date/Boolean/DatabaseFunction 190 | :auto-increment |:autoinc | Boolean 191 | :remarks | | String 192 | ;; constraints (s.t. = subject to) 193 | :nullable |:null | Boolean 194 | :primary-key |:pk | Boolean 195 | :primary-key-name |:pkname | String/Keyword - s.t. db-iden 196 | :primary-key-tablespace|:pktspace | String/Keyword - s.t. db-iden 197 | :references |:refs | String (Foreign key definition) 198 | :unique |:uniq | Boolean 199 | :unique-constraint-name|:ucname | String/Keyword - s.t. db-iden 200 | :check | | String 201 | :delete-cascade |:dcascade | Boolean 202 | :foreign-key-name |:fkname | String/Keyword - s.t. db-iden 203 | :initially-deferred |:idefer | Boolean 204 | :deferrable |:defer | Boolean 205 | Examples (when used inside 'change/create-table'): 206 | [:id :int :null false :pk true :autoinc true] 207 | [:name [:varchar 40] :null false] 208 | [:gender [:char 1] :null false] 209 | [:birth-date :date :null false] 210 | See also: 211 | as-coltype 212 | http://www.liquibase.org/documentation/column" 213 | ^ColumnConfig [colname coltype ; coltype (mixed) - keyword, string, vector (1st arg: db-iden) 214 | & {:keys [default-value default ; String/Number/Date/Boolean/DatabaseFunction 215 | auto-increment autoinc ; Boolean 216 | remarks ; String 217 | ;; constraints 218 | nullable null ; Boolean 219 | primary-key pk ; Boolean 220 | primary-key-name pkname ; String/Keyword - s.t. db-iden 221 | primary-key-tablespace pktspace ; String/Keyword - s.t. db-iden 222 | references refs ; String (Foreign key definition) 223 | unique uniq ; Boolean 224 | unique-constraint-name ucname ; String/Keyword - s.t. db-iden 225 | check ; String 226 | delete-cascade dcascade ; Boolean 227 | foreign-key-name fkname ; String/Keyword - s.t. db-iden 228 | initially-deferred idefer ; Boolean 229 | deferrable defer ; Boolean 230 | ]}] 231 | (let [col (ColumnConfig.) 232 | con (ConstraintsConfig.) 233 | ;; optional column properties 234 | c-default (or default-value default ) 235 | c-autoinc (or auto-increment autoinc ) 236 | c-remarks remarks 237 | ;; constraints 238 | c-null (or nullable null ) 239 | c-pk (or primary-key pk ) 240 | c-pkname (or primary-key-name pkname ) 241 | c-pktspace (or primary-key-tablespace pktspace) 242 | c-refs (or references refs ) 243 | c-uniq (or unique uniq ) 244 | c-ucname (or unique-constraint-name ucname ) 245 | c-check check 246 | c-dcascade (or delete-cascade dcascade) 247 | c-fkname (or foreign-key-name fkname ) 248 | c-idefer (or initially-deferred idefer ) 249 | c-defer (or deferrable defer )] 250 | ;; set base column properties 251 | (doto col 252 | (.setName (sp/db-iden colname)) 253 | (.setType (apply as-coltype (mu/as-vector coltype)))) 254 | ;; set optional column properties 255 | (if-nn c-default (set-default-value col c-default)) 256 | (if-nn c-autoinc (.setAutoIncrement col ^Boolean c-autoinc)) 257 | (if-nn c-remarks (.setRemarks col ^String c-remarks)) 258 | ;; set constraints 259 | (if-nn c-null (.setNullable con ^Boolean c-null )) 260 | (if-nn c-pk (.setPrimaryKey con ^Boolean c-pk )) 261 | (if-nn c-pkname (.setPrimaryKeyName con ^String (sp/db-iden 262 | c-pkname ))) 263 | (if-nn c-pktspace (.setPrimaryKeyTablespace con ^String (sp/db-iden 264 | c-pktspace))) 265 | (if-nn c-refs (.setReferences con ^String c-refs )) 266 | (if-nn c-uniq (.setUnique con ^Boolean c-uniq )) 267 | (if-nn c-ucname (.setUniqueConstraintName con ^String (sp/db-iden 268 | c-ucname ))) 269 | (if-nn c-check (.setCheckConstraint con ^String c-check )) 270 | (if-nn c-dcascade (.setDeleteCascade con ^Boolean c-dcascade )) 271 | (if-nn c-fkname (.setForeignKeyName con ^String (sp/db-iden 272 | c-fkname ))) 273 | (if-nn c-idefer (.setInitiallyDeferred con ^Boolean c-idefer )) 274 | (if-nn c-defer (.setDeferrable con ^Boolean c-defer )) 275 | (.setConstraints col con) 276 | col)) 277 | 278 | 279 | (defn as-dbident-names 280 | "Return comma-separated name string for a given bunch of potentially 281 | Clojure-oriented names." 282 | ^String [names] 283 | (mu/comma-sep-str (map sp/db-iden (mu/as-vector names)))) 284 | -------------------------------------------------------------------------------- /src/clj_liquibase/precondition.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.precondition 2 | "Clojure wrappers for liquibase.change.Change implementations. 3 | See also: 4 | http://www.liquibase.org/documentation/index (Available Database Refactorings)" 5 | (:require 6 | [clj-jdbcutil.core :as sp] 7 | [clj-miscutil.core :as mu] 8 | [clj-liquibase.internal :as in]) 9 | (:import 10 | (liquibase.precondition Precondition PreconditionLogic) 11 | (liquibase.precondition.core ChangeLogPropertyDefinedPrecondition 12 | ChangeSetExecutedPrecondition 13 | ColumnExistsPrecondition 14 | DBMSPrecondition 15 | ForeignKeyExistsPrecondition 16 | IndexExistsPrecondition 17 | PrimaryKeyExistsPrecondition 18 | RunningAsPrecondition 19 | SequenceExistsPrecondition 20 | SqlPrecondition 21 | TableExistsPrecondition 22 | ViewExistsPrecondition 23 | ;; -- containers --- 24 | AndPrecondition 25 | NotPrecondition 26 | OrPrecondition 27 | PreconditionContainer))) 28 | 29 | 30 | (defn pre-cond? 31 | "Return true if given argument is a pre-condition, false otherwise." 32 | [x] 33 | (instance? Precondition x)) 34 | 35 | 36 | (defn changelog-prop-defined 37 | "Change-log property defined" 38 | ^ChangeLogPropertyDefinedPrecondition 39 | [prop value] {:post [(mu/verify-cond (instance? ChangeLogPropertyDefinedPrecondition %))] 40 | :pre [(mu/verify-arg (or (keyword? prop) (string? prop))) 41 | (mu/verify-arg (or (keyword? value) (string? value)))]} 42 | (let [pc (ChangeLogPropertyDefinedPrecondition.)] 43 | (doto pc 44 | (.setProperty (mu/as-string prop)) 45 | (.setValue (mu/as-string value))) 46 | pc)) 47 | 48 | 49 | (defn changeset-executed 50 | "Change-set executed" 51 | ^ChangeSetExecutedPrecondition 52 | [file id author] {:post [(mu/verify-cond (instance? ChangeSetExecutedPrecondition %))] 53 | :pre [(mu/verify-arg (string? file)) 54 | (mu/verify-arg (mu/not-nil? id)) 55 | (mu/verify-arg (or (keyword? author) (string? author)))]} 56 | (let [pc (ChangeSetExecutedPrecondition.)] 57 | (doto pc 58 | (.setChangeLogFile ^String file) 59 | (.setId (mu/as-string id)) 60 | (.setAuthor (mu/as-string author))) 61 | pc)) 62 | 63 | 64 | (defn column-exists 65 | "Specified column exists" 66 | ^ColumnExistsPrecondition 67 | [schema-name table-name column-name] {:post [(mu/verify-cond (instance? ColumnExistsPrecondition %))] 68 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 69 | (mu/verify-arg (or (keyword? table-name) (string? table-name))) 70 | (mu/verify-arg (or (keyword? column-name) (string? column-name)))]} 71 | (let [pc (ColumnExistsPrecondition.)] 72 | (doto pc 73 | (.setSchemaName (sp/db-iden schema-name)) 74 | (.setTableName (sp/db-iden table-name)) 75 | (.setColumnName (sp/db-iden column-name))) 76 | pc)) 77 | 78 | 79 | ;; wrapper-based custom preconditions are not supported (they need class name) 80 | 81 | 82 | (defn dbms 83 | "Check database type. Example: 84 | (dbms :mysql)" 85 | ^DBMSPrecondition 86 | [db-type] {:post [(mu/verify-cond (instance? DBMSPrecondition %))] 87 | :pre [(mu/verify-arg (or (keyword? db-type) (string? db-type)))]} 88 | (let [pc (DBMSPrecondition.)] 89 | (doto pc 90 | (.setType (mu/as-string db-type))) 91 | pc)) 92 | 93 | 94 | ;; TODO - do we really need *both* `table-name` and `key-name` arguments? 95 | (defn foreign-key-exists 96 | "Return Precondition that asserts given Foreign key exists" 97 | ^ForeignKeyExistsPrecondition 98 | [schema-name table-name key-name] {:post [(mu/verify-cond (instance? ForeignKeyExistsPrecondition %))] 99 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 100 | (mu/verify-arg (or (keyword? table-name) (string? table-name))) 101 | (mu/verify-arg (or (keyword? key-name) (string? key-name)))]} 102 | (let [pc (ForeignKeyExistsPrecondition.)] 103 | (doto pc 104 | (.setSchemaName (sp/db-iden schema-name)) 105 | (.setForeignKeyTableName (sp/db-iden table-name)) 106 | (.setForeignKeyName (sp/db-iden key-name))) 107 | pc)) 108 | 109 | 110 | (defn index-exists 111 | ^IndexExistsPrecondition 112 | [schema-name table-name column-names index-name] {:post [(mu/verify-cond (instance? IndexExistsPrecondition %))] 113 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 114 | (mu/verify-arg (or (keyword? table-name) (string? table-name))) 115 | (mu/verify-arg (or (keyword? column-names) (string? column-names) 116 | (and (coll? column-names) 117 | (every? #(or (keyword? %) (string? %)) 118 | column-names)))) 119 | (mu/verify-arg (or (keyword? index-name) (string? index-name)))]} 120 | (let [pc (IndexExistsPrecondition.)] 121 | (doto pc 122 | (.setSchemaName (sp/db-iden schema-name)) 123 | (.setTableName (sp/db-iden table-name)) 124 | (.setColumnNames (mu/comma-sep-str 125 | (map sp/db-iden (mu/as-vector column-names)))) 126 | (.setIndexName (sp/db-iden index-name))) 127 | pc)) 128 | 129 | 130 | (defn primary-key-exists 131 | ^PrimaryKeyExistsPrecondition 132 | [schema-name table-name primary-key-name] {:post [(mu/verify-cond (instance? PrimaryKeyExistsPrecondition %))] 133 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 134 | (mu/verify-arg (or (keyword? primary-key-name) (string? primary-key-name))) 135 | (mu/verify-arg (or (keyword? table-name) (string? table-name)))]} 136 | (let [pc (PrimaryKeyExistsPrecondition.)] 137 | (doto pc 138 | (.setSchemaName (sp/db-iden schema-name)) 139 | (.setPrimaryKeyName (sp/db-iden primary-key-name)) 140 | (.setTableName (sp/db-iden table-name))) 141 | pc)) 142 | 143 | 144 | (defn running-as 145 | "Verify database user name" 146 | ^RunningAsPrecondition 147 | [^String user-name] {:post [(mu/verify-cond (instance? RunningAsPrecondition %))] 148 | :pre [(mu/verify-arg (string? user-name))]} 149 | (let [pc (RunningAsPrecondition.)] 150 | (.setUsername pc user-name) 151 | pc)) 152 | 153 | 154 | (defn sequence-exists 155 | "Verify that given sequence exists" 156 | ^SequenceExistsPrecondition 157 | [schema-name sequence-name] {:post [(mu/verify-cond (instance? SequenceExistsPrecondition %))] 158 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 159 | (mu/verify-arg (or (keyword? sequence-name) (string? sequence-name)))]} 160 | (let [pc (SequenceExistsPrecondition.)] 161 | (doto pc 162 | (.setSchemaName (sp/db-iden schema-name)) 163 | (.setSequenceName (sp/db-iden sequence-name))) 164 | pc)) 165 | 166 | 167 | (defn sql 168 | "SQL Check" 169 | ^SqlPrecondition 170 | [expected ^String sql-stmt] {:post [(mu/verify-cond (instance? SqlPrecondition %))] 171 | :pre [(mu/verify-arg (string? sql-stmt))]} 172 | (let [pc (SqlPrecondition.)] 173 | (doto pc 174 | (.setExpectedResult (or (nil? expected) nil 175 | (mu/as-string expected))) 176 | (.setSql sql-stmt)) 177 | pc)) 178 | 179 | 180 | (defn table-exists 181 | "Verify that said table exists" 182 | ^TableExistsPrecondition 183 | [schema-name table-name] {:post [(mu/verify-cond (instance? TableExistsPrecondition %))] 184 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 185 | (mu/verify-arg (or (keyword? table-name) (string? table-name)))]} 186 | (let [pc (TableExistsPrecondition.)] 187 | (doto pc 188 | (.setSchemaName (sp/db-iden schema-name)) 189 | (.setTableName (sp/db-iden table-name))) 190 | pc)) 191 | 192 | 193 | (defn view-exists 194 | "Verify that view exists" 195 | ^ViewExistsPrecondition 196 | [schema-name view-name] {:post [(mu/verify-cond (instance? ViewExistsPrecondition %))] 197 | :pre [(mu/verify-arg (or (keyword? schema-name) (string? schema-name))) 198 | (mu/verify-arg (or (keyword? view-name) (string? view-name)))]} 199 | (let [pc (ViewExistsPrecondition.)] 200 | (doto pc 201 | (.setSchemaName (sp/db-iden schema-name)) 202 | (.setViewName (sp/db-iden view-name))) 203 | pc)) 204 | 205 | 206 | (defn pc-and 207 | "Verify that ALL immediate nested preconditions are met" 208 | ^AndPrecondition 209 | [pc & more] {:post [(mu/verify-cond (instance? AndPrecondition %))] 210 | :pre [(mu/verify-arg (every? #(instance? Precondition %) 211 | (into [pc] more)))]} 212 | (let [pl (AndPrecondition.)] 213 | (doseq [each (into [pc] more)] 214 | (.addNestedPrecondition pl each)) 215 | pl)) 216 | 217 | 218 | (defn pc-not 219 | "Verify that NONE OF immediate nested preconditions is met" 220 | ^NotPrecondition 221 | [pc & more] {:post [(mu/verify-cond (instance? NotPrecondition %))] 222 | :pre [(mu/verify-arg (every? #(instance? Precondition %) 223 | (into [pc] more)))]} 224 | (let [pl (NotPrecondition.)] 225 | (doseq [each (into [pc] more)] 226 | (.addNestedPrecondition pl each)) 227 | pl)) 228 | 229 | 230 | (defn pc-or 231 | "Verify that ANY OF immediate nested preconditions is met" 232 | ^OrPrecondition 233 | [pc & more] {:post [(mu/verify-cond (instance? OrPrecondition %))] 234 | :pre [(mu/verify-arg (every? #(instance? Precondition %) 235 | (into [pc] more)))]} 236 | (let [pl (OrPrecondition.)] 237 | (doseq [each (into [pc] more)] 238 | (.addNestedPrecondition pl each)) 239 | pl)) 240 | 241 | 242 | (def on-fail-error-values 243 | {:halt "HALT" ; Immediately halt execution of entire change log [default] 244 | :continue "CONTINUE" ; Skip over change set. Execution of change set will be attempted again on the next update. Continue with change log. 245 | :mark-ran "MARK_RAN" ; Skip over change set, but mark it as ran. Continue with change log 246 | :warn "WARN" ; Output warning and continue executing change set as normal. 247 | }) 248 | 249 | 250 | (def on-update-sql-values 251 | {:ignore "IGNORE" ; Ignore the preCondition in updateSQL mode 252 | :test "TEST" ; Test the changeSet in updateSQL mode 253 | :fail "FAIL" ; Fail the preCondition in updateSQL mode 254 | }) 255 | 256 | 257 | (defn pre-cond 258 | "Return a PreconditionContainer that verifies that ALL immediate nested 259 | preconditions are met. 260 | Optional args: 261 | :on-fail What to do when preconditions fail; either of 262 | :halt (default), :continue, :mark-ran, :warn 263 | :on-fail-msg Custom message (string) to output when preconditions fail 264 | :on-error What to do when preconditions error; either of 265 | :halt (default), :continue, :mark-ran, :warn 266 | :on-error-msg Custom message (string) to output when preconditions fail 267 | :on-update-sql What to do in updateSQL mode; either of 268 | :run, :fail, :ignore 269 | See also: 270 | http://www.liquibase.org/documentation/preconditions" 271 | ^PreconditionContainer 272 | [pre-cond-list 273 | & {:keys [on-fail ; onFail -- What to do when preconditions fail 274 | on-fail-msg ; onFailMessage -- Custom message to output when preconditions fail 275 | on-error ; onError -- What to do when preconditions error 276 | on-error-msg ; onErrorMessage -- Custom message to output when preconditions fail 277 | on-update-sql ; onUpdateSQL -- What to do in updateSQL mode 278 | ] 279 | :or {on-fail nil 280 | on-fail-msg nil 281 | on-error nil 282 | on-error-msg nil 283 | on-update-sql nil} 284 | :as opt}] 285 | {:post [(mu/verify-cond (instance? PreconditionContainer %))] 286 | :pre [(mu/verify-opt #{:on-fail :on-fail-msg :on-error :on-error-msg 287 | :on-update-sql} opt) 288 | (mu/verify-arg (and (coll? pre-cond-list) 289 | (every? #(instance? Precondition %) pre-cond-list))) 290 | (mu/verify-arg (or (nil? on-fail) (contains? on-fail-error-values on-fail))) 291 | (mu/verify-arg (or (nil? on-error) (contains? on-fail-error-values on-error))) 292 | (mu/verify-arg (or (nil? on-update-sql) (contains? on-update-sql-values on-update-sql))) 293 | (mu/verify-arg (or (nil? on-fail-msg) (string? on-fail-msg))) 294 | (mu/verify-arg (or (nil? on-error-msg) (string? on-error-msg)))]} 295 | (let [pc (PreconditionContainer.)] 296 | (doseq [each pre-cond-list] 297 | (.addNestedPrecondition pc each)) 298 | (when on-fail (.setOnFail pc (on-fail on-fail-error-values))) 299 | (when on-error (.setOnError pc (on-error on-fail-error-values))) 300 | (when on-update-sql (.setOnSqlOutput pc ^String (on-update-sql on-update-sql-values))) 301 | (when on-fail-msg (.setOnFailMessage pc on-fail-msg)) 302 | (when on-error-msg (.setOnErrorMessage pc on-error-msg)) 303 | pc)) 304 | -------------------------------------------------------------------------------- /src/clj_liquibase/sql_visitor.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.sql-visitor 2 | (:require [clj-miscutil.core :as mu]) 3 | (:import 4 | (java.util.regex Pattern) 5 | (liquibase.sql.visitor SqlVisitor 6 | AppendSqlVisitor PrependSqlVisitor 7 | RegExpReplaceSqlVisitor ReplaceSqlVisitor))) 8 | 9 | 10 | (defn visitor? 11 | "Return true if `v` is an SqlVisitor, false otherwise." 12 | [v] 13 | (instance? SqlVisitor v)) 14 | 15 | 16 | (defn make-append-visitor 17 | "Return visitor that appends `text` to generated SQL." 18 | ^AppendSqlVisitor 19 | [^String text] {:post [(visitor? %)] 20 | :pre [(mu/verify-arg (mu/not-nil? text))]} 21 | (doto (AppendSqlVisitor.) 22 | (.setValue (mu/as-string text)))) 23 | 24 | 25 | (defn make-prepend-visitor 26 | "Return visitor that prefixes generated SQL with `text`." 27 | ^PrependSqlVisitor 28 | [^String text] {:post [(visitor? %)] 29 | :pre [(mu/verify-arg (mu/not-nil? text))]} 30 | (doto (PrependSqlVisitor.) 31 | (.setValue (mu/as-string text)))) 32 | 33 | 34 | (defn make-replace-visitor 35 | "Return visitor that replaces `needle` with `text` in generated SQL. Note that 36 | `needle` can be either string or regex (java.util.regex.Pattern instance.)" 37 | ^SqlVisitor 38 | [needle ^String new-text] {:post [(visitor? %)] 39 | :pre [(mu/verify-arg (mu/not-nil? needle)) 40 | (mu/verify-arg (mu/not-nil? new-text))]} 41 | (let [regex? (instance? Pattern needle) 42 | replace (if regex? (.pattern ^Pattern needle) (mu/as-string needle))] 43 | (if regex? 44 | (doto (RegExpReplaceSqlVisitor.) 45 | (.setReplace ^String replace) 46 | (.setWith ^String (mu/as-string new-text))) 47 | (doto (ReplaceSqlVisitor.) 48 | (.setReplace ^String replace) 49 | (.setWith ^String (mu/as-string new-text)))))) 50 | 51 | 52 | (defn for-dbms! 53 | "Restrict a visitor (that applies to all DBMS by default) to specified DBMS 54 | list `dbms`." 55 | [dbms ^SqlVisitor visitor] 56 | (doto visitor 57 | (.setApplicableDbms (set (map mu/as-string (mu/as-vector dbms)))))) 58 | 59 | 60 | (defn apply-to-rollback! 61 | "Specify whether a visitor should be applied to rollbacks. By default a 62 | visitor is not applied to rollbacks." 63 | [apply? ^SqlVisitor visitor] 64 | (doto visitor 65 | (.setApplyToRollback apply?))) 66 | 67 | 68 | (defn for-contexts! 69 | "Restrict visitor to specified `contexts` only. By default a visitor applies 70 | to all contexts." 71 | [contexts ^SqlVisitor visitor] 72 | (doto visitor 73 | (.setContexts (set (map mu/as-string (mu/as-vector contexts)))))) 74 | 75 | 76 | (defn make-visitors 77 | "Return a list of visitors from a DSL-like fluent list of arguments. 78 | Example: 79 | (make-visitors :include (map (partial for-dbms! :mysql) 80 | (make-visitors :append \"engine=InnoDB\")) 81 | :append \" -- creating table\n\" 82 | :replace [:integer :bigint] 83 | :replace {:string \"VARCHAR(256)\" 84 | #\"varchar*\" \"VARCHAR2\"} 85 | :prepend \"IF NOT EXIST\")" 86 | [k v & args] {:post [(coll? %)] 87 | :pre [(mu/verify-arg (even? (count args)))]} 88 | (let [pairs (partition 2 (into [k v] args)) 89 | makev (fn [k v] 90 | (case k 91 | :include (mu/as-vector v) 92 | :append [(make-append-visitor v)] 93 | :prepend [(make-prepend-visitor v)] 94 | :replace (if (map? v) 95 | (map (fn [[ik iv]] (make-replace-visitor ik iv)) v) 96 | (if (and (coll? v) (= 2 (count v))) 97 | [(apply make-replace-visitor v)] 98 | (mu/illegal-argval 99 | 'v "list of 2 arguments (needle, new-text)" 100 | v))) 101 | (mu/illegal-argval 102 | 'k ":include/:append/:prepend/:replace" k)))] 103 | (into [] (reduce concat (map (fn [[k v]] (makev k v)) pairs))))) 104 | 105 | 106 | ;; ===== Some common visitors ===== 107 | 108 | (def ^{:doc "SQL visitor to enforce InnoDB storage engine on MySQL"} 109 | mysql-innodb (for-dbms! :mysql (make-append-visitor " engine=InnoDB"))) 110 | -------------------------------------------------------------------------------- /test/child1.edn: -------------------------------------------------------------------------------- 1 | {:database-change-log [{"changeSet" {"id" "1" 2 | "author" "nvoxland" 3 | "changes" [{"createTable" {"tableName" "person" 4 | "columns" [{"column" {"name" "id" 5 | "type" "int" 6 | "autoIncrement" true, 7 | "constraints" {primary-key? true 8 | nullable? false}}} 9 | {"column" {"name" "firstname" 10 | "type" "varchar(50)"}} 11 | {"column" {"name" "lastname" 12 | "type" "varchar(50)" 13 | "constraints" {"nullable" false}}} 14 | {"column" {"name" "state" 15 | "type" "char(2)"}}]}}]}}]} -------------------------------------------------------------------------------- /test/clj_liquibase/example.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.example 2 | (:require 3 | [clojure.pprint :as pp] 4 | [clj-miscutil.core :as mu] 5 | [clj-liquibase.core :as lb] 6 | [clj-liquibase.change :as ch] 7 | [clj-dbcp.core :as dbcp] 8 | [clj-jdbcutil.core :as spec])) 9 | 10 | 11 | (def ds (dbcp/make-datasource :h2 {:target :memory :database :default})) 12 | 13 | ;;(def ds (dbcp/make-datasource :mysql {:host "localhost" :database "bituf" :user "root" :password "root"})) 14 | 15 | 16 | (def dbspec (spec/make-dbspec ds)) 17 | 18 | 19 | (def ct-change1 (mu/! (ch/create-table "sampletable1" 20 | [[:id :int :null false :pk true :autoinc true] 21 | [:name [:varchar 40] :null false] 22 | [:gender [:char 1] :null false]]))) 23 | 24 | (def ct-change2 (mu/! (ch/create-table "sampletable2" 25 | [[:id :int :null false :pk true :autoinc true] 26 | [:name [:varchar 40] :null false] 27 | [:gender [:char 1] :null false]]))) 28 | 29 | (def ct-change3 (mu/! (ch/sql "SELECT * FROM sampletable1"))) 30 | 31 | (def changeset-1 ["id=1" "author=shantanu" [ct-change1]]) 32 | 33 | 34 | (def changeset-2 ["id=2" "author=shantanu" [ct-change2]]) 35 | 36 | 37 | (def changeset-3 ["id=3" "author=shantanu" [ct-change3]]) 38 | 39 | 40 | (lb/defchangelog changelog-1 "example" [changeset-1]) 41 | 42 | (lb/defchangelog changelog-2 "example" [changeset-1 changeset-2]) 43 | 44 | (lb/defchangelog changelog-3 "example" [changeset-1 changeset-2 changeset-3]) 45 | 46 | 47 | ;; ----- execute 48 | 49 | 50 | (defn update-1 51 | [] 52 | (spec/with-connection 53 | dbspec 54 | (lb/with-lb 55 | (lb/update changelog-1) 56 | (lb/tag "tag1")))) 57 | 58 | (defn update-2 59 | [] 60 | (spec/with-connection 61 | dbspec 62 | (lb/with-lb 63 | (lb/update changelog-2) 64 | (lb/tag "tag2")))) 65 | 66 | (defn update-3 67 | [] 68 | (spec/with-connection 69 | dbspec 70 | (lb/with-lb 71 | (lb/update changelog-3) 72 | (lb/tag "tag3")))) 73 | 74 | (defn rollback-to-1 75 | [] 76 | (spec/with-connection 77 | dbspec 78 | (lb/with-lb 79 | (lb/rollback-to-tag changelog-2 "tag1" [])))) 80 | 81 | 82 | ;(mu/! (update-1)) 83 | 84 | ;(mu/! (update-2)) 85 | 86 | ;(mu/! (rollback-to-1)) 87 | -------------------------------------------------------------------------------- /test/clj_liquibase/test_change.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-change 2 | (:require 3 | [clj-liquibase.change :as change]) 4 | (:import 5 | (java.util Date) 6 | (liquibase.change.core 7 | ;; Structural Refactorings 8 | AddColumnChange RenameColumnChange ModifyDataTypeChange 9 | DropColumnChange AlterSequenceChange CreateTableChange 10 | RenameTableChange DropTableChange CreateViewChange 11 | RenameViewChange DropViewChange MergeColumnChange 12 | CreateProcedureChange 13 | ;; Data Quality Refactorings 14 | AddLookupTableChange AddNotNullConstraintChange DropNotNullConstraintChange 15 | AddUniqueConstraintChange DropUniqueConstraintChange CreateSequenceChange 16 | DropSequenceChange AddAutoIncrementChange AddDefaultValueChange 17 | DropDefaultValueChange 18 | ;; Referential Integrity Refactorings 19 | AddForeignKeyConstraintChange DropForeignKeyConstraintChange 20 | AddPrimaryKeyChange DropPrimaryKeyChange 21 | ;; Non-Refactoring Transformations 22 | InsertDataChange LoadDataChange LoadUpdateDataChange UpdateDataChange 23 | DeleteDataChange TagDatabaseChange StopChange 24 | ;; Architectural Refactorings 25 | CreateIndexChange DropIndexChange 26 | ;; Custom Refactorings 27 | RawSQLChange SQLFileChange) 28 | (liquibase.statement DatabaseFunction) 29 | (liquibase.util ISODateFormat)) 30 | (:use clj-liquibase.test-util) 31 | (:use clojure.test)) 32 | 33 | 34 | (defn test-change 35 | "Assert Change instance by running 'pch' partial function through arg colls. 36 | Each arg coll is a list where the first element is test description (string)." 37 | [^Class ch-class pch argcoll & argcolls] 38 | (let [ch? #(instance? ch-class %)] 39 | (doseq [each (into [argcoll] argcolls)] 40 | (let [desc (first each) 41 | args (rest each)] 42 | (is (ch? (apply pch args)) desc))))) 43 | 44 | 45 | (def min-args ["Minimum args"]) 46 | (def with-schema-name ["With :schema-name argument" :schema-name :sample]) 47 | (def with-schema ["With :schema argument" :schema :sample]) 48 | (def with-where-clause ["With :where-clause value" :where-clause "grade < 5"]) 49 | (def with-where ["With :where value" :where "code NOT NULL"]) 50 | 51 | (def long-names "Optional args with long-names") 52 | (def short-names "Optional args with short-names") 53 | 54 | 55 | ;; ----- Structural Refactorings ----- 56 | 57 | 58 | (deftest test-add-columns 59 | (testing "add-columns with required args" 60 | (let [ch change/add-columns 61 | pch (partial ch :table [[:emp :char]])] 62 | (is (thrown? IllegalArgumentException (ch :table [])) "Empty columndef-list") 63 | (is (thrown? IllegalArgumentException (ch :table [[:name]])) "Insufficient arguments") 64 | (test-change AddColumnChange (partial ch :table [[:name :type]]) 65 | min-args) 66 | (test-change AddColumnChange 67 | (partial ch :table [[:name :type][:name2 :type2]]) ["Multiple columndefs"]) 68 | (test-change AddColumnChange 69 | (partial ch :table [[:name :type :default true]]) ["With optional argument"]) 70 | (test-change AddColumnChange pch with-schema-name with-schema)))) 71 | 72 | 73 | (deftest test-rename-column 74 | (testing "rename-column" 75 | (test-change RenameColumnChange 76 | (partial change/rename-column :table :oldname :newname) 77 | min-args 78 | with-schema-name 79 | with-schema 80 | ["With :column-data-type argument" :column-data-type :char] 81 | ["With :data-type argument" :data-type :char]))) 82 | 83 | 84 | (deftest test-modify-column 85 | (testing "modify-column" 86 | (test-change ModifyDataTypeChange 87 | (partial change/modify-column :table :colname [:float 10 7]) 88 | min-args 89 | with-schema-name 90 | with-schema))) 91 | 92 | 93 | (deftest test-drop-column 94 | (testing "drop-column" 95 | (test-change DropColumnChange 96 | (partial change/drop-column :table :colname) 97 | min-args 98 | with-schema-name 99 | with-schema))) 100 | 101 | 102 | (deftest test-alter-sequence 103 | (testing "alter-sequence" 104 | (test-change AlterSequenceChange 105 | (partial change/alter-sequence :seq-name 500) 106 | min-args 107 | with-schema-name 108 | with-schema 109 | [long-names 110 | :max-value 6779 ; number or string 111 | :min-value 2000 ; number or string 112 | :ordered true ; Boolean 113 | ] 114 | [short-names 115 | :max 5697 ; number or string 116 | :min "56" ; number or string 117 | :ord true ; Boolean 118 | ]))) 119 | 120 | 121 | (deftest test-create-table 122 | (testing "create-table" 123 | (let [ct change/create-table 124 | pch (partial ct :emp [[:c1 :int] [:c2 [:varchar 30]]])] 125 | (is (thrown? IllegalArgumentException 126 | (ct :tbl-name [])) "No column-defs") 127 | (test-change CreateTableChange 128 | (partial ct :tbl-name [[:c1 :int]]) min-args) 129 | (test-change CreateTableChange pch 130 | with-schema-name 131 | with-schema 132 | [long-names 133 | :table-space :hello ; string/Keyword - s.t. clj-to-dbident 134 | :remarks "Later" ; string 135 | ] 136 | [short-names 137 | :tspace :go-air ; string/Keyword - s.t. clj-to-dbident 138 | :remarks "Notnow" ; string 139 | ]))) 140 | (testing "create-table-withid" 141 | (test-change CreateTableChange 142 | (partial change/create-table-withid :emp 143 | [[:c1 :int] [:c2 [:varchar 30]]]) 144 | min-args) 145 | (test-change CreateTableChange 146 | (partial change/create-table-withid :emp 147 | [[:c1 :int] [:c2 [:varchar 30]]]) 148 | ["With :idcol" :idcol "id"]))) 149 | 150 | 151 | (deftest test-rename-table 152 | (testing "rename-table" 153 | (test-change RenameTableChange 154 | (partial change/rename-table :old-name :new-name) 155 | min-args 156 | with-schema-name 157 | with-schema))) 158 | 159 | 160 | (deftest test-drop-table 161 | (testing "drop-table" 162 | (test-change DropTableChange 163 | (partial change/drop-table :table-name) 164 | min-args 165 | with-schema-name 166 | with-schema 167 | [long-names :cascade-constraints false] 168 | [short-names :cascade true]))) 169 | 170 | 171 | (deftest test-create-view 172 | (testing "create-view" 173 | (test-change CreateViewChange 174 | (partial change/create-view :view-name "SELECT * FROM emp;") 175 | min-args 176 | with-schema-name 177 | with-schema 178 | [long-names :replace-if-exists false] 179 | [short-names :replace true]))) 180 | 181 | 182 | (deftest test-rename-view 183 | (testing "rename-view" 184 | (test-change RenameViewChange 185 | (partial change/rename-view :old-name :new-name) 186 | min-args 187 | with-schema-name 188 | with-schema))) 189 | 190 | 191 | (deftest test-drop-view 192 | (testing "drop-view" 193 | (test-change DropViewChange 194 | (partial change/drop-view :view-name) 195 | min-args 196 | with-schema-name 197 | with-schema))) 198 | 199 | 200 | (deftest test-merge-columns 201 | (testing "merge-columns" 202 | (test-change MergeColumnChange 203 | (partial change/merge-columns 204 | :table-name :column1-name "|" :column2-name 205 | :final-column-name [:char 100]) 206 | min-args 207 | with-schema-name 208 | with-schema))) 209 | 210 | 211 | (deftest test-create-stored-procedure 212 | (testing "create-stored-procedure" 213 | (test-change CreateProcedureChange 214 | (partial change/create-stored-procedure 215 | "CREATE OR REPLACE PROCEDURE testHello 216 | IS 217 | BEGIN 218 | DBMS_OUTPUT.PUT_LINE('Hello From The Database!'); 219 | END;") 220 | min-args 221 | ["With :comments argument" :comments "foobar"]))) 222 | 223 | 224 | ;; ----- Data Quality Refactorings ----- 225 | 226 | 227 | (deftest test-add-lookup-table 228 | (testing "add-lookup-table" 229 | (test-change AddLookupTableChange 230 | (partial change/add-lookup-table 231 | :existing-table-name :existing-column-name 232 | :new-table-name :new-column-name 233 | :constraint-name) 234 | min-args 235 | [long-names 236 | :existing-table-schema-name :sample ; String/Keyword - s.t. clj-to-dbident 237 | :new-table-schema-name :another ; String/Keyword - s.t. clj-to-dbident 238 | :new-column-data-type :int ; String/vector - s.t. as-coltype 239 | ] 240 | [short-names 241 | :existing-schema :sample ; String/Keyword - s.t. clj-to-dbident 242 | :new-schema :another ; String/Keyword - s.t. clj-to-dbident 243 | :new-data-type [:char 10] ; String/vector - s.t. as-coltype 244 | ]))) 245 | 246 | 247 | (deftest test-add-not-null-constraint 248 | (testing "add-not-null-constraint" 249 | (test-change AddNotNullConstraintChange 250 | (partial change/add-not-null-constraint 251 | :table-name :column-name [:char 100]) 252 | min-args 253 | with-schema-name 254 | with-schema 255 | [long-names :default-null-value "default"] 256 | [short-names :default "default"]))) 257 | 258 | 259 | (deftest test-drop-not-null-constraint 260 | (testing "drop-not-null-constraint" 261 | (test-change DropNotNullConstraintChange 262 | (partial change/drop-not-null-constraint :table-name :column-name) 263 | min-args 264 | with-schema-name 265 | with-schema 266 | [long-names :column-data-type [:char 100]] 267 | [short-names :data-type [:char 100]]))) 268 | 269 | 270 | (deftest test-add-unique-constraint 271 | (testing "add-unique-constraint" 272 | (test-change AddUniqueConstraintChange 273 | (partial change/add-unique-constraint 274 | :table-name [:col1 :col2] :constraint-name) 275 | min-args 276 | with-schema-name 277 | with-schema 278 | [long-names 279 | :table-space :tspace ; String/Keyword - s.t. clj-to-dbident 280 | :deferrable false ; Boolean 281 | :initially-deferred true ; Boolean 282 | :disabled false ; Boolean 283 | ] 284 | [short-names 285 | :tspace :tspace ; String/Keyword - s.t. clj-to-dbident 286 | :defer false ; Boolean 287 | :idefer true ; Boolean 288 | :disabled true ; Boolean 289 | ]))) 290 | 291 | 292 | (deftest test-drop-unique-constraint 293 | (testing "drop-unique-constraint" 294 | (test-change DropUniqueConstraintChange 295 | (partial change/drop-unique-constraint :table-name :constraint-name) 296 | min-args 297 | with-schema-name 298 | with-schema))) 299 | 300 | 301 | (deftest test-create-sequence 302 | (testing "create-sequence" 303 | (test-change CreateSequenceChange 304 | (partial change/create-sequence :sequence-name) 305 | min-args 306 | with-schema-name 307 | with-schema 308 | [long-names 309 | :start-value 1000 ; BigInteger 310 | :increment-by 1 ; BigInteger 311 | :max-value 9999 ; BigInteger 312 | :min-value 1000 ; BigInteger 313 | :ordered true ; Boolean 314 | :cycle true ; Boolean 315 | ] 316 | [short-names 317 | :start 1000 ; BigInteger 318 | :incby 1 ; BigInteger 319 | :max 9999 ; BigInteger 320 | :min 1000 ; BigInteger 321 | :ord true ; Boolean 322 | :cyc true ; Boolean 323 | ]))) 324 | 325 | 326 | (deftest test-drop-sequence 327 | (testing "drop-sequence" 328 | (test-change DropSequenceChange 329 | (partial change/drop-sequence :sequence-name) 330 | min-args 331 | with-schema-name 332 | with-schema))) 333 | 334 | 335 | (deftest test-add-auto-increment 336 | (testing "add-auto-increment" 337 | (test-change AddAutoIncrementChange 338 | (partial change/add-auto-increment 339 | :table-name :column-name :column-data-type) 340 | min-args 341 | with-schema-name 342 | with-schema))) 343 | 344 | 345 | (deftest test-add-default-value 346 | (testing "add-default-value" 347 | (doseq [each ["default-value" 100 (Date.) true (change/dbfn "NOW") 348 | (java.sql.Date. (.getTime (Date.))) 349 | (java.sql.Time. (.getTime (Date.))) 350 | (java.sql.Timestamp. (.getTime (Date.)))]] 351 | (test-change AddDefaultValueChange 352 | (partial change/add-default-value :table-name :column-name each) 353 | min-args 354 | with-schema-name 355 | with-schema)))) 356 | 357 | 358 | (deftest test-drop-default-value 359 | (testing "drop-default-value" 360 | (test-change DropDefaultValueChange 361 | (partial change/drop-default-value :table-name :column-name) 362 | min-args 363 | with-schema-name 364 | with-schema 365 | ["With :column-data-type value" :column-data-type [:char 100]] 366 | ["With :data-type value" :data-type [:float 12 5]]))) 367 | 368 | 369 | ;; ----- Referential Integrity Refactorings ----- 370 | 371 | 372 | (deftest test-add-foreign-key-constraint 373 | (testing "add-foreign-key-constraint" 374 | (test-change AddForeignKeyConstraintChange 375 | (partial change/add-foreign-key-constraint 376 | :constraint-name :base-table-name [:base-column1 :base-column2] 377 | :referenced-table-name [:referenced-column1 :referenced-column2]) 378 | min-args 379 | [long-names 380 | :base-table-schema-name :base ; String 381 | :referenced-table-schema-name :ref ; String 382 | :deferrable false ; Boolean 383 | :initially-deferred true ; Boolean 384 | :on-delete "none" ; String 385 | :on-update "none" ; String 386 | ] 387 | [short-names 388 | :base-schema :base ; String 389 | :ref-schema :ref ; String 390 | :defer false ; Boolean 391 | :idefer true ; Boolean 392 | :ondel "none" ; String 393 | :onupd "none" ; String 394 | ]))) 395 | 396 | 397 | (deftest test-drop-foreign-key-constraint 398 | (testing "drop-foreign-key-constraint" 399 | (test-change DropForeignKeyConstraintChange 400 | (partial change/drop-foreign-key-constraint :constraint-name :base-table) 401 | min-args 402 | with-schema-name 403 | with-schema))) 404 | 405 | 406 | (deftest test-add-primary-key 407 | (testing "add-primary-key" 408 | (test-change AddPrimaryKeyChange 409 | (partial change/add-primary-key :table-name [:col1 :col2] :constraint-name) 410 | min-args 411 | with-schema-name 412 | with-schema 413 | ["With :table-space value" :table-space :space] 414 | ["With :tspace value" :tspace :space]))) 415 | 416 | 417 | (deftest test-drop-primary-key 418 | (testing "drop-primary-key" 419 | (test-change DropPrimaryKeyChange 420 | (partial change/drop-primary-key :table-name) 421 | min-args 422 | with-schema-name 423 | with-schema 424 | ["With :constraint-name value" :constraint-name :constraint] 425 | ["With :constr value" :constr :constraint]))) 426 | 427 | 428 | ;; ----- Non-Refactoring Transformations ----- 429 | 430 | 431 | (deftest test-insert-data 432 | (testing "insert-data" 433 | (test-change InsertDataChange 434 | (partial change/insert-data :table-name 435 | {:name "Abraham" :age 30 :male true :joined (Date.) :now (change/dbfn "NOW")}) 436 | min-args 437 | with-schema-name 438 | with-schema))) 439 | 440 | 441 | (deftest test-load-data 442 | (testing "load-data" 443 | (test-change LoadDataChange 444 | (partial change/load-data :table-name "filename.csv" 445 | (array-map :col1 :string :col2 :numeric :col3 :date :col4 :boolean)) 446 | min-args 447 | with-schema-name 448 | with-schema 449 | ["With :encoding value" :encoding "UTF-16"] 450 | ["With :enc value" :enc "UTF-8"]))) 451 | 452 | 453 | (deftest test-load-update-data 454 | (testing "load-update-data" 455 | (test-change LoadUpdateDataChange 456 | (partial change/load-update-data :table-name "filename.csv" 457 | [:pk-col1 :pk-col2] 458 | (array-map :col1 :string :col2 :numeric :col3 :date :col4 :boolean)) 459 | min-args 460 | with-schema-name 461 | with-schema 462 | ["With :encoding value" :encoding "UTF-16"] 463 | ["With :enc value" :enc "UTF-8"]))) 464 | 465 | 466 | (deftest test-update-data 467 | (testing "update-data" 468 | (test-change UpdateDataChange 469 | (partial change/update-data :table-name 470 | {:name "Abraham" :age 30 :male true :joined (Date.) :now (change/dbfn "NOW")}) 471 | min-args 472 | with-schema-name 473 | with-schema 474 | with-where-clause 475 | with-where))) 476 | 477 | 478 | (deftest test-delete-data 479 | (testing "delete-data" 480 | (test-change DeleteDataChange 481 | (partial change/delete-data :table-name) 482 | min-args 483 | with-schema-name 484 | with-schema 485 | with-where-clause 486 | with-where))) 487 | 488 | 489 | (deftest test-tag-database 490 | (testing "tag-database" 491 | (test-change TagDatabaseChange 492 | (partial change/tag-database :tag) 493 | min-args))) 494 | 495 | 496 | (deftest test-stop 497 | (testing "stop" 498 | (test-change StopChange change/stop min-args) 499 | (test-change StopChange (partial change/stop "Some message") min-args))) 500 | 501 | 502 | ;; ----- Architectural Refactorings ----- 503 | 504 | 505 | (deftest test-create-index 506 | (testing "create-index" 507 | (test-change CreateIndexChange 508 | (partial change/create-index :table-name [:col1 :col2]) 509 | min-args 510 | with-schema-name 511 | with-schema 512 | [long-names 513 | :index-name "fooindex" ; String 514 | :unique true ; Boolean 515 | :table-space "space" ; String 516 | ] 517 | [short-names 518 | :index "fooindex" ; String 519 | :uniq false ; Boolean 520 | :tspace "space" ; String 521 | ]))) 522 | 523 | 524 | (deftest test-drop-index 525 | (testing "drop-index" 526 | (test-change DropIndexChange 527 | (partial change/drop-index :index-name :table-name) 528 | min-args 529 | with-schema-name 530 | with-schema))) 531 | 532 | 533 | ;; ----- Custom Refactorings ----- 534 | 535 | 536 | (deftest test-sql 537 | (testing "sql" 538 | (test-change RawSQLChange 539 | (partial change/sql "SELECT * FROM DATABASECHAGELOG") 540 | ["With :comment value" :comment "This is a comment"] 541 | ["With :dbms value" :dbms "h2"] 542 | ["With :end-delimiter value" :end-delimiter ";"] 543 | ["With :split-statements value" :split-statements true] 544 | ["With :strip-comments value" :strip-comments true]))) 545 | 546 | 547 | (deftest test-sql-file 548 | (testing "sql-file" 549 | (test-change SQLFileChange 550 | (partial change/sql-file "test-sql-file.sql") 551 | ["With :dbms value" :dbms "h2"] 552 | ["With :encoding value" :encoding "utf-8"] 553 | ["With :end-delimiter value" :end-delimiter ";"] 554 | ["With :split-statements value" :split-statements true] 555 | ["With :strip-comments value" :strip-comments true]))) 556 | 557 | 558 | ;; ***** Run the tests ***** 559 | 560 | 561 | (defn test-ns-hook [] 562 | ;; ----- Structural Refactorings ----- 563 | (test-add-columns) 564 | (test-rename-column) 565 | (test-modify-column) 566 | (test-drop-column) 567 | (test-alter-sequence) 568 | (test-create-table) 569 | (test-rename-table) 570 | (test-drop-table) 571 | (test-create-view) 572 | (test-rename-view) 573 | (test-drop-view) 574 | (test-merge-columns) 575 | (test-create-stored-procedure) 576 | ;; ----- Data Quality Refactorings ----- 577 | (test-add-lookup-table) 578 | (test-add-not-null-constraint) 579 | (test-drop-not-null-constraint) 580 | (test-add-unique-constraint) 581 | (test-drop-unique-constraint) 582 | (test-create-sequence) 583 | (test-drop-sequence) 584 | (test-add-auto-increment) 585 | (test-add-default-value) 586 | (test-drop-default-value) 587 | ;; ----- Referential Integrity Refactorings ----- 588 | (test-add-foreign-key-constraint) 589 | (test-drop-foreign-key-constraint) 590 | (test-add-primary-key) 591 | (test-drop-primary-key) 592 | ;; ----- Non-Refactoring Transformations ----- 593 | (test-insert-data) 594 | (test-load-data) 595 | (test-load-update-data) 596 | (test-update-data) 597 | (test-delete-data) 598 | (test-tag-database) 599 | (test-stop) 600 | ;; ----- Architectural Refactorings ----- 601 | (test-create-index) 602 | (test-drop-index) 603 | ;; ----- Custom Refactorings ----- 604 | (test-sql) 605 | (test-sql-file)) 606 | -------------------------------------------------------------------------------- /test/clj_liquibase/test_cli.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-cli 2 | (:require 3 | [clj-liquibase.cli :as ll] 4 | [clj-liquibase.core :as lb] 5 | [clj-liquibase.test-core :as tl]) 6 | (:import 7 | (java.io File) 8 | (java.sql SQLException)) 9 | (:use clj-liquibase.test-util) 10 | (:use [clojure.test])) 11 | 12 | 13 | (deftest test-update-args 14 | (testing "update args" 15 | (let [p {} 16 | a {:datasource "foo.db/ds" :changelog "foo.db/cl"}] 17 | (is (= a (ll/parse-update-args p "--datasource=foo.db/ds" "-cfoo.db/cl")) "--datasource") 18 | (is (= a (ll/parse-update-args p "-dfoo.db/ds" "-cfoo.db/cl")) "-d")) 19 | (let [p {:datasource :foo} 20 | a {:changelog "foo.db/default" :datasource :foo}] 21 | (is (= a (ll/parse-update-args p "--changelog=foo.db/default")) "--changelog") 22 | (is (= a (ll/parse-update-args p "-cfoo.db/default")) "-c")) 23 | (let [p {:datasource :foo} 24 | a {:chs-count "5" :changelog "x" :datasource :foo}] 25 | (is (= a (ll/parse-update-args p "--chs-count=5" "-cx")) "--chs-count") 26 | (is (= a (ll/parse-update-args p "-n5" "-cx")) "-n")) 27 | (let [p {:datasource :foo} 28 | a {:contexts "a,b" :changelog "x" :datasource :foo}] 29 | (is (= a (ll/parse-update-args p "--contexts=a,b" "-cx")) "--contexts") 30 | (is (= a (ll/parse-update-args p "-ta,b" "-cx")) "-t")) 31 | (let [p {:datasource :foo} 32 | a {:sql-only nil :changelog "x" :datasource :foo}] 33 | (is (= a (ll/parse-update-args p "--sql-only" "-cx")) "--sql-only") 34 | (is (= a (ll/parse-update-args p "-s" "-cx")) "-s")) 35 | (let [p {:datasource :foo} 36 | a {:changelog "foo.db/default" 37 | :chs-count "5" 38 | :contexts "a,b" 39 | :sql-only nil 40 | :datasource :foo}] 41 | (is (= a (ll/parse-update-args p 42 | "--changelog=foo.db/default" 43 | "--chs-count=5" 44 | "--contexts=a,b" 45 | "--sql-only")) "all combined (full version)") 46 | (is (= a (ll/parse-update-args p 47 | "-cfoo.db/default" 48 | "-n5" 49 | "-ta,b" 50 | "-s")) "all combined (short version)") 51 | (is (thrown? IllegalArgumentException (ll/parse-update-args p "--bad"))) 52 | (is (= {:help nil} (ll/parse-update-args p "--help")))))) 53 | 54 | 55 | (deftest test-update 56 | (testing "all defaults" 57 | (tl/with-lb-action (tl/clb-setup)) 58 | (ll/update {:datasource (tl/make-ds) :changelog tl/clog-1})) 59 | (testing "datasource default and changelog arg" 60 | (tl/with-lb-action (tl/clb-setup)) 61 | (ll/update {:datasource (tl/make-ds)} 62 | "-cclj-liquibase.test-core/clog-1")) 63 | (testing "entrypoint with defaults" 64 | (tl/with-lb-action (tl/clb-setup)) 65 | (ll/entry "update" {:datasource (tl/make-ds) :changelog tl/clog-1})) 66 | (testing "entrypoint with long args" 67 | (tl/with-lb-action (tl/clb-setup)) 68 | (ll/entry "update" {:datasource (tl/make-ds)} 69 | "--changelog=clj-liquibase.test-core/clog-1")) 70 | (testing "entrypoint with short args" 71 | (tl/with-lb-action (tl/clb-setup)) 72 | (ll/entry "update" {:datasource (tl/make-ds)} 73 | "-cclj-liquibase.test-core/clog-1"))) 74 | 75 | 76 | (deftest test-rollback-args 77 | (testing "rollback args" 78 | (let [p {:datasource :foo} 79 | a {:changelog "foo.db/default" :datasource :foo}] 80 | (is (= a (ll/parse-rollback-args p "--changelog=foo.db/default")) "--changelog") 81 | (is (= a (ll/parse-rollback-args p "-cfoo.db/default")) "-c")) 82 | (let [p {:datasource :foo} 83 | a {:chs-count "5" :changelog "x" :datasource :foo}] 84 | (is (= a (ll/parse-rollback-args p "--chs-count=5" "-cx")) "--chs-count") 85 | (is (= a (ll/parse-rollback-args p "-n5" "-cx")) "-n")) 86 | (let [p {:datasource :foo} 87 | a {:tag "v2.0" :changelog "x" :datasource :foo}] 88 | (is (= a (ll/parse-rollback-args p "--tag=v2.0" "-cx")) "--tag") 89 | (is (= a (ll/parse-rollback-args p "-gv2.0" "-cx")) "-g")) 90 | (let [p {:datasource :foo} 91 | a {:date "2011-02-26" :changelog "x" :datasource :foo}] 92 | (is (= a (ll/parse-rollback-args p "--date=2011-02-26" "-cx")) "--date") 93 | (is (= a (ll/parse-rollback-args p "-e2011-02-26" "-cx")) "-e")) 94 | (let [p {:datasource :foo} 95 | a {:contexts "a,b" :changelog "x" :datasource :foo}] 96 | (is (= a (ll/parse-rollback-args p "--contexts=a,b" "-cx")) "--contexts") 97 | (is (= a (ll/parse-rollback-args p "-ta,b" "-cx")) "-t")) 98 | (let [p {:datasource :foo} 99 | a {:sql-only nil :changelog "x" :datasource :foo}] 100 | (is (= a (ll/parse-rollback-args p "--sql-only" "-cx")) "--sql-only") 101 | (is (= a (ll/parse-rollback-args p "-s" "-cx")) "-s")) 102 | (let [p {:datasource :foo} 103 | a {:changelog "foo.db/default" 104 | :chs-count "5" 105 | :contexts "a,b" 106 | :sql-only nil 107 | :datasource :foo}] 108 | (is (= a (ll/parse-rollback-args p 109 | "--changelog=foo.db/default" 110 | "--chs-count=5" 111 | "--contexts=a,b" 112 | "--sql-only")) "all combined (full version)") 113 | (is (= a (ll/parse-rollback-args p 114 | "-cfoo.db/default" 115 | "-n5" 116 | "-ta,b" 117 | "-s")) "all combined (short version)") 118 | (is (thrown? IllegalArgumentException (ll/parse-rollback-args p "--bad"))) 119 | (is (= {:help nil} (ll/parse-rollback-args p "--help")))))) 120 | 121 | 122 | (deftest test-rollback 123 | (testing "all defaults" 124 | (tl/with-lb-action 125 | (tl/clb-setup) 126 | (lb/update tl/clog-1) 127 | (lb/tag "mytag") 128 | (lb/update tl/clog-2) 129 | (is (zero? (count (query "SELECT * FROM sampletable3"))))) 130 | (ll/rollback {:datasource (tl/make-ds) :changelog tl/clog-2 :tag "mytag"}) 131 | (tl/with-lb-action 132 | (is (thrown? SQLException 133 | (query "SELECT * FROM sampletable3")) "Table should not exist"))) 134 | (testing "datasource default, changelog arg, tag arg" 135 | (tl/with-lb-action 136 | (tl/clb-setup) 137 | (lb/update tl/clog-1) 138 | (lb/tag "mytag") 139 | (lb/update tl/clog-2) 140 | (is (zero? (count (query "SELECT * FROM sampletable3"))))) 141 | (ll/rollback {:datasource (tl/make-ds)} 142 | "-cclj-liquibase.test-core/clog-2" "-gmytag") 143 | (tl/with-lb-action 144 | (is (thrown? SQLException 145 | (query "SELECT * FROM sampletable3")) "Table should not exist"))) 146 | (testing "entrypoint with defaults" 147 | (tl/with-lb-action 148 | (tl/clb-setup) 149 | (lb/update tl/clog-1) 150 | (lb/tag "mytag") 151 | (lb/update tl/clog-2) 152 | (is (zero? (count (query "SELECT * FROM sampletable3"))))) 153 | (ll/entry "rollback" {:datasource (tl/make-ds) :changelog tl/clog-2 :tag "mytag"}) 154 | (tl/with-lb-action 155 | (is (thrown? SQLException 156 | (query "SELECT * FROM sampletable3")) "Table should not exist"))) 157 | (testing "entrypoint with long args" 158 | (tl/with-lb-action 159 | (tl/clb-setup) 160 | (lb/update tl/clog-1) 161 | (lb/tag "mytag") 162 | (lb/update tl/clog-2) 163 | (is (zero? (count (query "SELECT * FROM sampletable3"))))) 164 | (ll/entry "rollback" {:datasource (tl/make-ds)} 165 | "--changelog=clj-liquibase.test-core/clog-2" "--tag=mytag") 166 | (tl/with-lb-action 167 | (is (thrown? SQLException 168 | (query "SELECT * FROM sampletable3")) "Table should not exist"))) 169 | (testing "entrypoint with short args" 170 | (tl/with-lb-action 171 | (tl/clb-setup) 172 | (lb/update tl/clog-1) 173 | (lb/tag "mytag") 174 | (lb/update tl/clog-2) 175 | (is (zero? (count (query "SELECT * FROM sampletable3"))))) 176 | (ll/entry "rollback" {:datasource (tl/make-ds)} 177 | "-cclj-liquibase.test-core/clog-2" "-gmytag") 178 | (tl/with-lb-action 179 | (is (thrown? SQLException 180 | (query "SELECT * FROM sampletable3")) "Table should not exist")))) 181 | 182 | 183 | (deftest test-tag-args 184 | (testing "rollback args" 185 | (let [p {} 186 | a {:tag "y" :datasource "foo/bar"}] 187 | (is (= a (ll/parse-tag-args p "--datasource=foo/bar" "-gy")) "--datasource") 188 | (is (= a (ll/parse-tag-args p "-dfoo/bar" "-gy")) "-d")) 189 | (let [p {:datasource :foo} 190 | a {:tag "v2.0" :datasource :foo}] 191 | (is (= a (ll/parse-tag-args p "--tag=v2.0")) "--tag") 192 | (is (= a (ll/parse-tag-args p "-gv2.0")) "-g")) 193 | (let [p {:datasource :foo} 194 | a {:tag "foo" 195 | :datasource :foo}] 196 | (is (= a (ll/parse-tag-args p 197 | "--tag=foo")) "all combined (full version)") 198 | (is (= a (ll/parse-tag-args p 199 | "-gfoo")) "all combined (short version)") 200 | (is (thrown? IllegalArgumentException (ll/parse-tag-args p "--bad"))) 201 | (is (= {:help nil} (ll/parse-tag-args p "--help")))))) 202 | 203 | 204 | (deftest test-tag 205 | (testing "all defaults" 206 | (tl/with-lb-action 207 | (tl/clb-setup) 208 | (lb/update tl/clog-1) 209 | (lb/tag "mytag")) 210 | (ll/tag {:datasource (tl/make-ds) :tag "mytag"}) 211 | (tl/with-lb-action 212 | (is (= "mytag" (query-value "SELECT tag FROM databasechangelog")) 213 | "Tag name should match"))) 214 | (testing "datasource default, changelog arg, tag arg" 215 | (tl/with-lb-action 216 | (tl/clb-setup) 217 | (lb/update tl/clog-1) 218 | (lb/tag "mytag")) 219 | (ll/tag {:datasource (tl/make-ds)} 220 | "-gmytag") 221 | (tl/with-lb-action 222 | (is (= "mytag" (query-value "SELECT tag FROM databasechangelog")) 223 | "Tag name should match"))) 224 | (testing "entrypoint with defaults" 225 | (tl/with-lb-action 226 | (tl/clb-setup) 227 | (lb/update tl/clog-1) 228 | (lb/tag "mytag")) 229 | (ll/entry "tag" {:datasource (tl/make-ds) :tag "mytag"}) 230 | (tl/with-lb-action 231 | (is (= "mytag" (query-value "SELECT tag FROM databasechangelog")) 232 | "Tag name should match"))) 233 | (testing "entrypoint with long args" 234 | (tl/with-lb-action 235 | (tl/clb-setup) 236 | (lb/update tl/clog-1) 237 | (lb/tag "mytag")) 238 | (ll/entry "tag" {:datasource (tl/make-ds)} 239 | "--tag=mytag") 240 | (tl/with-lb-action 241 | (is (= "mytag" (query-value "SELECT tag FROM databasechangelog")) 242 | "Tag name should match"))) 243 | (testing "entrypoint with short args" 244 | (tl/with-lb-action 245 | (tl/clb-setup) 246 | (lb/update tl/clog-1) 247 | (lb/tag "mytag")) 248 | (ll/entry "tag" {:datasource (tl/make-ds)} 249 | "-gmytag") 250 | (tl/with-lb-action 251 | (is (= "mytag" (query-value "SELECT tag FROM databasechangelog")) 252 | "Tag name should match")))) 253 | 254 | 255 | (deftest test-dbdoc-args 256 | (testing "dbdoc args" 257 | (let [p {} 258 | a {:datasource "foo/bar" :output-dir "y" :changelog "x"}] 259 | (is (= a (ll/parse-dbdoc-args p "--datasource=foo/bar" "-cx" "-oy")) "--datasource") 260 | (is (= a (ll/parse-dbdoc-args p "-dfoo/bar" "-cx" "-oy")) "-d")) 261 | (let [p {:datasource :foo} 262 | a {:output-dir "y" :changelog "foo.db/default" :datasource :foo}] 263 | (is (= a (ll/parse-dbdoc-args p "--changelog=foo.db/default" "-oy")) "--changelog") 264 | (is (= a (ll/parse-dbdoc-args p "-cfoo.db/default" "-oy")) "-c")) 265 | (let [p {:datasource :foo} 266 | a {:output-dir "foo/bar" :changelog "x" :datasource :foo}] 267 | (is (= a (ll/parse-dbdoc-args p "--output-dir=foo/bar" "-cx")) "--output-dir") 268 | (is (= a (ll/parse-dbdoc-args p "-ofoo/bar" "-cx")) "-o")) 269 | (let [p {:datasource :foo} 270 | a {:datasource :foo 271 | :changelog "foo.db/default" 272 | :output-dir "foo"}] 273 | (is (= a (ll/parse-dbdoc-args p 274 | "--changelog=foo.db/default" 275 | "--output-dir=foo")) "all combined (full version)") 276 | (is (= a (ll/parse-dbdoc-args p 277 | "-cfoo.db/default" 278 | "-ofoo")) "all combined (short version)") 279 | (is (thrown? IllegalArgumentException (ll/parse-dbdoc-args p "--bad"))) 280 | (is (= {:help nil} (ll/parse-dbdoc-args p "--help")))))) 281 | 282 | 283 | (defn rm-rf 284 | [^File file] 285 | (let [file (if (instance? File file) file (File. (str file)))] 286 | (cond 287 | ;; directory 288 | (.isDirectory file) 289 | (doseq [^File each (.listFiles file)] 290 | (rm-rf each)) 291 | ;; file 292 | (.isFile file) 293 | (if (not (.delete file)) 294 | (throw (RuntimeException. (str "Cannot delete file: " file))))))) 295 | 296 | 297 | (deftest test-dbdoc 298 | (testing "all defaults" 299 | (rm-rf "target/dbdoc") 300 | (tl/with-lb-action 301 | (tl/clb-setup) 302 | (lb/update tl/clog-1)) 303 | (ll/dbdoc {:datasource (tl/make-ds) :changelog tl/clog-2 :output-dir "target/dbdoc"}) 304 | (is (.exists (File. "target/dbdoc/index.html")))) 305 | (testing "datasource default, changelog arg, tag arg" 306 | (rm-rf "target/dbdoc") 307 | (tl/with-lb-action 308 | (tl/clb-setup) 309 | (lb/update tl/clog-1)) 310 | (ll/dbdoc {:datasource (tl/make-ds)} 311 | "--changelog=clj-liquibase.test-core/clog-2" "--output-dir=target/dbdoc") 312 | (is (.exists (File. "target/dbdoc/index.html")))) 313 | (testing "entrypoint with defaults" 314 | (rm-rf "target/dbdoc") 315 | (tl/with-lb-action 316 | (tl/clb-setup) 317 | (lb/update tl/clog-1)) 318 | (ll/entry "dbdoc" {:datasource (tl/make-ds) :changelog tl/clog-2 :output-dir "target/dbdoc"}) 319 | (is (.exists (File. "target/dbdoc/index.html")))) 320 | (testing "entrypoint with long args" 321 | (rm-rf "target/dbdoc") 322 | (tl/with-lb-action 323 | (tl/clb-setup) 324 | (lb/update tl/clog-1)) 325 | (ll/entry "dbdoc" {:datasource (tl/make-ds)} 326 | "--changelog=clj-liquibase.test-core/clog-2" "--output-dir=target/dbdoc") 327 | (is (.exists (File. "target/dbdoc/index.html")))) 328 | (testing "entrypoint with short args" 329 | (rm-rf "target/dbdoc") 330 | (tl/with-lb-action 331 | (tl/clb-setup) 332 | (lb/update tl/clog-1)) 333 | (ll/entry "dbdoc" {:datasource (tl/make-ds)} 334 | "-cclj-liquibase.test-core/clog-2" "-otarget/dbdoc") 335 | (is (.exists (File. "target/dbdoc/index.html"))))) 336 | 337 | 338 | (deftest test-diff-args 339 | (testing "diff args" 340 | (let [p {} 341 | a {:datasource "foo/bar" 342 | :ref-datasource "bar/baz"}] 343 | (is (= a (ll/parse-diff-args p "--datasource=foo/bar" "-rbar/baz")) "--datasource") 344 | (is (= a (ll/parse-diff-args p "-dfoo/bar" "-rbar/baz")) "-d") 345 | (is (= a (ll/parse-diff-args p "-dfoo/bar" "--ref-datasource=bar/baz")) "--ref-datasource") 346 | (is (= a (ll/parse-diff-args p "-dfoo/bar" "-rbar/baz")) "-r")) 347 | (let [p {:datasource :foo} 348 | a {:datasource "bar" :ref-datasource "baz"}] 349 | (is (= a (ll/parse-diff-args p "-dbar" "-rbaz")) "override defaults via CLI arg")) 350 | (is (= {:help nil} (ll/parse-diff-args {} "--help"))))) 351 | 352 | 353 | (def ref-ds (tl/make-ds)) 354 | 355 | 356 | (deftest test-diff 357 | (tl/with-lb-action 358 | (tl/clb-setup) 359 | (lb/update tl/clog-1)) 360 | (testing "all defaults" 361 | (ll/diff {:datasource (tl/make-ds) :ref-datasource (tl/make-ds)})) 362 | (testing "datasource default, ref-datasource arg" 363 | (ll/diff {:datasource (tl/make-ds)} 364 | "-rclj-liquibase.test-cli/ref-ds")) 365 | (testing "entrypoint with all defaults" 366 | (ll/entry "diff" {:datasource (tl/make-ds) :ref-datasource (tl/make-ds)})) 367 | (testing "entrypoint with datasource default, ref-datasource long arg" 368 | (ll/entry "diff" {:datasource (tl/make-ds)} 369 | "--ref-datasource=clj-liquibase.test-cli/ref-ds")) 370 | (testing "entrypoint with datasource default, ref-datasource short arg" 371 | (ll/entry "diff" {:datasource (tl/make-ds)} 372 | "-rclj-liquibase.test-cli/ref-ds"))) 373 | 374 | 375 | (defn test-ns-hook 376 | [] 377 | (test-update-args) 378 | (test-update) 379 | (test-rollback-args) 380 | (test-rollback) 381 | (test-tag-args) 382 | (test-tag) 383 | (test-dbdoc-args) 384 | (test-dbdoc) 385 | (test-diff-args) 386 | (test-diff)) -------------------------------------------------------------------------------- /test/clj_liquibase/test_core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-core 2 | (:require 3 | [clojure.string :as sr] 4 | [clojure.pprint :as pp] 5 | [clj-miscutil.core :as mu] 6 | [clj-liquibase.core :as lb] 7 | [clj-liquibase.change :as ch] 8 | [clj-dbcp.core :as dbcp] 9 | [clj-jdbcutil.core :as spec]) 10 | (:import 11 | (java.io File) 12 | (java.sql Connection SQLException) 13 | (javax.sql DataSource) 14 | (clj_jdbcutil.core IRow) 15 | (clj_jdbcutil.core Row)) 16 | (:use clj-liquibase.test-util) 17 | (:use clojure.test)) 18 | 19 | 20 | (defn clb-setup 21 | "Setup database for running tests" 22 | [] 23 | (spec/with-connection spec/*dbspec* 24 | (let [conn (:connection spec/*dbspec*)] 25 | (assert (or (println "Testing connection") conn (println "= NULL"))) 26 | (with-open [stmt (.createStatement ^Connection conn)] 27 | (doseq [each [[:sample-view-1 "VIEW"] 28 | [:foo "SEQUENCE"] 29 | :sample-table-1 :sample-table-2 "sampletable3" 30 | :databasechangelog :databasechangeloglock]] 31 | (try 32 | (let [[n t] (as-vector each) 33 | t (or t "TABLE")] 34 | (.executeUpdate stmt (format "DROP %s %s" 35 | t (spec/db-iden n))) 36 | (println "Deleted " t (spec/db-iden n))) 37 | (catch SQLException e 38 | (println "Ignoring exception: " e)))))))) 39 | 40 | 41 | (def db {:h2-mem {:dbcp #(dbcp/make-datasource :h2 {:target :memory :database "default"}) 42 | :int "INTEGER"} 43 | :mysql {:dbcp #(dbcp/make-datasource :mysql {:host "localhost" :database "bituf" 44 | :user "root" :password "root"}) 45 | :int "INT"}}) 46 | 47 | 48 | (def dialect :h2-mem) 49 | ;(def dialect :mysql) 50 | 51 | 52 | (defn make-ds [] {:post [(instance? DataSource %)]} 53 | ((:dbcp (dialect db)))) 54 | 55 | 56 | (defn db-int [] {:post [(string? %)]} 57 | (:int (dialect db))) 58 | 59 | 60 | (defn dbspec [] {:post [(map? %)]} 61 | (spec/make-dbspec (make-ds))) 62 | 63 | 64 | (defn lb-action 65 | "Run Liquibase action" 66 | [f] {:post [(mu/not-fn? %)] 67 | :pre [(fn? f)]} 68 | (spec/with-connection (dbspec) 69 | (lb/with-lb 70 | (mu/! (f))))) 71 | 72 | 73 | (defmacro with-lb-action 74 | "Run body of code as Liquibase action" 75 | [& body] 76 | `(lb-action (fn [] ~@body))) 77 | 78 | 79 | (def ct-change1 (mu/! (ch/create-table :sample-table-1 80 | [[:id :int :null false :pk true :autoinc true :pkname :pk-sample-table-1] 81 | [:name [:varchar 40] :null false] 82 | [:gender [:char 1] :null false]]))) 83 | 84 | (def ct-change2 (mu/! (ch/create-table :sample-table-2 85 | [[:id :int :null false :pk true :autoinc true] 86 | [:f-id :int] 87 | [:name [:varchar 40] :null false] 88 | [:gender [:char 1] :null false]]))) 89 | 90 | (def ct-change3 (mu/! (ch/create-table "sampletable3" 91 | [[:id :int :null false :pk true :autoinc true] 92 | [:name [:varchar 40] :null false] 93 | [:gender [:char 1] :null false]]))) 94 | 95 | 96 | (def changeset-1 ["id=1" "author=shantanu" [ct-change1 ct-change2]]) 97 | 98 | 99 | (def changeset-2 ["id=2" "author=shantanu" [ct-change3]]) 100 | 101 | 102 | (lb/defchangelog clog-1 "core" [changeset-1]) 103 | 104 | 105 | (lb/defchangelog clog-2 "core" [changeset-1 changeset-2]) 106 | 107 | 108 | ;; ===== ChangeSet ===== 109 | 110 | 111 | (deftest test-make-changeset 112 | (testing "make-changeset" 113 | (let [pf (partial lb/make-changeset "id=1" "author=shantanu" [ct-change1])] 114 | (is (lb/changeset? (pf :filepath "dummy")) "Minimum arguments") 115 | (is (lb/changeset? (pf 116 | :logical-filepath "somename" 117 | :dbms :mysql 118 | :run-always true 119 | :run-on-change false 120 | :context "some-ctx" 121 | :run-in-transaction true 122 | :fail-on-error true 123 | :comment "describe this" 124 | :pre-conditions nil 125 | :rollback-changes [] 126 | :valid-checksum "1234" 127 | )) "Optional arguments with longname") 128 | (is (lb/changeset? (pf 129 | :filepath "dummy" 130 | :always false 131 | :on-change true 132 | :ctx "sample" 133 | :in-txn false 134 | :fail-err true 135 | :pre-cond nil 136 | :rollback [] 137 | :valid-csum "something" 138 | )) "Optional arguments with shortname")))) 139 | 140 | 141 | ;; ===== DatabaseChangeLog ===== 142 | 143 | 144 | (deftest test-make-changelog 145 | (testing "make-changelog" 146 | (let [cl? lb/changelog? 147 | fp "dummy" 148 | mcl lb/make-changelog] 149 | (is (thrown? IllegalArgumentException (cl? (mcl fp []))) "No changeset") 150 | (is (cl? (mcl fp [changeset-1] )) "1 changeset") 151 | (is (cl? (mcl fp [changeset-1 changeset-2])) "2 changesets") 152 | (is (cl? (mcl fp [changeset-1 changeset-2] 153 | :pre-conditions nil)) 154 | "With optional args - long names") 155 | (is (cl? (mcl fp [changeset-1 changeset-2] 156 | :pre-cond nil)) 157 | "With optional args - short names")))) 158 | 159 | 160 | (deftest test-defchangelog 161 | (testing "defchangelog" 162 | (is (fn? clog-1)) 163 | (is (fn? clog-2)))) 164 | 165 | 166 | ;; ===== Actions ===== 167 | 168 | 169 | (defn update-test-helper 170 | "Example table-desc is below: 171 | [:table-name 172 | [:id :int :null false :pk true :autoinc true] 173 | [:name [:varchar 40] :null false] 174 | [:gender [:char 1] :null false]]" 175 | [table-desc & more] 176 | (println "Entered uth") 177 | (let [u-tables (into [table-desc] more)] 178 | (println (format "Asserting %d tables" (count u-tables))) 179 | (mu/! 180 | (doseq [each u-tables] 181 | (let [[^String t-name & t-cols] each 182 | conn ^java.sql.Connection (:connection spec/*dbspec*) 183 | _ (assert (mu/not-nil? conn)) 184 | dbmdata (.getMetaData conn) 185 | _ (assert (mu/not-nil? dbmdata)) 186 | catalogs (spec/get-catalogs dbmdata) 187 | schemas (spec/get-schemas dbmdata) 188 | tables (spec/get-tables dbmdata) 189 | tb-names (spec/table-names tables) 190 | columns (spec/get-columns dbmdata :table-pattern t-name)] 191 | (println "\n**** All catalogs ****") 192 | (mu/! (mu/print-table (map #(.asMap ^IRow %) catalogs))) 193 | 194 | (println "\n**** All schemas ****") 195 | (mu/! (mu/print-table (map #(.asMap ^IRow %) schemas))) 196 | 197 | (println "\n**** All tables ****") 198 | (when (not (empty? tables)) 199 | (pp/pprint (keys (.asMap ^IRow (first tables)))) 200 | (mu/! (mu/print-table (map #(.asVec ^IRow %) tables)))) 201 | 202 | (is (= (count u-tables) (- (count tb-names) 2))) 203 | 204 | (is (-> (map sr/upper-case tb-names) 205 | (mu/contains-val? t-name)) (format "%s does not contain %s" 206 | (map sr/upper-case tb-names) 207 | t-name)) 208 | 209 | (println "\n**** All columns ****") 210 | (when (not (empty? columns)) 211 | (pp/pprint (keys (.asMap ^IRow (first columns)))) 212 | (mu/! (mu/print-table (map #(.asVec ^IRow %) columns)))) 213 | 214 | (let [sel-cols [:table-name :column-name :type-name :is-nullable 215 | :is-autoincrement] 216 | act-cols (vec (map #(select-keys (.asMap ^IRow %) sel-cols) 217 | columns)) 218 | exp-cols (vec (map #(zipmap sel-cols %) t-cols))] 219 | (is (= (count act-cols) (count exp-cols))) 220 | (dorun (map #(is (= (mu/map-vals sr/upper-case %1) 221 | (mu/map-vals sr/upper-case %2))) 222 | act-cols exp-cols)))))))) 223 | 224 | 225 | (defn update-test 226 | [] 227 | (clb-setup) 228 | (lb/update clog-1) 229 | (update-test-helper 230 | ["SAMPLE_TABLE_1" 231 | ["SAMPLE_TABLE_1" "ID" (db-int) "NO" "YES"] 232 | ["SAMPLE_TABLE_1" "NAME" "VARCHAR" "NO" "NO" ] 233 | ["SAMPLE_TABLE_1" "GENDER" "CHAR" "NO" "NO" ]] 234 | ["SAMPLE_TABLE_2" 235 | ["SAMPLE_TABLE_2" "ID" (db-int) "NO" "YES"] 236 | ["SAMPLE_TABLE_2" "F_ID" (db-int) "YES" "NO"] 237 | ["SAMPLE_TABLE_2" "NAME" "VARCHAR" "NO" "NO" ] 238 | ["SAMPLE_TABLE_2" "GENDER" "CHAR" "NO" "NO" ]])) 239 | 240 | 241 | (deftest test-update 242 | (testing "update" 243 | (lb-action 244 | update-test))) 245 | 246 | 247 | (defn update-idempotency-test 248 | [] 249 | (clb-setup) 250 | (lb/update clog-1) 251 | (lb/update clog-1) 252 | (update-test-helper 253 | ["SAMPLE_TABLE_1" 254 | ["SAMPLE_TABLE_1" "ID" (db-int) "NO" "YES"] 255 | ["SAMPLE_TABLE_1" "NAME" "VARCHAR" "NO" "NO" ] 256 | ["SAMPLE_TABLE_1" "GENDER" "CHAR" "NO" "NO" ]] 257 | ["SAMPLE_TABLE_2" 258 | ["SAMPLE_TABLE_2" "ID" (db-int) "NO" "YES"] 259 | ["SAMPLE_TABLE_2" "F_ID" (db-int) "YES" "NO"] 260 | ["SAMPLE_TABLE_2" "NAME" "VARCHAR" "NO" "NO" ] 261 | ["SAMPLE_TABLE_2" "GENDER" "CHAR" "NO" "NO" ]])) 262 | 263 | 264 | (deftest test-update-idempotency 265 | (testing "update(idempotency)" 266 | (lb-action 267 | update-idempotency-test))) 268 | 269 | 270 | (defn update-by-count-test 271 | [] 272 | (clb-setup) 273 | (lb/update-by-count clog-2 1) 274 | (update-test-helper 275 | ["SAMPLE_TABLE_1" 276 | ["SAMPLE_TABLE_1" "ID" (db-int) "NO" "YES"] 277 | ["SAMPLE_TABLE_1" "NAME" "VARCHAR" "NO" "NO" ] 278 | ["SAMPLE_TABLE_1" "GENDER" "CHAR" "NO" "NO" ]] 279 | ["SAMPLE_TABLE_2" 280 | ["SAMPLE_TABLE_2" "ID" (db-int) "NO" "YES"] 281 | ["SAMPLE_TABLE_2" "F_ID" (db-int) "YES" "NO"] 282 | ["SAMPLE_TABLE_2" "NAME" "VARCHAR" "NO" "NO" ] 283 | ["SAMPLE_TABLE_2" "GENDER" "CHAR" "NO" "NO" ]])) 284 | 285 | 286 | (deftest test-update-by-count 287 | (testing "update-by-count" 288 | (lb-action 289 | update-by-count-test))) 290 | 291 | 292 | (defn tag-test 293 | [] 294 | (clb-setup) 295 | (lb/update clog-1) 296 | (lb/tag "mytag") 297 | (is (= "mytag" (mu/! (query-value "SELECT tag FROM databasechangelog"))) 298 | "Tag name should match")) 299 | 300 | 301 | (deftest test-tag 302 | (testing "tag" 303 | (lb-action 304 | tag-test))) 305 | 306 | 307 | (defn rollback-to-tag-test 308 | [] 309 | (clb-setup) 310 | (lb/update clog-1) 311 | (lb/tag "mytag") 312 | (lb/update clog-2) 313 | (is (zero? (count (query "SELECT * FROM sampletable3")))) 314 | (lb/rollback-to-tag clog-2 "mytag") 315 | (is (thrown? SQLException 316 | (query "SELECT * FROM sampletable3")) "Table should not exist")) 317 | 318 | 319 | (deftest test-rollback-to-tag 320 | (testing "rollback-to-tag" 321 | (lb-action 322 | rollback-to-tag-test))) 323 | 324 | 325 | (defn rollback-to-date-test 326 | [] 327 | (clb-setup) 328 | (lb/update clog-1) 329 | (lb/tag "mytag") 330 | (lb/update clog-2) 331 | (is (zero? (count (query "SELECT * FROM sampletable3")))) 332 | (lb/rollback-to-date clog-2 (java.util.Date.)) 333 | (is (zero? (count (query "SELECT * FROM sampletable3"))))) 334 | 335 | 336 | (deftest test-rollback-to-date 337 | (testing "rollback-to-date" 338 | (lb-action 339 | rollback-to-date-test))) 340 | 341 | 342 | (defn rollback-by-count-test 343 | [] 344 | (clb-setup) 345 | (lb/update clog-1) 346 | (lb/tag "tag1") 347 | (lb/update clog-2) 348 | (lb/tag "tag2") 349 | (let [tt (fn [f tables] ; test table 350 | (doseq [each tables] 351 | (is (zero? (count (query (format "SELECT * FROM %s" each)))) 352 | (format "Table %s should exist having no rows" each))) 353 | (f) 354 | (doseq [each tables] 355 | (is (thrown? SQLException 356 | (query (format "SELECT * FROM %s" each))) 357 | (format "Table %s should not exist" each))))] 358 | ;; rollback 1 changeset 359 | (tt #(lb/rollback-by-count clog-2 1) ["sampletable3"]) 360 | ;; rollback 1 more changeset 361 | (tt #(lb/rollback-by-count clog-2 1) ["sample_table_1" 362 | "sample_table_2"]))) 363 | 364 | 365 | (deftest test-rollback-by-count 366 | (testing "rollback-by-count" 367 | (lb-action 368 | rollback-by-count-test))) 369 | 370 | 371 | (defn generate-doc-test 372 | [] 373 | (clb-setup) 374 | (lb/update clog-1) 375 | (lb/generate-doc clog-2 "target/dbdoc")) 376 | 377 | 378 | (deftest test-generate-doc 379 | (testing "generate-doc" 380 | (lb-action generate-doc-test) 381 | (is (.exists (File. "target/dbdoc/index.html"))))) 382 | 383 | 384 | (defmacro with-readonly 385 | [& body] 386 | `(spec/with-connection (spec/assoc-readonly spec/*dbspec*) 387 | ~@body)) 388 | 389 | 390 | (deftest test-generate-sql 391 | (testing "generate-sql" 392 | (with-lb-action 393 | (doseq [[each msg] [[(fn [w] 394 | (with-readonly 395 | (lb/update clog-1 [] w))) 396 | "Update Database Script"] 397 | [(fn [w] 398 | (with-readonly 399 | (lb/update-by-count clog-2 1 [] w))) 400 | "Update 1 Change-sets Database Script"] 401 | [(fn [w] 402 | (lb/update clog-1) 403 | (lb/tag "mytag") 404 | (lb/update clog-2) 405 | (with-readonly 406 | (lb/rollback-to-tag clog-2 "mytag" [] w))) 407 | "Rollback to 'mytag' Script"] 408 | [(fn [w] 409 | (lb/update clog-1) 410 | (lb/tag "mytag") 411 | (lb/update clog-2) 412 | (with-readonly 413 | (lb/rollback-to-date clog-2 (java.util.Date.) [] w))) 414 | "Rollback to"] 415 | [(fn [w] 416 | (lb/update clog-1) 417 | (lb/tag "tag1") 418 | (lb/update clog-2) 419 | (lb/tag "tag2") 420 | (with-readonly 421 | (lb/rollback-by-count clog-2 1 [] w))) 422 | "Rollback to 1 Change-sets Script"]]] 423 | (clb-setup) 424 | (let [^String script (mu/with-stringwriter w 425 | (each w))] 426 | (println "^^^^^^^^" script "$$$$$$$$") 427 | (is (and (string? script) 428 | (mu/posnum? (.indexOf ^String script ^String msg))))))))) 429 | 430 | 431 | (defn diff-test 432 | [] 433 | (clb-setup) 434 | (lb/diff lb/*db-instance*)) 435 | 436 | 437 | (deftest test-diff 438 | (testing "diff" 439 | (lb-action diff-test))) 440 | 441 | 442 | (defn test-ns-hook [] 443 | ;; ===== ChangeSet ===== 444 | (test-make-changeset) 445 | ;; ===== DatabaseChangeLog ===== 446 | (test-make-changelog) 447 | (test-defchangelog) 448 | ;; ===== Actions ===== 449 | (test-update) 450 | (test-update-idempotency) 451 | (test-update-by-count) 452 | (test-tag) 453 | (test-rollback-to-tag) 454 | (test-rollback-to-date) 455 | (test-rollback-by-count) 456 | (test-generate-doc) 457 | (test-generate-sql) 458 | (test-diff)) 459 | -------------------------------------------------------------------------------- /test/clj_liquibase/test_internal.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-internal 2 | (:require 3 | [clj-liquibase.internal :as in] 4 | [clj-liquibase.change :as ch]) 5 | (:import 6 | (liquibase.structure.core Column) 7 | (liquibase.change ColumnConfig ConstraintsConfig) 8 | (liquibase.change.core LoadDataColumnConfig) 9 | (liquibase.statement DatabaseFunction) 10 | (liquibase.util ISODateFormat) 11 | (java.util Date)) 12 | (:use clojure.test)) 13 | 14 | 15 | (deftest test-dbfn? 16 | (testing "dbfn?" 17 | (is (in/dbfn? (ch/dbfn "foo"))))) 18 | 19 | 20 | (deftest test-as-coltype 21 | (testing "as-coltype" 22 | (is (= "int" (in/as-coltype :int))) 23 | (is (= "char(10)" (in/as-coltype :char 10))) 24 | (is (= "float(17, 5)" (in/as-coltype :float 17 5))))) 25 | 26 | 27 | (deftest test-if-nn 28 | (testing "if-nn" 29 | (is (= 20 (in/if-nn 10 20))) 30 | (is (= nil (in/if-nn nil 20))))) 31 | 32 | 33 | (deftest test-as-column-config 34 | (testing "as-column-config" 35 | (let [cc in/as-column-config 36 | cc? #(instance? ColumnConfig %)] 37 | (is (cc? (cc :colname :coltype)) "only required parameters") 38 | (is (cc? (cc :colname :char 39 | :default-value "foobar" ; String/Number/Date/Boolean/DatabaseFunction 40 | :auto-increment true ; Boolean 41 | :remarks "comment" ; String 42 | ;; constraints 43 | :nullable false ; Boolean 44 | :primary-key true ; Boolean 45 | :primary-key-name "dummy" ; String 46 | :primary-key-tablespace "nonamex" ; String 47 | :references "nonamex" ; String 48 | :unique false ; Boolean 49 | :unique-constraint-name "onlyid" ; String 50 | :check "check" ; String 51 | :delete-cascade true ; Boolean 52 | :foreign-key-name "keyname" ; String 53 | :initially-deferred false ; Boolean 54 | :deferrable true ; Boolean 55 | )) "all optional parameters with fullname") 56 | (is (cc? (cc :colname :char 57 | :default "foobar" ; String/Number/Date/Boolean/DatabaseFunction 58 | :autoinc true ; Boolean 59 | :remarks "comment" ; String 60 | ;; constraints 61 | :null false ; Boolean 62 | :pk true ; Boolean 63 | :pkname "dummy" ; String - s.t. clj-to-dbident 64 | :pktspace "nonamex" ; String - s.t. clj-to-dbident 65 | :refs "nonamex" ; String 66 | :uniq false ; Boolean 67 | :ucname "onlyid" ; String - s.t. clj-to-dbident 68 | :check "check" ; String 69 | :dcascade true ; Boolean 70 | :fkname "keyname" ; String - s.t. clj-to-dbident 71 | :idefer false ; Boolean 72 | :defer true ; Boolean 73 | )) "all optional parameters with shortnames") 74 | (is (cc? (cc :colname :date 75 | :default (ch/dbfn "NOW"))) "Default value: DatabaseFunction") 76 | (is (cc? (cc :colname :int 77 | :default 100)) "Default value: Number") 78 | (is (cc? (cc :colname [:tinyint 1] 79 | :default false)) "Default value: Boolean") 80 | (is (cc? (cc :colname :date 81 | :default (Date.))) "Default value: Date")))) 82 | 83 | 84 | (defn test-ns-hook [] 85 | (test-dbfn?) 86 | (test-as-coltype) 87 | (test-if-nn) 88 | (test-as-column-config)) -------------------------------------------------------------------------------- /test/clj_liquibase/test_parser.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-parser 2 | (:require 3 | [clj-liquibase.core :as lb] 4 | [clj-liquibase.test-core :as tc]) 5 | (:use clojure.test)) 6 | 7 | 8 | (lb/defparser p "example.edn") 9 | 10 | 11 | (deftest test-parse-changelog 12 | (testing "parse-changelog" 13 | (is (lb/changelog? (lb/parse-changelog "example.edn")))) 14 | (testing "defparser" 15 | (is (lb/changelog? (p)))) 16 | (testing "update on parsed changelog" 17 | (tc/lb-action 18 | #(do 19 | (tc/clb-setup) 20 | (lb/update p))))) 21 | -------------------------------------------------------------------------------- /test/clj_liquibase/test_precondition.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-precondition 2 | (:require 3 | [clj-jdbcutil.core :as sp] 4 | [clj-miscutil.core :as mu] 5 | [clj-liquibase.core :as lb] 6 | [clj-liquibase.change :as ch] 7 | [clj-liquibase.precondition :as pc] 8 | [clj-liquibase.test-core :as tl]) 9 | (:import 10 | (java.util Date) 11 | (liquibase.exception MigrationFailedException PreconditionFailedException)) 12 | (:use clj-liquibase.test-util) 13 | (:use clojure.test)) 14 | 15 | 16 | (def changeset-1 ["id=1" "author=shantanu" [tl/ct-change1 tl/ct-change2 17 | (ch/insert-data 18 | :sample-table-1 {:name "Henry" 19 | :gender "M"}) 20 | (ch/create-view 21 | :sample-view-1 "SELECT * FROM sample_table_1") 22 | (ch/create-sequence 23 | :foo) 24 | (ch/create-index 25 | :sample-table-1 [:name] 26 | :index-name :sample-1-index-1) 27 | (ch/add-foreign-key-constraint 28 | :f-cons ; constraint-name 29 | :sample-table-2 ; base-table-name 30 | [:f-id] ; base-column-names 31 | :sample-table-1 ; referenced-table-name 32 | [:id] ; referenced-column-names 33 | )]]) 34 | 35 | 36 | (def changeset-2 ["id=2" "author=shantanu" [tl/ct-change3]]) 37 | 38 | 39 | ;(lb/defchangelog clog-1 "precond" [changeset-1]) 40 | 41 | 42 | ;(lb/defchangelog clog-2 "precond" [changeset-1 changeset-2]) 43 | 44 | 45 | (deftest test-changeset-executed 46 | (sp/with-connection (tl/dbspec) 47 | (lb/with-lb 48 | (let [cs-0 (into changeset-1 49 | [:pre-cond [(pc/changeset-executed 50 | "precond" "id=0" "author=shantanu")]]) 51 | cs-1 changeset-1 52 | cs-2 (into changeset-2 53 | [:pre-cond [(pc/changeset-executed 54 | "precond" "id=1" "author=shantanu")]])] 55 | (lb/defchangelog clog-0 "precond" [cs-0]) 56 | (lb/defchangelog clog-1 "precond" [cs-1]) 57 | (lb/defchangelog clog-2 "precond" [cs-1 cs-2]) 58 | (tl/clb-setup) 59 | (let [[r e] (mu/maybe (lb/update clog-0))] 60 | (is (instance? MigrationFailedException e)) 61 | (is (instance? PreconditionFailedException (.getCause ^Exception e)))) 62 | (lb/update clog-1) 63 | (lb/update clog-2))))) 64 | 65 | 66 | (deftest test-column-exists 67 | (sp/with-connection (tl/dbspec) 68 | (lb/with-lb 69 | (let [cs-1 changeset-1 70 | cs-2 (into changeset-2 71 | [:pre-cond [(pc/column-exists 72 | "" :sample-table-1 :id)]])] 73 | (lb/defchangelog clog-1 "precond" [cs-1]) 74 | (lb/defchangelog clog-2 "precond" [cs-1 cs-2]) 75 | (tl/clb-setup) 76 | (lb/update clog-1) 77 | (lb/update clog-2))))) 78 | 79 | 80 | (deftest test-dbms 81 | (sp/with-connection (tl/dbspec) 82 | (lb/with-lb 83 | (let [cs-1 changeset-1 84 | cs-2 (into changeset-2 85 | [:pre-cond [(pc/dbms :h2)]])] 86 | (lb/defchangelog clog-1 "precond" [cs-1]) 87 | (lb/defchangelog clog-2 "precond" [cs-1 cs-2]) 88 | (tl/clb-setup) 89 | (lb/update clog-1) 90 | (lb/update clog-2))))) 91 | 92 | 93 | (deftest test-foreign-key-exists 94 | (sp/with-connection (tl/dbspec) 95 | (lb/with-lb 96 | (let [cs-1 changeset-1 97 | cs-2 changeset-2] 98 | (lb/defchangelog clog-1 "precond" [cs-1]) 99 | (lb/defchangelog clog-2 "precond" 100 | [cs-1 (into cs-2 101 | [:pre-cond [(pc/foreign-key-exists 102 | "" :sample-table-2 :f-cons)]])]) 103 | (tl/clb-setup) 104 | (lb/update clog-1) 105 | (lb/update clog-2))))) 106 | 107 | 108 | (deftest test-index-exists 109 | (sp/with-connection (tl/dbspec) 110 | (lb/with-lb 111 | (let [cs-1 changeset-1 112 | cs-2 changeset-2] 113 | (lb/defchangelog clog-1 "precond" [cs-1]) 114 | (lb/defchangelog clog-2 "precond" 115 | [cs-1 (into cs-2 116 | [:pre-cond [(pc/index-exists 117 | "" :sample-table-1 [:name] 118 | :sample-1-index-1)]])]) 119 | (tl/clb-setup) 120 | (lb/update clog-1) 121 | (lb/update clog-2))))) 122 | 123 | 124 | (deftest test-primary-key-exists 125 | (sp/with-connection (tl/dbspec) 126 | (lb/with-lb 127 | (let [cs-1 changeset-1 128 | cs-2 changeset-2] 129 | (lb/defchangelog clog-1 "precond" [cs-1]) 130 | (lb/defchangelog clog-2 "precond" 131 | [cs-1 (into cs-2 132 | [:pre-cond [(pc/primary-key-exists 133 | "" :sample-table-1 134 | :pk-sample-table-1)]])]) 135 | (tl/clb-setup) 136 | (lb/update clog-1) 137 | (lb/update clog-2))))) 138 | 139 | 140 | (deftest test-running-as 141 | (sp/with-connection (tl/dbspec) 142 | (lb/with-lb 143 | (let [cs-1 (into changeset-1 144 | [:pre-cond [(pc/running-as "sa")]])] 145 | (lb/defchangelog clog-1 "precond" [cs-1]) 146 | (tl/clb-setup) 147 | (lb/update clog-1))))) 148 | 149 | 150 | (deftest test-sequence-exists 151 | (sp/with-connection (tl/dbspec) 152 | (lb/with-lb 153 | (let [cs-1 changeset-1 154 | cs-2 (into changeset-2 155 | [:pre-cond [(pc/sequence-exists "" :foo)]])] 156 | (lb/defchangelog clog-1 "precond" [cs-1]) 157 | (lb/defchangelog clog-2 "precond" [cs-2]) 158 | (tl/clb-setup) 159 | (lb/update clog-1) 160 | (lb/update clog-2))))) 161 | 162 | 163 | (deftest test-sql 164 | (sp/with-connection (tl/dbspec) 165 | (lb/with-lb 166 | (let [cs-1 changeset-1 167 | cs-2 (into changeset-2 168 | [:pre-cond [(pc/sql 1 "SELECT COUNT(*) FROM sample_table_1")]])] 169 | (lb/defchangelog clog-1 "precond" [cs-1]) 170 | (lb/defchangelog clog-2 "precond" [cs-2]) 171 | (tl/clb-setup) 172 | (lb/update clog-1) 173 | (lb/update clog-2))))) 174 | 175 | 176 | (deftest test-table-exists 177 | (sp/with-connection (tl/dbspec) 178 | (lb/with-lb 179 | (let [cs-1 changeset-1 180 | cs-2 (into changeset-2 181 | [:pre-cond [(pc/table-exists "" :sample-table-1)]])] 182 | (lb/defchangelog clog-1 "precond" [cs-1]) 183 | (lb/defchangelog clog-2 "precond" [cs-2]) 184 | (tl/clb-setup) 185 | (lb/update clog-1) 186 | (lb/update clog-2))))) 187 | 188 | 189 | (deftest test-view-exists 190 | (sp/with-connection (tl/dbspec) 191 | (lb/with-lb 192 | (let [cs-1 changeset-1 193 | cs-2 (into changeset-2 194 | [:pre-cond [(pc/view-exists "" :sample-view-1)]])] 195 | (lb/defchangelog clog-1 "precond" [cs-1]) 196 | (lb/defchangelog clog-2 "precond" [cs-2]) 197 | (tl/clb-setup) 198 | (lb/update clog-1) 199 | (lb/update clog-2))))) 200 | 201 | 202 | (deftest test-pc-and 203 | (testing "pc-and" 204 | (sp/with-connection (tl/dbspec) 205 | (lb/with-lb 206 | (let [cs-1 changeset-1 207 | cs-2 (into changeset-2 208 | [:pre-cond [(pc/pc-and 209 | (pc/table-exists "" :sample-table-1) 210 | (pc/view-exists "" :sample-view-1)) 211 | ]])] 212 | (lb/defchangelog clog-1 "precond" [cs-1]) 213 | (lb/defchangelog clog-2 "precond" [cs-2]) 214 | (tl/clb-setup) 215 | (lb/update clog-1) 216 | (lb/update clog-2)))))) 217 | 218 | 219 | (deftest test-pc-not 220 | (testing "pc-not" 221 | (sp/with-connection (tl/dbspec) 222 | (lb/with-lb 223 | (let [cs-1 changeset-1 224 | cs-2 (into changeset-2 225 | [:pre-cond [(pc/pc-not 226 | (pc/table-exists "" :sample-table-11) 227 | (pc/view-exists "" :sample-view-11)) 228 | ]])] 229 | (lb/defchangelog clog-1 "precond" [cs-1]) 230 | (lb/defchangelog clog-2 "precond" [cs-2]) 231 | (tl/clb-setup) 232 | (lb/update clog-1) 233 | (lb/update clog-2)))))) 234 | 235 | 236 | (deftest test-pc-or 237 | (testing "pc-or" 238 | (sp/with-connection (tl/dbspec) 239 | (lb/with-lb 240 | (let [cs-1 changeset-1 241 | cs-2 (into changeset-2 242 | [:pre-cond [(pc/pc-or 243 | (pc/table-exists "" :sample-table-11) 244 | (pc/view-exists "" :sample-view-1)) 245 | ]])] 246 | (lb/defchangelog clog-1 "precond" [cs-1]) 247 | (lb/defchangelog clog-2 "precond" [cs-2]) 248 | (tl/clb-setup) 249 | (lb/update clog-1) 250 | (lb/update clog-2)))))) 251 | 252 | 253 | (deftest test-pre-cond 254 | (testing "pre-cond" 255 | (let [pcs [(pc/pre-cond [(pc/dbms :h2)] :on-error :halt :on-error-msg "halt" :on-fail :halt :on-fail-msg "halt") 256 | (pc/pre-cond [(pc/dbms :h2)] :on-error :continue :on-error-msg "continue" :on-fail :continue :on-fail-msg "continue") 257 | (pc/pre-cond [(pc/dbms :h2)] :on-error :mark-ran :on-error-msg "mark_ran" :on-fail :mark-ran :on-fail-msg "mark_ran") 258 | (pc/pre-cond [(pc/dbms :h2)] :on-error :warn :on-error-msg "warn" :on-fail :warn :on-fail-msg "warn") 259 | (pc/pre-cond [(pc/dbms :h2)] :on-update-sql :ignore) 260 | (pc/pre-cond [(pc/dbms :h2)] :on-update-sql :test) 261 | (pc/pre-cond [(pc/dbms :h2)] :on-update-sql :fail)]] 262 | (doseq [each pcs] 263 | (sp/with-connection (tl/dbspec) 264 | (lb/with-lb 265 | (lb/defchangelog cl "precond" [(into changeset-1 [:pre-cond each])]) 266 | (lb/update cl))))))) 267 | 268 | 269 | (defn test-ns-hook [] 270 | (test-changeset-executed) 271 | (test-column-exists) 272 | (test-dbms) 273 | (test-foreign-key-exists) 274 | (test-index-exists) 275 | (test-primary-key-exists) 276 | (test-running-as) 277 | (test-sequence-exists) 278 | (test-sql) 279 | (test-table-exists) 280 | (test-view-exists) 281 | (test-pc-and) 282 | (test-pc-not) 283 | (test-pc-or) 284 | (test-pre-cond)) -------------------------------------------------------------------------------- /test/clj_liquibase/test_sql_visitor.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-sql-visitor 2 | (:require [clj-liquibase.sql-visitor :as vis]) 3 | (:import 4 | (liquibase.sql.visitor SqlVisitor 5 | AppendSqlVisitor PrependSqlVisitor 6 | RegExpReplaceSqlVisitor ReplaceSqlVisitor)) 7 | (:use clj-liquibase.test-util) 8 | (:use clojure.test)) 9 | 10 | 11 | (deftest test-can-make-append-visitor 12 | (doseq [[k r] {"some text" "some text" 13 | :some-text "some-text"}] 14 | (let [v (vis/make-append-visitor k)] 15 | (is (= r (.getValue v)))))) 16 | 17 | 18 | (deftest test-can-make-prepend-visitor 19 | (doseq [[k r] {"some text" "some text" 20 | :some-text "some-text"}] 21 | (let [v (vis/make-prepend-visitor k)] 22 | (is (= r (.getValue v)))))) 23 | 24 | 25 | (deftest test-can-make-replace-visitor 26 | (doseq [[rk rr wk wr] [["some text" "some text" "new text" "new text"] 27 | [:some-text "some-text" :new-text "new-text"] 28 | [#"some-text" "some-text" :new-text "new-text"]]] 29 | (let [v (vis/make-replace-visitor rk wk)] 30 | (cond 31 | (= ReplaceSqlVisitor 32 | (class v)) (do (is (= rr (.getReplace ^ReplaceSqlVisitor v))) 33 | (is (= wr (.getWith ^ReplaceSqlVisitor v)))) 34 | (= RegExpReplaceSqlVisitor 35 | (class v)) (do (is (= rr (.getReplace ^RegExpReplaceSqlVisitor v))) 36 | (is (= wr (.getWith ^RegExpReplaceSqlVisitor v)))) 37 | :or (throw (RuntimeException. 38 | "v must be of type ReplaceSqlVisitor/RegExpReplaceSqlVisitor")))))) 39 | 40 | 41 | (deftest test-misc-visitor-constraints 42 | (let [v (vis/make-append-visitor "text") 43 | f (vis/for-dbms! :mysql v) 44 | r (vis/apply-to-rollback! true v) 45 | c (vis/for-contexts! :some-ctx v)] 46 | (is (= #{"mysql"} (.getApplicableDbms ^SqlVisitor f)) "for-dbms!") 47 | (is (= true (.isApplyToRollback ^SqlVisitor r)) "apply-to-rollback!") 48 | (is (= #{"some-ctx"} (.getContexts ^SqlVisitor c)) "for-contexts!"))) 49 | 50 | 51 | (deftest test-make-visitors 52 | (let [vs (vis/make-visitors 53 | :include (map (partial vis/for-dbms! :mysql) 54 | (vis/make-visitors :append "engine=InnoDB")) 55 | :append " -- creating table\n" 56 | :replace [:integer :bigint] 57 | :replace {:string "VARCHAR(256)" 58 | #"varchar*" "VARCHAR2"} 59 | :prepend "IF NOT EXIST")] 60 | (is (coll? vs)) 61 | (is (= 6 (count vs))) 62 | (is (every? vis/visitor? vs)))) 63 | 64 | 65 | (defn test-ns-hook [] 66 | (test-can-make-append-visitor) 67 | (test-can-make-prepend-visitor) 68 | (test-can-make-replace-visitor) 69 | (test-misc-visitor-constraints) 70 | (test-make-visitors)) -------------------------------------------------------------------------------- /test/clj_liquibase/test_util.clj: -------------------------------------------------------------------------------- 1 | (ns clj-liquibase.test-util 2 | (:require 3 | [clj-jdbcutil.core :as sp] 4 | [clj-miscutil.core :as mu]) 5 | (:import 6 | (java.util List) 7 | (java.sql Connection Statement)) 8 | (:use clojure.test)) 9 | 10 | 11 | (defn fail 12 | ([] 13 | (is false "Failed")) 14 | ([msg] 15 | (is false msg))) 16 | 17 | 18 | (defn todo [] (is false "Not implemented yet")) 19 | 20 | 21 | (defn query 22 | ^List [^String sql] 23 | (with-open [st ^Statement (.createStatement 24 | ^Connection (:connection sp/*dbspec*))] 25 | (sp/row-seq (.executeQuery st sql)))) 26 | 27 | 28 | (defn query-value 29 | "Execute SQL query and return the first column value." 30 | [^String sql] 31 | (let [res (query sql)] 32 | ((first res)))) 33 | 34 | 35 | (defn as-vector 36 | [v] 37 | (if (coll? v) (vec v) 38 | [v])) 39 | -------------------------------------------------------------------------------- /test/example.edn: -------------------------------------------------------------------------------- 1 | {:database-change-log [; semi-colon marks an in-line comment 2 | ;{"preConditions" [{"runningAs" {"username" "liquibase"}}]} 3 | ; keywords work as keys (hyphen triggers camel-case) 4 | {:include {:file "child1.edn"}} 5 | ; symbols also work as keys (hyphen triggers camel-case) 6 | {change-set {"id" "2" 7 | "author" "nvoxland" 8 | "changes" [{"addColumn" {"tableName" "person" 9 | "columns" [{"column" {"name" "username" 10 | "type" "varchar(8)"}}]}}]}} 11 | {"changeSet" {"id" "3" 12 | "author" "nvoxland" 13 | "changes" [{"addLookupTable" {"existingTableName" "person" 14 | "existingColumnName""state" 15 | "newTableName" "state" 16 | "newColumnName" "id" 17 | "newColumnDataType" "char(2)"}}]}}]} 18 | --------------------------------------------------------------------------------