Liking cljdoc? Tell your friends :D

Introduction to clj-liquibase

TODO: write great documentation

Liquibase is an Open Source (Apache 2 license) database change management library. Clj-Liquibase provides a way to write Liquibase changesets and changelogs in Clojure, while inheriting other attributes of Liquibase.

In order to work with Clj-Liquibase, you need to know the Liquibase abstractions change, changeset and changelog objects, which constitute the changes that can be tracked and applied to JDBC databases via various commands. These terms are described in the sections below.

Supported commands:

  • Update
  • Tag
  • Rollback
    • By tag
    • By date
    • By changeset count
  • DB documentation
  • Diff

Understanding change, changeset, changelog

A change (instance of class liquibase.change.Change) is the smallest unit of a database change. A change cannot be tracked as it is; hence it must be wrapped in a changeset (instance of class liquibase.changelog.ChangeSet.) A changeset can contain several change objects in the desired order, and is marked with id and author attributes.

A changelog (instance of the class liquibase.changelog.DatabaseChangeLog) contains all changesets in the desired order meant for a database schema. The database may actually be up to date with, or behind the changelog at a certain point of time. A Clj-Liquibase command lets you apply the changelog to the database in the intended way.

Important points to note:

  • The change and changeset definition (in code) cannot be modified once applied to the database.
  • A changelog definition (in code) cannot be modified after being applied to the database. However, you can add more changeset objects to it later.

Change

Note: Defining changesets programmatically is deprecated as of version 0.6.0, and will be removed in the future.

Change objects can be constructed by using the factory functions in the clj-liquibase.change namespace, which are described in the sub-sections below:

Structural Refactorings

Function nameRequired argsOptional kwargsDescription
add-columnstable-name:schema-nameAdd columns to an existing table
columns Column definition
rename-columntable-name:schema-nameRename column in an existing table
old-column-name:column-data-type
new-column-name
modify-columntable-name:schema-nameModify data type of a column in an existing table
column-name
new-data-type
drop-columntable-name:schema-nameDrop specified column from an existing table
column-name
alter-sequenceseq-name:schema-nameModifies a database sequence
increment-by:max-value
:min-value
:ordered
create-tabletable-name:schema-nameCreate a new table
columns:table-spaceColumn definition
:remarks
create-table-withidsame as abovesame as aboveSame as above, except it creates auto-incremented ID column
:idcol
rename-tableold-table-name:schema-nameRename an existing table
new-table-name
drop-tabletable-name:schema-nameDrop an existing table
:cascade-constraints
create-viewview-name:schema-nameCreate a database view
select-query:replace-if-exists
rename-viewold-view-name:schema-nameRename an existing database view
new-view-name
drop-viewview-name:schema-nameDrop an existing database view
merge-columnstable-name:schema-nameMerge two columns of the same table into one
column1-name
join-string
column2-name
final-column-name
final-column-type
create-stored-procedureprocedure-body:commentsCreate database stored procedure
Column config

The functions add-columns, create-table and create-table-withid accept a columns argument, which is a collection of column-config elements. Each column-config is a vector of 2 required args followed by optional keyword args.

Required args: column-name, column-type Optional kwargs:

Long nameShort nameAllowed types
:default-value:defaultString/Number/java.util.Date/Boolean/DatabaseFunction
:auto-increment:autoincBoolean
:remarks String
:nullable:nullBoolean
:primary-key:pkBoolean
:primary-key-name:pknameString/Keyword
:primary-key-tablespace:pktspaceString/Keyword
:references:refsString (Foreign key definition)
:unique:uniqBoolean
:unique-constraint-name:ucnameString/Keyword
:check String
:delete-cascade:dcascadeBoolean
:foreign-key-name:fknameString/Keyword
:initially-deferred:ideferBoolean
:deferrable:deferBoolean
Example

Example of creating a change object:

(clj-liquibase.change/create-table "sampletable1"
                                   [[:id     :int          :null false :pk true :autoinc true]
                                    [:name   [:varchar 40] :null false]
                                    [:gender [:char 1]     :null false]])

Data Quality Refactorings

Function nameRequired argsOptional kwargsDescription
add-lookup-tableexisting-table-name:existing-table-schema-nameAdd a lookup table
existing-column-name:new-table-schema-name
new-table-name:new-column-data-type
new-column-name
constraint-name
add-not-null-constrainttable-name:schema-nameAdd NOT NULL constraint on specified column in a table
column-name:default-null-value
column-data-type
drop-not-null-constrainttable-name:schema-nameDrop NOT NULL constraint for specified column
column-name:column-data-type
add-unique-constrainttable-name:schema-nameAdd UNIQUE constraint for specified columns
column-names:table-space
constraint-name:deferrable
:initially-deferred
:disabled
drop-unique-constrainttable-name:schema-nameDrop specified UNIQUE constraint
constraint-name
create-sequencesequence-name:schema-nameCreate a database sequence
:start-value
:increment-by
:max-value
:min-value
:ordered
:cycle
drop-sequencesequence-name:schema-nameDrop specified database sequence
add-auto-incrementtable-name:schema-nameConvert an existing column to auto-increment type
column-name
column-data-type
add-default-valuetable-name:schema-nameAdd default value for specified column
column-name:column-data-type
default-value
drop-default-valuetable-name:schema-nameDrop default value for specified column
column-name:column-data-type

