Skip to content

Features#

Cypher Change Log Format#

[Required plugin version | 4.9.0.1]

The extension supports polyglot change logs (XML, SQL, YAML ...). The SQL format has been aliased to the more idiomatic Cypher format.

Cypher file names must end with .cypher. Cypher change logs must start with the comment --liquibase formatted cypher. Here is an example of a supported Cypher file:

-- liquibase formatted cypher

-- changeset fbiville:my-movie-init
CREATE (:Movie {title: 'My Life', genre: 'Comedy'});

Cypher files that are part of a folder inclusion must only contain a single Cypher query and no comment directive.

The extension supports change log file and folder inclusion.

Cypher and Rollback Changes#

[Required plugin version (Cypher alias) | 4.7.1.1]

The built-in SQL and rollback changes are supported. The SQL change is also aliased to cypher.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
    </changeSet>

    <changeSet id="translate" author="fbiville">
        <neo4j:cypher>MATCH (m:Movie {title: 'My Life'}) SET m.genre = 'Comédie'</neo4j:cypher>
        <rollback>MATCH (m:Movie {title: 'My Life'}) SET m.genre = 'Comedy'</rollback>
    </changeSet>

</databaseChangeLog>

Warning

  • The cypher XML tag needs to be prepended with the corresponding extension namespace prefix.
  • If the query contains XML special characters such as < or >, make sure to surround the query content with <![CDATA[ at the beginning and ]]> at the end.
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "translate",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "MATCH (m:Movie {title: 'My Life'}) SET m.genre = 'Comédie'"
          }
        ],
        "rollback": [
          {
            "cypher": "MATCH (m:Movie {title: 'My Life'}) SET m.genre = 'Comedy'"
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
  - changeSet:
      id: translate
      author: fbiville
      changes:
        - cypher: 'MATCH (m:Movie {title: ''My Life''}) SET m.genre = ''Comédie'''
      rollback:
        - cypher: 'MATCH (m:Movie {title: ''My Life''}) SET m.genre = ''Comedy'''
-- liquibase formatted cypher

-- changeset fbiville:my-movie-init
CREATE (:Movie {title: 'My Life', genre: 'Comedy'});

-- changeset fbiville:translate
MATCH (m:Movie {title: 'My Life'}) SET m.genre = 'Comédie'
-- rollback MATCH (m:Movie {title: 'My Life'}) SET m.genre = 'Comedy'

Neo4j Preconditions#

Learn more about preconditions in the Liquibase documentation, especially the failure and error handling section.

Built-in precondition support#

The extension has been successfully tested with the following built-in precondition since the very first release of the plugin:

  • dbms (targeting Neo4j)
  • sqlCheck (aliased as cypherCheck, see below)

Others may work but have not been tested.

Version check#

[Required plugin version | 4.9.0]

The version precondition asserts the runtime Neo4j version starts with the specified string. It can be combined with other preconditions with the standard boolean operators.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-neo4j-44-deployment" author="fbiville">
        <preConditions onFail="CONTINUE">
            <neo4j:version matches="4.4"/>
        </preConditions>
        <neo4j:cypher>CREATE (:Neo4j {neo4j44: true})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-neo4j-non44-deployment" author="fbiville">
        <preConditions onFail="CONTINUE">
            <not>
                <neo4j:version matches="4.4"/>
            </not>
        </preConditions>
        <neo4j:cypher>CREATE (:Neo4j {neo4j44: false})</neo4j:cypher>
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-neo4j-44-deployment",
        "author": "fbiville",
        "preConditions": [
          {
            "onFail": "CONTINUE"
          },
          {
            "version": {
              "matches": "4.4"
            }
          }
        ],
        "changes": [
          {
            "cypher": "CREATE (:Neo4j {neo4j44: true})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-neo4j-non44-deployment",
        "author": "fbiville",
        "preConditions": [
          {
            "onFail": "CONTINUE"
          },
          {
            "not": {
              "version": {
                "matches": "4.4"
              }
            }
          }
        ],
        "changes": [
          {
            "cypher": "CREATE (:Neo4j {neo4j44: false})"
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-neo4j-44-deployment
      author: fbiville
      preConditions:
        - onFail: 'CONTINUE'
        - version:
            matches: '4.4'
      changes:
        - cypher: 'CREATE (:Neo4j {neo4j44: true})'
  - changeSet:
      id: my-neo4j-non44-deployment
      author: fbiville
      preConditions:
        - onFail: 'CONTINUE'
        - not:
            - version:
                matches: '4.4'
      changes:
        - cypher: 'CREATE (:Neo4j {neo4j44: false})'

Edition check#

[Required plugin version | 4.9.0]

The edition check asserts whether the target Neo4j deployment is the Community Edition or Enterprise Edition. It can be combined with other preconditions with the standard boolean operators.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-neo4j-ee-deployment" author="fbiville">
        <preConditions onFail="CONTINUE">
            <neo4j:edition enterprise="true"/>
        </preConditions>
        <neo4j:cypher>CREATE (:Neo4j {enterprise: true})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-neo4j-ce-deployment" author="fbiville">
        <preConditions onFail="CONTINUE">
            <neo4j:edition enterprise="false"/>
        </preConditions>
        <neo4j:cypher>CREATE (:Neo4j {enterprise: false})</neo4j:cypher>
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-neo4j-ee-deployment",
        "author": "fbiville",
        "preConditions": [
          {
            "onFail": "CONTINUE"
          },
          {
            "edition": {
              "enterprise": true
            }
          }
        ],
        "changes": [
          {
            "cypher": "CREATE (:Neo4j {enterprise: true})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-neo4j-ce-deployment",
        "author": "fbiville",
        "preConditions": [
          {
            "onFail": "CONTINUE"
          },
          {
            "edition": {
              "enterprise": false
            }
          }
        ],
        "changes": [
          {
            "cypher": "CREATE (:Neo4j {enterprise: false})"
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-neo4j-ee-deployment
      author: fbiville
      preConditions:
        - edition:
            enterprise: true
        - onFail: 'CONTINUE'
      changes:
        - cypher: 'CREATE (:Neo4j {enterprise: true})'
  - changeSet:
      id: my-neo4j-ce-deployment
      author: fbiville
      preConditions:
        - edition:
            enterprise: false
        - onFail: 'CONTINUE'
      changes:
        - cypher: 'CREATE (:Neo4j {enterprise: false})'

Cypher check alias#

[Required plugin version | 4.9.0]

The cypherCheck aliases the existing sqlCheck precondition. Cypher formatted change log files can only use sqlCheck at the moment. cypherCheck can be combined with other preconditions with the standard boolean operators.

Warning

Before version 4.21.1.2, JSON and YAML change logs had to specify a sql attribute instead of cypher.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-neo4j-deployment" author="fbiville">
        <preConditions onFail="CONTINUE">
            <neo4j:cypherCheck expectedResult="0">MATCH (n:Neo4j) RETURN count(n)</neo4j:cypherCheck>
        </preConditions>
        <neo4j:cypher>CREATE (:Neo4j)</neo4j:cypher>
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-neo4j-deployment",
        "author": "fbiville",
        "preConditions": [
          {
            "onFail": "CONTINUE"
          },
          {
            "cypherCheck": {
              "expectedResult": "0",
              "cypher": "MATCH (n:Neo4j) RETURN count(n)"
            }
          }
        ],
        "changes": [
          {
            "cypher": {
              "cypher": "CREATE (:Neo4j)"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-neo4j-deployment
      author: fbiville
      preConditions:
        - onFail: 'CONTINUE'
        - cypherCheck:
            expectedResult: '0'
            cypher: MATCH (n:Neo4j) RETURN count(n)
      changes:
        - cypher:
            cypher: CREATE (:Neo4j)

Insert Change#

[Required plugin version | 4.21.1]

The change allows to define the creation of individual nodes, with a single label specified with labelName.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">

    <changeSet id="insert-node" author="fbiville">
        <neo4j:insert labelName="Person">
            <column name="id" value="8987212b-a6ff-48a1-901f-8c4b39bd6d9e" type="uuid"/>
            <column name="age" valueNumeric="30" type="integer"/>
            <column name="first_name" value="Florent"/>
            <column name="last_name" value="Biville"/>
            <column name="local_date" valueDate="2022-12-25" type="date"/>
            <column name="local_time" valueDate="22:23:24" type="date"/>
            <column name="local_date_time" valueDate="2018-02-01T12:13:14" type="date"/>
            <column name="zoned_date_time" valueDate="2020-07-12T22:23:24+02:00" type="date"/>
            <column name="polite" valueBoolean="true" type="boolean"/>
            <column name="picture" value="DLxmEfVUC9CAmjiNyVphWw==" type="blob"/>
            <column name="bio"
                    value="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisi tellus, elementum id mi vitae, faucibus lacinia purus. Integer nec velit sit amet velit tincidunt ultrices eu eu massa. Vestibulum in libero vel neque interdum blandit in non libero. Aenean iaculis, erat ac molestie laoreet, risus ex faucibus odio, a fermentum turpis elit eget ex. Donec volutpat bibendum enim pretium pulvinar. Proin rutrum neque dui, a suscipit tellus semper suscipit. Praesent lobortis ut lorem vitae volutpat. Pellentesque a lorem eu lacus faucibus facilisis nec sed metus. Aenean lacinia luctus ultricies. Pellentesque cursus justo non iaculis tristique. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Duis tempor nisi ut turpis bibendum facilisis. Donec aliquet porttitor lacus, non rhoncus lectus laoreet et."
                    type="clob"/>
        </neo4j:insert>
    </changeSet>
</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "insert-node",
        "author": "fbiville",
        "changes": [
          {
            "insert": {
              "columns": [
                {
                  "column": {
                    "name": "id",
                    "type": "uuid",
                    "value": "8987212b-a6ff-48a1-901f-8c4b39bd6d9e"
                  }
                },
                {
                  "column": {
                    "name": "age",
                    "type": "integer",
                    "valueNumeric": 30
                  }
                },
                {
                  "column": {
                    "name": "first_name",
                    "value": "Florent"
                  }
                },
                {
                  "column": {
                    "name": "last_name",
                    "value": "Biville"
                  }
                },
                {
                  "column": {
                    "name": "local_date",
                    "type": "date",
                    "valueDate": "2022-12-25"
                  }
                },
                {
                  "column": {
                    "name": "local_time",
                    "type": "date",
                    "valueDate": "22:23:24"
                  }
                },
                {
                  "column": {
                    "name": "local_date_time",
                    "type": "date",
                    "valueDate": "2018-02-01T12:13:14"
                  }
                },
                {
                  "column": {
                    "name": "zoned_date_time",
                    "type": "date",
                    "valueDate": "2020-07-12T22:23:24+02:00"
                  }
                },
                {
                  "column": {
                    "name": "polite",
                    "type": "boolean",
                    "valueBoolean": true
                  }
                },
                {
                  "column": {
                    "name": "picture",
                    "type": "blob",
                    "value": "DLxmEfVUC9CAmjiNyVphWw=="
                  }
                },
                {
                  "column": {
                    "name": "bio",
                    "type": "clob",
                    "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisi tellus, elementum id mi vitae, faucibus lacinia purus. Integer nec velit sit amet velit tincidunt ultrices eu eu massa. Vestibulum in libero vel neque interdum blandit in non libero. Aenean iaculis, erat ac molestie laoreet, risus ex faucibus odio, a fermentum turpis elit eget ex. Donec volutpat bibendum enim pretium pulvinar. Proin rutrum neque dui, a suscipit tellus semper suscipit. Praesent lobortis ut lorem vitae volutpat. Pellentesque a lorem eu lacus faucibus facilisis nec sed metus. Aenean lacinia luctus ultricies. Pellentesque cursus justo non iaculis tristique. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Duis tempor nisi ut turpis bibendum facilisis. Donec aliquet porttitor lacus, non rhoncus lectus laoreet et."
                  }
                }
              ],
              "labelName": "Person"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: insert-node
      author: fbiville
      changes:
        - insert:
            columns:
              - column:
                  name: id
                  type: uuid
                  value: 8987212b-a6ff-48a1-901f-8c4b39bd6d9e
              - column:
                  name: age
                  type: integer
                  valueNumeric: !!float '30'
              - column:
                  name: first_name
                  value: Florent
              - column:
                  name: last_name
                  value: Biville
              - column:
                  name: local_date
                  type: date
                  valueDate: '2022-12-25'
              - column:
                  name: local_time
                  type: date
                  valueDate: '22:23:24'
              - column:
                  name: local_date_time
                  type: date
                  valueDate: '2018-02-01T12:13:14'
              - column:
                  name: zoned_date_time
                  type: date
                  valueDate: '2020-07-12T22:23:24+02:00'
              - column:
                  name: polite
                  type: boolean
                  valueBoolean: true
              - column:
                  name: picture
                  type: blob
                  value: DLxmEfVUC9CAmjiNyVphWw==
              - column:
                  name: bio
                  type: clob
                  value: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean
                    nisi tellus, elementum id mi vitae, faucibus lacinia purus. Integer
                    nec velit sit amet velit tincidunt ultrices eu eu massa. Vestibulum
                    in libero vel neque interdum blandit in non libero. Aenean iaculis,
                    erat ac molestie laoreet, risus ex faucibus odio, a fermentum turpis
                    elit eget ex. Donec volutpat bibendum enim pretium pulvinar. Proin rutrum
                    neque dui, a suscipit tellus semper suscipit. Praesent lobortis ut lorem
                    vitae volutpat. Pellentesque a lorem eu lacus faucibus facilisis nec
                    sed metus. Aenean lacinia luctus ultricies. Pellentesque cursus justo
                    non iaculis tristique. Vestibulum ante ipsum primis in faucibus orci
                    luctus et ultrices posuere cubilia curae; Duis tempor nisi ut turpis
                    bibendum facilisis. Donec aliquet porttitor lacus, non rhoncus lectus
                    laoreet et.
            labelName: Person

Please refer to the Load Data documentation for the supported value types for each column.

Load Data#

[Required Liquibase core version | 4.11.0] [Required plugin version | 4.16.1.1]

Assuming the following (S)CSV data.scsv file:

name;age;some_date;ignored;uuid;is_polite;blob
Florent;30.5;2022-12-25;ignored;8d1208fc-f401-496c-9cb8-483fef121234;false;DLxmEfVUC9CAmjiNyVphWw==
Andrea;32;2020-07-12T22:23:24+02:00;ignored!;1bc59ddb-8d4d-41d0-9c9a-34e837de5678;true;NULL
Nathan;34;2018-02-01T12:13:14;ignored!;123e4567-e89b-12d3-a456-426614174000;true;NULL
Robert;36;22:23:24;ignored!;9986a49a-0cce-4982-b491-b8177fd0ef81;true;NULL
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="customer-import" author="asanturbano">
        <loadData
                file="e2e/load-data/data.scsv"
                separator=";"
                tableName="CsvPerson">
            <column name="first_name" header="name" type="string"/>
            <column name="wisdom_index" header="age" type="numeric"/>
            <column name="some_date" index="2" type="date"/>
            <column name="_" header="ignored" type="skip"/>
            <column name="uuid" header="uuid" type="uuid"/>
            <column name="polite" header="is_polite" type="boolean"/>
            <column name="picture" header="blob" type="blob"/>
        </loadData>
    </changeSet>
</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "customer-import",
        "author": "asanturbano",
        "changes": [
          {
            "loadData": {
              "columns": [
                {
                  "column": {
                    "header": "name",
                    "name": "first_name",
                    "type": "string"
                  }
                },
                {
                  "column": {
                    "header": "age",
                    "name": "wisdom_index",
                    "type": "numeric"
                  }
                },
                {
                  "column": {
                    "index": 2,
                    "name": "some_date",
                    "type": "date"
                  }
                },
                {
                  "column": {
                    "header": "ignored",
                    "name": "_",
                    "type": "skip"
                  }
                },
                {
                  "column": {
                    "header": "uuid",
                    "name": "uuid",
                    "type": "uuid"
                  }
                },
                {
                  "column": {
                    "header": "is_polite",
                    "name": "polite",
                    "type": "boolean"
                  }
                },
                {
                  "column": {
                    "header": "blob",
                    "name": "picture",
                    "type": "blob"
                  }
                }
              ],
              "file": "e2e/load-data/data.scsv",
              "separator": ";",
              "tableName": "CsvPerson"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: customer-import
      author: asanturbano
      changes:
        - loadData:
            columns:
              - column:
                  header: name
                  name: first_name
                  type: string
              - column:
                  header: age
                  name: wisdom_index
                  type: numeric
              - column:
                  index: 2
                  name: some_date
                  type: date
              - column:
                  header: ignored
                  name: _
                  type: skip
              - column:
                  header: uuid
                  name: uuid
                  type: uuid
              - column:
                  header: is_polite
                  name: polite
                  type: boolean
              - column:
                  header: blob
                  name: picture
                  type: blob
            file: e2e/load-data/data.scsv
            separator: ;
            tableName: CsvPerson

The general documentation of this change is available here.

The table below details how each supported data type is mapped to its Neo4j counterpart:

Load Data Type Liquibase Java Type Example Value Resulting Neo4j Java Type
BLOB String DLxmEfVUC9CAmjiNyVphWw== (base64-encoded) byte[]
BOOLEAN Boolean true or false Boolean
CLOB String String
DATE java.sql.Timestamp 2018-02-01T12:13:14 java.time.LocalDateTime
DATE java.sql.Date 2018-02-01 java.time.LocalDate
DATE java.sql.Time 12:13:14 java.time.LocalTime
DATE liquibase.statement.DatabaseFunction 2018-02-01T12:13:14+02:00 java.time.ZonedDateTime
NUMERIC liquibase.change.ColumnConfig.ValueNumeric 42 or 42.0 Long or Double
STRING String "a string" String
UUID String 1bc59ddb-8d4d-41d0-9c9a-34e837de5678 String

SKIP is also supported: the value will be ignored.

Warning

BLOB files (see issue), CLOB files (see issue), SEQUENCE, COMPUTED, OTHER and UNKNOWN load data types are currently unsupported.

Make sure to use the right valueXxx attribute:

  • valueBoolean for boolean values
  • valueDate for date/time values
  • valueNumeric for numeric values
  • value for everything else

Graph refactorings#

Node Merge#

[Required plugin version | 4.13.0]

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Horror'})</neo4j:cypher>
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Documentary'})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:mergeNodes fragment="(m:Movie {title: 'My Life'}) WITH m ORDER BY m.genre ASC" outputVariable="m">
            <neo4j:propertyPolicy nameMatcher=".*" mergeStrategy="KEEP_FIRST"/>
        </neo4j:mergeNodes>
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          },
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Horror'})"
          },
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Documentary'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "mergeNodes": {
              "fragment": "(m:Movie {title: 'My Life'}) WITH m ORDER BY m.genre ASC",
              "outputVariable": "m",
              "propertyPolicies": [
                {
                  "propertyPolicy": {
                    "mergeStrategy": "KEEP_FIRST",
                    "nameMatcher": ".*"
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Horror''})'
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Documentary''})'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - mergeNodes:
            fragment: '(m:Movie {title: ''My Life''}) WITH m ORDER BY m.genre ASC'
            outputVariable: m
            propertyPolicies:
              - propertyPolicy:
                  mergeStrategy: 'KEEP_FIRST'
                  nameMatcher: .*

Specify a Cypher query fragment, which defines the nodes to match for the merge operation. If fewer than two nodes are matched, the merge is a no-op.

Specify the variable in that fragment that refer to the matched nodes. This is reused to create the internal merge Cypher queries to execute.

Finally, make sure to define the merge policy for each persisted property. The extension goes through every unique property name, selects the first matching merge policy, in declaration order. If at least one property name does not match a policy, the merge fails and is canceled. Once the policy is matched for the property name, one of the following operations happens:

  • KEEP_FIRST: the first defined property value for that name is kept
  • KEEP_LAST: the last defined property value for that name is kept
  • KEEP_ALL: all defined property values are aggregated into an array (even if only a single value is found)

Note

"first" and "last" are defined by the ordering of the specified Cypher query fragment. It is strongly advised to explicitly order the matched nodes with the ORDER BY clause like in the example.

Node Property Extraction#

[Required plugin version | 4.17.2]

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
    </changeSet>

    <changeSet id="genre-extraction" author="marouane">
        <neo4j:extractProperty property="genre" fromNodes="(m:Movie) WITH m ORDER BY id(m) ASC" nodesNamed="m">
            <neo4j:toNodes withLabel="Genre" withProperty="genre">
                <neo4j:linkedFromSource withType="HAS_GENRE" withDirection="OUTGOING" />
            </neo4j:toNodes>
        </neo4j:extractProperty>
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "genre-extraction",
        "author": "marouane",
        "changes": [
          {
            "extractProperty": {
              "fromNodes": "(m:Movie) WITH m ORDER BY id(m) ASC",
              "nodesNamed": "m",
              "property": "genre",
              "toNodes": {
                "withLabel": "Genre",
                "withProperty": "genre",
                "linkedFromSource": {
                  "withDirection": "OUTGOING",
                  "withType": "HAS_GENRE"
                }
              }
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
  - changeSet:
      id: genre-extraction
      author: marouane
      changes:
        - extractProperty:
            fromNodes: '(m:Movie) WITH m ORDER BY id(m) ASC'
            nodesNamed: 'm'
            property: 'genre'
            toNodes:
              withLabel: 'Genre'
              withProperty: 'genre'
              linkedFromSource:
                withDirection: 'OUTGOING'
                withType: 'HAS_GENRE'

The node property extraction refactoring allows to extract node properties into their own nodes. As for the node merge refactoring, the nodes to extract properties from are specified as a Cypher fragment (fromNodes attribute) and the variable name (nodesNamed attribute) bound to these nodes. The property name to extract is specified with the property attribute.

The source nodes matched by the Cypher fragment will have their property removed. That property will be set on the extracted nodes, with the name described by the withProperty attribute. The extracted nodes' label is defined with the withLabel attribute. Set the merge property to true to avoid duplicates with potentially existing nodes with the same label and property. The default behavior is to create extracted nodes every time.

Optionally, the extracted nodes can be linked with the source nodes. In that case, a type and a direction need to be specified with respectively the withType and withDirection attributes.

Note

The relation direction is from the perspective of the source nodes. In the example, OUTGOING means the relationship starts from the source node and goes out to the extracted node. Conversely, INCOMING would mean the relationship comes in the source node from the extracted node.

It is also possible to avoid relationship duplicates by setting the corresponding merge attribute to true. The default is to always create relationships.

Warning

merge=false on nodes with merge=true on relationships will trigger a validation warning. Indeed, creating extracted nodes imply that new relationships will be created as well. Setting merge=true on relationships in that case incur an unnecessary execution penalty.

Node Label Rename#

[Required plugin version | 4.25.0.1]

The label rename refactoring allows to rename one label to another, matching all or some of its nodes, in a single transaction or in batches.

As illustrated below, the main attributes of the refactoring are:

  • from: value of the existing label
  • to: value of the new label, replacing the existing one

Global Rename#

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
        <neo4j:cypher>CREATE (:Book {title: 'My Life', genre: 'Autobiography'})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameLabel from="Movie" to="Film" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          },
          {
            "cypher": "CREATE (:Book {title: 'My Life', genre: 'Autobiography'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameLabel": {
              "from": "Movie",
              "to": "Film"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
        - cypher: 'CREATE (:Book {title: ''My Life'', genre: ''Autobiography''})'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameLabel:
            from: 'Movie'
            to: 'Film'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
        <neo4j:cypher>CREATE (:Book {title: 'My Life', genre: 'Autobiography'})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameLabel from="Movie" to="Film" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          },
          {
            "cypher": "CREATE (:Book {title: 'My Life', genre: 'Autobiography'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameLabel": {
              "from": "Movie",
              "to": "Film",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
        - cypher: 'CREATE (:Book {title: ''My Life'', genre: ''Autobiography''})'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameLabel:
            from: 'Movie'
            to: 'Film'
            enableBatchImport: true
            batchSize: 1

Partial Rename#

The following attributes can also be set, in order to match only a subset of the nodes with the label specified in from:

  • fragment specifies the pattern to match the nodes against
  • outputVariable specifies the Cypher variable name defined in fragment that denotes the targeted nodes

Note

The nodes that are going to be renamed sit at the intersection of what is defined in fragment and the nodes whose label is specified by from. In other words, if none of the nodes defined in fragment carry the label defined in from, the rename is not going to modify any of those.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
        <neo4j:cypher>CREATE (:Movie {title: 'My Birthday', genre: 'Musical'})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameLabel from="Movie" to="Film" fragment="(m:Movie {title: 'My Birthday'})" outputVariable="m"  />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          },
          {
            "cypher": "CREATE (:Movie {title: 'My Birthday', genre: 'Musical'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameLabel": {
              "from": "Movie",
              "to": "Film",
              "fragment": "(m:Movie {title: 'My Birthday'})",
              "outputVariable": "m"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
        - cypher: 'CREATE (:Movie {title: ''My Birthday'', genre: ''Musical''})'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameLabel:
            from: 'Movie'
            to: 'Film'
            fragment: '(m:Movie {title: ''My Birthday''})'
            outputVariable: 'm'

Since this operation can potentially affect a lot of nodes, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even run out of memory.

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher>CREATE (:Movie {title: 'My Life', genre: 'Comedy'})</neo4j:cypher>
        <neo4j:cypher>CREATE (:Movie {title: 'My Birthday', genre: 'Musical'})</neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameLabel from="Movie" to="Film" fragment="(m:Movie {title: 'My Birthday'})" outputVariable="m" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {title: 'My Life', genre: 'Comedy'})"
          },
          {
            "cypher": "CREATE (:Movie {title: 'My Birthday', genre: 'Musical'})"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameLabel": {
              "from": "Movie",
              "to": "Film",
              "fragment": "(m:Movie {title: 'My Birthday'})",
              "outputVariable": "m",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {title: ''My Life'', genre: ''Comedy''})'
        - cypher: 'CREATE (:Movie {title: ''My Birthday'', genre: ''Musical''})'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameLabel:
            from: 'Movie'
            to: 'Film'
            fragment: '(m:Movie {title: ''My Birthday''})'
            outputVariable: 'm'
            enableBatchImport: true
            batchSize: 1

Relationship Type Rename#

[Required plugin version | 4.25.0.1]

The type rename refactoring allows to rename one type to another, matching all or some of its relationships, in a single transaction or in batches.

As illustrated below, the main attributes of the refactoring are:

  • from: value of the existing type
  • to: value of the new type, replacing the existing one

Global Rename#

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameType from="SEEN_BY" to="VIEWED_BY" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameType": {
              "from": "SEEN_BY",
              "to": "VIEWED_BY"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''now''}]->(:Person)'
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''yesterday''}]->(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameType:
            from: 'SEEN_BY'
            to: 'VIEWED_BY'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameType from="SEEN_BY" to="VIEWED_BY" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameType": {
              "from": "SEEN_BY",
              "to": "VIEWED_BY",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''now''}]->(:Person)'
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''yesterday''}]->(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameType:
            from: 'SEEN_BY'
            to: 'VIEWED_BY'
            enableBatchImport: true
            batchSize: 1

Partial Rename#

The following attributes can also be set, in order to match only a subset of the relationships with the type specified in from:

  • fragment specifies the pattern to match the relationships against
  • outputVariable specifies the Cypher variable name defined in fragment that denotes the targeted relationships

Note

The relationships that are going to be renamed sit at the intersection of what is defined in fragment and the relationships whose type is specified by from. In other words, if none of the relationships defined in fragment have the type defined in from, the rename is not going to modify any of those.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameType from="SEEN_BY" to="VIEWED_BY" fragment="(:Person)&lt;-[r:SEEN_BY]-()" outputVariable="r"  />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameType": {
              "from": "SEEN_BY",
              "to": "VIEWED_BY",
              "fragment": "(:Person)<-[r:SEEN_BY]-()",
              "outputVariable": "r"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''now''}]->(:Person)'
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''yesterday''}]->(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameType:
            from: 'SEEN_BY'
            to: 'VIEWED_BY'
            fragment: '(:Person)<-[r:SEEN_BY]-()'
            outputVariable: 'r'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameType from="SEEN_BY" to="VIEWED_BY" fragment="(:Person)&lt;-[r:SEEN_BY]-()" outputVariable="r" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'now'}]->(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)-[:SEEN_BY {date: 'yesterday'}]->(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameType": {
              "from": "SEEN_BY",
              "to": "VIEWED_BY",
              "fragment": "(:Person)<-[r:SEEN_BY]-()",
              "outputVariable": "r",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''now''}]->(:Person)'
        - cypher: 'CREATE (:Movie)-[:SEEN_BY {date: ''yesterday''}]->(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameType:
            from: 'SEEN_BY'
            to: 'VIEWED_BY'
            fragment: '(:Person)<-[r:SEEN_BY]-()'
            outputVariable: 'r'
            enableBatchImport: true
            batchSize: 1

Relationship Direction Inversion#

[Required plugin version | 4.25.1.1]

The direction inversion refactoring allows to flip the start and end node of relationships with the specified type, matching all or some of them, in a single transaction or in batches.

As illustrated below, the main attributes of the refactoring are:

  • type: type of the relationship(s) to invert

Global Inversion#

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)<-[:VIEWED_BY {date: 'yesterday'}]-(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:invertDirection type="VIEWED_BY" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)<-[:VIEWED_BY {date: 'yesterday'}]-(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "invertDirection": {
              "type": "VIEWED_BY"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)<-[:VIEWED_BY {date: ''now''}]-(:Person)'
        - cypher: 'CREATE (:Movie)<-[:VIEWED_BY {date: ''yesterday''}]-(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - invertDirection:
            type: 'VIEWED_BY'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)<-[:VIEWED_BY {date: 'yesterday'}]-(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:invertDirection type="VIEWED_BY" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)<-[:VIEWED_BY {date: 'yesterday'}]-(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "invertDirection": {
              "type": "VIEWED_BY",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)<-[:VIEWED_BY {date: ''now''}]-(:Person)'
        - cypher: 'CREATE (:Movie)<-[:VIEWED_BY {date: ''yesterday''}]-(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - invertDirection:
            type: 'VIEWED_BY'
            enableBatchImport: true
            batchSize: 1

Partial Inversion#

The following attributes can also be set, in order to match only a subset of the relationships with the type specified in type:

  • fragment specifies the pattern to match the relationships against
  • outputVariable specifies the Cypher variable name defined in fragment that denotes the targeted relationships

Note

The relationships that are going to be inverted sit at the intersection of what is defined in fragment and the relationships whose type is specified by type. In other words, if none of the relationships defined in fragment have the type defined in type, the inversion is not going to modify any of those.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:VIEWED_BY {date: 'now'}]->(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:VIEWED_BY {date: 'yesterday'}]->(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:invertDirection type="VIEWED_BY" fragment="(:Person)-[r:VIEWED_BY]-&gt;()" outputVariable="r"  />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)-[:VIEWED_BY {date: 'yesterday'}]->(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "invertDirection": {
              "type": "VIEWED_BY",
              "fragment": "(:Person)-[r:VIEWED_BY]->()",
              "outputVariable": "r"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)<-[:VIEWED_BY {date: ''now''}]-(:Person)'
        - cypher: 'CREATE (:Movie)-[:VIEWED_BY {date: ''yesterday''}]->(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - invertDirection:
            type: 'VIEWED_BY'
            fragment: '(:Person)-[r:VIEWED_BY]->()'
            outputVariable: 'r'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)]]></neo4j:cypher>
        <neo4j:cypher><![CDATA[CREATE (:Movie)-[:VIEWED_BY {date: 'yesterday'}]->(:Dog)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:invertDirection type="VIEWED_BY" fragment="(:Person)-[r:VIEWED_BY]-&gt;()" outputVariable="r" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie)<-[:VIEWED_BY {date: 'now'}]-(:Person)"
          },
          {
            "cypher": "CREATE (:Movie)-[:VIEWED_BY {date: 'yesterday'}]->(:Dog)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "invertDirection": {
              "type": "VIEWED_BY",
              "fragment": "(:Person)-[r:VIEWED_BY]->()",
              "outputVariable": "r",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie)<-[:VIEWED_BY {date: ''now''}]-(:Person)'
        - cypher: 'CREATE (:Movie)-[:VIEWED_BY {date: ''yesterday''}]->(:Dog)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - invertDirection:
            type: 'VIEWED_BY'
            fragment: '(:Person)-[r:VIEWED_BY]->()'
            outputVariable: 'r'
            enableBatchImport: true
            batchSize: 1

Property Rename#

[Required plugin version | 4.25.1.1]

The property rename refactoring allows to rename a property of all enclosing entities, or only nodes or relationships.

As illustrated below, the main attributes of the refactoring are:

  • from: name of the existing property
  • to: new name of the property, replacing the existing one
  • entityType: type of the enclosing entity to match. One of: ALL (default), NODE, RELATIONSHIP.

Global Rename#

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameProperty from="calendar_date" to="date"  />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameProperty": {
              "from": "calendar_date",
              "to": "date"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {calendar_date: ''today''})-[:SEEN_BY {calendar_date: ''now''}]->(:Person)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameProperty:
            from: 'calendar_date'
            to: 'date'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameProperty from="calendar_date" to="date" enableBatchImport="true" batchSize="1"  />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameProperty": {
              "from": "calendar_date",
              "to": "date",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {calendar_date: ''today''})-[:SEEN_BY {calendar_date: ''now''}]->(:Person)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameProperty:
            from: 'calendar_date'
            to: 'date'
            enableBatchImport: true
            batchSize: 1

Node-only Property Rename#

When setting the entityType attribute to NODE, only the matching properties of nodes will be renamed:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameProperty from="calendar_date" to="date" entityType="NODE" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameProperty": {
              "from": "calendar_date",
              "to": "date",
              "entityType": "NODE"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {calendar_date: ''today''})-[:SEEN_BY {calendar_date: ''now''}]->(:Person)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameProperty:
            from: 'calendar_date'
            to: 'date'
            entityType: 'NODE'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameProperty from="calendar_date" to="date" entityType="NODE" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameProperty": {
              "from": "calendar_date",
              "to": "date",
              "entityType": "NODE",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {calendar_date: ''today''})-[:SEEN_BY {calendar_date: ''now''}]->(:Person)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameProperty:
            from: 'calendar_date'
            to: 'date'
            entityType: 'NODE'
            enableBatchImport: true
            batchSize: 1

Relationship-only Property Rename#

When setting the entityType attribute to RELATIONSHIP, only the matching properties of relationships will be renamed:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville">
        <neo4j:renameProperty from="calendar_date" to="date" entityType="RELATIONSHIP" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "changes": [
          {
            "renameProperty": {
              "from": "calendar_date",
              "to": "date",
              "entityType": "RELATIONSHIP"
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {calendar_date: ''today''})-[:SEEN_BY {calendar_date: ''now''}]->(:Person)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      changes:
        - renameProperty:
            from: 'calendar_date'
            to: 'date'
            entityType: 'RELATIONSHIP'

Since this operation can potentially affect a lot of data, running the change in a single transaction may be infeasible since the transaction would likely run either too slow, or even out of memory.

To prevent this, enableBatchImport must be set to true. Since it relies on CALL {} IN TRANSACTIONS under the hood, the enclosing change set's runInTransaction must also be set to false. This results in the change being executed in batches.

Warning

This setting only works if the target Neo4j instance supports CALL {} IN TRANSACTIONS (version 4.4 and later). If not, the Neo4j plugin will run the change in a single, autocommit transaction.

Make sure to read about the consequences of changing runInTransaction.

The batchSize attribute controls how many transactions run. If the attribute is not set, the batch size is defined on the Neo4j server side.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:neo4j="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init-oops" author="fbiville">
        <neo4j:cypher><![CDATA[CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)]]></neo4j:cypher>
    </changeSet>

    <changeSet id="my-movie-init-fixed" author="fbiville" runInTransaction="false">
        <neo4j:renameProperty from="calendar_date" to="date" entityType="RELATIONSHIP" enableBatchImport="true" batchSize="1" />
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init-oops",
        "author": "fbiville",
        "changes": [
          {
            "cypher": "CREATE (:Movie {calendar_date: 'today'})-[:SEEN_BY {calendar_date: 'now'}]->(:Person)"
          }
        ]
      }
    },
    {
      "changeSet": {
        "id": "my-movie-init-fixed",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "renameProperty": {
              "from": "calendar_date",
              "to": "date",
              "entityType": "RELATIONSHIP",
              "enableBatchImport": true,
              "batchSize": 1
            }
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init-oops
      author: fbiville
      changes:
        - cypher: 'CREATE (:Movie {calendar_date: ''today''})-[:SEEN_BY {calendar_date: ''now''}]->(:Person)'
  - changeSet:
      id: my-movie-init-fixed
      author: fbiville
      runInTransaction: false
      changes:
        - renameProperty:
            from: 'calendar_date'
            to: 'date'
            entityType: 'RELATIONSHIP'
            enableBatchImport: true
            batchSize: 1

Change Set's runInTransaction#

The default value of runInTransaction is true. This means that all changes of a given change set run in a single, explicit transaction.

This is the right default and should be changed only if you need any of the following two Cypher constructs:

Indeed, using those constructs without disabling runInTransaction fails with a similar error message:

A query with 'CALL { ... } IN TRANSACTIONS' can only be executed in an implicit transaction, but tried to execute in an explicit transaction.

Setting runInTransaction to false on a change set means that all its changes are going to run in their own auto-commit (or implicit) transaction.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="my-movie-init" author="fbiville" runInTransaction="false">
        <sql>CALL { CREATE (:Movie {title: 'My Life', genre: 'Comedy'}) } IN TRANSACTIONS</sql>
    </changeSet>

</databaseChangeLog>
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "my-movie-init",
        "author": "fbiville",
        "runInTransaction": false,
        "changes": [
          {
            "cypher": "CALL { CREATE (:Movie {title: 'My Life', genre: 'Comedy'}) } IN TRANSACTIONS"
          }
        ]
      }
    }
  ]
}
databaseChangeLog:
  - changeSet:
      id: my-movie-init
      author: fbiville
      runInTransaction: false
      changes:
        - cypher: 'CALL { CREATE (:Movie {title: ''My Life'', genre: ''Comedy''}) } IN TRANSACTIONS'
