View on GitHub


5-minute guide

Open your favourite editor and define two migrations (a.k.a. changesets) as follows:

<?xml version="1.0" encoding="UTF-8"?>
<changelog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="http://www.liquigraph.org/schema/1.0/liquigraph.xsd">
    <changeset id="hello-world" author="you">
        <query>CREATE (n:Sentence {text:'Hello monde!'}) RETURN n</query>
    </changeset>
    <changeset id="hello-world-fixed" author="you">
        <query>MATCH (n:Sentence {text:'Hello monde!'}) SET n.text='Hello world!' RETURN n</query>
    </changeset>
</changelog>

These migrations can be run using the Java API, via your Maven project or simply using the command line. Choose the way that fits your project

Java API

Save the migration file at ${your_project}/${root_classpath_folder}/changelog.xml.

Then, include Liquigraph in your pom.xml:

<dependency>
    <groupId>org.liquigraph</groupId>
    <artifactId>liquigraph-core</artifactId>
    <version>3.0.1</version>
</dependency>

Finally, running the migrations can be done as follows:

import org.liquigraph.core.api.Liquigraph;
import org.liquigraph.core.configuration.Configuration;
import org.liquigraph.core.configuration.ConfigurationBuilder;

// [...]

Configuration configuration = new ConfigurationBuilder()
				.withMasterChangelogLocation("changelog.xml")
				.withUri("jdbc:neo4j:http://localhost:7474")
				.withRunMode()
				.build();

Liquigraph liquigraph = new Liquigraph();
liquigraph.runMigrations(configuration);

If you want to simulate (also known as "dry-run") your migrations without actually running them, replace withRunMode() by withDryRunMode(Paths.get(outputDirectory)), where outputDirectory specifies the path of the directory where output.cypher will be written in.

The Maven way

Save the migration file at ${your_project}/src/main/resources/changelog.xml.

Then, include Liquigraph plugin in your pom.xml:

<plugin>
    <groupId>org.liquigraph</groupId>
    <artifactId>liquigraph-maven-plugin</artifactId>
    <version>3.0.1</version>
    <configuration>
        <changelog>changelog.xml</changelog><!-- classpath location -->
        <jdbcUri>jdbc:neo4j:http://localhost:7474</jdbcUri>
    </configuration>
    <executions>
        <execution>
          <id>run-migrations</id>
          <goals>
            <goal>dry-run</goal>
            <goal>run</goal>
          </goals>
        </execution>
    </executions>
</plugin>

And finally, run:

$> mvn clean package

Full documentation is here.

Highway to shell

Save the migration file at ${your_project}/migrations/changelog.xml.

Homebrew users, skip the following installation steps altogether. Specific instructions are here.

Prerequisites

  1. make sure at least JDK7 is set up and Java is included in your path
  2. decompress Liquigraph zip, tar.gz or tar.bz2 in LIQUIGRAPH_DIR
  3. include LIQUIGRAPH_DIR/liquigraph-cli/ to your PATH
  4. Unix users: make sure LIQUIGRAPH_DIR/liquigraph-cli/liquigraph.sh is executable

You can now check your installation by executing the following command (reminder to non-Unix users: replace .sh with .bat):

Running Liquigraph

Interactive mode

$> cd LIQUIGRAPH_DIR/liquigraph-cli
$> ./liquigraph.sh --help

A description should be displayed. Then, simulate the changes (replace /tmp with whatever floats your boat):

$> ./liquigraph.sh --changelog "${your_project}/migrations/changelog.xml" \
		   --username neo4j \
		   --password \ # leave empty (password prompt will appear)
		   --graph-db-uri jdbc:neo4j:http://localhost:7474/ \
		   --dry-run-output-directory /tmp
# check contents
$> less /tmp/output.cypher

And finally, run:

$> ./liquigraph.sh --changelog "${your_project}/migrations/changelog.xml" \
		   --username neo4j \
		   --password \ # leave empty (password prompt will appear)
		   --graph-db-uri jdbc:neo4j:http://localhost:7474/

Non-interactive mode

The above methods will prompt for your Neo4j password. If you wish to run liquigraph without password prompt (i.e. non interactively):

$> touch my_password.txt
# update mypassword.txt to contain your password and chmod appropriately
$> ./liquigraph.sh --changelog "${your_project}/migrations/changelog.xml" \
 		   --username neo4j \
 		   --graph-db-uri jdbc:neo4j:http://localhost:7474/ \
		   --password < my_password.txt
			

Detailed section

Definitions

Changelog

A Liquigraph changelog defines a set of migrations to be performed. There can be only one changelog as entry point per project.

<?xml version="1.0" encoding="UTF-8"?>
<changelog xmlns="https://fbiville.github.io/liquigraph/schema/1.0/liquigraph.xsd">
    <!-- import "sub-changelogs"-->
    <import resource="version_1/sub_changelog.xml" />
    <import resource="version_2/sub_changelog.xml" />
    <!-- and/or define directly changesets-->
    <changeset [...] /> 
    <!-- [...] -->
</changelog>

Both sub_changelog.xml files could import changelogs and/or define changeset elements. Their root element is also changelog.

Changeset

