From 008f534f71fc68fc95b444d15cef3b80bfb24016 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Wed, 7 Aug 2024 12:50:06 +0200 Subject: [PATCH 1/3] Implement a simple sql template to support Postgre Signed-off-by: Paolo Di Tommaso --- src/main/java/io/seqera/migtool/App.java | 4 +++- src/main/java/io/seqera/migtool/Helper.java | 2 ++ src/main/java/io/seqera/migtool/MigTool.java | 16 +++++++++---- .../migtool/builder/DefaultSqlTemplate.java | 22 +++++++++++++++++ .../migtool/builder/PostgresSqlTemplate.java | 24 +++++++++++++++++++ .../seqera/migtool/builder/SqlTemplate.java | 22 +++++++++++++++++ 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java create mode 100644 src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java create mode 100644 src/main/java/io/seqera/migtool/builder/SqlTemplate.java diff --git a/src/main/java/io/seqera/migtool/App.java b/src/main/java/io/seqera/migtool/App.java index 33514a2..9a19864 100644 --- a/src/main/java/io/seqera/migtool/App.java +++ b/src/main/java/io/seqera/migtool/App.java @@ -2,6 +2,7 @@ import java.util.concurrent.Callable; +import io.seqera.migtool.builder.SqlTemplate; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -51,7 +52,8 @@ public Integer call() { .withDialect(dialect!=null ? dialect : dialectFromUrl(url)) .withDriver(driver!=null ? driver : driverFromUrl(url)) .withLocations(location) - .withPattern(pattern); + .withPattern(pattern) + .withBuilder(SqlTemplate.from(dialect!=null ? dialect : dialectFromUrl(url))); try { tool.run(); diff --git a/src/main/java/io/seqera/migtool/Helper.java b/src/main/java/io/seqera/migtool/Helper.java index b6d6daa..16e5f3b 100644 --- a/src/main/java/io/seqera/migtool/Helper.java +++ b/src/main/java/io/seqera/migtool/Helper.java @@ -216,6 +216,8 @@ static public String driverFromUrl(String url) { return "org.h2.Driver"; if( "sqlite".equals(dialect)) return "org.sqlite.JDBC"; + if( "postgresql".equals(dialect)) + return "org.postgresql.Driver"; return null; } } diff --git a/src/main/java/io/seqera/migtool/MigTool.java b/src/main/java/io/seqera/migtool/MigTool.java index 5612f44..27b43bf 100644 --- a/src/main/java/io/seqera/migtool/MigTool.java +++ b/src/main/java/io/seqera/migtool/MigTool.java @@ -21,6 +21,8 @@ import groovy.lang.Closure; import groovy.lang.GroovyShell; import groovy.sql.Sql; +import io.seqera.migtool.builder.DefaultSqlTemplate; +import io.seqera.migtool.builder.SqlTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +44,7 @@ public class MigTool { static final String MIGTOOL_TABLE = "MIGTOOL_HISTORY"; - static final String[] DIALECTS = {"h2", "mysql", "mariadb","sqlite"}; + static final String[] DIALECTS = {"h2", "mysql", "mariadb","sqlite","postgresql"}; String driver; String url; @@ -54,6 +56,7 @@ public class MigTool { Pattern pattern; String schema; String catalog; + SqlTemplate builder = new DefaultSqlTemplate(); private final List migrationEntries; private final List patchEntries; @@ -106,6 +109,11 @@ public MigTool withPattern(String pattern) { return this; } + public MigTool withBuilder(SqlTemplate builder) { + this.builder = builder; + return this; + } + /** * Main application entry point */ @@ -350,7 +358,7 @@ protected void apply() { protected void checkRank(MigRecord entry) { try(Connection conn=getConnection(); Statement stm = conn.createStatement()) { - ResultSet rs = stm.executeQuery("select max(`rank`) from "+MIGTOOL_TABLE); + ResultSet rs = stm.executeQuery(builder.selectMaxRank(MIGTOOL_TABLE)); int last = rs.next() ? rs.getInt(1) : 0; int expected = last+1; if( entry.rank != expected) { @@ -434,7 +442,7 @@ private int migrate(MigRecord entry) throws SQLException { int delta = (int)(System.currentTimeMillis()-now); // save the current migration - final String insertSql = "insert into "+MIGTOOL_TABLE+" (`rank`,`script`,`checksum`,`created_on`,`execution_time`) values (?,?,?,?,?)"; + final String insertSql = builder.insetMigration(MIGTOOL_TABLE); try (Connection conn=getConnection(); PreparedStatement insert = conn.prepareStatement(insertSql)) { insert.setInt(1, entry.rank); insert.setString(2, entry.script); @@ -447,7 +455,7 @@ private int migrate(MigRecord entry) throws SQLException { } protected boolean checkMigrated(MigRecord entry) { - String sql = "select `id`, `checksum`, `script` from " + MIGTOOL_TABLE + " where `rank` = ? and `script` = ?"; + final String sql = builder.selectMigration(MIGTOOL_TABLE); try (Connection conn=getConnection(); PreparedStatement stm = conn.prepareStatement(sql)) { stm.setInt(1, entry.rank); diff --git a/src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java b/src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java new file mode 100644 index 0000000..cd4411a --- /dev/null +++ b/src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java @@ -0,0 +1,22 @@ +package io.seqera.migtool.builder; + +/** + * + * @author Paolo Di Tommaso + */ +public class DefaultSqlTemplate extends SqlTemplate { + @Override + public String selectMaxRank(String table) { + return "select max(`rank`) from " + table; + } + + @Override + public String insetMigration(String table) { + return "insert into "+table+" (`rank`,`script`,`checksum`,`created_on`,`execution_time`) values (?,?,?,?,?)"; + } + + @Override + public String selectMigration(String table) { + return "select `id`, `checksum`, `script` from "+table+ " where `rank` = ? and `script` = ?"; + } +} diff --git a/src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java b/src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java new file mode 100644 index 0000000..9274709 --- /dev/null +++ b/src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java @@ -0,0 +1,24 @@ +package io.seqera.migtool.builder; + +/** + * + * @author Paolo Di Tommaso + */ +class PostgresSqlTemplate extends SqlTemplate { + + @Override + public String selectMaxRank(String table) { + return "select max(rank) from " + table; + } + + @Override + public String insetMigration(String table) { + return "insert into "+table+" (rank,script,checksum,created_on,execution_time) values (?,?,?,?,?)"; + } + + @Override + public String selectMigration(String table) { + return "select id, checksum, script from "+table+ " where rank = ? and script = ?"; + } + +} diff --git a/src/main/java/io/seqera/migtool/builder/SqlTemplate.java b/src/main/java/io/seqera/migtool/builder/SqlTemplate.java new file mode 100644 index 0000000..c7d4fcd --- /dev/null +++ b/src/main/java/io/seqera/migtool/builder/SqlTemplate.java @@ -0,0 +1,22 @@ +package io.seqera.migtool.builder; + +/** + * + * @author Paolo Di Tommaso + */ +public abstract class SqlTemplate { + + abstract public String selectMaxRank(String table); + + abstract public String insetMigration(String table); + + abstract public String selectMigration(String table); + + static public SqlTemplate from(String dialect) { + if( "postgresql".equals(dialect) ) + return new PostgresSqlTemplate(); + else + return new DefaultSqlTemplate(); + } + +} From c79052f696044f6ef834bf7e65c8769307cfb1f1 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 8 Aug 2024 19:23:41 +0200 Subject: [PATCH 2/3] Add tests + rename package Signed-off-by: Paolo Di Tommaso --- src/main/java/io/seqera/migtool/App.java | 7 ++--- src/main/java/io/seqera/migtool/MigTool.java | 26 ++++++++-------- .../DefaultSqlTemplate.java | 5 ++-- .../PostgreSqlTemplate.java} | 5 ++-- .../{builder => template}/SqlTemplate.java | 10 +++++-- .../io/seqera/migtool/MigToolTest.groovy | 29 +++++++++++++++++- .../migtool/template/SqlTemplateTest.groovy | 30 +++++++++++++++++++ 7 files changed, 88 insertions(+), 24 deletions(-) rename src/main/java/io/seqera/migtool/{builder => template}/DefaultSqlTemplate.java (80%) rename src/main/java/io/seqera/migtool/{builder/PostgresSqlTemplate.java => template/PostgreSqlTemplate.java} (81%) rename src/main/java/io/seqera/migtool/{builder => template}/SqlTemplate.java (57%) create mode 100644 src/test/groovy/io/seqera/migtool/template/SqlTemplateTest.groovy diff --git a/src/main/java/io/seqera/migtool/App.java b/src/main/java/io/seqera/migtool/App.java index 9a19864..fc4b858 100644 --- a/src/main/java/io/seqera/migtool/App.java +++ b/src/main/java/io/seqera/migtool/App.java @@ -2,13 +2,11 @@ import java.util.concurrent.Callable; -import io.seqera.migtool.builder.SqlTemplate; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; - -import static io.seqera.migtool.Helper.driverFromUrl; import static io.seqera.migtool.Helper.dialectFromUrl; +import static io.seqera.migtool.Helper.driverFromUrl; /** * Mig tool main launcher @@ -52,8 +50,7 @@ public Integer call() { .withDialect(dialect!=null ? dialect : dialectFromUrl(url)) .withDriver(driver!=null ? driver : driverFromUrl(url)) .withLocations(location) - .withPattern(pattern) - .withBuilder(SqlTemplate.from(dialect!=null ? dialect : dialectFromUrl(url))); + .withPattern(pattern); try { tool.run(); diff --git a/src/main/java/io/seqera/migtool/MigTool.java b/src/main/java/io/seqera/migtool/MigTool.java index 27b43bf..92c1ed7 100644 --- a/src/main/java/io/seqera/migtool/MigTool.java +++ b/src/main/java/io/seqera/migtool/MigTool.java @@ -14,15 +14,21 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.GroovyShell; import groovy.sql.Sql; -import io.seqera.migtool.builder.DefaultSqlTemplate; -import io.seqera.migtool.builder.SqlTemplate; +import io.seqera.migtool.template.SqlTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +62,7 @@ public class MigTool { Pattern pattern; String schema; String catalog; - SqlTemplate builder = new DefaultSqlTemplate(); + SqlTemplate template = SqlTemplate.defaultTemplate(); private final List migrationEntries; private final List patchEntries; @@ -90,6 +96,7 @@ public MigTool withPassword(String password) { public MigTool withDialect(String dialect) { this.dialect = dialect; + this.template = SqlTemplate.from(dialect); return this; } @@ -109,11 +116,6 @@ public MigTool withPattern(String pattern) { return this; } - public MigTool withBuilder(SqlTemplate builder) { - this.builder = builder; - return this; - } - /** * Main application entry point */ @@ -358,7 +360,7 @@ protected void apply() { protected void checkRank(MigRecord entry) { try(Connection conn=getConnection(); Statement stm = conn.createStatement()) { - ResultSet rs = stm.executeQuery(builder.selectMaxRank(MIGTOOL_TABLE)); + ResultSet rs = stm.executeQuery(template.selectMaxRank(MIGTOOL_TABLE)); int last = rs.next() ? rs.getInt(1) : 0; int expected = last+1; if( entry.rank != expected) { @@ -442,7 +444,7 @@ private int migrate(MigRecord entry) throws SQLException { int delta = (int)(System.currentTimeMillis()-now); // save the current migration - final String insertSql = builder.insetMigration(MIGTOOL_TABLE); + final String insertSql = template.insetMigration(MIGTOOL_TABLE); try (Connection conn=getConnection(); PreparedStatement insert = conn.prepareStatement(insertSql)) { insert.setInt(1, entry.rank); insert.setString(2, entry.script); @@ -455,7 +457,7 @@ private int migrate(MigRecord entry) throws SQLException { } protected boolean checkMigrated(MigRecord entry) { - final String sql = builder.selectMigration(MIGTOOL_TABLE); + final String sql = template.selectMigration(MIGTOOL_TABLE); try (Connection conn=getConnection(); PreparedStatement stm = conn.prepareStatement(sql)) { stm.setInt(1, entry.rank); diff --git a/src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java b/src/main/java/io/seqera/migtool/template/DefaultSqlTemplate.java similarity index 80% rename from src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java rename to src/main/java/io/seqera/migtool/template/DefaultSqlTemplate.java index cd4411a..f8646df 100644 --- a/src/main/java/io/seqera/migtool/builder/DefaultSqlTemplate.java +++ b/src/main/java/io/seqera/migtool/template/DefaultSqlTemplate.java @@ -1,10 +1,11 @@ -package io.seqera.migtool.builder; +package io.seqera.migtool.template; /** + * Default SQL template for migtool SQL statements * * @author Paolo Di Tommaso */ -public class DefaultSqlTemplate extends SqlTemplate { +class DefaultSqlTemplate extends SqlTemplate { @Override public String selectMaxRank(String table) { return "select max(`rank`) from " + table; diff --git a/src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java b/src/main/java/io/seqera/migtool/template/PostgreSqlTemplate.java similarity index 81% rename from src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java rename to src/main/java/io/seqera/migtool/template/PostgreSqlTemplate.java index 9274709..cdd45f5 100644 --- a/src/main/java/io/seqera/migtool/builder/PostgresSqlTemplate.java +++ b/src/main/java/io/seqera/migtool/template/PostgreSqlTemplate.java @@ -1,10 +1,11 @@ -package io.seqera.migtool.builder; +package io.seqera.migtool.template; /** + * PostreSQL dialect implementation * * @author Paolo Di Tommaso */ -class PostgresSqlTemplate extends SqlTemplate { +class PostgreSqlTemplate extends SqlTemplate { @Override public String selectMaxRank(String table) { diff --git a/src/main/java/io/seqera/migtool/builder/SqlTemplate.java b/src/main/java/io/seqera/migtool/template/SqlTemplate.java similarity index 57% rename from src/main/java/io/seqera/migtool/builder/SqlTemplate.java rename to src/main/java/io/seqera/migtool/template/SqlTemplate.java index c7d4fcd..b512c72 100644 --- a/src/main/java/io/seqera/migtool/builder/SqlTemplate.java +++ b/src/main/java/io/seqera/migtool/template/SqlTemplate.java @@ -1,6 +1,8 @@ -package io.seqera.migtool.builder; +package io.seqera.migtool.template; /** + * Implements a simple template pattern to provide specialised + * version of required SQL statements depending on the specified SQL "dialect" * * @author Paolo Di Tommaso */ @@ -14,9 +16,13 @@ public abstract class SqlTemplate { static public SqlTemplate from(String dialect) { if( "postgresql".equals(dialect) ) - return new PostgresSqlTemplate(); + return new PostgreSqlTemplate(); else return new DefaultSqlTemplate(); } + public static SqlTemplate defaultTemplate() { + return new DefaultSqlTemplate(); + } + } diff --git a/src/test/groovy/io/seqera/migtool/MigToolTest.groovy b/src/test/groovy/io/seqera/migtool/MigToolTest.groovy index c33ef8c..3b006e1 100644 --- a/src/test/groovy/io/seqera/migtool/MigToolTest.groovy +++ b/src/test/groovy/io/seqera/migtool/MigToolTest.groovy @@ -4,8 +4,9 @@ package io.seqera.migtool import java.nio.file.Files -import io.seqera.migtool.resources.ClassFromJarWithResources +import io.seqera.migtool.template.SqlTemplate +import io.seqera.migtool.resources.ClassFromJarWithResources import spock.lang.Specification class MigToolTest extends Specification { @@ -657,4 +658,30 @@ class MigToolTest extends Specification { thrown(IllegalArgumentException) } + def 'should validate tool parameters' () { + when: + def tool = new MigTool() + .withDriver('org.h2.Driver') + .withDialect('h2') + .withUrl('jdbc:h2:mem:test15;DB_CLOSE_DELAY=-1') + .withUser('sa') + .withPassword('') + .withLocations("/foo/bar") + then: + tool.driver == 'org.h2.Driver' + tool.dialect == 'h2' + tool.url == 'jdbc:h2:mem:test15;DB_CLOSE_DELAY=-1' + tool.user == 'sa' + tool.password == '' + tool.locations == "/foo/bar" + tool.template.class == SqlTemplate.defaultTemplate().class + + when: + tool = new MigTool() + .withDialect('postgresql') + then: + tool.dialect == 'postgresql' + tool.template.class == SqlTemplate.from('postgresql').class + } + } diff --git a/src/test/groovy/io/seqera/migtool/template/SqlTemplateTest.groovy b/src/test/groovy/io/seqera/migtool/template/SqlTemplateTest.groovy new file mode 100644 index 0000000..bbecdc7 --- /dev/null +++ b/src/test/groovy/io/seqera/migtool/template/SqlTemplateTest.groovy @@ -0,0 +1,30 @@ +package io.seqera.migtool.template + +import spock.lang.Specification + +/** + * + * @author Paolo Di Tommaso + */ +class SqlTemplateTest extends Specification{ + + def 'should validate default template' () { + given: + def t = SqlTemplate.from('anything') + + expect: + t.selectMaxRank('FOO') == 'select max(`rank`) from FOO' + t.insetMigration('FOO') == 'insert into FOO (`rank`,`script`,`checksum`,`created_on`,`execution_time`) values (?,?,?,?,?)' + t.selectMigration('FOO') == 'select `id`, `checksum`, `script` from FOO where `rank` = ? and `script` = ?' + } + + def 'should validate postgre template' () { + given: + def t = SqlTemplate.from('postgresql') + + expect: + t.selectMaxRank('FOO') == 'select max(rank) from FOO' + t.insetMigration('FOO') == 'insert into FOO (rank,script,checksum,created_on,execution_time) values (?,?,?,?,?)' + t.selectMigration('FOO') == 'select id, checksum, script from FOO where rank = ? and script = ?' + } +} From 633885aceef8463d8fdd3cc414c7ac33861e5ab1 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 8 Aug 2024 20:15:36 +0200 Subject: [PATCH 3/3] Add PosgreSQL schema + tests Signed-off-by: Paolo Di Tommaso --- build.gradle | 3 + src/main/resources/schema/postgresql.sql | 9 + .../io/seqera/migtool/PostgreSqlTest.groovy | 154 ++++++++++++++++++ .../V01__create_organization_table.sql | 37 +++++ .../postgresql/V02__query_tables.groovy | 6 + 5 files changed, 209 insertions(+) create mode 100644 src/main/resources/schema/postgresql.sql create mode 100644 src/test/groovy/io/seqera/migtool/PostgreSqlTest.groovy create mode 100644 src/test/resources/migrate-db/postgresql/V01__create_organization_table.sql create mode 100644 src/test/resources/migrate-db/postgresql/V02__query_tables.groovy diff --git a/build.gradle b/build.gradle index 6cdca65..c55263c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'mysql:mysql-connector-java:8.0.28' runtimeOnly 'org.xerial:sqlite-jdbc:3.42.0.0' + runtimeOnly("org.postgresql:postgresql:42.7.3") implementation 'info.picocli:picocli:4.6.3' annotationProcessor 'info.picocli:picocli-codegen:4.6.3' @@ -62,6 +63,8 @@ dependencies { testImplementation "org.testcontainers:testcontainers:1.16.3" testImplementation "org.testcontainers:mysql:1.16.3" testImplementation "org.testcontainers:spock:1.16.3" + testImplementation 'org.testcontainers:postgresql:1.16.3' + testImplementation("org.postgresql:postgresql:42.7.3") // Dummy library containing some migration files among their resources for testing purposes testImplementation files("libs/jar-with-resources.jar") diff --git a/src/main/resources/schema/postgresql.sql b/src/main/resources/schema/postgresql.sql new file mode 100644 index 0000000..9d47356 --- /dev/null +++ b/src/main/resources/schema/postgresql.sql @@ -0,0 +1,9 @@ +create table if not exists MIGTOOL_HISTORY +( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + rank INTEGER NOT NULL, + script VARCHAR(250) NOT NULL, + checksum VARCHAR(64) NOT NULL, + created_on timestamp NOT NULL, + execution_time INTEGER +); diff --git a/src/test/groovy/io/seqera/migtool/PostgreSqlTest.groovy b/src/test/groovy/io/seqera/migtool/PostgreSqlTest.groovy new file mode 100644 index 0000000..c783f8d --- /dev/null +++ b/src/test/groovy/io/seqera/migtool/PostgreSqlTest.groovy @@ -0,0 +1,154 @@ +package io.seqera.migtool + +import org.postgresql.util.PSQLException +import org.testcontainers.containers.PostgreSQLContainer +import spock.lang.Specification +/** + * + * @author Paolo Di Tommaso + */ +class PostgreSqlTest extends Specification { + + private static final int PORT = 3306 + + + static PostgreSQLContainer container + + static { + container = new PostgreSQLContainer("postgres:16-alpine") + // start it -- note: it's stopped automatically + // https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control/ + container.start() + } + + def 'should do something' () { + given: + def tool = new MigTool() + .withDriver('org.postgresql.Driver') + .withDialect('postgresql') + .withUrl(container.getJdbcUrl()) + .withUser(container.getUsername()) + .withPassword(container.getPassword()) + .withLocations('file:src/test/resources/migrate-db/postgresql') + + when: + tool.run() + + then: + tool.existTable(tool.getConnection(), 'organization') + tool.existTable(tool.getConnection(), 'license') + !tool.existTable(tool.getConnection(), 'foo') + + } + + def 'should run a successful Groovy script' () { + given: + def tool = new MigTool() + .withDriver('org.postgresql.Driver') + .withDialect('postgresql') + .withUrl(container.getJdbcUrl()) + .withUser(container.getUsername()) + .withPassword(container.getPassword()) + .withLocations('file:src/test/resources/migrate-db/postgresql') + + and: 'set up the initial tables' + tool.run() + + when: 'run a script that inserts some data' + def insertScript = ''' + import java.sql.Timestamp + + def now = new Timestamp(System.currentTimeMillis()) + def newOrgs = [ + ["1", "C", "C", "e@e.com", now, now], + ["2", "C", "C", "e@e.com", now, now], + ] + + newOrgs.each { o -> + sql.executeInsert( + "INSERT INTO organization(id, company, contact, email, date_created, last_updated) VALUES (?, ?, ?, ?, ?, ?)", + o.toArray() + ) + } + ''' + def insertRecord = new MigRecord(rank: 2, script: 'V02__insert-data.groovy', checksum: 'checksum2', statements: [insertScript]) + tool.runGroovyMigration(insertRecord) + + then: 'the script ran successfully' + noExceptionThrown() + + when: 'run another script to check whether the data is present' + def checkScript = ''' + def expectedOrgIds = ["1", "2"] + + def orgs = sql.rows("SELECT * FROM organization") + orgs.each { o -> + assert o.id in expectedOrgIds + } + ''' + def checkRecord = new MigRecord(rank: 3, script: 'V03__check-data.groovy', checksum: 'checksum3', statements: [checkScript]) + tool.runGroovyMigration(checkRecord) + + then: 'the script ran successfully (the new records are present)' + noExceptionThrown() + } + + def 'should run a failing Groovy script' () { + given: + def tool = new MigTool() + .withDriver('org.postgresql.Driver') + .withDialect('postgresql') + .withUrl(container.getJdbcUrl()) + .withUser(container.getUsername()) + .withPassword(container.getPassword()) + .withLocations('file:src/test/resources/migrate-db/postgresql') + + and: 'set up the initial tables' + tool.run() + + when: 'run a script that inserts some data, but fails at some point' + def insertScript = ''' + import java.sql.Timestamp + + def now = new Timestamp(System.currentTimeMillis()) + def newOrgs = [ + ["3", "C", "C", "e@e.com", now, now], + ["4", "C", "C", "e@e.com", now, now], + ["3", "C", "C", "e@e.com", now, now], // Duplicated id: will fail + ] + + newOrgs.each { o -> + sql.executeInsert( + "INSERT INTO organization(id, company, contact, email, date_created, last_updated) VALUES (?, ?, ?, ?, ?, ?)", + o.toArray() + ) + } + ''' + def insertRecord = new MigRecord(rank: 2, script: 'V02__insert-data.groovy', checksum: 'checksum2', statements: [insertScript]) + tool.runGroovyMigration(insertRecord) + + then: 'an exception is thrown' + def e = thrown(IllegalStateException) + e.message.startsWith('GROOVY MIGRATION FAILED') + + and: 'the root cause is present and the stack trace contains the expected offending line number' + e.cause.class == PSQLException + e.cause.stackTrace.any { t -> t.toString() ==~ /.+\.groovy:\d+.+/ } + + when: 'run another script to check whether the data is present' + def checkScript = ''' + def expectedMissingOrgIds = ["3", "4"] + + def orgs = sql.rows("SELECT * FROM organization") + orgs.each { o -> + assert o.id !in expectedMissingOrgIds + } + ''' + def checkRecord = new MigRecord(rank: 3, script: 'V03__check-data.groovy', checksum: 'checksum3', statements: [checkScript]) + tool.runGroovyMigration(checkRecord) + + then: 'the script ran successfully (no records were persisted: the transaction rolled back)' + noExceptionThrown() + } + +} diff --git a/src/test/resources/migrate-db/postgresql/V01__create_organization_table.sql b/src/test/resources/migrate-db/postgresql/V01__create_organization_table.sql new file mode 100644 index 0000000..60d4965 --- /dev/null +++ b/src/test/resources/migrate-db/postgresql/V01__create_organization_table.sql @@ -0,0 +1,37 @@ +CREATE TABLE organization +( + id varchar(25) NOT NULL, + company varchar(125) NOT NULL, + contact varchar(125) NOT NULL, + email varchar(125) NOT NULL, + address varchar(255) DEFAULT NULL, + zip varchar(25) DEFAULT NULL, + country varchar(125) DEFAULT NULL, + deleted boolean NOT NULL DEFAULT false, + date_created timestamp NOT NULL, + last_updated timestamp NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE license +( + id varchar(25) NOT NULL, + features varchar(255) NOT NULL, + product varchar(75) NOT NULL, + contact varchar(125) NOT NULL, + email varchar(125) NOT NULL, + secret bytea NOT NULL, + organization_id varchar(25) NOT NULL, + activation timestamp NOT NULL, + expiration timestamp NOT NULL, + date_created timestamp NOT NULL, + last_updated timestamp NOT NULL, + last_accessed timestamp, + last_access_ip varchar(255) DEFAULT NULL, + access_count int NOT NULL DEFAULT 0, + suspended boolean NOT NULL DEFAULT false, + deleted boolean NOT NULL DEFAULT false, + expired boolean NOT NULL DEFAULT false, + PRIMARY KEY (id), + FOREIGN KEY (organization_id) REFERENCES organization(id) +); diff --git a/src/test/resources/migrate-db/postgresql/V02__query_tables.groovy b/src/test/resources/migrate-db/postgresql/V02__query_tables.groovy new file mode 100644 index 0000000..fa72b26 --- /dev/null +++ b/src/test/resources/migrate-db/postgresql/V02__query_tables.groovy @@ -0,0 +1,6 @@ + +def orgs = sql.rows("SELECT * FROM organization") +def licenses = sql.rows("SELECT * FROM license") + +println "Orgs: ${orgs}" +println "Licenses: ${licenses}" \ No newline at end of file