-- liquibase formatted sql

-- changeset fbiville:my-movie-init runInTransaction:false
CALL { CREATE (:Movie {title: 'My Life', genre: 'Comedy'}) } IN TRANSACTIONS

History Consistency#

runInTransaction is a sharp tool and can lead to unintended consequences.

If any of the changes of the enclosing change set fails, the change set is not going to be stored in the history graph.

Re-running this change set results in all changes being run again, even the ones that successfully ran before.

In situations where runInTransactions="false" cannot be avoided, make sure the affected change set's queries are idempotent. Defining constraints and using Cypher's MERGE instead of CREATE usually helps.

Neo4j Isolation Level Refresher#

CALL {} IN TRANSACTIONS and PERIODIC COMMIT spawn a new transaction for each batch. Since Neo4j's default isolation level is 'read-committed', these new transactions can read data modified by previous transactions, whether they originate from the same statement or from a completely different one.

Let us illustrate this with a simple example.

Assume the data is initialized with:

CREATE (:Person {name: 'Alejandro'})
CREATE (:Person {name: 'Filipe'})
CREATE (:Person {name: 'Florent'})
CREATE (:Person {name: 'Marouane'})
CREATE (:Person {name: 'Nathan'})
This adds 5 nodes of label Person with a name property.

Running MATCH (p:Person) CALL { WITH p DELETE p } IN TRANSACTIONS OF 2 ROWS may have different outcomes. This query deletes all nodes with the Person label in batches of 2.

The execution may succeed and run with 3 batches if the data was not concurrently altered by other transactions.

It may need more batches if concurrent transactions create more Person nodes.

It may need fewer batches if concurrent transactions delete Person nodes.

The CALL {} IN TRANSACTIONS may also fail if concurrent transactions create a relationship to any of the above nodes, since DELETE assumes disconnected nodes (DETACH DELETE deletes nodes AND their relationships).