A Liquigraph changeset describes one or more create or update statement. These statements must be written in Cypher Query Language and are wrapped in a single transaction (1 transaction per changeset). A changeset can be run only once (incremental) and cannot be altered (immutable) against the same graph database instance, by default. Finally, a changeset is uniquely identified within the changelog by its mandatory ID and author attributes.

<changeset id="unique_identifier" author="team_or_individual_name">
    <!-- 1 to n queries: all executed in 1 transaction -->
    <query>CREATE (m:MyAwesomeNode) RETURN m</query>
    <query>CREATE (m:MyOtherAwesomeNode) RETURN m</query>
</changeset>

Both sub_changelog.xml files could import changelogs and/or define changeset elements.

Execution context

An execution context is a simple string, defined at changeset level. A changeset can have 0, 1 or more execution contexts (in the latter case, they're comma-separated). For instance:

<changeset id="hello-world" author="you" contexts="foo,bar">
   <query>CREATE (n:Sentence {text:'Hello monde!'}) RETURN n</query>
</changeset>

If no execution contexts are specified at runtime, all changesets will match.
If one or more execution contexts are specified at runtime, changesets will be selected:

  • if they do not declare any execution contexts
  • or one of their declared contexts match one of the runtime contexts
Changeset immutability

As previously mentioned, Liquigraph changesets are immutable by default (an error will be thrown if they have been altered). That said, there may be situations where changesets should be run whenever their contents have changed. To achieve this, you just need to define one extra attribute:

<changeset id="hello-world" author="you" run-on-change="true">
   <query>CREATE (n:Sentence {text:'Hello monde!'}) RETURN n</query>
</changeset>
Changeset incrementality

As previously mentioned, Liquigraph changesets are incremental by default (they will be executed only once). That said, there may be situations where changesets should be run at every execution. To achieve this, you just need to define one extra attribute:

<changeset id="hello-world" author="you" run-always="true">
   <query>CREATE (n:Sentence {text:'Hello monde!'}) RETURN n</query>
</changeset>
Changeset precondition

Changeset preconditions act like guards. They make sure that the changeset query will be executed if and only if the precondition is met.

A precondition can be simple (1 Cypher query) or compound (with boolean AND/OR operators).

In any case, each of the subqueries/the simple query has to return exactly one column named result of type boolean (true|false).

Note that a precondition cannot modify the database itself: all changes will be rolled back.

If the precondition fails, an error policy has to be selected amongst the following choices:

  • CONTINUE: ignores the precondition error and skips the associated changeset execution
  • MARK_AS_EXECUTED: ignores the precondition error and marks the changeset as executed (without actually executing it)
  • FAIL: halts the whole execution, reporting the precondition error

Here is a basic precondition example (you are right: this is the dumbest precondition ever):

<changeset id="hello-world" author="you">
   <precondition if-not-met="MARK_AS_EXECUTED">
      <query>RETURN true AS result</query>
   </precondition>
   <query>CREATE (n:Sentence {text:'Hello monde!'}) RETURN n</query>
</changeset>

Here comes a slightly more complicated example:

<changeset id="contest-winner-selection" author="futuroscope-engineering">
   <precondition if-not-met="FAIL">
      <or>
         <and>
            <query>MATCH (p:User {twitter:'@fbiville'}) RETURN NOT (p.underAge) AS result</query>
            <query><![CDATA[MATCH (p:User {twitter:'@fbiville'}) OPTIONAL MATCH (p)-[:SUFFERS_FROM]->(d:NEURO_DISORDER {name:'photosensitive epilepsy'}) RETURN (d IS NULL) AS result]]></query>
         </and>
         <query><![CDATA[MATCH (p:User {twitter:'@fbiville'}) OPTIONAL MATCH (p)<-[:HAS_PARENTAL_CONTROL]-(parent:User) RETURN NOT (parent IS NULL) AS RESULT]]></query>
      </or>
   </precondition>
   <query><![CDATA[MATCH (p:User {twitter:'@fbiville'}) CREATE (p)-[:IS_OFFERED_FREE_PASS_TO]->(:Location {name:'Futuroscope'})]]></query>
</changeset>
Changeset postcondition

Changeset postconditions allow a single changeset to be applied as many times as necessary to complete the migration, by repeating the changeset as long as the postcondition is met.

A postcondition can be simple (1 Cypher query) or compound (with boolean AND/OR operators).

In any case, each of the subqueries/the simple query has to return exactly one column named result of type boolean (true|false).

Note that a postcondition cannot modify the database itself: all changes will be rolled back.

Once the postcondition returns false, the changeset is considered complete.

A repeatable changeset can be used to perform a migration on a large database without risking an OutOfMemoryError because of the large number of nodes or relationships impacted, by splitting the migration into smaller batches.

Here is a postcondition example, deleting all relationships in the database by batches of 10:

<changeset id="delete-relationships" author="you">
   <query><![CDATA[MATCH ()-[r]->() WITH r LIMIT 10 DELETE r]]></query>
   <postcondition>
      <query><![CDATA[OPTIONAL MATCH ()-[r]->() WITH r LIMIT 1 RETURN (r IS NOT NULL) AS result]]></query>
   </postcondition>
</changeset>

Troubleshooting

Feel free to ping @fbiville if you have any questions.

Bug reporting, improvement suggestion... belong here.