Referential Integrity Refactorings

Function nameRequired argsOptional kwargsDescription
add-foreign-key-constraintconstraint-name:base-table-schema-nameAdd foreign key constraint to an existing column
base-table-name:referenced-table-schema-name
base-column-names:deferrable
referenced-table-name:initially-deferred
referenced-column-names:on-delete
:on-update
drop-foreign-key-constraintconstraint-name:schema-nameDrop a foreign key constraint
base-table-name
add-primary-keytable-name:schema-nameAdd primary key from one or more columns
column-names:table-space
constraint-name
drop-primary-keytable-name:schema-nameDrop an existing primary key
:constraint-name

Non-Refactoring Transformations

Function nameRequired argsTypeOptional kwargsDescription
insert-datatable-namestr/kw:schema-nameInsert data into specified table
column-value-mapmap
load-datatable-namestr/kw:schema-nameLoad data from CSV file into specified table
csv-filenamestring:encoding
columns-speccoll/map
load-update-datatable-namestr/kw:schema-nameLoad and save (insert/update) data from CSV file into specified table
csv-filenamestring:encoding
primary-key-cols
columns-speccoll/map
update-datatable-name :schema-nameUpdate data in existing table
column-name-value-map :where-clause
delete-datatable-name :schema-nameDelete data from specified table
:where-clause
tag-databasetag Tag the database with specified tag
stop Stop Liquibase execution immediately, useful for debugging
Columns config for loading data

Loading data from CSV files into the database requires translation rules. The functions load-data and load-update-data accept an argument columns-spec that is a collection of column-config elements. Every column-config is a collection of 2 required arguments followed by optional keyword args:

Required arguments:

  • First element: colname (keyword/string)
  • Second element: coltype (either of "STRING", "NUMERIC", "DATE", "BOOLEAN")

Optional keyword args with corresponding values:

:index (number) :header (Keyword/String)

Architectural Refactorings

Function nameRequired argsTypeOptional kwargsDescription
create-indextable-namestringable:schema-nameCreate index with specified column names
column-namescollection:index-name
:unique
:table-space
drop-indexindex-namestringable:schema-nameDrop an existing index
table-namestringable

Custom Refactorings

Function nameRequired argsTypeOptional kwargsDescription
sqlsqlstring:commentExecute given SQL
:dbms
:encoding
:end-delimiter
:split-statements
:strip-comments
sql-filefile-pathstring:dbmsExecute SQL from file
:encoding
:end-delimiter
:split-statements
:strip-comments

Short names for keyword args

Note that you can use the following short names for corresponding keyword args:

Keyword arg (long name)Short nameValue typeDefault
:schema-name:schemastring/keyword
:existing-table-schema-name:existing-schemastring/keyword
:new-table-schema-name:new-schemastring/keyword
:column-data-type:data-typestring/keyword/vector
:new-column-data-type:new-data-typestring/keyword/vector
:max-value:maxnumber or string
:min-value:minnumber or string
:ordered:ordtrue or false
:table-space:tspacestring/keyword
:cascade-constraints:cascadelogical boolean
:replace-if-exists:replacelogical boolean
:default-null-value:defaultstring
:deferrable:deferlogical boolean
:initially-deferred:ideferlogical boolean
:start-value:startcoerced as BigInteger
:increment-by:incbycoerced as BigInteger
:cycle:cyclogical boolean
:encoding:encstring"UTF-8"
:base-table-schema-name:base-schemastring
:referenced-table-schema-name:ref-schemastring
:on-delete:ondelstring
:on-update:onupdstring
:where-clause:wherestring
:index-name:indexstring
:unique:uniqlogical boolean

Constructing Changeset objects

A changeset can be constructed using the function clj-liquibase.core/make-changeset. Required args: id (string), author (string), changes (collection of change objects) Optional kwargs:

Long nameShort nameType
:dbms String/Keyword/vector-of-multiple
:run-always:alwaysBoolean
:run-on-change:on-changeBoolean
:context:ctxString
:run-in-transaction:in-txnBoolean (true by default)
:fail-on-error:fail-errBoolean
:comment String
:pre-conditions:pre-condlist of Precondition objects, or PreconditionContainer object
:valid-checksum:valid-csumString
:visitors collection of SqlVisitor objects

An example changeset-construction look like this:

;; assume `ch1` is a change object
(clj-liquibase.core/make-changeset "id=1" "author=shantanu" [ch1])

A shorter way to define a changeset for use in a changelog is to only store the arguments in a vector -- defchangelog automatically creates a changeset from the arguments in the vector:

(def ch-set1 ["id=1" "author=shantanu" [ch1]])

Note: Changeset ID should be unique per user across all changesets in a changelog, i.e. several users can have identical changeset ID.

The recommended way to create a changeset is to wrap only one change object, the main reason being pre-conditions and SQL-visitors can be applied only at the changeset level. Since changesets cannot be modified after being applied to the database, it would be impossible to go back and refactor the changesets. However, one can add conditional SQL-visitor to a changeset later to modify the generated SQL statement a little to suit a different database type.

Precondition

TODO

SQL Visitor

TODO

Defining Changelog

Defining changelog via external file

If you have a changelog defined in a EDN, YAML, SQL, JSON or XML file in classpath or file system, you can define the changelog as follows:

(clj-liquibase.core/defparser "changelog.edn") ; reads from classpath

(clj-liquibase.core/defparser "changelog.yml" {:source :filesystem}) ; reads from file system

Defining changelog programmatically (DEPRECATED)

Note: Defining changelog programmatically is deprecated as of version 0.6.0, and will be removed in the future.

A changelog can be defined using the defchangelog macro, which essentially defines a partially applied function such that when executed with no args it returns a liquibase.changelog.DatabaseChangeLog object.

(clj-liquibase.core/defchangelog changelog-name
  "logical-schema-name" [changeset-1 changeset-2 changeset-3])

Alternatively, you can also create a changelog using the factory function clj-liquibase.core/make-changelog. The macro defchangelog returns a higher order function that calls make-changelog.

The function make-changelog and (hence) the defchangelog macro accept an optional keyword argument :pre-conditions (short name :pre-cond) to specify the pre-condition checks for the changelog.

A changelog definition cannot be modified once applied to the database; however, you can incrementally add changesets to a changelog as time goes.

Command Line Interface (CLI) integration

The Command-Line Interface is the easiest integration option for applications that want to use Clj-Liquibase. The clj-liquibase.cli namespace has a built-in command-line argument parser that knows about the CLI commands and their various switches respectively.

An application simply needs to collect user-provided command line arguments and invoke:

(clj-liquibase.cli/entry cmd opts & args)

The clj-liquibase.cli/entry arguments are described below:

ArgumentDescription
cmdany of "help" "version" "update" "rollback" "tag" "dbdoc" "diff"
optsdefault options
argsuser provided arguments

The various switches for their respective commands are listed below:

CommandRequiredOptionalOpt no-valueDescription
help Show help text
version Show Clj-Liquibase version
update:datasource:chs-count:sql-onlyUpdate database to specified changelog
:changelog:contexts
rollback:datasource:chs-count:sql-onlyRollback database to specified changeset-count/tag/ISO-date
:changelog:tag
:date
:contexts
tag:datasource Tag the database on ad hoc basis
:tag
dbdoc:datasource:contexts Generate database/changelog documentation
:changelog
:output-dir
diff:datasource Report difference between 2 database instances
:ref-datasource

The switches listed above may either be provided as part of the opts map, or as command-line arguments in args as follows:

SwitchLong-name exampleShort-name example
:changelog"--changelog=a.schema/cl""-c=a.schema/cl"
:chs-count"--chs-count=10""-n10"
:contexts"--contexts=foo,bar""-tfoo,bar"
:datasource"--datasource=foo.bar/ds""-dfoo.bar/ds"
:date"--date=2012-09-16""-e2012-09-16"
:output-dir"--output-dir=target/doc""-otarget/doc"
:ref-datasource"--ref-datasource=foo/ds""-rfoo/ds"
:sql-only"--sql-only" (no value)"-s" (no value)
:tag"--tag=v0.1.0""-gv0.1.0"

Please note that :datasource, :changelog and :ref-datasource may point to var names that would be resolved at runtime to obtain the corresponding values.

Integrating in an app

The following example shows how to integrate an app with the Clj-Liquibase CLI:

(ns foo.schema
  (:require
    [foo.globals :as globals]
    [clj-liquibase.change :as ch]
    [clj-liquibase.core   :as lb]
    [clj-liquibase.cli    :as cli]))

;; assuming that globals/ds is bound to a DataSource

(defchangelog ch-log "foo" [..change-sets..])

(defn -main
  [& [cmd & args]]
  (apply cli/entry cmd {:datasource globals/ds :changelog ch-log} args))

You can run this example as follows:

$ lein run -m foo.schema update
$ lein run -m foo.schema tag --tag=v0.1.0

Core functions

The CLI commands shown above are implemented via corresponding functions in the clj-liquibase.core namespace listed below:

  • update update-by-count
  • tag
  • rollback-to-tag rollback-to-date rollback-by-count
  • generate-doc
  • diff

The core functions that implement the commands are supposed to be invoked in a context where certain dynamic vars are bound to appropriate values. Feel encouraged to inspect the source code in the namespace clj-liquibase.core.

Can you improve this documentation? These fine people already did:
Shantanu Kumar & Friedrich von Never
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close